tina4-python 3.13.16__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.16 → tina4_python-3.13.19}/PKG-INFO +1 -1
  2. {tina4_python-3.13.16 → tina4_python-3.13.19}/pyproject.toml +1 -1
  3. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/CLAUDE.md +31 -8
  4. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/__init__.py +2 -2
  5. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/response.py +33 -1
  6. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/orm/__init__.py +2 -2
  7. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/orm/model.py +21 -11
  8. {tina4_python-3.13.16 → tina4_python-3.13.19}/.gitignore +0 -0
  9. {tina4_python-3.13.16 → tina4_python-3.13.19}/README.md +0 -0
  10. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/HtmlElement.py +0 -0
  11. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/Testing.py +0 -0
  12. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/ai/__init__.py +0 -0
  13. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/cli/__init__.py +0 -0
  17. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/container/__init__.py +0 -0
  18. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/__init__.py +0 -0
  19. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/rate_limiter.py +0 -0
  24. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/request.py +0 -0
  25. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/router.py +0 -0
  26. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/server.py +0 -0
  27. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/crud/__init__.py +0 -0
  28. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/__init__.py +0 -0
  29. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/adapter.py +0 -0
  30. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/connection.py +0 -0
  31. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/firebird.py +0 -0
  32. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/mongodb.py +0 -0
  33. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/mssql.py +0 -0
  34. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/mysql.py +0 -0
  35. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/odbc.py +0 -0
  36. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/postgres.py +0 -0
  37. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/sqlite.py +0 -0
  38. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/debug/__init__.py +0 -0
  39. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/debug/error_overlay.py +0 -0
  40. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/dev_admin/__init__.py +0 -0
  41. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/dev_admin/metrics.py +0 -0
  42. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/dev_admin/plan.py +0 -0
  43. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/dev_admin/project_index.py +0 -0
  44. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/docs.py +0 -0
  45. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/dotenv/__init__.py +0 -0
  46. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/env.py +0 -0
  47. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/frond/FROND.md +0 -0
  48. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/frond/__init__.py +0 -0
  49. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/frond/engine.py +0 -0
  50. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/auth/meta.json +0 -0
  51. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  52. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/database/meta.json +0 -0
  53. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  54. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/error-overlay/meta.json +0 -0
  55. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  56. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/orm/meta.json +0 -0
  57. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  58. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  59. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/queue/meta.json +0 -0
  60. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  61. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/rest-api/meta.json +0 -0
  62. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  63. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/templates/meta.json +0 -0
  64. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  65. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  66. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/graphql/__init__.py +0 -0
  67. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/i18n/__init__.py +0 -0
  68. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/mcp/__init__.py +0 -0
  69. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/mcp/protocol.py +0 -0
  70. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/mcp/tools.py +0 -0
  71. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/messenger/__init__.py +0 -0
  72. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/migration/__init__.py +0 -0
  73. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/migration/runner.py +0 -0
  74. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/orm/fields.py +0 -0
  75. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/__feedback/widget.js +0 -0
  76. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/frond.js +0 -0
  82. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/frond.min.js +0 -0
  83. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  84. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  85. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/tina4.min.js +0 -0
  86. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/tina4js.min.js +0 -0
  87. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/swagger/index.html +0 -0
  88. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  89. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/query_builder/__init__.py +0 -0
  90. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/__init__.py +0 -0
  91. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/job.py +0 -0
  92. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/kafka_backend.py +0 -0
  93. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/lite_backend.py +0 -0
  94. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/mongo_backend.py +0 -0
  95. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue_backends/__init__.py +0 -0
  97. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue_backends/kafka_backend.py +0 -0
  98. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue_backends/mongo_backend.py +0 -0
  99. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/__init__.py +0 -0
  101. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  102. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_badges.scss +0 -0
  103. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  104. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_cards.scss +0 -0
  105. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_forms.scss +0 -0
  106. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_grid.scss +0 -0
  107. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_modals.scss +0 -0
  108. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_nav.scss +0 -0
  109. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_reset.scss +0 -0
  110. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_tables.scss +0 -0
  111. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_typography.scss +0 -0
  112. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  113. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_variables.scss +0 -0
  114. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/base.scss +0 -0
  115. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/colors.scss +0 -0
  116. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/tina4.scss +0 -0
  117. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/seeder/__init__.py +0 -0
  118. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/service/__init__.py +0 -0
  119. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session/__init__.py +0 -0
  120. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.16 → 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.16
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.16"
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}")
@@ -328,7 +328,7 @@ tina4_python/ # Core framework package (v3.0.0)
328
328
  │ ├── connection.py # Database class (URL-based connection)
329
329
  │ ├── adapter.py # DatabaseAdapter, DatabaseResult, SQLTranslator
330
330
  │ ├── sqlite.py, postgres.py, mysql.py, mssql.py, firebird.py, odbc.py
331
- ├── orm/ # Active Record ORM (ORM, Field, orm_bind)
331
+ ├── orm/ # Active Record ORM (ORM, Field, bind_database)
332
332
  │ ├── model.py # ORM base class
333
333
  │ └── fields.py # IntegerField, StringField, etc.
334
334
  ├── frond/ # Template engine (Frond — Jinja2/Twig-compatible)
@@ -479,6 +479,9 @@ request.session # Session object — use .get(key) and .set(key, value)
479
479
 
480
480
  ```python
481
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
482
485
  return response("<h1>Hello</h1>") # HTML
483
486
  return response("Not found", 404) # With status code
484
487
  return response.redirect("/login") # Redirect
@@ -487,6 +490,8 @@ return response.file("doc.pdf") # Serve a file
487
490
  return response.stream(generator) # SSE/streaming response (text/event-stream)
488
491
  ```
489
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
+
490
495
  Add custom headers before returning:
491
496
  ```python
492
497
  from tina4_python.core.response import Response
@@ -821,19 +826,37 @@ class User(ORM):
821
826
  email = StringField()
822
827
  ```
823
828
 
824
- 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
+
825
835
  ```python
826
- from tina4_python.orm import orm_bind
836
+ from tina4_python.orm import bind_database
827
837
  from tina4_python.database import Database
828
838
 
829
- 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
830
849
  ```
831
850
 
832
851
  ### ORM operations
833
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
+
834
855
  ```python
835
856
  # Create
836
- 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
837
860
  user.save()
838
861
 
839
862
  # Load — alias for select_one (class method, returns instance or None)
@@ -1813,7 +1836,7 @@ async def dashboard(request, response):
1813
1836
  ## v3 Features Summary
1814
1837
 
1815
1838
  - **55 built-in features**, zero third-party dependencies
1816
- - **2,299 tests** passing across all modules
1839
+ - **2,852 tests** passing across all modules
1817
1840
  - **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
1818
1841
  - **`tina4python generate`**: model, route, migration, middleware scaffolding
1819
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.16"
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":
@@ -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
 
@@ -890,7 +900,7 @@ class ORM(metaclass=ORMMeta):
890
900
  Resolution order matches _get_db():
891
901
  1. cls._db as a Database instance
892
902
  2. cls._db as a named string → registry lookup
893
- 3. Global default set via orm_bind()
903
+ 3. Global default set via bind_database()
894
904
  """
895
905
  return cls._get_db()
896
906
 
File without changes