tina4-python 3.13.74__tar.gz → 3.13.76__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 (168) hide show
  1. {tina4_python-3.13.74 → tina4_python-3.13.76}/PKG-INFO +1 -1
  2. {tina4_python-3.13.74 → tina4_python-3.13.76}/pyproject.toml +1 -1
  3. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/core/server.py +32 -0
  4. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/migration/runner.py +33 -11
  5. {tina4_python-3.13.74 → tina4_python-3.13.76}/.gitignore +0 -0
  6. {tina4_python-3.13.74 → tina4_python-3.13.76}/README.md +0 -0
  7. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/CLAUDE.md +0 -0
  8. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/HtmlElement.py +0 -0
  9. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/Testing.py +0 -0
  10. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/__init__.py +0 -0
  11. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/ai/__init__.py +0 -0
  12. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/api/__init__.py +0 -0
  13. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/auth/__init__.py +0 -0
  14. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/cache/__init__.py +0 -0
  15. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/cli/__init__.py +0 -0
  16. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/container/__init__.py +0 -0
  17. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/context/__init__.py +0 -0
  18. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/context/chunker.py +0 -0
  19. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/core/__init__.py +0 -0
  20. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/core/cache.py +0 -0
  21. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/core/constants.py +0 -0
  22. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/core/events.py +0 -0
  23. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/core/middleware.py +0 -0
  24. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/core/rate_limiter.py +0 -0
  25. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/core/request.py +0 -0
  26. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/core/response.py +0 -0
  27. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/core/router.py +0 -0
  28. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/crud/__init__.py +0 -0
  29. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/database/__init__.py +0 -0
  30. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/database/adapter.py +0 -0
  31. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/database/connection.py +0 -0
  32. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/database/firebird.py +0 -0
  33. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/database/mongodb.py +0 -0
  34. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/database/mssql.py +0 -0
  35. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/database/mysql.py +0 -0
  36. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/database/odbc.py +0 -0
  37. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/database/postgres.py +0 -0
  38. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/database/sqlite.py +0 -0
  39. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/debug/__init__.py +0 -0
  40. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/debug/error_overlay.py +0 -0
  41. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/dev_admin/__init__.py +0 -0
  42. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/dev_admin/metrics.py +0 -0
  43. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/dev_admin/plan.py +0 -0
  44. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/dev_admin/project_index.py +0 -0
  45. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/docs.py +0 -0
  46. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/docstore/__init__.py +0 -0
  47. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/dotenv/__init__.py +0 -0
  48. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/env.py +0 -0
  49. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/frond/FROND.md +0 -0
  50. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/frond/__init__.py +0 -0
  51. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/frond/engine.py +0 -0
  52. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/auth/meta.json +0 -0
  53. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  54. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/database/meta.json +0 -0
  55. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  56. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/error-overlay/meta.json +0 -0
  57. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  58. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/orm/meta.json +0 -0
  59. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  60. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  61. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/queue/meta.json +0 -0
  62. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  63. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/rest-api/meta.json +0 -0
  64. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  65. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/templates/meta.json +0 -0
  66. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  67. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  68. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/graphql/__init__.py +0 -0
  69. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/i18n/__init__.py +0 -0
  70. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/mcp/__init__.py +0 -0
  71. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/mcp/protocol.py +0 -0
  72. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/mcp/tools.py +0 -0
  73. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/messenger/__init__.py +0 -0
  74. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/migration/__init__.py +0 -0
  75. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/orm/__init__.py +0 -0
  76. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/orm/fields.py +0 -0
  77. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/orm/model.py +0 -0
  78. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/__feedback/widget.js +0 -0
  79. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/css/tina4.css +0 -0
  80. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/css/tina4.min.css +0 -0
  81. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/favicon.ico +0 -0
  82. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/images/logo.svg +0 -0
  83. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  84. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/js/frond.js +0 -0
  85. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/js/frond.min.js +0 -0
  86. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  87. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  88. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/js/tina4.min.js +0 -0
  89. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/js/tina4js.min.js +0 -0
  90. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/swagger/index.html +0 -0
  91. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  92. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/query_builder/__init__.py +0 -0
  93. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/queue/__init__.py +0 -0
  94. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/queue/job.py +0 -0
  95. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/queue/kafka_backend.py +0 -0
  96. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/queue/lite_backend.py +0 -0
  97. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/queue/mongo_backend.py +0 -0
  98. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/queue/rabbitmq_backend.py +0 -0
  99. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/queue_backends/__init__.py +0 -0
  100. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/queue_backends/kafka_backend.py +0 -0
  101. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/queue_backends/mongo_backend.py +0 -0
  102. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  103. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/realtime/__init__.py +0 -0
  104. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/realtime/models/Attachment.py +0 -0
  105. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/realtime/models/Channel.py +0 -0
  106. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/realtime/models/ChannelMember.py +0 -0
  107. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/realtime/models/Message.py +0 -0
  108. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/realtime/models/Workspace.py +0 -0
  109. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/realtime/models/__init__.py +0 -0
  110. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/realtime/storage.py +0 -0
  111. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/__init__.py +0 -0
  112. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  113. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_badges.scss +0 -0
  114. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  115. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_cards.scss +0 -0
  116. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_forms.scss +0 -0
  117. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_grid.scss +0 -0
  118. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_modals.scss +0 -0
  119. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_nav.scss +0 -0
  120. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_reset.scss +0 -0
  121. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_tables.scss +0 -0
  122. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_typography.scss +0 -0
  123. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  124. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/_variables.scss +0 -0
  125. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/base.scss +0 -0
  126. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/colors.scss +0 -0
  127. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/scss/tina4css/tina4.scss +0 -0
  128. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/seeder/__init__.py +0 -0
  129. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/service/__init__.py +0 -0
  130. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/session/__init__.py +0 -0
  131. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  133. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/session_handlers/redis_handler.py +0 -0
  134. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/session_handlers/valkey_handler.py +0 -0
  135. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/swagger/__init__.py +0 -0
  136. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/components/crud.twig +0 -0
  137. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  138. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  139. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/docker/python/Dockerfile +0 -0
  140. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  141. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/errors/302.twig +0 -0
  142. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/errors/401.twig +0 -0
  143. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/errors/403.twig +0 -0
  144. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/errors/404.twig +0 -0
  145. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/errors/500.twig +0 -0
  146. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/errors/502.twig +0 -0
  147. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/errors/503.twig +0 -0
  148. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/errors/base.twig +0 -0
  149. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/frontend/README.md +0 -0
  150. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/templates/readme.md +0 -0
  151. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/test/__init__.py +0 -0
  152. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/test_client/__init__.py +0 -0
  153. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/validator/__init__.py +0 -0
  166. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/websocket/__init__.py +0 -0
  167. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/websocket/backplane.py +0 -0
  168. {tina4_python-3.13.74 → tina4_python-3.13.76}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.13.74
3
+ Version: 3.13.76
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.74"
3
+ version = "3.13.76"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -12,6 +12,7 @@ import asyncio
12
12
  import contextvars
13
13
  import importlib
14
14
  import uuid
15
+ from email.utils import formatdate, parsedate_to_datetime
15
16
  from pathlib import Path
16
17
 
17
18
  from tina4_python.core.request import Request
@@ -1975,6 +1976,25 @@ async def app(scope: dict, receive, send):
1975
1976
  await send({"type": "http.response.body", "body": b""})
1976
1977
  return
1977
1978
 
1979
+ # If-Modified-Since -> 304, for responses carrying a Last-Modified (static
1980
+ # assets). If-None-Match takes precedence (RFC 9110 13.1.3), so this only
1981
+ # runs when the client sent no ETag validator.
1982
+ if not if_none_match:
1983
+ if_modified_since = request.headers.get("if-modified-since", "")
1984
+ last_modified = ""
1985
+ for name, value in headers:
1986
+ if name == b"last-modified":
1987
+ last_modified = value.decode()
1988
+ break
1989
+ if if_modified_since and last_modified:
1990
+ try:
1991
+ if parsedate_to_datetime(last_modified) <= parsedate_to_datetime(if_modified_since):
1992
+ await send({"type": "http.response.start", "status": 304, "headers": []})
1993
+ await send({"type": "http.response.body", "body": b""})
1994
+ return
1995
+ except (TypeError, ValueError):
1996
+ pass # Unparseable date -> serve the body (never 304 on garbage).
1997
+
1978
1998
  await send({"type": "http.response.start", "status": response.status_code, "headers": headers})
1979
1999
  await send({"type": "http.response.body", "body": response.content})
1980
2000
 
@@ -2010,6 +2030,18 @@ def _try_static(path: str) -> Response | None:
2010
2030
  if file_path.is_file():
2011
2031
  resp = Response()
2012
2032
  resp.file(str(file_path))
2033
+ # Static assets may be cached but must be revalidated on every use,
2034
+ # so a redeployed file reaches the browser on the next load without a
2035
+ # manual hard refresh. The response already carries an ETag
2036
+ # (build_headers) and the pipeline answers If-None-Match with a 304,
2037
+ # so revalidation is a cheap round-trip, not a re-download. A
2038
+ # Last-Modified is added too (pipeline honours If-Modified-Since ->
2039
+ # 304) so the validators match the other frameworks.
2040
+ resp.header("cache-control", "no-cache, must-revalidate")
2041
+ resp.header(
2042
+ "last-modified",
2043
+ formatdate(file_path.stat().st_mtime, usegmt=True),
2044
+ )
2013
2045
  return resp
2014
2046
  return None
2015
2047
 
@@ -296,7 +296,11 @@ def _ensure_tracking_table(db, migration_folder: str | None = None):
296
296
  # and copy the values across so already-applied migrations are still seen as
297
297
  # applied — without this the `migration_name`-based read below finds nothing
298
298
  # and every migration re-runs. The old `migration_id` column is left in
299
- # place (harmless, ignored from now on).
299
+ # place: it cannot be dropped portably (SQLite cannot drop a NOT NULL or a
300
+ # column without a full table rebuild). It is ignored on READS, but it is
301
+ # NOT ignorable on WRITES — it is NOT NULL on that table, so
302
+ # `_record_applied` must keep populating it (see there). Leaving it unwritten
303
+ # is what broke every migration on an upgraded database in 3.13.55-3.13.75.
300
304
  if "migration_id" in cols and "migration_name" not in cols:
301
305
  col_type = "VARCHAR(500)" if _is_firebird(db) else "TEXT"
302
306
  try:
@@ -641,18 +645,36 @@ def _record_applied(db, name: str, desc: str, batch: int, passed: int = 1) -> No
641
645
  """
642
646
  now = datetime.now(timezone.utc).isoformat()
643
647
  db.execute("DELETE FROM tina4_migration WHERE migration_name = ?", [name])
648
+
649
+ # Build the column list from the columns that ACTUALLY exist on this table,
650
+ # so a legacy column an earlier in-place upgrade left behind is populated
651
+ # rather than defaulted to NULL. A table created by v3 <= 3.13.54 carries
652
+ # `migration_id NOT NULL UNIQUE`; the rename upgrade adds `migration_name`
653
+ # beside it but cannot drop the NOT NULL portably (SQLite cannot drop it at
654
+ # all without a table rebuild), so this insert has to satisfy it or every
655
+ # migration on that database fails and none can ever apply.
656
+ cols = _column_names(db)
657
+ insert_cols = ["migration_name", "description", "batch", "executed_at", "passed"]
658
+ values = [name, desc, batch, now, passed]
659
+
660
+ # v3 <= 3.13.54: `migration_id` held exactly the value `migration_name` holds
661
+ # now, so writing `name` restores that invariant. Safe against its UNIQUE:
662
+ # the DELETE above removed this migration's own row first, so a re-run
663
+ # replaces its row instead of colliding.
664
+ if "migration_id" in cols:
665
+ insert_cols.append("migration_id")
666
+ values.append(name)
667
+
644
668
  if _is_firebird(db):
645
669
  row = db.fetch_one("SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS next_id FROM RDB$DATABASE")
646
- next_id = row["next_id"] if row else 1
647
- db.execute(
648
- "INSERT INTO tina4_migration (id, migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?, ?)",
649
- [next_id, name, desc, batch, now, passed],
650
- )
651
- else:
652
- db.execute(
653
- "INSERT INTO tina4_migration (migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?)",
654
- [name, desc, batch, now, passed],
655
- )
670
+ insert_cols.insert(0, "id")
671
+ values.insert(0, row["next_id"] if row else 1)
672
+
673
+ placeholders = ", ".join("?" for _ in insert_cols)
674
+ db.execute(
675
+ f"INSERT INTO tina4_migration ({', '.join(insert_cols)}) VALUES ({placeholders})",
676
+ values,
677
+ )
656
678
 
657
679
 
658
680
  def _migrate(db, migration_folder: str = "migrations", delimiter: str = ";") -> list[str]:
File without changes