tina4-python 3.13.5__tar.gz → 3.13.7__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.
Files changed (157) hide show
  1. {tina4_python-3.13.5 → tina4_python-3.13.7}/PKG-INFO +1 -1
  2. {tina4_python-3.13.5 → tina4_python-3.13.7}/pyproject.toml +1 -1
  3. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/core/server.py +33 -4
  5. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/database/firebird.py +4 -2
  6. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/database/mongodb.py +4 -2
  7. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/database/mssql.py +4 -2
  8. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/database/mysql.py +4 -2
  9. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/database/odbc.py +4 -1
  10. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/database/postgres.py +75 -6
  11. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/queue_backends/mongo_backend.py +4 -2
  12. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/errors/500.twig +1 -1
  13. {tina4_python-3.13.5 → tina4_python-3.13.7}/.gitignore +0 -0
  14. {tina4_python-3.13.5 → tina4_python-3.13.7}/README.md +0 -0
  15. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/CLAUDE.md +0 -0
  16. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/HtmlElement.py +0 -0
  17. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/Testing.py +0 -0
  18. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/ai/__init__.py +0 -0
  19. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/api/__init__.py +0 -0
  20. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/auth/__init__.py +0 -0
  21. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/cache/__init__.py +0 -0
  22. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/cli/__init__.py +0 -0
  23. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/container/__init__.py +0 -0
  24. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/core/__init__.py +0 -0
  25. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/core/cache.py +0 -0
  26. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/core/constants.py +0 -0
  27. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/core/events.py +0 -0
  28. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/core/middleware.py +0 -0
  29. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/core/rate_limiter.py +0 -0
  30. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/core/request.py +0 -0
  31. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/core/response.py +0 -0
  32. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/core/router.py +0 -0
  33. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/crud/__init__.py +0 -0
  34. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/database/__init__.py +0 -0
  35. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/database/adapter.py +0 -0
  36. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/database/connection.py +0 -0
  37. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/database/sqlite.py +0 -0
  38. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/debug/__init__.py +0 -0
  39. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/debug/error_overlay.py +0 -0
  40. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/dev_admin/__init__.py +0 -0
  41. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/dev_admin/metrics.py +0 -0
  42. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/dev_admin/plan.py +0 -0
  43. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/dev_admin/project_index.py +0 -0
  44. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/docs.py +0 -0
  45. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/dotenv/__init__.py +0 -0
  46. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/env.py +0 -0
  47. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/frond/FROND.md +0 -0
  48. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/frond/__init__.py +0 -0
  49. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/frond/engine.py +0 -0
  50. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/auth/meta.json +0 -0
  51. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  52. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/database/meta.json +0 -0
  53. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  54. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/error-overlay/meta.json +0 -0
  55. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  56. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/orm/meta.json +0 -0
  57. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  58. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  59. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/queue/meta.json +0 -0
  60. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  61. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/rest-api/meta.json +0 -0
  62. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  63. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/templates/meta.json +0 -0
  64. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  65. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  66. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/graphql/__init__.py +0 -0
  67. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/i18n/__init__.py +0 -0
  68. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/mcp/__init__.py +0 -0
  69. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/mcp/protocol.py +0 -0
  70. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/mcp/tools.py +0 -0
  71. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/messenger/__init__.py +0 -0
  72. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/migration/__init__.py +0 -0
  73. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/migration/runner.py +0 -0
  74. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/orm/__init__.py +0 -0
  75. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/orm/fields.py +0 -0
  76. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/orm/model.py +0 -0
  77. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/__feedback/widget.js +0 -0
  78. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/css/tina4.css +0 -0
  79. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/css/tina4.min.css +0 -0
  80. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/favicon.ico +0 -0
  81. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/images/logo.svg +0 -0
  82. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  83. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/js/frond.js +0 -0
  84. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/js/frond.min.js +0 -0
  85. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  86. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  87. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/js/tina4.min.js +0 -0
  88. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/js/tina4js.min.js +0 -0
  89. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/swagger/index.html +0 -0
  90. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  91. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/query_builder/__init__.py +0 -0
  92. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/queue/__init__.py +0 -0
  93. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/queue/job.py +0 -0
  94. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/queue/kafka_backend.py +0 -0
  95. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/queue/lite_backend.py +0 -0
  96. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/queue/mongo_backend.py +0 -0
  97. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/queue/rabbitmq_backend.py +0 -0
  98. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/queue_backends/__init__.py +0 -0
  99. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/queue_backends/kafka_backend.py +0 -0
  100. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  101. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/__init__.py +0 -0
  102. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  103. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_badges.scss +0 -0
  104. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  105. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_cards.scss +0 -0
  106. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_forms.scss +0 -0
  107. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_grid.scss +0 -0
  108. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_modals.scss +0 -0
  109. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_nav.scss +0 -0
  110. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_reset.scss +0 -0
  111. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_tables.scss +0 -0
  112. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_typography.scss +0 -0
  113. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  114. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/_variables.scss +0 -0
  115. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/base.scss +0 -0
  116. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/colors.scss +0 -0
  117. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/scss/tina4css/tina4.scss +0 -0
  118. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/seeder/__init__.py +0 -0
  119. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/service/__init__.py +0 -0
  120. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/session/__init__.py +0 -0
  121. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/session_handlers/__init__.py +0 -0
  122. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  123. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/session_handlers/redis_handler.py +0 -0
  124. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/session_handlers/valkey_handler.py +0 -0
  125. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/swagger/__init__.py +0 -0
  126. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/components/crud.twig +0 -0
  127. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  128. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  129. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/docker/python/Dockerfile +0 -0
  130. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  131. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/errors/302.twig +0 -0
  132. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/errors/401.twig +0 -0
  133. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/errors/403.twig +0 -0
  134. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/errors/404.twig +0 -0
  135. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.5 → tina4_python-3.13.7}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.13.5
3
+ Version: 3.13.7
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.5"
3
+ version = "3.13.7"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.13.5"
11
+ __version__ = "3.13.7"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -1307,9 +1307,38 @@ def _handle_route_error(
1307
1307
  error: Exception, request: Request, response: Response,
1308
1308
  request_id: str, is_dev: bool,
1309
1309
  ) -> Response:
1310
- """Format an error response for a failed route handler."""
1310
+ """Format an error response for a failed route handler.
1311
+
1312
+ Two contracts (v3.13.7):
1313
+
1314
+ 1. Fire ``tina4.request.error`` before rendering, so observers
1315
+ (centralised logging, APM, Sentry) see the failure even though
1316
+ the framework caught it. Payload is a single dict ``{exception,
1317
+ request}`` — the canonical shape mirrored by PHP/Ruby/Node.
1318
+ Listener exceptions are swallowed so a broken listener can't
1319
+ break the 500 page.
1320
+
1321
+ 2. In non-debug mode, never put the stack trace in the response
1322
+ body (CWE-209). The trace is still available via ``Log.error``
1323
+ above and the dev-admin ``BrokenTracker`` in dev mode. The
1324
+ framework's own 500.twig now guards the trace block with
1325
+ ``{% if error_message %}``.
1326
+ """
1311
1327
  Log.error(f"Route error: {error}", path=request.path)
1312
1328
  _write_broken(request, error)
1329
+
1330
+ try:
1331
+ from tina4_python.core.events import emit as _emit
1332
+ _emit("tina4.request.error", {"exception": error, "request": request})
1333
+ except Exception as listener_err:
1334
+ try:
1335
+ Log.warning(
1336
+ f"Listener for tina4.request.error raised: "
1337
+ f"{type(listener_err).__name__}: {listener_err}"
1338
+ )
1339
+ except Exception:
1340
+ pass
1341
+
1313
1342
  if is_dev:
1314
1343
  try:
1315
1344
  import traceback as _tb
@@ -1324,9 +1353,9 @@ def _handle_route_error(
1324
1353
  overlay_html = render_error_overlay(error, request)
1325
1354
  response.status(500).html(overlay_html)
1326
1355
  else:
1327
- import traceback
1328
- tb = traceback.format_exc()
1329
- html = _render_error_page(500, request.path, request_id, tb)
1356
+ # Production: NO traceback in the body. The trace is logged via
1357
+ # Log.error above; clients only see the generic page + request_id.
1358
+ html = _render_error_page(500, request.path, request_id, "")
1330
1359
  if html:
1331
1360
  response.status(500).html(html)
1332
1361
  else:
@@ -116,8 +116,10 @@ class FirebirdAdapter(DatabaseAdapter):
116
116
  """
117
117
  if _driver is None:
118
118
  raise ImportError(
119
- "A Firebird driver is required. "
120
- "Install: pip install firebird-driver (or pip install fdb for legacy)"
119
+ "A Firebird driver is required. Install one of:\n"
120
+ " uv add tina4-python[firebird] # extra for projects using uv\n"
121
+ " pip install firebird-driver # bare driver (or 'fdb' for legacy)\n"
122
+ " uv add tina4-python[all-db] # all five database drivers"
121
123
  )
122
124
 
123
125
  parsed = urlparse(connection_string)
@@ -348,8 +348,10 @@ class MongoDBAdapter(DatabaseAdapter):
348
348
  import pymongo
349
349
  except ImportError:
350
350
  raise ImportError(
351
- "pymongo is required for MongoDB connections. "
352
- "Install: pip install pymongo"
351
+ "pymongo is required for MongoDB connections. Install one of:\n"
352
+ " uv add tina4-python[mongo] # extra for projects using uv\n"
353
+ " pip install pymongo # bare driver\n"
354
+ " uv add tina4-python[all-db] # all five database drivers"
353
355
  )
354
356
 
355
357
  # Normalise the scheme so MongoClient accepts it
@@ -29,8 +29,10 @@ class MSSQLAdapter(DatabaseAdapter):
29
29
  import pymssql
30
30
  except ImportError:
31
31
  raise ImportError(
32
- "pymssql is required for MSSQL connections. "
33
- "Install: pip install pymssql"
32
+ "pymssql is required for MSSQL connections. Install one of:\n"
33
+ " uv add tina4-python[mssql] # extra for projects using uv\n"
34
+ " pip install pymssql # bare driver\n"
35
+ " uv add tina4-python[all-db] # all five database drivers"
34
36
  )
35
37
 
36
38
  parsed = urlparse(connection_string)
@@ -29,8 +29,10 @@ class MySQLAdapter(DatabaseAdapter):
29
29
  import mysql.connector
30
30
  except ImportError:
31
31
  raise ImportError(
32
- "mysql-connector-python is required for MySQL connections. "
33
- "Install: pip install mysql-connector-python"
32
+ "mysql-connector-python is required for MySQL connections. Install one of:\n"
33
+ " uv add tina4-python[mysql] # extra for projects using uv\n"
34
+ " pip install mysql-connector-python # bare driver\n"
35
+ " uv add tina4-python[all-db] # all five database drivers"
34
36
  )
35
37
 
36
38
  parsed = urlparse(connection_string)
@@ -31,7 +31,10 @@ class ODBCAdapter(DatabaseAdapter):
31
31
  import pyodbc
32
32
  except ImportError:
33
33
  raise ImportError(
34
- "pyodbc is required for ODBC connections. Install: pip install pyodbc"
34
+ "pyodbc is required for ODBC connections. Install one of:\n"
35
+ " uv add tina4-python[odbc] # extra for projects using uv\n"
36
+ " pip install pyodbc # bare driver\n"
37
+ " uv add tina4-python[all-db] # all five database drivers"
35
38
  )
36
39
 
37
40
  # Strip URL prefix if present
@@ -19,6 +19,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
19
19
  super().__init__()
20
20
  self._conn = None
21
21
  self._in_transaction: bool = False
22
+ self.last_error: str | None = None
22
23
 
23
24
  @staticmethod
24
25
  def _safe_execute(cursor, sql: str, params=None):
@@ -36,12 +37,76 @@ class PostgreSQLAdapter(DatabaseAdapter):
36
37
  Pass ``None`` (or omit the second arg) and psycopg2 skips the
37
38
  substitution pass entirely — literal ``%`` flows through
38
39
  untouched. So we route empty/None params through that path.
40
+
41
+ Kept static for direct unit-test access (a number of
42
+ regression tests in test_postgres_percent_substitution.py call
43
+ it with a fake cursor). For the error-aware variant that adds
44
+ logging + auto-rollback (issue #46), use the instance method
45
+ :meth:`_exec_with_handling`.
39
46
  """
40
47
  if params:
41
48
  cursor.execute(sql, params)
42
49
  else:
43
50
  cursor.execute(sql)
44
51
 
52
+ def _exec_with_handling(self, cursor, sql: str, params=None):
53
+ """Run a query through :meth:`_safe_execute` and surface failures.
54
+
55
+ Issue #46: psycopg2 keeps the connection in an
56
+ ``InFailedSqlTransaction`` state after any failed query — every
57
+ subsequent statement returns the cascade message ``current
58
+ transaction is aborted, commands ignored until end of transaction
59
+ block``, *far* from the actual cause. Without framework-side
60
+ logging, the original SQL + error never reach the user.
61
+
62
+ On failure this method:
63
+
64
+ 1. Logs the original error via ``Log.error`` with SQL+params context
65
+ 2. Stores it on ``self.last_error``
66
+ 3. Auto-rollbacks when NOT inside an explicit transaction (so the
67
+ next caller gets a clean connection)
68
+ 4. Re-raises so callers still see the failure
69
+
70
+ Inside an explicit transaction we do NOT rollback — the user owns
71
+ that transaction (could be running a SAVEPOINT/retry pattern). We
72
+ still log + store ``last_error`` so they can diagnose.
73
+ """
74
+ try:
75
+ self._safe_execute(cursor, sql, params)
76
+ except Exception as e:
77
+ self._on_query_error(e, sql, params)
78
+ raise
79
+
80
+ def _on_query_error(self, exc: Exception, sql: str, params=None):
81
+ """Handle a failed query: log, store, auto-rollback if implicit."""
82
+ # Lazy import — avoid circular import at module load and let the
83
+ # framework boot without Log if something exotic is happening.
84
+ try:
85
+ from tina4_python.debug import Log
86
+ preview = sql.strip().splitlines()[0][:200] if sql else "<no sql>"
87
+ Log.error(
88
+ f"PostgreSQL query failed: {exc.__class__.__name__}: {exc}",
89
+ sql=preview,
90
+ params=params,
91
+ )
92
+ except Exception:
93
+ pass
94
+
95
+ self.last_error = str(exc)
96
+
97
+ # If the query failed *outside* an explicit transaction block,
98
+ # rollback the implicit one so the connection becomes usable
99
+ # again. Without this, every subsequent query on this connection
100
+ # cascades into ``InFailedSqlTransaction`` until something else
101
+ # calls rollback. With it, the next caller sees a clean conn
102
+ # and the actual error from this call propagates as the cause.
103
+ if not self._in_transaction and self._conn is not None:
104
+ try:
105
+ self._conn.rollback()
106
+ except Exception:
107
+ # Don't mask the original error if rollback itself fails.
108
+ pass
109
+
45
110
  def connect(self, connection_string: str, username: str = "", password: str = "", **kwargs):
46
111
  """Connect to PostgreSQL.
47
112
 
@@ -53,8 +118,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
53
118
  import psycopg2.extras
54
119
  except ImportError:
55
120
  raise ImportError(
56
- "psycopg2 is required for PostgreSQL connections. "
57
- "Install: pip install psycopg2-binary"
121
+ "psycopg2 is required for PostgreSQL connections. Install one of:\n"
122
+ " uv add tina4-python[postgres] # extra for projects using uv\n"
123
+ " pip install psycopg2-binary # bare driver\n"
124
+ " uv add tina4-python[all-db] # all five database drivers"
58
125
  )
59
126
 
60
127
  parsed = urlparse(connection_string)
@@ -85,7 +152,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
85
152
  )
86
153
 
87
154
  cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
88
- self._safe_execute(cursor, sql, params)
155
+ self._exec_with_handling(cursor, sql, params)
89
156
 
90
157
  records = []
91
158
  last_id = None
@@ -151,10 +218,12 @@ class PostgreSQLAdapter(DatabaseAdapter):
151
218
  except Exception:
152
219
  total = 0
153
220
 
154
- # Apply pagination
221
+ # Apply pagination — the real query, so use the error-handling
222
+ # wrapper. The count probe above is best-effort and stays on
223
+ # _safe_execute (a failed probe shouldn't taint last_error).
155
224
  paginated_sql = f"{sql} LIMIT %s OFFSET %s"
156
225
  paginated_params = (params or []) + [limit, offset]
157
- self._safe_execute(cursor, paginated_sql, paginated_params)
226
+ self._exec_with_handling(cursor, paginated_sql, paginated_params)
158
227
  rows = [self._decode_blobs(dict(row)) for row in cursor.fetchall()]
159
228
 
160
229
  return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
@@ -164,7 +233,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
164
233
 
165
234
  sql = self._translate_sql(sql)
166
235
  cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
167
- self._safe_execute(cursor, sql, params)
236
+ self._exec_with_handling(cursor, sql, params)
168
237
  row = cursor.fetchone()
169
238
  return self._decode_blobs(dict(row)) if row else None
170
239
 
@@ -59,8 +59,10 @@ class MongoConnector:
59
59
  self._pymongo = pymongo
60
60
  except ImportError:
61
61
  raise ImportError(
62
- "pymongo is required for the MongoDB queue backend. "
63
- "Install it with: pip install pymongo"
62
+ "pymongo is required for the MongoDB queue backend. Install one of:\n"
63
+ " uv add tina4-python[mongo] # extra for projects using uv\n"
64
+ " pip install pymongo # bare driver\n"
65
+ " uv add tina4-python[all-db] # all five database drivers"
64
66
  )
65
67
 
66
68
  # ── Public Interface ─────────────────────────────────────────
@@ -27,7 +27,7 @@ body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; c
27
27
  <div class="error-title">Server Error</div>
28
28
  </div>
29
29
  <div class="error-msg">Something went wrong while processing your request.</div>
30
- <pre class="error-trace">{{ error_message }}</pre>
30
+ {% if error_message %}<pre class="error-trace">{{ error_message }}</pre>{% endif %}
31
31
  <div class="error-footer">
32
32
  <span class="error-hint">Fix the error and save to auto-reload</span>
33
33
  <span class="error-id">{{ request_id }}</span>
File without changes
File without changes