tina4-python 3.13.10__tar.gz → 3.13.11__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.10 → tina4_python-3.13.11}/PKG-INFO +1 -1
- {tina4_python-3.13.10 → tina4_python-3.13.11}/pyproject.toml +1 -1
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/database/connection.py +21 -3
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/database/postgres.py +50 -2
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/orm/fields.py +25 -1
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/orm/model.py +83 -10
- {tina4_python-3.13.10 → tina4_python-3.13.11}/.gitignore +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/README.md +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/env.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -421,7 +421,14 @@ class Database:
|
|
|
421
421
|
|
|
422
422
|
def fetch(self, sql: str, params: list = None,
|
|
423
423
|
limit: int = 100, offset: int = 0) -> DatabaseResult:
|
|
424
|
-
"""Fetch rows with pagination.
|
|
424
|
+
"""Fetch rows with pagination.
|
|
425
|
+
|
|
426
|
+
v3.13.11 (issue #49 Gap 3): mirror :meth:`execute` and capture
|
|
427
|
+
``last_error`` here too — pre-v3.13.11 a failed ``fetch`` left
|
|
428
|
+
``db.get_error()`` returning ``None`` even though the adapter
|
|
429
|
+
had stored the failure on its own ``last_error``. Callers had
|
|
430
|
+
no way to read the cause via the public API.
|
|
431
|
+
"""
|
|
425
432
|
if self._cache_enabled:
|
|
426
433
|
key = self._cache_key(sql + f":L{limit}:S{offset}", params)
|
|
427
434
|
cached = self._cache_get(key)
|
|
@@ -430,13 +437,24 @@ class Database:
|
|
|
430
437
|
self._cache_hits += 1
|
|
431
438
|
return cached
|
|
432
439
|
adapter = self._get_adapter()
|
|
433
|
-
|
|
440
|
+
try:
|
|
441
|
+
result = adapter.fetch(sql, params, limit, offset)
|
|
442
|
+
self.last_error = None
|
|
443
|
+
except Exception:
|
|
444
|
+
self.last_error = getattr(adapter, "last_error", None) or self.last_error
|
|
445
|
+
raise
|
|
434
446
|
self._cache_set(key, result)
|
|
435
447
|
with self._cache_lock:
|
|
436
448
|
self._cache_misses += 1
|
|
437
449
|
return result
|
|
438
450
|
adapter = self._get_adapter()
|
|
439
|
-
|
|
451
|
+
try:
|
|
452
|
+
result = adapter.fetch(sql, params, limit, offset)
|
|
453
|
+
self.last_error = None
|
|
454
|
+
return result
|
|
455
|
+
except Exception:
|
|
456
|
+
self.last_error = getattr(adapter, "last_error", None) or self.last_error
|
|
457
|
+
raise
|
|
440
458
|
|
|
441
459
|
def fetch_all(self, sql: str, params: list = None,
|
|
442
460
|
limit: int = 100, offset: int = 0) -> list[dict]:
|
|
@@ -137,7 +137,16 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
137
137
|
raise
|
|
138
138
|
|
|
139
139
|
def _on_query_error(self, exc: Exception, sql: str, params=None):
|
|
140
|
-
"""Handle a failed query: log, store, auto-rollback if implicit.
|
|
140
|
+
"""Handle a failed query: log, store, auto-rollback if implicit.
|
|
141
|
+
|
|
142
|
+
v3.13.11 (issue #49 Gap 1): inside an explicit transaction the
|
|
143
|
+
auto-rollback step defers to the user — they may be running a
|
|
144
|
+
SAVEPOINT/retry pattern. The visibility half stays: the original
|
|
145
|
+
cause is still logged via ``Log.error``, and an additional
|
|
146
|
+
``Log.warning`` makes the context explicit so a tail-the-log
|
|
147
|
+
operator can spot the upstream cause that's about to be buried
|
|
148
|
+
by a downstream ``InFailedSqlTransaction`` cascade.
|
|
149
|
+
"""
|
|
141
150
|
# Lazy import — avoid circular import at module load and let the
|
|
142
151
|
# framework boot without Log if something exotic is happening.
|
|
143
152
|
try:
|
|
@@ -148,6 +157,15 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
148
157
|
sql=preview,
|
|
149
158
|
params=params,
|
|
150
159
|
)
|
|
160
|
+
# v3.13.11: extra in-txn marker so the original cause is
|
|
161
|
+
# easy to spot above the cascade line.
|
|
162
|
+
if self._in_transaction:
|
|
163
|
+
Log.warning(
|
|
164
|
+
"Above failure occurred inside an explicit transaction — "
|
|
165
|
+
"auto-rollback skipped (caller owns the transaction). "
|
|
166
|
+
"Subsequent queries on this connection will cascade with "
|
|
167
|
+
"'current transaction is aborted' until rollback() is called."
|
|
168
|
+
)
|
|
151
169
|
except Exception:
|
|
152
170
|
pass
|
|
153
171
|
|
|
@@ -166,6 +184,29 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
166
184
|
# Don't mask the original error if rollback itself fails.
|
|
167
185
|
pass
|
|
168
186
|
|
|
187
|
+
def _log_silent_probe_failure(self, exc: Exception, sql: str, params=None):
|
|
188
|
+
"""v3.13.11 (issue #49 Gap 1): log a probe-query failure as a
|
|
189
|
+
warning WITHOUT touching ``last_error`` or rolling back.
|
|
190
|
+
|
|
191
|
+
Used by the COUNT probe in :meth:`fetch` which is best-effort —
|
|
192
|
+
it's allowed to fail without surfacing to the caller — but inside
|
|
193
|
+
an explicit transaction the connection is now poisoned, the
|
|
194
|
+
paginated query that follows will cascade, and the real cause
|
|
195
|
+
used to be lost entirely. This logs it so an operator scrolling
|
|
196
|
+
back through the log can spot the upstream cause.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
from tina4_python.debug import Log
|
|
200
|
+
preview = sql.strip().splitlines()[0][:200] if sql else "<no sql>"
|
|
201
|
+
Log.warning(
|
|
202
|
+
f"PostgreSQL probe query failed (original cause, before "
|
|
203
|
+
f"any cascade): {exc.__class__.__name__}: {exc}",
|
|
204
|
+
sql=preview,
|
|
205
|
+
params=params,
|
|
206
|
+
)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
169
210
|
def connect(self, connection_string: str, username: str = "", password: str = "", **kwargs):
|
|
170
211
|
"""Connect to PostgreSQL.
|
|
171
212
|
|
|
@@ -280,8 +321,15 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
280
321
|
try:
|
|
281
322
|
self._safe_execute(cursor, count_sql, params)
|
|
282
323
|
total = cursor.fetchone()["cnt"]
|
|
283
|
-
except Exception:
|
|
324
|
+
except Exception as e:
|
|
284
325
|
total = 0
|
|
326
|
+
# v3.13.11 (issue #49 Gap 1): log the probe failure as a
|
|
327
|
+
# warning so the original cause appears in the log even
|
|
328
|
+
# though the probe itself swallows. Without this line, a
|
|
329
|
+
# failure inside an explicit transaction is invisible —
|
|
330
|
+
# the cascade message from the paginated query below is
|
|
331
|
+
# all the operator sees.
|
|
332
|
+
self._log_silent_probe_failure(e, count_sql, params)
|
|
285
333
|
# If the probe failed because the connection arrived (or
|
|
286
334
|
# became) aborted, roll back so the real pagination query
|
|
287
335
|
# below sees a clean txn. Without this we'd cascade.
|
|
@@ -51,12 +51,36 @@ class Field:
|
|
|
51
51
|
self.choices = choices
|
|
52
52
|
self.validator = validator # Custom callable(value) → raises ValueError
|
|
53
53
|
|
|
54
|
+
# ── v3.13.11 (issue #50): callable-default resolution ──────────────
|
|
55
|
+
#
|
|
56
|
+
# Pre-v3.13.11 a callable default (e.g. ``DateTimeField(default=lambda:
|
|
57
|
+
# datetime.now())``) reached the driver verbatim and blew up with
|
|
58
|
+
# ``can't adapt type 'function'``. Now we evaluate the callable at the
|
|
59
|
+
# point of use, so every instance gets a freshly computed value (per-row
|
|
60
|
+
# timestamps actually differ). Types are excluded — ``default=int`` is
|
|
61
|
+
# almost never intended to mean "call int()".
|
|
62
|
+
def _resolve_default(self):
|
|
63
|
+
d = self.default
|
|
64
|
+
if callable(d) and not isinstance(d, type):
|
|
65
|
+
return d()
|
|
66
|
+
return d
|
|
67
|
+
|
|
54
68
|
def validate(self, value):
|
|
55
69
|
"""Validate and coerce value to the field type."""
|
|
56
70
|
if value is None:
|
|
57
71
|
if self.required and self.default is None:
|
|
58
72
|
raise ValueError(f"Field '{self.name}' is required")
|
|
59
|
-
return self.
|
|
73
|
+
return self._resolve_default()
|
|
74
|
+
|
|
75
|
+
# v3.13.11 (issue #50): if the user stored a callable as the field
|
|
76
|
+
# value (e.g. ``default=lambda: datetime.now()`` reached us without
|
|
77
|
+
# being resolved on instance init), evaluate it now so the driver
|
|
78
|
+
# gets a real value, not the function object. Types are excluded —
|
|
79
|
+
# ``default=int`` is almost never intended to mean "call int()".
|
|
80
|
+
if callable(value) and not isinstance(value, type):
|
|
81
|
+
value = value()
|
|
82
|
+
if value is None:
|
|
83
|
+
return None
|
|
60
84
|
|
|
61
85
|
# Type coercion.
|
|
62
86
|
#
|
|
@@ -177,9 +177,14 @@ class ORM(metaclass=ORMMeta):
|
|
|
177
177
|
# Initialize relationship cache
|
|
178
178
|
self._rel_cache = {}
|
|
179
179
|
|
|
180
|
-
# Set defaults from field definitions
|
|
180
|
+
# Set defaults from field definitions.
|
|
181
|
+
# v3.13.11 (issue #50): callable defaults are evaluated *per
|
|
182
|
+
# instance* so per-row timestamps (e.g. ``default=lambda:
|
|
183
|
+
# datetime.now()``) actually differ. Pre-v3.13.11 the function
|
|
184
|
+
# object reached the driver verbatim and blew up with
|
|
185
|
+
# ``can't adapt type 'function'``.
|
|
181
186
|
for name, field in self._fields.items():
|
|
182
|
-
setattr(self, name, field.
|
|
187
|
+
setattr(self, name, field._resolve_default())
|
|
183
188
|
|
|
184
189
|
# Accept JSON string or dict
|
|
185
190
|
if isinstance(data, str):
|
|
@@ -304,10 +309,22 @@ class ORM(metaclass=ORMMeta):
|
|
|
304
309
|
# ── CRUD ────────────────────────────────────────────────────
|
|
305
310
|
|
|
306
311
|
def save(self) -> Self | bool:
|
|
307
|
-
"""Insert or update. Returns self on success, False on failure.
|
|
312
|
+
"""Insert or update. Returns self on success, False on failure.
|
|
313
|
+
|
|
314
|
+
v3.13.11 (issue #50): for non-auto-increment primary keys (e.g.
|
|
315
|
+
user-supplied string IDs like ``"GC-100"``), the decision between
|
|
316
|
+
INSERT and UPDATE is made on whether the row *exists*, not on
|
|
317
|
+
whether the PK is set. Pre-v3.13.11 a natural-key save() always
|
|
318
|
+
chose UPDATE — matched zero rows — and silently returned success
|
|
319
|
+
without inserting anything.
|
|
320
|
+
|
|
321
|
+
Auto-increment behaviour is unchanged: ``pk_value is None`` means
|
|
322
|
+
"new row, let the engine assign an ID" and we still INSERT.
|
|
323
|
+
"""
|
|
308
324
|
db = self._get_db()
|
|
309
325
|
pk = self._get_pk()
|
|
310
326
|
pk_value = getattr(self, pk, None)
|
|
327
|
+
pk_field = self._fields[pk]
|
|
311
328
|
table = self._get_table()
|
|
312
329
|
pk_db_col = self.field_mapping.get(pk, self._fields[pk].column)
|
|
313
330
|
|
|
@@ -316,22 +333,52 @@ class ORM(metaclass=ORMMeta):
|
|
|
316
333
|
if field.auto_increment and pk_value is None:
|
|
317
334
|
continue # Skip auto-increment on insert
|
|
318
335
|
value = getattr(self, name)
|
|
336
|
+
# v3.13.11 (issue #50): resolve callable values at write time
|
|
337
|
+
# too, in case the user set ``self.created_at = lambda: ...``
|
|
338
|
+
# directly. Defensive — Field.validate() already handles this
|
|
339
|
+
# for the normal __init__ + _populate paths.
|
|
340
|
+
if callable(value) and not isinstance(value, type):
|
|
341
|
+
value = value()
|
|
319
342
|
if value is not None or not field.auto_increment:
|
|
320
343
|
# Use field_mapping for the column name, fall back to field.column
|
|
321
344
|
db_col = self.field_mapping.get(name, field.column)
|
|
322
345
|
data[db_col] = value
|
|
323
346
|
|
|
347
|
+
# v3.13.11 (issue #50): pick INSERT vs UPDATE on row existence
|
|
348
|
+
# for non-auto-increment PKs. Auto-increment keeps the legacy
|
|
349
|
+
# behaviour (PK is None → INSERT, PK is set → UPDATE).
|
|
350
|
+
is_update = False
|
|
351
|
+
if pk_value is not None:
|
|
352
|
+
if pk_field.auto_increment:
|
|
353
|
+
is_update = True
|
|
354
|
+
else:
|
|
355
|
+
# Natural-key model — check if the row already exists.
|
|
356
|
+
# type(self).exists() handles soft-delete + scope filters
|
|
357
|
+
# the same way the rest of the ORM does.
|
|
358
|
+
try:
|
|
359
|
+
is_update = type(self).exists(pk_value)
|
|
360
|
+
except Exception:
|
|
361
|
+
# If we can't tell (e.g. table doesn't exist yet),
|
|
362
|
+
# fall back to INSERT — the user will see the real
|
|
363
|
+
# error from the driver instead of a silent no-op.
|
|
364
|
+
is_update = False
|
|
365
|
+
|
|
324
366
|
db.start_transaction()
|
|
325
367
|
try:
|
|
326
|
-
if
|
|
368
|
+
if is_update:
|
|
327
369
|
update_data = {k: v for k, v in data.items() if k != pk_db_col}
|
|
328
370
|
if update_data:
|
|
329
371
|
db.update(table, update_data, f"{pk_db_col} = ?", [pk_value])
|
|
330
372
|
else:
|
|
331
373
|
db.insert(table, data)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
374
|
+
# Only adopt the engine-assigned ID for auto-increment PKs.
|
|
375
|
+
# Natural-key PKs were already set by the caller; don't
|
|
376
|
+
# overwrite them with the driver's last_id (which on PG
|
|
377
|
+
# may be a sequence value that doesn't apply here).
|
|
378
|
+
if pk_field.auto_increment:
|
|
379
|
+
last_id = db.get_last_id()
|
|
380
|
+
if last_id and pk in self._fields:
|
|
381
|
+
setattr(self, pk, last_id)
|
|
335
382
|
db.commit()
|
|
336
383
|
except Exception:
|
|
337
384
|
db.rollback()
|
|
@@ -673,10 +720,21 @@ class ORM(metaclass=ORMMeta):
|
|
|
673
720
|
StringField → VARCHAR(255)
|
|
674
721
|
TextField → TEXT
|
|
675
722
|
NumericField/FloatField → REAL
|
|
676
|
-
BooleanField →
|
|
723
|
+
BooleanField → engine-aware (see below)
|
|
677
724
|
DateTimeField → DATETIME
|
|
678
725
|
BlobField → BLOB
|
|
679
726
|
|
|
727
|
+
v3.13.11 BooleanField mapping (engine-aware):
|
|
728
|
+
SQLite → INTEGER (no native bool — historic convention)
|
|
729
|
+
PostgreSQL → BOOLEAN (native type; psycopg2 binds Python
|
|
730
|
+
``bool`` as BOOLEAN, so INTEGER columns
|
|
731
|
+
caused ``operator does not exist:
|
|
732
|
+
boolean = integer``)
|
|
733
|
+
MySQL → BOOLEAN (synonym for TINYINT(1))
|
|
734
|
+
MSSQL → BIT (the SQL Server bool type)
|
|
735
|
+
Firebird → INTEGER (driver round-trip is uneven; integer
|
|
736
|
+
is the safer choice on Firebird)
|
|
737
|
+
|
|
680
738
|
Auto-increment primary keys use engine-appropriate syntax.
|
|
681
739
|
Returns True on success.
|
|
682
740
|
"""
|
|
@@ -684,6 +742,21 @@ class ORM(metaclass=ORMMeta):
|
|
|
684
742
|
|
|
685
743
|
db = cls._get_db()
|
|
686
744
|
table = cls._get_table()
|
|
745
|
+
engine = (db.get_database_type() or "").lower()
|
|
746
|
+
|
|
747
|
+
# v3.13.11: BooleanField now uses each engine's native type
|
|
748
|
+
# where it's reliable. SQLite and Firebird stay on INTEGER —
|
|
749
|
+
# SQLite has no native bool, and Firebird's driver round-trip
|
|
750
|
+
# for native BOOLEAN is uneven across versions.
|
|
751
|
+
if engine == "postgres":
|
|
752
|
+
bool_sql = "BOOLEAN"
|
|
753
|
+
elif engine == "mysql":
|
|
754
|
+
bool_sql = "BOOLEAN" # MySQL alias for TINYINT(1)
|
|
755
|
+
elif engine == "mssql":
|
|
756
|
+
bool_sql = "BIT"
|
|
757
|
+
else:
|
|
758
|
+
# sqlite, firebird, odbc, anything else
|
|
759
|
+
bool_sql = "INTEGER"
|
|
687
760
|
|
|
688
761
|
# Don't recreate if table already exists
|
|
689
762
|
if db.table_exists(table):
|
|
@@ -706,7 +779,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
706
779
|
elif kind in ("NumericField", "FloatField"):
|
|
707
780
|
sql_type = "REAL"
|
|
708
781
|
elif kind == "BooleanField":
|
|
709
|
-
sql_type =
|
|
782
|
+
sql_type = bool_sql
|
|
710
783
|
elif kind == "DateTimeField":
|
|
711
784
|
sql_type = "DATETIME"
|
|
712
785
|
elif kind == "BlobField":
|
|
@@ -719,7 +792,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
719
792
|
elif ft == float:
|
|
720
793
|
sql_type = "REAL"
|
|
721
794
|
elif ft == bool:
|
|
722
|
-
sql_type =
|
|
795
|
+
sql_type = bool_sql
|
|
723
796
|
elif ft == bytes:
|
|
724
797
|
sql_type = "BLOB"
|
|
725
798
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/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.10 → tina4_python-3.13.11}/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.10 → tina4_python-3.13.11}/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.10 → tina4_python-3.13.11}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/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.10 → tina4_python-3.13.11}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.11}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|