tina4-python 3.13.97__tar.gz → 3.13.99__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 (177) hide show
  1. {tina4_python-3.13.97 → tina4_python-3.13.99}/PKG-INFO +2 -2
  2. {tina4_python-3.13.97 → tina4_python-3.13.99}/pyproject.toml +1 -1
  3. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/CLAUDE.md +17 -19
  4. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/Testing.py +21 -12
  5. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/__init__.py +7 -3
  6. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/api/__init__.py +14 -1
  7. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/auth/__init__.py +5 -1
  8. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/cli/__init__.py +100 -85
  9. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/middleware.py +157 -29
  10. tina4_python-3.13.99/tina4_python/core/port_takeover.py +309 -0
  11. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/request.py +200 -15
  12. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/response.py +7 -2
  13. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/router.py +23 -2
  14. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/server.py +394 -165
  15. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/crud/__init__.py +100 -17
  16. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/adapter.py +192 -68
  17. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/connection.py +71 -22
  18. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/firebird.py +72 -15
  19. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/mongodb.py +71 -15
  20. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/mssql.py +61 -11
  21. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/mysql.py +94 -18
  22. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/odbc.py +72 -14
  23. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/postgres.py +2 -2
  24. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/sql_translator.py +130 -19
  25. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/sqlite.py +55 -9
  26. tina4_python-3.13.99/tina4_python/debug/__init__.py +902 -0
  27. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/debug/error_overlay.py +60 -34
  28. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/dev_admin/__init__.py +141 -25
  29. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/frond/engine.py +23 -3
  30. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +1 -1
  31. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/mcp/__init__.py +10 -1
  32. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/messenger/__init__.py +25 -0
  33. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/migration/runner.py +26 -0
  34. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/orm/__init__.py +2 -2
  35. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/orm/fields.py +223 -54
  36. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/orm/model.py +266 -70
  37. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/js/tina4-dev-admin.min.js +23 -19
  38. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/__init__.py +2 -2
  39. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/seeder/__init__.py +46 -6
  40. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/swagger/__init__.py +46 -8
  41. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/test_client/__init__.py +21 -1
  42. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/validator/__init__.py +7 -1
  43. tina4_python-3.13.97/tina4_python/debug/__init__.py +0 -689
  44. tina4_python-3.13.97/tina4_python/scss/tina4css/_alerts.scss +0 -34
  45. tina4_python-3.13.97/tina4_python/scss/tina4css/_badges.scss +0 -22
  46. tina4_python-3.13.97/tina4_python/scss/tina4css/_buttons.scss +0 -69
  47. tina4_python-3.13.97/tina4_python/scss/tina4css/_cards.scss +0 -49
  48. tina4_python-3.13.97/tina4_python/scss/tina4css/_forms.scss +0 -156
  49. tina4_python-3.13.97/tina4_python/scss/tina4css/_grid.scss +0 -81
  50. tina4_python-3.13.97/tina4_python/scss/tina4css/_modals.scss +0 -84
  51. tina4_python-3.13.97/tina4_python/scss/tina4css/_nav.scss +0 -149
  52. tina4_python-3.13.97/tina4_python/scss/tina4css/_pagination.scss +0 -63
  53. tina4_python-3.13.97/tina4_python/scss/tina4css/_reset.scss +0 -94
  54. tina4_python-3.13.97/tina4_python/scss/tina4css/_tables.scss +0 -54
  55. tina4_python-3.13.97/tina4_python/scss/tina4css/_typography.scss +0 -55
  56. tina4_python-3.13.97/tina4_python/scss/tina4css/_utilities.scss +0 -208
  57. tina4_python-3.13.97/tina4_python/scss/tina4css/_variables.scss +0 -117
  58. tina4_python-3.13.97/tina4_python/scss/tina4css/base.scss +0 -1
  59. tina4_python-3.13.97/tina4_python/scss/tina4css/colors.scss +0 -48
  60. tina4_python-3.13.97/tina4_python/scss/tina4css/tina4.scss +0 -18
  61. {tina4_python-3.13.97 → tina4_python-3.13.99}/.gitignore +0 -0
  62. {tina4_python-3.13.97 → tina4_python-3.13.99}/README.md +0 -0
  63. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/HtmlElement.py +0 -0
  64. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/ai/__init__.py +0 -0
  65. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/cache/__init__.py +0 -0
  66. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/container/__init__.py +0 -0
  67. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/context/__init__.py +0 -0
  68. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/context/chunker.py +0 -0
  69. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/__init__.py +0 -0
  70. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/cache.py +0 -0
  71. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/constants.py +0 -0
  72. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/events.py +0 -0
  73. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/core/rate_limiter.py +0 -0
  74. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/__init__.py +0 -0
  75. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/database/database_url.py +0 -0
  76. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/dev_admin/metrics.py +0 -0
  77. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/dev_admin/plan.py +0 -0
  78. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/dev_admin/project_index.py +0 -0
  79. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/docs.py +0 -0
  80. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/docstore/__init__.py +0 -0
  81. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/dotenv/__init__.py +0 -0
  82. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/env.py +0 -0
  83. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/frond/FROND.md +0 -0
  84. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/frond/__init__.py +0 -0
  85. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/frond/compiler.py +0 -0
  86. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/frond/parser.py +0 -0
  87. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/auth/meta.json +0 -0
  88. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/database/meta.json +0 -0
  89. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  90. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/error-overlay/meta.json +0 -0
  91. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  92. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/orm/meta.json +0 -0
  93. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  94. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  95. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/queue/meta.json +0 -0
  96. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  97. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/rest-api/meta.json +0 -0
  98. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  99. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/templates/meta.json +0 -0
  100. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  101. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  102. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/graphql/__init__.py +0 -0
  103. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/i18n/__init__.py +0 -0
  104. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/mcp/protocol.py +0 -0
  105. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/mcp/tools.py +0 -0
  106. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/migration/__init__.py +0 -0
  107. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/mqtt/__init__.py +0 -0
  108. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/mqtt/message.py +0 -0
  109. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/__feedback/widget.js +0 -0
  110. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/css/tina4.css +0 -0
  111. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/css/tina4.min.css +0 -0
  112. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/favicon.ico +0 -0
  113. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/images/logo.svg +0 -0
  114. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  115. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/js/frond.js +0 -0
  116. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/js/frond.min.js +0 -0
  117. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/js/tina4.min.js +0 -0
  118. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/js/tina4js.min.js +0 -0
  119. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/swagger/index.html +0 -0
  120. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  121. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/query_builder/__init__.py +0 -0
  122. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/__init__.py +0 -0
  123. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/amqp_url.py +0 -0
  124. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/job.py +0 -0
  125. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/kafka_backend.py +0 -0
  126. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/lite_backend.py +0 -0
  127. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/mongo_backend.py +0 -0
  128. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue/rabbitmq_backend.py +0 -0
  129. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue_backends/__init__.py +0 -0
  130. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue_backends/kafka_backend.py +0 -0
  131. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue_backends/mongo_backend.py +0 -0
  132. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  133. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/Attachment.py +0 -0
  134. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/Channel.py +0 -0
  135. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/ChannelMember.py +0 -0
  136. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/Message.py +0 -0
  137. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/Workspace.py +0 -0
  138. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/models/__init__.py +0 -0
  139. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/realtime/storage.py +0 -0
  140. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/service/__init__.py +0 -0
  141. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session/__init__.py +0 -0
  142. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session_handlers/__init__.py +0 -0
  143. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session_handlers/memcached_handler.py +0 -0
  144. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  145. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session_handlers/redis_handler.py +0 -0
  146. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/session_handlers/valkey_handler.py +0 -0
  147. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/components/crud.twig +0 -0
  148. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  149. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  150. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/docker/python/Dockerfile +0 -0
  151. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  152. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/302.twig +0 -0
  153. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/401.twig +0 -0
  154. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/403.twig +0 -0
  155. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/404.twig +0 -0
  156. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/500.twig +0 -0
  157. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/502.twig +0 -0
  158. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/503.twig +0 -0
  159. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/errors/base.twig +0 -0
  160. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/frontend/README.md +0 -0
  161. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/templates/readme.md +0 -0
  162. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/test/__init__.py +0 -0
  163. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  166. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  167. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  168. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  169. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  170. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  171. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  172. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  173. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  174. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  175. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/websocket/__init__.py +0 -0
  176. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/websocket/backplane.py +0 -0
  177. {tina4_python-3.13.97 → tina4_python-3.13.99}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: tina4-python
3
- Version: 3.13.97
3
+ Version: 3.13.99
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.97"
3
+ version = "3.13.99"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -238,7 +238,7 @@ Tina4 provides a full toolkit. Before writing custom code, check if the framewor
238
238
  | Static file serving | Put files in `src/public/` — auto-served |
239
239
  | Translations / i18n | `I18n` from `tina4_python.i18n` |
240
240
  | HTML generation in code | `HTMLElement` from `tina4_python.HtmlElement` |
241
- | Inline testing | `@tests`, `assert_equal`, `assert_raises` from `tina4_python.Testing` |
241
+ | Inline testing | `@tests`, `expect_equal`, `expect_raises` from `tina4_python.Testing` |
242
242
  | Event system | `on`, `emit`, `once`, `off` from `tina4_python.core.events` |
243
243
  | AI assistant context | `detect_ai`, `install_context` from `tina4_python.ai` |
244
244
  | Response caching | `ResponseCache`, `cache_stats`, `clear_cache` from `tina4_python.cache` |
@@ -357,7 +357,7 @@ tina4_python/ # Core framework package (v3.0.0)
357
357
  ├── dotenv/ # .env file loader
358
358
  ├── cli/ # CLI commands
359
359
  ├── HtmlElement.py # Programmatic HTML builder (HTMLElement, add_html_helpers)
360
- ├── Testing.py # Inline testing framework (tests, assert_equal, run_all)
360
+ ├── Testing.py # Inline testing framework (tests, expect_equal, run_all)
361
361
  ├── templates/ # Built-in framework templates (Twig)
362
362
  ├── public/ # Built-in static assets
363
363
  └── scss/ # Built-in SCSS
@@ -973,7 +973,7 @@ uv run tina4python migrate
973
973
  - Files are executed in **numeric-prefix order** (`9_` before `10_`) and split on the `;` delimiter. A file without a numeric/timestamp prefix logs a warning — its order is undefined
974
974
  - State is tracked in the `tina4_migration` table (auto-created per engine, canonical columns `id, migration_name VARCHAR(500) NOT NULL UNIQUE, description VARCHAR(500), batch INTEGER NOT NULL DEFAULT 1, executed_at VARCHAR(50) NOT NULL, passed INTEGER NOT NULL DEFAULT 1` — identical across all four frameworks). A migration is **applied** when a row exists for it with `passed = 1` (the applied-read is `WHERE passed = 1`). `migrate()` writes **only `passed = 1` rows**: on a failure the file's transaction is rolled back and **no row is written** for it (it is NOT recorded as `passed=0`), and `migrate()` raises. The public `record_migration(name, batch, passed=...)` API can write a `passed=0` row; any `passed=0` row (including one carried over from a v2 table) is treated as **not applied**, so it is reported pending. A leftover `passed=0` row (a prior failure or a v2 carry-over) **re-applies cleanly**: on the next `migrate()` the success path deletes any existing row for that `migration_name` before writing the fresh `passed=1` row, so the migration runs again instead of colliding on the unique `migration_name` — the bookkeeping table holds at most one row per migration with its latest state. The v2->v3 upgrade logs a warning naming any `passed=0` rows so you know which ones will re-run.
975
975
  - **Each migration FILE is wrapped in its own transaction**: on a failure the file rolls back and `migrate()` **raises** (it does not write `passed=0`, delete anything, or `sys.exit`). Already-applied files stay applied — fix the bad file and re-run. The explicit `tina4 migrate` CLI surfaces the raise as a non-zero exit; startup auto-migration logs it and the service still boots (see TINA4_AUTO_MIGRATE above).
976
- - **Atomicity caveat:** per-file transactions are truly atomic only on engines with **transactional DDL (PostgreSQL)**. MySQL, Firebird, and SQLite auto-commit DDL, so a multi-statement migration that fails midway on those engines leaves earlier statements applied — keep one logical change per file. CREATE TABLE / ALTER-ADD are made idempotent on Firebird/MSSQL (existence-checked) so a re-run doesn't error.
976
+ - **Atomicity caveat:** per-file transactions are truly atomic on engines with **transactional DDL (PostgreSQL, and SQLite)**. SQLite's DDL is transactional too (autocommit is off inside `start_transaction()`), so a multi-statement migration that fails midway on SQLite rolls back cleanly, including any `CREATE TABLE` that already ran in the same file — proven by `tests/test_migration.py::TestMigratePositive::test_partial_failure_rolls_back`. MySQL and Firebird **auto-commit DDL**, so the same failure on those two engines leaves earlier statements applied — keep one logical change per file there. CREATE TABLE / ALTER-ADD are made idempotent on Firebird/MSSQL (existence-checked) so a re-run doesn't error.
977
977
 
978
978
  ### Engine-specific DDL patterns
979
979
 
@@ -1369,11 +1369,11 @@ Supports all HTML tags, void tags (`<br>`, `<img>`, `<input>`, etc.), and auto-e
1369
1369
  Tina4 includes a decorator-based test framework for inline test cases:
1370
1370
 
1371
1371
  ```python
1372
- from tina4_python.Testing import tests, assert_equal, assert_raises
1372
+ from tina4_python.Testing import tests, expect_equal, expect_raises
1373
1373
 
1374
1374
  @tests(
1375
- assert_equal((5, 3), 8),
1376
- assert_raises(ValueError, (None,))
1375
+ expect_equal((5, 3), 8),
1376
+ expect_raises(ValueError, (None,))
1377
1377
  )
1378
1378
  def add(a, b=None):
1379
1379
  if b is None:
@@ -1383,7 +1383,7 @@ def add(a, b=None):
1383
1383
 
1384
1384
  Run all decorated tests:
1385
1385
  ```bash
1386
- uv run tina4python test # Discovers @tests in src/**/*.py
1386
+ uv run tina4python test # Discovers @tests in src/**/*.py, real exit code
1387
1387
  ```
1388
1388
 
1389
1389
  Or programmatically:
@@ -1542,16 +1542,14 @@ container.get("unknown") # KeyError: service not registered: unknown
1542
1542
  Rich, syntax-highlighted HTML error overlay for development mode. Shows stack traces with source code context, request details, and environment info.
1543
1543
 
1544
1544
  ```python
1545
- from tina4_python.debug.error_overlay import render_error_overlay, render_production_error, is_debug_mode
1545
+ from tina4_python.debug.error_overlay import render_error_overlay, is_debug_mode
1546
1546
 
1547
- # Check if overlay should be shown
1547
+ # Dev only — the caller gates the overlay on is_debug_mode()
1548
1548
  if is_debug_mode(): # True when TINA4_DEBUG is true
1549
- html = render_error_overlay(exception, request=request)
1550
- else:
1551
- html = render_production_error(status_code=500, message="Internal Server Error")
1549
+ html = render_error_overlay(exception, request)
1552
1550
  ```
1553
1551
 
1554
- The error overlay is automatically activated when `TINA4_DEBUG=true`. In production, `render_production_error()` returns a safe, generic error page with no stack traces or source code.
1552
+ The error overlay is dev-only (gated on `is_debug_mode()`/`TINA4_DEBUG`). The production 500 is NOT rendered here — the server dispatch renders `errors/500.twig` with an empty `error_message` (CWE-209), so the exception detail stays in the server log only. Sensitive request fields (Authorization / Cookie / Set-Cookie headers and password-like body/param keys) are redacted even in the overlay, the frame count is capped, and the caller guards the render.
1555
1553
 
1556
1554
  ## HtmlElement — Programmatic HTML Builder
1557
1555
 
@@ -1587,14 +1585,14 @@ HTMLElement("img", {"src": "/logo.png"}) # <img src="/logo.png">
1587
1585
  Attach test assertions directly to functions with the `@tests` decorator. Tests are registered globally and run via CLI or programmatically.
1588
1586
 
1589
1587
  ```python
1590
- from tina4_python.Testing import tests, assert_equal, assert_raises, assert_true, assert_false, run_all
1588
+ from tina4_python.Testing import tests, expect_equal, expect_raises, expect_true, expect_false, run_all
1591
1589
 
1592
1590
  @tests(
1593
- assert_equal((5, 3), 8), # add(5, 3) == 8
1594
- assert_equal((0, 0), 0), # add(0, 0) == 0
1595
- assert_raises(ValueError, (None,)), # add(None) raises ValueError
1596
- assert_true((1, 1)), # add(1, 1) is truthy
1597
- assert_false((0, 0)), # add(0, 0) is falsy
1591
+ expect_equal((5, 3), 8), # add(5, 3) == 8
1592
+ expect_equal((0, 0), 0), # add(0, 0) == 0
1593
+ expect_raises(ValueError, (None,)), # add(None) raises ValueError
1594
+ expect_true((1, 1)), # add(1, 1) is truthy
1595
+ expect_false((0, 0)), # add(0, 0) is falsy
1598
1596
  )
1599
1597
  def add(a, b=None):
1600
1598
  if b is None:
@@ -3,14 +3,14 @@
3
3
  # License: MIT https://opensource.org/licenses/MIT
4
4
  """
5
5
  Inline testing framework — decorate functions with test assertions
6
- that run alongside the code.
6
+ that run alongside the code, and run them with ``tina4 test``.
7
7
 
8
8
  Usage:
9
- from tina4_python.Testing import tests, assert_equal, assert_raises
9
+ from tina4_python.Testing import tests, expect_equal, expect_raises
10
10
 
11
11
  @tests(
12
- assert_equal((5, 3), 8),
13
- assert_raises(ValueError, (None,))
12
+ expect_equal((5, 3), 8),
13
+ expect_raises(ValueError, (None,))
14
14
  )
15
15
  def add(a, b=None):
16
16
  if b is None:
@@ -20,6 +20,15 @@ Usage:
20
20
  Run all tests:
21
21
  from tina4_python.Testing import run_all
22
22
  run_all()
23
+
24
+ The builders are named ``expect_*`` — deliberately distinct from the xUnit
25
+ ``assert_*`` assertions in ``tina4_python.test`` (which take positional
26
+ ``(actual, expected, message)`` and assert immediately). ``expect_*`` are
27
+ DESCRIPTORS: ``expect_equal((args,), expected)`` records "call the decorated
28
+ function with ``args`` and check the result equals ``expected``" for the
29
+ runner to execute later. Keeping the two surfaces' names apart means importing
30
+ the wrong one is a loud ``ImportError``/``AttributeError``, never a silent
31
+ change of call semantics.
23
32
  """
24
33
 
25
34
  import sys
@@ -32,23 +41,23 @@ _registry: list[dict] = []
32
41
 
33
42
  # ── Assertion builders ──────────────────────────────────────────────
34
43
 
35
- def assert_equal(args: tuple, expected):
36
- """Assert that calling the decorated function with *args* returns *expected*."""
44
+ def expect_equal(args: tuple, expected):
45
+ """Expect that calling the decorated function with *args* returns *expected*."""
37
46
  return {"type": "equal", "args": args, "expected": expected}
38
47
 
39
48
 
40
- def assert_raises(exception_class: type, args: tuple):
41
- """Assert that calling the decorated function with *args* raises *exception_class*."""
49
+ def expect_raises(exception_class: type, args: tuple):
50
+ """Expect that calling the decorated function with *args* raises *exception_class*."""
42
51
  return {"type": "raises", "exception": exception_class, "args": args}
43
52
 
44
53
 
45
- def assert_true(args: tuple):
46
- """Assert that calling the decorated function with *args* returns a truthy value."""
54
+ def expect_true(args: tuple):
55
+ """Expect that calling the decorated function with *args* returns a truthy value."""
47
56
  return {"type": "true", "args": args}
48
57
 
49
58
 
50
- def assert_false(args: tuple):
51
- """Assert that calling the decorated function with *args* returns a falsy value."""
59
+ def expect_false(args: tuple):
60
+ """Expect that calling the decorated function with *args* returns a falsy value."""
52
61
  return {"type": "false", "args": args}
53
62
 
54
63
 
@@ -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.97"
54
+ return "3.13.99"
55
55
 
56
56
 
57
57
  __version__ = _resolve_version()
@@ -106,6 +106,7 @@ _LAZY: dict[str, tuple[str, str]] = {
106
106
  "TextField": ("tina4_python.orm", "TextField"),
107
107
  "BlobField": ("tina4_python.orm", "BlobField"),
108
108
  "NumericField": ("tina4_python.orm", "NumericField"),
109
+ "DecimalField": ("tina4_python.orm", "DecimalField"),
109
110
  "JSONField": ("tina4_python.orm", "JSONField"),
110
111
  "ForeignKeyField": ("tina4_python.orm", "ForeignKeyField"),
111
112
  "IntField": ("tina4_python.orm", "IntField"),
@@ -148,9 +149,12 @@ _LAZY: dict[str, tuple[str, str]] = {
148
149
  "StorageBackend": ("tina4_python.realtime.storage", "StorageBackend"),
149
150
  "LocalStorage": ("tina4_python.realtime.storage", "LocalStorage"),
150
151
  "S3Storage": ("tina4_python.realtime.storage", "S3Storage"),
151
- # Inline testing
152
+ # Inline testing (decorator/descriptor surface — expect_* builders)
152
153
  "tests": ("tina4_python.Testing", "tests"),
153
- "assert_equal_inline": ("tina4_python.Testing", "assert_equal"),
154
+ "expect_equal": ("tina4_python.Testing", "expect_equal"),
155
+ "expect_raises": ("tina4_python.Testing", "expect_raises"),
156
+ "expect_true": ("tina4_python.Testing", "expect_true"),
157
+ "expect_false": ("tina4_python.Testing", "expect_false"),
154
158
  }
155
159
 
156
160
 
@@ -24,6 +24,12 @@ from urllib.parse import urlencode, urlparse
24
24
  from urllib.request import Request, HTTPRedirectHandler, HTTPSHandler, build_opener
25
25
  from urllib.error import HTTPError, URLError
26
26
 
27
+ # Safe: importing the tina4_python.api submodule always fully initializes the
28
+ # tina4_python package first, and __version__ is set near the very top of
29
+ # tina4_python/__init__.py, well before Api (a LAZY-loaded name) is ever
30
+ # touched -- no circular import.
31
+ from tina4_python import __version__
32
+
27
33
 
28
34
  # Statuses that warrant an automatic retry when ``max_retries`` > 0: rate-limit
29
35
  # (429) plus the transient server-side 5xx family. 4xx client errors (401,
@@ -382,7 +388,14 @@ class Api:
382
388
 
383
389
  def _build_request(self, method: str, url: str, body, content_type: str,
384
390
  extra_headers: dict | None = None) -> Request:
385
- headers = dict(self._headers)
391
+ # VERSION-DEC-03 (feature 130): every outbound request carries a
392
+ # default ``Tina4/<version>`` User-Agent. ``self._headers`` is merged
393
+ # in AFTER the default, and ``extra_headers`` after that, so a
394
+ # caller-supplied User-Agent (via the constructor's ``headers=``,
395
+ # ``add_headers()``, or a per-call ``extra_headers``) always wins --
396
+ # this is a default, never a clobber.
397
+ headers = {"User-Agent": f"Tina4/{__version__}"}
398
+ headers.update(self._headers)
386
399
  if self.auth_header:
387
400
  headers["Authorization"] = self.auth_header
388
401
 
@@ -783,7 +783,11 @@ class AuthMiddleware:
783
783
  token = auth_header[7:]
784
784
  if not Auth.valid_token_static(token):
785
785
  return request, response({"error": "Invalid token"}, 401)
786
- request.auth = Auth.get_payload_static(token)
786
+ # Stash the payload on request.user (REQ-PY-NO-USER, 3.13.99 — the
787
+ # field every other framework already exposes). This used to assign
788
+ # request.auth, which __slots__ has never declared, so this branch
789
+ # raised AttributeError on every SUCCESSFUL authentication.
790
+ request.user = Auth.get_payload_static(token)
787
791
  return request, response
788
792
 
789
793
 
@@ -128,8 +128,8 @@ def _fields_or_default(fields_str: str) -> list[tuple[str, str]]:
128
128
  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
- boolean_flags = {"no-browser", "no-reload", "production", "managed", "all", "clear", "json",
132
- "public", "no-migration", "once"}
131
+ boolean_flags = {"no-browser", "no-reload", "no-kill", "production", "managed", "all", "clear",
132
+ "json", "public", "no-migration", "once"}
133
133
 
134
134
  flags = {}
135
135
  positional = []
@@ -221,87 +221,37 @@ def _parse_every(every: str) -> int:
221
221
  return 60
222
222
 
223
223
 
224
- def _in_container() -> bool:
225
- """True when this process is running inside a container.
226
-
227
- Reclaiming a port makes sense on a dev machine, where a previous
228
- ``tina4 serve`` may still hold it. Inside a container the server IS the
229
- container, so there is never a stale sibling to reclaim from -- and trying
230
- is actively dangerous (see ``_kill_process_on_port``).
231
- """
232
- if os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv"):
233
- return True
234
- try:
235
- with open("/proc/1/cgroup", "r", encoding="utf-8", errors="replace") as fh:
236
- blob = fh.read()
237
- return "docker" in blob or "containerd" in blob or "kubepods" in blob
238
- except OSError:
239
- return False
240
-
241
-
242
- def selectable_pids(lsof_output: str, me: int, my_group: int | None = None) -> list[int]:
243
- """The PIDs from ``lsof -ti`` output that are safe to signal.
244
-
245
- Pure so the safety rule can be tested directly. An unvalidated parse is a
246
- footgun with real teeth: when ``lsof`` is present but prints a different
247
- shape than ``-ti`` implies, a non-numeric field coerces to 0, and
248
- signalling PID 0 sends the signal to EVERY process in the caller's own
249
- process group -- the server kills itself. That is exactly what happened in
250
- a container, where the log read "Killed existing process on port 7148
251
- (PID: 1 ...)" and the container exited 143.
252
-
253
- So: accept only all-digit tokens, and never PID 0 (our process group),
254
- PID 1 (init), ourselves, or our own process group.
255
- """
256
- pids = []
257
- for token in lsof_output.split():
258
- if not token.isdigit():
259
- continue # never coerce junk into a PID
260
- pid = int(token)
261
- if pid <= 1 or pid == me:
262
- continue # 0 = our process group, 1 = init, me = suicide
263
- if my_group is not None and pid == my_group:
264
- continue
265
- if pid not in pids:
266
- pids.append(pid)
267
- return pids
224
+ # The port-takeover safety logic (identity check, PID safety filter, container
225
+ # guard, dev gate, opt-out) lives in ONE shared module so the CLI path here and
226
+ # the runtime bind-failure fallback in core/server.py cannot diverge
227
+ # (TAKEOVER-DEC-02). `selectable_pids` and `_in_container` are re-exported so
228
+ # their existing callers/tests keep resolving `tina4_python.cli.selectable_pids`.
229
+ from tina4_python.core.port_takeover import ( # noqa: E402
230
+ selectable_pids,
231
+ in_container as _in_container,
232
+ take_over_port,
233
+ is_dev,
234
+ no_takeover_opted_out,
235
+ )
268
236
 
269
237
 
270
238
  def _kill_process_on_port(port: int) -> bool:
271
- """Kill any process listening on the given port. Returns True if killed.
239
+ """Reclaim *port* from a stale Tina4 dev server, only when it is safe.
272
240
 
273
- Skipped entirely in a container, where the server IS the container and
274
- there is no stale sibling to reclaim from. The PID safety rule lives in
275
- :func:`selectable_pids`.
276
- """
277
- if _in_container():
278
- return False
279
- try:
280
- result = subprocess.run(
281
- ["lsof", "-ti", f":{port}"],
282
- capture_output=True, text=True, timeout=5,
283
- )
284
- if result.returncode != 0 or not result.stdout.strip():
285
- return False
241
+ Routes through the shared identity-checked takeover (TAKEOVER-DEC-01/02):
242
+ a holder is signalled ONLY when a Tina4 dev server recorded its PID in the
243
+ per-port PID file. A foreign holder is left running and a clear message is
244
+ printed; takeover is also skipped in a container, outside dev mode, and when
245
+ opted out (`TINA4_NO_TAKEOVER` / `tina4 serve --no-kill`).
286
246
 
287
- me = os.getpid()
288
- my_group = os.getpgrp() if hasattr(os, "getpgrp") else None
289
- killed = []
290
- for pid in selectable_pids(result.stdout, me, my_group):
291
- try:
292
- os.kill(pid, signal.SIGTERM)
293
- killed.append(str(pid))
294
- except (ProcessLookupError, PermissionError):
295
- pass
296
-
297
- if not killed:
298
- return False
299
- import time
300
- time.sleep(0.5)
301
- print(f" ⚠ Killed existing process on port {port} (PID: {', '.join(killed)})")
247
+ Returns True only when a Tina4 holder was actually signalled.
248
+ """
249
+ result = take_over_port(port, dev=is_dev(), no_takeover=no_takeover_opted_out())
250
+ if result.reclaimed:
251
+ print(f" ⚠ {result.message}")
302
252
  return True
303
- except (FileNotFoundError, subprocess.TimeoutExpired):
304
- pass
253
+ if result.refused and result.message:
254
+ print(f" {result.message}")
305
255
  return False
306
256
 
307
257
 
@@ -834,7 +784,12 @@ def _serve(args):
834
784
  # --no-reload flag
835
785
  no_reload = "no-reload" in flags
836
786
 
837
- # Kill existing process on port
787
+ # --no-kill opts out of port takeover for the whole process, so the CLI path
788
+ # here AND the runtime bind-failure fallback both honour it (TAKEOVER-DEC-03).
789
+ if "no-kill" in flags:
790
+ os.environ["TINA4_NO_TAKEOVER"] = "true"
791
+
792
+ # Reclaim the port from a stale Tina4 dev server only (identity-checked).
838
793
  port = cli_port or int(os.environ.get("PORT", os.environ.get("TINA4_PORT", "7146")))
839
794
  _kill_process_on_port(port)
840
795
 
@@ -907,14 +862,14 @@ def _migrate_status(args):
907
862
  if completed:
908
863
  print("\nCompleted migrations:")
909
864
  for m in completed:
910
- print(f" [batch {m['batch']}] {m['migration_id']} ({m['executed_at']})")
865
+ print(f" [batch {m['batch']}] {m['migration_name']} ({m['executed_at']})")
911
866
  else:
912
867
  print("\nNo completed migrations.")
913
868
 
914
869
  if pending:
915
870
  print("\nPending migrations:")
916
871
  for m in pending:
917
- print(f" {m['migration_id']} ({m['description']})")
872
+ print(f" {m['migration_name']} ({m['description']})")
918
873
  else:
919
874
  print("\nNo pending migrations.")
920
875
 
@@ -977,10 +932,70 @@ def _routes(args):
977
932
  print(f"\n{len(routes)} route(s) registered.")
978
933
 
979
934
 
935
+ def _run_inline_tests(root: Path) -> bool:
936
+ """Discover ``@tests``-decorated functions under ``src/`` and run them.
937
+
938
+ Returns True if any inline test FAILED or ERRORED (so the caller can exit
939
+ non-zero), False if everything passed or there were no inline tests.
940
+
941
+ Only files whose text contains ``@tests`` are imported — a source file
942
+ without an inline test is never executed, so ``tina4 test`` cannot run a
943
+ scanned file's arbitrary side effect. This is the ``@tests`` decorator
944
+ surface wired to a real exit code (INLINE-DEC-01).
945
+ """
946
+ import importlib.util
947
+
948
+ from tina4_python.Testing import reset, run_all
949
+
950
+ src = root / "src"
951
+ if not src.is_dir():
952
+ return False
953
+
954
+ reset()
955
+ discovered = 0
956
+ for path in sorted(src.rglob("*.py")):
957
+ try:
958
+ text = path.read_text(encoding="utf-8", errors="ignore")
959
+ except OSError:
960
+ continue
961
+ if "@tests" not in text:
962
+ continue
963
+ discovered += 1
964
+ module_name = "tina4_inline_" + re.sub(r"\W", "_", str(path.relative_to(root)))
965
+ spec = importlib.util.spec_from_file_location(module_name, str(path))
966
+ if spec is None or spec.loader is None:
967
+ continue
968
+ try:
969
+ module = importlib.util.module_from_spec(spec)
970
+ spec.loader.exec_module(module)
971
+ except Exception as exc: # noqa: BLE001 — report and keep going
972
+ print(f" ! could not import {path.relative_to(root)}: {exc}")
973
+
974
+ if discovered == 0:
975
+ return False
976
+
977
+ results = run_all()
978
+ return (results["failed"] + results["errors"]) > 0
979
+
980
+
980
981
  def _test(args):
981
- """Run the test suite."""
982
- result = subprocess.run([sys.executable, "-m", "pytest", "tests/"] + args)
983
- sys.exit(result.returncode)
982
+ """Run the inline ``@tests`` suite AND the pytest suite; exit non-zero if either fails.
983
+
984
+ The inline stage discovers ``@tests``-decorated functions under ``src/`` and
985
+ runs them with a real exit code (INLINE-DEC-01) — the batteries-included flow
986
+ the docs advertise. The pytest stage still runs the ``tests/`` suite when that
987
+ directory exists (preserving the python#96 exit-code contract), so a project
988
+ that keeps its tests in ``tests/`` is unaffected.
989
+ """
990
+ inline_failed = _run_inline_tests(Path.cwd())
991
+
992
+ pytest_code = 0
993
+ if Path("tests").is_dir():
994
+ pytest_code = subprocess.run(
995
+ [sys.executable, "-m", "pytest", "tests/"] + args
996
+ ).returncode
997
+
998
+ sys.exit(1 if (inline_failed or pytest_code) else 0)
984
999
 
985
1000
 
986
1001
  def _build(args):
@@ -1211,8 +1226,8 @@ def _gen_route(name: str, flags: dict, *, emit_test: bool = True):
1211
1226
  @get("/api/{route_path}")
1212
1227
  async def list_{route_path}(request, response):
1213
1228
  """List all {route_path} with pagination."""
1214
- page = int(request.params.get("page", 1))
1215
- per_page = int(request.params.get("per_page", 20))
1229
+ page = int(request.query.get("page", 1))
1230
+ per_page = int(request.query.get("per_page", 20))
1216
1231
  offset = (page - 1) * per_page
1217
1232
  records, total = {model}.where("1=1", limit=per_page, offset=offset, with_count=True)
1218
1233
  return response({{