tina4-python 3.13.58__tar.gz → 3.13.60__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.58 → tina4_python-3.13.60}/PKG-INFO +5 -5
  2. {tina4_python-3.13.58 → tina4_python-3.13.60}/README.md +4 -4
  3. {tina4_python-3.13.58 → tina4_python-3.13.60}/pyproject.toml +1 -1
  4. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/CLAUDE.md +5 -5
  5. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/__init__.py +1 -1
  6. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/ai/__init__.py +84 -17
  7. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/database/connection.py +27 -10
  8. {tina4_python-3.13.58 → tina4_python-3.13.60}/.gitignore +0 -0
  9. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/HtmlElement.py +0 -0
  10. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/Testing.py +0 -0
  11. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/api/__init__.py +0 -0
  12. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/auth/__init__.py +0 -0
  13. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/cache/__init__.py +0 -0
  14. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/cli/__init__.py +0 -0
  15. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/container/__init__.py +0 -0
  16. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/core/__init__.py +0 -0
  17. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/core/cache.py +0 -0
  18. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/core/constants.py +0 -0
  19. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/core/events.py +0 -0
  20. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/core/middleware.py +0 -0
  21. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/core/rate_limiter.py +0 -0
  22. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/core/request.py +0 -0
  23. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/core/response.py +0 -0
  24. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/core/router.py +0 -0
  25. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/core/server.py +0 -0
  26. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/crud/__init__.py +0 -0
  27. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/database/__init__.py +0 -0
  28. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/database/adapter.py +0 -0
  29. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/database/firebird.py +0 -0
  30. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/database/mongodb.py +0 -0
  31. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/database/mssql.py +0 -0
  32. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/database/mysql.py +0 -0
  33. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/database/odbc.py +0 -0
  34. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/database/postgres.py +0 -0
  35. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/database/sqlite.py +0 -0
  36. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/dev_admin/__init__.py +0 -0
  39. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/dev_admin/metrics.py +0 -0
  40. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/dev_admin/plan.py +0 -0
  41. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/dev_admin/project_index.py +0 -0
  42. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/docs.py +0 -0
  43. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/docstore/__init__.py +0 -0
  44. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/dotenv/__init__.py +0 -0
  45. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/env.py +0 -0
  46. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/frond/FROND.md +0 -0
  47. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/frond/__init__.py +0 -0
  48. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/frond/engine.py +0 -0
  49. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/auth/meta.json +0 -0
  50. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  51. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/database/meta.json +0 -0
  52. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  53. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/error-overlay/meta.json +0 -0
  54. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  55. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/orm/meta.json +0 -0
  56. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  57. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  58. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/queue/meta.json +0 -0
  59. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  60. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/rest-api/meta.json +0 -0
  61. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  62. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/templates/meta.json +0 -0
  63. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  64. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  65. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/graphql/__init__.py +0 -0
  66. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/i18n/__init__.py +0 -0
  67. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/mcp/__init__.py +0 -0
  68. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/mcp/protocol.py +0 -0
  69. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/mcp/tools.py +0 -0
  70. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/messenger/__init__.py +0 -0
  71. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/migration/__init__.py +0 -0
  72. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/migration/runner.py +0 -0
  73. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/orm/__init__.py +0 -0
  74. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/orm/fields.py +0 -0
  75. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/orm/model.py +0 -0
  76. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/__feedback/widget.js +0 -0
  77. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/css/tina4.css +0 -0
  78. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/css/tina4.min.css +0 -0
  79. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/favicon.ico +0 -0
  80. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/images/logo.svg +0 -0
  81. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  82. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/js/frond.js +0 -0
  83. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/js/frond.min.js +0 -0
  84. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  85. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  86. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/js/tina4.min.js +0 -0
  87. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/js/tina4js.min.js +0 -0
  88. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/swagger/index.html +0 -0
  89. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  90. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/query_builder/__init__.py +0 -0
  91. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/queue/__init__.py +0 -0
  92. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/queue/job.py +0 -0
  93. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/queue/kafka_backend.py +0 -0
  94. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/queue/lite_backend.py +0 -0
  95. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/queue/mongo_backend.py +0 -0
  96. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/queue/rabbitmq_backend.py +0 -0
  97. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/queue_backends/__init__.py +0 -0
  98. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/queue_backends/kafka_backend.py +0 -0
  99. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/queue_backends/mongo_backend.py +0 -0
  100. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  101. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/realtime/__init__.py +0 -0
  102. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/realtime/models/Attachment.py +0 -0
  103. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/realtime/models/Channel.py +0 -0
  104. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/realtime/models/ChannelMember.py +0 -0
  105. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/realtime/models/Message.py +0 -0
  106. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/realtime/models/Workspace.py +0 -0
  107. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/realtime/models/__init__.py +0 -0
  108. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/realtime/storage.py +0 -0
  109. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/__init__.py +0 -0
  110. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  111. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_badges.scss +0 -0
  112. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  113. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_cards.scss +0 -0
  114. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_forms.scss +0 -0
  115. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_grid.scss +0 -0
  116. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_modals.scss +0 -0
  117. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_nav.scss +0 -0
  118. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_reset.scss +0 -0
  119. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_tables.scss +0 -0
  120. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_typography.scss +0 -0
  121. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  122. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/_variables.scss +0 -0
  123. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/base.scss +0 -0
  124. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/colors.scss +0 -0
  125. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/scss/tina4css/tina4.scss +0 -0
  126. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/seeder/__init__.py +0 -0
  127. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/service/__init__.py +0 -0
  128. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/session/__init__.py +0 -0
  129. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/session_handlers/__init__.py +0 -0
  130. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  131. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/session_handlers/redis_handler.py +0 -0
  132. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/session_handlers/valkey_handler.py +0 -0
  133. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/swagger/__init__.py +0 -0
  134. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/components/crud.twig +0 -0
  135. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  136. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  137. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/docker/python/Dockerfile +0 -0
  138. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  139. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/errors/302.twig +0 -0
  140. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/errors/401.twig +0 -0
  141. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/errors/403.twig +0 -0
  142. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/errors/404.twig +0 -0
  143. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/errors/500.twig +0 -0
  144. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/errors/502.twig +0 -0
  145. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/errors/503.twig +0 -0
  146. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/errors/base.twig +0 -0
  147. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/frontend/README.md +0 -0
  148. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/templates/readme.md +0 -0
  149. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/test/__init__.py +0 -0
  150. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/test_client/__init__.py +0 -0
  151. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  152. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  153. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/validator/__init__.py +0 -0
  164. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/websocket/__init__.py +0 -0
  165. {tina4_python-3.13.58 → tina4_python-3.13.60}/tina4_python/websocket/backplane.py +0 -0
  166. {tina4_python-3.13.58 → tina4_python-3.13.60}/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.58
3
+ Version: 3.13.60
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.58"
3
+ version = "3.13.60"
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)
@@ -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.58"
11
+ __version__ = "3.13.60"
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
 
@@ -274,7 +274,8 @@ class Database:
274
274
  parsed = urlparse(self.url)
275
275
  scheme = parsed.scheme.lower()
276
276
 
277
- # Handle sqlite:///path (three slashes = absolute, two = relative)
277
+ # Handle sqlite: URLs see _connection_path for the slash rule
278
+ # (three slashes = relative to cwd, four = absolute).
278
279
  if scheme.startswith("sqlite"):
279
280
  scheme = "sqlite"
280
281
 
@@ -314,16 +315,32 @@ class Database:
314
315
  return self.url
315
316
 
316
317
  # In-memory forms — passthrough
317
- raw_path = parsed.path
318
- if raw_path in (":memory:", "/:memory:"):
318
+ if self.url in ("sqlite::memory:", "sqlite:///:memory:"):
319
319
  return ":memory:"
320
320
 
321
- # Strip exactly one leading "/" the URL "/"-delimiter between the
322
- # empty netloc and the start of the path.
323
- if raw_path.startswith("/"):
324
- stripped = raw_path[1:]
321
+ # Strip the scheme on the RAW url string, NOT via urlparse. urlparse collapses
322
+ # "sqlite:/x" and "sqlite:///x" to the same .path, which loses the distinction
323
+ # between a one-slash ABSOLUTE path and the documented three-slash RELATIVE form —
324
+ # that was the "sqlite:<abspath> silently goes relative" footgun. Mirror the
325
+ # sequential strip php/ruby/nodejs use (strip sqlite:/// then sqlite:// then sqlite:):
326
+ # sqlite:///app.db → "app.db" (three slashes = relative to cwd)
327
+ # sqlite:///data/app.db → "data/app.db"
328
+ # sqlite:////abs/app.db → "/abs/app.db" (four slashes = absolute)
329
+ # sqlite:///C:/Users/x → "C:/Users/x" (Windows absolute)
330
+ # sqlite:/abs/app.db → "/abs/app.db" (one slash = a real absolute path)
331
+ # sqlite://rel/app.db → "rel/app.db" (two-slash legacy = relative)
332
+ # sqlite:app.db → "app.db" (relative)
333
+ url = self.url
334
+ if url.startswith("sqlite:///"):
335
+ stripped = url[len("sqlite:///"):]
336
+ elif url.startswith("sqlite://"):
337
+ stripped = url[len("sqlite://"):]
338
+ elif url.startswith("sqlite:"):
339
+ stripped = url[len("sqlite:"):]
325
340
  else:
326
- stripped = raw_path
341
+ stripped = url
342
+ if stripped == ":memory:":
343
+ return ":memory:"
327
344
 
328
345
  # Windows absolute path (drive-letter form): C:/... or C:\...
329
346
  is_windows_abs = (
@@ -332,8 +349,8 @@ class Database:
332
349
  and stripped[1] == ":"
333
350
  and stripped[2] in ("/", "\\")
334
351
  )
335
- # Unix absolute path: urlparse("sqlite:////abs/app.db").path == "//abs/app.db"
336
- # After one strip it's still "/abs/app.db" still starts with "/".
352
+ # Unix absolute path — a leading "/" that survived the scheme strip
353
+ # (four-slash "sqlite:////abs" or the one-slash "sqlite:/abs" form).
337
354
  is_unix_abs = stripped.startswith("/")
338
355
 
339
356
  if is_windows_abs or is_unix_abs: