tina4-python 3.13.15__tar.gz → 3.13.19__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.15 → tina4_python-3.13.19}/PKG-INFO +1 -1
  2. {tina4_python-3.13.15 → tina4_python-3.13.19}/pyproject.toml +1 -1
  3. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/CLAUDE.md +65 -35
  4. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/__init__.py +2 -2
  5. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/response.py +33 -1
  6. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/adapter.py +10 -0
  7. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/postgres.py +6 -1
  8. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/orm/__init__.py +2 -2
  9. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/orm/model.py +53 -15
  10. {tina4_python-3.13.15 → tina4_python-3.13.19}/.gitignore +0 -0
  11. {tina4_python-3.13.15 → tina4_python-3.13.19}/README.md +0 -0
  12. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/HtmlElement.py +0 -0
  13. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/Testing.py +0 -0
  14. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/ai/__init__.py +0 -0
  15. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/api/__init__.py +0 -0
  16. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/auth/__init__.py +0 -0
  17. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/cache/__init__.py +0 -0
  18. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/cli/__init__.py +0 -0
  19. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/container/__init__.py +0 -0
  20. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/__init__.py +0 -0
  21. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/cache.py +0 -0
  22. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/constants.py +0 -0
  23. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/events.py +0 -0
  24. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/middleware.py +0 -0
  25. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/rate_limiter.py +0 -0
  26. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/request.py +0 -0
  27. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/router.py +0 -0
  28. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/server.py +0 -0
  29. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/crud/__init__.py +0 -0
  30. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/__init__.py +0 -0
  31. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/connection.py +0 -0
  32. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/firebird.py +0 -0
  33. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/mongodb.py +0 -0
  34. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/mssql.py +0 -0
  35. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/mysql.py +0 -0
  36. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/odbc.py +0 -0
  37. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/sqlite.py +0 -0
  38. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/debug/__init__.py +0 -0
  39. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/debug/error_overlay.py +0 -0
  40. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/dev_admin/__init__.py +0 -0
  41. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/dev_admin/metrics.py +0 -0
  42. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/dev_admin/plan.py +0 -0
  43. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/dev_admin/project_index.py +0 -0
  44. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/docs.py +0 -0
  45. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/dotenv/__init__.py +0 -0
  46. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/env.py +0 -0
  47. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/frond/FROND.md +0 -0
  48. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/frond/__init__.py +0 -0
  49. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/frond/engine.py +0 -0
  50. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/auth/meta.json +0 -0
  51. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  52. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/database/meta.json +0 -0
  53. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  54. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/error-overlay/meta.json +0 -0
  55. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  56. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/orm/meta.json +0 -0
  57. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  58. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  59. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/queue/meta.json +0 -0
  60. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  61. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/rest-api/meta.json +0 -0
  62. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  63. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/templates/meta.json +0 -0
  64. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  65. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  66. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/graphql/__init__.py +0 -0
  67. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/i18n/__init__.py +0 -0
  68. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/mcp/__init__.py +0 -0
  69. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/mcp/protocol.py +0 -0
  70. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/mcp/tools.py +0 -0
  71. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/messenger/__init__.py +0 -0
  72. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/migration/__init__.py +0 -0
  73. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/migration/runner.py +0 -0
  74. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/orm/fields.py +0 -0
  75. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/__feedback/widget.js +0 -0
  76. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/frond.js +0 -0
  82. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/frond.min.js +0 -0
  83. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  84. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  85. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/tina4.min.js +0 -0
  86. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/tina4js.min.js +0 -0
  87. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/swagger/index.html +0 -0
  88. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  89. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/query_builder/__init__.py +0 -0
  90. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/__init__.py +0 -0
  91. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/job.py +0 -0
  92. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/kafka_backend.py +0 -0
  93. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/lite_backend.py +0 -0
  94. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/mongo_backend.py +0 -0
  95. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue_backends/__init__.py +0 -0
  97. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue_backends/kafka_backend.py +0 -0
  98. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue_backends/mongo_backend.py +0 -0
  99. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/__init__.py +0 -0
  101. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  102. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_badges.scss +0 -0
  103. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  104. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_cards.scss +0 -0
  105. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_forms.scss +0 -0
  106. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_grid.scss +0 -0
  107. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_modals.scss +0 -0
  108. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_nav.scss +0 -0
  109. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_reset.scss +0 -0
  110. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_tables.scss +0 -0
  111. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_typography.scss +0 -0
  112. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  113. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_variables.scss +0 -0
  114. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/base.scss +0 -0
  115. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/colors.scss +0 -0
  116. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/tina4.scss +0 -0
  117. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/seeder/__init__.py +0 -0
  118. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/service/__init__.py +0 -0
  119. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session/__init__.py +0 -0
  120. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.15 → tina4_python-3.13.19}/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.15
3
+ Version: 3.13.19
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.15"
3
+ version = "3.13.19"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -128,13 +128,13 @@ Rules:
128
128
  ```python
129
129
  # app.py
130
130
  from tina4_python.core import run
131
- from tina4_python.orm import orm_bind
131
+ from tina4_python.orm import bind_database
132
132
  from tina4_python.frond import Frond
133
133
  from tina4_python.database import Database
134
134
 
135
135
  # 1. Database & ORM
136
136
  db = Database("sqlite:///app.db")
137
- orm_bind(db)
137
+ bind_database(db)
138
138
 
139
139
  # 2. Custom Twig filters
140
140
  Frond.add_filter("money", lambda v: f"{float(v or 0):,.2f}")
@@ -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`
@@ -327,7 +328,7 @@ tina4_python/ # Core framework package (v3.0.0)
327
328
  │ ├── connection.py # Database class (URL-based connection)
328
329
  │ ├── adapter.py # DatabaseAdapter, DatabaseResult, SQLTranslator
329
330
  │ ├── sqlite.py, postgres.py, mysql.py, mssql.py, firebird.py, odbc.py
330
- ├── orm/ # Active Record ORM (ORM, Field, orm_bind)
331
+ ├── orm/ # Active Record ORM (ORM, Field, bind_database)
331
332
  │ ├── model.py # ORM base class
332
333
  │ └── fields.py # IntegerField, StringField, etc.
333
334
  ├── frond/ # Template engine (Frond — Jinja2/Twig-compatible)
@@ -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
@@ -478,6 +479,9 @@ request.session # Session object — use .get(key) and .set(key, value)
478
479
 
479
480
  ```python
480
481
  return response({"data": []}) # JSON (auto-detected from dict/list)
482
+ return response(User.find(1)) # ORM model -> JSON object
483
+ return response(User.all()) # list of models -> JSON array
484
+ return response(db.fetch("SELECT * FROM users")) # DatabaseResult -> JSON array
481
485
  return response("<h1>Hello</h1>") # HTML
482
486
  return response("Not found", 404) # With status code
483
487
  return response.redirect("/login") # Redirect
@@ -486,6 +490,8 @@ return response.file("doc.pdf") # Serve a file
486
490
  return response.stream(generator) # SSE/streaming response (text/event-stream)
487
491
  ```
488
492
 
493
+ `response()` auto-serializes a domain object — an ORM model, a list of models, or a `DatabaseResult` — to JSON with no manual `to_dict()` / `to_json()`. A single model becomes a JSON object; a list of models or a `DatabaseResult` becomes a JSON array. Dicts, lists and strings behave exactly as before (purely additive).
494
+
489
495
  Add custom headers before returning:
490
496
  ```python
491
497
  from tina4_python.core.response import Response
@@ -820,19 +826,37 @@ class User(ORM):
820
826
  email = StringField()
821
827
  ```
822
828
 
823
- Initialize in `app.py`:
829
+ **Binding the database:**
830
+
831
+ - **`.env` default (no call needed)** — models auto-bind to `TINA4_DATABASE_URL` when it is set, so most apps need no binding call at all.
832
+ - **`bind_database(db)`** — override the default explicitly; assigns the connection to all ORM subclasses that don't select a named one.
833
+ - **`bind_database(db, name="analytics")` + `_db = "analytics"`** — register a named/secondary connection and point a model at it. A missing named connection raises a clear error.
834
+
824
835
  ```python
825
- from tina4_python.orm import orm_bind
836
+ from tina4_python.orm import bind_database
826
837
  from tina4_python.database import Database
827
838
 
828
- orm_bind(Database("sqlite:///app.db")) # Assigns DB to all ORM subclasses
839
+ # Most apps: nothing to do the .env default (TINA4_DATABASE_URL) is auto-bound.
840
+
841
+ # Override the default explicitly:
842
+ bind_database(Database("sqlite:///app.db")) # Assigns DB to all ORM subclasses
843
+
844
+ # Register a NAMED connection and point a model at it:
845
+ bind_database(Database("postgres://…/analytics", "u", "p"), name="analytics")
846
+
847
+ class Visit(ORM):
848
+ _db = "analytics" # this model uses the analytics connection
829
849
  ```
830
850
 
831
851
  ### ORM operations
832
852
 
853
+ The constructor accepts a dict, a JSON object string, or keyword args. Passing a list/array raises a clear `TypeError` (map over the list to build many records).
854
+
833
855
  ```python
834
856
  # Create
835
- user = User({"name": "Alice", "email": "alice@example.com"})
857
+ user = User({"name": "Alice", "email": "alice@example.com"}) # dict
858
+ user = User('{"name": "Alice", "email": "alice@example.com"}') # JSON object string
859
+ user = User(name="Alice", email="alice@example.com") # keyword args
836
860
  user.save()
837
861
 
838
862
  # Load — alias for select_one (class method, returns instance or None)
@@ -853,17 +877,17 @@ user.save()
853
877
  user.delete()
854
878
 
855
879
  # Query
856
- result = User().select(filter="name = ?", params=["Alice"], limit=10)
880
+ result = User().select(sql="name = ?", params=["Alice"], limit=10)
857
881
 
858
882
  # Convert to dict
859
883
  user.to_dict()
860
884
 
861
- # NoSQL support: to_mongo() generates MongoDB query documents from the same fluent API
862
- result.to_mongo()
885
+ # NoSQL support: to_mongo() lives on QueryBuilder and generates MongoDB query documents from the same fluent API
886
+ User.query().where("name = ?", ["Alice"]).to_mongo()
863
887
  ```
864
888
 
865
889
  ### Available field types
866
- `IntegerField`, `StringField`, `TextField`, `DateTimeField`, `NumericField`, `BlobField`, `JSONBField`, `ForeignKeyField`
890
+ `IntegerField`, `StringField`, `TextField`, `DateTimeField`, `NumericField`, `BlobField`, `BooleanField`, `FloatField`, `ForeignKeyField`
867
891
 
868
892
  Import from `tina4_python` or `tina4_python.orm.fields`.
869
893
 
@@ -1187,7 +1211,8 @@ from tina4_python.graphql import GraphQL
1187
1211
  gql = GraphQL()
1188
1212
  gql.schema.from_orm(User)
1189
1213
  gql.schema.from_orm(Product)
1190
- gql.register_route("/graphql") # POST = queries, GET = GraphiQL IDE
1214
+ # The engine serves HTTP at gql.endpoint (default "/graphql", set via TINA4_GRAPHQL_ENDPOINT).
1215
+ # To wire it manually, call gql.execute_json(...) from a @post route.
1191
1216
  ```
1192
1217
 
1193
1218
  `from_orm()` creates: type, single query (`user(id: ID)`), list query (`users(limit, offset)`), create/update/delete mutations.
@@ -1196,16 +1221,18 @@ gql.register_route("/graphql") # POST = queries, GET = GraphiQL IDE
1196
1221
 
1197
1222
  ```python
1198
1223
  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
- })
1224
+ gql.schema.add_query(
1225
+ "widget",
1226
+ args={"id": "ID"},
1227
+ return_type="Widget",
1228
+ resolver=lambda root, args, ctx: {"id": args["id"], "name": "Cog", "price": 5.0},
1229
+ )
1230
+ gql.schema.add_mutation(
1231
+ "deleteWidget",
1232
+ args={"id": "ID!"},
1233
+ return_type="Boolean",
1234
+ resolver=lambda root, args, ctx: True,
1235
+ )
1209
1236
  ```
1210
1237
 
1211
1238
  ### Programmatic usage (no HTTP)
@@ -1322,8 +1349,8 @@ uv run tina4python test # Discovers @tests in src/**/*.py
1322
1349
 
1323
1350
  Or programmatically:
1324
1351
  ```python
1325
- from tina4_python.Testing import run_all_tests
1326
- run_all_tests(quiet=False, failfast=False)
1352
+ from tina4_python.Testing import run_all
1353
+ run_all(quiet=False, failfast=False)
1327
1354
  ```
1328
1355
 
1329
1356
  ## Events — Decoupled Communication
@@ -1374,7 +1401,7 @@ clear() # remove all listeners for all events
1374
1401
  Detect AI coding tools in a project and install Tina4-aware context files so any assistant understands the framework.
1375
1402
 
1376
1403
  ```python
1377
- from tina4_python.ai import detect_ai, detect_ai_names, install_context, install_all, status_report
1404
+ from tina4_python.ai import detect_ai, detect_ai_names, install_context, install_selected, status_report
1378
1405
 
1379
1406
  # Detect which AI tools are present
1380
1407
  tools = detect_ai(".")
@@ -1383,12 +1410,15 @@ tools = detect_ai(".")
1383
1410
  # Just the names of detected tools
1384
1411
  names = detect_ai_names(".") # ["claude-code", "cursor"]
1385
1412
 
1386
- # Install context files for detected tools
1387
- created = install_context(".", tools=None, force=False)
1413
+ # Install context for ALL known AI tools (tools=None default)
1414
+ created = install_context(".")
1388
1415
  # ["CLAUDE.md", ".cursorules"]
1389
1416
 
1390
- # Install context for ALL known AI tools (not just detected)
1391
- created = install_all(".", force=False)
1417
+ # Install context for specific tools only
1418
+ created = install_context(".", tools=["claude-code", "cursor"])
1419
+
1420
+ # Or select by CSV/"all" string
1421
+ created = install_selected(".", "all")
1392
1422
 
1393
1423
  # Human-readable status report
1394
1424
  print(status_report("."))
@@ -1511,7 +1541,7 @@ HTMLElement("img", {"src": "/logo.png"}) # <img src="/logo.png">
1511
1541
  Attach test assertions directly to functions with the `@tests` decorator. Tests are registered globally and run via CLI or programmatically.
1512
1542
 
1513
1543
  ```python
1514
- from tina4_python.Testing import tests, assert_equal, assert_raises, assert_true, assert_false, run_all_tests
1544
+ from tina4_python.Testing import tests, assert_equal, assert_raises, assert_true, assert_false, run_all
1515
1545
 
1516
1546
  @tests(
1517
1547
  assert_equal((5, 3), 8), # add(5, 3) == 8
@@ -1534,7 +1564,7 @@ uv run tina4python test # Discovers @tests in src/**/*.py
1534
1564
 
1535
1565
  ```python
1536
1566
  # Programmatic execution
1537
- results = run_all_tests(quiet=False, failfast=False)
1567
+ results = run_all(quiet=False, failfast=False)
1538
1568
  # {"passed": 5, "failed": 0, "errors": 0, "details": [...]}
1539
1569
  ```
1540
1570
 
@@ -1769,7 +1799,7 @@ async def invite(request, response):
1769
1799
  queue = Queue(topic="emails")
1770
1800
  for job in queue.consume("emails"):
1771
1801
  email = job.data
1772
- html = Template.render(f"emails/{email['template']}.twig", email["data"])
1802
+ html = Frond().render(f"emails/{email['template']}.twig", email["data"])
1773
1803
  # ... send via SMTP
1774
1804
  job.complete()
1775
1805
  ```
@@ -1806,7 +1836,7 @@ async def dashboard(request, response):
1806
1836
  ## v3 Features Summary
1807
1837
 
1808
1838
  - **55 built-in features**, zero third-party dependencies
1809
- - **2,299 tests** passing across all modules
1839
+ - **2,852 tests** passing across all modules
1810
1840
  - **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
1811
1841
  - **`tina4python generate`**: model, route, migration, middleware scaffolding
1812
1842
  - **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), query caching (`TINA4_DB_CACHE=true`, `cache_stats()`, `cache_clear()`)
@@ -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.15"
11
+ __version__ = "3.13.19"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -34,7 +34,7 @@ from tina4_python.database import Database # noqa: E402, F401
34
34
 
35
35
  # ── ORM ──
36
36
  from tina4_python.orm import ( # noqa: E402, F401
37
- ORM, orm_bind, Field,
37
+ ORM, bind_database, Field,
38
38
  IntegerField, StringField, BooleanField, FloatField,
39
39
  DateTimeField, TextField, BlobField, NumericField,
40
40
  ForeignKeyField,
@@ -70,6 +70,34 @@ def set_frond(engine):
70
70
  _global_frond = engine
71
71
 
72
72
 
73
+ def _to_jsonable(data):
74
+ """Normalise domain objects into JSON-serialisable structures.
75
+
76
+ Lets a route hand domain objects straight to ``response(...)`` /
77
+ ``response.json(...)`` without calling ``.to_dict()``/``.to_json()`` by hand:
78
+
79
+ return response(user) # ORM model -> dict
80
+ return response(User.all()) # list[ORM] -> list[dict]
81
+ return response(db.fetch(sql)) # DatabaseResult -> list[dict]
82
+
83
+ Plain ``dict`` / ``str`` / ``bytes`` / ``None`` pass through unchanged, so
84
+ existing handlers behave exactly as before.
85
+ """
86
+ if data is None or isinstance(data, (dict, str, bytes)):
87
+ return data
88
+ # Query result (DatabaseResult): exposes both a ``records`` list and a
89
+ # ``to_array`` method — the pair distinguishes it from a plain {"records": …} dict.
90
+ if isinstance(getattr(data, "records", None), list) and callable(getattr(data, "to_array", None)):
91
+ return data.records
92
+ # ORM model: duck-typed on a callable ``to_dict``.
93
+ if callable(getattr(data, "to_dict", None)):
94
+ return data.to_dict()
95
+ # Collections: normalise each element (list of models -> list of dicts).
96
+ if isinstance(data, (list, tuple)):
97
+ return [_to_jsonable(item) for item in data]
98
+ return data
99
+
100
+
73
101
  class Response:
74
102
  """HTTP response builder with compression and ETag support."""
75
103
 
@@ -107,6 +135,10 @@ class Response:
107
135
  for k, v in headers.items():
108
136
  self._headers.append((k, v))
109
137
 
138
+ # Normalise ORM models / collections / query results so handlers can
139
+ # `return response(model)` without serialising by hand.
140
+ data = _to_jsonable(data)
141
+
110
142
  if content_type:
111
143
  # Explicit content type provided
112
144
  self.content_type = content_type
@@ -232,7 +264,7 @@ class Response:
232
264
  if status_code:
233
265
  self.status_code = status_code
234
266
  self.content_type = "application/json"
235
- self.content = json.dumps(data, default=str, separators=(",", ":")).encode()
267
+ self.content = json.dumps(_to_jsonable(data), default=str, separators=(",", ":")).encode()
236
268
  return self
237
269
 
238
270
  def html(self, content: str, status_code: int = None) -> "Response":
@@ -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
 
@@ -535,7 +535,12 @@ class PostgreSQLAdapter(DatabaseAdapter):
535
535
  """
536
536
  sql = SQLTranslator.placeholder_style(sql, "%s")
537
537
  sql = SQLTranslator.auto_increment_syntax(sql, "postgresql")
538
- 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).
539
544
  return sql
540
545
 
541
546
  def _supports_returning(self) -> bool:
@@ -24,10 +24,10 @@ from tina4_python.orm.fields import (
24
24
  IntField, StrField, BoolField, # short aliases
25
25
  has_many, has_one, belongs_to, # relationship descriptors
26
26
  )
27
- from tina4_python.orm.model import ORM, orm_bind
27
+ from tina4_python.orm.model import ORM, bind_database
28
28
 
29
29
  __all__ = [
30
- "ORM", "orm_bind",
30
+ "ORM", "bind_database",
31
31
  "Field",
32
32
  # Verbose (preferred)
33
33
  "IntegerField", "StringField", "BooleanField", "FloatField",
@@ -17,13 +17,13 @@ from tina4_python.core.cache import Cache
17
17
  # Module-level query cache — shared across all ORM models
18
18
  _query_cache = Cache(default_ttl=0, max_size=500)
19
19
 
20
- # Global database reference — set via orm_bind()
20
+ # Global database reference — set via bind_database()
21
21
  _database = None
22
22
  # Named database connections registry
23
23
  _databases: dict[str, object] = {}
24
24
 
25
25
 
26
- def orm_bind(db, name: str = None):
26
+ def bind_database(db, name: str = None):
27
27
  """Bind a Database instance to ORM models.
28
28
 
29
29
  Args:
@@ -32,11 +32,11 @@ def orm_bind(db, name: str = None):
32
32
  If None, sets the global default used by all models without _db.
33
33
 
34
34
  Usage:
35
- orm_bind(db_main) # default for all models
36
- orm_bind(db_audit, name="audit") # named connection
35
+ bind_database(db_main) # default for all models
36
+ bind_database(db_audit, name="audit") # named connection
37
37
 
38
38
  # Decorator style — class is returned unchanged:
39
- @orm_bind(db)
39
+ @bind_database(db)
40
40
  class User(ORM):
41
41
  ...
42
42
 
@@ -49,7 +49,7 @@ def orm_bind(db, name: str = None):
49
49
  else:
50
50
  _databases[name] = db
51
51
 
52
- # Return a pass-through decorator so @orm_bind(db) syntax works.
52
+ # Return a pass-through decorator so @bind_database(db) syntax works.
53
53
  # Without this the decorator would set the class to None.
54
54
  def _decorator(cls):
55
55
  return cls
@@ -191,6 +191,16 @@ class ORM(metaclass=ORMMeta):
191
191
  import json
192
192
  data = json.loads(data)
193
193
 
194
+ # A single model is one record — reject a list/array with a clear
195
+ # message instead of a cryptic "'list' object has no attribute 'items'".
196
+ if data is not None and not isinstance(data, dict):
197
+ raise TypeError(
198
+ f"{type(self).__name__}() expects a dict, a JSON object string, or "
199
+ f"keyword args for one record — got {type(data).__name__}. To build "
200
+ f"many records, map over the list: "
201
+ f"[{type(self).__name__}(row) for row in rows]."
202
+ )
203
+
194
204
  # Populate from dict or kwargs
195
205
  if data:
196
206
  self._populate(data)
@@ -269,7 +279,7 @@ class ORM(metaclass=ORMMeta):
269
279
  Resolution order:
270
280
  1. cls._db as a Database instance (direct assignment)
271
281
  2. cls._db as a string name → look up in _databases registry
272
- 3. Global _database (set via orm_bind(db))
282
+ 3. Global _database (set via bind_database(db))
273
283
  """
274
284
  if cls._db is not None:
275
285
  if isinstance(cls._db, str):
@@ -277,7 +287,7 @@ class ORM(metaclass=ORMMeta):
277
287
  if db is None:
278
288
  raise RuntimeError(
279
289
  f"Named database '{cls._db}' not found. "
280
- f"Call orm_bind(db, name='{cls._db}') first."
290
+ f"Call bind_database(db, name='{cls._db}') first."
281
291
  )
282
292
  return db
283
293
  return cls._db # Direct Database instance
@@ -291,10 +301,10 @@ class ORM(metaclass=ORMMeta):
291
301
  username = os.environ.get("TINA4_DATABASE_USERNAME", "")
292
302
  password = os.environ.get("TINA4_DATABASE_PASSWORD", "")
293
303
  db = Database(url, username, password)
294
- orm_bind(db)
304
+ bind_database(db)
295
305
  return db
296
306
  raise RuntimeError(
297
- "No database bound. Call orm_bind(db) or set TINA4_DATABASE_URL in .env"
307
+ "No database bound. Call bind_database(db) or set TINA4_DATABASE_URL in .env"
298
308
  )
299
309
  return _database
300
310
 
@@ -748,7 +758,10 @@ class ORM(metaclass=ORMMeta):
748
758
  # where it's reliable. SQLite and Firebird stay on INTEGER —
749
759
  # SQLite has no native bool, and Firebird's driver round-trip
750
760
  # for native BOOLEAN is uneven across versions.
751
- if engine == "postgres":
761
+ # v3.13.16: db.get_database_type() returns "postgresql" (with the -ql),
762
+ # so the old `== "postgres"` check never matched and BooleanField got
763
+ # INTEGER on PG — which then can't accept a Python bool on insert.
764
+ if engine in ("postgres", "postgresql"):
752
765
  bool_sql = "BOOLEAN"
753
766
  elif engine == "mysql":
754
767
  bool_sql = "BOOLEAN" # MySQL alias for TINYINT(1)
@@ -758,6 +771,16 @@ class ORM(metaclass=ORMMeta):
758
771
  # sqlite, firebird, odbc, anything else
759
772
  bool_sql = "INTEGER"
760
773
 
774
+ # v3.13.16: DateTimeField was emitted as "DATETIME" unconditionally,
775
+ # but PostgreSQL and Firebird have no DATETIME type — CREATE TABLE blew
776
+ # up with `type "datetime" does not exist`. Emit each engine's real
777
+ # timestamp type. (MySQL/MSSQL/SQLite keep DATETIME: it's valid there,
778
+ # and on MySQL it avoids TIMESTAMP's auto-update + 2038 surprises.)
779
+ if engine in ("postgres", "postgresql", "firebird"):
780
+ datetime_sql = "TIMESTAMP"
781
+ else:
782
+ datetime_sql = "DATETIME"
783
+
761
784
  # Don't recreate if table already exists
762
785
  if db.table_exists(table):
763
786
  return True
@@ -781,7 +804,7 @@ class ORM(metaclass=ORMMeta):
781
804
  elif kind == "BooleanField":
782
805
  sql_type = bool_sql
783
806
  elif kind == "DateTimeField":
784
- sql_type = "DATETIME"
807
+ sql_type = datetime_sql
785
808
  elif kind == "BlobField":
786
809
  sql_type = "BLOB"
787
810
  else:
@@ -809,7 +832,14 @@ class ORM(metaclass=ORMMeta):
809
832
  if isinstance(default_val, str):
810
833
  parts.append(f"DEFAULT '{default_val}'")
811
834
  elif isinstance(default_val, bool):
812
- parts.append(f"DEFAULT {1 if default_val else 0}")
835
+ # v3.13.16: a native BOOLEAN column (PG/MySQL) needs
836
+ # TRUE/FALSE; INTEGER- and BIT-backed bools (SQLite,
837
+ # Firebird, MSSQL) need 1/0. `DEFAULT 0` on a PG BOOLEAN
838
+ # raises "default expression is of type integer".
839
+ if bool_sql == "BOOLEAN":
840
+ parts.append(f"DEFAULT {'TRUE' if default_val else 'FALSE'}")
841
+ else:
842
+ parts.append(f"DEFAULT {1 if default_val else 0}")
813
843
  else:
814
844
  parts.append(f"DEFAULT {default_val}")
815
845
 
@@ -821,8 +851,16 @@ class ORM(metaclass=ORMMeta):
821
851
  engine = db.get_database_type()
822
852
  sql = SQLTranslator.auto_increment_syntax(sql, engine)
823
853
 
824
- db.execute(sql)
854
+ # v3.13.16: don't claim success when the DDL failed. db.execute()
855
+ # swallows the driver error into get_error() and returns False, so a
856
+ # bad type (or any DDL error) used to leave create_table() returning
857
+ # True while no table was actually created — a silent, misleading pass.
858
+ ok = db.execute(sql)
825
859
  db.commit()
860
+ if ok is False:
861
+ from tina4_python.debug import Log
862
+ Log.error(f"create_table failed for {table}: {db.get_error()}", sql=sql)
863
+ return False
826
864
  return True
827
865
 
828
866
  # ── Cached Queries ────────────────────────────────────────
@@ -862,7 +900,7 @@ class ORM(metaclass=ORMMeta):
862
900
  Resolution order matches _get_db():
863
901
  1. cls._db as a Database instance
864
902
  2. cls._db as a named string → registry lookup
865
- 3. Global default set via orm_bind()
903
+ 3. Global default set via bind_database()
866
904
  """
867
905
  return cls._get_db()
868
906
 
File without changes