tina4-python 3.13.126__tar.gz → 3.13.128__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.126 → tina4_python-3.13.128}/PKG-INFO +1 -1
  2. {tina4_python-3.13.126 → tina4_python-3.13.128}/pyproject.toml +1 -2
  3. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/cli/__init__.py +99 -1
  5. {tina4_python-3.13.126 → tina4_python-3.13.128}/.gitignore +0 -0
  6. {tina4_python-3.13.126 → tina4_python-3.13.128}/README.md +0 -0
  7. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/CLAUDE.md +0 -0
  8. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/HtmlElement.py +0 -0
  9. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/Testing.py +0 -0
  10. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/_import_helper.py +0 -0
  11. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/ai/__init__.py +0 -0
  12. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/ai/client.py +0 -0
  13. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/container/__init__.py +0 -0
  17. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/context/__init__.py +0 -0
  18. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/context/chunker.py +0 -0
  19. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/__init__.py +0 -0
  20. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/cache.py +0 -0
  21. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/constants.py +0 -0
  22. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/events.py +0 -0
  23. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/middleware.py +0 -0
  24. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/port_takeover.py +0 -0
  25. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/rate_limiter.py +0 -0
  26. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/request.py +0 -0
  27. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/response.py +0 -0
  28. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/router.py +0 -0
  29. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/core/server.py +0 -0
  30. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/crud/__init__.py +0 -0
  31. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/__init__.py +0 -0
  32. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/adapter.py +0 -0
  33. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/connection.py +0 -0
  34. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/database_url.py +0 -0
  35. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/firebird.py +0 -0
  36. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/mongodb.py +0 -0
  37. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/mssql.py +0 -0
  38. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/mysql.py +0 -0
  39. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/odbc.py +0 -0
  40. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/postgres.py +0 -0
  41. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/sql_translator.py +0 -0
  42. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/database/sqlite.py +0 -0
  43. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/debug/__init__.py +0 -0
  44. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/debug/error_overlay.py +0 -0
  45. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/dev_admin/__init__.py +0 -0
  46. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/dev_admin/metrics.py +0 -0
  47. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/dev_admin/plan.py +0 -0
  48. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/dev_admin/project_index.py +0 -0
  49. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/docs.py +0 -0
  50. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/docstore/__init__.py +0 -0
  51. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/dotenv/__init__.py +0 -0
  52. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/env.py +0 -0
  53. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/frond/FROND.md +0 -0
  54. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/frond/__init__.py +0 -0
  55. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/frond/compiler.py +0 -0
  56. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/frond/engine.py +0 -0
  57. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/frond/parser.py +0 -0
  58. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/auth/meta.json +0 -0
  59. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  60. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/database/meta.json +0 -0
  61. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  62. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/error-overlay/meta.json +0 -0
  63. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  64. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/orm/meta.json +0 -0
  65. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  66. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  67. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/queue/meta.json +0 -0
  68. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  69. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/rest-api/meta.json +0 -0
  70. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  71. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/templates/meta.json +0 -0
  72. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  73. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  74. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/graph/__init__.py +0 -0
  75. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/graph/adapter.py +0 -0
  76. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/graph/adapters/__init__.py +0 -0
  77. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/graph/adapters/arango.py +0 -0
  78. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/graph/adapters/bolt.py +0 -0
  79. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/graph/adapters/ultipa.py +0 -0
  80. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/graph/graph_url.py +0 -0
  81. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/graphql/__init__.py +0 -0
  82. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/i18n/__init__.py +0 -0
  83. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/mcp/__init__.py +0 -0
  84. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/mcp/protocol.py +0 -0
  85. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/mcp/tools.py +0 -0
  86. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/messenger/__init__.py +0 -0
  87. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/migration/__init__.py +0 -0
  88. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/migration/runner.py +0 -0
  89. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/mqtt/__init__.py +0 -0
  90. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/mqtt/message.py +0 -0
  91. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/orm/__init__.py +0 -0
  92. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/orm/fields.py +0 -0
  93. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/orm/model.py +0 -0
  94. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/orm/point.py +0 -0
  95. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/__feedback/widget.js +0 -0
  96. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/css/tina4.css +0 -0
  97. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/css/tina4.min.css +0 -0
  98. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/favicon.ico +0 -0
  99. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/images/logo.svg +0 -0
  100. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  101. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/js/frond.js +0 -0
  102. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/js/frond.min.js +0 -0
  103. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  104. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/js/tina4.min.js +0 -0
  105. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/js/tina4js.min.js +0 -0
  106. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/swagger/index.html +0 -0
  107. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  108. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/query_builder/__init__.py +0 -0
  109. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue/__init__.py +0 -0
  110. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue/amqp_url.py +0 -0
  111. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue/job.py +0 -0
  112. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue/kafka_backend.py +0 -0
  113. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue/lite_backend.py +0 -0
  114. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue/mongo_backend.py +0 -0
  115. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue/rabbitmq_backend.py +0 -0
  116. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue_backends/__init__.py +0 -0
  117. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue_backends/kafka_backend.py +0 -0
  118. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue_backends/mongo_backend.py +0 -0
  119. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  120. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/realtime/__init__.py +0 -0
  121. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/realtime/models/Attachment.py +0 -0
  122. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/realtime/models/Channel.py +0 -0
  123. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/realtime/models/ChannelMember.py +0 -0
  124. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/realtime/models/Message.py +0 -0
  125. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/realtime/models/Workspace.py +0 -0
  126. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/realtime/models/__init__.py +0 -0
  127. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/realtime/storage.py +0 -0
  128. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/seeder/__init__.py +0 -0
  129. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/service/__init__.py +0 -0
  130. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/session/__init__.py +0 -0
  131. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/session_handlers/memcached_handler.py +0 -0
  133. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  134. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/session_handlers/redis_handler.py +0 -0
  135. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/session_handlers/valkey_handler.py +0 -0
  136. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/sso.py +0 -0
  137. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/swagger/__init__.py +0 -0
  138. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/components/crud.twig +0 -0
  139. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  140. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  141. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/docker/python/Dockerfile +0 -0
  142. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  143. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/errors/302.twig +0 -0
  144. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/errors/401.twig +0 -0
  145. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/errors/403.twig +0 -0
  146. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/errors/404.twig +0 -0
  147. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/errors/500.twig +0 -0
  148. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/errors/502.twig +0 -0
  149. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/errors/503.twig +0 -0
  150. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/errors/base.twig +0 -0
  151. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/frontend/README.md +0 -0
  152. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/templates/readme.md +0 -0
  153. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/test/__init__.py +0 -0
  154. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/test_client/__init__.py +0 -0
  155. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  166. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  167. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/validator/__init__.py +0 -0
  168. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/websocket/__init__.py +0 -0
  169. {tina4_python-3.13.126 → tina4_python-3.13.128}/tina4_python/websocket/backplane.py +0 -0
  170. {tina4_python-3.13.126 → tina4_python-3.13.128}/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.126
3
+ Version: 3.13.128
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.126"
3
+ version = "3.13.128"
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.128"
55
55
 
56
56
 
57
57
  __version__ = _resolve_version()
@@ -129,7 +129,7 @@ def _parse_flags(args: list[str]) -> tuple[dict, list[str]]:
129
129
  """Parse --key value and --flag from args. Returns (flags, positional)."""
130
130
  # Boolean-only flags that never take a value argument
131
131
  boolean_flags = {"no-browser", "no-reload", "no-kill", "production", "managed", "all", "clear",
132
- "json", "public", "no-migration", "once", "dry-run", "quote"}
132
+ "json", "public", "no-migration", "once", "dry-run", "quote", "fix", "no-install"}
133
133
 
134
134
  flags = {}
135
135
  positional = []
@@ -927,6 +927,103 @@ def _test(args):
927
927
  sys.exit(1 if (inline_failed or pytest_code) else 0)
928
928
 
929
929
 
930
+ def _resolve_ruff(root: Path):
931
+ """Locate the PROJECT's own ruff and return its command prefix (a list), or
932
+ None. Only the project virtualenv is consulted (where ``uv add --dev ruff``
933
+ puts it), on POSIX and Windows layouts -- deliberately NOT a global ruff on
934
+ PATH: linting must be reproducible from the project's own pinned dev
935
+ dependency, and a global tool is not the project's choice. When the project
936
+ has none, the caller installs it dev-only rather than borrowing a stray
937
+ global one (which would also make a global install non-deterministic)."""
938
+ for candidate in (root / ".venv" / "bin" / "ruff", root / ".venv" / "Scripts" / "ruff.exe"):
939
+ if candidate.is_file():
940
+ return [str(candidate)]
941
+ return None
942
+
943
+
944
+ def _lint(args):
945
+ """Lint the project's source. The framework ships NO linter (so a Tina4 app
946
+ stays zero-dependency); ``tina4 lint`` uses the project's own ``ruff`` and
947
+ INSTALLS it as a DEV dependency on demand when it is absent. Layers:
948
+
949
+ * **ruff present** (on PATH or in the project ``.venv``): run it. ``--fix``
950
+ runs ``ruff check --fix`` (safe autofixes). ruff reports syntax too, so it
951
+ is the whole pass when present.
952
+ * **ruff absent:** silently ``uv add --dev ruff`` -- running ``tina4 lint`` is
953
+ the consent to add it -- then run it. It lands in the DEV group only, never
954
+ the app's runtime dependencies. ``--no-install`` skips this.
955
+ * **Baseline (zero dependency):** the built-in ``compile()`` syntax parse --
956
+ no execution, no ``.pyc``, no dependency. Used with ``--no-install`` or when
957
+ the install cannot run (no ``uv`` on PATH, offline).
958
+
959
+ Contract (identical across all four frameworks): exit 0 = clean, non-zero =
960
+ findings; the summary names the tool that ran. Scope is the user's app
961
+ (``src/`` + ``app.py``), mirroring how ``tina4 test`` runs the project's own
962
+ tests -- not the framework's code.
963
+
964
+ tina4 lint # ruff (installed dev-only on demand), else baseline
965
+ tina4 lint --fix # ruff --fix
966
+ tina4 lint --no-install # ruff if already present, else the syntax baseline
967
+ """
968
+ flags, _ = _parse_flags(list(args))
969
+ fix = bool(flags.get("fix"))
970
+ no_install = bool(flags.get("no-install"))
971
+ root = Path.cwd()
972
+
973
+ py_files = []
974
+ src = root / "src"
975
+ if src.is_dir():
976
+ py_files.extend(sorted(src.rglob("*.py")))
977
+ if (root / "app.py").is_file():
978
+ py_files.append(root / "app.py")
979
+ if not py_files:
980
+ print(" lint: nothing to lint (no src/ or app.py).")
981
+ sys.exit(0)
982
+
983
+ ruff = _resolve_ruff(root)
984
+
985
+ # Silent on-demand install: running `tina4 lint` is the consent to add ruff
986
+ # as a DEV dependency of the project. --no-install opts out (CI / offline) and
987
+ # falls through to the zero-dependency baseline. Needs `uv` on PATH.
988
+ if not ruff and not no_install and shutil.which("uv"):
989
+ print(" · ruff not found -- adding it as a dev dependency (uv add --dev ruff)...")
990
+ rc = subprocess.run(["uv", "add", "--dev", "ruff"]).returncode
991
+ if rc == 0:
992
+ ruff = _resolve_ruff(root)
993
+ else:
994
+ print(" · could not install ruff -- using the zero-dependency syntax baseline.")
995
+
996
+ if ruff:
997
+ cmd = ruff + ["check"] + (["--fix"] if fix else []) + [str(p) for p in py_files]
998
+ code = subprocess.run(cmd).returncode
999
+ label = "ruff --fix" if fix else "ruff"
1000
+ if code != 0:
1001
+ print(f" ✗ lint failed -- {len(py_files)} file(s) [{label}]")
1002
+ sys.exit(1)
1003
+ print(f" ✓ lint clean -- {len(py_files)} file(s) [{label}]")
1004
+ sys.exit(0)
1005
+
1006
+ # Zero-dependency baseline: built-in compile() syntax parse. No execution,
1007
+ # no bytecode written, no dependency added.
1008
+ if fix:
1009
+ print(" · syntax baseline has no autofix (--no-install, or ruff unavailable).")
1010
+ syntax_errors = 0
1011
+ for pf in py_files:
1012
+ try:
1013
+ compile(pf.read_text(encoding="utf-8", errors="ignore"), str(pf), "exec")
1014
+ except SyntaxError as exc:
1015
+ print(f" ✗ {pf.relative_to(root)}:{exc.lineno}: {exc.msg}")
1016
+ syntax_errors += 1
1017
+ except OSError as exc:
1018
+ print(f" ✗ {pf.relative_to(root)}: cannot read ({exc})")
1019
+ syntax_errors += 1
1020
+ if syntax_errors:
1021
+ print(f" ✗ lint failed -- {syntax_errors} syntax error(s) in {len(py_files)} file(s) [compile]")
1022
+ sys.exit(1)
1023
+ print(f" ✓ lint clean -- {len(py_files)} file(s) [compile, syntax only]")
1024
+ sys.exit(0)
1025
+
1026
+
930
1027
  def _build(args):
931
1028
  """Build the deployable Docker image for this Tina4 app.
932
1029
 
@@ -3756,6 +3853,7 @@ COMMANDS = {
3756
3853
  "test": {"handler": _test, "summary": "Run test suite"},
3757
3854
  "queue": {"handler": _queue, "usage": "<work|stats|retry|clear> [topic]", "subcommands": list(_QUEUE_SUBCOMMANDS), "summary": "Run queue workers and manage jobs"},
3758
3855
  "build": {"handler": _build, "usage": "[--tag NAME] [--file PATH]", "summary": "Build the deployable Docker image"},
3856
+ "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
3857
  "ai": {"handler": _ai, "usage": "[--all]", "summary": "Install AI coding assistant context"},
3760
3858
  "generate": {"handler": _generate, "usage": "<what> <name> [options]", "subcommands": list(GENERATORS), "summary": "Generate scaffolding (see Generators below)"},
3761
3859
  "console": {"handler": _console, "summary": "Start interactive REPL with framework loaded"},