tina4-python 3.11.32__tar.gz → 3.11.35__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.32 → tina4_python-3.11.35}/.gitignore +0 -4
- {tina4_python-3.11.32 → tina4_python-3.11.35}/PKG-INFO +2 -2
- {tina4_python-3.11.32 → tina4_python-3.11.35}/pyproject.toml +2 -5
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/__init__.py +1 -1
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/core/server.py +5 -57
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/database/connection.py +1 -32
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/database/firebird.py +90 -16
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/debug/__init__.py +3 -6
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/dev_admin/__init__.py +90 -656
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/dev_admin/plan.py +0 -108
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/frond/engine.py +0 -76
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/mcp/tools.py +0 -39
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/orm/model.py +6 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/js/tina4-dev-admin.js +140 -274
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/js/tina4-dev-admin.min.js +140 -274
- tina4_python-3.11.32/tina4_python/docs.py +0 -821
- {tina4_python-3.11.32 → tina4_python-3.11.35}/README.md +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/Testing.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/core/events.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/core/request.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/core/response.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/core/router.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.11.32 → tina4_python-3.11.35}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tina4-python
|
|
3
|
-
Version: 3.11.
|
|
4
|
-
Summary: Tina4
|
|
3
|
+
Version: 3.11.35
|
|
4
|
+
Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
|
|
5
5
|
Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.12
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tina4-python"
|
|
3
|
-
|
|
4
|
-
description = "Tina4
|
|
3
|
+
version = "3.11.35"
|
|
4
|
+
description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
|
|
7
7
|
]
|
|
@@ -55,9 +55,6 @@ dev = [
|
|
|
55
55
|
requires = ["hatchling"]
|
|
56
56
|
build-backend = "hatchling.build"
|
|
57
57
|
|
|
58
|
-
[tool.hatch.version]
|
|
59
|
-
path = "tina4_python/__init__.py"
|
|
60
|
-
|
|
61
58
|
[tool.hatch.build]
|
|
62
59
|
include = ["tina4_python/**/*"]
|
|
63
60
|
|
|
@@ -837,10 +837,7 @@ 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
|
|
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`.
|
|
840
|
+
# Unified SPA dev admin
|
|
844
841
|
response.html("""<!DOCTYPE html>
|
|
845
842
|
<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>
|
|
846
843
|
<body><div id="app" data-framework="python" data-color="#3b82f6"></div>
|
|
@@ -850,19 +847,8 @@ async def _handle_dev_admin(request: Request, response: Response) -> Response:
|
|
|
850
847
|
handler_info = handlers.get(request.path)
|
|
851
848
|
if handler_info and request.method == handler_info[0]:
|
|
852
849
|
try:
|
|
853
|
-
def _resp(data, code=200
|
|
854
|
-
|
|
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):
|
|
850
|
+
def _resp(data, code=200):
|
|
851
|
+
if isinstance(data, str):
|
|
866
852
|
response.status(code).html(data)
|
|
867
853
|
else:
|
|
868
854
|
response.status(code).json(data)
|
|
@@ -1196,14 +1182,8 @@ async def handle(request: Request) -> Response:
|
|
|
1196
1182
|
from tina4_python.dotenv import is_truthy
|
|
1197
1183
|
_is_dev = is_truthy(os.environ.get("TINA4_DEBUG", ""))
|
|
1198
1184
|
|
|
1199
|
-
# Dev admin
|
|
1200
|
-
|
|
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
|
-
):
|
|
1185
|
+
# Dev admin
|
|
1186
|
+
if _is_dev and request.path.startswith("/__dev"):
|
|
1207
1187
|
return await _handle_dev_admin(request, response)
|
|
1208
1188
|
|
|
1209
1189
|
# Swagger
|
|
@@ -1635,38 +1615,6 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1635
1615
|
log_level = os.environ.get("TINA4_LOG_LEVEL", "error" if not is_production else "error")
|
|
1636
1616
|
Log.configure(level=log_level, production=is_production)
|
|
1637
1617
|
|
|
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
|
-
|
|
1670
1618
|
# Ensure folders
|
|
1671
1619
|
_ensure_folders()
|
|
1672
1620
|
|
|
@@ -166,11 +166,6 @@ 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
|
-
|
|
174
169
|
# Query cache — off by default, opt-in via TINA4_DB_CACHE=true
|
|
175
170
|
from tina4_python.dotenv import is_truthy
|
|
176
171
|
self._cache_enabled: bool = is_truthy(os.environ.get("TINA4_DB_CACHE", "false"))
|
|
@@ -313,25 +308,7 @@ class Database:
|
|
|
313
308
|
# ── Pool-aware adapter access ─────────────────────────────
|
|
314
309
|
|
|
315
310
|
def _get_adapter(self) -> DatabaseAdapter:
|
|
316
|
-
"""Get an adapter
|
|
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
|
|
311
|
+
"""Get an adapter — from pool (round-robin) or single connection."""
|
|
335
312
|
if self._pool is not None:
|
|
336
313
|
return self._pool.checkout()
|
|
337
314
|
return self._adapter
|
|
@@ -445,24 +422,16 @@ class Database:
|
|
|
445
422
|
return adapter.delete(table, filter_sql, params)
|
|
446
423
|
|
|
447
424
|
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."""
|
|
451
425
|
adapter = self._get_adapter()
|
|
452
|
-
self._tx_local.adapter = adapter
|
|
453
426
|
adapter.start_transaction()
|
|
454
427
|
|
|
455
428
|
def commit(self):
|
|
456
|
-
"""Commit the current transaction and release the adapter pin."""
|
|
457
429
|
adapter = self._get_adapter()
|
|
458
430
|
adapter.commit()
|
|
459
|
-
self._tx_local.adapter = None
|
|
460
431
|
|
|
461
432
|
def rollback(self):
|
|
462
|
-
"""Roll back the current transaction and release the adapter pin."""
|
|
463
433
|
adapter = self._get_adapter()
|
|
464
434
|
adapter.rollback()
|
|
465
|
-
self._tx_local.adapter = None
|
|
466
435
|
|
|
467
436
|
def table_exists(self, name: str) -> bool:
|
|
468
437
|
adapter = self._get_adapter()
|
|
@@ -28,10 +28,27 @@ except ImportError:
|
|
|
28
28
|
class FirebirdAdapter(DatabaseAdapter):
|
|
29
29
|
"""Firebird database driver using firebird-driver or fdb."""
|
|
30
30
|
|
|
31
|
+
# Substring markers (lowercased) that identify a dead-socket Firebird
|
|
32
|
+
# error worth reconnecting for. Idle Firebird connections die silently
|
|
33
|
+
# behind NAT timeouts, server-side ConnectionIdleTimeout, or Docker
|
|
34
|
+
# network rotation; without this the next prepare() crashes the request.
|
|
35
|
+
_DEAD_CONN_MARKERS = (
|
|
36
|
+
"error writing data to the connection",
|
|
37
|
+
"error reading data from the connection",
|
|
38
|
+
"connection shutdown",
|
|
39
|
+
"connection lost",
|
|
40
|
+
"network error",
|
|
41
|
+
"connection is not active",
|
|
42
|
+
"broken pipe",
|
|
43
|
+
)
|
|
44
|
+
|
|
31
45
|
def __init__(self):
|
|
32
46
|
super().__init__()
|
|
33
47
|
self._conn = None
|
|
34
48
|
self._in_transaction: bool = False
|
|
49
|
+
# Remembered connection params — populated by connect(), used by
|
|
50
|
+
# _reconnect() when a dead socket is detected mid-request.
|
|
51
|
+
self._connect_params: dict | None = None
|
|
35
52
|
|
|
36
53
|
def connect(self, connection_string: str, username: str = "", password: str = "", **kwargs):
|
|
37
54
|
"""Connect to Firebird.
|
|
@@ -54,28 +71,82 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
54
71
|
password = parsed.password or password or "masterkey"
|
|
55
72
|
charset = kwargs.pop("charset", "UTF8")
|
|
56
73
|
|
|
74
|
+
# Cache for transparent reconnect — never logged, lives only in
|
|
75
|
+
# adapter memory alongside the connection it owns.
|
|
76
|
+
self._connect_params = {
|
|
77
|
+
"host": host, "port": port, "db_path": db_path,
|
|
78
|
+
"user": user, "password": password, "charset": charset,
|
|
79
|
+
"extra": dict(kwargs),
|
|
80
|
+
}
|
|
81
|
+
self._open()
|
|
82
|
+
|
|
83
|
+
def _open(self) -> None:
|
|
84
|
+
"""Open the underlying Firebird connection from cached params."""
|
|
85
|
+
p = self._connect_params
|
|
86
|
+
if p is None:
|
|
87
|
+
raise RuntimeError("FirebirdAdapter._open called before connect()")
|
|
88
|
+
|
|
57
89
|
if _driver_name == "firebird-driver":
|
|
58
90
|
# Modern firebird-driver uses dsn format: host/port:path
|
|
59
|
-
dsn = f"{host}/{port}:{db_path}" if port != 3050 else f"{host}:{db_path}"
|
|
91
|
+
dsn = f"{p['host']}/{p['port']}:{p['db_path']}" if p['port'] != 3050 else f"{p['host']}:{p['db_path']}"
|
|
60
92
|
self._conn = _driver.connect(
|
|
61
93
|
dsn,
|
|
62
|
-
user=user,
|
|
63
|
-
password=password,
|
|
64
|
-
charset=charset,
|
|
65
|
-
**
|
|
94
|
+
user=p["user"],
|
|
95
|
+
password=p["password"],
|
|
96
|
+
charset=p["charset"],
|
|
97
|
+
**p["extra"],
|
|
66
98
|
)
|
|
67
99
|
else:
|
|
68
100
|
# Legacy fdb
|
|
69
101
|
self._conn = _driver.connect(
|
|
70
|
-
host=host,
|
|
71
|
-
port=port,
|
|
72
|
-
database=db_path,
|
|
73
|
-
user=user,
|
|
74
|
-
password=password,
|
|
75
|
-
charset=charset,
|
|
76
|
-
**
|
|
102
|
+
host=p["host"],
|
|
103
|
+
port=p["port"],
|
|
104
|
+
database=p["db_path"],
|
|
105
|
+
user=p["user"],
|
|
106
|
+
password=p["password"],
|
|
107
|
+
charset=p["charset"],
|
|
108
|
+
**p["extra"],
|
|
77
109
|
)
|
|
78
110
|
|
|
111
|
+
@classmethod
|
|
112
|
+
def _is_dead_connection(cls, exc: BaseException) -> bool:
|
|
113
|
+
"""Match dead-socket error messages from firebird-driver / fdb.
|
|
114
|
+
Substring + case-insensitive so we catch both driver wording variants.
|
|
115
|
+
"""
|
|
116
|
+
msg = str(exc).lower()
|
|
117
|
+
return any(m in msg for m in cls._DEAD_CONN_MARKERS)
|
|
118
|
+
|
|
119
|
+
def _reconnect(self) -> None:
|
|
120
|
+
"""Force-close any stale handle and reopen. Safe to call repeatedly;
|
|
121
|
+
idempotent on a dead connection."""
|
|
122
|
+
try:
|
|
123
|
+
if self._conn is not None:
|
|
124
|
+
self._conn.close()
|
|
125
|
+
except Exception:
|
|
126
|
+
pass # connection already gone — nothing to clean up
|
|
127
|
+
self._conn = None
|
|
128
|
+
self._in_transaction = False
|
|
129
|
+
self._open()
|
|
130
|
+
|
|
131
|
+
def _safe_cursor_execute(self, cursor, sql: str, params: list | None):
|
|
132
|
+
"""Execute on a cursor with one transparent reconnect+retry on
|
|
133
|
+
dead-connection errors. Skipped inside an explicit transaction —
|
|
134
|
+
atomicity beats resilience there; the caller handles rollback.
|
|
135
|
+
|
|
136
|
+
Returns the cursor (possibly a fresh one after reconnect) so the
|
|
137
|
+
caller can fetch results from it.
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
cursor.execute(sql, params or [])
|
|
141
|
+
return cursor
|
|
142
|
+
except Exception as e:
|
|
143
|
+
if not self._is_dead_connection(e) or self._in_transaction:
|
|
144
|
+
raise
|
|
145
|
+
self._reconnect()
|
|
146
|
+
cursor = self._conn.cursor()
|
|
147
|
+
cursor.execute(sql, params or [])
|
|
148
|
+
return cursor
|
|
149
|
+
|
|
79
150
|
def close(self):
|
|
80
151
|
if self._conn:
|
|
81
152
|
self._conn.close()
|
|
@@ -92,7 +163,7 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
92
163
|
sql = sql[:returning_match.start()]
|
|
93
164
|
|
|
94
165
|
cursor = self._conn.cursor()
|
|
95
|
-
cursor.
|
|
166
|
+
cursor = self._safe_cursor_execute(cursor, sql, params)
|
|
96
167
|
|
|
97
168
|
records = []
|
|
98
169
|
last_id = None
|
|
@@ -145,16 +216,19 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
145
216
|
# Count total rows
|
|
146
217
|
count_sql = f"SELECT COUNT(*) FROM ({sql})"
|
|
147
218
|
try:
|
|
148
|
-
cursor.
|
|
219
|
+
cursor = self._safe_cursor_execute(cursor, count_sql, params)
|
|
149
220
|
total = cursor.fetchone()[0]
|
|
150
221
|
except Exception:
|
|
151
222
|
total = 0
|
|
223
|
+
# Reconnect may have just happened — get a fresh cursor for the
|
|
224
|
+
# paginated query below regardless of whether count succeeded.
|
|
225
|
+
cursor = self._conn.cursor()
|
|
152
226
|
|
|
153
227
|
# Apply Firebird pagination — ROWS start TO end
|
|
154
228
|
start = offset + 1
|
|
155
229
|
end = offset + limit
|
|
156
230
|
paginated_sql = f"{sql} ROWS {start} TO {end}"
|
|
157
|
-
cursor.
|
|
231
|
+
cursor = self._safe_cursor_execute(cursor, paginated_sql, params)
|
|
158
232
|
|
|
159
233
|
desc = cursor.description
|
|
160
234
|
col_names = [d[0].strip().lower() for d in desc] if desc else []
|
|
@@ -165,7 +239,7 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
165
239
|
def fetch_one(self, sql: str, params: list = None) -> dict | None:
|
|
166
240
|
sql = self._translate_sql(sql)
|
|
167
241
|
cursor = self._conn.cursor()
|
|
168
|
-
cursor.
|
|
242
|
+
cursor = self._safe_cursor_execute(cursor, sql, params)
|
|
169
243
|
desc = cursor.description
|
|
170
244
|
row = cursor.fetchone()
|
|
171
245
|
if row is None:
|
|
@@ -200,15 +200,12 @@ 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
|
|
203
|
+
# Always write ALL levels to file (raw log, no filtering)
|
|
204
204
|
if cls._writer:
|
|
205
205
|
cls._writer.write(line)
|
|
206
206
|
|
|
207
|
-
#
|
|
208
|
-
|
|
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"]:
|
|
207
|
+
# Write errors to separate file
|
|
208
|
+
if level == "error" and cls._error_writer:
|
|
212
209
|
cls._error_writer.write(line)
|
|
213
210
|
|
|
214
211
|
@classmethod
|