tina4-python 3.13.7__tar.gz → 3.13.9__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.7 → tina4_python-3.13.9}/PKG-INFO +1 -1
  2. {tina4_python-3.13.7 → tina4_python-3.13.9}/pyproject.toml +1 -1
  3. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/ai/__init__.py +132 -3
  5. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/database/postgres.py +81 -8
  6. {tina4_python-3.13.7 → tina4_python-3.13.9}/.gitignore +0 -0
  7. {tina4_python-3.13.7 → tina4_python-3.13.9}/README.md +0 -0
  8. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/CLAUDE.md +0 -0
  9. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/HtmlElement.py +0 -0
  10. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/Testing.py +0 -0
  11. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/api/__init__.py +0 -0
  12. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/auth/__init__.py +0 -0
  13. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/cache/__init__.py +0 -0
  14. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/cli/__init__.py +0 -0
  15. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/container/__init__.py +0 -0
  16. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/core/__init__.py +0 -0
  17. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/core/cache.py +0 -0
  18. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/core/constants.py +0 -0
  19. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/core/events.py +0 -0
  20. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/core/middleware.py +0 -0
  21. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/core/rate_limiter.py +0 -0
  22. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/core/request.py +0 -0
  23. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/core/response.py +0 -0
  24. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/core/router.py +0 -0
  25. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/core/server.py +0 -0
  26. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/crud/__init__.py +0 -0
  27. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/database/__init__.py +0 -0
  28. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/database/adapter.py +0 -0
  29. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/database/connection.py +0 -0
  30. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/database/firebird.py +0 -0
  31. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/database/mongodb.py +0 -0
  32. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/database/mssql.py +0 -0
  33. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/database/mysql.py +0 -0
  34. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/database/odbc.py +0 -0
  35. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/database/sqlite.py +0 -0
  36. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/dev_admin/__init__.py +0 -0
  39. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/dev_admin/metrics.py +0 -0
  40. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/dev_admin/plan.py +0 -0
  41. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/dev_admin/project_index.py +0 -0
  42. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/docs.py +0 -0
  43. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/dotenv/__init__.py +0 -0
  44. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/env.py +0 -0
  45. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/frond/FROND.md +0 -0
  46. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/frond/__init__.py +0 -0
  47. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/frond/engine.py +0 -0
  48. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/auth/meta.json +0 -0
  49. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  50. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/database/meta.json +0 -0
  51. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  52. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/error-overlay/meta.json +0 -0
  53. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  54. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/orm/meta.json +0 -0
  55. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  56. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  57. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/queue/meta.json +0 -0
  58. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  59. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/rest-api/meta.json +0 -0
  60. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  61. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/templates/meta.json +0 -0
  62. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  63. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  64. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/graphql/__init__.py +0 -0
  65. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/i18n/__init__.py +0 -0
  66. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/mcp/__init__.py +0 -0
  67. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/mcp/protocol.py +0 -0
  68. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/mcp/tools.py +0 -0
  69. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/messenger/__init__.py +0 -0
  70. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/migration/__init__.py +0 -0
  71. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/migration/runner.py +0 -0
  72. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/orm/__init__.py +0 -0
  73. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/orm/fields.py +0 -0
  74. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/orm/model.py +0 -0
  75. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/__feedback/widget.js +0 -0
  76. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/js/frond.js +0 -0
  82. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/js/frond.min.js +0 -0
  83. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  84. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  85. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/js/tina4.min.js +0 -0
  86. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/js/tina4js.min.js +0 -0
  87. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/swagger/index.html +0 -0
  88. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  89. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/query_builder/__init__.py +0 -0
  90. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/queue/__init__.py +0 -0
  91. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/queue/job.py +0 -0
  92. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/queue/kafka_backend.py +0 -0
  93. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/queue/lite_backend.py +0 -0
  94. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/queue/mongo_backend.py +0 -0
  95. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/queue/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/queue_backends/__init__.py +0 -0
  97. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/queue_backends/kafka_backend.py +0 -0
  98. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/queue_backends/mongo_backend.py +0 -0
  99. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/__init__.py +0 -0
  101. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  102. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_badges.scss +0 -0
  103. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  104. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_cards.scss +0 -0
  105. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_forms.scss +0 -0
  106. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_grid.scss +0 -0
  107. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_modals.scss +0 -0
  108. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_nav.scss +0 -0
  109. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_reset.scss +0 -0
  110. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_tables.scss +0 -0
  111. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_typography.scss +0 -0
  112. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  113. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/_variables.scss +0 -0
  114. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/base.scss +0 -0
  115. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/colors.scss +0 -0
  116. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/scss/tina4css/tina4.scss +0 -0
  117. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/seeder/__init__.py +0 -0
  118. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/service/__init__.py +0 -0
  119. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/session/__init__.py +0 -0
  120. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.7 → tina4_python-3.13.9}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.7 → tina4_python-3.13.9}/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.7
3
+ Version: 3.13.9
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.7"
3
+ version = "3.13.9"
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.7"
11
+ __version__ = "3.13.9"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -161,6 +161,136 @@ def install_context(root: str = ".", tools: "list[str] | None" = None) -> list[s
161
161
  return install_selected(root, selection)
162
162
 
163
163
 
164
+ # ── v3.13.9: non-destructive context-file writer ───────────────────────────
165
+ #
166
+ # Pre-v3.13.9 the installer wrote a full developer guide to CLAUDE.md
167
+ # (and the other context files) on every run, clobbering whatever the
168
+ # user had put there. Now it writes only a marker-bracketed Tina4 skill
169
+ # block — pointing the assistant at .claude/skills/tina4-*/SKILL.md —
170
+ # and leaves the rest of the file alone.
171
+
172
+
173
+ def _markers_for(context_file: str) -> "tuple[str, str]":
174
+ """Return ``(start_marker, end_marker)`` for a context file.
175
+
176
+ Markdown files get HTML comment markers (invisible when rendered).
177
+ Rule files (.cursorules, .windsurfrules, .clinerules) use ``#``
178
+ line comments which most rule loaders treat as comments.
179
+ """
180
+ if context_file.lower().endswith(".md"):
181
+ return ("<!-- tina4-skills:start -->", "<!-- tina4-skills:end -->")
182
+ return ("# tina4-skills:start", "# tina4-skills:end")
183
+
184
+
185
+ def _skill_block(context_file: str) -> str:
186
+ """Return the marker-bracketed Tina4 skill registration block."""
187
+ start, end = _markers_for(context_file)
188
+ if context_file.lower().endswith(".md"):
189
+ body = (
190
+ "## Tina4 Skills\n\n"
191
+ "When working on this Tina4 project, these skills give the "
192
+ "assistant project-aware behaviour:\n\n"
193
+ "- **tina4-developer** — Read `.claude/skills/tina4-developer/SKILL.md` before building features.\n"
194
+ "- **tina4-js** — Read `.claude/skills/tina4-js/SKILL.md` for frontend work.\n"
195
+ "- **tina4-maintainer** — Read `.claude/skills/tina4-maintainer/SKILL.md` for framework-level changes.\n\n"
196
+ "See https://tina4.com for full docs."
197
+ )
198
+ else:
199
+ body = (
200
+ "Tina4 Skills — read these files before working on this project:\n"
201
+ " .claude/skills/tina4-developer/SKILL.md (feature development)\n"
202
+ " .claude/skills/tina4-js/SKILL.md (frontend / tina4-js)\n"
203
+ " .claude/skills/tina4-maintainer/SKILL.md (framework-level changes)\n"
204
+ "Docs: https://tina4.com"
205
+ )
206
+ return f"{start}\n{body}\n{end}"
207
+
208
+
209
+ def _has_markers(existing: str, start: str, end: str) -> bool:
210
+ """True iff both start and end markers appear in order."""
211
+ s_idx = existing.find(start)
212
+ if s_idx == -1:
213
+ return False
214
+ return existing.find(end, s_idx + len(start)) != -1
215
+
216
+
217
+ def _replace_marker_block(existing: str, block: str, start: str, end: str) -> str:
218
+ """Replace the bracketed block in ``existing`` with ``block``."""
219
+ s_idx = existing.find(start)
220
+ if s_idx == -1:
221
+ return existing.rstrip() + "\n\n" + block + "\n"
222
+ e_idx = existing.find(end, s_idx + len(start))
223
+ if e_idx == -1:
224
+ return existing.rstrip() + "\n\n" + block + "\n"
225
+ before = existing[:s_idx].rstrip()
226
+ after = existing[e_idx + len(end):].lstrip("\n")
227
+ glue_before = "\n\n" if before else ""
228
+ glue_after = "\n" + after if after else "\n"
229
+ return f"{before}{glue_before}{block}{glue_after}"
230
+
231
+
232
+ # Headers the pre-v3.13.9 installer wrote at the top of CLAUDE.md.
233
+ # Detecting these lets us migrate cleanly off the old clobber install.
234
+ _OLD_FRAMEWORK_HEADERS = (
235
+ "# Tina4 Python",
236
+ "# Tina4 PHP",
237
+ "# Tina4 Ruby",
238
+ "# CLAUDE.md — AI Developer Guide for tina4-nodejs",
239
+ "# CLAUDE.md - AI Developer Guide for tina4-nodejs",
240
+ )
241
+
242
+
243
+ def _looks_like_old_framework_install(existing: str) -> bool:
244
+ """True if the file starts with a header the pre-v3.13.9 installer
245
+ wrote. Used to migrate one-time off the old clobber-style install."""
246
+ head = existing.lstrip()[:400]
247
+ return any(head.startswith(m) for m in _OLD_FRAMEWORK_HEADERS)
248
+
249
+
250
+ def _write_or_merge(context_path: "Path", context_file: str, framework_guide: str) -> str:
251
+ """Write the context file non-destructively. Returns a human-readable
252
+ action verb for the caller's log line.
253
+
254
+ Four branches:
255
+ 1. Doesn't exist → write framework guide + skill block
256
+ 2. Has markers → refresh just the skill block (idempotent)
257
+ 3. Old header → migrate: replace old dump with new guide + block
258
+ 4. User content → append the skill block, preserve everything else
259
+ """
260
+ block = _skill_block(context_file)
261
+ start, end = _markers_for(context_file)
262
+
263
+ if not context_path.exists():
264
+ context_path.write_text(
265
+ framework_guide.rstrip() + "\n\n" + block + "\n",
266
+ encoding="utf-8",
267
+ )
268
+ return "Installed"
269
+
270
+ existing = context_path.read_text(encoding="utf-8")
271
+ if _has_markers(existing, start, end):
272
+ new_content = _replace_marker_block(existing, block, start, end)
273
+ context_path.write_text(new_content, encoding="utf-8")
274
+ return "Refreshed skill block in"
275
+
276
+ if _looks_like_old_framework_install(existing):
277
+ head = existing.lstrip()
278
+ preamble = existing[: len(existing) - len(head)]
279
+ new_content = (
280
+ (preamble.rstrip() + "\n\n" if preamble.strip() else "")
281
+ + framework_guide.rstrip()
282
+ + "\n\n"
283
+ + block
284
+ + "\n"
285
+ )
286
+ context_path.write_text(new_content, encoding="utf-8")
287
+ return "Migrated (replaced old framework dump in)"
288
+
289
+ new_content = existing.rstrip() + "\n\n" + block + "\n"
290
+ context_path.write_text(new_content, encoding="utf-8")
291
+ return "Appended skill block to"
292
+
293
+
164
294
  def _install_for_tool(root: Path, tool: dict, context: str) -> list[str]:
165
295
  """Install context file for a single tool."""
166
296
  created = []
@@ -171,9 +301,8 @@ def _install_for_tool(root: Path, tool: dict, context: str) -> list[str]:
171
301
  (root / tool["config_dir"]).mkdir(parents=True, exist_ok=True)
172
302
  context_path.parent.mkdir(parents=True, exist_ok=True)
173
303
 
174
- # Always overwriteuser chose to install
175
- context_path.write_text(context, encoding="utf-8")
176
- action = "Updated" if context_path.exists() else "Installed"
304
+ # v3.13.9: non-destructive write see _write_or_merge below.
305
+ action = _write_or_merge(context_path, tool["context_file"], context)
177
306
  rel = str(context_path.relative_to(root))
178
307
  created.append(rel)
179
308
  try:
@@ -49,6 +49,60 @@ class PostgreSQLAdapter(DatabaseAdapter):
49
49
  else:
50
50
  cursor.execute(sql)
51
51
 
52
+ def _heal_aborted_txn(self):
53
+ """Issue #46 follow-on (v3.13.8): pre-flight rollback if the
54
+ connection arrived in an aborted-transaction state.
55
+
56
+ v3.13.6 added auto-rollback inside :meth:`_on_query_error` — that
57
+ clears the abort *on* a failure the wrapper catches. But the
58
+ connection can still arrive poisoned from elsewhere:
59
+
60
+ - A boot-time query that failed before request handling started
61
+ (migration probe, ORM ``information_schema`` lookup, etc.)
62
+ - A failure inside an explicit transaction where the user owned
63
+ the rollback but never issued one
64
+ - A direct ``cursor.execute`` call in another adapter method
65
+ (e.g. the ``SELECT lastval()`` SAVEPOINT probe) that managed to
66
+ leave the txn dirty
67
+
68
+ In all those cases the *next* query — even one routed through
69
+ our wrapper — hits ``InFailedSqlTransaction`` immediately, and
70
+ the cascade message buries whatever the original cause was.
71
+
72
+ Healing fix: before executing, ask psycopg2 whether the
73
+ connection is in ``TRANSACTION_STATUS_INERROR`` and rollback if
74
+ so. Only when NOT inside an explicit transaction — explicit txns
75
+ stay under the caller's control.
76
+
77
+ Reported by Schalk (24rent) against v3.13.7.
78
+ """
79
+ if self._in_transaction or self._conn is None:
80
+ return
81
+ try:
82
+ import psycopg2.extensions as _ext
83
+ status = self._conn.info.transaction_status
84
+ except Exception:
85
+ return
86
+ if status != _ext.TRANSACTION_STATUS_INERROR:
87
+ return
88
+ try:
89
+ self._conn.rollback()
90
+ except Exception:
91
+ # If even the heal-rollback fails, fall through — the next
92
+ # cursor.execute will raise with the real psycopg2 error and
93
+ # _on_query_error will pick it up. Better than masking.
94
+ return
95
+ try:
96
+ from tina4_python.debug import Log
97
+ Log.warning(
98
+ "PostgreSQL connection arrived in aborted-transaction "
99
+ "state — issued pre-flight ROLLBACK so the next query "
100
+ "starts clean. Look back in the log for the original "
101
+ "PostgreSQL query failed entry."
102
+ )
103
+ except Exception:
104
+ pass
105
+
52
106
  def _exec_with_handling(self, cursor, sql: str, params=None):
53
107
  """Run a query through :meth:`_safe_execute` and surface failures.
54
108
 
@@ -59,18 +113,23 @@ class PostgreSQLAdapter(DatabaseAdapter):
59
113
  block``, *far* from the actual cause. Without framework-side
60
114
  logging, the original SQL + error never reach the user.
61
115
 
62
- On failure this method:
116
+ This method:
63
117
 
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)
118
+ 0. (v3.13.8) Pre-flight heal if the connection arrived in an
119
+ aborted-transaction state from anywhere outside this wrapper,
120
+ rollback first so this query starts on a clean txn.
121
+ 1. Logs failures via ``Log.error`` with SQL+params context
122
+ 2. Stores them on ``self.last_error``
123
+ 3. Auto-rollbacks on failure when NOT inside an explicit
124
+ transaction (so the next caller gets a clean connection)
68
125
  4. Re-raises so callers still see the failure
69
126
 
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.
127
+ Inside an explicit transaction we do NOT auto-rollback — the
128
+ user owns that transaction (could be running a SAVEPOINT/retry
129
+ pattern). We still log + store ``last_error`` so they can
130
+ diagnose, and the heal step also defers to explicit txns.
73
131
  """
132
+ self._heal_aborted_txn()
74
133
  try:
75
134
  self._safe_execute(cursor, sql, params)
76
135
  except Exception as e:
@@ -208,6 +267,12 @@ class PostgreSQLAdapter(DatabaseAdapter):
208
267
 
209
268
  sql = self._translate_sql(sql)
210
269
 
270
+ # v3.13.8: heal first so the COUNT probe below doesn't open on a
271
+ # poisoned connection — the probe uses raw _safe_execute (it's
272
+ # allowed to fail without polluting last_error) so it bypasses
273
+ # _exec_with_handling's heal step.
274
+ self._heal_aborted_txn()
275
+
211
276
  cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
212
277
 
213
278
  # Count total rows
@@ -217,6 +282,14 @@ class PostgreSQLAdapter(DatabaseAdapter):
217
282
  total = cursor.fetchone()["cnt"]
218
283
  except Exception:
219
284
  total = 0
285
+ # If the probe failed because the connection arrived (or
286
+ # became) aborted, roll back so the real pagination query
287
+ # below sees a clean txn. Without this we'd cascade.
288
+ if not self._in_transaction and self._conn is not None:
289
+ try:
290
+ self._conn.rollback()
291
+ except Exception:
292
+ pass
220
293
 
221
294
  # Apply pagination — the real query, so use the error-handling
222
295
  # wrapper. The count probe above is best-effort and stays on
File without changes
File without changes