tina4-python 3.13.12__tar.gz → 3.13.14__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.12 → tina4_python-3.13.14}/PKG-INFO +1 -1
- {tina4_python-3.13.12 → tina4_python-3.13.14}/pyproject.toml +1 -1
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/core/middleware.py +9 -4
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/core/server.py +37 -1
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/database/adapter.py +21 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/database/mssql.py +16 -5
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/database/mysql.py +8 -2
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/database/postgres.py +30 -10
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/database/sqlite.py +16 -3
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/debug/__init__.py +12 -4
- {tina4_python-3.13.12 → tina4_python-3.13.14}/.gitignore +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/README.md +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/env.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -361,9 +361,15 @@ class RequestLoggerMiddleware:
|
|
|
361
361
|
"""Request logger — stamps start time before the handler and logs elapsed time after.
|
|
362
362
|
|
|
363
363
|
Mirrors the PHP, Ruby and Node.js RequestLogger classes.
|
|
364
|
+
|
|
365
|
+
v3.13.14: routes through the Tina4 ``Log`` class (was stdlib
|
|
366
|
+
``logging.getLogger``, whose ``info()`` is silently dropped by an
|
|
367
|
+
unconfigured root logger — so these lines never reached stdout).
|
|
368
|
+
The dev server also logs every request globally (see
|
|
369
|
+
``server._finalize_response``); this middleware remains for callers
|
|
370
|
+
that want per-route request logging.
|
|
364
371
|
"""
|
|
365
372
|
|
|
366
|
-
_logger = logging.getLogger("tina4.request")
|
|
367
373
|
_start_times: dict = {}
|
|
368
374
|
_lock = threading.Lock()
|
|
369
375
|
|
|
@@ -385,7 +391,6 @@ class RequestLoggerMiddleware:
|
|
|
385
391
|
method = getattr(request, "method", "?")
|
|
386
392
|
path = getattr(request, "url", None) or getattr(request, "path", "/")
|
|
387
393
|
status = getattr(response, "status_code", None) or getattr(response, "status", 0)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
)
|
|
394
|
+
from tina4_python.debug import Log
|
|
395
|
+
Log.info(f"{method} {path} -> {status} ({elapsed_ms}ms)")
|
|
391
396
|
return request, response
|
|
@@ -1404,6 +1404,21 @@ def _handle_no_route(request: Request, response: Response, request_id: str) -> R
|
|
|
1404
1404
|
return response
|
|
1405
1405
|
|
|
1406
1406
|
|
|
1407
|
+
def _request_logging_enabled(is_dev: bool) -> bool:
|
|
1408
|
+
"""Whether to emit a per-request log line (v3.13.14).
|
|
1409
|
+
|
|
1410
|
+
``TINA4_LOG_REQUESTS`` is the explicit control (true/false). When unset,
|
|
1411
|
+
request logging follows dev mode: on under ``TINA4_DEBUG``, off in
|
|
1412
|
+
production (so prod doesn't pay the per-request logging cost unless the
|
|
1413
|
+
operator opts in). Same contract across all four frameworks.
|
|
1414
|
+
"""
|
|
1415
|
+
from tina4_python.dotenv import is_truthy
|
|
1416
|
+
val = os.environ.get("TINA4_LOG_REQUESTS")
|
|
1417
|
+
if val is not None and val != "":
|
|
1418
|
+
return is_truthy(val)
|
|
1419
|
+
return is_dev
|
|
1420
|
+
|
|
1421
|
+
|
|
1407
1422
|
def _finalize_response(
|
|
1408
1423
|
request: Request, response: Response, route: dict | None,
|
|
1409
1424
|
request_id: str, is_dev: bool, req_start: float,
|
|
@@ -1444,6 +1459,22 @@ def _finalize_response(
|
|
|
1444
1459
|
except Exception:
|
|
1445
1460
|
pass
|
|
1446
1461
|
|
|
1462
|
+
# Request log line (v3.13.14). The dev dashboard's RequestInspector above
|
|
1463
|
+
# only feeds the /__dev UI — it never reached stdout, so `tina4 serve`
|
|
1464
|
+
# printed the startup banner then went silent even as requests flowed.
|
|
1465
|
+
# Emit a per-request line through the Tina4 Log so it lands on stdout
|
|
1466
|
+
# (docker logs / k8s) like every other log. On by default in dev; opt-in
|
|
1467
|
+
# in production via TINA4_LOG_REQUESTS to avoid per-request overhead.
|
|
1468
|
+
if _request_logging_enabled(is_dev):
|
|
1469
|
+
try:
|
|
1470
|
+
import time as _time
|
|
1471
|
+
elapsed_ms = round((_time.perf_counter() - req_start) * 1000, 3)
|
|
1472
|
+
Log.info(
|
|
1473
|
+
f"{request.method} {request.path} -> {response.status_code} ({elapsed_ms}ms)"
|
|
1474
|
+
)
|
|
1475
|
+
except Exception:
|
|
1476
|
+
pass
|
|
1477
|
+
|
|
1447
1478
|
# Session save + cookie.
|
|
1448
1479
|
# Skip saving if this was a brand-new session that the route never wrote to —
|
|
1449
1480
|
# that prevents empty orphaned session files accumulating on disk.
|
|
@@ -2081,7 +2112,12 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
2081
2112
|
|
|
2082
2113
|
# Init logger
|
|
2083
2114
|
is_production = os.environ.get("TINA4_ENV", "development") == "production"
|
|
2084
|
-
|
|
2115
|
+
# v3.13.14: default level is INFO (was ERROR). ERROR-by-default meant a
|
|
2116
|
+
# deployed app that logged at info/debug appeared silent — operators
|
|
2117
|
+
# "weren't getting logs". INFO shows request/startup/warn/error without
|
|
2118
|
+
# debug noise, and matches PHP/Ruby/Node defaults. Override per-deploy
|
|
2119
|
+
# with TINA4_LOG_LEVEL.
|
|
2120
|
+
log_level = os.environ.get("TINA4_LOG_LEVEL", "info")
|
|
2085
2121
|
Log.configure(level=log_level, production=is_production)
|
|
2086
2122
|
|
|
2087
2123
|
# Install a top-level exception hook so uncaught exceptions bubbling
|
|
@@ -375,6 +375,27 @@ class DatabaseAdapter:
|
|
|
375
375
|
stripped = stripped[:-1].rstrip()
|
|
376
376
|
return stripped
|
|
377
377
|
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _split_schema(name: str) -> tuple[str | None, str]:
|
|
380
|
+
"""Split a possibly-qualified table name into (schema, table).
|
|
381
|
+
|
|
382
|
+
v3.13.14 (#48): a model whose ``table_name`` is qualified —
|
|
383
|
+
PostgreSQL ``gift_cards.gift_card``, MSSQL ``dbo.widget``,
|
|
384
|
+
MySQL ``otherdb.table``, SQLite ``attached.table`` — lives in
|
|
385
|
+
that schema/catalog, not the default. Each adapter's
|
|
386
|
+
``table_exists`` / ``get_columns`` use this to query the right
|
|
387
|
+
namespace instead of matching the whole dotted string as one
|
|
388
|
+
flat table name. Returns ``(None, name)`` for a bare name so
|
|
389
|
+
callers default to the engine's current namespace. Splits on the
|
|
390
|
+
first dot; quoted identifiers with embedded dots aren't supported
|
|
391
|
+
(they weren't before either). Firebird has no schemas, so its
|
|
392
|
+
adapter ignores this.
|
|
393
|
+
"""
|
|
394
|
+
if "." in name:
|
|
395
|
+
schema, _, table = name.partition(".")
|
|
396
|
+
return schema, table
|
|
397
|
+
return None, name
|
|
398
|
+
|
|
378
399
|
def fetch(self, sql: str, params: list = None,
|
|
379
400
|
limit: int = 100, offset: int = 0) -> DatabaseResult:
|
|
380
401
|
"""Execute a read query and return multiple rows."""
|
|
@@ -190,10 +190,17 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
190
190
|
self._in_transaction = False
|
|
191
191
|
|
|
192
192
|
def table_exists(self, name: str) -> bool:
|
|
193
|
+
# v3.13.14 (#48): honour a schema-qualified name ("dbo.widget",
|
|
194
|
+
# "sales.orders"). Schemas are everyday in SQL Server; pre-fix this
|
|
195
|
+
# had no schema filter and matched the whole dotted string as a flat
|
|
196
|
+
# TABLE_NAME, so any qualified table_name was invisible. A bare name
|
|
197
|
+
# still matches in any schema (unchanged behaviour).
|
|
198
|
+
schema, tbl = self._split_schema(name)
|
|
193
199
|
row = self.fetch_one(
|
|
194
200
|
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES "
|
|
195
|
-
"WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = %s"
|
|
196
|
-
|
|
201
|
+
"WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = %s "
|
|
202
|
+
"AND (%s IS NULL OR TABLE_SCHEMA = %s)",
|
|
203
|
+
[tbl, schema, schema],
|
|
197
204
|
)
|
|
198
205
|
return row is not None
|
|
199
206
|
|
|
@@ -206,6 +213,9 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
206
213
|
return [r["TABLE_NAME"] for r in result.records]
|
|
207
214
|
|
|
208
215
|
def get_columns(self, table: str) -> list[dict]:
|
|
216
|
+
# v3.13.14 (#48): honour a schema-qualified name; a bare name matches
|
|
217
|
+
# in any schema (unchanged).
|
|
218
|
+
schema, tbl = self._split_schema(table)
|
|
209
219
|
sql = """
|
|
210
220
|
SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT,
|
|
211
221
|
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS is_primary
|
|
@@ -215,12 +225,13 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
215
225
|
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
216
226
|
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku
|
|
217
227
|
ON tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
|
|
218
|
-
WHERE tc.TABLE_NAME = %s AND tc.
|
|
228
|
+
WHERE tc.TABLE_NAME = %s AND (%s IS NULL OR tc.TABLE_SCHEMA = %s)
|
|
229
|
+
AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
|
219
230
|
) pk ON c.COLUMN_NAME = pk.COLUMN_NAME
|
|
220
|
-
WHERE c.TABLE_NAME = %s
|
|
231
|
+
WHERE c.TABLE_NAME = %s AND (%s IS NULL OR c.TABLE_SCHEMA = %s)
|
|
221
232
|
ORDER BY c.ORDINAL_POSITION
|
|
222
233
|
"""
|
|
223
|
-
result = self.fetch(sql, [
|
|
234
|
+
result = self.fetch(sql, [tbl, schema, schema, tbl, schema, schema], limit=10000)
|
|
224
235
|
return [
|
|
225
236
|
{
|
|
226
237
|
"name": r["COLUMN_NAME"],
|
|
@@ -174,10 +174,16 @@ class MySQLAdapter(DatabaseAdapter):
|
|
|
174
174
|
self._in_transaction = False
|
|
175
175
|
|
|
176
176
|
def table_exists(self, name: str) -> bool:
|
|
177
|
+
# v3.13.14 (#48): honour a database-qualified name ("otherdb.table").
|
|
178
|
+
# In MySQL "schema" == database; default to the connected database.
|
|
179
|
+
# Pre-fix this matched the whole dotted string as a flat TABLE_NAME
|
|
180
|
+
# under DATABASE() only, so a cross-database qualified name was never
|
|
181
|
+
# found and create_table()/migrations misfired.
|
|
182
|
+
schema, tbl = self._split_schema(name)
|
|
177
183
|
row = self.fetch_one(
|
|
178
184
|
"SELECT TABLE_NAME FROM information_schema.TABLES "
|
|
179
|
-
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s",
|
|
180
|
-
[
|
|
185
|
+
"WHERE TABLE_SCHEMA = COALESCE(%s, DATABASE()) AND TABLE_NAME = %s",
|
|
186
|
+
[schema, tbl],
|
|
181
187
|
)
|
|
182
188
|
return row is not None
|
|
183
189
|
|
|
@@ -423,20 +423,40 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
423
423
|
self._in_transaction = False
|
|
424
424
|
|
|
425
425
|
def table_exists(self, name: str) -> bool:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
426
|
+
# v3.13.x (#48): support schema-qualified names ("schema.table") and the
|
|
427
|
+
# connection search_path. to_regclass() resolves a (possibly qualified)
|
|
428
|
+
# relation name to its OID using the SAME rules Postgres applies to a
|
|
429
|
+
# FROM clause, returning NULL when absent. Pre-fix this hardcoded
|
|
430
|
+
# schemaname='public' AND matched the whole dotted string as a flat
|
|
431
|
+
# tablename, so a table in a non-public schema was invisible —
|
|
432
|
+
# table_exists() always returned False, so create_table()/migrations
|
|
433
|
+
# misfired and ORM introspection saw nothing.
|
|
434
|
+
row = self.fetch_one("SELECT to_regclass(%s) AS oid", [name])
|
|
435
|
+
return bool(row and row.get("oid") is not None)
|
|
431
436
|
|
|
432
437
|
def get_tables(self) -> list[str]:
|
|
438
|
+
# v3.13.x (#48): list tables from every user schema, not just public.
|
|
439
|
+
# public tables stay bare ("users"); tables in other schemas are
|
|
440
|
+
# returned schema-qualified ("gift_cards.gift_card") so the names round
|
|
441
|
+
# -trip back through table_exists()/get_columns().
|
|
433
442
|
result = self.fetch(
|
|
434
|
-
"
|
|
443
|
+
"""
|
|
444
|
+
SELECT schemaname, tablename FROM pg_tables
|
|
445
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
446
|
+
ORDER BY schemaname, tablename
|
|
447
|
+
""",
|
|
435
448
|
limit=10000,
|
|
436
449
|
)
|
|
437
|
-
return [
|
|
450
|
+
return [
|
|
451
|
+
r["tablename"] if r["schemaname"] == "public"
|
|
452
|
+
else f"{r['schemaname']}.{r['tablename']}"
|
|
453
|
+
for r in result.records
|
|
454
|
+
]
|
|
438
455
|
|
|
439
456
|
def get_columns(self, table: str) -> list[dict]:
|
|
457
|
+
# v3.13.x (#48): honour a schema-qualified name; default to public.
|
|
458
|
+
schema, tbl = self._split_schema(table)
|
|
459
|
+
schema = schema or "public"
|
|
440
460
|
sql = """
|
|
441
461
|
SELECT c.column_name, c.data_type, c.is_nullable, c.column_default,
|
|
442
462
|
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END AS is_primary
|
|
@@ -446,12 +466,12 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
446
466
|
FROM information_schema.table_constraints tc
|
|
447
467
|
JOIN information_schema.key_column_usage ku
|
|
448
468
|
ON tc.constraint_name = ku.constraint_name
|
|
449
|
-
WHERE tc.table_name = %s AND tc.constraint_type = 'PRIMARY KEY'
|
|
469
|
+
WHERE tc.table_name = %s AND tc.table_schema = %s AND tc.constraint_type = 'PRIMARY KEY'
|
|
450
470
|
) pk ON c.column_name = pk.column_name
|
|
451
|
-
WHERE c.table_name = %s AND c.table_schema =
|
|
471
|
+
WHERE c.table_name = %s AND c.table_schema = %s
|
|
452
472
|
ORDER BY c.ordinal_position
|
|
453
473
|
"""
|
|
454
|
-
result = self.fetch(sql, [
|
|
474
|
+
result = self.fetch(sql, [tbl, schema, tbl, schema], limit=10000)
|
|
455
475
|
return [
|
|
456
476
|
{
|
|
457
477
|
"name": r["column_name"],
|
|
@@ -179,9 +179,16 @@ class SQLiteAdapter(DatabaseAdapter):
|
|
|
179
179
|
self._in_transaction = False
|
|
180
180
|
|
|
181
181
|
def table_exists(self, name: str) -> bool:
|
|
182
|
+
# v3.13.14 (#48): honour an attached-database prefix ("attached.table").
|
|
183
|
+
# SQLite's "schema" is an ATTACH alias; each has its own sqlite_master.
|
|
184
|
+
# Bare names use the main database (unchanged). The alias must be a
|
|
185
|
+
# plain identifier (it's developer-supplied, but guard against the dot
|
|
186
|
+
# being part of an odd name).
|
|
187
|
+
schema, tbl = self._split_schema(name)
|
|
188
|
+
master = f"{schema}.sqlite_master" if schema and schema.isidentifier() else "sqlite_master"
|
|
182
189
|
row = self.fetch_one(
|
|
183
|
-
"SELECT name FROM
|
|
184
|
-
[name]
|
|
190
|
+
f"SELECT name FROM {master} WHERE type='table' AND name=?",
|
|
191
|
+
[tbl if (schema and schema.isidentifier()) else name]
|
|
185
192
|
)
|
|
186
193
|
return row is not None
|
|
187
194
|
|
|
@@ -193,7 +200,13 @@ class SQLiteAdapter(DatabaseAdapter):
|
|
|
193
200
|
return [r["name"] for r in result.records]
|
|
194
201
|
|
|
195
202
|
def get_columns(self, table: str) -> list[dict]:
|
|
196
|
-
|
|
203
|
+
# v3.13.14 (#48): honour an attached-database prefix via
|
|
204
|
+
# `PRAGMA <schema>.table_info(<table>)`.
|
|
205
|
+
schema, tbl = self._split_schema(table)
|
|
206
|
+
if schema and schema.isidentifier() and tbl.isidentifier():
|
|
207
|
+
cursor = self._conn.execute(f"PRAGMA {schema}.table_info({tbl})")
|
|
208
|
+
else:
|
|
209
|
+
cursor = self._conn.execute(f"PRAGMA table_info({table})")
|
|
197
210
|
return [
|
|
198
211
|
{
|
|
199
212
|
"name": row["name"],
|
|
@@ -412,10 +412,18 @@ class Log:
|
|
|
412
412
|
# File always gets ALL levels (no filtering for file output)
|
|
413
413
|
line = cls._format_line(level, message, **kwargs)
|
|
414
414
|
|
|
415
|
-
# Console output respects TINA4_LOG_LEVEL and the stdout toggle
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
415
|
+
# Console output respects TINA4_LOG_LEVEL and the stdout toggle.
|
|
416
|
+
# v3.13.14: stdout is NOT suppressed in production — containers
|
|
417
|
+
# treat stdout as the canonical log sink (docker logs / k8s read
|
|
418
|
+
# PID 1 stdout), and the pre-v3.13.14 `not _is_production` gate
|
|
419
|
+
# meant deployed containers got nothing. Production still emits
|
|
420
|
+
# JSON (see _format_line / _is_production), just on stdout now.
|
|
421
|
+
# flush=True so logs appear immediately on a non-TTY pipe rather
|
|
422
|
+
# than sitting in Python's block buffer until the process exits.
|
|
423
|
+
if cls._stdout_enabled and cls._should_log(level):
|
|
424
|
+
color = "" if cls._is_production else cls.COLORS.get(level, "")
|
|
425
|
+
reset = "" if cls._is_production else cls.RESET
|
|
426
|
+
print(f"{color}{line}{reset}", flush=True)
|
|
419
427
|
|
|
420
428
|
# Always write ALL levels to the main file (raw log, no filtering)
|
|
421
429
|
if cls._writer:
|
|
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
|
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/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.12 → tina4_python-3.13.14}/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
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/queue_backends/rabbitmq_backend.py
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
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/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.12 → tina4_python-3.13.14}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.14}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|