tina4-python 3.13.43__tar.gz → 3.13.45__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.43 → tina4_python-3.13.45}/PKG-INFO +1 -1
- {tina4_python-3.13.43 → tina4_python-3.13.45}/pyproject.toml +1 -1
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/core/middleware.py +7 -4
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/core/request.py +5 -1
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/core/server.py +3 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/database/adapter.py +65 -5
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/database/mssql.py +21 -4
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/database/mysql.py +8 -1
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/database/postgres.py +18 -2
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/database/sqlite.py +21 -4
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/dev_admin/__init__.py +11 -11
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/migration/runner.py +18 -5
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue_backends/kafka_backend.py +22 -3
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue_backends/rabbitmq_backend.py +45 -7
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/session/__init__.py +5 -1
- {tina4_python-3.13.43 → tina4_python-3.13.45}/.gitignore +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/README.md +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/env.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -284,10 +284,13 @@ class CsrfMiddleware:
|
|
|
284
284
|
@staticmethod
|
|
285
285
|
def before_csrf(request, response):
|
|
286
286
|
"""Validate CSRF token before the route handler runs."""
|
|
287
|
-
#
|
|
288
|
-
|
|
289
|
-
#
|
|
290
|
-
#
|
|
287
|
+
# TINA4_CSRF=false (or 0/no) disables all CSRF checks, even when the
|
|
288
|
+
# middleware is attached explicitly — this is the documented kill
|
|
289
|
+
# switch ("TINA4_CSRF=false disables all checks"). When unset the
|
|
290
|
+
# default is enabled (true).
|
|
291
|
+
csrf_enabled = os.environ.get("TINA4_CSRF", "true").lower() not in ("false", "0", "no")
|
|
292
|
+
if not csrf_enabled:
|
|
293
|
+
return request, response
|
|
291
294
|
|
|
292
295
|
# Skip safe HTTP methods
|
|
293
296
|
method = getattr(request, "method", "GET").upper()
|
|
@@ -72,7 +72,7 @@ class Request:
|
|
|
72
72
|
__slots__ = (
|
|
73
73
|
"method", "path", "url", "query_string", "params", "query", "headers",
|
|
74
74
|
"body", "raw_body", "cookies", "files", "ip", "remote_ip",
|
|
75
|
-
"content_type", "session", "_route_params",
|
|
75
|
+
"content_type", "session", "_route_params", "_handler",
|
|
76
76
|
)
|
|
77
77
|
|
|
78
78
|
def __init__(self):
|
|
@@ -92,6 +92,10 @@ class Request:
|
|
|
92
92
|
self.content_type: str = ""
|
|
93
93
|
self.session = None # Set by session middleware
|
|
94
94
|
self._route_params: dict = {} # Dynamic route params ({id}, etc.)
|
|
95
|
+
self._handler = None # Matched route handler — set by dispatch
|
|
96
|
+
# before middleware runs, so before_*
|
|
97
|
+
# middleware (e.g. CsrfMiddleware) can read
|
|
98
|
+
# handler metadata like _noauth.
|
|
95
99
|
|
|
96
100
|
@classmethod
|
|
97
101
|
def from_scope(cls, scope: dict, body: bytes = b"") -> "Request":
|
|
@@ -1753,6 +1753,9 @@ async def handle(request: Request) -> Response:
|
|
|
1753
1753
|
if route:
|
|
1754
1754
|
request._route_params = params
|
|
1755
1755
|
request.merge_route_params()
|
|
1756
|
+
# Expose the matched handler on the request so before_* middleware
|
|
1757
|
+
# (e.g. CsrfMiddleware) can read handler metadata such as _noauth.
|
|
1758
|
+
request._handler = route.get("handler")
|
|
1756
1759
|
try:
|
|
1757
1760
|
skip = _check_auth(request, response, route)
|
|
1758
1761
|
if not skip:
|
|
@@ -351,13 +351,32 @@ class DatabaseAdapter:
|
|
|
351
351
|
["Alice"], ["Bob"], ["Eve"]
|
|
352
352
|
])
|
|
353
353
|
"""
|
|
354
|
+
rows = params_list or []
|
|
355
|
+
if not rows:
|
|
356
|
+
return DatabaseResult(affected_rows=0)
|
|
357
|
+
# Run the whole batch in ONE transaction on ONE connection so it is
|
|
358
|
+
# atomic AND affected_rows/last_id are reliable. In autocommit mode each
|
|
359
|
+
# standalone execute() commits on its own (possibly different, pooled)
|
|
360
|
+
# connection, which scattered the per-row rowcount / last_insert_id and
|
|
361
|
+
# made the aggregate non-deterministic. When already inside an explicit
|
|
362
|
+
# transaction we just join it (never nest).
|
|
363
|
+
owns_txn = self._autocommit and not getattr(self, "_in_transaction", False)
|
|
364
|
+
if owns_txn:
|
|
365
|
+
self.start_transaction()
|
|
354
366
|
total_affected = 0
|
|
355
367
|
last_id = None
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
368
|
+
try:
|
|
369
|
+
for params in rows:
|
|
370
|
+
result = self.execute(sql, params)
|
|
371
|
+
total_affected += result.affected_rows
|
|
372
|
+
if result.last_id is not None:
|
|
373
|
+
last_id = result.last_id
|
|
374
|
+
if owns_txn:
|
|
375
|
+
self.commit()
|
|
376
|
+
except Exception:
|
|
377
|
+
if owns_txn:
|
|
378
|
+
self.rollback()
|
|
379
|
+
raise
|
|
361
380
|
return DatabaseResult(
|
|
362
381
|
affected_rows=total_affected,
|
|
363
382
|
last_id=last_id,
|
|
@@ -389,6 +408,47 @@ class DatabaseAdapter:
|
|
|
389
408
|
stripped = stripped[:-1].rstrip()
|
|
390
409
|
return stripped
|
|
391
410
|
|
|
411
|
+
@staticmethod
|
|
412
|
+
def _strip_trailing_order_by(sql: str) -> str:
|
|
413
|
+
"""Strip a trailing top-level ``ORDER BY`` so the SQL can be safely
|
|
414
|
+
wrapped in ``SELECT COUNT(*) FROM (<sql>)`` for the row-count probe.
|
|
415
|
+
|
|
416
|
+
SQL Server rejects an ``ORDER BY`` inside a derived-table subquery
|
|
417
|
+
unless it carries ``TOP``/``OFFSET``/``FETCH`` (error 20018), which
|
|
418
|
+
silently zeroed the MSSQL count probe for any query ending in
|
|
419
|
+
``ORDER BY`` (issue #262 -- the bug existed in this master adapter too,
|
|
420
|
+
not only the mirrors). ``ORDER BY`` does not affect ``COUNT(*)``, so
|
|
421
|
+
dropping it for the probe ONLY is safe; the paginated query keeps its
|
|
422
|
+
``ORDER BY`` for ``OFFSET/FETCH``. An ``ORDER BY`` nested in a subquery,
|
|
423
|
+
or one already legalised by a following ``OFFSET``/``FETCH``/``FOR``, is
|
|
424
|
+
left intact. Parity with PHP ``SqlNormalizerTrait::stripTrailingOrderBy``.
|
|
425
|
+
"""
|
|
426
|
+
if not sql or not re.search(r"\bORDER\s+BY\b", sql, re.IGNORECASE):
|
|
427
|
+
return sql
|
|
428
|
+
last_top_level = -1
|
|
429
|
+
for match in re.finditer(r"\bORDER\s+BY\b", sql, re.IGNORECASE):
|
|
430
|
+
pos = match.start()
|
|
431
|
+
before = sql[:pos]
|
|
432
|
+
balanced_before = before.count("(") == before.count(")")
|
|
433
|
+
depth = 0
|
|
434
|
+
balanced_after = True
|
|
435
|
+
for ch in sql[pos:]:
|
|
436
|
+
if ch == "(":
|
|
437
|
+
depth += 1
|
|
438
|
+
elif ch == ")":
|
|
439
|
+
depth -= 1
|
|
440
|
+
if depth < 0:
|
|
441
|
+
balanced_after = False
|
|
442
|
+
break
|
|
443
|
+
if balanced_before and balanced_after:
|
|
444
|
+
last_top_level = pos
|
|
445
|
+
if last_top_level == -1:
|
|
446
|
+
return sql
|
|
447
|
+
tail = sql[last_top_level:]
|
|
448
|
+
if re.search(r"\b(?:OFFSET|FETCH|FOR)\b", tail, re.IGNORECASE):
|
|
449
|
+
return sql
|
|
450
|
+
return sql[:last_top_level].rstrip()
|
|
451
|
+
|
|
392
452
|
@staticmethod
|
|
393
453
|
def _split_schema(name: str) -> tuple[str | None, str]:
|
|
394
454
|
"""Split a possibly-qualified table name into (schema, table).
|
|
@@ -64,6 +64,14 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
64
64
|
cursor = self._conn.cursor(as_dict=True)
|
|
65
65
|
cursor.execute(sql, tuple(params) if params else ())
|
|
66
66
|
|
|
67
|
+
# Capture the affected-row count from the MAIN statement NOW, before any
|
|
68
|
+
# follow-up SELECT (SCOPE_IDENTITY / RETURNING fetch) runs on the SAME
|
|
69
|
+
# cursor and overwrites cursor.rowcount. Reading it at the end reflected
|
|
70
|
+
# the SCOPE_IDENTITY SELECT instead of the INSERT, so every INSERT
|
|
71
|
+
# reported affected_rows=0 (a batch insert summed to 0 even though the
|
|
72
|
+
# rows landed — surfaced by the live MySQL/MSSQL batch test).
|
|
73
|
+
affected = cursor.rowcount if cursor.rowcount is not None and cursor.rowcount >= 0 else 0
|
|
74
|
+
|
|
67
75
|
records = []
|
|
68
76
|
last_id = None
|
|
69
77
|
|
|
@@ -89,8 +97,6 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
89
97
|
if row:
|
|
90
98
|
records = [dict(row)]
|
|
91
99
|
|
|
92
|
-
affected = cursor.rowcount if cursor.rowcount >= 0 else 0
|
|
93
|
-
|
|
94
100
|
if not self._in_transaction and self.autocommit:
|
|
95
101
|
self._conn.commit()
|
|
96
102
|
|
|
@@ -116,7 +122,11 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
116
122
|
# the main cursor half-consumed, and the main query below is
|
|
117
123
|
# deliberately NOT wrapped so its error FAILS LOUD (parity with
|
|
118
124
|
# execute()) instead of looking like "no rows".
|
|
119
|
-
|
|
125
|
+
# Strip a trailing top-level ORDER BY before wrapping: SQL Server rejects
|
|
126
|
+
# ORDER BY in a derived-table subquery without TOP/OFFSET/FETCH (#262),
|
|
127
|
+
# which otherwise zeroed the count for any query ending in ORDER BY. The
|
|
128
|
+
# paginated query below keeps its ORDER BY.
|
|
129
|
+
count_sql = f"SELECT COUNT(*) AS cnt FROM ({self._strip_trailing_order_by(sql)}) AS _count_subquery"
|
|
120
130
|
probe = self._conn.cursor(as_dict=True)
|
|
121
131
|
try:
|
|
122
132
|
probe.execute(count_sql, tuple(params) if params else ())
|
|
@@ -155,7 +165,14 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
155
165
|
row = cursor.fetchone()
|
|
156
166
|
return dict(row) if row else None
|
|
157
167
|
|
|
158
|
-
def insert(self, table: str, data: dict) -> DatabaseResult:
|
|
168
|
+
def insert(self, table: str, data: dict | list) -> DatabaseResult:
|
|
169
|
+
# A list of dicts is a batch insert — delegate to the base class, which
|
|
170
|
+
# builds one parameterised INSERT and runs it per row via execute_many.
|
|
171
|
+
# (Database.insert / the docs advertise ``data: dict | list``; without
|
|
172
|
+
# this branch a list crashed with ``'list' object has no attribute
|
|
173
|
+
# 'keys'`` because this override only handled the single-dict case.)
|
|
174
|
+
if isinstance(data, list):
|
|
175
|
+
return super().insert(table, data)
|
|
159
176
|
columns = ", ".join(data.keys())
|
|
160
177
|
placeholders = ", ".join(["%s"] * len(data))
|
|
161
178
|
sql = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
|
|
@@ -140,7 +140,14 @@ class MySQLAdapter(DatabaseAdapter):
|
|
|
140
140
|
row = cursor.fetchone()
|
|
141
141
|
return dict(row) if row else None
|
|
142
142
|
|
|
143
|
-
def insert(self, table: str, data: dict) -> DatabaseResult:
|
|
143
|
+
def insert(self, table: str, data: dict | list) -> DatabaseResult:
|
|
144
|
+
# A list of dicts is a batch insert — delegate to the base class, which
|
|
145
|
+
# builds one parameterised INSERT and runs it per row via execute_many.
|
|
146
|
+
# (Database.insert / the docs advertise ``data: dict | list``; without
|
|
147
|
+
# this branch a list crashed with ``'list' object has no attribute
|
|
148
|
+
# 'keys'`` because this override only handled the single-dict case.)
|
|
149
|
+
if isinstance(data, list):
|
|
150
|
+
return super().insert(table, data)
|
|
144
151
|
columns = ", ".join(data.keys())
|
|
145
152
|
placeholders = ", ".join(["%s"] * len(data))
|
|
146
153
|
sql = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
|
|
@@ -283,6 +283,12 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
283
283
|
cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
|
284
284
|
self._exec_with_handling(cursor, sql, params)
|
|
285
285
|
|
|
286
|
+
# Capture rowcount NOW, before the lastval()/SAVEPOINT probes below run
|
|
287
|
+
# their own cursor.execute() calls and overwrite cursor.rowcount. A
|
|
288
|
+
# no-RETURNING INSERT was reporting affected_rows=0 because those probe
|
|
289
|
+
# statements clobbered the INSERT's rowcount before it was read at the end.
|
|
290
|
+
affected = cursor.rowcount if cursor.rowcount >= 0 else 0
|
|
291
|
+
|
|
286
292
|
records = []
|
|
287
293
|
last_id = None
|
|
288
294
|
|
|
@@ -317,7 +323,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
317
323
|
except Exception:
|
|
318
324
|
cursor.execute("ROLLBACK TO SAVEPOINT _t4_lastval_probe")
|
|
319
325
|
|
|
320
|
-
|
|
326
|
+
# NOTE: affected_rows was captured right after the main statement above,
|
|
327
|
+
# before the lastval()/SAVEPOINT probes ran their own cursor.execute()
|
|
328
|
+
# calls (which reset cursor.rowcount). Do not recompute it from
|
|
329
|
+
# cursor.rowcount here or a no-RETURNING INSERT reports 0.
|
|
321
330
|
|
|
322
331
|
if not self._in_transaction and self.autocommit:
|
|
323
332
|
self._conn.commit()
|
|
@@ -417,7 +426,14 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
417
426
|
row[key] = bytes(value)
|
|
418
427
|
return row
|
|
419
428
|
|
|
420
|
-
def insert(self, table: str, data: dict) -> DatabaseResult:
|
|
429
|
+
def insert(self, table: str, data: dict | list) -> DatabaseResult:
|
|
430
|
+
# A list of dicts is a batch insert — delegate to the base class, which
|
|
431
|
+
# builds one parameterised INSERT and runs it per row via execute_many.
|
|
432
|
+
# (Database.insert / the docs advertise ``data: dict | list``; without
|
|
433
|
+
# this branch a list crashed with ``'list' object has no attribute
|
|
434
|
+
# 'keys'`` because this override only handled the single-dict case.)
|
|
435
|
+
if isinstance(data, list):
|
|
436
|
+
return super().insert(table, data)
|
|
421
437
|
columns = ", ".join(data.keys())
|
|
422
438
|
placeholders = ", ".join(["%s"] * len(data))
|
|
423
439
|
sql = f"INSERT INTO {table} ({columns}) VALUES ({placeholders}) RETURNING *"
|
|
@@ -95,15 +95,25 @@ class SQLiteAdapter(DatabaseAdapter):
|
|
|
95
95
|
def execute_many(self, sql: str, params_list: list[list] = None) -> DatabaseResult:
|
|
96
96
|
"""Optimized batch execute using SQLite's executemany."""
|
|
97
97
|
sql = self._translate_sql(sql)
|
|
98
|
-
|
|
98
|
+
rows = params_list or []
|
|
99
|
+
self._conn.executemany(sql, rows)
|
|
100
|
+
|
|
101
|
+
# cursor.rowcount / cursor.lastrowid are unreliable after executemany()
|
|
102
|
+
# in sqlite3 (rowcount can come back 0 or -1, lastrowid is not set) and
|
|
103
|
+
# were non-deterministic across pooled connections. The batch is
|
|
104
|
+
# all-or-raise, so every supplied row was applied; the last inserted id
|
|
105
|
+
# is read deterministically from last_insert_rowid() on this connection.
|
|
106
|
+
affected = len(rows)
|
|
107
|
+
last_row = self._conn.execute("SELECT last_insert_rowid()").fetchone()
|
|
108
|
+
last_id = last_row[0] if last_row and last_row[0] else None
|
|
99
109
|
|
|
100
110
|
if not self._in_transaction and self.autocommit:
|
|
101
111
|
if self._conn.in_transaction:
|
|
102
112
|
self._conn.execute("COMMIT")
|
|
103
113
|
|
|
104
114
|
return DatabaseResult(
|
|
105
|
-
affected_rows=
|
|
106
|
-
last_id=
|
|
115
|
+
affected_rows=affected,
|
|
116
|
+
last_id=last_id,
|
|
107
117
|
)
|
|
108
118
|
|
|
109
119
|
def fetch(self, sql: str, params: list = None,
|
|
@@ -144,7 +154,14 @@ class SQLiteAdapter(DatabaseAdapter):
|
|
|
144
154
|
row = cursor.fetchone()
|
|
145
155
|
return dict(row) if row else None
|
|
146
156
|
|
|
147
|
-
def insert(self, table: str, data: dict) -> DatabaseResult:
|
|
157
|
+
def insert(self, table: str, data: dict | list) -> DatabaseResult:
|
|
158
|
+
# A list of dicts is a batch insert — delegate to the base class, which
|
|
159
|
+
# builds one parameterised INSERT and runs it per row via execute_many.
|
|
160
|
+
# (Database.insert / the docs advertise ``data: dict | list``; without
|
|
161
|
+
# this branch a list crashed with ``'list' object has no attribute
|
|
162
|
+
# 'keys'`` because this override only handled the single-dict case.)
|
|
163
|
+
if isinstance(data, list):
|
|
164
|
+
return super().insert(table, data)
|
|
148
165
|
columns = ", ".join(data.keys())
|
|
149
166
|
placeholders = ", ".join(["?"] * len(data))
|
|
150
167
|
sql = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
|
|
@@ -159,10 +159,10 @@ class BrokenTracker:
|
|
|
159
159
|
|
|
160
160
|
if filepath.exists():
|
|
161
161
|
try:
|
|
162
|
-
existing = json.loads(filepath.read_text())
|
|
162
|
+
existing = json.loads(filepath.read_text(encoding="utf-8"))
|
|
163
163
|
existing["count"] = existing.get("count", 1) + 1
|
|
164
164
|
existing["last_seen"] = datetime.now(timezone.utc).isoformat()
|
|
165
|
-
filepath.write_text(json.dumps(existing, indent=2))
|
|
165
|
+
filepath.write_text(json.dumps(existing, indent=2), encoding="utf-8")
|
|
166
166
|
return sig_hash
|
|
167
167
|
except (json.JSONDecodeError, OSError):
|
|
168
168
|
pass
|
|
@@ -178,7 +178,7 @@ class BrokenTracker:
|
|
|
178
178
|
"last_seen": datetime.now(timezone.utc).isoformat(),
|
|
179
179
|
"resolved": False,
|
|
180
180
|
}
|
|
181
|
-
filepath.write_text(json.dumps(entry, indent=2))
|
|
181
|
+
filepath.write_text(json.dumps(entry, indent=2), encoding="utf-8")
|
|
182
182
|
return sig_hash
|
|
183
183
|
|
|
184
184
|
@classmethod
|
|
@@ -190,7 +190,7 @@ class BrokenTracker:
|
|
|
190
190
|
entries = []
|
|
191
191
|
for f in sorted(broken_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
|
|
192
192
|
try:
|
|
193
|
-
entries.append(json.loads(f.read_text()))
|
|
193
|
+
entries.append(json.loads(f.read_text(encoding="utf-8")))
|
|
194
194
|
except (json.JSONDecodeError, OSError):
|
|
195
195
|
continue
|
|
196
196
|
return entries
|
|
@@ -202,9 +202,9 @@ class BrokenTracker:
|
|
|
202
202
|
if not filepath.exists():
|
|
203
203
|
return False
|
|
204
204
|
try:
|
|
205
|
-
entry = json.loads(filepath.read_text())
|
|
205
|
+
entry = json.loads(filepath.read_text(encoding="utf-8"))
|
|
206
206
|
entry["resolved"] = True
|
|
207
|
-
filepath.write_text(json.dumps(entry, indent=2))
|
|
207
|
+
filepath.write_text(json.dumps(entry, indent=2), encoding="utf-8")
|
|
208
208
|
return True
|
|
209
209
|
except (json.JSONDecodeError, OSError):
|
|
210
210
|
return False
|
|
@@ -217,7 +217,7 @@ class BrokenTracker:
|
|
|
217
217
|
return
|
|
218
218
|
for f in broken_dir.glob("*.json"):
|
|
219
219
|
try:
|
|
220
|
-
entry = json.loads(f.read_text())
|
|
220
|
+
entry = json.loads(f.read_text(encoding="utf-8"))
|
|
221
221
|
if entry.get("resolved"):
|
|
222
222
|
f.unlink()
|
|
223
223
|
except (json.JSONDecodeError, OSError):
|
|
@@ -1791,7 +1791,7 @@ async def _api_connections(request, response):
|
|
|
1791
1791
|
username = ""
|
|
1792
1792
|
password = ""
|
|
1793
1793
|
if env_path.exists():
|
|
1794
|
-
for line in env_path.read_text().splitlines():
|
|
1794
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
1795
1795
|
line = line.strip()
|
|
1796
1796
|
if line.startswith("#") or "=" not in line:
|
|
1797
1797
|
continue
|
|
@@ -1863,7 +1863,7 @@ async def _api_connections_save(request, response):
|
|
|
1863
1863
|
env_path = Path(".env")
|
|
1864
1864
|
lines = []
|
|
1865
1865
|
if env_path.exists():
|
|
1866
|
-
lines = env_path.read_text().splitlines()
|
|
1866
|
+
lines = env_path.read_text(encoding="utf-8").splitlines()
|
|
1867
1867
|
keys_found = {"TINA4_DATABASE_URL": False, "TINA4_DATABASE_USERNAME": False, "TINA4_DATABASE_PASSWORD": False}
|
|
1868
1868
|
new_lines = []
|
|
1869
1869
|
for line in lines:
|
|
@@ -1891,7 +1891,7 @@ async def _api_connections_save(request, response):
|
|
|
1891
1891
|
if not found:
|
|
1892
1892
|
val = {"TINA4_DATABASE_URL": url, "TINA4_DATABASE_USERNAME": username, "TINA4_DATABASE_PASSWORD": password}[key]
|
|
1893
1893
|
new_lines.append(f"{key}={val}")
|
|
1894
|
-
env_path.write_text("\n".join(new_lines) + "\n")
|
|
1894
|
+
env_path.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
|
|
1895
1895
|
return response({"success": True})
|
|
1896
1896
|
except Exception as e:
|
|
1897
1897
|
return response({"success": False, "error": str(e)})
|
|
@@ -1906,7 +1906,7 @@ async def _api_gallery_list(request, response):
|
|
|
1906
1906
|
for entry in sorted(gallery_dir.iterdir()):
|
|
1907
1907
|
meta_file = entry / "meta.json"
|
|
1908
1908
|
if entry.is_dir() and meta_file.exists():
|
|
1909
|
-
meta = json.loads(meta_file.read_text())
|
|
1909
|
+
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
1910
1910
|
meta["id"] = entry.name
|
|
1911
1911
|
# List the files that would be deployed
|
|
1912
1912
|
src_dir = entry / "src"
|
|
@@ -95,13 +95,26 @@ def _create_v3_table(db) -> None:
|
|
|
95
95
|
)
|
|
96
96
|
""")
|
|
97
97
|
else:
|
|
98
|
-
|
|
98
|
+
# Engine-aware bookkeeping DDL. Each engine spells an auto-increment
|
|
99
|
+
# integer PK differently (SQLite AUTOINCREMENT, PostgreSQL SERIAL, MySQL
|
|
100
|
+
# AUTO_INCREMENT, MSSQL IDENTITY), and a TEXT column cannot carry a UNIQUE
|
|
101
|
+
# constraint on MySQL -- so migration_id is VARCHAR. (SQLite gives VARCHAR
|
|
102
|
+
# TEXT affinity, so this stays behaviour-identical there.) Mirrors the
|
|
103
|
+
# engine-aware DDL in ORM.create_table; without it `migrate()` died with
|
|
104
|
+
# "syntax error at AUTOINCREMENT" on PostgreSQL/MySQL/MSSQL.
|
|
105
|
+
engine = (db.get_database_type() or "sqlite").lower()
|
|
106
|
+
id_column = {
|
|
107
|
+
"postgresql": "id SERIAL PRIMARY KEY",
|
|
108
|
+
"mysql": "id INTEGER PRIMARY KEY AUTO_INCREMENT",
|
|
109
|
+
"mssql": "id INTEGER IDENTITY(1,1) PRIMARY KEY",
|
|
110
|
+
}.get(engine, "id INTEGER PRIMARY KEY AUTOINCREMENT")
|
|
111
|
+
db.execute(f"""
|
|
99
112
|
CREATE TABLE tina4_migration (
|
|
100
|
-
|
|
101
|
-
migration_id
|
|
102
|
-
description
|
|
113
|
+
{id_column},
|
|
114
|
+
migration_id VARCHAR(500) NOT NULL UNIQUE,
|
|
115
|
+
description VARCHAR(500),
|
|
103
116
|
batch INTEGER NOT NULL DEFAULT 1,
|
|
104
|
-
executed_at
|
|
117
|
+
executed_at VARCHAR(50) NOT NULL,
|
|
105
118
|
passed INTEGER NOT NULL DEFAULT 1
|
|
106
119
|
)
|
|
107
120
|
""")
|
|
@@ -17,6 +17,7 @@ import os
|
|
|
17
17
|
import secrets
|
|
18
18
|
import socket
|
|
19
19
|
import struct
|
|
20
|
+
import time
|
|
20
21
|
import zlib
|
|
21
22
|
|
|
22
23
|
|
|
@@ -86,11 +87,29 @@ class KafkaConnector:
|
|
|
86
87
|
self._ensure_connected()
|
|
87
88
|
|
|
88
89
|
if self._use_confluent:
|
|
89
|
-
|
|
90
|
+
first = topic not in self._subscribed_topics
|
|
91
|
+
if first:
|
|
90
92
|
self._consumer.subscribe([topic])
|
|
91
93
|
self._subscribed_topics.add(topic)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
# The FIRST poll after subscribing must drive the consumer-group
|
|
95
|
+
# join + partition assignment, which takes several seconds on a cold
|
|
96
|
+
# broker. Until partitions are assigned, poll() returns None even
|
|
97
|
+
# when the topic already has messages -- so a single short poll made
|
|
98
|
+
# dequeue() return None right after enqueue(). Poll in a bounded loop
|
|
99
|
+
# on first subscribe (deadline TINA4_KAFKA_ASSIGN_TIMEOUT, default
|
|
100
|
+
# 15s); steady state stays a single ~1s poll.
|
|
101
|
+
deadline = time.monotonic() + (
|
|
102
|
+
float(os.environ.get("TINA4_KAFKA_ASSIGN_TIMEOUT", "15")) if first else 1.0
|
|
103
|
+
)
|
|
104
|
+
msg = None
|
|
105
|
+
while True:
|
|
106
|
+
candidate = self._consumer.poll(timeout=0.5)
|
|
107
|
+
if candidate is not None and not candidate.error():
|
|
108
|
+
msg = candidate
|
|
109
|
+
break
|
|
110
|
+
if time.monotonic() >= deadline:
|
|
111
|
+
break
|
|
112
|
+
if msg is None:
|
|
94
113
|
return None
|
|
95
114
|
self._last_message = msg
|
|
96
115
|
try:
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
@@ -135,7 +135,14 @@ class RabbitMQConnector:
|
|
|
135
135
|
self._ensure_connected()
|
|
136
136
|
|
|
137
137
|
if self._use_pika:
|
|
138
|
-
|
|
138
|
+
# Declare durably (idempotent — creates the queue if missing) rather
|
|
139
|
+
# than passive=True. A passive declare raises ChannelClosedByBroker
|
|
140
|
+
# (404 NOT_FOUND) when the queue does not yet exist, so size() on a
|
|
141
|
+
# fresh topic crashed on the pika path. The raw-AMQP path already
|
|
142
|
+
# declares-then-counts and returns 0 for an unseen queue; this brings
|
|
143
|
+
# the pika path to the same behaviour.
|
|
144
|
+
self._ensure_queue_pika(topic)
|
|
145
|
+
result = self._channel.queue_declare(queue=topic, durable=True)
|
|
139
146
|
return result.method.message_count
|
|
140
147
|
else:
|
|
141
148
|
return self._queue_size_raw(topic)
|
|
@@ -200,11 +207,13 @@ class RabbitMQConnector:
|
|
|
200
207
|
# Connection.StartOk
|
|
201
208
|
self._send_connection_start_ok()
|
|
202
209
|
|
|
203
|
-
# Connection.Tune
|
|
204
|
-
self._read_frame()
|
|
210
|
+
# Connection.Tune (the broker proposes channel-max, frame-max, heartbeat)
|
|
211
|
+
tune = self._read_frame()
|
|
205
212
|
|
|
206
|
-
# Connection.TuneOk
|
|
207
|
-
|
|
213
|
+
# Connection.TuneOk — negotiate from the broker's proposal (never exceed
|
|
214
|
+
# its channel-max; a hardcoded channel-max of 0 = "no limit" is treated as
|
|
215
|
+
# greater than the proposed 2047 and RabbitMQ aborts the handshake).
|
|
216
|
+
self._send_connection_tune_ok(tune)
|
|
208
217
|
|
|
209
218
|
# Connection.Open
|
|
210
219
|
self._send_connection_open()
|
|
@@ -299,8 +308,37 @@ class RabbitMQConnector:
|
|
|
299
308
|
)
|
|
300
309
|
self._write_method(0, 10, 11, args)
|
|
301
310
|
|
|
302
|
-
def _send_connection_tune_ok(self):
|
|
303
|
-
|
|
311
|
+
def _send_connection_tune_ok(self, tune: dict | None = None):
|
|
312
|
+
"""Send Connection.TuneOk negotiated from the broker's Connection.Tune.
|
|
313
|
+
|
|
314
|
+
AMQP 0-9-1 requires the client's TuneOk values to not exceed the server's
|
|
315
|
+
proposal. The Tune payload (after the 4-byte class+method) is
|
|
316
|
+
channel-max:short, frame-max:long, heartbeat:short. We echo channel-max
|
|
317
|
+
(the server's cap; 0 = unlimited), clamp frame-max to min(desired, server)
|
|
318
|
+
treating 0 as unlimited, and choose a heartbeat. Hardcoding channel-max=0
|
|
319
|
+
means "no limit", which RabbitMQ treats as exceeding its proposed 2047 and
|
|
320
|
+
aborts the connection right after Open.
|
|
321
|
+
"""
|
|
322
|
+
desired_frame_max = 131072
|
|
323
|
+
desired_heartbeat = 60
|
|
324
|
+
|
|
325
|
+
channel_max = 2047
|
|
326
|
+
frame_max = desired_frame_max
|
|
327
|
+
heartbeat = desired_heartbeat
|
|
328
|
+
|
|
329
|
+
if tune is not None and len(tune.get("payload", b"")) >= 12:
|
|
330
|
+
# payload: class_id(2) + method_id(2) + channel-max(2) + frame-max(4) + heartbeat(2)
|
|
331
|
+
_cls, _method, server_channel_max, server_frame_max, server_heartbeat = struct.unpack(
|
|
332
|
+
"!HHHIH", tune["payload"][:12]
|
|
333
|
+
)
|
|
334
|
+
# channel-max: never exceed the server's proposal (echo its value).
|
|
335
|
+
channel_max = server_channel_max
|
|
336
|
+
# frame-max: min(desired, server), treating 0 as unlimited on either side.
|
|
337
|
+
frame_max = desired_frame_max if server_frame_max == 0 else min(desired_frame_max, server_frame_max)
|
|
338
|
+
# heartbeat: our choice, clamped to the server's if it proposed a non-zero one.
|
|
339
|
+
heartbeat = desired_heartbeat if server_heartbeat == 0 else min(desired_heartbeat, server_heartbeat)
|
|
340
|
+
|
|
341
|
+
args = struct.pack("!HIH", channel_max, frame_max, heartbeat)
|
|
304
342
|
self._write_method(0, 10, 31, args)
|
|
305
343
|
|
|
306
344
|
def _send_connection_open(self):
|
|
@@ -195,7 +195,11 @@ class Session:
|
|
|
195
195
|
from tina4_python.session_handlers import MongoDBSessionHandler
|
|
196
196
|
return MongoDBSessionHandler()
|
|
197
197
|
elif backend in ("database", "db"):
|
|
198
|
-
|
|
198
|
+
# Resolve the same connection the ORM uses (global bound db, then
|
|
199
|
+
# TINA4_DATABASE_URL). DatabaseSessionHandler "uses whatever DB is
|
|
200
|
+
# connected" — so reuse the single ORM resolver rather than guess.
|
|
201
|
+
from tina4_python.orm.model import ORM
|
|
202
|
+
return DatabaseSessionHandler(ORM._get_db())
|
|
199
203
|
else:
|
|
200
204
|
return FileSessionHandler()
|
|
201
205
|
|
|
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
|
|
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
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/public/swagger/oauth2-redirect.html
RENAMED
|
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
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/templates/docker/python/Dockerfile
RENAMED
|
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
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.43 → tina4_python-3.13.45}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|