tina4-python 3.13.47__tar.gz → 3.13.48__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 (158) hide show
  1. {tina4_python-3.13.47 → tina4_python-3.13.48}/PKG-INFO +1 -1
  2. {tina4_python-3.13.47 → tina4_python-3.13.48}/pyproject.toml +1 -1
  3. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/i18n/__init__.py +59 -20
  5. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/swagger/__init__.py +14 -82
  6. {tina4_python-3.13.47 → tina4_python-3.13.48}/.gitignore +0 -0
  7. {tina4_python-3.13.47 → tina4_python-3.13.48}/README.md +0 -0
  8. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/CLAUDE.md +0 -0
  9. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/HtmlElement.py +0 -0
  10. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/Testing.py +0 -0
  11. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/ai/__init__.py +0 -0
  12. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/api/__init__.py +0 -0
  13. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/auth/__init__.py +0 -0
  14. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/cache/__init__.py +0 -0
  15. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/cli/__init__.py +0 -0
  16. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/container/__init__.py +0 -0
  17. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/core/__init__.py +0 -0
  18. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/core/cache.py +0 -0
  19. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/core/constants.py +0 -0
  20. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/core/events.py +0 -0
  21. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/core/middleware.py +0 -0
  22. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/core/rate_limiter.py +0 -0
  23. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/core/request.py +0 -0
  24. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/core/response.py +0 -0
  25. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/core/router.py +0 -0
  26. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/core/server.py +0 -0
  27. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/crud/__init__.py +0 -0
  28. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/database/__init__.py +0 -0
  29. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/database/adapter.py +0 -0
  30. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/database/connection.py +0 -0
  31. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/database/firebird.py +0 -0
  32. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/database/mongodb.py +0 -0
  33. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/database/mssql.py +0 -0
  34. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/database/mysql.py +0 -0
  35. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/database/odbc.py +0 -0
  36. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/database/postgres.py +0 -0
  37. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/database/sqlite.py +0 -0
  38. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/debug/__init__.py +0 -0
  39. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/debug/error_overlay.py +0 -0
  40. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/dev_admin/__init__.py +0 -0
  41. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/dev_admin/metrics.py +0 -0
  42. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/dev_admin/plan.py +0 -0
  43. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/dev_admin/project_index.py +0 -0
  44. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/docs.py +0 -0
  45. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/docstore/__init__.py +0 -0
  46. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/dotenv/__init__.py +0 -0
  47. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/env.py +0 -0
  48. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/frond/FROND.md +0 -0
  49. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/frond/__init__.py +0 -0
  50. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/frond/engine.py +0 -0
  51. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/auth/meta.json +0 -0
  52. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  53. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/database/meta.json +0 -0
  54. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  55. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/error-overlay/meta.json +0 -0
  56. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  57. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/orm/meta.json +0 -0
  58. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  59. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  60. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/queue/meta.json +0 -0
  61. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  62. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/rest-api/meta.json +0 -0
  63. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  64. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/templates/meta.json +0 -0
  65. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  66. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  67. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/graphql/__init__.py +0 -0
  68. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/mcp/__init__.py +0 -0
  69. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/mcp/protocol.py +0 -0
  70. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/mcp/tools.py +0 -0
  71. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/messenger/__init__.py +0 -0
  72. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/migration/__init__.py +0 -0
  73. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/migration/runner.py +0 -0
  74. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/orm/__init__.py +0 -0
  75. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/orm/fields.py +0 -0
  76. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/orm/model.py +0 -0
  77. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/__feedback/widget.js +0 -0
  78. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/css/tina4.css +0 -0
  79. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/css/tina4.min.css +0 -0
  80. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/favicon.ico +0 -0
  81. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/images/logo.svg +0 -0
  82. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  83. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/js/frond.js +0 -0
  84. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/js/frond.min.js +0 -0
  85. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  86. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  87. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/js/tina4.min.js +0 -0
  88. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/js/tina4js.min.js +0 -0
  89. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/swagger/index.html +0 -0
  90. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  91. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/query_builder/__init__.py +0 -0
  92. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/queue/__init__.py +0 -0
  93. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/queue/job.py +0 -0
  94. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/queue/kafka_backend.py +0 -0
  95. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/queue/lite_backend.py +0 -0
  96. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/queue/mongo_backend.py +0 -0
  97. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/queue/rabbitmq_backend.py +0 -0
  98. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/queue_backends/__init__.py +0 -0
  99. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/queue_backends/kafka_backend.py +0 -0
  100. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/queue_backends/mongo_backend.py +0 -0
  101. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  102. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/__init__.py +0 -0
  103. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  104. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_badges.scss +0 -0
  105. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  106. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_cards.scss +0 -0
  107. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_forms.scss +0 -0
  108. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_grid.scss +0 -0
  109. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_modals.scss +0 -0
  110. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_nav.scss +0 -0
  111. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_reset.scss +0 -0
  112. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_tables.scss +0 -0
  113. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_typography.scss +0 -0
  114. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  115. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/_variables.scss +0 -0
  116. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/base.scss +0 -0
  117. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/colors.scss +0 -0
  118. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/scss/tina4css/tina4.scss +0 -0
  119. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/seeder/__init__.py +0 -0
  120. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/service/__init__.py +0 -0
  121. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/session/__init__.py +0 -0
  122. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/session_handlers/__init__.py +0 -0
  123. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  124. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/session_handlers/redis_handler.py +0 -0
  125. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/session_handlers/valkey_handler.py +0 -0
  126. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/components/crud.twig +0 -0
  127. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  128. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  129. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/docker/python/Dockerfile +0 -0
  130. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  131. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/errors/302.twig +0 -0
  132. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/errors/401.twig +0 -0
  133. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/errors/403.twig +0 -0
  134. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/errors/404.twig +0 -0
  135. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/errors/500.twig +0 -0
  136. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/errors/502.twig +0 -0
  137. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/errors/503.twig +0 -0
  138. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/errors/base.twig +0 -0
  139. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/frontend/README.md +0 -0
  140. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/templates/readme.md +0 -0
  141. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/test/__init__.py +0 -0
  142. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/test_client/__init__.py +0 -0
  143. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  144. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  145. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  146. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  147. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  148. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  149. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  150. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  151. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  152. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  153. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/validator/__init__.py +0 -0
  156. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/websocket/__init__.py +0 -0
  157. {tina4_python-3.13.47 → tina4_python-3.13.48}/tina4_python/websocket/backplane.py +0 -0
  158. {tina4_python-3.13.47 → tina4_python-3.13.48}/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.47
3
+ Version: 3.13.48
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.47"
3
+ version = "3.13.48"
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.47"
11
+ __version__ = "3.13.48"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -9,10 +9,27 @@ Simple key-based translations loaded from JSON files.
9
9
  _("greeting") # "Hello" or "Bonjour" depending on locale
10
10
  """
11
11
  import os
12
+ import re
12
13
  import json
13
14
  from pathlib import Path
14
15
 
15
16
 
17
+ _PLACEHOLDER = re.compile(r"\{(\w+)\}")
18
+
19
+
20
+ def _interpolate(template: str, params: dict) -> str:
21
+ """Substitute {name} placeholders from params.
22
+
23
+ Partial + literal-leftover: a placeholder present in params is replaced; a
24
+ missing or malformed placeholder ({x.y}, {n:d}, a lone brace) is left
25
+ untouched. Never raises -- a bad template must not crash t().
26
+ """
27
+ return _PLACEHOLDER.sub(
28
+ lambda m: str(params[m.group(1)]) if m.group(1) in params else m.group(0),
29
+ template,
30
+ )
31
+
32
+
16
33
  class I18n:
17
34
  """Internationalization support with JSON translation files.
18
35
 
@@ -61,12 +78,11 @@ class I18n:
61
78
  if value is None:
62
79
  value = key
63
80
 
64
- # Interpolate
81
+ # Interpolate {placeholder} tokens. Partial substitution: each token
82
+ # present in kwargs is replaced; a missing or malformed placeholder is
83
+ # left literal. Never raises (a bad template must not crash t()).
65
84
  if kwargs:
66
- try:
67
- value = value.format(**kwargs)
68
- except (KeyError, IndexError):
69
- pass
85
+ value = _interpolate(value, kwargs)
70
86
 
71
87
  return value
72
88
 
@@ -86,9 +102,10 @@ class I18n:
86
102
  if locale:
87
103
  old = self._current_locale
88
104
  self.locale = locale
89
- result = self.t(key, **(params or {}))
90
- self.locale = old
91
- return result
105
+ try:
106
+ return self.t(key, **(params or {}))
107
+ finally:
108
+ self.locale = old
92
109
  return self.t(key, **(params or {}))
93
110
 
94
111
  def load_translations(self, locale: str) -> dict:
@@ -183,28 +200,50 @@ class I18n:
183
200
 
184
201
  @staticmethod
185
202
  def _flatten(data: dict, prefix: str = "") -> dict:
186
- """Flatten nested dict with leaf-key aliasing.
203
+ """Flatten a nested dict to dot-paths, then add leaf-key aliases.
187
204
 
188
- {"nav": {"home": "Home"}} {"nav.home": "Home", "home": "Home"}
205
+ {"nav": {"home": "Home"}} -> {"nav.home": "Home", "home": "Home"}
189
206
 
190
- Both the full dot-path AND the leaf key are stored, so templates
191
- can use either ``t("home")`` or ``t("nav.home")``. If two
192
- sections define the same leaf key, the full dot-path always works
193
- and the leaf key keeps whichever value was seen first.
207
+ Two passes so the alias rule is correct:
208
+ 1. Flatten to full dot-path keys only.
209
+ 2. Add each leaf key as a shortcut ONLY if it is not already present.
210
+
211
+ So the FIRST dot-path wins on a leaf-key collision, and an explicit
212
+ top-level flat key is never overwritten by a derived alias. (The old
213
+ single-pass recursive merge was last-wins and could clobber an
214
+ explicit flat key -- silent data loss.)
194
215
  """
216
+ flat = I18n._flatten_paths(data, prefix)
217
+ result = dict(flat)
218
+ for full_key, value in flat.items():
219
+ leaf = full_key.rsplit(".", 1)[-1]
220
+ if leaf not in result:
221
+ result[leaf] = value
222
+ return result
223
+
224
+ @staticmethod
225
+ def _flatten_paths(data: dict, prefix: str = "") -> dict:
226
+ """Flatten a nested dict to dot-path keys only (no leaf aliasing)."""
195
227
  result = {}
196
228
  for key, value in data.items():
197
229
  full_key = f"{prefix}.{key}" if prefix else key
198
230
  if isinstance(value, dict):
199
- result.update(I18n._flatten(value, full_key))
231
+ result.update(I18n._flatten_paths(value, full_key))
200
232
  else:
201
- str_value = str(value)
202
- result[full_key] = str_value
203
- # Also store the leaf key as a shortcut (first-wins on conflict)
204
- if key not in result:
205
- result[key] = str_value
233
+ result[full_key] = I18n._coerce_scalar(value)
206
234
  return result
207
235
 
236
+ @staticmethod
237
+ def _coerce_scalar(value) -> str:
238
+ """Render a non-string locale scalar JSON-natively (true/false/null)."""
239
+ if value is True:
240
+ return "true"
241
+ if value is False:
242
+ return "false"
243
+ if value is None:
244
+ return "null"
245
+ return str(value)
246
+
208
247
  @staticmethod
209
248
  def _resolve(key: str, translations: dict) -> str | None:
210
249
  return translations.get(key)
@@ -39,23 +39,11 @@ v3.13.42 — configurability for external/public APIs:
39
39
  import json
40
40
  import os
41
41
  import re
42
- import functools
43
42
 
44
43
 
45
44
  # ── Decorators ─────────────────────────────────────────────────
46
45
  # These attach metadata to route handlers for Swagger generation.
47
46
 
48
- # Every swagger attr a decorator may need to carry forward when it wraps a
49
- # handler that another decorator already annotated.
50
- _SWAGGER_ATTRS = (
51
- "_swagger_description", "_swagger_detail", "_swagger_params", "_swagger_query",
52
- "_swagger_summary", "_swagger_tags", "_swagger_example", "_swagger_example_content_type",
53
- "_swagger_example_response", "_swagger_example_responses", "_swagger_deprecated",
54
- "_swagger_model", "_swagger_model_list",
55
- "_swagger_security", "_swagger_request_schema", "_swagger_response_schemas",
56
- )
57
-
58
-
59
47
  # ── Configuration registry ─────────────────────────────────────
60
48
  # Process-wide registries for security schemes and reusable component schemas
61
49
  # declared programmatically (Swagger.add_security_scheme / Swagger.add_schema).
@@ -65,13 +53,6 @@ _REGISTERED_SCHEMES: dict[str, dict] = {}
65
53
  _REGISTERED_SCHEMAS: dict[str, dict] = {}
66
54
 
67
55
 
68
- def _carry(fn, wrapper):
69
- """Copy any already-set swagger attrs from fn onto wrapper (idempotent)."""
70
- for attr in _SWAGGER_ATTRS:
71
- if hasattr(fn, attr) and not hasattr(wrapper, attr):
72
- setattr(wrapper, attr, getattr(fn, attr))
73
-
74
-
75
56
  def description(text: str = "", detail: str = "", params: dict | None = None,
76
57
  query: dict | None = None):
77
58
  """Add a description, optional detail body, and parameter docs to a route.
@@ -99,19 +80,12 @@ def description(text: str = "", detail: str = "", params: dict | None = None,
99
80
  fn._swagger_params = params
100
81
  if query:
101
82
  fn._swagger_query = query
102
-
103
- @functools.wraps(fn)
104
- def wrapper(*args, **kwargs):
105
- return fn(*args, **kwargs)
106
- wrapper._swagger_description = text
107
- if detail:
108
- wrapper._swagger_detail = detail
109
- if params:
110
- wrapper._swagger_params = params
111
- if query:
112
- wrapper._swagger_query = query
113
- _carry(fn, wrapper)
114
- return wrapper
83
+ # No wrapper: annotate the handler in place and return the SAME object.
84
+ # @get is innermost and registers this exact object, so every stacked
85
+ # decorator must land its metadata here. A wrapper would be registered
86
+ # by nothing (the outer decorators never reach the router) and silently
87
+ # drop all metadata except the decorator adjacent to @get.
88
+ return fn
115
89
  return decorator
116
90
 
117
91
 
@@ -119,12 +93,7 @@ def summary(text: str):
119
93
  """Add a short summary to a route handler."""
120
94
  def decorator(fn):
121
95
  fn._swagger_summary = text
122
- @functools.wraps(fn)
123
- def wrapper(*args, **kwargs):
124
- return fn(*args, **kwargs)
125
- wrapper._swagger_summary = text
126
- _carry(fn, wrapper)
127
- return wrapper
96
+ return fn
128
97
  return decorator
129
98
 
130
99
 
@@ -140,12 +109,7 @@ def tags(tag_list):
140
109
 
141
110
  def decorator(fn):
142
111
  fn._swagger_tags = tag_list
143
- @functools.wraps(fn)
144
- def wrapper(*args, **kwargs):
145
- return fn(*args, **kwargs)
146
- wrapper._swagger_tags = tag_list
147
- _carry(fn, wrapper)
148
- return wrapper
112
+ return fn
149
113
  return decorator
150
114
 
151
115
 
@@ -160,13 +124,7 @@ def example(data: dict | list, content_type: str = "application/json"):
160
124
  def decorator(fn):
161
125
  fn._swagger_example = data
162
126
  fn._swagger_example_content_type = content_type
163
- @functools.wraps(fn)
164
- def wrapper(*args, **kwargs):
165
- return fn(*args, **kwargs)
166
- wrapper._swagger_example = data
167
- wrapper._swagger_example_content_type = content_type
168
- _carry(fn, wrapper)
169
- return wrapper
127
+ return fn
170
128
  return decorator
171
129
 
172
130
 
@@ -198,13 +156,7 @@ def example_response(status_or_data, data=None):
198
156
  responses[status_code] = body
199
157
  fn._swagger_example_responses = responses
200
158
  fn._swagger_example_response = body
201
- @functools.wraps(fn)
202
- def wrapper(*args, **kwargs):
203
- return fn(*args, **kwargs)
204
- wrapper._swagger_example_responses = responses
205
- wrapper._swagger_example_response = body
206
- _carry(fn, wrapper)
207
- return wrapper
159
+ return fn
208
160
  return decorator
209
161
 
210
162
 
@@ -212,12 +164,7 @@ def deprecated():
212
164
  """Mark a route as deprecated."""
213
165
  def decorator(fn):
214
166
  fn._swagger_deprecated = True
215
- @functools.wraps(fn)
216
- def wrapper(*args, **kwargs):
217
- return fn(*args, **kwargs)
218
- wrapper._swagger_deprecated = True
219
- _carry(fn, wrapper)
220
- return wrapper
167
+ return fn
221
168
  return decorator
222
169
 
223
170
 
@@ -257,12 +204,7 @@ def security(scheme_or_reqs="bearerAuth", scopes=None):
257
204
  reqs = _normalize_security(scheme_or_reqs, scopes)
258
205
  def decorator(fn):
259
206
  fn._swagger_security = reqs
260
- @functools.wraps(fn)
261
- def wrapper(*args, **kwargs):
262
- return fn(*args, **kwargs)
263
- wrapper._swagger_security = reqs
264
- _carry(fn, wrapper)
265
- return wrapper
207
+ return fn
266
208
  return decorator
267
209
 
268
210
 
@@ -274,12 +216,7 @@ def request_schema(name: str, content_type: str = "application/json"):
274
216
  """
275
217
  def decorator(fn):
276
218
  fn._swagger_request_schema = (name, content_type)
277
- @functools.wraps(fn)
278
- def wrapper(*args, **kwargs):
279
- return fn(*args, **kwargs)
280
- wrapper._swagger_request_schema = (name, content_type)
281
- _carry(fn, wrapper)
282
- return wrapper
219
+ return fn
283
220
  return decorator
284
221
 
285
222
 
@@ -294,12 +231,7 @@ def response_schema(name: str, status: int = 200, is_list: bool = False):
294
231
  existing = dict(getattr(fn, "_swagger_response_schemas", {}) or {})
295
232
  existing[int(status)] = (name, bool(is_list))
296
233
  fn._swagger_response_schemas = existing
297
- @functools.wraps(fn)
298
- def wrapper(*args, **kwargs):
299
- return fn(*args, **kwargs)
300
- wrapper._swagger_response_schemas = existing
301
- _carry(fn, wrapper)
302
- return wrapper
234
+ return fn
303
235
  return decorator
304
236
 
305
237
 
File without changes