tina4-python 3.13.86__tar.gz → 3.13.87__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 (172) hide show
  1. {tina4_python-3.13.86 → tina4_python-3.13.87}/PKG-INFO +1 -1
  2. {tina4_python-3.13.86 → tina4_python-3.13.87}/pyproject.toml +1 -1
  3. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/CLAUDE.md +1 -1
  4. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/cli/__init__.py +2 -0
  5. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/dev_admin/metrics.py +11 -2
  6. tina4_python-3.13.87/tina4_python/frond/compiler.py +259 -0
  7. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/frond/engine.py +475 -646
  8. tina4_python-3.13.87/tina4_python/frond/parser.py +745 -0
  9. {tina4_python-3.13.86 → tina4_python-3.13.87}/.gitignore +0 -0
  10. {tina4_python-3.13.86 → tina4_python-3.13.87}/README.md +0 -0
  11. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/HtmlElement.py +0 -0
  12. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/Testing.py +0 -0
  13. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/__init__.py +0 -0
  14. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/ai/__init__.py +0 -0
  15. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/api/__init__.py +0 -0
  16. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/auth/__init__.py +0 -0
  17. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/cache/__init__.py +0 -0
  18. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/container/__init__.py +0 -0
  19. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/context/__init__.py +0 -0
  20. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/context/chunker.py +0 -0
  21. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/__init__.py +0 -0
  22. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/cache.py +0 -0
  23. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/constants.py +0 -0
  24. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/events.py +0 -0
  25. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/middleware.py +0 -0
  26. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/rate_limiter.py +0 -0
  27. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/request.py +0 -0
  28. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/response.py +0 -0
  29. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/router.py +0 -0
  30. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/server.py +0 -0
  31. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/crud/__init__.py +0 -0
  32. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/__init__.py +0 -0
  33. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/adapter.py +0 -0
  34. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/connection.py +0 -0
  35. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/firebird.py +0 -0
  36. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/mongodb.py +0 -0
  37. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/mssql.py +0 -0
  38. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/mysql.py +0 -0
  39. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/odbc.py +0 -0
  40. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/postgres.py +0 -0
  41. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/sqlite.py +0 -0
  42. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/debug/__init__.py +0 -0
  43. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/debug/error_overlay.py +0 -0
  44. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/dev_admin/__init__.py +0 -0
  45. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/dev_admin/plan.py +0 -0
  46. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/dev_admin/project_index.py +0 -0
  47. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/docs.py +0 -0
  48. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/docstore/__init__.py +0 -0
  49. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/dotenv/__init__.py +0 -0
  50. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/env.py +0 -0
  51. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/frond/FROND.md +0 -0
  52. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/frond/__init__.py +0 -0
  53. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/auth/meta.json +0 -0
  54. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  55. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/database/meta.json +0 -0
  56. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  57. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/error-overlay/meta.json +0 -0
  58. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  59. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/orm/meta.json +0 -0
  60. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  61. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  62. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/queue/meta.json +0 -0
  63. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  64. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/rest-api/meta.json +0 -0
  65. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  66. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/templates/meta.json +0 -0
  67. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  68. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  69. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/graphql/__init__.py +0 -0
  70. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/i18n/__init__.py +0 -0
  71. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/mcp/__init__.py +0 -0
  72. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/mcp/protocol.py +0 -0
  73. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/mcp/tools.py +0 -0
  74. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/messenger/__init__.py +0 -0
  75. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/migration/__init__.py +0 -0
  76. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/migration/runner.py +0 -0
  77. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/mqtt/__init__.py +0 -0
  78. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/mqtt/message.py +0 -0
  79. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/orm/__init__.py +0 -0
  80. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/orm/fields.py +0 -0
  81. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/orm/model.py +0 -0
  82. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/__feedback/widget.js +0 -0
  83. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/css/tina4.css +0 -0
  84. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/css/tina4.min.css +0 -0
  85. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/favicon.ico +0 -0
  86. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/images/logo.svg +0 -0
  87. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  88. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/js/frond.js +0 -0
  89. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/js/frond.min.js +0 -0
  90. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  91. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/js/tina4.min.js +0 -0
  92. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/js/tina4js.min.js +0 -0
  93. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/swagger/index.html +0 -0
  94. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  95. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/query_builder/__init__.py +0 -0
  96. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/__init__.py +0 -0
  97. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/job.py +0 -0
  98. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/kafka_backend.py +0 -0
  99. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/lite_backend.py +0 -0
  100. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/mongo_backend.py +0 -0
  101. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/rabbitmq_backend.py +0 -0
  102. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue_backends/__init__.py +0 -0
  103. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue_backends/kafka_backend.py +0 -0
  104. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue_backends/mongo_backend.py +0 -0
  105. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  106. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/__init__.py +0 -0
  107. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/Attachment.py +0 -0
  108. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/Channel.py +0 -0
  109. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/ChannelMember.py +0 -0
  110. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/Message.py +0 -0
  111. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/Workspace.py +0 -0
  112. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/__init__.py +0 -0
  113. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/storage.py +0 -0
  114. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/__init__.py +0 -0
  115. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  116. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_badges.scss +0 -0
  117. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  118. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_cards.scss +0 -0
  119. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_forms.scss +0 -0
  120. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_grid.scss +0 -0
  121. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_modals.scss +0 -0
  122. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_nav.scss +0 -0
  123. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_pagination.scss +0 -0
  124. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_reset.scss +0 -0
  125. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_tables.scss +0 -0
  126. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_typography.scss +0 -0
  127. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  128. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_variables.scss +0 -0
  129. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/base.scss +0 -0
  130. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/colors.scss +0 -0
  131. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/tina4.scss +0 -0
  132. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/seeder/__init__.py +0 -0
  133. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/service/__init__.py +0 -0
  134. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/session/__init__.py +0 -0
  135. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/session_handlers/__init__.py +0 -0
  136. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  137. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/session_handlers/redis_handler.py +0 -0
  138. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/session_handlers/valkey_handler.py +0 -0
  139. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/swagger/__init__.py +0 -0
  140. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/components/crud.twig +0 -0
  141. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  142. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  143. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/docker/python/Dockerfile +0 -0
  144. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  145. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/302.twig +0 -0
  146. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/401.twig +0 -0
  147. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/403.twig +0 -0
  148. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/404.twig +0 -0
  149. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/500.twig +0 -0
  150. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/502.twig +0 -0
  151. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/503.twig +0 -0
  152. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/base.twig +0 -0
  153. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/frontend/README.md +0 -0
  154. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/readme.md +0 -0
  155. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/test/__init__.py +0 -0
  156. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/test_client/__init__.py +0 -0
  157. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  166. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  167. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  168. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  169. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/validator/__init__.py +0 -0
  170. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/websocket/__init__.py +0 -0
  171. {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/websocket/backplane.py +0 -0
  172. {tina4_python-3.13.86 → tina4_python-3.13.87}/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.86
3
+ Version: 3.13.87
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.86"
3
+ version = "3.13.87"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -1913,7 +1913,7 @@ async def dashboard(request, response):
1913
1913
  ## v3 Features Summary
1914
1914
 
1915
1915
  - **98 built-in features**, zero third-party dependencies
1916
- - **2,899 tests** passing across all modules
1916
+ - **3,887 tests** passing across all modules
1917
1917
  - **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
1918
1918
  - **`tina4python generate`**: model, route, migration, middleware scaffolding
1919
1919
  - **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), DB query caching — request-scoped auto cache **off by default — opt-in via `TINA4_AUTO_CACHING=true`** (TTL `TINA4_AUTO_CACHING_TTL=5`s) dedupes identical reads within a request and flushes on writes; it ships OFF because a request-scoped cache can return pre-write state in a read-after-write (`SELECT MAX(id)` before an `INSERT` → duplicate keys), so opt in per read-heavy endpoint. Persistent cross-request cache also opt-in via `TINA4_DB_CACHE=true` (TTL `TINA4_DB_CACHE_TTL=30`s) routed through the unified backend set via `TINA4_DB_CACHE_BACKEND` (memory/file/redis/valkey/memcached/mongodb/database) + `TINA4_DB_CACHE_URL` so instances share one cache with global write-invalidation; `cache_stats()` reports `mode` (request/persistent/off) and `backend`, `cache_clear()`
@@ -787,6 +787,8 @@ def _init(args):
787
787
  print(f"\nProject scaffolded at {target.resolve()}")
788
788
  print(" Run: tina4python serve")
789
789
  print(" Run: tina4python ai (detect & install AI tool context)")
790
+ print(" Then: open the dev admin at /__dev, paste your Tina4 MCP key,")
791
+ print(" and describe what to build. See https://tina4.com/build-with-ai")
790
792
 
791
793
 
792
794
  # ── Serve ─────────────────────────────────────────────────────────────
@@ -323,7 +323,14 @@ def full_analysis(root: str = "src") -> dict:
323
323
  "total_functions": len(all_functions),
324
324
  "avg_complexity": round(avg_cc, 2),
325
325
  "avg_maintainability": round(avg_mi, 1),
326
+ # Display-only: the top-15 for the "most complex functions" report.
327
+ # Do NOT source offenders / --fail-on from this — capping here silently
328
+ # hides the 16th+ over-threshold function from the gate. offenders()
329
+ # reads "all_functions" (below) instead.
326
330
  "most_complex_functions": all_functions[:15],
331
+ # Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
332
+ # so no function over the complexity threshold ever escapes the gate.
333
+ "all_functions": all_functions,
327
334
  "file_metrics": file_metrics,
328
335
  "violations": violations,
329
336
  "dependency_graph": import_graph,
@@ -369,8 +376,10 @@ def offenders(root: str = "src", top: int = 20) -> dict:
369
376
 
370
377
  items: list[dict] = []
371
378
 
372
- # Function-level: cyclomatic complexity.
373
- for fn in analysis.get("most_complex_functions", []):
379
+ # Function-level: cyclomatic complexity. Use the FULL function list (not the
380
+ # display-capped most_complex_functions[:15]) so a 16th+ over-threshold
381
+ # function is never silently dropped from the offenders list or --fail-on.
382
+ for fn in analysis.get("all_functions", analysis.get("most_complex_functions", [])):
374
383
  cc = fn["complexity"]
375
384
  if cc > 10:
376
385
  items.append({
@@ -0,0 +1,259 @@
1
+ # Tina4 Frond — Ahead-of-time template compiler (ADR-0001, part 1).
2
+ """
3
+ Compile a parsed Frond AST ONCE into a native Python callable so that
4
+ ``render()`` just CALLS the compiled function and pays no per-render tree-walk.
5
+
6
+ Design (behaviour-safe by construction):
7
+
8
+ * The compiler consumes the SAME tree the interpreter does, produced by
9
+ :mod:`tina4_python.frond.parser`. It no longer sees tokens, so it cannot
10
+ disagree with the interpreter about structure: which nodes belong to which
11
+ branch/body, where a loop ends, and where whitespace is trimmed are all
12
+ decided once, by the parser. (This module used to carry ``_collect_if`` /
13
+ ``_collect_for`` — a second, hand-synchronised copy of the engine's own token
14
+ grouping — plus ``_apply_whitespace_control``. All three are deleted; the
15
+ parser owns that work now.)
16
+ * The emitted function reuses the engine's OWN runtime primitives for every
17
+ value-producing operation — ``engine._eval_var`` for ``{{ ... }}``,
18
+ ``_eval_comparison`` for ``if`` conditions, ``_eval_expr`` for ``for``
19
+ iterables, ``engine._handle_set`` for ``set``. Only the *structure* (which
20
+ nodes belong to which branch/body, the whitespace-free text runs, the loop
21
+ bookkeeping) is baked into native Python control flow. Because every value is
22
+ produced by the identical primitive the interpreter calls, a compiled template
23
+ renders byte-identically to the interpreted one.
24
+ * Only the common hot-path constructs are compiled: text, ``{{ var }}`` (with
25
+ dotted paths / filters / ternary — all handled inside ``_eval_var``),
26
+ ``if/elif/elseif/else``, ``for`` (+ loop vars + for-else), ``set``, comments,
27
+ and ``{% raw %}`` (which the tokenizer has already turned into literal text).
28
+ * Whitespace control is compiled too, because the parser baked the structural
29
+ trims into the text nodes. The ONE case that stays render-dependent is a
30
+ strip-before (``{{- `` / ``{%- ``) whose left neighbour is not literal text
31
+ (a value / block / comment output, mid-body): there the interpreter
32
+ right-strips the live output buffer, so the parser flags that node with
33
+ ``strip_before`` and this module falls back on it.
34
+ * ANY other construct (extends/block, include, macro, from/import, cache,
35
+ spaceless, autoescape, live) or a malformed tag makes :func:`compile_template`
36
+ return ``None`` so the caller falls back to the interpreter for that whole
37
+ template. A codegen/compile error also returns ``None``. A render is therefore
38
+ never broken by the compiler.
39
+ """
40
+
41
+
42
+ class _Unsupported(Exception):
43
+ """Raised internally when a node uses a construct the compiler does not
44
+ yet emit. Caught by :func:`compile_template`, which then returns ``None``
45
+ so the caller uses the interpreted renderer for the whole template."""
46
+
47
+
48
+ def _tostr(value) -> str:
49
+ """Coerce a rendered value to its output string.
50
+
51
+ THE cross-framework output contract, and it MUST stay identical to the
52
+ interpreter's copy in ``engine.Frond._to_output`` -- the compiled path and the
53
+ interpreted path have to render the same bytes. Change one, change both.
54
+
55
+ A boolean renders lowercase ``true``/``false``: Frond is a template language,
56
+ not Python, and lowercase is the form usable directly in HTML and JavaScript
57
+ (``data-active="{{ flag }}"`` -> ``data-active="true"``, testable from JS).
58
+
59
+ Breaking in 3.13.87: this used to emit Python's ``True``/``False``. The four
60
+ frameworks had drifted to four different answers -- Python ``True``/``False``
61
+ (Jinja2-faithful), PHP ``1``/``''`` (Twig-faithful, with ``false`` rendering as
62
+ an EMPTY STRING), Ruby inconsistent between a comparison and a bare variable,
63
+ Node ``true``/``false``. All four now agree; a 72-expression corpus locks it.
64
+
65
+ ``is True`` / ``is False`` identity checks are deliberate: ``1 == True`` in
66
+ Python, and an integer 1 must still render as ``1``.
67
+ """
68
+ if value is None:
69
+ return ""
70
+ if value is True:
71
+ return "true"
72
+ if value is False:
73
+ return "false"
74
+ return str(value)
75
+
76
+
77
+ def _pad(indent: int) -> str:
78
+ return " " * indent
79
+
80
+
81
+ def compile_template(ast):
82
+ """Compile an AST (a list of nodes) to ``_rendered(engine, ctx) -> str``.
83
+
84
+ Returns the callable, or ``None`` when the template contains a construct
85
+ that is not compiled yet / when codegen or ``compile()`` fails — in which
86
+ case the caller must render the template with the interpreter.
87
+ """
88
+ # Lazy import breaks the engine<->compiler import cycle (each imports the
89
+ # other only at call time, by which point both modules are loaded).
90
+ from tina4_python.frond import engine as _engine
91
+
92
+ try:
93
+ lines = ["def _rendered(engine, ctx):", " _b = []", " _ap = _b.append"]
94
+ _emit_body(ast, "ctx", 1, lines, {"n": 0})
95
+ lines.append(' return "".join(_b)')
96
+ source = "\n".join(lines)
97
+
98
+ namespace = {
99
+ "_LoopContext": _engine._LoopContext,
100
+ "_eval_expr": _engine._eval_expr,
101
+ "_eval_comparison": _engine._eval_comparison,
102
+ "_tostr": _tostr,
103
+ "isinstance": isinstance,
104
+ "list": list,
105
+ "len": len,
106
+ "enumerate": enumerate,
107
+ }
108
+ code = compile(source, "<frond_compiled>", "exec")
109
+ exec(code, namespace) # noqa: S102 — trusted, generated from template nodes
110
+ return namespace["_rendered"]
111
+ except _Unsupported:
112
+ return None
113
+ except Exception:
114
+ # A codegen/compile error must never break a render — fall back.
115
+ return None
116
+
117
+
118
+ # ── Codegen ─────────────────────────────────────────────────────
119
+
120
+
121
+ def _emit_body(nodes, ctxvar, indent, out, state):
122
+ """Emit code for a list of sibling nodes (mirrors ``_render_nodes`` dispatch
123
+ for the compiled subset). ``ctxvar`` is the name of the context variable in
124
+ scope (``ctx`` at top level, a loop-context name inside a ``for``)."""
125
+ for node in nodes:
126
+ kind = node.kind
127
+
128
+ if kind == "text":
129
+ out.append(_pad(indent) + "_ap(" + repr(node.text) + ")")
130
+ continue
131
+
132
+ if kind == "comment":
133
+ continue
134
+
135
+ # A strip-before the parser could not bake into a text node: the
136
+ # interpreter right-strips the LIVE output buffer there, which depends on
137
+ # rendered data, so the whole template falls back.
138
+ if node.strip_before:
139
+ raise _Unsupported
140
+
141
+ if kind == "output":
142
+ out.append(
143
+ _pad(indent)
144
+ + "_ap(_tostr(engine._eval_var(" + repr(node.expr) + ", " + ctxvar + ")))"
145
+ )
146
+
147
+ elif kind == "if":
148
+ _emit_if(node, ctxvar, indent, out, state)
149
+
150
+ elif kind == "for":
151
+ _emit_for(node, ctxvar, indent, out, state)
152
+
153
+ elif kind == "set":
154
+ out.append(
155
+ _pad(indent) + "engine._handle_set(" + repr(node.content) + ", " + ctxvar + ")"
156
+ )
157
+
158
+ else:
159
+ # extends/block, include, macro, from/import, cache, spaceless,
160
+ # autoescape, live, and any stray terminator: not compiled — fall
161
+ # back to the interpreter for this template.
162
+ raise _Unsupported
163
+
164
+
165
+ def _emit_if(node, ctxvar, indent, out, state):
166
+ branches = node.branches
167
+ if not branches:
168
+ raise _Unsupported
169
+
170
+ # A ``None`` condition is the ``{% else %}`` branch; native ``else:`` must be
171
+ # last. Anything else (an else before an elseif) is malformed — fall back.
172
+ for idx, (cond, _body) in enumerate(branches):
173
+ if cond is None and idx != len(branches) - 1:
174
+ raise _Unsupported
175
+
176
+ first = True
177
+ for cond, body in branches:
178
+ if cond is None:
179
+ out.append(_pad(indent) + "else:")
180
+ else:
181
+ keyword = "if" if first else "elif"
182
+ out.append(
183
+ _pad(indent)
184
+ + keyword
185
+ + " _eval_comparison("
186
+ + repr(cond)
187
+ + ", "
188
+ + ctxvar
189
+ + ", engine._eval_var_raw):"
190
+ )
191
+ body_start = len(out)
192
+ _emit_body(body, ctxvar, indent + 1, out, state)
193
+ if len(out) == body_start:
194
+ out.append(_pad(indent + 1) + "pass")
195
+ first = False
196
+
197
+
198
+ def _emit_for(node, ctxvar, indent, out, state):
199
+ var1 = node.key_var
200
+ var2 = node.value_var
201
+
202
+ suffix = state["n"]
203
+ state["n"] += 1
204
+ it = "_it%d" % suffix
205
+ items = "_items%d" % suffix
206
+ total = "_total%d" % suffix
207
+ isd = "_isd%d" % suffix
208
+ idx = "_idx%d" % suffix
209
+ item = "_item%d" % suffix
210
+ lc = "_lc%d" % suffix
211
+ k = "_k%d" % suffix
212
+ v = "_v%d" % suffix
213
+
214
+ out.append(_pad(indent) + it + " = _eval_expr(" + repr(node.iterable) + ", " + ctxvar + ")")
215
+ out.append(_pad(indent) + "if not " + it + ":")
216
+ body_start = len(out)
217
+ if node.else_body:
218
+ _emit_body(node.else_body, ctxvar, indent + 1, out, state)
219
+ if len(out) == body_start:
220
+ out.append(_pad(indent + 1) + "pass")
221
+
222
+ out.append(_pad(indent) + "else:")
223
+ out.append(
224
+ _pad(indent + 1)
225
+ + items + " = list(" + it + ".items()) if isinstance(" + it + ", dict) else list(" + it + ")"
226
+ )
227
+ out.append(_pad(indent + 1) + total + " = len(" + items + ")")
228
+ out.append(_pad(indent + 1) + isd + " = isinstance(" + it + ", dict)")
229
+ out.append(_pad(indent + 1) + "for " + idx + ", " + item + " in enumerate(" + items + "):")
230
+ out.append(_pad(indent + 2) + lc + " = _LoopContext(" + ctxvar + ")")
231
+ loop_dict = (
232
+ '{"index": ' + idx + "+1, "
233
+ '"index0": ' + idx + ", "
234
+ '"first": ' + idx + "==0, "
235
+ '"last": ' + idx + "==" + total + "-1, "
236
+ '"length": ' + total + ", "
237
+ '"revindex": ' + total + "-" + idx + ", "
238
+ '"revindex0": ' + total + "-" + idx + "-1, "
239
+ '"even": (' + idx + "+1)%2==0, "
240
+ '"odd": (' + idx + "+1)%2!=0}"
241
+ )
242
+ out.append(_pad(indent + 2) + lc + '["loop"] = ' + loop_dict)
243
+
244
+ out.append(_pad(indent + 2) + "if " + isd + ":")
245
+ out.append(_pad(indent + 3) + k + ", " + v + " = " + item)
246
+ out.append(_pad(indent + 3) + lc + "[" + repr(var1) + "] = " + k)
247
+ if var2:
248
+ out.append(_pad(indent + 3) + lc + "[" + repr(var2) + "] = " + v)
249
+ out.append(_pad(indent + 2) + "else:")
250
+ if var2:
251
+ out.append(_pad(indent + 3) + lc + "[" + repr(var1) + "] = " + idx)
252
+ out.append(_pad(indent + 3) + lc + "[" + repr(var2) + "] = " + item)
253
+ else:
254
+ out.append(_pad(indent + 3) + lc + "[" + repr(var1) + "] = " + item)
255
+
256
+ body_start = len(out)
257
+ _emit_body(node.body, lc, indent + 2, out, state)
258
+ if len(out) == body_start:
259
+ out.append(_pad(indent + 2) + "pass")