tina4-python 3.13.3__tar.gz → 3.13.4__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 (157) hide show
  1. {tina4_python-3.13.3 → tina4_python-3.13.4}/PKG-INFO +1 -1
  2. {tina4_python-3.13.3 → tina4_python-3.13.4}/pyproject.toml +1 -1
  3. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/core/request.py +52 -1
  5. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/core/router.py +11 -6
  6. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/core/server.py +87 -3
  7. {tina4_python-3.13.3 → tina4_python-3.13.4}/.gitignore +0 -0
  8. {tina4_python-3.13.3 → tina4_python-3.13.4}/README.md +0 -0
  9. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/CLAUDE.md +0 -0
  10. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/HtmlElement.py +0 -0
  11. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/Testing.py +0 -0
  12. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/ai/__init__.py +0 -0
  13. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/cli/__init__.py +0 -0
  17. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/container/__init__.py +0 -0
  18. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/core/__init__.py +0 -0
  19. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/core/rate_limiter.py +0 -0
  24. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/core/response.py +0 -0
  25. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/crud/__init__.py +0 -0
  26. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/database/__init__.py +0 -0
  27. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/database/adapter.py +0 -0
  28. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/database/connection.py +0 -0
  29. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/database/firebird.py +0 -0
  30. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/database/mongodb.py +0 -0
  31. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/database/mssql.py +0 -0
  32. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/database/mysql.py +0 -0
  33. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/database/odbc.py +0 -0
  34. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/database/postgres.py +0 -0
  35. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/database/sqlite.py +0 -0
  36. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/dev_admin/__init__.py +0 -0
  39. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/dev_admin/metrics.py +0 -0
  40. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/dev_admin/plan.py +0 -0
  41. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/dev_admin/project_index.py +0 -0
  42. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/docs.py +0 -0
  43. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/dotenv/__init__.py +0 -0
  44. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/env.py +0 -0
  45. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/frond/FROND.md +0 -0
  46. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/frond/__init__.py +0 -0
  47. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/frond/engine.py +0 -0
  48. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/auth/meta.json +0 -0
  49. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  50. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/database/meta.json +0 -0
  51. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  52. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/error-overlay/meta.json +0 -0
  53. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  54. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/orm/meta.json +0 -0
  55. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  56. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  57. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/queue/meta.json +0 -0
  58. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  59. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/rest-api/meta.json +0 -0
  60. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  61. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/templates/meta.json +0 -0
  62. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  63. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  64. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/graphql/__init__.py +0 -0
  65. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/i18n/__init__.py +0 -0
  66. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/mcp/__init__.py +0 -0
  67. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/mcp/protocol.py +0 -0
  68. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/mcp/tools.py +0 -0
  69. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/messenger/__init__.py +0 -0
  70. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/migration/__init__.py +0 -0
  71. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/migration/runner.py +0 -0
  72. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/orm/__init__.py +0 -0
  73. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/orm/fields.py +0 -0
  74. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/orm/model.py +0 -0
  75. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/__feedback/widget.js +0 -0
  76. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/js/frond.js +0 -0
  82. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/js/frond.min.js +0 -0
  83. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  84. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  85. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/js/tina4.min.js +0 -0
  86. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/js/tina4js.min.js +0 -0
  87. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/swagger/index.html +0 -0
  88. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  89. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/query_builder/__init__.py +0 -0
  90. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/queue/__init__.py +0 -0
  91. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/queue/job.py +0 -0
  92. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/queue/kafka_backend.py +0 -0
  93. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/queue/lite_backend.py +0 -0
  94. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/queue/mongo_backend.py +0 -0
  95. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/queue/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/queue_backends/__init__.py +0 -0
  97. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/queue_backends/kafka_backend.py +0 -0
  98. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/queue_backends/mongo_backend.py +0 -0
  99. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/__init__.py +0 -0
  101. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  102. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_badges.scss +0 -0
  103. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  104. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_cards.scss +0 -0
  105. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_forms.scss +0 -0
  106. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_grid.scss +0 -0
  107. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_modals.scss +0 -0
  108. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_nav.scss +0 -0
  109. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_reset.scss +0 -0
  110. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_tables.scss +0 -0
  111. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_typography.scss +0 -0
  112. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  113. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_variables.scss +0 -0
  114. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/base.scss +0 -0
  115. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/colors.scss +0 -0
  116. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/scss/tina4css/tina4.scss +0 -0
  117. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/seeder/__init__.py +0 -0
  118. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/service/__init__.py +0 -0
  119. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/session/__init__.py +0 -0
  120. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.3 → tina4_python-3.13.4}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.3 → tina4_python-3.13.4}/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.3
3
+ Version: 3.13.4
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.3"
3
+ version = "3.13.4"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -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.3"
11
+ __version__ = "3.13.4"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -15,6 +15,57 @@ class PayloadTooLarge(Exception):
15
15
  pass
16
16
 
17
17
 
18
+ class CaseInsensitiveDict(dict):
19
+ """Dict subclass for HTTP headers — string keys are case-insensitive.
20
+
21
+ HTTP header field-names are case-insensitive per RFC 7230 §3.2.
22
+ With this class, ``request.headers["Content-Type"]``,
23
+ ``request.headers.get("content-type")``, and
24
+ ``request.headers.get("CONTENT-TYPE")`` all return the same value.
25
+
26
+ Keys are stored lowercase internally. Cross-framework parity:
27
+ same behaviour now ships in tina4-php (Tina4\\Request),
28
+ tina4-ruby (Tina4::Request), tina4-nodejs (Tina4Request) so the
29
+ chapter 10 documented examples (``headers.get("Content-Type")``)
30
+ finally do what they read like. tina4-book#141 PY-10-03.
31
+ """
32
+
33
+ def __init__(self, *args, **kwargs):
34
+ super().__init__()
35
+ if args or kwargs:
36
+ self.update(dict(*args, **kwargs))
37
+
38
+ @staticmethod
39
+ def _norm(key):
40
+ return key.lower() if isinstance(key, str) else key
41
+
42
+ def __setitem__(self, key, value):
43
+ super().__setitem__(self._norm(key), value)
44
+
45
+ def __getitem__(self, key):
46
+ return super().__getitem__(self._norm(key))
47
+
48
+ def __contains__(self, key):
49
+ return super().__contains__(self._norm(key))
50
+
51
+ def __delitem__(self, key):
52
+ super().__delitem__(self._norm(key))
53
+
54
+ def get(self, key, default=None):
55
+ return super().get(self._norm(key), default)
56
+
57
+ def setdefault(self, key, default=None):
58
+ return super().setdefault(self._norm(key), default)
59
+
60
+ def pop(self, key, *args):
61
+ return super().pop(self._norm(key), *args)
62
+
63
+ def update(self, *args, **kwargs):
64
+ # Route through __setitem__ so every key gets normalised.
65
+ for k, v in dict(*args, **kwargs).items():
66
+ self[k] = v
67
+
68
+
18
69
  class Request:
19
70
  """Parsed HTTP request — everything a route handler needs."""
20
71
 
@@ -31,7 +82,7 @@ class Request:
31
82
  self.query_string: str = ""
32
83
  self.params: dict = {} # Query string + route params merged
33
84
  self.query: dict = {} # Query string params only (separate from route params)
34
- self.headers: dict = {} # Lowercase header keys
85
+ self.headers: dict = CaseInsensitiveDict() # Case-insensitive HTTP headers
35
86
  self.body: dict | str | None = None # Parsed body
36
87
  self.raw_body: bytes = b""
37
88
  self.cookies: dict = {}
@@ -687,7 +687,14 @@ def secured():
687
687
  # ── Middleware Decorator ───────────────────────────────────────
688
688
 
689
689
  def middleware(*middleware_classes):
690
- """Attach middleware classes to a route handler."""
690
+ """Attach middleware classes or functions to a route handler.
691
+
692
+ Middleware is purely additive — it does NOT change the route's auth
693
+ requirement. POST/PUT/PATCH/DELETE routes stay Bearer-token-gated by
694
+ default. Use ``@noauth()`` to open a write route, ``@secured()`` to
695
+ lock a read route. This rule is the same across all four Tina4
696
+ frameworks (tina4-book#141, PY-10-02).
697
+ """
691
698
  def decorator(fn):
692
699
  fn._middleware = list(middleware_classes)
693
700
  # If route was already registered (decorator applied after @get/@post),
@@ -695,11 +702,9 @@ def middleware(*middleware_classes):
695
702
  if hasattr(fn, "_route_ref"):
696
703
  existing = fn._route_ref._route.get("middleware", [])
697
704
  fn._route_ref._route["middleware"] = list(middleware_classes) + existing
698
- # Custom middleware means developer handles auth — disable built-in
699
- # gate unless @secured() was explicitly set.
700
- route = fn._route_ref._route
701
- if not hasattr(fn, "_secured") and route.get("auth_required"):
702
- route["auth_required"] = False
705
+ # Intentionally does NOT touch route["auth_required"]. Prior
706
+ # behaviour silently flipped it to False, creating an undocumented
707
+ # auth bypass for any write route that added custom middleware.
703
708
  return fn
704
709
  return decorator
705
710
 
@@ -1151,10 +1151,40 @@ def _check_auth(request: Request, response: Response, route: dict) -> bool:
1151
1151
  return False
1152
1152
 
1153
1153
 
1154
+ def _is_function_middleware(mw) -> bool:
1155
+ """A function-style middleware is a non-class callable that takes 3+ args.
1156
+
1157
+ Class-based middleware uses ``before_*`` / ``after_*`` method-name
1158
+ dispatch. Function-based middleware uses Express/FastAPI-style
1159
+ ``async def mw(request, response, next_handler)`` with a continuation
1160
+ callable that invokes the next layer (or the route handler).
1161
+ Documented in chapter 10 for 8+ examples; before tina4-book#141
1162
+ PY-10-01 the framework silently ignored the function bodies.
1163
+ """
1164
+ import inspect
1165
+ if isinstance(mw, type):
1166
+ return False # Class — class-based before_*/after_* dispatch
1167
+ if not callable(mw):
1168
+ return False
1169
+ try:
1170
+ sig = inspect.signature(mw)
1171
+ # The third (or later) parameter is the next_handler continuation.
1172
+ return len(sig.parameters) >= 3
1173
+ except (TypeError, ValueError):
1174
+ return False
1175
+
1176
+
1154
1177
  def _run_before_middleware(request: Request, response: Response, route: dict) -> tuple[Request, Response, bool]:
1155
- """Run before_* middleware methods. Returns (request, response, skip_handler)."""
1178
+ """Run class-based before_* middleware methods. Returns (request, response, skip_handler).
1179
+
1180
+ Function-style ``async def mw(req, resp, next_handler)`` middleware is
1181
+ skipped here — it's handled by ``_invoke_handler_with_middleware``
1182
+ which wraps the route handler with each function's continuation.
1183
+ """
1156
1184
  skip = False
1157
1185
  for _mw_cls in route.get("middleware", []):
1186
+ if _is_function_middleware(_mw_cls):
1187
+ continue # Handled by the continuation wrapper instead
1158
1188
  _mw_inst = _mw_cls() if isinstance(_mw_cls, type) else _mw_cls
1159
1189
  for _attr_name in dir(_mw_inst):
1160
1190
  if _attr_name.startswith("before_"):
@@ -1172,8 +1202,15 @@ def _run_before_middleware(request: Request, response: Response, route: dict) ->
1172
1202
 
1173
1203
 
1174
1204
  def _run_after_middleware(request: Request, response: Response, route: dict) -> tuple[Request, Response]:
1175
- """Run after_* middleware methods."""
1205
+ """Run class-based after_* middleware methods.
1206
+
1207
+ Function-style middleware that wraps the handler (with next_handler)
1208
+ handles its own "after" code on the return path — no separate pass
1209
+ needed for it here.
1210
+ """
1176
1211
  for _mw_cls in route.get("middleware", []):
1212
+ if _is_function_middleware(_mw_cls):
1213
+ continue
1177
1214
  _mw_inst = _mw_cls() if isinstance(_mw_cls, type) else _mw_cls
1178
1215
  for _attr_name in dir(_mw_inst):
1179
1216
  if _attr_name.startswith("after_"):
@@ -1185,6 +1222,53 @@ def _run_after_middleware(request: Request, response: Response, route: dict) ->
1185
1222
  return request, response
1186
1223
 
1187
1224
 
1225
+ def _make_mw_continuation(mw, inner_next):
1226
+ """Build a `next_handler` continuation for a function-style middleware.
1227
+
1228
+ Captures `mw` and the next layer (`inner_next`) in a closure. When
1229
+ invoked, calls `mw(req, resp, next_handler=inner_next)`. The
1230
+ middleware is responsible for awaiting `inner_next(req, resp)` if it
1231
+ wants the chain to continue — otherwise it short-circuits.
1232
+ """
1233
+ async def wrapper(req, resp):
1234
+ return await mw(req, resp, inner_next)
1235
+ return wrapper
1236
+
1237
+
1238
+ async def _invoke_handler_with_middleware(request: Request, response: Response, route: dict, params: dict) -> Response:
1239
+ """Invoke the route handler, wrapping with any function-style middleware.
1240
+
1241
+ Function-style middleware (``async def mw(req, resp, next_handler)``)
1242
+ forms a Russian-doll continuation chain — the first declared
1243
+ middleware is the outermost layer; it calls ``next_handler`` to
1244
+ descend, and the route handler is the innermost. Class-based
1245
+ middleware is dispatched separately by ``_run_before_middleware`` /
1246
+ ``_run_after_middleware`` and does not go through this wrapper.
1247
+ """
1248
+ fn_middlewares = [
1249
+ mw for mw in route.get("middleware", [])
1250
+ if _is_function_middleware(mw)
1251
+ ]
1252
+
1253
+ if not fn_middlewares:
1254
+ return await _invoke_handler(request, response, route, params)
1255
+
1256
+ # Build the chain from the inside out: innermost wraps the handler
1257
+ # first, then each outer layer wraps that. The result is a callable
1258
+ # `next_handler` that, when invoked, runs the whole chain.
1259
+ async def call_route_handler(req, resp):
1260
+ return await _invoke_handler(req, resp, route, params)
1261
+
1262
+ next_handler = call_route_handler
1263
+ for mw in reversed(fn_middlewares):
1264
+ next_handler = _make_mw_continuation(mw, next_handler)
1265
+
1266
+ result = await next_handler(request, response)
1267
+ if isinstance(result, Response):
1268
+ return result
1269
+ return response
1270
+
1271
+
1188
1272
  async def _invoke_handler(request: Request, response: Response, route: dict, params: dict) -> Response:
1189
1273
  """Call the route handler with the correct arguments."""
1190
1274
  import inspect
@@ -1430,7 +1514,7 @@ async def handle(request: Request) -> Response:
1430
1514
  if not skip:
1431
1515
  request, response, skip = _run_before_middleware(request, response, route)
1432
1516
  if not skip:
1433
- response = await _invoke_handler(request, response, route, params)
1517
+ response = await _invoke_handler_with_middleware(request, response, route, params)
1434
1518
  request, response = _run_after_middleware(request, response, route)
1435
1519
  except Exception as e:
1436
1520
  response = _handle_route_error(e, request, response, request_id, _is_dev)
File without changes
File without changes