tina4-python 3.13.89__tar.gz → 3.13.91__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 (174) hide show
  1. {tina4_python-3.13.89 → tina4_python-3.13.91}/PKG-INFO +1 -1
  2. {tina4_python-3.13.89 → tina4_python-3.13.91}/pyproject.toml +1 -1
  3. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/cli/__init__.py +5 -3
  4. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/dev_admin/__init__.py +13 -3
  5. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/dev_admin/metrics.py +67 -5
  6. tina4_python-3.13.91/tina4_python/dev_admin/metrics_engine.py +108 -0
  7. tina4_python-3.13.91/tina4_python/public/js/tina4-dev-admin.min.js +1127 -0
  8. tina4_python-3.13.89/tina4_python/public/js/tina4-dev-admin.min.js +0 -1127
  9. {tina4_python-3.13.89 → tina4_python-3.13.91}/.gitignore +0 -0
  10. {tina4_python-3.13.89 → tina4_python-3.13.91}/README.md +0 -0
  11. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/CLAUDE.md +0 -0
  12. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/HtmlElement.py +0 -0
  13. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/Testing.py +0 -0
  14. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/__init__.py +0 -0
  15. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/ai/__init__.py +0 -0
  16. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/api/__init__.py +0 -0
  17. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/auth/__init__.py +0 -0
  18. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/cache/__init__.py +0 -0
  19. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/container/__init__.py +0 -0
  20. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/context/__init__.py +0 -0
  21. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/context/chunker.py +0 -0
  22. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/__init__.py +0 -0
  23. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/cache.py +0 -0
  24. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/constants.py +0 -0
  25. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/events.py +0 -0
  26. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/middleware.py +0 -0
  27. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/rate_limiter.py +0 -0
  28. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/request.py +0 -0
  29. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/response.py +0 -0
  30. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/router.py +0 -0
  31. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/core/server.py +0 -0
  32. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/crud/__init__.py +0 -0
  33. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/__init__.py +0 -0
  34. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/adapter.py +0 -0
  35. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/connection.py +0 -0
  36. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/firebird.py +0 -0
  37. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/mongodb.py +0 -0
  38. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/mssql.py +0 -0
  39. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/mysql.py +0 -0
  40. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/odbc.py +0 -0
  41. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/postgres.py +0 -0
  42. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/database/sqlite.py +0 -0
  43. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/debug/__init__.py +0 -0
  44. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/debug/error_overlay.py +0 -0
  45. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/dev_admin/plan.py +0 -0
  46. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/dev_admin/project_index.py +0 -0
  47. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/docs.py +0 -0
  48. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/docstore/__init__.py +0 -0
  49. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/dotenv/__init__.py +0 -0
  50. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/env.py +0 -0
  51. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/frond/FROND.md +0 -0
  52. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/frond/__init__.py +0 -0
  53. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/frond/compiler.py +0 -0
  54. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/frond/engine.py +0 -0
  55. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/frond/parser.py +0 -0
  56. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/auth/meta.json +0 -0
  57. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  58. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/database/meta.json +0 -0
  59. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  60. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/error-overlay/meta.json +0 -0
  61. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  62. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/orm/meta.json +0 -0
  63. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  64. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  65. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/queue/meta.json +0 -0
  66. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  67. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/rest-api/meta.json +0 -0
  68. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  69. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/templates/meta.json +0 -0
  70. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  71. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  72. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/graphql/__init__.py +0 -0
  73. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/i18n/__init__.py +0 -0
  74. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/mcp/__init__.py +0 -0
  75. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/mcp/protocol.py +0 -0
  76. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/mcp/tools.py +0 -0
  77. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/messenger/__init__.py +0 -0
  78. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/migration/__init__.py +0 -0
  79. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/migration/runner.py +0 -0
  80. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/mqtt/__init__.py +0 -0
  81. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/mqtt/message.py +0 -0
  82. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/orm/__init__.py +0 -0
  83. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/orm/fields.py +0 -0
  84. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/orm/model.py +0 -0
  85. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/__feedback/widget.js +0 -0
  86. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/css/tina4.css +0 -0
  87. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/css/tina4.min.css +0 -0
  88. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/favicon.ico +0 -0
  89. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/images/logo.svg +0 -0
  90. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  91. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/js/frond.js +0 -0
  92. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/js/frond.min.js +0 -0
  93. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/js/tina4.min.js +0 -0
  94. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/js/tina4js.min.js +0 -0
  95. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/swagger/index.html +0 -0
  96. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  97. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/query_builder/__init__.py +0 -0
  98. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/__init__.py +0 -0
  99. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/job.py +0 -0
  100. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/kafka_backend.py +0 -0
  101. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/lite_backend.py +0 -0
  102. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/mongo_backend.py +0 -0
  103. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue/rabbitmq_backend.py +0 -0
  104. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue_backends/__init__.py +0 -0
  105. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue_backends/kafka_backend.py +0 -0
  106. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue_backends/mongo_backend.py +0 -0
  107. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  108. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/__init__.py +0 -0
  109. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/Attachment.py +0 -0
  110. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/Channel.py +0 -0
  111. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/ChannelMember.py +0 -0
  112. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/Message.py +0 -0
  113. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/Workspace.py +0 -0
  114. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/models/__init__.py +0 -0
  115. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/realtime/storage.py +0 -0
  116. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/__init__.py +0 -0
  117. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  118. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_badges.scss +0 -0
  119. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  120. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_cards.scss +0 -0
  121. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_forms.scss +0 -0
  122. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_grid.scss +0 -0
  123. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_modals.scss +0 -0
  124. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_nav.scss +0 -0
  125. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_pagination.scss +0 -0
  126. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_reset.scss +0 -0
  127. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_tables.scss +0 -0
  128. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_typography.scss +0 -0
  129. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  130. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/_variables.scss +0 -0
  131. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/base.scss +0 -0
  132. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/colors.scss +0 -0
  133. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/scss/tina4css/tina4.scss +0 -0
  134. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/seeder/__init__.py +0 -0
  135. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/service/__init__.py +0 -0
  136. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/session/__init__.py +0 -0
  137. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/session_handlers/__init__.py +0 -0
  138. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  139. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/session_handlers/redis_handler.py +0 -0
  140. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/session_handlers/valkey_handler.py +0 -0
  141. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/swagger/__init__.py +0 -0
  142. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/components/crud.twig +0 -0
  143. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  144. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  145. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/docker/python/Dockerfile +0 -0
  146. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  147. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/302.twig +0 -0
  148. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/401.twig +0 -0
  149. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/403.twig +0 -0
  150. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/404.twig +0 -0
  151. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/500.twig +0 -0
  152. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/502.twig +0 -0
  153. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/503.twig +0 -0
  154. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/errors/base.twig +0 -0
  155. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/frontend/README.md +0 -0
  156. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/templates/readme.md +0 -0
  157. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/test/__init__.py +0 -0
  158. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/test_client/__init__.py +0 -0
  159. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  166. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  167. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  168. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  169. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  170. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  171. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/validator/__init__.py +0 -0
  172. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/websocket/__init__.py +0 -0
  173. {tina4_python-3.13.89 → tina4_python-3.13.91}/tina4_python/websocket/backplane.py +0 -0
  174. {tina4_python-3.13.89 → tina4_python-3.13.91}/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.89
3
+ Version: 3.13.91
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.89"
3
+ version = "3.13.91"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -1766,7 +1766,9 @@ def _sample_literal(field_type: str) -> str:
1766
1766
 
1767
1767
  def _emit_model_test(model: str, table: str, fields: list) -> None:
1768
1768
  """model → real SQLite roundtrip (create / read back / missing → None)."""
1769
- fields = fields or [("name", "string")]
1769
+ # Reuse the single DEFAULT_FIELDS constant rather than re-stating the literal,
1770
+ # so the co-emitted test can never describe a shape the model does not have.
1771
+ fields = fields or list(DEFAULT_FIELDS)
1770
1772
  payload = ", ".join(f'"{fname}": {_sample_literal(ftype)}' for fname, ftype in fields)
1771
1773
  # Assert a STRING field round-trips (type-safe); else just the id round-trips
1772
1774
  # (avoids datetime/bool/float equality pitfalls on the read-back).
@@ -2615,7 +2617,7 @@ def _gen_form(name: str, flags: dict = None):
2615
2617
  tina4python generate form Product --fields "name:string,price:float"
2616
2618
  """
2617
2619
  flags = flags or {}
2618
- fields = _parse_fields(flags.get("fields", ""))
2620
+ fields = _fields_or_default(flags.get("fields", ""))
2619
2621
  table = _to_table(name)
2620
2622
  route_name = table + "s"
2621
2623
 
@@ -2637,7 +2639,7 @@ def _gen_form(name: str, flags: dict = None):
2637
2639
 
2638
2640
  # Build form fields
2639
2641
  field_html = ""
2640
- for fname, ftype in (fields or [("name", "string")]):
2642
+ for fname, ftype in fields:
2641
2643
  itype = input_types.get(ftype, "text")
2642
2644
  label = fname.replace("_", " ").title()
2643
2645
  step = ' step="0.01"' if ftype in ("float", "numeric", "decimal") else ""
@@ -2041,9 +2041,19 @@ async def _api_metrics(request, response):
2041
2041
 
2042
2042
 
2043
2043
  async def _api_metrics_full(request, response):
2044
- """Full analysis — AST-based, cached 60s."""
2045
- from tina4_python.dev_admin.metrics import full_analysis
2046
- return response(full_analysis())
2044
+ """Full analysis — from the tina4 CLI metrics engine, else the local module.
2045
+
2046
+ The engine (ADR-0002) resolves imports to real file paths, so it is the only
2047
+ source with usable coupling data. It returns None when it is absent or too
2048
+ old to emit a field the dashboard renders, and we fall back rather than
2049
+ serve a payload with holes in it.
2050
+ """
2051
+ from tina4_python.dev_admin.metrics import full_analysis, resolve_scan_target
2052
+ from tina4_python.dev_admin.metrics_engine import engine_analysis
2053
+
2054
+ root, scan_mode = resolve_scan_target()
2055
+ from_engine = engine_analysis(root, scan_mode)
2056
+ return response(from_engine if from_engine is not None else full_analysis())
2047
2057
 
2048
2058
 
2049
2059
  async def _api_metrics_file(request, response):
@@ -23,6 +23,18 @@ _last_scan_root: str = ""
23
23
  # ── Quick Metrics ──────────────────────────────────────────────
24
24
 
25
25
 
26
+ def _is_code_line(line: str) -> bool:
27
+ """True for a line that counts toward LOC: not blank, not a comment.
28
+
29
+ The single definition of the rule. It used to be restated at each file-level
30
+ call site while function LOC ignored it entirely and returned a raw line
31
+ span, so `loc` meant two different things in one payload - see
32
+ _function_loc.
33
+ """
34
+ stripped = line.strip()
35
+ return bool(stripped) and not stripped.startswith("#")
36
+
37
+
26
38
  def _resolve_root(root: str = "src") -> str:
27
39
  """Pick the right directory to scan.
28
40
 
@@ -41,6 +53,23 @@ def _resolve_root(root: str = "src") -> str:
41
53
  return framework_dir
42
54
 
43
55
 
56
+ def resolve_scan_target(root: str = "src") -> tuple[str, str]:
57
+ """Return (directory to scan, scan_mode) for any metrics producer.
58
+
59
+ The CLI engine is language-agnostic and cannot know which directory holds a
60
+ framework package, so root resolution and the "framework" label stay here -
61
+ shared by this module and the engine adapter so the two never disagree about
62
+ what was measured.
63
+ """
64
+ resolved = _resolve_root(root)
65
+ framework_dir = str(Path(__import__("tina4_python").__file__).parent)
66
+ resolved_path = Path(resolved)
67
+ scanning_framework = (
68
+ resolved_path == Path(framework_dir) or str(resolved_path).startswith(framework_dir)
69
+ )
70
+ return resolved, "framework" if scanning_framework else "project"
71
+
72
+
44
73
  def quick_metrics(root: str = "src") -> dict:
45
74
  """Scan project files and return instant metrics."""
46
75
  root = _resolve_root(root)
@@ -224,7 +253,7 @@ def full_analysis(root: str = "src") -> dict:
224
253
  except ValueError:
225
254
  rel_path = (str(f.relative_to(root_path.parent)) if root_path.parent != f else f.name).replace("\\", "/")
226
255
  lines = source.splitlines()
227
- loc = sum(1 for l in lines if l.strip() and not l.strip().startswith("#"))
256
+ loc = sum(1 for l in lines if _is_code_line(l))
228
257
 
229
258
  # Extract imports for coupling analysis
230
259
  imports = _extract_imports(tree, rel_path)
@@ -491,7 +520,7 @@ def file_detail(file_path: str) -> dict:
491
520
 
492
521
  functions.sort(key=lambda x: x["complexity"], reverse=True)
493
522
 
494
- loc = sum(1 for l in lines if l.strip() and not l.strip().startswith("#"))
523
+ loc = sum(1 for l in lines if _is_code_line(l))
495
524
  classes = sum(1 for n in ast.walk(tree) if isinstance(n, ast.ClassDef))
496
525
  imports = _extract_imports(tree, file_path)
497
526
 
@@ -526,13 +555,37 @@ def file_detail(file_path: str) -> dict:
526
555
  # ── AST Helpers ────────────────────────────────────────────────
527
556
 
528
557
 
558
+ def _own_body_nodes(node: ast.AST):
559
+ """Yield the nodes belonging to this function's OWN body.
560
+
561
+ Nested function and class definitions are skipped: they are reported as
562
+ functions in their own right, so counting their decision points here too
563
+ would charge one branch to two different functions. That over-count grew
564
+ with nesting depth - an IIFE-style wrapper or a registrar that defines
565
+ twenty inner handlers absorbed the entire file's complexity and sat at the
566
+ top of the offenders list while the real hot spots hid below it.
567
+
568
+ A lambda is NOT skipped. Lambdas are never listed as separate functions, so
569
+ their decision points would simply vanish; they stay with the function that
570
+ encloses them.
571
+ """
572
+ stack = list(ast.iter_child_nodes(node))
573
+ while stack:
574
+ child = stack.pop()
575
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
576
+ continue
577
+ yield child
578
+ stack.extend(ast.iter_child_nodes(child))
579
+
580
+
529
581
  def _cyclomatic_complexity(node: ast.AST) -> int:
530
582
  """Calculate cyclomatic complexity for a function/method node.
531
583
 
532
584
  CC = 1 + number of decision points (if/elif/for/while/except/and/or/assert)
585
+ in the function's own body. Nested functions are measured separately.
533
586
  """
534
587
  cc = 1
535
- for child in ast.walk(node):
588
+ for child in _own_body_nodes(node):
536
589
  if isinstance(child, (ast.If, ast.IfExp)):
537
590
  cc += 1
538
591
  elif isinstance(child, ast.For):
@@ -553,9 +606,18 @@ def _cyclomatic_complexity(node: ast.AST) -> int:
553
606
 
554
607
 
555
608
  def _function_loc(node: ast.AST, lines: list) -> int:
556
- """Count lines of code in a function."""
609
+ """Count lines of CODE in a function, by the same rule as file-level LOC.
610
+
611
+ This returned `end_lineno - lineno + 1` - a raw line span - while the
612
+ docstring claimed lines of code and every file-level LOC excluded blanks and
613
+ comments. So a function's LOC and its file's LOC were different units sitting
614
+ side by side in the dashboard, and the maintainability index (which divides
615
+ by LOC) penalised a well-commented function for its comments.
616
+ """
557
617
  if hasattr(node, "end_lineno") and node.end_lineno:
558
- return node.end_lineno - node.lineno + 1
618
+ span = lines[node.lineno - 1:node.end_lineno]
619
+ # Floor of 1: a one-line body must never report 0.
620
+ return max(1, sum(1 for l in span if _is_code_line(l)))
559
621
  # Fallback: count indented lines
560
622
  start = node.lineno - 1
561
623
  count = 1
@@ -0,0 +1,108 @@
1
+ """Code metrics from the tina4 CLI engine (ADR-0002).
2
+
3
+ The metrics engine now lives in the Rust CLI: one tree-sitter implementation
4
+ covering every language, instead of a hand-rolled AST walker per framework. This
5
+ adapter shells out to it and reshapes the payload into the dashboard's contract.
6
+
7
+ It returns None whenever the CLI cannot supply a COMPLETE payload - no binary on
8
+ PATH, a non-zero exit, unparseable JSON, or an older CLI whose engine predates a
9
+ field the dashboard renders. The caller then falls back to the in-framework
10
+ module, so a stale CLI degrades quietly instead of blanking the chart.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import shutil
17
+ import subprocess
18
+ from pathlib import Path
19
+
20
+ # The dashboard reads these at the top level; the CLI nests them under "summary"
21
+ # because its own text report and the --fail-on gate group them there.
22
+ _SUMMARY_KEYS = (
23
+ "files_analyzed",
24
+ "total_functions",
25
+ "avg_complexity",
26
+ "avg_maintainability",
27
+ )
28
+
29
+ # Fields the dashboard renders that a pre-3.8.59 engine does not emit. Checking
30
+ # for the DATA is honest where checking a version string is not: a user may run
31
+ # any CLI build, and the payload tells us what it can actually do.
32
+ _REQUIRED_FILE_KEYS = ("path", "loc", "avg_complexity", "maintainability", "has_tests")
33
+ _REQUIRED_FUNCTION_KEYS = ("name", "file", "line", "complexity", "loc")
34
+
35
+ _TIMEOUT_SECONDS = 60
36
+
37
+
38
+ def engine_path() -> str | None:
39
+ """Absolute path to the tina4 CLI binary, or None when it is not installed."""
40
+ return shutil.which("tina4")
41
+
42
+
43
+ def engine_analysis(root: str, scan_mode: str = "project") -> dict | None:
44
+ """Run `tina4 metrics --json` over root and adapt it to the dashboard shape.
45
+
46
+ The framework passes --path and owns scan_mode: it knows where its own
47
+ package lives, and the engine is deliberately language-agnostic, so root
48
+ resolution stays on this side of the boundary.
49
+ """
50
+ binary = engine_path()
51
+ if binary is None:
52
+ return None
53
+
54
+ try:
55
+ proc = subprocess.run(
56
+ [binary, "metrics", "--path", str(root), "--json"],
57
+ capture_output=True,
58
+ text=True,
59
+ timeout=_TIMEOUT_SECONDS,
60
+ )
61
+ except (OSError, subprocess.SubprocessError):
62
+ return None
63
+
64
+ if proc.returncode != 0 or not proc.stdout.strip():
65
+ return None
66
+
67
+ try:
68
+ payload = json.loads(proc.stdout)
69
+ except (json.JSONDecodeError, ValueError):
70
+ return None
71
+
72
+ return adapt_payload(payload, root, scan_mode)
73
+
74
+
75
+ def adapt_payload(payload: dict, root: str, scan_mode: str = "project") -> dict | None:
76
+ """Flatten the engine payload into the dashboard contract, or None if short.
77
+
78
+ Split out from engine_analysis so the reshaping is testable without a
79
+ subprocess - the shaping is where the contract actually lives.
80
+ """
81
+ if not isinstance(payload, dict):
82
+ return None
83
+
84
+ summary = payload.get("summary")
85
+ file_metrics = payload.get("file_metrics")
86
+ functions = payload.get("most_complex_functions")
87
+ if not isinstance(summary, dict) or not isinstance(file_metrics, list):
88
+ return None
89
+ if not isinstance(functions, list):
90
+ return None
91
+
92
+ if any(key not in summary for key in _SUMMARY_KEYS):
93
+ return None
94
+ if file_metrics and any(key not in file_metrics[0] for key in _REQUIRED_FILE_KEYS):
95
+ return None
96
+ if functions and any(key not in functions[0] for key in _REQUIRED_FUNCTION_KEYS):
97
+ return None
98
+
99
+ result = {key: summary[key] for key in _SUMMARY_KEYS}
100
+ result["file_metrics"] = file_metrics
101
+ result["most_complex_functions"] = functions[:15]
102
+ result["dependency_graph"] = payload.get("dependency_graph") or {}
103
+ # The framework decides these two - the engine always reports "project"
104
+ # because it cannot know which directory is a framework package.
105
+ result["scan_mode"] = scan_mode
106
+ result["scan_root"] = str(Path(root).resolve())
107
+ result["engine"] = "tina4-cli"
108
+ return result