tina4-python 3.12.0__tar.gz → 3.12.1__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 (154) hide show
  1. {tina4_python-3.12.0 → tina4_python-3.12.1}/PKG-INFO +2 -2
  2. {tina4_python-3.12.0 → tina4_python-3.12.1}/pyproject.toml +2 -5
  3. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/database/firebird.py +90 -16
  5. {tina4_python-3.12.0 → tina4_python-3.12.1}/.gitignore +0 -0
  6. {tina4_python-3.12.0 → tina4_python-3.12.1}/README.md +0 -0
  7. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/CLAUDE.md +0 -0
  8. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/HtmlElement.py +0 -0
  9. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/Testing.py +0 -0
  10. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/ai/__init__.py +0 -0
  11. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/api/__init__.py +0 -0
  12. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/auth/__init__.py +0 -0
  13. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/cache/__init__.py +0 -0
  14. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/cli/__init__.py +0 -0
  15. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/container/__init__.py +0 -0
  16. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/core/__init__.py +0 -0
  17. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/core/cache.py +0 -0
  18. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/core/constants.py +0 -0
  19. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/core/events.py +0 -0
  20. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/core/middleware.py +0 -0
  21. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/core/rate_limiter.py +0 -0
  22. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/core/request.py +0 -0
  23. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/core/response.py +0 -0
  24. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/core/router.py +0 -0
  25. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/core/server.py +0 -0
  26. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/crud/__init__.py +0 -0
  27. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/database/__init__.py +0 -0
  28. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/database/adapter.py +0 -0
  29. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/database/connection.py +0 -0
  30. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/database/mongodb.py +0 -0
  31. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/database/mssql.py +0 -0
  32. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/database/mysql.py +0 -0
  33. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/database/odbc.py +0 -0
  34. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/database/postgres.py +0 -0
  35. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/database/sqlite.py +0 -0
  36. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/dev_admin/__init__.py +0 -0
  39. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/dev_admin/metrics.py +0 -0
  40. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/dev_admin/plan.py +0 -0
  41. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/dev_admin/project_index.py +0 -0
  42. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/docs.py +0 -0
  43. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/dotenv/__init__.py +0 -0
  44. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/frond/FROND.md +0 -0
  45. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/frond/__init__.py +0 -0
  46. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/frond/engine.py +0 -0
  47. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/auth/meta.json +0 -0
  48. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  49. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/database/meta.json +0 -0
  50. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  51. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/error-overlay/meta.json +0 -0
  52. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  53. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/orm/meta.json +0 -0
  54. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  55. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  56. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/queue/meta.json +0 -0
  57. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  58. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/rest-api/meta.json +0 -0
  59. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  60. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/templates/meta.json +0 -0
  61. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  62. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  63. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/graphql/__init__.py +0 -0
  64. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/i18n/__init__.py +0 -0
  65. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/mcp/__init__.py +0 -0
  66. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/mcp/protocol.py +0 -0
  67. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/mcp/tools.py +0 -0
  68. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/messenger/__init__.py +0 -0
  69. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/migration/__init__.py +0 -0
  70. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/migration/runner.py +0 -0
  71. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/orm/__init__.py +0 -0
  72. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/orm/fields.py +0 -0
  73. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/orm/model.py +0 -0
  74. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/css/tina4.css +0 -0
  75. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/css/tina4.min.css +0 -0
  76. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/favicon.ico +0 -0
  77. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/images/logo.svg +0 -0
  78. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  79. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/js/frond.js +0 -0
  80. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/js/frond.min.js +0 -0
  81. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  82. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  83. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/js/tina4.min.js +0 -0
  84. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/js/tina4js.min.js +0 -0
  85. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/swagger/index.html +0 -0
  86. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  87. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/query_builder/__init__.py +0 -0
  88. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/queue/__init__.py +0 -0
  89. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/queue/job.py +0 -0
  90. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/queue/kafka_backend.py +0 -0
  91. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/queue/lite_backend.py +0 -0
  92. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/queue/mongo_backend.py +0 -0
  93. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/queue/rabbitmq_backend.py +0 -0
  94. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/queue_backends/__init__.py +0 -0
  95. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/queue_backends/kafka_backend.py +0 -0
  96. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/queue_backends/mongo_backend.py +0 -0
  97. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  98. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/__init__.py +0 -0
  99. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  100. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_badges.scss +0 -0
  101. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  102. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_cards.scss +0 -0
  103. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_forms.scss +0 -0
  104. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_grid.scss +0 -0
  105. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_modals.scss +0 -0
  106. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_nav.scss +0 -0
  107. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_reset.scss +0 -0
  108. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_tables.scss +0 -0
  109. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_typography.scss +0 -0
  110. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  111. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/_variables.scss +0 -0
  112. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/base.scss +0 -0
  113. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/colors.scss +0 -0
  114. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/scss/tina4css/tina4.scss +0 -0
  115. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/seeder/__init__.py +0 -0
  116. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/service/__init__.py +0 -0
  117. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/session/__init__.py +0 -0
  118. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/session_handlers/__init__.py +0 -0
  119. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  120. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/session_handlers/redis_handler.py +0 -0
  121. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/session_handlers/valkey_handler.py +0 -0
  122. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/swagger/__init__.py +0 -0
  123. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/components/crud.twig +0 -0
  124. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  125. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  126. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/docker/python/Dockerfile +0 -0
  127. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  128. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/errors/302.twig +0 -0
  129. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/errors/401.twig +0 -0
  130. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/errors/403.twig +0 -0
  131. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/errors/404.twig +0 -0
  132. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/errors/500.twig +0 -0
  133. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/errors/502.twig +0 -0
  134. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/errors/503.twig +0 -0
  135. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/errors/base.twig +0 -0
  136. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/frontend/README.md +0 -0
  137. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/templates/readme.md +0 -0
  138. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/test_client/__init__.py +0 -0
  139. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  140. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  141. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  142. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  143. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  144. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  145. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  146. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  147. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  148. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  149. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  150. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  151. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/validator/__init__.py +0 -0
  152. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/websocket/__init__.py +0 -0
  153. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/websocket/backplane.py +0 -0
  154. {tina4_python-3.12.0 → tina4_python-3.12.1}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.12.0
4
- Summary: Tina4 for Python — 54 built-in features, zero dependencies
3
+ Version: 3.12.1
4
+ Summary: Tina4 Python v3 Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
7
7
  Requires-Python: >=3.12
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- dynamic = ["version"]
4
- description = "Tina4 for Python — 54 built-in features, zero dependencies"
3
+ version = "3.12.1"
4
+ description = "Tina4 Python v3 Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
7
7
  ]
@@ -55,9 +55,6 @@ dev = [
55
55
  requires = ["hatchling"]
56
56
  build-backend = "hatchling.build"
57
57
 
58
- [tool.hatch.version]
59
- path = "tina4_python/__init__.py"
60
-
61
58
  [tool.hatch.build]
62
59
  include = ["tina4_python/**/*"]
63
60
 
@@ -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.12.0"
11
+ __version__ = "3.12.1"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -28,10 +28,27 @@ except ImportError:
28
28
  class FirebirdAdapter(DatabaseAdapter):
29
29
  """Firebird database driver using firebird-driver or fdb."""
30
30
 
31
+ # Substring markers (lowercased) that identify a dead-socket Firebird
32
+ # error worth reconnecting for. Idle Firebird connections die silently
33
+ # behind NAT timeouts, server-side ConnectionIdleTimeout, or Docker
34
+ # network rotation; without this the next prepare() crashes the request.
35
+ _DEAD_CONN_MARKERS = (
36
+ "error writing data to the connection",
37
+ "error reading data from the connection",
38
+ "connection shutdown",
39
+ "connection lost",
40
+ "network error",
41
+ "connection is not active",
42
+ "broken pipe",
43
+ )
44
+
31
45
  def __init__(self):
32
46
  super().__init__()
33
47
  self._conn = None
34
48
  self._in_transaction: bool = False
49
+ # Remembered connection params — populated by connect(), used by
50
+ # _reconnect() when a dead socket is detected mid-request.
51
+ self._connect_params: dict | None = None
35
52
 
36
53
  def connect(self, connection_string: str, username: str = "", password: str = "", **kwargs):
37
54
  """Connect to Firebird.
@@ -54,28 +71,82 @@ class FirebirdAdapter(DatabaseAdapter):
54
71
  password = parsed.password or password or "masterkey"
55
72
  charset = kwargs.pop("charset", "UTF8")
56
73
 
74
+ # Cache for transparent reconnect — never logged, lives only in
75
+ # adapter memory alongside the connection it owns.
76
+ self._connect_params = {
77
+ "host": host, "port": port, "db_path": db_path,
78
+ "user": user, "password": password, "charset": charset,
79
+ "extra": dict(kwargs),
80
+ }
81
+ self._open()
82
+
83
+ def _open(self) -> None:
84
+ """Open the underlying Firebird connection from cached params."""
85
+ p = self._connect_params
86
+ if p is None:
87
+ raise RuntimeError("FirebirdAdapter._open called before connect()")
88
+
57
89
  if _driver_name == "firebird-driver":
58
90
  # Modern firebird-driver uses dsn format: host/port:path
59
- dsn = f"{host}/{port}:{db_path}" if port != 3050 else f"{host}:{db_path}"
91
+ dsn = f"{p['host']}/{p['port']}:{p['db_path']}" if p['port'] != 3050 else f"{p['host']}:{p['db_path']}"
60
92
  self._conn = _driver.connect(
61
93
  dsn,
62
- user=user,
63
- password=password,
64
- charset=charset,
65
- **kwargs,
94
+ user=p["user"],
95
+ password=p["password"],
96
+ charset=p["charset"],
97
+ **p["extra"],
66
98
  )
67
99
  else:
68
100
  # Legacy fdb
69
101
  self._conn = _driver.connect(
70
- host=host,
71
- port=port,
72
- database=db_path,
73
- user=user,
74
- password=password,
75
- charset=charset,
76
- **kwargs,
102
+ host=p["host"],
103
+ port=p["port"],
104
+ database=p["db_path"],
105
+ user=p["user"],
106
+ password=p["password"],
107
+ charset=p["charset"],
108
+ **p["extra"],
77
109
  )
78
110
 
111
+ @classmethod
112
+ def _is_dead_connection(cls, exc: BaseException) -> bool:
113
+ """Match dead-socket error messages from firebird-driver / fdb.
114
+ Substring + case-insensitive so we catch both driver wording variants.
115
+ """
116
+ msg = str(exc).lower()
117
+ return any(m in msg for m in cls._DEAD_CONN_MARKERS)
118
+
119
+ def _reconnect(self) -> None:
120
+ """Force-close any stale handle and reopen. Safe to call repeatedly;
121
+ idempotent on a dead connection."""
122
+ try:
123
+ if self._conn is not None:
124
+ self._conn.close()
125
+ except Exception:
126
+ pass # connection already gone — nothing to clean up
127
+ self._conn = None
128
+ self._in_transaction = False
129
+ self._open()
130
+
131
+ def _safe_cursor_execute(self, cursor, sql: str, params: list | None):
132
+ """Execute on a cursor with one transparent reconnect+retry on
133
+ dead-connection errors. Skipped inside an explicit transaction —
134
+ atomicity beats resilience there; the caller handles rollback.
135
+
136
+ Returns the cursor (possibly a fresh one after reconnect) so the
137
+ caller can fetch results from it.
138
+ """
139
+ try:
140
+ cursor.execute(sql, params or [])
141
+ return cursor
142
+ except Exception as e:
143
+ if not self._is_dead_connection(e) or self._in_transaction:
144
+ raise
145
+ self._reconnect()
146
+ cursor = self._conn.cursor()
147
+ cursor.execute(sql, params or [])
148
+ return cursor
149
+
79
150
  def close(self):
80
151
  if self._conn:
81
152
  self._conn.close()
@@ -92,7 +163,7 @@ class FirebirdAdapter(DatabaseAdapter):
92
163
  sql = sql[:returning_match.start()]
93
164
 
94
165
  cursor = self._conn.cursor()
95
- cursor.execute(sql, params or [])
166
+ cursor = self._safe_cursor_execute(cursor, sql, params)
96
167
 
97
168
  records = []
98
169
  last_id = None
@@ -145,16 +216,19 @@ class FirebirdAdapter(DatabaseAdapter):
145
216
  # Count total rows
146
217
  count_sql = f"SELECT COUNT(*) FROM ({sql})"
147
218
  try:
148
- cursor.execute(count_sql, params or [])
219
+ cursor = self._safe_cursor_execute(cursor, count_sql, params)
149
220
  total = cursor.fetchone()[0]
150
221
  except Exception:
151
222
  total = 0
223
+ # Reconnect may have just happened — get a fresh cursor for the
224
+ # paginated query below regardless of whether count succeeded.
225
+ cursor = self._conn.cursor()
152
226
 
153
227
  # Apply Firebird pagination — ROWS start TO end
154
228
  start = offset + 1
155
229
  end = offset + limit
156
230
  paginated_sql = f"{sql} ROWS {start} TO {end}"
157
- cursor.execute(paginated_sql, params or [])
231
+ cursor = self._safe_cursor_execute(cursor, paginated_sql, params)
158
232
 
159
233
  desc = cursor.description
160
234
  col_names = [d[0].strip().lower() for d in desc] if desc else []
@@ -165,7 +239,7 @@ class FirebirdAdapter(DatabaseAdapter):
165
239
  def fetch_one(self, sql: str, params: list = None) -> dict | None:
166
240
  sql = self._translate_sql(sql)
167
241
  cursor = self._conn.cursor()
168
- cursor.execute(sql, params or [])
242
+ cursor = self._safe_cursor_execute(cursor, sql, params)
169
243
  desc = cursor.description
170
244
  row = cursor.fetchone()
171
245
  if row is None:
File without changes
File without changes