tina4-python 3.13.88__tar.gz → 3.13.89__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.88 → tina4_python-3.13.89}/PKG-INFO +1 -1
  2. {tina4_python-3.13.88 → tina4_python-3.13.89}/pyproject.toml +1 -1
  3. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/cli/__init__.py +27 -14
  4. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/frond/engine.py +11 -0
  5. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/frond/parser.py +59 -2
  6. {tina4_python-3.13.88 → tina4_python-3.13.89}/.gitignore +0 -0
  7. {tina4_python-3.13.88 → tina4_python-3.13.89}/README.md +0 -0
  8. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/CLAUDE.md +0 -0
  9. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/HtmlElement.py +0 -0
  10. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/Testing.py +0 -0
  11. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/__init__.py +0 -0
  12. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/ai/__init__.py +0 -0
  13. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/container/__init__.py +0 -0
  17. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/context/__init__.py +0 -0
  18. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/context/chunker.py +0 -0
  19. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/core/__init__.py +0 -0
  20. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/core/cache.py +0 -0
  21. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/core/constants.py +0 -0
  22. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/core/events.py +0 -0
  23. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/core/middleware.py +0 -0
  24. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/core/rate_limiter.py +0 -0
  25. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/core/request.py +0 -0
  26. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/core/response.py +0 -0
  27. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/core/router.py +0 -0
  28. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/core/server.py +0 -0
  29. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/crud/__init__.py +0 -0
  30. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/database/__init__.py +0 -0
  31. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/database/adapter.py +0 -0
  32. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/database/connection.py +0 -0
  33. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/database/firebird.py +0 -0
  34. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/database/mongodb.py +0 -0
  35. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/database/mssql.py +0 -0
  36. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/database/mysql.py +0 -0
  37. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/database/odbc.py +0 -0
  38. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/database/postgres.py +0 -0
  39. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/database/sqlite.py +0 -0
  40. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/debug/__init__.py +0 -0
  41. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/debug/error_overlay.py +0 -0
  42. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/dev_admin/__init__.py +0 -0
  43. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/dev_admin/metrics.py +0 -0
  44. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/dev_admin/plan.py +0 -0
  45. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/dev_admin/project_index.py +0 -0
  46. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/docs.py +0 -0
  47. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/docstore/__init__.py +0 -0
  48. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/dotenv/__init__.py +0 -0
  49. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/env.py +0 -0
  50. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/frond/FROND.md +0 -0
  51. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/frond/__init__.py +0 -0
  52. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/frond/compiler.py +0 -0
  53. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/auth/meta.json +0 -0
  54. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  55. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/database/meta.json +0 -0
  56. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  57. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/error-overlay/meta.json +0 -0
  58. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  59. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/orm/meta.json +0 -0
  60. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  61. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  62. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/queue/meta.json +0 -0
  63. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  64. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/rest-api/meta.json +0 -0
  65. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  66. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/templates/meta.json +0 -0
  67. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  68. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  69. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/graphql/__init__.py +0 -0
  70. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/i18n/__init__.py +0 -0
  71. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/mcp/__init__.py +0 -0
  72. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/mcp/protocol.py +0 -0
  73. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/mcp/tools.py +0 -0
  74. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/messenger/__init__.py +0 -0
  75. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/migration/__init__.py +0 -0
  76. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/migration/runner.py +0 -0
  77. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/mqtt/__init__.py +0 -0
  78. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/mqtt/message.py +0 -0
  79. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/orm/__init__.py +0 -0
  80. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/orm/fields.py +0 -0
  81. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/orm/model.py +0 -0
  82. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/__feedback/widget.js +0 -0
  83. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/css/tina4.css +0 -0
  84. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/css/tina4.min.css +0 -0
  85. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/favicon.ico +0 -0
  86. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/images/logo.svg +0 -0
  87. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  88. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/js/frond.js +0 -0
  89. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/js/frond.min.js +0 -0
  90. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  91. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/js/tina4.min.js +0 -0
  92. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/js/tina4js.min.js +0 -0
  93. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/swagger/index.html +0 -0
  94. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  95. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/query_builder/__init__.py +0 -0
  96. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/queue/__init__.py +0 -0
  97. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/queue/job.py +0 -0
  98. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/queue/kafka_backend.py +0 -0
  99. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/queue/lite_backend.py +0 -0
  100. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/queue/mongo_backend.py +0 -0
  101. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/queue/rabbitmq_backend.py +0 -0
  102. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/queue_backends/__init__.py +0 -0
  103. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/queue_backends/kafka_backend.py +0 -0
  104. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/queue_backends/mongo_backend.py +0 -0
  105. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  106. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/realtime/__init__.py +0 -0
  107. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/realtime/models/Attachment.py +0 -0
  108. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/realtime/models/Channel.py +0 -0
  109. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/realtime/models/ChannelMember.py +0 -0
  110. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/realtime/models/Message.py +0 -0
  111. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/realtime/models/Workspace.py +0 -0
  112. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/realtime/models/__init__.py +0 -0
  113. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/realtime/storage.py +0 -0
  114. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/__init__.py +0 -0
  115. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  116. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_badges.scss +0 -0
  117. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  118. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_cards.scss +0 -0
  119. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_forms.scss +0 -0
  120. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_grid.scss +0 -0
  121. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_modals.scss +0 -0
  122. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_nav.scss +0 -0
  123. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_pagination.scss +0 -0
  124. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_reset.scss +0 -0
  125. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_tables.scss +0 -0
  126. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_typography.scss +0 -0
  127. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  128. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/_variables.scss +0 -0
  129. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/base.scss +0 -0
  130. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/colors.scss +0 -0
  131. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/scss/tina4css/tina4.scss +0 -0
  132. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/seeder/__init__.py +0 -0
  133. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/service/__init__.py +0 -0
  134. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/session/__init__.py +0 -0
  135. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/session_handlers/__init__.py +0 -0
  136. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  137. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/session_handlers/redis_handler.py +0 -0
  138. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/session_handlers/valkey_handler.py +0 -0
  139. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/swagger/__init__.py +0 -0
  140. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/components/crud.twig +0 -0
  141. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  142. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  143. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/docker/python/Dockerfile +0 -0
  144. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  145. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/errors/302.twig +0 -0
  146. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/errors/401.twig +0 -0
  147. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/errors/403.twig +0 -0
  148. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/errors/404.twig +0 -0
  149. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/errors/500.twig +0 -0
  150. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/errors/502.twig +0 -0
  151. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/errors/503.twig +0 -0
  152. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/errors/base.twig +0 -0
  153. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/frontend/README.md +0 -0
  154. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/templates/readme.md +0 -0
  155. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/test/__init__.py +0 -0
  156. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/test_client/__init__.py +0 -0
  157. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  166. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  167. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  168. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  169. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/validator/__init__.py +0 -0
  170. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/websocket/__init__.py +0 -0
  171. {tina4_python-3.13.88 → tina4_python-3.13.89}/tina4_python/websocket/backplane.py +0 -0
  172. {tina4_python-3.13.88 → tina4_python-3.13.89}/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.88
3
+ Version: 3.13.89
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.88"
3
+ version = "3.13.89"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -110,6 +110,21 @@ def _parse_fields(fields_str: str) -> list[tuple[str, str]]:
110
110
  return result
111
111
 
112
112
 
113
+ # Called without --fields, the generators fall back to a single `name` string
114
+ # column. That default MUST be materialised here, in one place, and then flow
115
+ # into the model, the migration, the template and the test alike. It used to
116
+ # live only inside the model template, so `generate model X` / `generate crud X`
117
+ # wrote a model declaring `name` while the migration - built from the parsed
118
+ # field list, which was empty - created only id + created_at. The first write
119
+ # then failed with "table x has no column named name".
120
+ DEFAULT_FIELDS: list[tuple[str, str]] = [("name", "string")]
121
+
122
+
123
+ def _fields_or_default(fields_str: str) -> list[tuple[str, str]]:
124
+ """Parsed --fields, or the default single `name` column when none given."""
125
+ return _parse_fields(fields_str) or list(DEFAULT_FIELDS)
126
+
127
+
113
128
  def _parse_flags(args: list[str]) -> tuple[dict, list[str]]:
114
129
  """Parse --key value and --flag from args. Returns (flags, positional)."""
115
130
  # Boolean-only flags that never take a value argument
@@ -1083,7 +1098,7 @@ def _gen_model(name: str, flags: dict, *, emit_test: bool = True):
1083
1098
  SQLite roundtrip test. Composite generators (crud/auth) pass emit_test=False
1084
1099
  and emit their own broader test instead.
1085
1100
  """
1086
- fields = _parse_fields(flags.get("fields", ""))
1101
+ fields = _fields_or_default(flags.get("fields", ""))
1087
1102
  table = _to_table(name)
1088
1103
 
1089
1104
  # Determine which ORM field types we need to import
@@ -1091,20 +1106,15 @@ def _gen_model(name: str, flags: dict, *, emit_test: bool = True):
1091
1106
  for _, ftype in fields:
1092
1107
  info = FIELD_TYPE_MAP.get(ftype, FIELD_TYPE_MAP["string"])
1093
1108
  used_types.add(info["orm"])
1094
- if not fields:
1095
- used_types.add("StringField")
1096
1109
  used_types.add("DateTimeField") # for created_at
1097
1110
 
1098
1111
  imports = ", ".join(sorted(used_types))
1099
1112
 
1100
1113
  # Build field lines
1101
1114
  field_lines = [f" id = IntegerField(primary_key=True, auto_increment=True)"]
1102
- if fields:
1103
- for fname, ftype in fields:
1104
- info = FIELD_TYPE_MAP.get(ftype, FIELD_TYPE_MAP["string"])
1105
- field_lines.append(f" {fname} = {info['orm']}()")
1106
- else:
1107
- field_lines.append(" name = StringField()")
1115
+ for fname, ftype in fields:
1116
+ info = FIELD_TYPE_MAP.get(ftype, FIELD_TYPE_MAP["string"])
1117
+ field_lines.append(f" {fname} = {info['orm']}()")
1108
1118
  field_lines.append(" created_at = DateTimeField()")
1109
1119
 
1110
1120
  # Write model file
@@ -1227,6 +1237,8 @@ async def get_{singular}(request, response):
1227
1237
  async def create_{singular}(request, response):
1228
1238
  """Create a new {singular}. {write_doc}"""
1229
1239
  {ext_create} item = {model}.create(request.body)
1240
+ if item is False:
1241
+ return response({{"error": "Could not create {singular}"}}, 400)
1230
1242
  return response(item.to_dict(), 201)
1231
1243
 
1232
1244
 
@@ -1241,7 +1253,8 @@ async def update_{singular}(request, response):
1241
1253
  {ext_update} for key, value in request.body.items():
1242
1254
  if hasattr(item, key) and key != "id":
1243
1255
  setattr(item, key, value)
1244
- item.save()
1256
+ if item.save() is False:
1257
+ return response({{"error": "Could not update {singular}"}}, 400)
1245
1258
  return response(item.to_dict())
1246
1259
 
1247
1260
 
@@ -1359,7 +1372,7 @@ def _gen_crud(name: str, flags: dict):
1359
1372
 
1360
1373
  tina4python generate crud Product --fields "name:string,price:float"
1361
1374
  """
1362
- fields = _parse_fields(flags.get("fields", ""))
1375
+ fields = _fields_or_default(flags.get("fields", ""))
1363
1376
  table = _to_table(name)
1364
1377
  route_name = table + "s" # routes are plural
1365
1378
 
@@ -1381,7 +1394,7 @@ def _gen_crud(name: str, flags: dict):
1381
1394
  template_path = template_dir / f"{route_name}.twig"
1382
1395
  if not template_path.exists():
1383
1396
  # Build column headers from fields
1384
- cols = [f for f, _ in fields] if fields else ["name"]
1397
+ cols = [f for f, _ in fields]
1385
1398
  th = "\n ".join(f"<th>{c.replace('_', ' ').title()}</th>" for c in cols)
1386
1399
  td = "\n ".join(f"<td>{{{{ item.{c} }}}}</td>" for c in cols)
1387
1400
 
@@ -2685,14 +2698,14 @@ def _gen_view(name: str, flags: dict = None):
2685
2698
  tina4python generate view Product --fields "name:string,price:float"
2686
2699
  """
2687
2700
  flags = flags or {}
2688
- fields = _parse_fields(flags.get("fields", ""))
2701
+ fields = _fields_or_default(flags.get("fields", ""))
2689
2702
  table = _to_table(name)
2690
2703
  route_name = table + "s"
2691
2704
 
2692
2705
  target = Path("src/templates/pages")
2693
2706
  target.mkdir(parents=True, exist_ok=True)
2694
2707
 
2695
- cols = [f for f, _ in fields] if fields else ["name"]
2708
+ cols = [f for f, _ in fields]
2696
2709
 
2697
2710
  # List view
2698
2711
  list_path = target / f"{route_name}.twig"
@@ -2030,6 +2030,17 @@ class Frond:
2030
2030
  elif kind == "set":
2031
2031
  self._handle_set(node.content, context)
2032
2032
 
2033
+ elif kind == "set_block":
2034
+ # {% set name %}...{% endset %} -- render the body and bind it,
2035
+ # emitting nothing here. The captured value is a SafeString
2036
+ # because it is template output that has already been escaped
2037
+ # on the way in; re-escaping it at {{ name }} would double-encode
2038
+ # every entity. Twig and Jinja2 both mark the capture safe.
2039
+ if node.name:
2040
+ context[node.name] = SafeString(
2041
+ self._render_nodes(node.body, context)
2042
+ )
2043
+
2033
2044
  elif kind == "include":
2034
2045
  # Sandbox: check tag
2035
2046
  if not (self._sandbox and self._allowed_tags is not None
@@ -223,6 +223,21 @@ class SetNode:
223
223
  strip_before: bool = False
224
224
 
225
225
 
226
+ @dataclass(slots=True)
227
+ class SetBlockNode:
228
+ """{% set name %}...{% endset %} — capture the rendered body into ``name``.
229
+
230
+ Core syntax in BOTH reference engines (Twig and Jinja2) and broken in all
231
+ four Tina4 frameworks until 3.13.89: the body rendered inline where it stood
232
+ and the variable was never assigned, so ``{% set g %}hi{% endset %}[{{ g }}]``
233
+ printed ``hi[]`` instead of ``[hi]``.
234
+ """
235
+ kind = "set_block"
236
+ name: str = ""
237
+ body: list = field(default_factory=list)
238
+ strip_before: bool = False
239
+
240
+
226
241
  @dataclass(slots=True)
227
242
  class IncludeNode:
228
243
  """{% include "file" [with {...}] [ignore missing] %}."""
@@ -462,6 +477,28 @@ def _needs_live_strip(tokens: list, index: int, strip_before: bool) -> bool:
462
477
  return strip_before and index > 0 and tokens[index - 1][0] != TEXT
463
478
 
464
479
 
480
+ # Every tag that OPENS a construct. An unknown tag is a typo, and 3.13.89 makes
481
+ # it raise rather than render its body: a mistyped guard -- {% iff is_admin %}
482
+ # instead of {% if is_admin %} -- used to render the gated content
483
+ # UNCONDITIONALLY, so a reviewer saw a guard that was not there. Twig and Jinja2
484
+ # both raise on an unknown tag; Frond now does too. There is no user-extension
485
+ # point for tags in any of the four frameworks, so an unknown name is always a
486
+ # mistake, never a plugin.
487
+ _KNOWN_TAGS = frozenset({
488
+ "autoescape", "block", "cache", "extends", "for", "from", "if", "import",
489
+ "include", "live", "macro", "raw", "set", "spaceless",
490
+ })
491
+
492
+ # Terminators and branch keywords. These reach _parse_block only when stray
493
+ # (their own collector consumes them in the normal case), and a stray one keeps
494
+ # the old render-nothing behaviour -- see the comment at the raise.
495
+ _TERMINATORS = frozenset({
496
+ "elif", "else", "elseif", "endautoescape", "endblock", "endcache",
497
+ "endfor", "endif", "endlive", "endmacro", "endraw", "endset",
498
+ "endspaceless",
499
+ })
500
+
501
+
465
502
  def _parse_block(content: str, strip_before: bool, strip_after: bool,
466
503
  tokens: list, i: int):
467
504
  """Build the node for the BLOCK token at ``i``. Returns (node, next_index);
@@ -477,6 +514,16 @@ def _parse_block(content: str, strip_before: bool, strip_after: bool,
477
514
  return _parse_for(content, strip_before, strip_after, tokens, i)
478
515
 
479
516
  if tag == "set":
517
+ # An assignment has an ``=``; without one this is the BLOCK form,
518
+ # {% set name %}...{% endset %}, which captures its rendered body.
519
+ # A bare ``in`` test is exact here, not a shortcut: the block form's tag
520
+ # content is only ever ``set <name>``, so an ``=`` anywhere -- even
521
+ # inside a quoted value like {% set msg = "a = b" %} -- means assignment.
522
+ if "=" not in content:
523
+ body, end = _collect_body(tokens, i, "set", "endset")
524
+ _bake_strip_after_end(tokens, strip_after, end)
525
+ name = parts[1] if len(parts) > 1 else ""
526
+ return SetBlockNode(name, _parse_nodes(body), strip_before), end
480
527
  return SetNode(content, strip_before), i + 1
481
528
 
482
529
  if tag == "include":
@@ -525,8 +572,18 @@ def _parse_block(content: str, strip_before: bool, strip_after: bool,
525
572
  if tag in ("block", "endblock"):
526
573
  return BlockMarkerNode(parts[1] if len(parts) > 1 else "", strip_before), i + 1
527
574
 
528
- # A stray terminator or an unknown tag: no output, advance one token.
529
- return SkipNode(strip_before), i + 1
575
+ if tag == "" or tag in _TERMINATORS:
576
+ # An empty tag ({% %}, or a bare whitespace-control {%- -%}) and a
577
+ # stray terminator (an {% endif %} with no {% if %}): no output,
578
+ # advance one token. Malformed, but it has always rendered nothing, and
579
+ # nothing is the safe answer -- unlike an unknown tag it cannot expose
580
+ # content that was meant to be gated.
581
+ return SkipNode(strip_before), i + 1
582
+
583
+ raise ValueError(
584
+ f'Frond: unknown tag "{tag}" -- known tags are: '
585
+ + ", ".join(sorted(_KNOWN_TAGS))
586
+ )
530
587
 
531
588
 
532
589
  def _parse_if(content: str, strip_before: bool, strip_after: bool,
File without changes