tina4-python 3.13.127__tar.gz → 3.13.129__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 (170) hide show
  1. {tina4_python-3.13.127 → tina4_python-3.13.129}/PKG-INFO +1 -1
  2. {tina4_python-3.13.127 → tina4_python-3.13.129}/pyproject.toml +1 -2
  3. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/cli/__init__.py +152 -14
  5. {tina4_python-3.13.127 → tina4_python-3.13.129}/.gitignore +0 -0
  6. {tina4_python-3.13.127 → tina4_python-3.13.129}/README.md +0 -0
  7. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/CLAUDE.md +0 -0
  8. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/HtmlElement.py +0 -0
  9. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/Testing.py +0 -0
  10. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/_import_helper.py +0 -0
  11. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/ai/__init__.py +0 -0
  12. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/ai/client.py +0 -0
  13. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/container/__init__.py +0 -0
  17. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/context/__init__.py +0 -0
  18. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/context/chunker.py +0 -0
  19. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/__init__.py +0 -0
  20. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/cache.py +0 -0
  21. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/constants.py +0 -0
  22. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/events.py +0 -0
  23. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/middleware.py +0 -0
  24. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/port_takeover.py +0 -0
  25. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/rate_limiter.py +0 -0
  26. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/request.py +0 -0
  27. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/response.py +0 -0
  28. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/router.py +0 -0
  29. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/core/server.py +0 -0
  30. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/crud/__init__.py +0 -0
  31. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/__init__.py +0 -0
  32. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/adapter.py +0 -0
  33. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/connection.py +0 -0
  34. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/database_url.py +0 -0
  35. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/firebird.py +0 -0
  36. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/mongodb.py +0 -0
  37. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/mssql.py +0 -0
  38. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/mysql.py +0 -0
  39. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/odbc.py +0 -0
  40. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/postgres.py +0 -0
  41. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/sql_translator.py +0 -0
  42. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/database/sqlite.py +0 -0
  43. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/debug/__init__.py +0 -0
  44. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/debug/error_overlay.py +0 -0
  45. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/dev_admin/__init__.py +0 -0
  46. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/dev_admin/metrics.py +0 -0
  47. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/dev_admin/plan.py +0 -0
  48. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/dev_admin/project_index.py +0 -0
  49. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/docs.py +0 -0
  50. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/docstore/__init__.py +0 -0
  51. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/dotenv/__init__.py +0 -0
  52. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/env.py +0 -0
  53. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/frond/FROND.md +0 -0
  54. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/frond/__init__.py +0 -0
  55. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/frond/compiler.py +0 -0
  56. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/frond/engine.py +0 -0
  57. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/frond/parser.py +0 -0
  58. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/auth/meta.json +0 -0
  59. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  60. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/database/meta.json +0 -0
  61. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  62. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/error-overlay/meta.json +0 -0
  63. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  64. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/orm/meta.json +0 -0
  65. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  66. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  67. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/queue/meta.json +0 -0
  68. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  69. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/rest-api/meta.json +0 -0
  70. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  71. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/templates/meta.json +0 -0
  72. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  73. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  74. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/graph/__init__.py +0 -0
  75. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/graph/adapter.py +0 -0
  76. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/graph/adapters/__init__.py +0 -0
  77. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/graph/adapters/arango.py +0 -0
  78. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/graph/adapters/bolt.py +0 -0
  79. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/graph/adapters/ultipa.py +0 -0
  80. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/graph/graph_url.py +0 -0
  81. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/graphql/__init__.py +0 -0
  82. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/i18n/__init__.py +0 -0
  83. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/mcp/__init__.py +0 -0
  84. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/mcp/protocol.py +0 -0
  85. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/mcp/tools.py +0 -0
  86. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/messenger/__init__.py +0 -0
  87. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/migration/__init__.py +0 -0
  88. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/migration/runner.py +0 -0
  89. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/mqtt/__init__.py +0 -0
  90. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/mqtt/message.py +0 -0
  91. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/orm/__init__.py +0 -0
  92. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/orm/fields.py +0 -0
  93. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/orm/model.py +0 -0
  94. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/orm/point.py +0 -0
  95. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/__feedback/widget.js +0 -0
  96. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/css/tina4.css +0 -0
  97. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/css/tina4.min.css +0 -0
  98. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/favicon.ico +0 -0
  99. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/images/logo.svg +0 -0
  100. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  101. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/js/frond.js +0 -0
  102. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/js/frond.min.js +0 -0
  103. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  104. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/js/tina4.min.js +0 -0
  105. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/js/tina4js.min.js +0 -0
  106. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/swagger/index.html +0 -0
  107. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  108. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/query_builder/__init__.py +0 -0
  109. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue/__init__.py +0 -0
  110. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue/amqp_url.py +0 -0
  111. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue/job.py +0 -0
  112. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue/kafka_backend.py +0 -0
  113. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue/lite_backend.py +0 -0
  114. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue/mongo_backend.py +0 -0
  115. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue/rabbitmq_backend.py +0 -0
  116. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue_backends/__init__.py +0 -0
  117. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue_backends/kafka_backend.py +0 -0
  118. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue_backends/mongo_backend.py +0 -0
  119. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  120. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/realtime/__init__.py +0 -0
  121. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/realtime/models/Attachment.py +0 -0
  122. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/realtime/models/Channel.py +0 -0
  123. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/realtime/models/ChannelMember.py +0 -0
  124. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/realtime/models/Message.py +0 -0
  125. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/realtime/models/Workspace.py +0 -0
  126. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/realtime/models/__init__.py +0 -0
  127. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/realtime/storage.py +0 -0
  128. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/seeder/__init__.py +0 -0
  129. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/service/__init__.py +0 -0
  130. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/session/__init__.py +0 -0
  131. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/session_handlers/memcached_handler.py +0 -0
  133. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  134. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/session_handlers/redis_handler.py +0 -0
  135. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/session_handlers/valkey_handler.py +0 -0
  136. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/sso.py +0 -0
  137. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/swagger/__init__.py +0 -0
  138. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/components/crud.twig +0 -0
  139. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  140. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  141. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/docker/python/Dockerfile +0 -0
  142. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  143. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/errors/302.twig +0 -0
  144. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/errors/401.twig +0 -0
  145. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/errors/403.twig +0 -0
  146. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/errors/404.twig +0 -0
  147. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/errors/500.twig +0 -0
  148. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/errors/502.twig +0 -0
  149. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/errors/503.twig +0 -0
  150. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/errors/base.twig +0 -0
  151. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/frontend/README.md +0 -0
  152. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/templates/readme.md +0 -0
  153. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/test/__init__.py +0 -0
  154. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/test_client/__init__.py +0 -0
  155. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  166. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  167. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/validator/__init__.py +0 -0
  168. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/websocket/__init__.py +0 -0
  169. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/websocket/backplane.py +0 -0
  170. {tina4_python-3.13.127 → tina4_python-3.13.129}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tina4-python
3
- Version: 3.13.127
3
+ Version: 3.13.129
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.127"
3
+ version = "3.13.129"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -105,7 +105,6 @@ dev = [
105
105
  "pytest-asyncio>=0.23",
106
106
  "pytest-cov>=5.0",
107
107
  "reportlab>=4.4.10",
108
- "ruff>=0.4",
109
108
  "setuptools>=82.0.1",
110
109
  "uvicorn>=0.42.0",
111
110
  ]
@@ -51,7 +51,7 @@ def _resolve_version() -> str:
51
51
  #
52
52
  # test_version_constant.py now asserts this literal equals the pyproject
53
53
  # version, so the release bump cannot leave it behind again.
54
- return "3.13.125"
54
+ return "3.13.129"
55
55
 
56
56
 
57
57
  __version__ = _resolve_version()
@@ -95,6 +95,46 @@ def _to_table(name: str) -> str:
95
95
  return table
96
96
 
97
97
 
98
+ def _resolve_table(name: str, flags: dict | None = None, *, announce: bool = False) -> str:
99
+ """The table name a generator uses, honouring ``--table-name`` and speaking up
100
+ instead of renaming silently.
101
+
102
+ ``announce`` prints the note/warning; it is True only for ``_gen_model`` (where
103
+ the table is born). Composite generators (crud) let the model sub-call
104
+ announce, and generators that target an EXISTING table (route/seeder/form/view)
105
+ still honour ``--table-name`` but stay quiet so the note is not repeated.
106
+
107
+ * ``--table-name <name>`` wins verbatim. If that name is ITSELF a reserved
108
+ word, warn loudly: Tina4 interpolates table names UNQUOTED, so the ORM's
109
+ generated SQL will fail on it and quoting it in raw SQL + migrations is now
110
+ the developer's job (we do not silently quote -- identifier quoting is a
111
+ global storage invariant, not a local fix).
112
+ * Otherwise fall back to :func:`_to_table`. When that auto-pluralises a
113
+ reserved-word class name (``Order`` -> ``orders``), print a one-line note
114
+ naming the rename and the ``--table-name`` escape hatch, so the developer is
115
+ informed rather than surprised.
116
+
117
+ Called once per generate (``_gen_model`` threads the result into the migration
118
+ and test), so the note prints once.
119
+ """
120
+ flags = flags or {}
121
+ override = flags.get("table-name")
122
+ if override and not isinstance(override, bool):
123
+ if announce and _to_snake(override) in SQL_RESERVED_TABLE_NAMES:
124
+ print(f" ! table_name '{override}' is a SQL reserved word. Tina4 interpolates "
125
+ f"table names UNQUOTED, so the ORM's generated SQL will fail on it -- "
126
+ f"quote it yourself in raw SQL and migrations.")
127
+ return override
128
+ bare = _to_snake(name)
129
+ if bare in SQL_RESERVED_TABLE_NAMES:
130
+ table = _pluralize_table(bare)
131
+ if announce:
132
+ print(f" · '{bare}' is a SQL reserved word; using table_name '{table}' "
133
+ f"(Tina4 interpolates table names unquoted). Override with --table-name <name>.")
134
+ return table
135
+ return bare
136
+
137
+
98
138
  def _parse_fields(fields_str: str) -> list[tuple[str, str]]:
99
139
  """Parse 'name:string,price:float' → [('name','string'), ('price','float')]."""
100
140
  if not fields_str or not fields_str.strip():
@@ -129,7 +169,7 @@ def _parse_flags(args: list[str]) -> tuple[dict, list[str]]:
129
169
  """Parse --key value and --flag from args. Returns (flags, positional)."""
130
170
  # Boolean-only flags that never take a value argument
131
171
  boolean_flags = {"no-browser", "no-reload", "no-kill", "production", "managed", "all", "clear",
132
- "json", "public", "no-migration", "once", "dry-run", "quote"}
172
+ "json", "public", "no-migration", "once", "dry-run", "fix", "no-install"}
133
173
 
134
174
  flags = {}
135
175
  positional = []
@@ -927,6 +967,103 @@ def _test(args):
927
967
  sys.exit(1 if (inline_failed or pytest_code) else 0)
928
968
 
929
969
 
970
+ def _resolve_ruff(root: Path):
971
+ """Locate the PROJECT's own ruff and return its command prefix (a list), or
972
+ None. Only the project virtualenv is consulted (where ``uv add --dev ruff``
973
+ puts it), on POSIX and Windows layouts -- deliberately NOT a global ruff on
974
+ PATH: linting must be reproducible from the project's own pinned dev
975
+ dependency, and a global tool is not the project's choice. When the project
976
+ has none, the caller installs it dev-only rather than borrowing a stray
977
+ global one (which would also make a global install non-deterministic)."""
978
+ for candidate in (root / ".venv" / "bin" / "ruff", root / ".venv" / "Scripts" / "ruff.exe"):
979
+ if candidate.is_file():
980
+ return [str(candidate)]
981
+ return None
982
+
983
+
984
+ def _lint(args):
985
+ """Lint the project's source. The framework ships NO linter (so a Tina4 app
986
+ stays zero-dependency); ``tina4 lint`` uses the project's own ``ruff`` and
987
+ INSTALLS it as a DEV dependency on demand when it is absent. Layers:
988
+
989
+ * **ruff present** (on PATH or in the project ``.venv``): run it. ``--fix``
990
+ runs ``ruff check --fix`` (safe autofixes). ruff reports syntax too, so it
991
+ is the whole pass when present.
992
+ * **ruff absent:** silently ``uv add --dev ruff`` -- running ``tina4 lint`` is
993
+ the consent to add it -- then run it. It lands in the DEV group only, never
994
+ the app's runtime dependencies. ``--no-install`` skips this.
995
+ * **Baseline (zero dependency):** the built-in ``compile()`` syntax parse --
996
+ no execution, no ``.pyc``, no dependency. Used with ``--no-install`` or when
997
+ the install cannot run (no ``uv`` on PATH, offline).
998
+
999
+ Contract (identical across all four frameworks): exit 0 = clean, non-zero =
1000
+ findings; the summary names the tool that ran. Scope is the user's app
1001
+ (``src/`` + ``app.py``), mirroring how ``tina4 test`` runs the project's own
1002
+ tests -- not the framework's code.
1003
+
1004
+ tina4 lint # ruff (installed dev-only on demand), else baseline
1005
+ tina4 lint --fix # ruff --fix
1006
+ tina4 lint --no-install # ruff if already present, else the syntax baseline
1007
+ """
1008
+ flags, _ = _parse_flags(list(args))
1009
+ fix = bool(flags.get("fix"))
1010
+ no_install = bool(flags.get("no-install"))
1011
+ root = Path.cwd()
1012
+
1013
+ py_files = []
1014
+ src = root / "src"
1015
+ if src.is_dir():
1016
+ py_files.extend(sorted(src.rglob("*.py")))
1017
+ if (root / "app.py").is_file():
1018
+ py_files.append(root / "app.py")
1019
+ if not py_files:
1020
+ print(" lint: nothing to lint (no src/ or app.py).")
1021
+ sys.exit(0)
1022
+
1023
+ ruff = _resolve_ruff(root)
1024
+
1025
+ # Silent on-demand install: running `tina4 lint` is the consent to add ruff
1026
+ # as a DEV dependency of the project. --no-install opts out (CI / offline) and
1027
+ # falls through to the zero-dependency baseline. Needs `uv` on PATH.
1028
+ if not ruff and not no_install and shutil.which("uv"):
1029
+ print(" · ruff not found -- adding it as a dev dependency (uv add --dev ruff)...")
1030
+ rc = subprocess.run(["uv", "add", "--dev", "ruff"]).returncode
1031
+ if rc == 0:
1032
+ ruff = _resolve_ruff(root)
1033
+ else:
1034
+ print(" · could not install ruff -- using the zero-dependency syntax baseline.")
1035
+
1036
+ if ruff:
1037
+ cmd = ruff + ["check"] + (["--fix"] if fix else []) + [str(p) for p in py_files]
1038
+ code = subprocess.run(cmd).returncode
1039
+ label = "ruff --fix" if fix else "ruff"
1040
+ if code != 0:
1041
+ print(f" ✗ lint failed -- {len(py_files)} file(s) [{label}]")
1042
+ sys.exit(1)
1043
+ print(f" ✓ lint clean -- {len(py_files)} file(s) [{label}]")
1044
+ sys.exit(0)
1045
+
1046
+ # Zero-dependency baseline: built-in compile() syntax parse. No execution,
1047
+ # no bytecode written, no dependency added.
1048
+ if fix:
1049
+ print(" · syntax baseline has no autofix (--no-install, or ruff unavailable).")
1050
+ syntax_errors = 0
1051
+ for pf in py_files:
1052
+ try:
1053
+ compile(pf.read_text(encoding="utf-8", errors="ignore"), str(pf), "exec")
1054
+ except SyntaxError as exc:
1055
+ print(f" ✗ {pf.relative_to(root)}:{exc.lineno}: {exc.msg}")
1056
+ syntax_errors += 1
1057
+ except OSError as exc:
1058
+ print(f" ✗ {pf.relative_to(root)}: cannot read ({exc})")
1059
+ syntax_errors += 1
1060
+ if syntax_errors:
1061
+ print(f" ✗ lint failed -- {syntax_errors} syntax error(s) in {len(py_files)} file(s) [compile]")
1062
+ sys.exit(1)
1063
+ print(f" ✓ lint clean -- {len(py_files)} file(s) [compile, syntax only]")
1064
+ sys.exit(0)
1065
+
1066
+
930
1067
  def _build(args):
931
1068
  """Build the deployable Docker image for this Tina4 app.
932
1069
 
@@ -1176,7 +1313,7 @@ def _resolve_generation(target: str, name: str, flags: dict,
1176
1313
 
1177
1314
  if target == "model":
1178
1315
  raw_table = _to_snake(name)
1179
- table = _to_table(name)
1316
+ table = _resolve_table(name, flags)
1180
1317
  transformations = []
1181
1318
  if raw_table in SQL_RESERVED_TABLE_NAMES and raw_table != table:
1182
1319
  transformations.append({
@@ -1185,8 +1322,8 @@ def _resolve_generation(target: str, name: str, flags: dict,
1185
1322
  "to": table,
1186
1323
  "reason": f"SQL reserved word '{raw_table}' would break CREATE TABLE",
1187
1324
  "override": (
1188
- f"--table {raw_table} --quote (requires quoted-identifier "
1189
- "mode, not yet implemented)"
1325
+ "--table-name <name> (table names interpolate unquoted; "
1326
+ "forcing a reserved name is yours to quote in raw SQL)"
1190
1327
  ),
1191
1328
  })
1192
1329
  migration_filename = f"{ts}_create_{table}.sql"
@@ -1340,11 +1477,11 @@ def _format_resolution_block(name: str, target: str, resolution: dict,
1340
1477
 
1341
1478
  if pluralize:
1342
1479
  lines.append("")
1343
- lines.append(f" To keep the raw name '{pluralize['from']}' as the table:")
1480
+ lines.append(" To set the table name yourself:")
1481
+ lines.append(f" tina4 generate {target} {name} --table-name <name>")
1344
1482
  lines.append(
1345
- f" tina4 generate {target} {name} "
1346
- f"--table {pluralize['from']} --quote "
1347
- "(opt-in, ADR-0062 forthcoming)"
1483
+ f" Tina4 interpolates table names unquoted; if you force the "
1484
+ f"reserved '{pluralize['from']}', you own the quoting in raw SQL."
1348
1485
  )
1349
1486
 
1350
1487
  # ADR-0063 additive sections. Each renders only when there is content, so
@@ -1540,7 +1677,7 @@ def _gen_model(name: str, flags: dict, *, emit_test: bool = True):
1540
1677
  and emit their own broader test instead.
1541
1678
  """
1542
1679
  fields = _fields_or_default(flags.get("fields", ""))
1543
- table = _to_table(name)
1680
+ table = _resolve_table(name, flags, announce=True)
1544
1681
 
1545
1682
  # Determine which ORM field types we need to import
1546
1683
  used_types = {"IntegerField"} # always need for id
@@ -1822,7 +1959,7 @@ def _gen_crud(name: str, flags: dict):
1822
1959
  tina4python generate crud Product --fields "name:string,price:float"
1823
1960
  """
1824
1961
  fields = _fields_or_default(flags.get("fields", ""))
1825
- table = _to_table(name)
1962
+ table = _resolve_table(name, flags)
1826
1963
  route_name = table + "s" # routes are plural
1827
1964
 
1828
1965
  print(f"\n Generating CRUD for {name}...\n")
@@ -2920,7 +3057,7 @@ def _gen_seeder(name: str, flags: dict = None):
2920
3057
 
2921
3058
  tina4python generate seeder Product
2922
3059
  """
2923
- table = _to_table(name)
3060
+ table = _resolve_table(name, flags)
2924
3061
  target = Path("src/seeds")
2925
3062
  target.mkdir(parents=True, exist_ok=True)
2926
3063
  path = target / f"{table}_seeder.py"
@@ -3096,7 +3233,7 @@ def _gen_form(name: str, flags: dict = None):
3096
3233
  """
3097
3234
  flags = flags or {}
3098
3235
  fields = _fields_or_default(flags.get("fields", ""))
3099
- table = _to_table(name)
3236
+ table = _resolve_table(name, flags)
3100
3237
  route_name = table + "s"
3101
3238
 
3102
3239
  # Input type mapping
@@ -3180,7 +3317,7 @@ def _gen_view(name: str, flags: dict = None):
3180
3317
  """
3181
3318
  flags = flags or {}
3182
3319
  fields = _fields_or_default(flags.get("fields", ""))
3183
- table = _to_table(name)
3320
+ table = _resolve_table(name, flags)
3184
3321
  route_name = table + "s"
3185
3322
 
3186
3323
  target = Path("src/templates/pages")
@@ -3725,7 +3862,7 @@ def _commands(args=None):
3725
3862
  # "subcommands"?: [str]} # sub-names for the manifest (generate)
3726
3863
 
3727
3864
  GENERATORS = {
3728
- "model": {"handler": _gen_model, "usage": '<Name> [--fields "name:string,price:float"]', "summary": "ORM model + matching migration"},
3865
+ "model": {"handler": _gen_model, "usage": '<Name> [--fields "name:string,price:float"] [--table-name <name>]', "summary": "ORM model + matching migration"},
3729
3866
  "route": {"handler": _gen_route, "usage": "<name> [--model Name] [--public]", "summary": "CRUD route file, secure by default (--public opens writes)"},
3730
3867
  "crud": {"handler": _gen_crud, "usage": '<Name> [--fields "..."] [--public]', "summary": "Model + migration + routes + form + view + test"},
3731
3868
  "migration": {"handler": _gen_migration, "usage": "<description>", "summary": "Timestamped migration file (UP/DOWN)"},
@@ -3756,6 +3893,7 @@ COMMANDS = {
3756
3893
  "test": {"handler": _test, "summary": "Run test suite"},
3757
3894
  "queue": {"handler": _queue, "usage": "<work|stats|retry|clear> [topic]", "subcommands": list(_QUEUE_SUBCOMMANDS), "summary": "Run queue workers and manage jobs"},
3758
3895
  "build": {"handler": _build, "usage": "[--tag NAME] [--file PATH]", "summary": "Build the deployable Docker image"},
3896
+ "lint": {"handler": _lint, "usage": "[--fix] [--no-install]", "summary": "Lint src/ + app.py (installs ruff dev-only on demand; zero-dep syntax baseline with --no-install)"},
3759
3897
  "ai": {"handler": _ai, "usage": "[--all]", "summary": "Install AI coding assistant context"},
3760
3898
  "generate": {"handler": _generate, "usage": "<what> <name> [options]", "subcommands": list(GENERATORS), "summary": "Generate scaffolding (see Generators below)"},
3761
3899
  "console": {"handler": _console, "summary": "Start interactive REPL with framework loaded"},