tina4-python 3.13.14__tar.gz → 3.13.16__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.14 → tina4_python-3.13.16}/PKG-INFO +1 -1
  2. {tina4_python-3.13.14 → tina4_python-3.13.16}/pyproject.toml +1 -1
  3. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/CLAUDE.md +34 -27
  4. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/__init__.py +1 -1
  5. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/adapter.py +10 -0
  6. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/postgres.py +46 -2
  7. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/orm/model.py +32 -4
  8. {tina4_python-3.13.14 → tina4_python-3.13.16}/.gitignore +0 -0
  9. {tina4_python-3.13.14 → tina4_python-3.13.16}/README.md +0 -0
  10. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/HtmlElement.py +0 -0
  11. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/Testing.py +0 -0
  12. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/ai/__init__.py +0 -0
  13. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/cli/__init__.py +0 -0
  17. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/container/__init__.py +0 -0
  18. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/__init__.py +0 -0
  19. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/rate_limiter.py +0 -0
  24. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/request.py +0 -0
  25. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/response.py +0 -0
  26. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/router.py +0 -0
  27. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/server.py +0 -0
  28. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/crud/__init__.py +0 -0
  29. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/__init__.py +0 -0
  30. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/connection.py +0 -0
  31. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/firebird.py +0 -0
  32. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/mongodb.py +0 -0
  33. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/mssql.py +0 -0
  34. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/mysql.py +0 -0
  35. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/odbc.py +0 -0
  36. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/sqlite.py +0 -0
  37. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/debug/__init__.py +0 -0
  38. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/debug/error_overlay.py +0 -0
  39. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/dev_admin/__init__.py +0 -0
  40. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/dev_admin/metrics.py +0 -0
  41. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/dev_admin/plan.py +0 -0
  42. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/dev_admin/project_index.py +0 -0
  43. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/docs.py +0 -0
  44. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/dotenv/__init__.py +0 -0
  45. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/env.py +0 -0
  46. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/frond/FROND.md +0 -0
  47. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/frond/__init__.py +0 -0
  48. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/frond/engine.py +0 -0
  49. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/auth/meta.json +0 -0
  50. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  51. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/database/meta.json +0 -0
  52. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  53. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/error-overlay/meta.json +0 -0
  54. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  55. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/orm/meta.json +0 -0
  56. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  57. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  58. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/queue/meta.json +0 -0
  59. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  60. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/rest-api/meta.json +0 -0
  61. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  62. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/templates/meta.json +0 -0
  63. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  64. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  65. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/graphql/__init__.py +0 -0
  66. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/i18n/__init__.py +0 -0
  67. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/mcp/__init__.py +0 -0
  68. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/mcp/protocol.py +0 -0
  69. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/mcp/tools.py +0 -0
  70. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/messenger/__init__.py +0 -0
  71. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/migration/__init__.py +0 -0
  72. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/migration/runner.py +0 -0
  73. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/orm/__init__.py +0 -0
  74. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/orm/fields.py +0 -0
  75. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/__feedback/widget.js +0 -0
  76. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/frond.js +0 -0
  82. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/frond.min.js +0 -0
  83. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  84. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  85. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/tina4.min.js +0 -0
  86. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/tina4js.min.js +0 -0
  87. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/swagger/index.html +0 -0
  88. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  89. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/query_builder/__init__.py +0 -0
  90. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/__init__.py +0 -0
  91. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/job.py +0 -0
  92. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/kafka_backend.py +0 -0
  93. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/lite_backend.py +0 -0
  94. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/mongo_backend.py +0 -0
  95. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue_backends/__init__.py +0 -0
  97. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue_backends/kafka_backend.py +0 -0
  98. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue_backends/mongo_backend.py +0 -0
  99. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/__init__.py +0 -0
  101. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  102. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_badges.scss +0 -0
  103. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  104. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_cards.scss +0 -0
  105. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_forms.scss +0 -0
  106. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_grid.scss +0 -0
  107. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_modals.scss +0 -0
  108. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_nav.scss +0 -0
  109. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_reset.scss +0 -0
  110. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_tables.scss +0 -0
  111. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_typography.scss +0 -0
  112. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  113. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_variables.scss +0 -0
  114. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/base.scss +0 -0
  115. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/colors.scss +0 -0
  116. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/tina4.scss +0 -0
  117. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/seeder/__init__.py +0 -0
  118. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/service/__init__.py +0 -0
  119. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session/__init__.py +0 -0
  120. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.14 → tina4_python-3.13.16}/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.14
3
+ Version: 3.13.16
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.14"
3
+ version = "3.13.16"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -282,8 +282,9 @@ Queue(topic="tasks").push({"action": "send_email"})
282
282
  - **Macro imports**: `{% from "macros/forms.twig" import field_group %}` — macros do NOT inherit parent context, pass variables explicitly
283
283
  4. **DatabaseResult**: `dba.fetch()` returns a `DatabaseResult` object, NOT a plain list
284
284
  - Use `.records` to get list of dicts: `result.records`
285
- - Use `.count` for row count (no `len()` support)
285
+ - Use `.count` for total row count; `len(result)` also works
286
286
  - Iteration works: `for row in result` or `list(result)`
287
+ - Index/slice access works: `result[0]`, `result[1:3]`
287
288
  - Other methods: `.to_json()`, `.to_array()`, `.to_csv()`, `.to_paginate()`
288
289
  4. **fetch_one()**: Returns a plain dict (or None), NOT a DatabaseResult
289
290
  5. **Dict access**: All query results use dict access `row["column"]` not attribute access `row.column`
@@ -356,7 +357,7 @@ tina4_python/ # Core framework package (v3.0.0)
356
357
  ├── dotenv/ # .env file loader
357
358
  ├── cli/ # CLI commands
358
359
  ├── HtmlElement.py # Programmatic HTML builder (HTMLElement, add_html_helpers)
359
- ├── Testing.py # Inline testing framework (tests, assert_equal, run_all_tests)
360
+ ├── Testing.py # Inline testing framework (tests, assert_equal, run_all)
360
361
  ├── templates/ # Built-in framework templates (Twig)
361
362
  ├── public/ # Built-in static assets
362
363
  └── scss/ # Built-in SCSS
@@ -853,17 +854,17 @@ user.save()
853
854
  user.delete()
854
855
 
855
856
  # Query
856
- result = User().select(filter="name = ?", params=["Alice"], limit=10)
857
+ result = User().select(sql="name = ?", params=["Alice"], limit=10)
857
858
 
858
859
  # Convert to dict
859
860
  user.to_dict()
860
861
 
861
- # NoSQL support: to_mongo() generates MongoDB query documents from the same fluent API
862
- result.to_mongo()
862
+ # NoSQL support: to_mongo() lives on QueryBuilder and generates MongoDB query documents from the same fluent API
863
+ User.query().where("name = ?", ["Alice"]).to_mongo()
863
864
  ```
864
865
 
865
866
  ### Available field types
866
- `IntegerField`, `StringField`, `TextField`, `DateTimeField`, `NumericField`, `BlobField`, `JSONBField`, `ForeignKeyField`
867
+ `IntegerField`, `StringField`, `TextField`, `DateTimeField`, `NumericField`, `BlobField`, `BooleanField`, `FloatField`, `ForeignKeyField`
867
868
 
868
869
  Import from `tina4_python` or `tina4_python.orm.fields`.
869
870
 
@@ -1187,7 +1188,8 @@ from tina4_python.graphql import GraphQL
1187
1188
  gql = GraphQL()
1188
1189
  gql.schema.from_orm(User)
1189
1190
  gql.schema.from_orm(Product)
1190
- gql.register_route("/graphql") # POST = queries, GET = GraphiQL IDE
1191
+ # The engine serves HTTP at gql.endpoint (default "/graphql", set via TINA4_GRAPHQL_ENDPOINT).
1192
+ # To wire it manually, call gql.execute_json(...) from a @post route.
1191
1193
  ```
1192
1194
 
1193
1195
  `from_orm()` creates: type, single query (`user(id: ID)`), list query (`users(limit, offset)`), create/update/delete mutations.
@@ -1196,16 +1198,18 @@ gql.register_route("/graphql") # POST = queries, GET = GraphiQL IDE
1196
1198
 
1197
1199
  ```python
1198
1200
  gql.schema.add_type("Widget", {"id": "ID", "name": "String", "price": "Float"})
1199
- gql.schema.add_query("widget", {
1200
- "type": "Widget",
1201
- "args": {"id": "ID"},
1202
- "resolve": lambda root, args, ctx: {"id": args["id"], "name": "Cog", "price": 5.0},
1203
- })
1204
- gql.schema.add_mutation("deleteWidget", {
1205
- "type": "Boolean",
1206
- "args": {"id": "ID!"},
1207
- "resolve": lambda root, args, ctx: True,
1208
- })
1201
+ gql.schema.add_query(
1202
+ "widget",
1203
+ args={"id": "ID"},
1204
+ return_type="Widget",
1205
+ resolver=lambda root, args, ctx: {"id": args["id"], "name": "Cog", "price": 5.0},
1206
+ )
1207
+ gql.schema.add_mutation(
1208
+ "deleteWidget",
1209
+ args={"id": "ID!"},
1210
+ return_type="Boolean",
1211
+ resolver=lambda root, args, ctx: True,
1212
+ )
1209
1213
  ```
1210
1214
 
1211
1215
  ### Programmatic usage (no HTTP)
@@ -1322,8 +1326,8 @@ uv run tina4python test # Discovers @tests in src/**/*.py
1322
1326
 
1323
1327
  Or programmatically:
1324
1328
  ```python
1325
- from tina4_python.Testing import run_all_tests
1326
- run_all_tests(quiet=False, failfast=False)
1329
+ from tina4_python.Testing import run_all
1330
+ run_all(quiet=False, failfast=False)
1327
1331
  ```
1328
1332
 
1329
1333
  ## Events — Decoupled Communication
@@ -1374,7 +1378,7 @@ clear() # remove all listeners for all events
1374
1378
  Detect AI coding tools in a project and install Tina4-aware context files so any assistant understands the framework.
1375
1379
 
1376
1380
  ```python
1377
- from tina4_python.ai import detect_ai, detect_ai_names, install_context, install_all, status_report
1381
+ from tina4_python.ai import detect_ai, detect_ai_names, install_context, install_selected, status_report
1378
1382
 
1379
1383
  # Detect which AI tools are present
1380
1384
  tools = detect_ai(".")
@@ -1383,12 +1387,15 @@ tools = detect_ai(".")
1383
1387
  # Just the names of detected tools
1384
1388
  names = detect_ai_names(".") # ["claude-code", "cursor"]
1385
1389
 
1386
- # Install context files for detected tools
1387
- created = install_context(".", tools=None, force=False)
1390
+ # Install context for ALL known AI tools (tools=None default)
1391
+ created = install_context(".")
1388
1392
  # ["CLAUDE.md", ".cursorules"]
1389
1393
 
1390
- # Install context for ALL known AI tools (not just detected)
1391
- created = install_all(".", force=False)
1394
+ # Install context for specific tools only
1395
+ created = install_context(".", tools=["claude-code", "cursor"])
1396
+
1397
+ # Or select by CSV/"all" string
1398
+ created = install_selected(".", "all")
1392
1399
 
1393
1400
  # Human-readable status report
1394
1401
  print(status_report("."))
@@ -1511,7 +1518,7 @@ HTMLElement("img", {"src": "/logo.png"}) # <img src="/logo.png">
1511
1518
  Attach test assertions directly to functions with the `@tests` decorator. Tests are registered globally and run via CLI or programmatically.
1512
1519
 
1513
1520
  ```python
1514
- from tina4_python.Testing import tests, assert_equal, assert_raises, assert_true, assert_false, run_all_tests
1521
+ from tina4_python.Testing import tests, assert_equal, assert_raises, assert_true, assert_false, run_all
1515
1522
 
1516
1523
  @tests(
1517
1524
  assert_equal((5, 3), 8), # add(5, 3) == 8
@@ -1534,7 +1541,7 @@ uv run tina4python test # Discovers @tests in src/**/*.py
1534
1541
 
1535
1542
  ```python
1536
1543
  # Programmatic execution
1537
- results = run_all_tests(quiet=False, failfast=False)
1544
+ results = run_all(quiet=False, failfast=False)
1538
1545
  # {"passed": 5, "failed": 0, "errors": 0, "details": [...]}
1539
1546
  ```
1540
1547
 
@@ -1769,7 +1776,7 @@ async def invite(request, response):
1769
1776
  queue = Queue(topic="emails")
1770
1777
  for job in queue.consume("emails"):
1771
1778
  email = job.data
1772
- html = Template.render(f"emails/{email['template']}.twig", email["data"])
1779
+ html = Frond().render(f"emails/{email['template']}.twig", email["data"])
1773
1780
  # ... send via SMTP
1774
1781
  job.complete()
1775
1782
  ```
@@ -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.14"
11
+ __version__ = "3.13.16"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -27,6 +27,16 @@ class DatabaseResult:
27
27
  def __len__(self):
28
28
  return self.count
29
29
 
30
+ def __getitem__(self, index):
31
+ """Index / slice access into the result rows.
32
+
33
+ v3.13.16: ``result[0]`` (and slicing) are documented (book ch5 §4
34
+ "Index Access") but used to raise ``TypeError: 'DatabaseResult'
35
+ object is not subscriptable``. Delegate to the materialised rows so
36
+ index and slice access behave like a list.
37
+ """
38
+ return self.records[index]
39
+
30
40
  def __bool__(self):
31
41
  return self.error is None
32
42
 
@@ -207,6 +207,35 @@ class PostgreSQLAdapter(DatabaseAdapter):
207
207
  except Exception:
208
208
  pass
209
209
 
210
+ def _end_read_txn(self):
211
+ """v3.13.15 (issue #51): close the implicit transaction a read
212
+ opens, so the connection doesn't leak as ``idle in transaction``.
213
+
214
+ psycopg2 runs with ``autocommit = False`` (the framework's safe
215
+ default — writes must be committed explicitly). A bare ``SELECT``
216
+ therefore opens a transaction too, and with nothing to commit it
217
+ sits ``idle in transaction`` for the life of the connection,
218
+ holding a pool slot and any locks it touched. Short-lived boot
219
+ connections (the migration runner's ``MAX(batch)`` lookup) leak
220
+ one each, eventually exhausting ``max_connections`` — at which
221
+ point module autodiscovery fails mid-boot and every route 404s
222
+ while ``/health-check`` still passes ("ready but broken").
223
+
224
+ A read has nothing to persist, so a ROLLBACK is the safe close.
225
+ Inside an explicit ``start_transaction()`` the caller owns the
226
+ transaction, so we defer (mirrors the heal / error-path guards).
227
+ Writes go through :meth:`execute`, which is deliberately left
228
+ untouched — with autocommit off they must be committed explicitly,
229
+ so auto-closing them here would silently drop data.
230
+ """
231
+ if self._in_transaction or self._conn is None:
232
+ return
233
+ try:
234
+ self._conn.rollback()
235
+ except Exception:
236
+ # Never let bookkeeping mask the rows we already fetched.
237
+ pass
238
+
210
239
  def connect(self, connection_string: str, username: str = "", password: str = "", **kwargs):
211
240
  """Connect to PostgreSQL.
212
241
 
@@ -357,6 +386,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
357
386
  self._exec_with_handling(cursor, paginated_sql, paginated_params)
358
387
  rows = [self._decode_blobs(dict(row)) for row in cursor.fetchall()]
359
388
 
389
+ # v3.13.15 (#51): rows are materialised above — close the implicit
390
+ # read transaction so the connection doesn't sit idle-in-transaction.
391
+ self._end_read_txn()
392
+
360
393
  return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
361
394
 
362
395
  def fetch_one(self, sql: str, params: list = None) -> dict | None:
@@ -368,7 +401,13 @@ class PostgreSQLAdapter(DatabaseAdapter):
368
401
  cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
369
402
  self._exec_with_handling(cursor, sql, params)
370
403
  row = cursor.fetchone()
371
- return self._decode_blobs(dict(row)) if row else None
404
+ result = self._decode_blobs(dict(row)) if row else None
405
+ # v3.13.15 (#51): close the implicit read transaction so the
406
+ # connection doesn't sit 'idle in transaction' (psycopg2 autocommit
407
+ # is off). This is the path the migration runner's MAX(batch)
408
+ # lookup leaked through.
409
+ self._end_read_txn()
410
+ return result
372
411
 
373
412
  @staticmethod
374
413
  def _decode_blobs(row: dict) -> dict:
@@ -496,7 +535,12 @@ class PostgreSQLAdapter(DatabaseAdapter):
496
535
  """
497
536
  sql = SQLTranslator.placeholder_style(sql, "%s")
498
537
  sql = SQLTranslator.auto_increment_syntax(sql, "postgresql")
499
- sql = SQLTranslator.boolean_to_int(sql)
538
+ # v3.13.16: do NOT run boolean_to_int on PostgreSQL — PG has a native
539
+ # BOOLEAN type, so TRUE/FALSE are valid literals and 1/0 are NOT
540
+ # interchangeable with them. Converting (e.g. `DEFAULT FALSE` -> `DEFAULT 0`,
541
+ # or `WHERE active = TRUE` -> `= 1`) raises "column is of type boolean but
542
+ # expression is of type integer" / "operator does not exist: boolean = integer".
543
+ # boolean_to_int stays for INTEGER/BIT-backed engines (SQLite, Firebird, MSSQL).
500
544
  return sql
501
545
 
502
546
  def _supports_returning(self) -> bool:
@@ -748,7 +748,10 @@ class ORM(metaclass=ORMMeta):
748
748
  # where it's reliable. SQLite and Firebird stay on INTEGER —
749
749
  # SQLite has no native bool, and Firebird's driver round-trip
750
750
  # for native BOOLEAN is uneven across versions.
751
- if engine == "postgres":
751
+ # v3.13.16: db.get_database_type() returns "postgresql" (with the -ql),
752
+ # so the old `== "postgres"` check never matched and BooleanField got
753
+ # INTEGER on PG — which then can't accept a Python bool on insert.
754
+ if engine in ("postgres", "postgresql"):
752
755
  bool_sql = "BOOLEAN"
753
756
  elif engine == "mysql":
754
757
  bool_sql = "BOOLEAN" # MySQL alias for TINYINT(1)
@@ -758,6 +761,16 @@ class ORM(metaclass=ORMMeta):
758
761
  # sqlite, firebird, odbc, anything else
759
762
  bool_sql = "INTEGER"
760
763
 
764
+ # v3.13.16: DateTimeField was emitted as "DATETIME" unconditionally,
765
+ # but PostgreSQL and Firebird have no DATETIME type — CREATE TABLE blew
766
+ # up with `type "datetime" does not exist`. Emit each engine's real
767
+ # timestamp type. (MySQL/MSSQL/SQLite keep DATETIME: it's valid there,
768
+ # and on MySQL it avoids TIMESTAMP's auto-update + 2038 surprises.)
769
+ if engine in ("postgres", "postgresql", "firebird"):
770
+ datetime_sql = "TIMESTAMP"
771
+ else:
772
+ datetime_sql = "DATETIME"
773
+
761
774
  # Don't recreate if table already exists
762
775
  if db.table_exists(table):
763
776
  return True
@@ -781,7 +794,7 @@ class ORM(metaclass=ORMMeta):
781
794
  elif kind == "BooleanField":
782
795
  sql_type = bool_sql
783
796
  elif kind == "DateTimeField":
784
- sql_type = "DATETIME"
797
+ sql_type = datetime_sql
785
798
  elif kind == "BlobField":
786
799
  sql_type = "BLOB"
787
800
  else:
@@ -809,7 +822,14 @@ class ORM(metaclass=ORMMeta):
809
822
  if isinstance(default_val, str):
810
823
  parts.append(f"DEFAULT '{default_val}'")
811
824
  elif isinstance(default_val, bool):
812
- parts.append(f"DEFAULT {1 if default_val else 0}")
825
+ # v3.13.16: a native BOOLEAN column (PG/MySQL) needs
826
+ # TRUE/FALSE; INTEGER- and BIT-backed bools (SQLite,
827
+ # Firebird, MSSQL) need 1/0. `DEFAULT 0` on a PG BOOLEAN
828
+ # raises "default expression is of type integer".
829
+ if bool_sql == "BOOLEAN":
830
+ parts.append(f"DEFAULT {'TRUE' if default_val else 'FALSE'}")
831
+ else:
832
+ parts.append(f"DEFAULT {1 if default_val else 0}")
813
833
  else:
814
834
  parts.append(f"DEFAULT {default_val}")
815
835
 
@@ -821,8 +841,16 @@ class ORM(metaclass=ORMMeta):
821
841
  engine = db.get_database_type()
822
842
  sql = SQLTranslator.auto_increment_syntax(sql, engine)
823
843
 
824
- db.execute(sql)
844
+ # v3.13.16: don't claim success when the DDL failed. db.execute()
845
+ # swallows the driver error into get_error() and returns False, so a
846
+ # bad type (or any DDL error) used to leave create_table() returning
847
+ # True while no table was actually created — a silent, misleading pass.
848
+ ok = db.execute(sql)
825
849
  db.commit()
850
+ if ok is False:
851
+ from tina4_python.debug import Log
852
+ Log.error(f"create_table failed for {table}: {db.get_error()}", sql=sql)
853
+ return False
826
854
  return True
827
855
 
828
856
  # ── Cached Queries ────────────────────────────────────────
File without changes