tina4-python 3.13.71__tar.gz → 3.13.72__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 (168) hide show
  1. {tina4_python-3.13.71 → tina4_python-3.13.72}/PKG-INFO +1 -1
  2. {tina4_python-3.13.71 → tina4_python-3.13.72}/pyproject.toml +1 -1
  3. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/cli/__init__.py +26 -0
  4. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/dev_admin/__init__.py +355 -18
  5. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/frond/engine.py +149 -32
  6. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/tina4-dev-admin.js +203 -170
  7. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/tina4-dev-admin.min.js +203 -170
  8. {tina4_python-3.13.71 → tina4_python-3.13.72}/.gitignore +0 -0
  9. {tina4_python-3.13.71 → tina4_python-3.13.72}/README.md +0 -0
  10. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/CLAUDE.md +0 -0
  11. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/HtmlElement.py +0 -0
  12. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/Testing.py +0 -0
  13. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/__init__.py +0 -0
  14. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/ai/__init__.py +0 -0
  15. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/api/__init__.py +0 -0
  16. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/auth/__init__.py +0 -0
  17. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/cache/__init__.py +0 -0
  18. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/container/__init__.py +0 -0
  19. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/context/__init__.py +0 -0
  20. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/context/chunker.py +0 -0
  21. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/__init__.py +0 -0
  22. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/cache.py +0 -0
  23. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/constants.py +0 -0
  24. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/events.py +0 -0
  25. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/middleware.py +0 -0
  26. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/rate_limiter.py +0 -0
  27. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/request.py +0 -0
  28. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/response.py +0 -0
  29. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/router.py +0 -0
  30. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/server.py +0 -0
  31. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/crud/__init__.py +0 -0
  32. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/__init__.py +0 -0
  33. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/adapter.py +0 -0
  34. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/connection.py +0 -0
  35. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/firebird.py +0 -0
  36. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/mongodb.py +0 -0
  37. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/mssql.py +0 -0
  38. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/mysql.py +0 -0
  39. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/odbc.py +0 -0
  40. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/postgres.py +0 -0
  41. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/sqlite.py +0 -0
  42. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/debug/__init__.py +0 -0
  43. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/debug/error_overlay.py +0 -0
  44. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/dev_admin/metrics.py +0 -0
  45. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/dev_admin/plan.py +0 -0
  46. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/dev_admin/project_index.py +0 -0
  47. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/docs.py +0 -0
  48. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/docstore/__init__.py +0 -0
  49. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/dotenv/__init__.py +0 -0
  50. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/env.py +0 -0
  51. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/frond/FROND.md +0 -0
  52. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/frond/__init__.py +0 -0
  53. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/auth/meta.json +0 -0
  54. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  55. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/database/meta.json +0 -0
  56. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  57. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/error-overlay/meta.json +0 -0
  58. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  59. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/orm/meta.json +0 -0
  60. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  61. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  62. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/queue/meta.json +0 -0
  63. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  64. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/rest-api/meta.json +0 -0
  65. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  66. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/templates/meta.json +0 -0
  67. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  68. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  69. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/graphql/__init__.py +0 -0
  70. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/i18n/__init__.py +0 -0
  71. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/mcp/__init__.py +0 -0
  72. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/mcp/protocol.py +0 -0
  73. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/mcp/tools.py +0 -0
  74. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/messenger/__init__.py +0 -0
  75. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/migration/__init__.py +0 -0
  76. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/migration/runner.py +0 -0
  77. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/orm/__init__.py +0 -0
  78. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/orm/fields.py +0 -0
  79. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/orm/model.py +0 -0
  80. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/__feedback/widget.js +0 -0
  81. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/css/tina4.css +0 -0
  82. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/css/tina4.min.css +0 -0
  83. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/favicon.ico +0 -0
  84. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/images/logo.svg +0 -0
  85. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  86. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/frond.js +0 -0
  87. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/frond.min.js +0 -0
  88. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/tina4.min.js +0 -0
  89. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/tina4js.min.js +0 -0
  90. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/swagger/index.html +0 -0
  91. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  92. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/query_builder/__init__.py +0 -0
  93. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/__init__.py +0 -0
  94. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/job.py +0 -0
  95. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/kafka_backend.py +0 -0
  96. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/lite_backend.py +0 -0
  97. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/mongo_backend.py +0 -0
  98. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/rabbitmq_backend.py +0 -0
  99. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue_backends/__init__.py +0 -0
  100. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue_backends/kafka_backend.py +0 -0
  101. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue_backends/mongo_backend.py +0 -0
  102. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  103. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/__init__.py +0 -0
  104. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/Attachment.py +0 -0
  105. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/Channel.py +0 -0
  106. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/ChannelMember.py +0 -0
  107. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/Message.py +0 -0
  108. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/Workspace.py +0 -0
  109. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/__init__.py +0 -0
  110. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/storage.py +0 -0
  111. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/__init__.py +0 -0
  112. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  113. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_badges.scss +0 -0
  114. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  115. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_cards.scss +0 -0
  116. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_forms.scss +0 -0
  117. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_grid.scss +0 -0
  118. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_modals.scss +0 -0
  119. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_nav.scss +0 -0
  120. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_reset.scss +0 -0
  121. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_tables.scss +0 -0
  122. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_typography.scss +0 -0
  123. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  124. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_variables.scss +0 -0
  125. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/base.scss +0 -0
  126. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/colors.scss +0 -0
  127. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/tina4.scss +0 -0
  128. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/seeder/__init__.py +0 -0
  129. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/service/__init__.py +0 -0
  130. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/session/__init__.py +0 -0
  131. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  133. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/session_handlers/redis_handler.py +0 -0
  134. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/session_handlers/valkey_handler.py +0 -0
  135. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/swagger/__init__.py +0 -0
  136. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/components/crud.twig +0 -0
  137. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  138. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  139. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/docker/python/Dockerfile +0 -0
  140. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  141. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/302.twig +0 -0
  142. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/401.twig +0 -0
  143. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/403.twig +0 -0
  144. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/404.twig +0 -0
  145. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/500.twig +0 -0
  146. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/502.twig +0 -0
  147. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/503.twig +0 -0
  148. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/base.twig +0 -0
  149. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/frontend/README.md +0 -0
  150. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/readme.md +0 -0
  151. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/test/__init__.py +0 -0
  152. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/test_client/__init__.py +0 -0
  153. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/validator/__init__.py +0 -0
  166. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/websocket/__init__.py +0 -0
  167. {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/websocket/backplane.py +0 -0
  168. {tina4_python-3.13.71 → tina4_python-3.13.72}/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.71
3
+ Version: 3.13.72
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.71"
3
+ version = "3.13.72"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -513,6 +513,32 @@ def _init(args):
513
513
  encoding="utf-8",
514
514
  )
515
515
 
516
+ # Create pyproject.toml — a Tina4 project is a uv project. The Dockerfiles
517
+ # already COPY pyproject.toml + uv.lock and `uv sync`, so a fresh project
518
+ # MUST ship a manifest. This is also what makes `deps/install` (uv add) and
519
+ # `uv run pytest` work out of the box. pytest ships in the dev group so the
520
+ # scaffolded tests are runnable from the first commit (tests-first).
521
+ pyproject = target / "pyproject.toml"
522
+ if not pyproject.exists():
523
+ project_name = target.resolve().name.replace(" ", "-").lower() or "tina4-app"
524
+ pyproject.write_text(
525
+ '[project]\n'
526
+ f'name = "{project_name}"\n'
527
+ 'version = "0.1.0"\n'
528
+ 'description = "A Tina4 application"\n'
529
+ 'requires-python = ">=3.11"\n'
530
+ 'dependencies = [\n'
531
+ ' "tina4-python",\n'
532
+ ']\n\n'
533
+ '[dependency-groups]\n'
534
+ 'dev = [\n'
535
+ ' "pytest",\n'
536
+ ']\n\n'
537
+ '[tool.uv]\n'
538
+ 'package = false\n',
539
+ encoding="utf-8",
540
+ )
541
+
516
542
  # Create .gitignore
517
543
  gitignore = target / ".gitignore"
518
544
  if not gitignore.exists():
@@ -500,6 +500,21 @@ def get_api_handlers() -> dict:
500
500
  # without shelling out from the browser.
501
501
  "/__dev/api/scaffold": ("GET", _api_scaffold_list),
502
502
  "/__dev/api/scaffold/run": ("POST", _api_scaffold_run),
503
+ # ── Run-chip endpoints (project-level operations) ──
504
+ # The dev-admin SPA's ▶ Migrate / ▶ Test / ▶ Seed chips call
505
+ # these. Each runs the whole-project operation via the framework's
506
+ # own machinery (matching the CLI), NOT by shelling out redundantly.
507
+ # /seed/run is distinct from /seed above (which seeds one named
508
+ # table); /run seeds every discovered ORM model.
509
+ "/__dev/api/migrate": ("POST", _api_migrate),
510
+ "/__dev/api/test": ("POST", _api_test),
511
+ "/__dev/api/seed/run": ("POST", _api_seed_run),
512
+ # ── Framework-grounding (mcp.tina4.com) token config ──
513
+ # Self-contained in Python (writes/reads the project .env
514
+ # directly) — does NOT depend on the Rust agent being up, unlike
515
+ # the Node dev-admin which proxies these to /mcp/status + /mcp/token.
516
+ "/__dev/api/grounding/status": ("GET", _api_grounding_status),
517
+ "/__dev/api/grounding/token": ("POST", _api_grounding_token),
503
518
  # ── Live Docs (per plan/v3/22-LIVE-API-RAG.md) ──
504
519
  # Thin HTTP wrappers around tina4_python.docs.Docs. Both
505
520
  # framework public API and the user's src/ surface are
@@ -2731,46 +2746,57 @@ async def _api_deps_install(request, response):
2731
2746
  name = body.get("name", "").strip()
2732
2747
  version = body.get("version", "").strip()
2733
2748
  registry = body.get("registry", "pypi")
2749
+ # A dev/test dependency (pytest, etc.) goes in the manifest's dev group, not
2750
+ # runtime deps. The install must PERSIST to the manifest — so we prefer the
2751
+ # project's dependency manager (uv/bundle) over a bare pip/gem install.
2752
+ dev = bool(body.get("dev"))
2734
2753
 
2735
2754
  if not name:
2736
2755
  return response({"error": "name required"}, 400)
2737
2756
 
2757
+ import shutil
2758
+
2738
2759
  try:
2739
2760
  if registry == "pypi":
2740
2761
  pkg = f"{name}>={version}" if version else name
2741
- result = subprocess.run(
2742
- ["pip", "install", pkg],
2743
- capture_output=True, text=True, timeout=60
2744
- )
2762
+ # uv add persists to pyproject.toml AND installs into the project's
2763
+ # venv. Fall back to pip only when uv or pyproject.toml is absent.
2764
+ if shutil.which("uv") and Path("pyproject.toml").exists():
2765
+ cmd = ["uv", "add", *(["--dev"] if dev else []), pkg]
2766
+ else:
2767
+ cmd = ["pip", "install", pkg]
2768
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
2745
2769
  if result.returncode != 0:
2746
- return response({"error": result.stderr.strip()}, 500)
2747
- return response({"message": f"Installed {name} {version}".strip(), "output": result.stdout})
2770
+ return response({"error": (result.stderr or result.stdout).strip()}, 500)
2771
+ return response({"message": f"Installed {name} {version}".strip(), "output": result.stdout or result.stderr})
2748
2772
 
2749
2773
  elif registry == "npm":
2750
2774
  pkg = f"{name}@{version}" if version else name
2751
- result = subprocess.run(
2752
- ["npm", "install", pkg],
2753
- capture_output=True, text=True, timeout=60
2754
- )
2775
+ cmd = ["npm", "install", *(["--save-dev"] if dev else []), pkg]
2776
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
2755
2777
  if result.returncode != 0:
2756
2778
  return response({"error": result.stderr.strip()}, 500)
2757
2779
  return response({"message": f"Installed {name}", "output": result.stdout})
2758
2780
 
2759
2781
  elif registry == "packagist":
2760
2782
  pkg = f"{name}:{version}" if version else name
2761
- result = subprocess.run(
2762
- ["composer", "require", pkg],
2763
- capture_output=True, text=True, timeout=60
2764
- )
2783
+ cmd = ["composer", "require", *(["--dev"] if dev else []), pkg]
2784
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
2765
2785
  if result.returncode != 0:
2766
2786
  return response({"error": result.stderr.strip()}, 500)
2767
2787
  return response({"message": f"Installed {name}", "output": result.stdout})
2768
2788
 
2769
2789
  elif registry == "rubygems":
2770
- result = subprocess.run(
2771
- ["gem", "install", name, "-v", version] if version else ["gem", "install", name],
2772
- capture_output=True, text=True, timeout=60
2773
- )
2790
+ # bundle add persists to the Gemfile. Fall back to gem install.
2791
+ if shutil.which("bundle") and Path("Gemfile").exists():
2792
+ cmd = ["bundle", "add", name]
2793
+ if version:
2794
+ cmd += ["--version", version]
2795
+ if dev:
2796
+ cmd += ["--group", "development"]
2797
+ else:
2798
+ cmd = ["gem", "install", name, *(["-v", version] if version else [])]
2799
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
2774
2800
  if result.returncode != 0:
2775
2801
  return response({"error": result.stderr.strip()}, 500)
2776
2802
  return response({"message": f"Installed {name}", "output": result.stdout})
@@ -3130,6 +3156,317 @@ async def _api_scaffold_run(request, response):
3130
3156
  return response({"ok": False, "error": str(exc)}, 500)
3131
3157
 
3132
3158
 
3159
+ # ─── Run-chip endpoints (migrate / test / seed-all) ────────────────
3160
+ #
3161
+ # Project-level operations the dev-admin SPA's ▶ Migrate / ▶ Test /
3162
+ # ▶ Seed chips fire. Distinct from /scaffold/run (creates one file) and
3163
+ # /seed (seeds one named table): these run the whole-project operation
3164
+ # via the framework's own in-process machinery — the same code paths
3165
+ # the CLI drives — rather than shelling out redundantly. Response shapes
3166
+ # match the shared dev-admin SPA (and the Node dev-admin reference).
3167
+
3168
+
3169
+ def _migration_stem(filename: str) -> str:
3170
+ """'000001_create_users.sql' → '000001_create_users' (drop the suffix)."""
3171
+ return Path(filename).stem
3172
+
3173
+
3174
+ async def _api_migrate(request, response):
3175
+ """POST /__dev/api/migrate — apply pending migrations.
3176
+
3177
+ Reuses the exact runner the CLI's ``_migrate`` and the ``_api_tool``
3178
+ 'migrate' tool drive (``tina4_python.migration.Migration.migrate``),
3179
+ but in-process so we can report the split result the SPA reads:
3180
+
3181
+ {applied: [...], skipped: [...], failed: [...]}
3182
+
3183
+ - applied: migration ids run during this call
3184
+ - skipped: migration ids already applied before this call
3185
+ - failed: a migration that raised — the runner stops and rolls back
3186
+ at the first failure, so at most one lands here
3187
+ """
3188
+ import re
3189
+
3190
+ from tina4_python.database import Database
3191
+ from tina4_python.migration import Migration
3192
+
3193
+ body = request.body if hasattr(request, "body") and request.body else {}
3194
+ mig_dir = body.get("dir", "migrations") if isinstance(body, dict) else "migrations"
3195
+ db_url = os.environ.get("TINA4_DATABASE_URL", "sqlite:///data/app.db")
3196
+ db = Database(db_url)
3197
+ try:
3198
+ mig = Migration(db, mig_dir)
3199
+ completed_before = {m["migration_name"] for m in mig.status()["completed"]}
3200
+ applied: list[str] = []
3201
+ failed: list[str] = []
3202
+ try:
3203
+ ran = mig.migrate() # list of applied filenames
3204
+ applied = [_migration_stem(f) for f in ran]
3205
+ except Exception as e:
3206
+ # The runner rolls back + re-raises at the first failing
3207
+ # migration. Recover what actually got applied from the
3208
+ # tracking table, and pull the failed file out of the message
3209
+ # ("Migration failed: <file> — <err>").
3210
+ completed_after = {m["migration_name"] for m in mig.status()["completed"]}
3211
+ applied = sorted(completed_after - completed_before)
3212
+ msg = str(e)
3213
+ match = re.search(r"Migration failed:\s*([^\s—]+)", msg)
3214
+ failed = [_migration_stem(match.group(1))] if match else [msg]
3215
+ skipped = sorted(completed_before)
3216
+ MessageLog.log(
3217
+ "migrate", f"Applied {len(applied)} migration(s)",
3218
+ {"applied": applied, "failed": failed},
3219
+ )
3220
+ return response({"applied": applied, "skipped": skipped, "failed": failed})
3221
+ finally:
3222
+ db.close()
3223
+
3224
+
3225
+ async def _api_test(request, response):
3226
+ """POST /__dev/api/test — run the project's pytest suite.
3227
+
3228
+ Returns ``{ok, code, output}`` where ``code`` is the pytest process
3229
+ exit code and ``output`` is the combined stdout+stderr. A non-zero
3230
+ exit (failing tests) is a valid, reportable result — NOT a 500 —
3231
+ mirroring the ``_api_tool`` 'test' invocation but in the SPA's chip
3232
+ shape.
3233
+ """
3234
+ import subprocess
3235
+ import sys
3236
+
3237
+ try:
3238
+ result = subprocess.run(
3239
+ [sys.executable, "-m", "pytest", "tests/", "-q", "--tb=short"],
3240
+ capture_output=True, text=True, timeout=180, cwd=os.getcwd(),
3241
+ )
3242
+ output = result.stdout or ""
3243
+ if result.stderr:
3244
+ output += ("\n" if output else "") + result.stderr
3245
+ MessageLog.log("test", f"pytest exited {result.returncode}",
3246
+ {"code": result.returncode})
3247
+ return response({
3248
+ "ok": result.returncode == 0,
3249
+ "code": result.returncode,
3250
+ "output": output.strip(),
3251
+ })
3252
+ except subprocess.TimeoutExpired:
3253
+ return response({"ok": False, "code": -1,
3254
+ "output": "Test suite timed out after 180 seconds"})
3255
+ except Exception as e:
3256
+ return response({"ok": False, "error": str(e)}, 500)
3257
+
3258
+
3259
+ def _discover_orm_models() -> list:
3260
+ """Import ORM model modules from ``src/orm/`` (then ``src/models/`` as
3261
+ a fallback) and return the discovered ORM subclasses.
3262
+
3263
+ Same directories the server discovers on startup; importing the
3264
+ modules ensures the classes are registered as ORM subclasses even if
3265
+ nothing has referenced them yet this process. Mirrors the import
3266
+ strategy in ``AutoCrud.discover``.
3267
+ """
3268
+ import importlib
3269
+ import importlib.util
3270
+ import inspect
3271
+ import sys
3272
+
3273
+ from tina4_python.orm import ORM
3274
+
3275
+ seen: set[int] = set()
3276
+ classes: list = []
3277
+ for models_dir in ("src/orm", "src/models"):
3278
+ if not os.path.isdir(models_dir):
3279
+ continue
3280
+ abs_dir = os.path.abspath(models_dir)
3281
+ parent = os.path.dirname(abs_dir)
3282
+ if parent not in sys.path:
3283
+ sys.path.insert(0, parent)
3284
+ module_prefix = f"{os.path.basename(parent)}.{os.path.basename(abs_dir)}"
3285
+ for filename in sorted(os.listdir(models_dir)):
3286
+ if not filename.endswith(".py") or filename.startswith("_"):
3287
+ continue
3288
+ module_name = filename[:-3]
3289
+ try:
3290
+ mod = importlib.import_module(f"{module_prefix}.{module_name}")
3291
+ except Exception:
3292
+ try:
3293
+ spec = importlib.util.spec_from_file_location(
3294
+ module_name, os.path.join(abs_dir, filename))
3295
+ if spec is None or spec.loader is None:
3296
+ continue
3297
+ mod = importlib.util.module_from_spec(spec)
3298
+ sys.modules[module_name] = mod
3299
+ spec.loader.exec_module(mod)
3300
+ except Exception:
3301
+ continue
3302
+ for attr_name in dir(mod):
3303
+ attr = getattr(mod, attr_name)
3304
+ if (inspect.isclass(attr) and issubclass(attr, ORM)
3305
+ and attr is not ORM and id(attr) not in seen):
3306
+ try:
3307
+ has_table = bool(attr._get_table())
3308
+ except Exception:
3309
+ has_table = False
3310
+ if has_table:
3311
+ seen.add(id(attr))
3312
+ classes.append(attr)
3313
+ return classes
3314
+
3315
+
3316
+ async def _api_seed_run(request, response):
3317
+ """POST /__dev/api/seed/run — run the whole-project seeder.
3318
+
3319
+ Distinct from ``/__dev/api/seed`` (which seeds one named table): this
3320
+ discovers every ORM model in ``src/orm/`` (``src/models/`` fallback)
3321
+ and seeds them all via the framework's FK-ordered ``seed_models()`` —
3322
+ the same real seeder the CLI uses. Returns ``{seeded, failed}``.
3323
+ """
3324
+ from tina4_python.seeder import seed_models
3325
+
3326
+ body = request.body if hasattr(request, "body") and request.body else {}
3327
+ count = 10
3328
+ if isinstance(body, dict) and body.get("count") is not None:
3329
+ try:
3330
+ count = int(body.get("count"))
3331
+ except (TypeError, ValueError):
3332
+ count = 10
3333
+
3334
+ try:
3335
+ classes = _discover_orm_models()
3336
+ if not classes:
3337
+ return response(
3338
+ {"seeded": 0, "failed": 0,
3339
+ "error": "No models found in src/orm/ or src/models/"}, 400)
3340
+ summaries = seed_models(classes, count)
3341
+ seeded = 0
3342
+ failed = 0
3343
+ for summary in summaries.values():
3344
+ seeded += getattr(summary, "seeded", 0)
3345
+ failed += getattr(summary, "failed", 0)
3346
+ MessageLog.log(
3347
+ "seed", f"Seeded {seeded} rows across {len(summaries)} model(s)",
3348
+ {"seeded": seeded, "failed": failed},
3349
+ )
3350
+ return response({"seeded": seeded, "failed": failed})
3351
+ except Exception as e:
3352
+ return response({"seeded": 0, "failed": 0, "error": str(e)}, 500)
3353
+
3354
+
3355
+ # ─── Framework-grounding (mcp.tina4.com) token config ──────────────
3356
+ #
3357
+ # Self-contained: reads/writes TINA4_MCP_TOKEN in the process env and
3358
+ # the project .env directly. The Node dev-admin proxies these to the
3359
+ # Rust agent (/mcp/status, /mcp/token); Python keeps them local so the
3360
+ # token panel works with a bare `tina4 serve` — no agent required.
3361
+
3362
+ _MCP_DEFAULT_URL = "https://mcp.tina4.com"
3363
+
3364
+
3365
+ def _env_file_values() -> dict:
3366
+ """Parse the project ``.env`` into a plain dict (best-effort).
3367
+
3368
+ Missing file or read error → ``{}``. Strips surrounding quotes so a
3369
+ quoted value round-trips to the same string the process env would hold.
3370
+ """
3371
+ values: dict = {}
3372
+ env_path = Path(".env")
3373
+ if env_path.exists():
3374
+ try:
3375
+ for line in env_path.read_text(encoding="utf-8").splitlines():
3376
+ s = line.strip()
3377
+ if not s or s.startswith("#") or "=" not in s:
3378
+ continue
3379
+ key, _, val = s.partition("=")
3380
+ values[key.strip()] = val.strip().strip('"').strip("'")
3381
+ except OSError:
3382
+ pass
3383
+ return values
3384
+
3385
+
3386
+ def _resolve_env(key: str, default: str = "") -> str:
3387
+ """Resolve an env var: process environment first, then project ``.env``."""
3388
+ val = os.environ.get(key)
3389
+ if val:
3390
+ return val
3391
+ return _env_file_values().get(key, default)
3392
+
3393
+
3394
+ def _upsert_env_var(key: str, value: str) -> None:
3395
+ """Write/upsert ``KEY=value`` into the project ``.env``.
3396
+
3397
+ An empty ``value`` removes the key entirely (clear). Preserves
3398
+ comments, other keys, and a trailing newline. Mirrors the .env-write
3399
+ pattern in ``_api_connections_save``.
3400
+ """
3401
+ env_path = Path(".env")
3402
+ lines = env_path.read_text(encoding="utf-8").splitlines() if env_path.exists() else []
3403
+ out: list[str] = []
3404
+ found = False
3405
+ for line in lines:
3406
+ s = line.strip()
3407
+ if s.startswith("#") or "=" not in s:
3408
+ out.append(line)
3409
+ continue
3410
+ k = s.split("=", 1)[0].strip()
3411
+ if k == key:
3412
+ found = True
3413
+ if value:
3414
+ out.append(f"{key}={value}")
3415
+ # empty value → drop the line (clear)
3416
+ else:
3417
+ out.append(line)
3418
+ if value and not found:
3419
+ out.append(f"{key}={value}")
3420
+ content = "\n".join(out)
3421
+ if content and not content.endswith("\n"):
3422
+ content += "\n"
3423
+ env_path.write_text(content, encoding="utf-8")
3424
+
3425
+
3426
+ def _grounding_snapshot() -> dict:
3427
+ token = _resolve_env("TINA4_MCP_TOKEN", "")
3428
+ url = _resolve_env("TINA4_MCP_URL", "") or _MCP_DEFAULT_URL
3429
+ return {
3430
+ "configured": bool(token),
3431
+ "last4": token[-4:] if token else "",
3432
+ "url": url,
3433
+ }
3434
+
3435
+
3436
+ async def _api_grounding_status(request, response):
3437
+ """GET /__dev/api/grounding/status — framework-grounding token config.
3438
+
3439
+ ``{configured, last4, url}``. ``configured`` = TINA4_MCP_TOKEN is set
3440
+ (process env or project .env). ``last4`` = last 4 chars of the token
3441
+ (or ""). ``url`` = TINA4_MCP_URL or the mcp.tina4.com default.
3442
+ """
3443
+ return response(_grounding_snapshot())
3444
+
3445
+
3446
+ async def _api_grounding_token(request, response):
3447
+ """POST /__dev/api/grounding/token {token} — upsert TINA4_MCP_TOKEN.
3448
+
3449
+ Writes the token into the project ``.env`` (empty token clears it) and
3450
+ updates the running process env so ``/status`` reflects the change
3451
+ immediately, before any restart re-reads ``.env``. Self-contained —
3452
+ never touches the Rust agent. Returns ``{ok, configured, last4}``.
3453
+ """
3454
+ body = request.body if hasattr(request, "body") and request.body else {}
3455
+ token = str(body.get("token") or "").strip() if isinstance(body, dict) else ""
3456
+ try:
3457
+ _upsert_env_var("TINA4_MCP_TOKEN", token)
3458
+ except OSError as e:
3459
+ return response({"ok": False, "error": str(e)}, 500)
3460
+ if token:
3461
+ os.environ["TINA4_MCP_TOKEN"] = token
3462
+ else:
3463
+ os.environ.pop("TINA4_MCP_TOKEN", None)
3464
+ MessageLog.log("grounding", "Updated TINA4_MCP_TOKEN",
3465
+ {"configured": bool(token)})
3466
+ return response({"ok": True, "configured": bool(token),
3467
+ "last4": token[-4:] if token else ""})
3468
+
3469
+
3133
3470
  _DOCS_SINGLETON = None # cached per-process so the framework index
3134
3471
  # builds once. User portion still mtime-refreshes
3135
3472
  # inside Docs.