tina4-python 3.11.35__tar.gz → 3.11.36__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.11.35 → tina4_python-3.11.36}/.gitignore +4 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/PKG-INFO +1 -1
- {tina4_python-3.11.35 → tina4_python-3.11.36}/pyproject.toml +1 -1
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/__init__.py +1 -1
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/core/server.py +57 -5
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/database/connection.py +32 -1
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/debug/__init__.py +6 -3
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/dev_admin/__init__.py +656 -90
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/dev_admin/plan.py +108 -0
- tina4_python-3.11.36/tina4_python/docs.py +821 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/frond/engine.py +76 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/mcp/tools.py +39 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/orm/model.py +0 -6
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/js/tina4-dev-admin.js +274 -140
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/js/tina4-dev-admin.min.js +274 -140
- {tina4_python-3.11.35 → tina4_python-3.11.36}/README.md +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/Testing.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/core/events.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/core/request.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/core/response.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/core/router.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.11.35 → tina4_python-3.11.36}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -837,7 +837,10 @@ async def _handle_dev_admin(request: Request, response: Response) -> Response:
|
|
|
837
837
|
"""Serve the /__dev dashboard and API routes."""
|
|
838
838
|
from tina4_python.dev_admin import get_api_handlers
|
|
839
839
|
if request.path in ("/__dev/", "/__dev", "/__dev/v2", "/__dev/v2/"):
|
|
840
|
-
# Unified SPA dev admin
|
|
840
|
+
# Unified SPA dev admin. The bundle derives its WS URL from
|
|
841
|
+
# `location.host` directly, so no environment shim is needed —
|
|
842
|
+
# the framework serves /__dev_reload on its own port and the
|
|
843
|
+
# SPA reaches it as `ws://<page-host>/__dev_reload`.
|
|
841
844
|
response.html("""<!DOCTYPE html>
|
|
842
845
|
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Tina4 Dev Admin</title></head>
|
|
843
846
|
<body><div id="app" data-framework="python" data-color="#3b82f6"></div>
|
|
@@ -847,8 +850,19 @@ async def _handle_dev_admin(request: Request, response: Response) -> Response:
|
|
|
847
850
|
handler_info = handlers.get(request.path)
|
|
848
851
|
if handler_info and request.method == handler_info[0]:
|
|
849
852
|
try:
|
|
850
|
-
def _resp(data, code=200):
|
|
851
|
-
|
|
853
|
+
def _resp(data, code=200, content_type=None):
|
|
854
|
+
# content_type overrides the auto-detected MIME —
|
|
855
|
+
# lets handlers stream binary with an explicit
|
|
856
|
+
# Content-Type (e.g. /__dev/api/file/raw).
|
|
857
|
+
if content_type is not None:
|
|
858
|
+
response.status(code)
|
|
859
|
+
response.content_type = content_type
|
|
860
|
+
response.content = data if isinstance(data, (bytes, bytearray)) else str(data).encode("utf-8")
|
|
861
|
+
elif isinstance(data, (bytes, bytearray)):
|
|
862
|
+
response.status(code)
|
|
863
|
+
response.content_type = "application/octet-stream"
|
|
864
|
+
response.content = data
|
|
865
|
+
elif isinstance(data, str):
|
|
852
866
|
response.status(code).html(data)
|
|
853
867
|
else:
|
|
854
868
|
response.status(code).json(data)
|
|
@@ -1182,8 +1196,14 @@ async def handle(request: Request) -> Response:
|
|
|
1182
1196
|
from tina4_python.dotenv import is_truthy
|
|
1183
1197
|
_is_dev = is_truthy(os.environ.get("TINA4_DEBUG", ""))
|
|
1184
1198
|
|
|
1185
|
-
# Dev admin
|
|
1186
|
-
|
|
1199
|
+
# Dev admin — also catches /ai/api/chat (SPA's ollama proxy) and the
|
|
1200
|
+
# bare /ai /vision /embed /image /rag service-health probes that
|
|
1201
|
+
# drive the "SERVICES ●●●●●" dots in the dev-admin UI.
|
|
1202
|
+
_dev_extra_paths = {"/ai/api/chat", "/ai", "/vision", "/embed", "/image", "/rag"}
|
|
1203
|
+
if _is_dev and (
|
|
1204
|
+
request.path.startswith("/__dev")
|
|
1205
|
+
or request.path in _dev_extra_paths
|
|
1206
|
+
):
|
|
1187
1207
|
return await _handle_dev_admin(request, response)
|
|
1188
1208
|
|
|
1189
1209
|
# Swagger
|
|
@@ -1615,6 +1635,38 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1615
1635
|
log_level = os.environ.get("TINA4_LOG_LEVEL", "error" if not is_production else "error")
|
|
1616
1636
|
Log.configure(level=log_level, production=is_production)
|
|
1617
1637
|
|
|
1638
|
+
# Install a top-level exception hook so uncaught exceptions bubbling
|
|
1639
|
+
# out of anything (a route handler, a background task, the event
|
|
1640
|
+
# loop itself on startup) land in logs/error.log. Without this,
|
|
1641
|
+
# an uncaught exception surfaces only via Python's default stderr
|
|
1642
|
+
# writer and never touches Log — the same gap PHP had before its
|
|
1643
|
+
# set_exception_handler fix. Chains to the previous hook so any
|
|
1644
|
+
# debugger / IDE hook already in place still fires.
|
|
1645
|
+
import sys as _sys
|
|
1646
|
+
import traceback as _traceback
|
|
1647
|
+
_prior_excepthook = _sys.excepthook
|
|
1648
|
+
|
|
1649
|
+
def _tina4_excepthook(exc_type, exc_value, exc_tb):
|
|
1650
|
+
# KeyboardInterrupt is a user-initiated Ctrl+C, not an error —
|
|
1651
|
+
# defer to the prior hook (which prints a clean traceback).
|
|
1652
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
1653
|
+
_prior_excepthook(exc_type, exc_value, exc_tb)
|
|
1654
|
+
return
|
|
1655
|
+
try:
|
|
1656
|
+
trace_text = "".join(_traceback.format_exception(exc_type, exc_value, exc_tb))
|
|
1657
|
+
Log.error(
|
|
1658
|
+
f"Uncaught {exc_type.__name__}: {exc_value}",
|
|
1659
|
+
trace=trace_text,
|
|
1660
|
+
)
|
|
1661
|
+
except Exception:
|
|
1662
|
+
# If logging itself fails (disk full, permissions, logger
|
|
1663
|
+
# not initialised yet), fall through to the prior hook so
|
|
1664
|
+
# the user still sees something in stderr.
|
|
1665
|
+
pass
|
|
1666
|
+
_prior_excepthook(exc_type, exc_value, exc_tb)
|
|
1667
|
+
|
|
1668
|
+
_sys.excepthook = _tina4_excepthook
|
|
1669
|
+
|
|
1618
1670
|
# Ensure folders
|
|
1619
1671
|
_ensure_folders()
|
|
1620
1672
|
|
|
@@ -166,6 +166,11 @@ class Database:
|
|
|
166
166
|
self._adapter: DatabaseAdapter = self._create_adapter()
|
|
167
167
|
self._adapter.connect(self._connection_path(), username=self.username, password=self.password, **kwargs)
|
|
168
168
|
|
|
169
|
+
# Per-thread transaction adapter pin. While set, every operation
|
|
170
|
+
# on this thread routes to the same adapter — so the round-robin
|
|
171
|
+
# pool can't rotate mid-transaction and silently break atomicity.
|
|
172
|
+
self._tx_local = threading.local()
|
|
173
|
+
|
|
169
174
|
# Query cache — off by default, opt-in via TINA4_DB_CACHE=true
|
|
170
175
|
from tina4_python.dotenv import is_truthy
|
|
171
176
|
self._cache_enabled: bool = is_truthy(os.environ.get("TINA4_DB_CACHE", "false"))
|
|
@@ -308,7 +313,25 @@ class Database:
|
|
|
308
313
|
# ── Pool-aware adapter access ─────────────────────────────
|
|
309
314
|
|
|
310
315
|
def _get_adapter(self) -> DatabaseAdapter:
|
|
311
|
-
"""Get an adapter
|
|
316
|
+
"""Get an adapter for the next operation.
|
|
317
|
+
|
|
318
|
+
With pooling enabled, ordinary calls round-robin through the pool.
|
|
319
|
+
Inside a transaction, however, all calls must land on the SAME
|
|
320
|
+
adapter — otherwise start_transaction(), execute() and commit()
|
|
321
|
+
each rotate to a different connection and the transaction is
|
|
322
|
+
meaningless (executes autocommit on whatever adapter they hit;
|
|
323
|
+
the final commit lands on yet another adapter that has nothing
|
|
324
|
+
to commit; rollback() is silently no-op'd).
|
|
325
|
+
|
|
326
|
+
We pin the adapter to the calling thread for the duration of the
|
|
327
|
+
transaction. start_transaction() sets the pin, commit()/rollback()
|
|
328
|
+
clear it. While pinned, _get_adapter() returns that same adapter
|
|
329
|
+
for every call so the whole transaction is atomic on one
|
|
330
|
+
connection.
|
|
331
|
+
"""
|
|
332
|
+
pinned = getattr(self._tx_local, "adapter", None)
|
|
333
|
+
if pinned is not None:
|
|
334
|
+
return pinned
|
|
312
335
|
if self._pool is not None:
|
|
313
336
|
return self._pool.checkout()
|
|
314
337
|
return self._adapter
|
|
@@ -422,16 +445,24 @@ class Database:
|
|
|
422
445
|
return adapter.delete(table, filter_sql, params)
|
|
423
446
|
|
|
424
447
|
def start_transaction(self):
|
|
448
|
+
"""Begin a transaction. Pins the adapter to this thread for the
|
|
449
|
+
whole transaction so executes and the final commit/rollback all
|
|
450
|
+
run on the same connection."""
|
|
425
451
|
adapter = self._get_adapter()
|
|
452
|
+
self._tx_local.adapter = adapter
|
|
426
453
|
adapter.start_transaction()
|
|
427
454
|
|
|
428
455
|
def commit(self):
|
|
456
|
+
"""Commit the current transaction and release the adapter pin."""
|
|
429
457
|
adapter = self._get_adapter()
|
|
430
458
|
adapter.commit()
|
|
459
|
+
self._tx_local.adapter = None
|
|
431
460
|
|
|
432
461
|
def rollback(self):
|
|
462
|
+
"""Roll back the current transaction and release the adapter pin."""
|
|
433
463
|
adapter = self._get_adapter()
|
|
434
464
|
adapter.rollback()
|
|
465
|
+
self._tx_local.adapter = None
|
|
435
466
|
|
|
436
467
|
def table_exists(self, name: str) -> bool:
|
|
437
468
|
adapter = self._get_adapter()
|
|
@@ -200,12 +200,15 @@ class Log:
|
|
|
200
200
|
color = cls.COLORS.get(level, "")
|
|
201
201
|
print(f"{color}{line}{cls.RESET}")
|
|
202
202
|
|
|
203
|
-
# Always write ALL levels to file (raw log, no filtering)
|
|
203
|
+
# Always write ALL levels to the main file (raw log, no filtering)
|
|
204
204
|
if cls._writer:
|
|
205
205
|
cls._writer.write(line)
|
|
206
206
|
|
|
207
|
-
#
|
|
208
|
-
|
|
207
|
+
# Mirror WARNING and ERROR into the dedicated error log so
|
|
208
|
+
# `tail -f logs/error.log` gives just the stuff worth looking
|
|
209
|
+
# at, without wading through DEBUG / INFO noise. Parity with
|
|
210
|
+
# tina4-php's Log class.
|
|
211
|
+
if cls._error_writer and cls.LEVELS.get(level, 0) >= cls.LEVELS["warning"]:
|
|
209
212
|
cls._error_writer.write(line)
|
|
210
213
|
|
|
211
214
|
@classmethod
|