supython 0.1.7__tar.gz → 0.1.9__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.7 → supython-0.1.9}/CHANGELOG.md +34 -0
  2. {supython-0.1.7 → supython-0.1.9}/PKG-INFO +10 -11
  3. {supython-0.1.7 → supython-0.1.9}/README.md +9 -10
  4. {supython-0.1.7 → supython-0.1.9}/pyproject.toml +1 -1
  5. {supython-0.1.7 → supython-0.1.9}/src/supython/cli.py +53 -4
  6. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/cron.py +31 -4
  7. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/decorators.py +37 -2
  8. {supython-0.1.7 → supython-0.1.9}/.gitignore +0 -0
  9. {supython-0.1.7 → supython-0.1.9}/LICENSE +0 -0
  10. {supython-0.1.7 → supython-0.1.9}/SECURITY.md +0 -0
  11. {supython-0.1.7 → supython-0.1.9}/src/supython/__init__.py +0 -0
  12. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/__init__.py +0 -0
  13. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/__init__.py +0 -0
  14. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/auth.py +0 -0
  15. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/auth_templates.py +0 -0
  16. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/auth_users.py +0 -0
  17. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/db.py +0 -0
  18. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/functions.py +0 -0
  19. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/jobs.py +0 -0
  20. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/ops.py +0 -0
  21. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/realtime.py +0 -0
  22. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/service_auth.py +0 -0
  23. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/service_auth_templates.py +0 -0
  24. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/service_auth_users.py +0 -0
  25. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/service_db.py +0 -0
  26. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/service_functions.py +0 -0
  27. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/service_jobs.py +0 -0
  28. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/service_ops.py +0 -0
  29. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/service_realtime.py +0 -0
  30. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/service_storage.py +0 -0
  31. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/storage.py +0 -0
  32. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/api/system.py +0 -0
  33. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/audit.py +0 -0
  34. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/deps.py +0 -0
  35. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/errors.py +0 -0
  36. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/schemas.py +0 -0
  37. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/session.py +0 -0
  38. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/spa.py +0 -0
  39. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Alert-dluGVkos.js +0 -0
  40. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Alert-dluGVkos.js.map +0 -0
  41. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Audit-Njung3HI.js +0 -0
  42. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Audit-Njung3HI.js.map +0 -0
  43. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Backups-DzPlFgrm.js +0 -0
  44. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Backups-DzPlFgrm.js.map +0 -0
  45. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Buckets-ByacGkU1.js +0 -0
  46. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Buckets-ByacGkU1.js.map +0 -0
  47. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Channels-BoIuTtam.js +0 -0
  48. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Channels-BoIuTtam.js.map +0 -0
  49. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +0 -0
  50. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js.map +0 -0
  51. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +0 -0
  52. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js.map +0 -0
  53. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Crons-B67vc39F.js +0 -0
  54. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Crons-B67vc39F.js.map +0 -0
  55. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js +0 -0
  56. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js.map +0 -0
  57. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/DataTable-COAAWEft.js +0 -0
  58. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/DataTable-COAAWEft.js.map +0 -0
  59. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +0 -0
  60. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js.map +0 -0
  61. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js +0 -0
  62. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js.map +0 -0
  63. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Empty-cr2r7e2u.js +0 -0
  64. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Empty-cr2r7e2u.js.map +0 -0
  65. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js +0 -0
  66. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js.map +0 -0
  67. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Grid-hFkp9F4P.js +0 -0
  68. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Grid-hFkp9F4P.js.map +0 -0
  69. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Input-DppYTq9C.js +0 -0
  70. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Input-DppYTq9C.js.map +0 -0
  71. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js +0 -0
  72. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js.map +0 -0
  73. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/JsonField-DibyJgun.js +0 -0
  74. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/JsonField-DibyJgun.js.map +0 -0
  75. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/LoginView-BjLyE3Ds.css +0 -0
  76. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/LoginView-CoOjECT_.js +0 -0
  77. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/LoginView-CoOjECT_.js.map +0 -0
  78. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Logs-D9WYrnIT.js +0 -0
  79. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Logs-D9WYrnIT.js.map +0 -0
  80. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Logs-DS1XPa0h.css +0 -0
  81. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js +0 -0
  82. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js.map +0 -0
  83. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +0 -0
  84. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js.map +0 -0
  85. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Queue-CywZs6vI.js +0 -0
  86. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Queue-CywZs6vI.js.map +0 -0
  87. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +0 -0
  88. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js.map +0 -0
  89. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js +0 -0
  90. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js.map +0 -0
  91. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Routes-BiLXE49D.js +0 -0
  92. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Routes-BiLXE49D.js.map +0 -0
  93. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Routes-C-ianIGD.css +0 -0
  94. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +0 -0
  95. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +0 -0
  96. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js.map +0 -0
  97. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Select-DIzZyRZb.js +0 -0
  98. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Select-DIzZyRZb.js.map +0 -0
  99. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Space-n5-XcguU.js +0 -0
  100. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Space-n5-XcguU.js.map +0 -0
  101. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js +0 -0
  102. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js.map +0 -0
  103. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +0 -0
  104. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js.map +0 -0
  105. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/TableData-CQIagLKn.js +0 -0
  106. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/TableData-CQIagLKn.js.map +0 -0
  107. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Tag-D1fOKpTH.js +0 -0
  108. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Tag-D1fOKpTH.js.map +0 -0
  109. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Templates-BS-ugkdq.js +0 -0
  110. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Templates-BS-ugkdq.js.map +0 -0
  111. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Thing-CEAniuMg.js +0 -0
  112. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Thing-CEAniuMg.js.map +0 -0
  113. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Users-wzwajhlh.js +0 -0
  114. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/Users-wzwajhlh.js.map +0 -0
  115. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +0 -0
  116. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/dist-VXIJLCYq.js +0 -0
  117. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/dist-VXIJLCYq.js.map +0 -0
  118. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/format-length-CGCY1rMh.js +0 -0
  119. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/format-length-CGCY1rMh.js.map +0 -0
  120. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/get-Ca6unauB.js +0 -0
  121. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/get-Ca6unauB.js.map +0 -0
  122. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/index-CeE6v959.js +0 -0
  123. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/index-CeE6v959.js.map +0 -0
  124. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/pinia-COXwfrOX.js +0 -0
  125. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/pinia-COXwfrOX.js.map +0 -0
  126. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/resources-Bt6thQCD.js +0 -0
  127. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/resources-Bt6thQCD.js.map +0 -0
  128. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js +0 -0
  129. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js.map +0 -0
  130. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js +0 -0
  131. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js.map +0 -0
  132. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js +0 -0
  133. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js.map +0 -0
  134. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/useResource-C_rJCY8C.js +0 -0
  135. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/useResource-C_rJCY8C.js.map +0 -0
  136. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/useTable-CnZc5zhi.js +0 -0
  137. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/useTable-CnZc5zhi.js.map +0 -0
  138. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/useTable-Dg0XlRlq.css +0 -0
  139. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/useToast-DsZKx0IX.js +0 -0
  140. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/useToast-DsZKx0IX.js.map +0 -0
  141. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/utils-sbXoq7Ir.js +0 -0
  142. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/assets/utils-sbXoq7Ir.js.map +0 -0
  143. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/favicon.svg +0 -0
  144. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/icons.svg +0 -0
  145. {supython-0.1.7 → supython-0.1.9}/src/supython/admin/static/index.html +0 -0
  146. {supython-0.1.7 → supython-0.1.9}/src/supython/app.py +0 -0
  147. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/__init__.py +0 -0
  148. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/_email_job.py +0 -0
  149. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/claims.py +0 -0
  150. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/deps.py +0 -0
  151. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/providers/__init__.py +0 -0
  152. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/providers/github.py +0 -0
  153. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/providers/google.py +0 -0
  154. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/providers/oauth.py +0 -0
  155. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/providers/registry.py +0 -0
  156. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/ratelimit.py +0 -0
  157. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/router.py +0 -0
  158. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/schemas.py +0 -0
  159. {supython-0.1.7 → supython-0.1.9}/src/supython/auth/service.py +0 -0
  160. {supython-0.1.7 → supython-0.1.9}/src/supython/backups/__init__.py +0 -0
  161. {supython-0.1.7 → supython-0.1.9}/src/supython/backups/_backup_job.py +0 -0
  162. {supython-0.1.7 → supython-0.1.9}/src/supython/backups/schemas.py +0 -0
  163. {supython-0.1.7 → supython-0.1.9}/src/supython/backups/service.py +0 -0
  164. {supython-0.1.7 → supython-0.1.9}/src/supython/body_size.py +0 -0
  165. {supython-0.1.7 → supython-0.1.9}/src/supython/client/__init__.py +0 -0
  166. {supython-0.1.7 → supython-0.1.9}/src/supython/client/_auth.py +0 -0
  167. {supython-0.1.7 → supython-0.1.9}/src/supython/client/_client.py +0 -0
  168. {supython-0.1.7 → supython-0.1.9}/src/supython/client/_config.py +0 -0
  169. {supython-0.1.7 → supython-0.1.9}/src/supython/client/_functions.py +0 -0
  170. {supython-0.1.7 → supython-0.1.9}/src/supython/client/_storage.py +0 -0
  171. {supython-0.1.7 → supython-0.1.9}/src/supython/client/py.typed +0 -0
  172. {supython-0.1.7 → supython-0.1.9}/src/supython/db.py +0 -0
  173. {supython-0.1.7 → supython-0.1.9}/src/supython/db_admin.py +0 -0
  174. {supython-0.1.7 → supython-0.1.9}/src/supython/extensions.py +0 -0
  175. {supython-0.1.7 → supython-0.1.9}/src/supython/functions/__init__.py +0 -0
  176. {supython-0.1.7 → supython-0.1.9}/src/supython/functions/context.py +0 -0
  177. {supython-0.1.7 → supython-0.1.9}/src/supython/functions/loader.py +0 -0
  178. {supython-0.1.7 → supython-0.1.9}/src/supython/functions/router.py +0 -0
  179. {supython-0.1.7 → supython-0.1.9}/src/supython/functions/schemas.py +0 -0
  180. {supython-0.1.7 → supython-0.1.9}/src/supython/gen/__init__.py +0 -0
  181. {supython-0.1.7 → supython-0.1.9}/src/supython/gen/_introspect.py +0 -0
  182. {supython-0.1.7 → supython-0.1.9}/src/supython/gen/types_py.py +0 -0
  183. {supython-0.1.7 → supython-0.1.9}/src/supython/gen/types_ts.py +0 -0
  184. {supython-0.1.7 → supython-0.1.9}/src/supython/health.py +0 -0
  185. {supython-0.1.7 → supython-0.1.9}/src/supython/hooks.py +0 -0
  186. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/__init__.py +0 -0
  187. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/backends.py +0 -0
  188. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/context.py +0 -0
  189. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/cron_inproc.py +0 -0
  190. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/registry.py +0 -0
  191. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/router.py +0 -0
  192. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/schemas.py +0 -0
  193. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/service.py +0 -0
  194. {supython-0.1.7 → supython-0.1.9}/src/supython/jobs/worker.py +0 -0
  195. {supython-0.1.7 → supython-0.1.9}/src/supython/jwks.py +0 -0
  196. {supython-0.1.7 → supython-0.1.9}/src/supython/keyset.py +0 -0
  197. {supython-0.1.7 → supython-0.1.9}/src/supython/logging_config.py +0 -0
  198. {supython-0.1.7 → supython-0.1.9}/src/supython/mail.py +0 -0
  199. {supython-0.1.7 → supython-0.1.9}/src/supython/mailer.py +0 -0
  200. {supython-0.1.7 → supython-0.1.9}/src/supython/migrate.py +0 -0
  201. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0001_extensions_and_roles.sql +0 -0
  202. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0002_auth_schema.sql +0 -0
  203. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0003_demo_todos.sql +0 -0
  204. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0004_auth_v0_2.sql +0 -0
  205. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0005_storage_schema.sql +0 -0
  206. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0006_realtime_schema.sql +0 -0
  207. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0007_jobs_schema.sql +0 -0
  208. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0008_jobs_last_error.sql +0 -0
  209. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0009_auth_rate_limits.sql +0 -0
  210. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0010_worker_heartbeat.sql +0 -0
  211. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0011_admin_schema.sql +0 -0
  212. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0012_auth_banned_until.sql +0 -0
  213. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0013_email_templates.sql +0 -0
  214. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0014_realtime_payload_warning.sql +0 -0
  215. {supython-0.1.7 → supython-0.1.9}/src/supython/migrations/0015_backups_schema.sql +0 -0
  216. {supython-0.1.7 → supython-0.1.9}/src/supython/passwords.py +0 -0
  217. {supython-0.1.7 → supython-0.1.9}/src/supython/realtime/__init__.py +0 -0
  218. {supython-0.1.7 → supython-0.1.9}/src/supython/realtime/broker.py +0 -0
  219. {supython-0.1.7 → supython-0.1.9}/src/supython/realtime/protocol.py +0 -0
  220. {supython-0.1.7 → supython-0.1.9}/src/supython/realtime/router.py +0 -0
  221. {supython-0.1.7 → supython-0.1.9}/src/supython/realtime/schemas.py +0 -0
  222. {supython-0.1.7 → supython-0.1.9}/src/supython/realtime/service.py +0 -0
  223. {supython-0.1.7 → supython-0.1.9}/src/supython/realtime/topics.py +0 -0
  224. {supython-0.1.7 → supython-0.1.9}/src/supython/realtime/websocket.py +0 -0
  225. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/__init__.py +0 -0
  226. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/init_project.py +0 -0
  227. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/Caddyfile.tmpl +0 -0
  228. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/README.md.tmpl +0 -0
  229. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/apps_hooks.py.tmpl +0 -0
  230. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/apps_jobs.py.tmpl +0 -0
  231. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/asgi.py.tmpl +0 -0
  232. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/docker-compose.prod.yml.tmpl +0 -0
  233. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/docker-compose.yml.tmpl +0 -0
  234. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +0 -0
  235. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +0 -0
  236. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/env.example.tmpl +0 -0
  237. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/functions_README.md.tmpl +0 -0
  238. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/gitignore.tmpl +0 -0
  239. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/manage.py.tmpl +0 -0
  240. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/migrations/.gitkeep +0 -0
  241. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/package_init.py.tmpl +0 -0
  242. {supython-0.1.7 → supython-0.1.9}/src/supython/scaffold/templates/settings.py.tmpl +0 -0
  243. {supython-0.1.7 → supython-0.1.9}/src/supython/secretset.py +0 -0
  244. {supython-0.1.7 → supython-0.1.9}/src/supython/security_headers.py +0 -0
  245. {supython-0.1.7 → supython-0.1.9}/src/supython/settings.py +0 -0
  246. {supython-0.1.7 → supython-0.1.9}/src/supython/settings_module.py +0 -0
  247. {supython-0.1.7 → supython-0.1.9}/src/supython/storage/__init__.py +0 -0
  248. {supython-0.1.7 → supython-0.1.9}/src/supython/storage/backends.py +0 -0
  249. {supython-0.1.7 → supython-0.1.9}/src/supython/storage/router.py +0 -0
  250. {supython-0.1.7 → supython-0.1.9}/src/supython/storage/schemas.py +0 -0
  251. {supython-0.1.7 → supython-0.1.9}/src/supython/storage/service.py +0 -0
  252. {supython-0.1.7 → supython-0.1.9}/src/supython/storage/signing.py +0 -0
  253. {supython-0.1.7 → supython-0.1.9}/src/supython/tokens.py +0 -0
@@ -31,6 +31,39 @@ Each entry links the relevant `PROJECT.md` section and decision-log row
31
31
 
32
32
  ---
33
33
 
34
+ ## [0.1.9] — 2026-05-29
35
+
36
+ ### Fixed
37
+
38
+ - `@cron(...)` now also registers the decorated function as the job
39
+ handler for `job_name`. Previously the decorator only wrote a
40
+ `CronDefinition`, so when pg_cron fired `jobs.enqueue(p_name :=
41
+ <job_name>)` the worker rejected the job with `unknown job:
42
+ <job_name>` on every tick. Pass `register_handler=False` to keep
43
+ the pre-fix behaviour (schedule fires a handler declared elsewhere
44
+ with `@job`).
45
+ - `supython cron list` and `supython cron sync` now load the user's
46
+ settings module and `EXTENSIONS` before reading the registry,
47
+ mirroring `worker run` and the FastAPI startup path. Without the
48
+ bootstrap, `cron list` always printed `no crons registered` and a
49
+ subsequent `cron sync` would silently wipe every row from
50
+ `jobs.cron_schedules` and unschedule every matching pg_cron entry.
51
+
52
+ ### Added
53
+
54
+ - `@cron(...)` accepts `register_handler` (default `True`) plus the
55
+ job kwargs forwarded to the auto-registered `JobDefinition`:
56
+ `max_attempts`, `backoff`, `backoff_base_s`, `backoff_max_s`,
57
+ `role`, `claims_from`, `accepts_payload`.
58
+ - `supython cron sync --allow-empty` (and the matching
59
+ `sync_pg_cron(conn, allow_empty=True)` argument). Without the flag,
60
+ `sync_pg_cron` now refuses to act when the in-process registry is
61
+ empty but `jobs.cron_schedules` is non-empty — guards against a
62
+ forgotten extension bootstrap silently wiping production schedules.
63
+ The empty-registry-and-empty-table cold-start case stays a no-op.
64
+
65
+ ---
66
+
34
67
  ## [0.1.0] — 2026-05-09
35
68
 
36
69
  The first public release. Everything currently on `main` collapses
@@ -201,4 +234,5 @@ v0.1–v0.7 plus a v1.1.x admin track; see §19 decision log
201
234
  ---
202
235
 
203
236
 
237
+ [0.1.9]: https://github.com/Tkeby/supython/releases/tag/v0.1.9
204
238
  [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.7
3
+ Version: 0.1.9
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
@@ -58,7 +58,7 @@ Description-Content-Type: text/markdown
58
58
 
59
59
  # supython
60
60
 
61
- > A lightweight, Postgres-first BaaS framework for Python. **v0.1.4 release**
61
+ > A lightweight, Postgres-first BaaS framework for Python.
62
62
 
63
63
  **the database owns the schema, Python owns the things SQL is bad at**.
64
64
  It leans on [PostgREST](https://postgrest.org)
@@ -69,7 +69,7 @@ storage, functions, workers, and an optional admin control plane.
69
69
  supython is for a specific person with a specific problem:
70
70
  > 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
71
 
72
- Shipped [v0.1.2]:
72
+
73
73
 
74
74
  **Core platform**
75
75
  - **Email/password auth** — signup, login, refresh-token rotation with **reuse detection**
@@ -106,7 +106,7 @@ Shipped [v0.1.2]:
106
106
  - **Secret rotation** — JWT keys, symmetric secrets, Postgres passwords; all with zero-downtime runbooks
107
107
  - **Multi-arch Docker image** — `linux/amd64` + `linux/arm64`, non-root user, `tini` PID 1, ~64 MB
108
108
 
109
- **Admin control plane** (shipped in v0.1.2)
109
+ **Admin control plane**
110
110
  - **Vue 3 + Vite SPA** at `/admin` — no runtime Node deps; pre-built static bundle in the wheel
111
111
  - **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
112
  - **Auth surface** — user search, ban/unban/force-logout, refresh-token inspector, audit log, email template editing
@@ -393,7 +393,7 @@ supython dev
393
393
  | `JOBS_DRAIN_TIMEOUT_S` | `30.0` | Graceful shutdown drain (seconds) |
394
394
  | `JOBS_DEV_INPROCESS` | `false` | Spawn worker in-process during `supython dev` |
395
395
 
396
- ## v0.2 auth endpoints
396
+ ## Auth endpoints
397
397
 
398
398
  | Endpoint | Method | Purpose |
399
399
  |---|---|---|
@@ -436,7 +436,7 @@ denylist without breaking that contract. The mitigation is a **short
436
436
  long-lived (`REFRESH_TOKEN_TTL=30d`) so users don't get prompted to log
437
437
  in every few minutes — clients refresh transparently.
438
438
 
439
- ### Custom JWT claims [shipped v0.1.3]
439
+ ### Custom JWT claims
440
440
 
441
441
  Register a claims provider to inject application-specific claims into every
442
442
  access token minted by the auth endpoints. Each provider is an async callable
@@ -472,7 +472,7 @@ Notes:
472
472
  - Refresh re-collects claims, so a token rotated via `/auth/v1/refresh`
473
473
  reflects current state.
474
474
 
475
- ### Reading the caller in your routes [shipped v0.1.4]
475
+ ### Reading the caller in your routes
476
476
 
477
477
  Application code mounts its own routers alongside supython's. To gate an
478
478
  endpoint on a valid bearer token — or to read custom claims out of it —
@@ -818,7 +818,7 @@ unit tests always run in isolation.
818
818
  **CI:** runners with Docker run `supython test up && supython test run`;
819
819
  runners without Docker run `pytest tests/unit` for a meaningful subset.
820
820
 
821
- ## Roadmap [shipped v0.1.2]
821
+ ## Roadmap
822
822
 
823
823
  - ✅ Email/password auth, PostgREST contract, RLS demo
824
824
  - ✅ OAuth, password reset, magic link, OTP, reuse detection, email backend, test suite
@@ -829,12 +829,11 @@ runners without Docker run `pytest tests/unit` for a meaningful subset.
829
829
  - ✅ Production observable: structured JSON logs, `/livez`/`/readyz`/`/health`, security headers, input size guards, audit log completeness, OAuth PKCE, secret rotation runbooks
830
830
  - ✅ (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
831
  - *(deferred)* — Realtime v2 over logical replication
832
- - v0.1.2 Release — final sweep, tag, publish wheel, production deployment with no patches
833
832
  - ✅ **TypeScript SDK** — `@supython/sdk` wrapping `@supabase/postgrest-js` + `@supabase/realtime-js`
834
833
 
835
- ### Post v0.1.2
834
+ ### Future
836
835
 
837
- - **v1.1+** — Admin control plane polish (backend + frontend shipped in v0.1.2; tests + remaining DoD items deferred)
836
+ - **Admin control plane** polish (tests + remaining DoD items)
838
837
  - **Realtime v2** — logical replication (demand-driven; swap when trigger overhead or >8KB payload data warrants it)
839
838
  - **Prometheus `/metrics`** + **OpenTelemetry** — optional extras
840
839
 
@@ -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.7"
7
+ version = "0.1.9"
8
8
  description = "A lightweight Postgres-first BaaS framework for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -115,6 +115,22 @@ 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
+ s = get_settings()
130
+ user = load_user_settings(s.settings_module) if s.settings_module else UserSettings()
131
+ load_extensions([*s.extensions, *user.extensions])
132
+
133
+
118
134
  @gen_cli.command("types")
119
135
  def gen_types(
120
136
  lang: str = typer.Option("py", "--lang", help="Target language (`py` or `ts`)."),
@@ -418,6 +434,7 @@ def cron_list() -> None:
418
434
  """List registered cron schedules."""
419
435
  from .jobs.registry import get_registry
420
436
 
437
+ _bootstrap_user_modules()
421
438
  crons = list(get_registry().iter_crons())
422
439
  if not crons:
423
440
  typer.echo("no crons registered.")
@@ -427,17 +444,29 @@ def cron_list() -> None:
427
444
 
428
445
 
429
446
  @cron_cli.command("sync")
430
- def cron_sync() -> None:
447
+ def cron_sync(
448
+ allow_empty: bool = typer.Option(
449
+ False,
450
+ "--allow-empty",
451
+ help=(
452
+ "Allow sync to proceed when the in-process registry has no @cron "
453
+ "definitions but `jobs.cron_schedules` has rows. Without this "
454
+ "flag the sync refuses, because it would otherwise wipe every "
455
+ "row and unschedule every pg_cron entry."
456
+ ),
457
+ ),
458
+ ) -> None:
431
459
  """Sync registered crons with pg_cron."""
432
460
  from .jobs.cron import sync_pg_cron
433
461
 
462
+ _bootstrap_user_modules()
434
463
  s = get_settings()
435
464
 
436
465
  async def _run():
437
466
  conn = await asyncpg.connect(s.database_url)
438
467
  try:
439
468
  await conn.execute("set role service_role")
440
- await sync_pg_cron(conn)
469
+ await sync_pg_cron(conn, allow_empty=allow_empty)
441
470
  finally:
442
471
  await conn.close()
443
472
 
@@ -1355,14 +1384,22 @@ def up(
1355
1384
  timeout: int = typer.Option(30, help="Seconds to wait for Postgres."),
1356
1385
  prod: bool = typer.Option(False, "--prod", help="Use docker-compose.prod.yml."),
1357
1386
  worker: bool = typer.Option(False, "--worker", help="Also start the worker (prod only)."),
1387
+ profile: list[str] = typer.Option(
1388
+ [],
1389
+ "--profile",
1390
+ help="Compose profile(s) to enable. Repeatable; user-defined extension services gated by a profile come up via a final sweep.",
1391
+ ),
1358
1392
  ) -> None:
1359
1393
  """Start Postgres + PostgREST and apply migrations.
1360
1394
 
1361
1395
  Brings the DB up first, runs migrations (which create the roles
1362
1396
  PostgREST needs), rotates the authenticator password, then starts PostgREST.
1397
+ Any user-added services gated behind a compose profile come up after the
1398
+ core services when `--profile <name>` is passed.
1363
1399
  """
1364
1400
  compose_file = _resolve_compose_file(prod)
1365
1401
  is_prod = compose_file == "docker-compose.prod.yml"
1402
+ user_profiles = tuple(profile)
1366
1403
  configure_logging("INFO", json_format=False)
1367
1404
  _compose_with(compose_file, "up", "-d", "db")
1368
1405
  typer.echo("waiting for Postgres ...")
@@ -1379,6 +1416,8 @@ def up(
1379
1416
  _compose_with(compose_file, "up", "-d", "postgrest", "supython")
1380
1417
  if worker:
1381
1418
  _compose_with(compose_file, "--profile", "worker", "up", "-d", "worker")
1419
+ if user_profiles:
1420
+ _compose_with(compose_file, "up", "-d", profiles=user_profiles)
1382
1421
  typer.echo("")
1383
1422
  typer.echo("ready.")
1384
1423
  typer.echo(" postgres localhost:54322 (bind 127.0.0.1 only)")
@@ -1386,12 +1425,18 @@ def up(
1386
1425
  typer.echo(" supython http://localhost:8000")
1387
1426
  if worker:
1388
1427
  typer.echo(" worker running (profile=worker)")
1428
+ for p in user_profiles:
1429
+ typer.echo(f" profile {p} # user-defined services")
1389
1430
  else:
1390
1431
  _compose_with(compose_file, "up", "-d", "postgrest")
1432
+ if user_profiles:
1433
+ _compose_with(compose_file, "up", "-d", profiles=user_profiles)
1391
1434
  typer.echo("")
1392
1435
  typer.echo("ready.")
1393
1436
  typer.echo(" postgres localhost:54322 (user/db: supython)")
1394
1437
  typer.echo(f" postgrest {s.postgrest_url}")
1438
+ for p in user_profiles:
1439
+ typer.echo(f" profile {p} # user-defined services")
1395
1440
  typer.echo(" next supython dev # start the auth/API service")
1396
1441
 
1397
1442
 
@@ -1610,11 +1655,15 @@ def _compose(*args: str) -> None:
1610
1655
  _compose_with(None, *args)
1611
1656
 
1612
1657
 
1613
- def _compose_with(file: str | None, *args: str) -> None:
1614
- """Run `docker compose [-f <file>] <args...>`, surfacing common errors."""
1658
+ def _compose_with(
1659
+ file: str | None, *args: str, profiles: tuple[str, ...] = ()
1660
+ ) -> None:
1661
+ """Run `docker compose [-f <file>] [--profile <p>]... <args...>`, surfacing common errors."""
1615
1662
  cmd = ["docker", "compose"]
1616
1663
  if file:
1617
1664
  cmd += ["-f", file]
1665
+ for p in profiles:
1666
+ cmd += ["--profile", p]
1618
1667
  cmd += list(args)
1619
1668
  try:
1620
1669
  subprocess.run(cmd, check=True)
@@ -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
File without changes
File without changes
File without changes