tina4-python 3.13.59__tar.gz → 3.13.61__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 (166) hide show
  1. {tina4_python-3.13.59 → tina4_python-3.13.61}/PKG-INFO +5 -5
  2. {tina4_python-3.13.59 → tina4_python-3.13.61}/README.md +4 -4
  3. {tina4_python-3.13.59 → tina4_python-3.13.61}/pyproject.toml +1 -1
  4. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/CLAUDE.md +10 -10
  5. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/__init__.py +1 -1
  6. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/ai/__init__.py +84 -17
  7. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/orm/model.py +29 -1
  8. {tina4_python-3.13.59 → tina4_python-3.13.61}/.gitignore +0 -0
  9. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/HtmlElement.py +0 -0
  10. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/Testing.py +0 -0
  11. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/api/__init__.py +0 -0
  12. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/auth/__init__.py +0 -0
  13. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/cache/__init__.py +0 -0
  14. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/cli/__init__.py +0 -0
  15. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/container/__init__.py +0 -0
  16. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/core/__init__.py +0 -0
  17. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/core/cache.py +0 -0
  18. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/core/constants.py +0 -0
  19. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/core/events.py +0 -0
  20. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/core/middleware.py +0 -0
  21. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/core/rate_limiter.py +0 -0
  22. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/core/request.py +0 -0
  23. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/core/response.py +0 -0
  24. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/core/router.py +0 -0
  25. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/core/server.py +0 -0
  26. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/crud/__init__.py +0 -0
  27. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/database/__init__.py +0 -0
  28. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/database/adapter.py +0 -0
  29. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/database/connection.py +0 -0
  30. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/database/firebird.py +0 -0
  31. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/database/mongodb.py +0 -0
  32. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/database/mssql.py +0 -0
  33. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/database/mysql.py +0 -0
  34. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/database/odbc.py +0 -0
  35. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/database/postgres.py +0 -0
  36. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/database/sqlite.py +0 -0
  37. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/debug/__init__.py +0 -0
  38. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/debug/error_overlay.py +0 -0
  39. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/dev_admin/__init__.py +0 -0
  40. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/dev_admin/metrics.py +0 -0
  41. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/dev_admin/plan.py +0 -0
  42. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/dev_admin/project_index.py +0 -0
  43. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/docs.py +0 -0
  44. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/docstore/__init__.py +0 -0
  45. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/dotenv/__init__.py +0 -0
  46. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/env.py +0 -0
  47. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/frond/FROND.md +0 -0
  48. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/frond/__init__.py +0 -0
  49. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/frond/engine.py +0 -0
  50. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/auth/meta.json +0 -0
  51. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  52. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/database/meta.json +0 -0
  53. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  54. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/error-overlay/meta.json +0 -0
  55. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  56. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/orm/meta.json +0 -0
  57. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  58. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  59. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/queue/meta.json +0 -0
  60. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  61. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/rest-api/meta.json +0 -0
  62. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  63. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/templates/meta.json +0 -0
  64. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  65. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  66. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/graphql/__init__.py +0 -0
  67. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/i18n/__init__.py +0 -0
  68. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/mcp/__init__.py +0 -0
  69. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/mcp/protocol.py +0 -0
  70. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/mcp/tools.py +0 -0
  71. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/messenger/__init__.py +0 -0
  72. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/migration/__init__.py +0 -0
  73. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/migration/runner.py +0 -0
  74. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/orm/__init__.py +0 -0
  75. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/orm/fields.py +0 -0
  76. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/__feedback/widget.js +0 -0
  77. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/css/tina4.css +0 -0
  78. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/css/tina4.min.css +0 -0
  79. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/favicon.ico +0 -0
  80. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/images/logo.svg +0 -0
  81. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  82. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/js/frond.js +0 -0
  83. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/js/frond.min.js +0 -0
  84. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  85. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  86. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/js/tina4.min.js +0 -0
  87. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/js/tina4js.min.js +0 -0
  88. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/swagger/index.html +0 -0
  89. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  90. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/query_builder/__init__.py +0 -0
  91. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/queue/__init__.py +0 -0
  92. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/queue/job.py +0 -0
  93. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/queue/kafka_backend.py +0 -0
  94. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/queue/lite_backend.py +0 -0
  95. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/queue/mongo_backend.py +0 -0
  96. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/queue/rabbitmq_backend.py +0 -0
  97. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/queue_backends/__init__.py +0 -0
  98. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/queue_backends/kafka_backend.py +0 -0
  99. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/queue_backends/mongo_backend.py +0 -0
  100. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  101. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/realtime/__init__.py +0 -0
  102. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/realtime/models/Attachment.py +0 -0
  103. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/realtime/models/Channel.py +0 -0
  104. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/realtime/models/ChannelMember.py +0 -0
  105. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/realtime/models/Message.py +0 -0
  106. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/realtime/models/Workspace.py +0 -0
  107. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/realtime/models/__init__.py +0 -0
  108. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/realtime/storage.py +0 -0
  109. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/__init__.py +0 -0
  110. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  111. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_badges.scss +0 -0
  112. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  113. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_cards.scss +0 -0
  114. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_forms.scss +0 -0
  115. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_grid.scss +0 -0
  116. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_modals.scss +0 -0
  117. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_nav.scss +0 -0
  118. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_reset.scss +0 -0
  119. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_tables.scss +0 -0
  120. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_typography.scss +0 -0
  121. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  122. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/_variables.scss +0 -0
  123. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/base.scss +0 -0
  124. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/colors.scss +0 -0
  125. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/scss/tina4css/tina4.scss +0 -0
  126. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/seeder/__init__.py +0 -0
  127. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/service/__init__.py +0 -0
  128. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/session/__init__.py +0 -0
  129. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/session_handlers/__init__.py +0 -0
  130. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  131. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/session_handlers/redis_handler.py +0 -0
  132. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/session_handlers/valkey_handler.py +0 -0
  133. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/swagger/__init__.py +0 -0
  134. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/components/crud.twig +0 -0
  135. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  136. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  137. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/docker/python/Dockerfile +0 -0
  138. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  139. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/errors/302.twig +0 -0
  140. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/errors/401.twig +0 -0
  141. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/errors/403.twig +0 -0
  142. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/errors/404.twig +0 -0
  143. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/errors/500.twig +0 -0
  144. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/errors/502.twig +0 -0
  145. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/errors/503.twig +0 -0
  146. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/errors/base.twig +0 -0
  147. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/frontend/README.md +0 -0
  148. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/templates/readme.md +0 -0
  149. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/test/__init__.py +0 -0
  150. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/test_client/__init__.py +0 -0
  151. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  152. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  153. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/validator/__init__.py +0 -0
  164. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/websocket/__init__.py +0 -0
  165. {tina4_python-3.13.59 → tina4_python-3.13.61}/tina4_python/websocket/backplane.py +0 -0
  166. {tina4_python-3.13.59 → tina4_python-3.13.61}/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.59
3
+ Version: 3.13.61
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
@@ -259,7 +259,7 @@ from tina4_python.core.router import get, post, noauth
259
259
  @get("/api/users")
260
260
  async def list_users(request, response):
261
261
  from src.orm.User import User
262
- return response(User().select(limit=100).to_array())
262
+ return response(User.select(limit=100))
263
263
 
264
264
  @get("/api/users/{id:int}")
265
265
  async def get_user(id, request, response):
@@ -320,7 +320,7 @@ Render it from a route:
320
320
  @get("/")
321
321
  async def home(request, response):
322
322
  from src.orm.User import User
323
- users = User().select(limit=20).to_array()
323
+ users = [u.to_dict() for u in User.select(limit=20)]
324
324
  return response.render("pages/home.twig", {"title": "Users", "users": users})
325
325
  ```
326
326
 
@@ -499,7 +499,7 @@ Auto-generated at `/swagger`:
499
499
  @tags(["users"])
500
500
  @get("/api/users")
501
501
  async def users(request, response):
502
- return response(User().select().to_array())
502
+ return response(User.select())
503
503
  ```
504
504
 
505
505
  ### Event System
@@ -661,7 +661,7 @@ profile = user.has_one("Profile", "user_id")
661
661
  customer = order.belongs_to("Customer", "customer_id")
662
662
 
663
663
  # Eager loading with include=
664
- users = User().select(include=["orders", "profile"])
664
+ users = User.select(include=["orders", "profile"])
665
665
  ```
666
666
 
667
667
  ### DB Query Caching
@@ -227,7 +227,7 @@ from tina4_python.core.router import get, post, noauth
227
227
  @get("/api/users")
228
228
  async def list_users(request, response):
229
229
  from src.orm.User import User
230
- return response(User().select(limit=100).to_array())
230
+ return response(User.select(limit=100))
231
231
 
232
232
  @get("/api/users/{id:int}")
233
233
  async def get_user(id, request, response):
@@ -288,7 +288,7 @@ Render it from a route:
288
288
  @get("/")
289
289
  async def home(request, response):
290
290
  from src.orm.User import User
291
- users = User().select(limit=20).to_array()
291
+ users = [u.to_dict() for u in User.select(limit=20)]
292
292
  return response.render("pages/home.twig", {"title": "Users", "users": users})
293
293
  ```
294
294
 
@@ -467,7 +467,7 @@ Auto-generated at `/swagger`:
467
467
  @tags(["users"])
468
468
  @get("/api/users")
469
469
  async def users(request, response):
470
- return response(User().select().to_array())
470
+ return response(User.select())
471
471
  ```
472
472
 
473
473
  ### Event System
@@ -629,7 +629,7 @@ profile = user.has_one("Profile", "user_id")
629
629
  customer = order.belongs_to("Customer", "customer_id")
630
630
 
631
631
  # Eager loading with include=
632
- users = User().select(include=["orders", "profile"])
632
+ users = User.select(include=["orders", "profile"])
633
633
  ```
634
634
 
635
635
  ### DB Query Caching
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.59"
3
+ version = "3.13.61"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -24,14 +24,14 @@ Every piece of repeated logic must be centralised. If two routes format a date,
24
24
  ```python
25
25
  @get("/api/orders")
26
26
  async def list_orders(request, response):
27
- orders = Order().select().to_array()
27
+ orders = [o.to_dict() for o in Order.select()]
28
28
  for o in orders:
29
29
  o["total"] = f"${abs(float(o['total'])):,.2f}" # duplicated
30
30
  return response(orders)
31
31
 
32
32
  @get("/api/invoices")
33
33
  async def list_invoices(request, response):
34
- invoices = Invoice().select().to_array()
34
+ invoices = [i.to_dict() for i in Invoice.select()]
35
35
  for i in invoices:
36
36
  i["amount"] = f"${abs(float(i['amount'])):,.2f}" # duplicated!
37
37
  return response(invoices)
@@ -262,16 +262,16 @@ Queue(topic="tasks").push({"action": "send_email"})
262
262
  ### 11. Key tina4_python Gotchas
263
263
 
264
264
  1. **Database import**: Use `from tina4_python.database import Database` (NOT `from tina4_python import Database`)
265
- 2. **noauth/secured import**: Use `from tina4_python.core.router import noauth, secured` (there is NO `tina4_python.Decorators` module). **Never** import `noauth` from `tina4_python.swagger` that version only affects documentation, not actual auth.
265
+ 2. **noauth/secured import**: Use `from tina4_python.core.router import noauth, secured` (there is NO `tina4_python.Decorators` module). `tina4_python.swagger` does **not** export `noauth` (importing it raises `ImportError`). The real docs-only trap is **`@security()`** (from `tina4_python.swagger`): it sets OpenAPI metadata only and does NOT gate a route — use `@secured()`/`@noauth()` for actual auth.
266
266
  2b. **Decorator ordering**: Route decorators (`@get`, `@post`, etc.) must be the **innermost** (closest to the function). Swagger/meta decorators (`@description`, `@tags`, `@noauth`, `@secured`) go above. Correct: `@noauth()` → `@description(...)` → `@post(...)` → `def handler`. Wrong: `@post(...)` → `@description(...)` → `def handler` (will crash).
267
- 3. **Jinja2 template syntax** (common mistakes):
267
+ 3. **Frond template syntax** (Frond is Jinja2-*like*, not Jinja2 — note the differences):
268
268
  - **Ternary operator supported**: Both `{{ x ? 'a' : 'b' }}` and `{{ 'a' if x else 'b' }}` work
269
- - **elif not elseif**: Use `{% elif %}` NOT `{% elseif %}`
269
+ - **elif or elseif**: Frond accepts **both** `{% elif %}` and `{% elseif %}`
270
270
  - **Unescaped output**: Both `{{ var | safe }}` and `{{ var | raw }}` work for unescaped output
271
271
  - **format filter**: Use `{{ "%.2f" | format(value) }}` for number formatting
272
272
  - **e() filter has NO arguments**: Use `{{ var|e }}` NOT `{{ var|e('js') }}` — Jinja2's `|e` is HTML-only with no mode parameter (that's PHP Twig syntax)
273
273
  - **JS string escaping**: Use `{{ var|replace("'", "\\'") }}` to escape single quotes for inline JS onclick handlers
274
- - **No `|escape('js')` or `|e('js')`**: These will throw `escape() takes 1 positional argument but 2 were given`
274
+ - **`|e`/`|escape` are HTML-only**: a mode arg like `|e('js')` is **ignored** by Frond (not an error) for inline-JS escaping use `{{ var|replace("'", "\\'") }}`
275
275
  - **Ternary inline**: Use `{{ 's' if count != 1 else '' }}` NOT `{{ count != 1 ? 's' : '' }}`
276
276
  - **Default values**: Use `{{ var|default('fallback') }}` — works on undefined and None
277
277
  - **Chaining filters**: `{{ var|default('')|replace("'", "\\'") }}` — left to right
@@ -289,7 +289,7 @@ Queue(topic="tasks").push({"action": "send_email"})
289
289
  4. **fetch_one()**: Returns a plain dict (or None), NOT a DatabaseResult
290
290
  5. **Dict access**: All query results use dict access `row["column"]` not attribute access `row.column`
291
291
  6. **Connection strings**: v3 uses standard URL format: `driver://host:port/database` with separate `username` and `password` parameters. Example: `Database("firebird://localhost:3050//path/to/db", "SYSDBA", "masterkey")`. Environment variable: `TINA4_DATABASE_URL`.
292
- 7. **Running the app**: `uv run python app.py <port> <name>` port and name are CLI args handled by tina4_python
292
+ 7. **Running the app**: use the `tina4` CLI — `tina4 serve`. The framework **refuses to boot via a plain `python app.py`** (it must be launched by the CLI; `TINA4_OVERRIDE_CLIENT=true` only for embedding). Port/name are CLI args.
293
293
  8. **SCSS**: Files in `src/scss/` are auto-compiled to `src/public/css/` on startup
294
294
  12. **Background tasks**: Use `background(fn, interval)` from `tina4_python.core.server` — never use `threading.Thread` for periodic work. The `background()` function runs tasks cooperatively in the asyncio event loop with proper shutdown handling.
295
295
 
@@ -890,8 +890,8 @@ user.save()
890
890
  # Delete
891
891
  user.delete()
892
892
 
893
- # Query
894
- result = User().select(sql="name = ?", params=["Alice"], limit=10)
893
+ # Query (WHERE fragment -> use where(); select() takes FULL SQL, not a fragment)
894
+ result = User.where("name = ?", params=["Alice"], limit=10)
895
895
 
896
896
  # Convert to dict
897
897
  user.to_dict()
@@ -1797,7 +1797,7 @@ uv run tina4python migrate
1797
1797
  ```python
1798
1798
  @get("/api/products")
1799
1799
  async def list_products(request, response):
1800
- return response(Product().select(limit=100).to_array())
1800
+ return response(Product.select(limit=100))
1801
1801
 
1802
1802
  @post("/api/products")
1803
1803
  async def create_product(request, response):
@@ -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.59"
11
+ __version__ = "3.13.61"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -36,6 +36,76 @@ AI_TOOLS = [
36
36
  ]
37
37
 
38
38
 
39
+ # ── Skill files (the SKILL.md system) ───────────────────────────────────────
40
+ # `tina4 ai` installs the actual skills — not just a CLAUDE.md pointer to them —
41
+ # into BOTH the project (.claude/skills, so they travel with the repo) and the
42
+ # user's global ~/.claude/skills (so they're available in every project). A
43
+ # Python project needs the python developer skill plus the two shared skills;
44
+ # all three are served from the tina4-python release tag.
45
+ _DEV_SKILL = "tina4-developer-python"
46
+ _SKILL_REPO = "tina4-python"
47
+ _SKILLS = {
48
+ _DEV_SKILL: ["auth-and-services.md", "data-and-orm.md", "deployment.md",
49
+ "routes-and-api.md", "templates-and-frontend.md", "realtime.md"],
50
+ "tina4-js": ["html-and-components.md", "signals-and-reactivity.md",
51
+ "persistence.md", "rtc.md"],
52
+ "tina4-maintainer": ["cli-and-deployment.md", "frond-and-frontend.md",
53
+ "routing-and-orm.md", "subsystems.md"],
54
+ }
55
+
56
+
57
+ def _skills_ref() -> str:
58
+ """Release tag to pull skills from — the installed framework version,
59
+ overridable with TINA4_SKILLS_REF (e.g. to test a branch)."""
60
+ ref = os.environ.get("TINA4_SKILLS_REF")
61
+ if ref:
62
+ return ref
63
+ try:
64
+ from tina4_python import __version__
65
+ return __version__
66
+ except Exception:
67
+ return "main"
68
+
69
+
70
+ def _fetch_bytes(url: str) -> "bytes | None":
71
+ import urllib.error
72
+ import urllib.request
73
+ try:
74
+ with urllib.request.urlopen(url, timeout=15) as resp:
75
+ return resp.read()
76
+ except (urllib.error.URLError, OSError):
77
+ return None
78
+
79
+
80
+ def install_skills(root: str = ".", targets: "list[Path] | None" = None) -> list[str]:
81
+ """Install the Tina4 SKILL.md skills into the project AND global
82
+ ~/.claude/skills, fetched from the release tag matching this framework
83
+ version. Returns the skills fully installed. Network-dependent."""
84
+ ref = _skills_ref()
85
+ base = f"https://raw.githubusercontent.com/tina4stack/{_SKILL_REPO}/{ref}/.claude/skills"
86
+ if targets is None:
87
+ targets = [Path(root).resolve() / ".claude" / "skills",
88
+ Path.home() / ".claude" / "skills"]
89
+ installed = []
90
+ for skill, refs in _SKILLS.items():
91
+ skill_ok = True
92
+ for dest in targets:
93
+ skill_dir = dest / skill
94
+ (skill_dir / "references").mkdir(parents=True, exist_ok=True)
95
+ data = _fetch_bytes(f"{base}/{skill}/SKILL.md")
96
+ if data is None:
97
+ skill_ok = False
98
+ continue
99
+ (skill_dir / "SKILL.md").write_bytes(data)
100
+ for r in refs:
101
+ rd = _fetch_bytes(f"{base}/{skill}/references/{r}")
102
+ if rd is not None:
103
+ (skill_dir / "references" / r).write_bytes(rd)
104
+ if skill_ok:
105
+ installed.append(skill)
106
+ return installed
107
+
108
+
39
109
  def is_installed(root: str, tool: dict) -> bool:
40
110
  """Check if a tool's context file already exists."""
41
111
  return (Path(root).resolve() / tool["context_file"]).exists()
@@ -197,7 +267,7 @@ def _skill_block(context_file: str) -> str:
197
267
  "## Tina4 Skills\n\n"
198
268
  "When working on this Tina4 project, these skills give the "
199
269
  "assistant project-aware behaviour:\n\n"
200
- "- **tina4-developer** — Read `.claude/skills/tina4-developer/SKILL.md` before building features.\n"
270
+ f"- **{_DEV_SKILL}** — Read `.claude/skills/{_DEV_SKILL}/SKILL.md` before building features.\n"
201
271
  "- **tina4-js** — Read `.claude/skills/tina4-js/SKILL.md` for frontend work.\n"
202
272
  "- **tina4-maintainer** — Read `.claude/skills/tina4-maintainer/SKILL.md` for framework-level changes.\n\n"
203
273
  "If Tina4 behaves differently from what these skills describe, that is a bug in the skill. "
@@ -208,7 +278,7 @@ def _skill_block(context_file: str) -> str:
208
278
  else:
209
279
  body = (
210
280
  "Tina4 Skills — read these files before working on this project:\n"
211
- " .claude/skills/tina4-developer/SKILL.md (feature development)\n"
281
+ f" .claude/skills/{_DEV_SKILL}/SKILL.md (feature development)\n"
212
282
  " .claude/skills/tina4-js/SKILL.md (frontend / tina4-js)\n"
213
283
  " .claude/skills/tina4-maintainer/SKILL.md (framework-level changes)\n"
214
284
  "Found a skill that disagrees with how Tina4 actually behaves? Tell the developer,\n"
@@ -367,21 +437,18 @@ def _install_claude_skills(root: Path) -> list[str]:
367
437
  rel = str(target.relative_to(root))
368
438
  created.append(rel)
369
439
 
370
- # Copy skill directories from framework .claude/skills/
371
- framework_root = pkg_dir.parent
372
- framework_skills_dir = framework_root / ".claude" / "skills"
373
- if framework_skills_dir.is_dir():
374
- target_skills_dir = root / ".claude" / "skills"
375
- target_skills_dir.mkdir(parents=True, exist_ok=True)
376
- for skill_dir in framework_skills_dir.iterdir():
377
- if skill_dir.is_dir():
378
- target_dir = target_skills_dir / skill_dir.name
379
- if target_dir.exists():
380
- shutil.rmtree(target_dir)
381
- shutil.copytree(skill_dir, target_dir)
382
- rel = str(target_dir.relative_to(root))
383
- created.append(rel)
384
- print(f" \033[32m✓\033[0m Updated {rel}")
440
+ # Install the SKILL.md skills into the project AND the user's global
441
+ # ~/.claude/skills, fetched from the release tag matching this framework
442
+ # version. (The previous code copied from framework_root/.claude/skills,
443
+ # which exists only in a dev/editable checkout — NOT in an installed pip
444
+ # package, where .claude/skills sits outside the tina4_python package and
445
+ # is never shipped. So installed users got no skills at all.)
446
+ for skill in install_skills(str(root)):
447
+ created.append(f".claude/skills/{skill}/")
448
+ try:
449
+ print(f" \033[32m✓\033[0m Installed .claude/skills/{skill} (project + global)")
450
+ except UnicodeEncodeError:
451
+ print(f" [OK] Installed .claude/skills/{skill} (project + global)")
385
452
 
386
453
  return created
387
454
 
@@ -437,7 +437,35 @@ class ORM(metaclass=ORMMeta):
437
437
  # which db.execute()/insert()/update() populate, falling back to
438
438
  # the exception text) on self.last_error so it survives, and log
439
439
  # it with model/table context. ──
440
- self.last_error = db.get_error() or str(e)
440
+ cause = db.get_error() or str(e)
441
+ # ── DX hint (v3.13.60): turn a bare driver error into an
442
+ # actionable fix for the two commonest ORM write footguns. Match
443
+ # the message case-insensitively (SQLite says "no such table" /
444
+ # "no such column: is_deleted" / "has no column named is_deleted";
445
+ # Postgres/MySQL say "does not exist" / "doesn't exist"). Any
446
+ # OTHER error keeps its raw cause untouched so we never mask an
447
+ # unrelated failure (e.g. a NOT NULL / duplicate-PK violation). ──
448
+ low = cause.lower()
449
+ if self.soft_delete and "is_deleted" in low and (
450
+ "no such column" in low or "has no column" in low
451
+ or "does not exist" in low or "doesn't exist" in low
452
+ or "unknown column" in low
453
+ ):
454
+ cause += (
455
+ " — soft_delete=True requires an is_deleted column; declare "
456
+ "it (is_deleted = IntegerField(default=0)) or add a migration"
457
+ )
458
+ elif "no such table" in low or (
459
+ ("does not exist" in low or "doesn't exist" in low)
460
+ # exclude a column-not-found (e.g. Postgres 'column "x" does not exist')
461
+ # so a genuine missing-column error never gets a spurious table hint
462
+ and "column" not in low
463
+ ):
464
+ cause += (
465
+ f" — table '{table}' does not exist; call "
466
+ f"{type(self).__name__}.create_table() or run a migration"
467
+ )
468
+ self.last_error = cause
441
469
  Log.error(
442
470
  f"{type(self).__name__}.save() failed for table "
443
471
  f"'{table}': {self.last_error}"