tina4-python 3.13.81__tar.gz → 3.13.82__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.
- {tina4_python-3.13.81 → tina4_python-3.13.82}/.gitignore +13 -9
- {tina4_python-3.13.81 → tina4_python-3.13.82}/PKG-INFO +5 -5
- {tina4_python-3.13.81 → tina4_python-3.13.82}/README.md +4 -4
- {tina4_python-3.13.81 → tina4_python-3.13.82}/pyproject.toml +1 -1
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/CLAUDE.md +2 -2
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/__init__.py +9 -1
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/cli/__init__.py +131 -10
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/core/server.py +110 -11
- tina4_python-3.13.82/tina4_python/mqtt/__init__.py +742 -0
- tina4_python-3.13.82/tina4_python/mqtt/message.py +93 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/css/tina4.css +56 -130
- tina4_python-3.13.82/tina4_python/public/css/tina4.min.css +1 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/__init__.py +47 -2
- tina4_python-3.13.82/tina4_python/scss/tina4css/_pagination.scss +63 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_utilities.scss +11 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/tina4.scss +1 -0
- tina4_python-3.13.82/tina4_python/test_client/__init__.py +209 -0
- tina4_python-3.13.81/tina4_python/public/css/tina4.min.css +0 -1
- tina4_python-3.13.81/tina4_python/test_client/__init__.py +0 -188
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/context/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/context/chunker.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/env.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/realtime/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/realtime/models/Attachment.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/realtime/models/Channel.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/realtime/models/ChannelMember.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/realtime/models/Message.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/realtime/models/Workspace.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/realtime/models/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/realtime/storage.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.81 → tina4_python-3.13.82}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -74,15 +74,19 @@ build/
|
|
|
74
74
|
example/.env
|
|
75
75
|
example/data/*.db
|
|
76
76
|
example/data/*.db-*
|
|
77
|
-
example/store/
|
|
78
|
-
example/store
|
|
79
|
-
example
|
|
80
|
-
example/
|
|
81
|
-
example/
|
|
82
|
-
example/
|
|
83
|
-
example/
|
|
84
|
-
example/
|
|
85
|
-
example/
|
|
77
|
+
# The example app runs from example/, not example/store/ (which no longer
|
|
78
|
+
# exists), so the old example/store/... rules below matched nothing and every
|
|
79
|
+
# runtime artifact the example writes leaked into git status - 466 of them, plus
|
|
80
|
+
# 6 that got committed. Anchored /logs/ missed example/logs/ for the same reason.
|
|
81
|
+
example/data/broken/
|
|
82
|
+
example/data/.broken/
|
|
83
|
+
example/data/mailbox/
|
|
84
|
+
example/data/queue/
|
|
85
|
+
example/data/sessions/
|
|
86
|
+
example/logs/
|
|
87
|
+
example/.tina4/
|
|
88
|
+
example/__pycache__/
|
|
89
|
+
example/src/**/__pycache__/
|
|
86
90
|
.claude/settings.local.json
|
|
87
91
|
.claude/worktrees/
|
|
88
92
|
/nonexistent_path/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tina4-python
|
|
3
|
-
Version: 3.13.
|
|
3
|
+
Version: 3.13.82
|
|
4
4
|
Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
|
|
5
5
|
Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -37,13 +37,13 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
<h1 align="center">Tina4 Python</h1>
|
|
38
38
|
|
|
39
39
|
<p align="center">
|
|
40
|
-
|
|
40
|
+
98 built-in features. Zero dependencies. One import, everything works.
|
|
41
41
|
</p>
|
|
42
42
|
|
|
43
43
|
<p align="center">
|
|
44
44
|
<a href="https://pypi.org/project/tina4-python/"><img src="https://img.shields.io/pypi/v/tina4-python?color=7b1fa2&label=PyPI" alt="PyPI"></a>
|
|
45
45
|
<img src="https://img.shields.io/badge/tests-2%2C281%20passing-brightgreen" alt="Tests">
|
|
46
|
-
<img src="https://img.shields.io/badge/features-
|
|
46
|
+
<img src="https://img.shields.io/badge/features-98-blue" alt="Features">
|
|
47
47
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
|
|
48
48
|
<a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
|
|
49
49
|
</p>
|
|
@@ -726,7 +726,7 @@ Benchmarked with `wrk`: 5,000 requests, 50 concurrent, median of 3 runs:
|
|
|
726
726
|
| Bottle | 4,355 | 0 | ~5 |
|
|
727
727
|
| Django | 4,050 | 20+ | ~22 |
|
|
728
728
|
|
|
729
|
-
Tina4 Python delivers competitive throughput with **zero dependencies and
|
|
729
|
+
Tina4 Python delivers competitive throughput with **zero dependencies and 98 features**. Frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
|
|
730
730
|
|
|
731
731
|
**Across all 4 Tina4 implementations:**
|
|
732
732
|
|
|
@@ -742,7 +742,7 @@ Run benchmarks locally: `python benchmarks/benchmark.py --python`
|
|
|
742
742
|
|
|
743
743
|
## Cross-Framework Parity
|
|
744
744
|
|
|
745
|
-
Tina4 ships identical features across four languages: same architecture, same conventions, same
|
|
745
|
+
Tina4 ships identical features across four languages: same architecture, same conventions, same 97 features:
|
|
746
746
|
|
|
747
747
|
| | Python | PHP | Ruby | Node.js |
|
|
748
748
|
|---|--------|-----|------|---------|
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
<h1 align="center">Tina4 Python</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
|
|
8
|
+
98 built-in features. Zero dependencies. One import, everything works.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://pypi.org/project/tina4-python/"><img src="https://img.shields.io/pypi/v/tina4-python?color=7b1fa2&label=PyPI" alt="PyPI"></a>
|
|
13
13
|
<img src="https://img.shields.io/badge/tests-2%2C281%20passing-brightgreen" alt="Tests">
|
|
14
|
-
<img src="https://img.shields.io/badge/features-
|
|
14
|
+
<img src="https://img.shields.io/badge/features-98-blue" alt="Features">
|
|
15
15
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
|
|
16
16
|
<a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
|
|
17
17
|
</p>
|
|
@@ -694,7 +694,7 @@ Benchmarked with `wrk`: 5,000 requests, 50 concurrent, median of 3 runs:
|
|
|
694
694
|
| Bottle | 4,355 | 0 | ~5 |
|
|
695
695
|
| Django | 4,050 | 20+ | ~22 |
|
|
696
696
|
|
|
697
|
-
Tina4 Python delivers competitive throughput with **zero dependencies and
|
|
697
|
+
Tina4 Python delivers competitive throughput with **zero dependencies and 98 features**. Frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
|
|
698
698
|
|
|
699
699
|
**Across all 4 Tina4 implementations:**
|
|
700
700
|
|
|
@@ -710,7 +710,7 @@ Run benchmarks locally: `python benchmarks/benchmark.py --python`
|
|
|
710
710
|
|
|
711
711
|
## Cross-Framework Parity
|
|
712
712
|
|
|
713
|
-
Tina4 ships identical features across four languages: same architecture, same conventions, same
|
|
713
|
+
Tina4 ships identical features across four languages: same architecture, same conventions, same 97 features:
|
|
714
714
|
|
|
715
715
|
| | Python | PHP | Ruby | Node.js |
|
|
716
716
|
|---|--------|-----|------|---------|
|
|
@@ -291,7 +291,7 @@ Queue(topic="tasks").push({"action": "send_email"})
|
|
|
291
291
|
6. **Connection strings**: v3 uses standard URL format: `driver://host:port/database` with separate `username` and `password` parameters. Example: `Database("firebird://localhost:3050//path/to/db", "SYSDBA", "masterkey")`. Environment variable: `TINA4_DATABASE_URL`.
|
|
292
292
|
7. **Running the app**: use the `tina4` CLI — `tina4 serve`. The framework **refuses to boot via a plain `python app.py`** (it must be launched by the CLI; `TINA4_OVERRIDE_CLIENT=true` only for embedding). Port/name are CLI args.
|
|
293
293
|
8. **SCSS**: Files in `src/scss/` are auto-compiled to `src/public/css/` on startup
|
|
294
|
-
12. **Background tasks**: Use `background(fn, interval)` from `tina4_python.core.server` — never use `threading.Thread` for periodic work. The `background()` function runs tasks cooperatively in the asyncio event loop with proper shutdown handling.
|
|
294
|
+
12. **Background tasks**: Use `background(fn, interval)` from `tina4_python.core.server` — never use `threading.Thread` for periodic work. The `background()` function runs tasks cooperatively in the asyncio event loop with proper shutdown handling. It returns a `BackgroundTask` handle: call `handle.stop()` to end ONE task and deregister it (idempotent; returns `False` if already stopped). `background_task_count()` reports how many are registered and `stop_all_background_tasks()` clears them all — a stopped task is never left in the registry.
|
|
295
295
|
|
|
296
296
|
|
|
297
297
|
---
|
|
@@ -1912,7 +1912,7 @@ async def dashboard(request, response):
|
|
|
1912
1912
|
|
|
1913
1913
|
## v3 Features Summary
|
|
1914
1914
|
|
|
1915
|
-
- **
|
|
1915
|
+
- **98 built-in features**, zero third-party dependencies
|
|
1916
1916
|
- **2,899 tests** passing across all modules
|
|
1917
1917
|
- **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
|
|
1918
1918
|
- **`tina4python generate`**: model, route, migration, middleware scaffolding
|
|
@@ -89,6 +89,9 @@ from tina4_python.auth import Auth # noqa: E402, F401
|
|
|
89
89
|
# ── Queue ──
|
|
90
90
|
from tina4_python.queue import Queue # noqa: E402, F401
|
|
91
91
|
|
|
92
|
+
# ── MQTT (IoT) ──
|
|
93
|
+
from tina4_python.mqtt import Mqtt, MqttMessage, MqttError # noqa: E402, F401
|
|
94
|
+
|
|
92
95
|
# ── Template engine ──
|
|
93
96
|
from tina4_python.frond import Frond # noqa: E402, F401
|
|
94
97
|
|
|
@@ -101,7 +104,12 @@ from tina4_python.cache import ( # noqa: E402, F401
|
|
|
101
104
|
from tina4_python.container import Container # noqa: E402, F401
|
|
102
105
|
|
|
103
106
|
# ── Server ──
|
|
104
|
-
from tina4_python.core.server import
|
|
107
|
+
from tina4_python.core.server import ( # noqa: E402, F401
|
|
108
|
+
run,
|
|
109
|
+
background,
|
|
110
|
+
background_task_count,
|
|
111
|
+
stop_all_background_tasks,
|
|
112
|
+
)
|
|
105
113
|
|
|
106
114
|
# ── HTTP Client ──
|
|
107
115
|
from tina4_python.api import Api # noqa: E402, F401
|
|
@@ -13,9 +13,15 @@ CLI commands for development workflow.
|
|
|
13
13
|
tina4python test # Run tests
|
|
14
14
|
tina4python generate # Generate scaffolding
|
|
15
15
|
tina4python ai # Detect AI tools and install context
|
|
16
|
+
|
|
17
|
+
Three further commands — doctor, setup, deploy — are owned by the `tina4` client
|
|
18
|
+
and reached by DELEGATION (see DELEGATED / _delegate_to_client below), so
|
|
19
|
+
`tina4python doctor` behaves exactly like `tina4 doctor` without cloning the
|
|
20
|
+
client's implementation into four languages.
|
|
16
21
|
"""
|
|
17
22
|
import os
|
|
18
23
|
import re
|
|
24
|
+
import shutil
|
|
19
25
|
import signal
|
|
20
26
|
import subprocess
|
|
21
27
|
import sys
|
|
@@ -187,6 +193,83 @@ def _kill_process_on_port(port: int) -> bool:
|
|
|
187
193
|
return False
|
|
188
194
|
|
|
189
195
|
|
|
196
|
+
# ── Delegation to the `tina4` client ─────────────────────────────────
|
|
197
|
+
#
|
|
198
|
+
# `doctor`, `setup` and `deploy` are owned by the Rust `tina4` client, not by any
|
|
199
|
+
# framework. `doctor` probes ALL FOUR runtimes plus package managers, ports and
|
|
200
|
+
# global AI-skills currency; `setup` installs language runtimes (Homebrew /
|
|
201
|
+
# Chocolatey, with UAC elevation on Windows) and scaffolds a project from
|
|
202
|
+
# nothing; `deploy` writes deployment boilerplate baked into the client binary.
|
|
203
|
+
# Cloning any of them into four languages would duplicate hundreds of lines per
|
|
204
|
+
# language for zero new capability — and four copies would immediately drift.
|
|
205
|
+
#
|
|
206
|
+
# So the framework CLI DELEGATES: it resolves `tina4` on PATH, runs it with the
|
|
207
|
+
# same argv, and exits with the client's exit code. All four frameworks reach the
|
|
208
|
+
# SAME implementation, which is a stronger parity guarantee than four ports.
|
|
209
|
+
#
|
|
210
|
+
# Delegation is ALLOW-LISTED, never blind. The client forwards ITS unknown
|
|
211
|
+
# commands to the framework CLI, so a framework that forwarded its unknowns back
|
|
212
|
+
# would ping-pong an unknown command between two processes forever. The closed
|
|
213
|
+
# DELEGATED set contains only commands the client dispatches natively, so no
|
|
214
|
+
# loop is possible by construction, and a real typo still gets "Unknown command".
|
|
215
|
+
|
|
216
|
+
CLIENT_BINARY = "tina4"
|
|
217
|
+
|
|
218
|
+
# Internal process marker (same class as the client's own TINA4_SETUP_ELEVATED):
|
|
219
|
+
# set on the child so a client that resolves back to a framework CLI is caught
|
|
220
|
+
# instead of spawning forever. NOT user configuration — deliberately absent from
|
|
221
|
+
# the CLI's known_vars().
|
|
222
|
+
DELEGATION_GUARD_ENV = "TINA4_CLI_DELEGATED"
|
|
223
|
+
|
|
224
|
+
# Exit codes. 127 is the conventional "command not found" and covers both ways
|
|
225
|
+
# the client can be unreachable (absent from PATH, or the loop guard tripping).
|
|
226
|
+
EXIT_CLIENT_UNAVAILABLE = 127
|
|
227
|
+
EXIT_UNKNOWN_COMMAND = 1
|
|
228
|
+
|
|
229
|
+
CLIENT_INSTALL_HINT = (
|
|
230
|
+
" Install it: curl -fsSL https://tina4.com/install.sh | sh\n"
|
|
231
|
+
" Windows: irm https://tina4.com/install.ps1 | iex"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _find_client() -> str | None:
|
|
236
|
+
"""Absolute path of the `tina4` client on PATH, or None if it isn't there."""
|
|
237
|
+
return shutil.which(CLIENT_BINARY)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _delegate_to_client(command: str, args: list) -> int:
|
|
241
|
+
"""Run `tina4 <command> <args...>`, returning the client's exit code.
|
|
242
|
+
|
|
243
|
+
Returns EXIT_CLIENT_UNAVAILABLE (127) with an actionable message when the
|
|
244
|
+
client is not on PATH, or when the re-entry guard shows the resolved `tina4`
|
|
245
|
+
came back to a framework CLI (a delegation loop).
|
|
246
|
+
"""
|
|
247
|
+
if os.environ.get(DELEGATION_GUARD_ENV) == command:
|
|
248
|
+
print(
|
|
249
|
+
f" Refusing to delegate '{command}' again — the 'tina4' on your PATH\n"
|
|
250
|
+
f" resolved back to a framework CLI instead of the tina4 client.\n\n"
|
|
251
|
+
f" Check which 'tina4' comes first on your PATH and put the client first.",
|
|
252
|
+
file=sys.stderr,
|
|
253
|
+
)
|
|
254
|
+
return EXIT_CLIENT_UNAVAILABLE
|
|
255
|
+
|
|
256
|
+
client = _find_client()
|
|
257
|
+
if client is None:
|
|
258
|
+
print(
|
|
259
|
+
f" '{command}' is provided by the tina4 client, which is not on your PATH.\n\n"
|
|
260
|
+
f"{CLIENT_INSTALL_HINT}\n\n"
|
|
261
|
+
f" Then run: {CLIENT_BINARY} {command}",
|
|
262
|
+
file=sys.stderr,
|
|
263
|
+
)
|
|
264
|
+
return EXIT_CLIENT_UNAVAILABLE
|
|
265
|
+
|
|
266
|
+
# stdio is inherited, so the client's interactive prompts (setup) and colour
|
|
267
|
+
# output work exactly as if it had been invoked directly.
|
|
268
|
+
env = dict(os.environ)
|
|
269
|
+
env[DELEGATION_GUARD_ENV] = command
|
|
270
|
+
return subprocess.run([client, command, *args], env=env).returncode
|
|
271
|
+
|
|
272
|
+
|
|
190
273
|
# ── Main entry point ─────────────────────────────────────────────────
|
|
191
274
|
|
|
192
275
|
def main():
|
|
@@ -205,9 +288,14 @@ def main():
|
|
|
205
288
|
spec = COMMANDS.get(command)
|
|
206
289
|
if spec:
|
|
207
290
|
spec["handler"](cmd_args)
|
|
291
|
+
elif command in DELEGATED:
|
|
292
|
+
sys.exit(_delegate_to_client(command, cmd_args))
|
|
208
293
|
else:
|
|
294
|
+
# A genuinely unknown command is an ERROR: exit non-zero so a typo in a
|
|
295
|
+
# script or CI step fails loudly instead of reporting success.
|
|
209
296
|
print(f"Unknown command: {command}")
|
|
210
297
|
_help([])
|
|
298
|
+
sys.exit(EXIT_UNKNOWN_COMMAND)
|
|
211
299
|
|
|
212
300
|
|
|
213
301
|
def _env_migrate(args):
|
|
@@ -259,21 +347,26 @@ def _env_migrate(args):
|
|
|
259
347
|
def _help(args=None):
|
|
260
348
|
"""Print the human-readable command reference.
|
|
261
349
|
|
|
262
|
-
Generated from the COMMANDS and GENERATORS registries — the SAME
|
|
263
|
-
source of truth that drives dispatch (`main`) and the
|
|
264
|
-
manifest — so the help text can never drift from what the
|
|
350
|
+
Generated from the COMMANDS, DELEGATED and GENERATORS registries — the SAME
|
|
351
|
+
single source of truth that drives dispatch (`main`) and the
|
|
352
|
+
`commands --json` manifest — so the help text can never drift from what the
|
|
353
|
+
CLI actually does.
|
|
265
354
|
"""
|
|
266
355
|
command_rows = [
|
|
267
356
|
(f"{name} {spec.get('usage', '')}".rstrip(), spec["summary"])
|
|
268
357
|
for name, spec in COMMANDS.items()
|
|
269
358
|
]
|
|
359
|
+
delegated_rows = [
|
|
360
|
+
(f"{name} {spec.get('usage', '')}".rstrip(), spec["summary"])
|
|
361
|
+
for name, spec in DELEGATED.items()
|
|
362
|
+
]
|
|
270
363
|
generator_rows = [
|
|
271
364
|
(f"generate {name} {spec.get('usage', '')}".rstrip(), spec["summary"])
|
|
272
365
|
for name, spec in GENERATORS.items()
|
|
273
366
|
]
|
|
274
367
|
# Align summaries in a column; a left cell longer than the cap overflows
|
|
275
368
|
# cleanly (2-space gap) rather than pushing every other summary out.
|
|
276
|
-
pad = min(46, max(len(left) for left, _ in command_rows + generator_rows))
|
|
369
|
+
pad = min(46, max(len(left) for left, _ in command_rows + delegated_rows + generator_rows))
|
|
277
370
|
|
|
278
371
|
def row(left, summary):
|
|
279
372
|
gap = pad if len(left) <= pad else len(left)
|
|
@@ -281,6 +374,10 @@ def _help(args=None):
|
|
|
281
374
|
|
|
282
375
|
lines = ["", "Tina4 Python — CLI", "", "Usage: tina4python <command> [options]", "", "Commands:"]
|
|
283
376
|
lines += [row(left, summary) for left, summary in command_rows]
|
|
377
|
+
lines += ["", f"Delegated to the {CLIENT_BINARY} client (same behaviour in every framework):"]
|
|
378
|
+
lines += [row(left, summary) for left, summary in delegated_rows]
|
|
379
|
+
lines += [f" (these run the {CLIENT_BINARY} client — install: "
|
|
380
|
+
f"curl -fsSL https://tina4.com/install.sh | sh)"]
|
|
284
381
|
lines += ["", "Generators:"]
|
|
285
382
|
lines += [row(left, summary) for left, summary in generator_rows]
|
|
286
383
|
lines += [
|
|
@@ -2943,15 +3040,20 @@ def _queue(args):
|
|
|
2943
3040
|
def _commands_manifest() -> dict:
|
|
2944
3041
|
"""Build the machine-readable manifest of the CLI's command surface.
|
|
2945
3042
|
|
|
2946
|
-
Pure data: reads the module-level COMMANDS
|
|
2947
|
-
version — no bootstrap, no database, no migrations, no app imports.
|
|
2948
|
-
exactly what `commands --json` serializes and what the tina4 Rust
|
|
2949
|
-
consumes to discover which commands this framework supports.
|
|
3043
|
+
Pure data: reads the module-level COMMANDS and DELEGATED registries and the
|
|
3044
|
+
framework version — no bootstrap, no database, no migrations, no app imports.
|
|
3045
|
+
This is exactly what `commands --json` serializes and what the tina4 Rust
|
|
3046
|
+
client consumes to discover which commands this framework supports.
|
|
3047
|
+
|
|
3048
|
+
Commands the framework hands to the `tina4` client carry `"delegated": true`,
|
|
3049
|
+
so the manifest describes the WHOLE surface the CLI accepts while still
|
|
3050
|
+
saying who implements each one. The client needs no change for this: its help
|
|
3051
|
+
renderer already drops manifest names that clash with its own natives.
|
|
2950
3052
|
|
|
2951
3053
|
Shape::
|
|
2952
3054
|
|
|
2953
3055
|
{"framework": "python", "version": "<x.y.z>",
|
|
2954
|
-
"commands": [{"name", "summary", "args"?, "subcommands"?}, ...]}
|
|
3056
|
+
"commands": [{"name", "summary", "args"?, "subcommands"?, "delegated"?}, ...]}
|
|
2955
3057
|
"""
|
|
2956
3058
|
from tina4_python import __version__
|
|
2957
3059
|
commands = []
|
|
@@ -2962,6 +3064,11 @@ def _commands_manifest() -> dict:
|
|
|
2962
3064
|
if spec.get("subcommands"):
|
|
2963
3065
|
entry["subcommands"] = list(spec["subcommands"])
|
|
2964
3066
|
commands.append(entry)
|
|
3067
|
+
for command_name, spec in DELEGATED.items():
|
|
3068
|
+
entry = {"name": command_name, "summary": spec["summary"], "delegated": True}
|
|
3069
|
+
if spec.get("args"):
|
|
3070
|
+
entry["args"] = list(spec["args"])
|
|
3071
|
+
commands.append(entry)
|
|
2965
3072
|
return {"framework": "python", "version": __version__, "commands": commands}
|
|
2966
3073
|
|
|
2967
3074
|
|
|
@@ -2988,7 +3095,8 @@ def _commands(args=None):
|
|
|
2988
3095
|
print(f"\nTina4 {manifest['framework']} — {manifest['version']}\n")
|
|
2989
3096
|
width = max(len(command["name"]) for command in manifest["commands"])
|
|
2990
3097
|
for command in manifest["commands"]:
|
|
2991
|
-
|
|
3098
|
+
marker = f" ({CLIENT_BINARY} client)" if command.get("delegated") else ""
|
|
3099
|
+
print(f" {command['name']:<{width}} {command['summary']}{marker}")
|
|
2992
3100
|
if command.get("subcommands"):
|
|
2993
3101
|
print(f" {'':<{width}} {', '.join(command['subcommands'])}")
|
|
2994
3102
|
print()
|
|
@@ -3046,5 +3154,18 @@ COMMANDS = {
|
|
|
3046
3154
|
"help": {"handler": _help, "summary": "Show this help"},
|
|
3047
3155
|
}
|
|
3048
3156
|
|
|
3157
|
+
# Commands the `tina4` client OWNS and this CLI reaches by delegation — see the
|
|
3158
|
+
# "Delegation to the `tina4` client" section above for why these are not ported.
|
|
3159
|
+
# There are no handlers here: `main()` runs `tina4 <name> <args...>` and exits
|
|
3160
|
+
# with its code. Keep this set closed and identical in all four frameworks — it
|
|
3161
|
+
# must contain ONLY commands the client dispatches natively, or delegation could
|
|
3162
|
+
# bounce back and loop. Summaries are the client's own wording, verbatim.
|
|
3163
|
+
DELEGATED = {
|
|
3164
|
+
"doctor": {"summary": "Check installed languages and tools"},
|
|
3165
|
+
"setup": {"summary": "Guided, menu-driven setup: install everything + scaffold a ready-to-run project"},
|
|
3166
|
+
"deploy": {"usage": "<docker|systemd|nginx|cpanel> [--force]", "args": ["target"],
|
|
3167
|
+
"summary": "Generate deployment scaffolding (Dockerfile, systemd unit, nginx block, cPanel)"},
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3049
3170
|
|
|
3050
3171
|
__all__ = ["main"]
|
|
@@ -11,6 +11,7 @@ import signal
|
|
|
11
11
|
import asyncio
|
|
12
12
|
import contextvars
|
|
13
13
|
import importlib
|
|
14
|
+
import threading
|
|
14
15
|
import uuid
|
|
15
16
|
from email.utils import formatdate, parsedate_to_datetime
|
|
16
17
|
from pathlib import Path
|
|
@@ -34,10 +35,63 @@ _ai_port_ctx: contextvars.ContextVar[bool] = contextvars.ContextVar("_ai_port_ct
|
|
|
34
35
|
_start_time: float = 0
|
|
35
36
|
|
|
36
37
|
# ── Background tasks registry ────────────────────────────────────────────
|
|
37
|
-
_background_tasks: list[
|
|
38
|
+
_background_tasks: list["BackgroundTask"] = []
|
|
39
|
+
# The registry is read from the event loop and written from user code (which may
|
|
40
|
+
# be on another thread), and stop() is a search-then-delete — not atomic under
|
|
41
|
+
# the GIL. One lock guards every mutation.
|
|
42
|
+
_background_lock = threading.Lock()
|
|
38
43
|
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
class BackgroundTask:
|
|
46
|
+
"""Handle for one registered background task.
|
|
47
|
+
|
|
48
|
+
Returned by :func:`background`. Call :meth:`stop` to end the task AND remove
|
|
49
|
+
it from the registry — a stopped task is never left behind, so
|
|
50
|
+
:func:`background_task_count` always reports what is actually running.
|
|
51
|
+
|
|
52
|
+
Mirrors Node's ``background()`` handle (``handle.stop()``) and Ruby's
|
|
53
|
+
``Tina4::Background.stop_task(task)``.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
__slots__ = ("callback", "interval", "stopped", "_runner")
|
|
57
|
+
|
|
58
|
+
def __init__(self, callback, interval: float):
|
|
59
|
+
self.callback = callback
|
|
60
|
+
self.interval = float(interval)
|
|
61
|
+
self.stopped = False
|
|
62
|
+
# The asyncio.Task ticking this callback, once the server loop has
|
|
63
|
+
# started it. None while the task is registered but not yet running.
|
|
64
|
+
self._runner = None
|
|
65
|
+
|
|
66
|
+
def stop(self) -> bool:
|
|
67
|
+
"""Stop this task and deregister it.
|
|
68
|
+
|
|
69
|
+
Works whether or not the server is running: before start it simply
|
|
70
|
+
leaves the registry, and once ticking it also cancels its runner.
|
|
71
|
+
|
|
72
|
+
Idempotent — a second call is a safe no-op.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if this call removed the task, False if it was already gone.
|
|
76
|
+
"""
|
|
77
|
+
removed = False
|
|
78
|
+
with _background_lock:
|
|
79
|
+
# Identity, not equality: only THIS handle goes, never a sibling
|
|
80
|
+
# that happens to hold the same callback and interval.
|
|
81
|
+
for index, registered in enumerate(_background_tasks):
|
|
82
|
+
if registered is self:
|
|
83
|
+
del _background_tasks[index]
|
|
84
|
+
removed = True
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
self.stopped = True
|
|
88
|
+
runner, self._runner = self._runner, None
|
|
89
|
+
if runner is not None and not runner.done():
|
|
90
|
+
runner.cancel()
|
|
91
|
+
return removed
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def background(callback, interval: float = 1.0) -> BackgroundTask:
|
|
41
95
|
"""Register a background task that runs periodically in the server event loop.
|
|
42
96
|
|
|
43
97
|
Matches PHP's $app->background(fn, interval) pattern.
|
|
@@ -45,8 +99,59 @@ def background(callback, interval: float = 1.0):
|
|
|
45
99
|
Args:
|
|
46
100
|
callback: Function to call (sync or async, no arguments).
|
|
47
101
|
interval: Seconds between invocations (default: 1.0).
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A BackgroundTask handle — call ``.stop()`` to end and deregister it.
|
|
48
105
|
"""
|
|
49
|
-
|
|
106
|
+
task = BackgroundTask(callback, interval)
|
|
107
|
+
with _background_lock:
|
|
108
|
+
_background_tasks.append(task)
|
|
109
|
+
return task
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def background_task_count() -> int:
|
|
113
|
+
"""Number of REGISTERED background tasks (stopped ones are already gone)."""
|
|
114
|
+
with _background_lock:
|
|
115
|
+
return len(_background_tasks)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def stop_all_background_tasks() -> int:
|
|
119
|
+
"""Stop and deregister every background task. Returns how many were stopped.
|
|
120
|
+
|
|
121
|
+
Each task deregisters itself, so there is no blanket ``clear()``: clearing
|
|
122
|
+
would also drop a task registered while this was running, leaving it ticking
|
|
123
|
+
but invisible in the registry.
|
|
124
|
+
"""
|
|
125
|
+
with _background_lock:
|
|
126
|
+
snapshot = list(_background_tasks)
|
|
127
|
+
return sum(1 for task in snapshot if task.stop())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _start_background_tasks(executor, shutdown) -> list:
|
|
131
|
+
"""Start an asyncio runner for every registered task. Called once by _serve.
|
|
132
|
+
|
|
133
|
+
Each runner is bound to its handle so ``BackgroundTask.stop()`` can cancel a
|
|
134
|
+
task that is already ticking, not merely deregister one that never started.
|
|
135
|
+
"""
|
|
136
|
+
with _background_lock:
|
|
137
|
+
# Never iterate the live list — a concurrent stop() mutates it.
|
|
138
|
+
snapshot = list(_background_tasks)
|
|
139
|
+
|
|
140
|
+
runners = []
|
|
141
|
+
for task in snapshot:
|
|
142
|
+
if task.stopped:
|
|
143
|
+
continue # stopped between registration and server start
|
|
144
|
+
runner = asyncio.create_task(
|
|
145
|
+
background_tick_loop(task.callback, task.interval, executor, shutdown)
|
|
146
|
+
)
|
|
147
|
+
task._runner = runner
|
|
148
|
+
# A stop() that landed while the runner was being created must still win,
|
|
149
|
+
# or the task would tick on with nothing holding a reference to cancel it.
|
|
150
|
+
if task.stopped:
|
|
151
|
+
runner.cancel()
|
|
152
|
+
continue
|
|
153
|
+
runners.append(runner)
|
|
154
|
+
return runners
|
|
50
155
|
|
|
51
156
|
|
|
52
157
|
async def background_tick_loop(callback, interval: float, executor, shutdown):
|
|
@@ -2812,16 +2917,10 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
2812
2917
|
# other of workers.
|
|
2813
2918
|
import concurrent.futures
|
|
2814
2919
|
_executor = concurrent.futures.ThreadPoolExecutor(
|
|
2815
|
-
max_workers=max(
|
|
2920
|
+
max_workers=max(background_task_count(), 2),
|
|
2816
2921
|
thread_name_prefix="tina4_bg",
|
|
2817
2922
|
)
|
|
2818
|
-
bg_tasks =
|
|
2819
|
-
for task_def in _background_tasks:
|
|
2820
|
-
bg_tasks.append(asyncio.create_task(
|
|
2821
|
-
background_tick_loop(
|
|
2822
|
-
task_def["callback"], task_def["interval"], _executor, shutdown,
|
|
2823
|
-
)
|
|
2824
|
-
))
|
|
2923
|
+
bg_tasks = _start_background_tasks(_executor, shutdown)
|
|
2825
2924
|
|
|
2826
2925
|
await shutdown.wait()
|
|
2827
2926
|
|