tina4-python 3.13.39__tar.gz → 3.13.41__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 (159) hide show
  1. {tina4_python-3.13.39 → tina4_python-3.13.41}/PKG-INFO +1 -1
  2. {tina4_python-3.13.39 → tina4_python-3.13.41}/pyproject.toml +1 -1
  3. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/CLAUDE.md +31 -9
  4. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/__init__.py +1 -1
  5. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/auth/__init__.py +5 -1
  6. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/request.py +6 -1
  7. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/server.py +29 -7
  8. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/crud/__init__.py +5 -0
  9. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/dev_admin/__init__.py +33 -4
  10. tina4_python-3.13.41/tina4_python/docstore/__init__.py +714 -0
  11. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/mcp/__init__.py +63 -24
  12. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/mcp/tools.py +18 -3
  13. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/__init__.py +38 -8
  14. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/lite_backend.py +132 -21
  15. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/mongo_backend.py +8 -2
  16. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue_backends/mongo_backend.py +71 -3
  17. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/session_handlers/mongodb_handler.py +8 -3
  18. tina4_python-3.13.41/tina4_python/swagger/__init__.py +612 -0
  19. tina4_python-3.13.39/tina4_python/swagger/__init__.py +0 -388
  20. {tina4_python-3.13.39 → tina4_python-3.13.41}/.gitignore +0 -0
  21. {tina4_python-3.13.39 → tina4_python-3.13.41}/README.md +0 -0
  22. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/HtmlElement.py +0 -0
  23. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/Testing.py +0 -0
  24. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/ai/__init__.py +0 -0
  25. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/api/__init__.py +0 -0
  26. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/cache/__init__.py +0 -0
  27. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/cli/__init__.py +0 -0
  28. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/container/__init__.py +0 -0
  29. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/__init__.py +0 -0
  30. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/cache.py +0 -0
  31. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/constants.py +0 -0
  32. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/events.py +0 -0
  33. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/middleware.py +0 -0
  34. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/rate_limiter.py +0 -0
  35. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/response.py +0 -0
  36. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/core/router.py +0 -0
  37. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/__init__.py +0 -0
  38. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/adapter.py +0 -0
  39. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/connection.py +0 -0
  40. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/firebird.py +0 -0
  41. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/mongodb.py +0 -0
  42. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/mssql.py +0 -0
  43. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/mysql.py +0 -0
  44. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/odbc.py +0 -0
  45. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/postgres.py +0 -0
  46. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/database/sqlite.py +0 -0
  47. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/debug/__init__.py +0 -0
  48. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/debug/error_overlay.py +0 -0
  49. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/dev_admin/metrics.py +0 -0
  50. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/dev_admin/plan.py +0 -0
  51. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/dev_admin/project_index.py +0 -0
  52. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/docs.py +0 -0
  53. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/dotenv/__init__.py +0 -0
  54. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/env.py +0 -0
  55. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/frond/FROND.md +0 -0
  56. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/frond/__init__.py +0 -0
  57. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/frond/engine.py +0 -0
  58. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/auth/meta.json +0 -0
  59. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  60. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/database/meta.json +0 -0
  61. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  62. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/error-overlay/meta.json +0 -0
  63. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  64. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/orm/meta.json +0 -0
  65. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  66. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  67. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/queue/meta.json +0 -0
  68. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  69. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/rest-api/meta.json +0 -0
  70. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  71. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/templates/meta.json +0 -0
  72. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  73. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  74. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/graphql/__init__.py +0 -0
  75. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/i18n/__init__.py +0 -0
  76. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/mcp/protocol.py +0 -0
  77. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/messenger/__init__.py +0 -0
  78. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/migration/__init__.py +0 -0
  79. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/migration/runner.py +0 -0
  80. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/orm/__init__.py +0 -0
  81. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/orm/fields.py +0 -0
  82. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/orm/model.py +0 -0
  83. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/__feedback/widget.js +0 -0
  84. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/css/tina4.css +0 -0
  85. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/css/tina4.min.css +0 -0
  86. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/favicon.ico +0 -0
  87. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/images/logo.svg +0 -0
  88. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  89. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/frond.js +0 -0
  90. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/frond.min.js +0 -0
  91. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  92. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  93. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/tina4.min.js +0 -0
  94. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/js/tina4js.min.js +0 -0
  95. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/swagger/index.html +0 -0
  96. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  97. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/query_builder/__init__.py +0 -0
  98. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/job.py +0 -0
  99. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/kafka_backend.py +0 -0
  100. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue/rabbitmq_backend.py +0 -0
  101. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue_backends/__init__.py +0 -0
  102. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue_backends/kafka_backend.py +0 -0
  103. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  104. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/__init__.py +0 -0
  105. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  106. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_badges.scss +0 -0
  107. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  108. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_cards.scss +0 -0
  109. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_forms.scss +0 -0
  110. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_grid.scss +0 -0
  111. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_modals.scss +0 -0
  112. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_nav.scss +0 -0
  113. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_reset.scss +0 -0
  114. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_tables.scss +0 -0
  115. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_typography.scss +0 -0
  116. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  117. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/_variables.scss +0 -0
  118. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/base.scss +0 -0
  119. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/colors.scss +0 -0
  120. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/scss/tina4css/tina4.scss +0 -0
  121. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/seeder/__init__.py +0 -0
  122. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/service/__init__.py +0 -0
  123. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/session/__init__.py +0 -0
  124. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/session_handlers/__init__.py +0 -0
  125. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/session_handlers/redis_handler.py +0 -0
  126. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/session_handlers/valkey_handler.py +0 -0
  127. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/components/crud.twig +0 -0
  128. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  129. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  130. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/docker/python/Dockerfile +0 -0
  131. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  132. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/302.twig +0 -0
  133. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/401.twig +0 -0
  134. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/403.twig +0 -0
  135. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/404.twig +0 -0
  136. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/500.twig +0 -0
  137. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/502.twig +0 -0
  138. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/503.twig +0 -0
  139. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/errors/base.twig +0 -0
  140. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/frontend/README.md +0 -0
  141. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/templates/readme.md +0 -0
  142. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/test/__init__.py +0 -0
  143. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/test_client/__init__.py +0 -0
  144. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  155. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  156. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/validator/__init__.py +0 -0
  157. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/websocket/__init__.py +0 -0
  158. {tina4_python-3.13.39 → tina4_python-3.13.41}/tina4_python/websocket/backplane.py +0 -0
  159. {tina4_python-3.13.39 → tina4_python-3.13.41}/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.39
3
+ Version: 3.13.41
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.39"
3
+ version = "3.13.41"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -1145,9 +1145,14 @@ for job in queue.consume():
1145
1145
  ```python
1146
1146
  queue = Queue(topic="tasks", max_retries=3)
1147
1147
 
1148
+ # Reservation / visibility timeout (seconds): a popped job is reserved this long;
1149
+ # if the consumer dies before complete()/fail() the next pop() reclaims it
1150
+ # (at-least-once delivery). Default 300; env TINA4_QUEUE_VISIBILITY_TIMEOUT; <= 0 disables.
1151
+ queue = Queue(topic="tasks", visibility_timeout=300)
1152
+
1148
1153
  # Check queue size
1149
1154
  queue.size() # pending jobs
1150
- queue.size("reserved") # currently processing
1155
+ queue.size("reserved") # currently reserved (in-flight) jobs
1151
1156
 
1152
1157
  # Retry failed jobs (under max_retries limit)
1153
1158
  queue.retry_failed()
@@ -1159,6 +1164,14 @@ dead = queue.dead_letters()
1159
1164
  queue.purge("completed")
1160
1165
  ```
1161
1166
 
1167
+ **At-least-once delivery (file + MongoDB backends).** A popped job is held as a
1168
+ reservation for `visibility_timeout` seconds. If the consumer crashes, OOMs, or
1169
+ is evicted before calling `job.complete()` / `job.fail()`, the next `pop()`
1170
+ reclaims the abandoned reservation: it increments `attempts` and re-enqueues the
1171
+ job, or dead-letters it once it has hit `max_retries`. A dead consumer therefore
1172
+ never strands a job. RabbitMQ and Kafka delegate redelivery to the broker (the
1173
+ framework timeout does not apply there).
1174
+
1162
1175
  ### When to use queues
1163
1176
  - Sending emails or SMS
1164
1177
  - Generating PDFs/reports
@@ -1633,14 +1646,23 @@ TINA4_SESSION_BACKEND=SessionFileHandler # SessionFileHandler, SessionRedisHand
1633
1646
  TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookies (default: Lax)
1634
1647
 
1635
1648
  # Swagger/OpenAPI
1636
- SWAGGER_TITLE=Tina4 API # API title (default: "Tina4 API")
1637
- SWAGGER_VERSION=1.0.0 # API version
1638
- SWAGGER_DESCRIPTION= # API description
1639
- SWAGGER_CONTACT_TEAM= # Contact name
1640
- SWAGGER_CONTACT_URL= # Contact URL
1641
- SWAGGER_CONTACT_EMAIL= # Contact email
1642
- SWAGGER_DEV_URL=http://localhost:7145 # Dev server URL for Swagger
1643
- ```
1649
+ TINA4_SWAGGER_ENABLED= # on/off for /swagger UI + /swagger/openapi.json. Explicit true/false wins; unset falls back to TINA4_DEBUG. Set false to DISABLE swagger in any environment; true to expose it in production
1650
+ TINA4_SWAGGER_TITLE=Tina4 API # API title (default: "Tina4 API")
1651
+ TINA4_SWAGGER_VERSION=1.0.0 # API version
1652
+ TINA4_SWAGGER_DESCRIPTION= # API description
1653
+ TINA4_SWAGGER_CONTACT_TEAM= # Contact name (legacy SWAGGER_CONTACT_TEAM still read as fallback)
1654
+ TINA4_SWAGGER_CONTACT_URL= # Contact URL (legacy SWAGGER_CONTACT_URL still read as fallback)
1655
+ TINA4_SWAGGER_CONTACT_EMAIL= # Contact email
1656
+ TINA4_SWAGGER_LICENSE= # SPDX license name (e.g. MIT) for info.license
1657
+ TINA4_SWAGGER_SERVERS= # comma-separated server URLs for the OpenAPI servers[] block; falls back to SWAGGER_DEV_URL
1658
+ TINA4_SWAGGER_UI_CDN= # base URL for the Swagger UI assets (default jsdelivr); point at a self-hosted mirror for air-gapped use
1659
+ SWAGGER_DEV_URL=http://localhost:7145 # single dev-server URL (used when TINA4_SWAGGER_SERVERS is unset)
1660
+ ```
1661
+
1662
+ The spec is OpenAPI 3.0.3. ORM models registered via AutoCrud become reusable
1663
+ `components.schemas` referenced by `$ref`, and secured routes emit a `bearerAuth`
1664
+ security requirement. Set `TINA4_SWAGGER_ENABLED=false` to turn the endpoints off
1665
+ anywhere (the production on/off switch, wired for real in 3.13.40).
1644
1666
 
1645
1667
  ### Debug levels
1646
1668
  - `ALL` / `DEBUG` — most verbose; every level on the console
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.13.39"
11
+ __version__ = "3.13.41"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -78,7 +78,11 @@ def _is_production() -> bool:
78
78
  _BLANK_SECRET_WARNING = (
79
79
  "Auth: TINA4_SECRET is not set — JWT signing is insecure. "
80
80
  "Set TINA4_SECRET to a random value (e.g. `openssl rand -hex 32`) "
81
- "in your environment or .env before serving traffic."
81
+ "in your environment or .env before serving traffic. "
82
+ "For LOCAL DEV, set TINA4_DEBUG=true and a per-machine secret is generated "
83
+ "automatically into .env.local (gitignored). Seeing this warning means the "
84
+ "run was NOT detected as dev — typically a container or CI without "
85
+ "TINA4_DEBUG set, or TINA4_ENV=production."
82
86
  )
83
87
 
84
88
 
@@ -71,7 +71,7 @@ class Request:
71
71
 
72
72
  __slots__ = (
73
73
  "method", "path", "url", "query_string", "params", "query", "headers",
74
- "body", "raw_body", "cookies", "files", "ip",
74
+ "body", "raw_body", "cookies", "files", "ip", "remote_ip",
75
75
  "content_type", "session", "_route_params",
76
76
  )
77
77
 
@@ -88,6 +88,7 @@ class Request:
88
88
  self.cookies: dict = {}
89
89
  self.files: dict = {}
90
90
  self.ip: str = ""
91
+ self.remote_ip: str = "" # Raw socket peer (never X-Forwarded-For) — for trust decisions
91
92
  self.content_type: str = ""
92
93
  self.session = None # Set by session middleware
93
94
  self._route_params: dict = {} # Dynamic route params ({id}, etc.)
@@ -107,6 +108,10 @@ class Request:
107
108
 
108
109
  req.content_type = req.headers.get("content-type", "")
109
110
  req.ip = _extract_ip(scope, req.headers)
111
+ # Raw socket peer address — NEVER honours X-Forwarded-For, so it can
112
+ # be trusted for loopback/remote authorisation (e.g. the MCP guard).
113
+ _client = scope.get("client")
114
+ req.remote_ip = _client[0] if _client else ""
110
115
 
111
116
  # Reconstruct the full absolute URL — scheme://host[:port]/path[?query].
112
117
  # Honours x-forwarded-proto and x-forwarded-host so apps behind a proxy
@@ -1174,14 +1174,28 @@ async def _handle_dev_admin(request: Request, response: Response) -> Response:
1174
1174
 
1175
1175
 
1176
1176
  def _handle_swagger(request: Request, response: Response) -> Response | None:
1177
- """Serve /swagger UI and /swagger/openapi.json. Returns Response or None."""
1177
+ """Serve /swagger UI and /swagger/openapi.json. Returns Response or None.
1178
+
1179
+ Self-gated on swagger.is_enabled() (TINA4_SWAGGER_ENABLED, else TINA4_DEBUG)
1180
+ so the documented production on/off switch is actually honoured — before
1181
+ v3.13.40 the dispatch gated only on TINA4_DEBUG and this env var was dead.
1182
+ """
1183
+ from tina4_python.swagger import is_enabled as _swagger_enabled
1184
+ if not _swagger_enabled():
1185
+ return None
1178
1186
  if request.path in ("/swagger", "/swagger/"):
1187
+ # The UI assets load from a CDN by default (keeps the framework
1188
+ # zero-dependency — no vendored ~1.4MB swagger-ui-dist). Air-gapped
1189
+ # deployments point TINA4_SWAGGER_UI_CDN at a self-hosted mirror.
1190
+ _cdn = os.environ.get(
1191
+ "TINA4_SWAGGER_UI_CDN", "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5"
1192
+ ).rstrip("/")
1179
1193
  swagger_html = (
1180
1194
  '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
1181
1195
  '<title>API Documentation</title>'
1182
- '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">'
1196
+ f'<link rel="stylesheet" href="{_cdn}/swagger-ui.css">'
1183
1197
  '</head><body><div id="swagger-ui"></div>'
1184
- '<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>'
1198
+ f'<script src="{_cdn}/swagger-ui-bundle.js"></script>'
1185
1199
  '<script>SwaggerUIBundle({ url: "/swagger/openapi.json", dom_id: "#swagger-ui" });</script>'
1186
1200
  '</body></html>'
1187
1201
  )
@@ -1715,8 +1729,11 @@ async def handle(request: Request) -> Response:
1715
1729
  ):
1716
1730
  return await _handle_dev_admin(request, response)
1717
1731
 
1718
- # Swagger
1719
- if _is_dev and request.method == "GET":
1732
+ # Swagger — _handle_swagger self-gates on swagger.is_enabled()
1733
+ # (TINA4_SWAGGER_ENABLED, else TINA4_DEBUG), so we call it on any GET:
1734
+ # this honours an explicit prod-enable AND an explicit dev-disable, both
1735
+ # of which the old `_is_dev`-only gate silently ignored.
1736
+ if request.method == "GET":
1720
1737
  swagger_resp = _handle_swagger(request, response)
1721
1738
  if swagger_resp is not None:
1722
1739
  return swagger_resp
@@ -2227,8 +2244,13 @@ def _check_legacy_env_vars() -> None:
2227
2244
  new = _LEGACY_ENV_VARS[old]
2228
2245
  msg.append(f" {old:<28} → {new}")
2229
2246
  msg.extend(["",
2230
- "Run `tina4 env --migrate` to rewrite your .env automatically,",
2231
- "or rename manually. See https://tina4.com/release/3.12.0",
2247
+ "Note: these may come from a .env file loaded by dotenv, not just",
2248
+ "the runtime environment check your image / build context (a .env",
2249
+ "baked into a Docker image is loaded at startup) as well as k8s/CI env.",
2250
+ "",
2251
+ "FIX: run `tina4 env --migrate` to rewrite your .env automatically",
2252
+ "(it renames every legacy name to its TINA4_ form in place).",
2253
+ "Or rename manually. See https://tina4.com/release/3.12.0",
2232
2254
  "Set TINA4_ALLOW_LEGACY_ENV=true to bypass during migration.",
2233
2255
  "─" * 72, ""])
2234
2256
  print("\n".join(msg), file=sys.stderr)
@@ -132,6 +132,8 @@ class AutoCrud:
132
132
  list_handler.__qualname__ = f"autocrud_list_{table}"
133
133
  list_handler._swagger_summary = f"List all {pretty_name}"
134
134
  list_handler._swagger_tags = [table]
135
+ list_handler._swagger_model = model_class
136
+ list_handler._swagger_model_list = True
135
137
  Router.add("GET", base_path, list_handler)
136
138
  generated.append({"method": "GET", "path": base_path, "table": table})
137
139
 
@@ -147,6 +149,7 @@ class AutoCrud:
147
149
  get_handler.__qualname__ = f"autocrud_get_{table}"
148
150
  get_handler._swagger_summary = f"Get {pretty_name} by ID"
149
151
  get_handler._swagger_tags = [table]
152
+ get_handler._swagger_model = model_class
150
153
  Router.add("GET", f"{base_path}/{{id}}", get_handler)
151
154
  generated.append({"method": "GET", "path": f"{base_path}/{{id}}", "table": table})
152
155
 
@@ -169,6 +172,7 @@ class AutoCrud:
169
172
  create_handler._swagger_summary = f"Create {pretty_name}"
170
173
  create_handler._swagger_tags = [table]
171
174
  create_handler._swagger_example = example_body
175
+ create_handler._swagger_model = model_class
172
176
  Router.add("POST", base_path, create_handler)
173
177
  generated.append({"method": "POST", "path": base_path, "table": table})
174
178
 
@@ -201,6 +205,7 @@ class AutoCrud:
201
205
  update_handler._swagger_summary = f"Update {pretty_name}"
202
206
  update_handler._swagger_tags = [table]
203
207
  update_handler._swagger_example = example_body
208
+ update_handler._swagger_model = model_class
204
209
  Router.add("PUT", f"{base_path}/{{id}}", update_handler)
205
210
  generated.append({"method": "PUT", "path": f"{base_path}/{{id}}", "table": table})
206
211
 
@@ -2864,12 +2864,42 @@ async def _api_git_status(request, response):
2864
2864
  # the same registry, so tools registered via the `@mcp_tool` decorator
2865
2865
  # show up on both immediately.
2866
2866
 
2867
+ def _mcp_token_ok(request) -> bool:
2868
+ """Timing-safe check of a remote MCP caller's token against
2869
+ TINA4_MCP_TOKEN (falling back to TINA4_API_KEY). Accepts an
2870
+ `Authorization: Bearer <token>` header, `X-MCP-Token`, or `X-Api-Key`."""
2871
+ import os as _os, hmac as _hmac
2872
+ expected = _os.environ.get("TINA4_MCP_TOKEN") or _os.environ.get("TINA4_API_KEY") or ""
2873
+ if not expected:
2874
+ return False
2875
+ headers = getattr(request, "headers", None) or {}
2876
+ auth = headers.get("authorization", "") or ""
2877
+ provided = auth[7:].strip() if auth[:7].lower() == "bearer " else ""
2878
+ if not provided:
2879
+ provided = headers.get("x-mcp-token", "") or headers.get("x-api-key", "")
2880
+ if not provided:
2881
+ return False
2882
+ return _hmac.compare_digest(str(provided), str(expected))
2883
+
2884
+
2885
+ def _mcp_request_allowed(request) -> bool:
2886
+ """Per-request MCP authorisation gate applied by EVERY MCP surface
2887
+ (JSON-RPC, SSE, and the REST shim). Loopback callers are allowed; a
2888
+ non-loopback (remote) caller needs TINA4_MCP_REMOTE + a valid token.
2889
+ Uses the raw socket peer (request.remote_ip), never X-Forwarded-For."""
2890
+ from tina4_python.mcp import is_request_allowed
2891
+ remote_ip = getattr(request, "remote_ip", "") or ""
2892
+ return is_request_allowed(remote_ip, _mcp_token_ok(request))
2893
+
2894
+
2867
2895
  async def _api_mcp_tools(request, response):
2868
2896
  """GET — return the MCP tool registry as a plain JSON list.
2869
2897
 
2870
2898
  Shape matches what dev-admin's `listMcpTools()` expects:
2871
2899
  {"tools": [{"name": "...", "description": "...", "schema": {...}}, ...]}
2872
2900
  """
2901
+ if not _mcp_request_allowed(request):
2902
+ return response({"tools": [], "error": "MCP forbidden"}, 404)
2873
2903
  try:
2874
2904
  from tina4_python.mcp import _get_default_server
2875
2905
  server = _get_default_server()
@@ -2950,8 +2980,8 @@ async def _api_mcp_rpc(request, response):
2950
2980
  (no id) yield an empty 204.
2951
2981
  """
2952
2982
  import json as _json
2953
- from tina4_python.mcp import _get_default_server, is_enabled
2954
- if not is_enabled():
2983
+ from tina4_python.mcp import _get_default_server
2984
+ if not _mcp_request_allowed(request):
2955
2985
  return response({"error": "MCP disabled"}, 404)
2956
2986
  server = _get_default_server()
2957
2987
  body = request.body
@@ -2966,8 +2996,7 @@ async def _api_mcp_sse(request, response):
2966
2996
  """GET — SSE handshake. Emits the `endpoint` event telling the client
2967
2997
  where to POST JSON-RPC messages, per the MCP HTTP+SSE transport.
2968
2998
  """
2969
- from tina4_python.mcp import is_enabled
2970
- if not is_enabled():
2999
+ if not _mcp_request_allowed(request):
2971
3000
  return response({"error": "MCP disabled"}, 404)
2972
3001
  base = request.path.rsplit("/sse", 1)[0]
2973
3002
  sse = f"event: endpoint\ndata: {base}/message\n\n"