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.
- {supython-0.1.8 → supython-0.1.10}/CHANGELOG.md +58 -0
- {supython-0.1.8 → supython-0.1.10}/PKG-INFO +11 -11
- {supython-0.1.8 → supython-0.1.10}/README.md +9 -10
- {supython-0.1.8 → supython-0.1.10}/pyproject.toml +2 -1
- {supython-0.1.8 → supython-0.1.10}/src/supython/app.py +2 -1
- {supython-0.1.8 → supython-0.1.10}/src/supython/cli.py +34 -3
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/cron.py +31 -4
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/decorators.py +37 -2
- {supython-0.1.8 → supython-0.1.10}/src/supython/settings.py +31 -0
- {supython-0.1.8 → supython-0.1.10}/.gitignore +0 -0
- {supython-0.1.8 → supython-0.1.10}/LICENSE +0 -0
- {supython-0.1.8 → supython-0.1.10}/SECURITY.md +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/auth.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/auth_templates.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/auth_users.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/db.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/functions.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/jobs.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/ops.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/realtime.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_auth.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_auth_templates.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_auth_users.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_db.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_functions.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_jobs.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_ops.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_realtime.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_storage.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/storage.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/system.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/audit.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/deps.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/errors.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/schemas.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/session.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/spa.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Alert-dluGVkos.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Alert-dluGVkos.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Audit-Njung3HI.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Audit-Njung3HI.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Backups-DzPlFgrm.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Backups-DzPlFgrm.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Buckets-ByacGkU1.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Buckets-ByacGkU1.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Channels-BoIuTtam.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Channels-BoIuTtam.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Crons-B67vc39F.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Crons-B67vc39F.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DataTable-COAAWEft.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DataTable-COAAWEft.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Empty-cr2r7e2u.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Empty-cr2r7e2u.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Grid-hFkp9F4P.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Grid-hFkp9F4P.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Input-DppYTq9C.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Input-DppYTq9C.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/JsonField-DibyJgun.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/JsonField-DibyJgun.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/LoginView-BjLyE3Ds.css +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/LoginView-CoOjECT_.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/LoginView-CoOjECT_.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Logs-D9WYrnIT.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Logs-D9WYrnIT.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Logs-DS1XPa0h.css +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Queue-CywZs6vI.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Queue-CywZs6vI.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Routes-BiLXE49D.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Routes-BiLXE49D.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Routes-C-ianIGD.css +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Select-DIzZyRZb.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Select-DIzZyRZb.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Space-n5-XcguU.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Space-n5-XcguU.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/TableData-CQIagLKn.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/TableData-CQIagLKn.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Tag-D1fOKpTH.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Tag-D1fOKpTH.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Templates-BS-ugkdq.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Templates-BS-ugkdq.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Thing-CEAniuMg.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Thing-CEAniuMg.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Users-wzwajhlh.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Users-wzwajhlh.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/dist-VXIJLCYq.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/dist-VXIJLCYq.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/format-length-CGCY1rMh.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/format-length-CGCY1rMh.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/get-Ca6unauB.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/get-Ca6unauB.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/index-CeE6v959.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/index-CeE6v959.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/pinia-COXwfrOX.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/pinia-COXwfrOX.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/resources-Bt6thQCD.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/resources-Bt6thQCD.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useResource-C_rJCY8C.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useResource-C_rJCY8C.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useTable-CnZc5zhi.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useTable-CnZc5zhi.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useTable-Dg0XlRlq.css +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useToast-DsZKx0IX.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useToast-DsZKx0IX.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/utils-sbXoq7Ir.js +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/utils-sbXoq7Ir.js.map +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/favicon.svg +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/icons.svg +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/index.html +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/_email_job.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/claims.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/deps.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/providers/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/providers/github.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/providers/google.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/providers/oauth.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/providers/registry.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/ratelimit.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/router.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/schemas.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/auth/service.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/backups/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/backups/_backup_job.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/backups/schemas.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/backups/service.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/body_size.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/client/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/client/_auth.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/client/_client.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/client/_config.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/client/_functions.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/client/_storage.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/client/py.typed +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/db.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/db_admin.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/extensions.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/functions/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/functions/context.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/functions/loader.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/functions/router.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/functions/schemas.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/gen/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/gen/_introspect.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/gen/types_py.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/gen/types_ts.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/health.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/hooks.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/backends.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/context.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/cron_inproc.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/registry.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/router.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/schemas.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/service.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/worker.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/jwks.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/keyset.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/logging_config.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/mail.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/mailer.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrate.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0001_extensions_and_roles.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0002_auth_schema.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0003_demo_todos.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0004_auth_v0_2.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0005_storage_schema.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0006_realtime_schema.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0007_jobs_schema.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0008_jobs_last_error.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0009_auth_rate_limits.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0010_worker_heartbeat.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0011_admin_schema.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0012_auth_banned_until.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0013_email_templates.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0014_realtime_payload_warning.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0015_backups_schema.sql +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/passwords.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/broker.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/protocol.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/router.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/schemas.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/service.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/topics.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/websocket.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/init_project.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/Caddyfile.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/README.md.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/apps_hooks.py.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/apps_jobs.py.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/asgi.py.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/docker-compose.prod.yml.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/docker-compose.yml.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/env.example.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/functions_README.md.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/gitignore.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/manage.py.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/migrations/.gitkeep +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/package_init.py.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/settings.py.tmpl +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/secretset.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/security_headers.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/settings_module.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/storage/__init__.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/storage/backends.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/storage/router.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/storage/schemas.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/storage/service.py +0 -0
- {supython-0.1.8 → supython-0.1.10}/src/supython/storage/signing.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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**
|
|
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
|
-
##
|
|
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
|
|
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
|
|
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
|
|
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
|
-
###
|
|
835
|
+
### Future
|
|
836
836
|
|
|
837
|
-
- **
|
|
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.
|
|
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
|
-
|
|
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**
|
|
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
|
-
##
|
|
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
|
|
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
|
|
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
|
|
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
|
-
###
|
|
776
|
+
### Future
|
|
778
777
|
|
|
779
|
-
- **
|
|
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
|
+
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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()
|
|
82
|
+
registry = get_registry()
|
|
83
|
+
registry.register_cron(
|
|
65
84
|
CronDefinition(
|
|
66
85
|
name=name,
|
|
67
86
|
cron_expr=cron_expr,
|
|
68
|
-
job_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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Channels-BoIuTtam.js.map
RENAMED
|
File without changes
|
{supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js
RENAMED
|
File without changes
|
{supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js.map
RENAMED
|
File without changes
|
|
File without changes
|
{supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js.map
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js
RENAMED
|
File without changes
|
{supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js.map
RENAMED
|
File without changes
|
|
File without changes
|
{supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DataTable-COAAWEft.js.map
RENAMED
|
File without changes
|
{supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js
RENAMED
|
File without changes
|