tina4-python 3.13.132__tar.gz → 3.13.133__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 (171) hide show
  1. {tina4_python-3.13.132 → tina4_python-3.13.133}/PKG-INFO +1 -1
  2. {tina4_python-3.13.132 → tina4_python-3.13.133}/pyproject.toml +1 -1
  3. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/swagger/__init__.py +227 -175
  5. {tina4_python-3.13.132 → tina4_python-3.13.133}/.gitignore +0 -0
  6. {tina4_python-3.13.132 → tina4_python-3.13.133}/README.md +0 -0
  7. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/CLAUDE.md +0 -0
  8. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/HtmlElement.py +0 -0
  9. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/Testing.py +0 -0
  10. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/_import_helper.py +0 -0
  11. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/ai/__init__.py +0 -0
  12. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/ai/client.py +0 -0
  13. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/cli/__init__.py +0 -0
  17. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/container/__init__.py +0 -0
  18. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/context/__init__.py +0 -0
  19. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/context/chunker.py +0 -0
  20. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/__init__.py +0 -0
  21. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/cache.py +0 -0
  22. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/constants.py +0 -0
  23. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/events.py +0 -0
  24. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/middleware.py +0 -0
  25. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/port_takeover.py +0 -0
  26. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/rate_limiter.py +0 -0
  27. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/request.py +0 -0
  28. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/response.py +0 -0
  29. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/router.py +0 -0
  30. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/core/server.py +0 -0
  31. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/crud/__init__.py +0 -0
  32. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/__init__.py +0 -0
  33. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/adapter.py +0 -0
  34. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/connection.py +0 -0
  35. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/database_url.py +0 -0
  36. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/firebird.py +0 -0
  37. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/mongodb.py +0 -0
  38. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/mssql.py +0 -0
  39. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/mysql.py +0 -0
  40. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/odbc.py +0 -0
  41. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/postgres.py +0 -0
  42. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/sql_translator.py +0 -0
  43. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/database/sqlite.py +0 -0
  44. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/debug/__init__.py +0 -0
  45. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/debug/error_overlay.py +0 -0
  46. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/dev_admin/__init__.py +0 -0
  47. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/dev_admin/metrics.py +0 -0
  48. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/dev_admin/plan.py +0 -0
  49. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/dev_admin/project_index.py +0 -0
  50. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/docs.py +0 -0
  51. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/docstore/__init__.py +0 -0
  52. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/dotenv/__init__.py +0 -0
  53. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/env.py +0 -0
  54. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/frond/FROND.md +0 -0
  55. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/frond/__init__.py +0 -0
  56. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/frond/compiler.py +0 -0
  57. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/frond/engine.py +0 -0
  58. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/frond/parser.py +0 -0
  59. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/auth/meta.json +0 -0
  60. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  61. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/database/meta.json +0 -0
  62. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  63. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/error-overlay/meta.json +0 -0
  64. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  65. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/orm/meta.json +0 -0
  66. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  67. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  68. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/queue/meta.json +0 -0
  69. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  70. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/rest-api/meta.json +0 -0
  71. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  72. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/templates/meta.json +0 -0
  73. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  74. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  75. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/graph/__init__.py +0 -0
  76. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/graph/adapter.py +0 -0
  77. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/graph/adapters/__init__.py +0 -0
  78. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/graph/adapters/arango.py +0 -0
  79. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/graph/adapters/bolt.py +0 -0
  80. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/graph/adapters/ultipa.py +0 -0
  81. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/graph/graph_url.py +0 -0
  82. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/graphql/__init__.py +0 -0
  83. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/i18n/__init__.py +0 -0
  84. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/mcp/__init__.py +0 -0
  85. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/mcp/protocol.py +0 -0
  86. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/mcp/tools.py +0 -0
  87. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/messenger/__init__.py +0 -0
  88. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/migration/__init__.py +0 -0
  89. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/migration/runner.py +0 -0
  90. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/mqtt/__init__.py +0 -0
  91. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/mqtt/message.py +0 -0
  92. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/orm/__init__.py +0 -0
  93. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/orm/collection.py +0 -0
  94. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/orm/fields.py +0 -0
  95. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/orm/model.py +0 -0
  96. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/orm/point.py +0 -0
  97. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/__feedback/widget.js +0 -0
  98. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/css/tina4.css +0 -0
  99. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/css/tina4.min.css +0 -0
  100. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/favicon.ico +0 -0
  101. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/images/logo.svg +0 -0
  102. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  103. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/js/frond.js +0 -0
  104. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/js/frond.min.js +0 -0
  105. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  106. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/js/tina4.min.js +0 -0
  107. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/js/tina4js.min.js +0 -0
  108. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/swagger/index.html +0 -0
  109. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  110. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/query_builder/__init__.py +0 -0
  111. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue/__init__.py +0 -0
  112. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue/amqp_url.py +0 -0
  113. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue/job.py +0 -0
  114. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue/kafka_backend.py +0 -0
  115. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue/lite_backend.py +0 -0
  116. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue/mongo_backend.py +0 -0
  117. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue/rabbitmq_backend.py +0 -0
  118. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue_backends/__init__.py +0 -0
  119. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue_backends/kafka_backend.py +0 -0
  120. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue_backends/mongo_backend.py +0 -0
  121. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  122. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/realtime/__init__.py +0 -0
  123. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/realtime/models/Attachment.py +0 -0
  124. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/realtime/models/Channel.py +0 -0
  125. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/realtime/models/ChannelMember.py +0 -0
  126. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/realtime/models/Message.py +0 -0
  127. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/realtime/models/Workspace.py +0 -0
  128. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/realtime/models/__init__.py +0 -0
  129. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/realtime/storage.py +0 -0
  130. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/seeder/__init__.py +0 -0
  131. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/service/__init__.py +0 -0
  132. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/session/__init__.py +0 -0
  133. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/session_handlers/__init__.py +0 -0
  134. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/session_handlers/memcached_handler.py +0 -0
  135. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  136. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/session_handlers/redis_handler.py +0 -0
  137. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/session_handlers/valkey_handler.py +0 -0
  138. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/sso.py +0 -0
  139. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/components/crud.twig +0 -0
  140. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  141. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  142. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/docker/python/Dockerfile +0 -0
  143. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  144. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/errors/302.twig +0 -0
  145. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/errors/401.twig +0 -0
  146. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/errors/403.twig +0 -0
  147. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/errors/404.twig +0 -0
  148. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/errors/500.twig +0 -0
  149. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/errors/502.twig +0 -0
  150. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/errors/503.twig +0 -0
  151. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/errors/base.twig +0 -0
  152. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/frontend/README.md +0 -0
  153. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/templates/readme.md +0 -0
  154. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/test/__init__.py +0 -0
  155. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/test_client/__init__.py +0 -0
  156. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  157. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  158. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  159. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  160. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  161. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  162. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  163. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  164. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  165. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  166. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  167. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  168. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/validator/__init__.py +0 -0
  169. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/websocket/__init__.py +0 -0
  170. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/websocket/backplane.py +0 -0
  171. {tina4_python-3.13.132 → tina4_python-3.13.133}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tina4-python
3
- Version: 3.13.132
3
+ Version: 3.13.133
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.132"
3
+ version = "3.13.133"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -51,7 +51,7 @@ def _resolve_version() -> str:
51
51
  #
52
52
  # test_version_constant.py now asserts this literal equals the pyproject
53
53
  # version, so the release bump cannot leave it behind again.
54
- return "3.13.132"
54
+ return "3.13.133"
55
55
 
56
56
 
57
57
  __version__ = _resolve_version()
@@ -469,27 +469,10 @@ class Swagger:
469
469
  Each route dict should have:
470
470
  method, path, handler, auth_required (optional)
471
471
  """
472
- info = {
473
- "title": self.title,
474
- "version": self.version,
475
- "description": self.description,
476
- }
477
- contact = {}
478
- if self.contact_team:
479
- contact["name"] = self.contact_team
480
- if self.contact_url:
481
- contact["url"] = self.contact_url
482
- if self.contact_email:
483
- contact["email"] = self.contact_email
484
- if contact:
485
- info["contact"] = contact
486
- if self.license_name:
487
- info["license"] = {"name": self.license_name}
488
-
489
472
  schemes = self._security_schemes()
490
473
  spec = {
491
474
  "openapi": self.openapi_version,
492
- "info": info,
475
+ "info": self._build_info(),
493
476
  "servers": self._servers(),
494
477
  "paths": {},
495
478
  "components": {
@@ -507,182 +490,251 @@ class Swagger:
507
490
  continue
508
491
  path = self._openapi_path(route["path"])
509
492
  method = route["method"].lower()
510
- handler = route.get("handler")
511
-
512
493
  if path not in spec["paths"]:
513
494
  spec["paths"][path] = {}
514
495
 
515
- operation = {
516
- "operationId": self._unique_operation_id(method, path, seen_ids),
517
- "responses": {
518
- "200": {"description": "Successful response"},
519
- },
520
- }
496
+ spec["paths"][path][method] = self._build_operation(
497
+ route, path, method, schemes, models, ref_schemas, used_tags, seen_ids
498
+ )
521
499
 
522
- model = getattr(handler, "_swagger_model", None) if handler else None
523
- ref = None
524
- if model is not None and hasattr(model, "_fields"):
525
- models[model.__name__] = model
526
- ref = f"#/components/schemas/{model.__name__}"
527
-
528
- # summary/tags are ALWAYS populated (SWAG-SHAPE-DRIFT, ADR-0004) —
529
- # PHP/Ruby/Node never omit them, so an undecorated route used to be
530
- # the one shape Python left out. An explicit @summary/@tags wins;
531
- # otherwise a "METHOD /path" summary and a first-path-segment tag
532
- # match the other three frameworks' fallback.
533
- operation["summary"] = (
534
- getattr(handler, "_swagger_summary", None) if handler else None
535
- ) or f"{method.upper()} {route['path']}"
536
- op_tags = (
537
- getattr(handler, "_swagger_tags", None) if handler else None
538
- ) or [self._infer_tag(route["path"])]
539
- operation["tags"] = op_tags
540
- for t in op_tags:
541
- if t not in used_tags:
542
- used_tags.append(t)
543
-
544
- # Extract metadata from handler decorators
545
- if handler:
546
- desc = getattr(handler, "_swagger_description", None)
547
- detail = getattr(handler, "_swagger_detail", None)
548
- if desc is not None or detail:
549
- operation["description"] = "\n\n".join(
550
- p for p in (desc, detail) if p
551
- )
552
- if hasattr(handler, "_swagger_deprecated"):
553
- operation["deprecated"] = True
554
-
555
- # Request body — a $ref to the model schema (preferred), else
556
- # an inferred schema from @example. application/json unless the
557
- # example declares multipart/form-data.
558
- if method in ("post", "put", "patch"):
559
- req_schema = getattr(handler, "_swagger_request_schema", None)
560
- ct = getattr(handler, "_swagger_example_content_type", "application/json")
561
- ex = getattr(handler, "_swagger_example", None)
562
- media: dict = {}
563
- if req_schema is not None:
564
- sname, sct = req_schema
565
- ct = sct or ct
566
- ref_schemas.add(sname)
567
- media["schema"] = {"$ref": f"#/components/schemas/{sname}"}
568
- elif ref is not None:
569
- media["schema"] = {"$ref": ref}
570
- elif ex is not None:
571
- media["schema"] = self._infer_schema(ex)
572
- if ex is not None:
573
- media["example"] = ex
574
- if media:
575
- operation["requestBody"] = {"content": {ct: media}}
576
-
577
- # Responses: model $ref (single or list), then any per-status
578
- # @example_response entries (explicit wins).
579
- if ref is not None:
580
- is_list = getattr(handler, "_swagger_model_list", False)
581
- schema = ({"type": "array", "items": {"$ref": ref}} if is_list
582
- else {"$ref": ref})
583
- operation["responses"]["200"] = {
584
- "description": "Successful response",
585
- "content": {"application/json": {"schema": schema}},
586
- }
587
- resp_examples = getattr(handler, "_swagger_example_responses", None)
588
- if resp_examples:
589
- for status_code, body in resp_examples.items():
590
- operation["responses"][str(status_code)] = {
591
- "description": "Successful response" if str(status_code).startswith("2") else "Response",
592
- "content": {
593
- "application/json": {
594
- "schema": self._infer_schema(body),
595
- "example": body,
596
- }
597
- },
500
+ self._build_component_schemas(spec, models, ref_schemas)
501
+
502
+ # top-level tags[] (name-only is valid OpenAPI; descriptions optional)
503
+ if used_tags:
504
+ spec["tags"] = [{"name": t} for t in used_tags]
505
+
506
+ return spec
507
+
508
+ # ── generate() building blocks ─────────────────────────────────
509
+ # generate() is a thin orchestrator; each cohesive slice of the spec
510
+ # (info block, one path operation, components) is built by one helper so
511
+ # the top-level function stays readable. The emitted document is unchanged.
512
+
513
+ def _build_info(self) -> dict:
514
+ """Build the OpenAPI info block: title/version/description plus the
515
+ optional contact and license blocks (each key present only when set)."""
516
+ info = {
517
+ "title": self.title,
518
+ "version": self.version,
519
+ "description": self.description,
520
+ }
521
+ contact = {}
522
+ if self.contact_team:
523
+ contact["name"] = self.contact_team
524
+ if self.contact_url:
525
+ contact["url"] = self.contact_url
526
+ if self.contact_email:
527
+ contact["email"] = self.contact_email
528
+ if contact:
529
+ info["contact"] = contact
530
+ if self.license_name:
531
+ info["license"] = {"name": self.license_name}
532
+ return info
533
+
534
+ def _build_operation(self, route: dict, path: str, method: str, schemes: dict,
535
+ models: dict, ref_schemas: set, used_tags: list,
536
+ seen_ids: set) -> dict:
537
+ """Build the OpenAPI operation object for one route (method + path).
538
+
539
+ Accumulators (models/ref_schemas/used_tags/seen_ids) are threaded through
540
+ and mutated in place, exactly as the inline loop did.
541
+ """
542
+ handler = route.get("handler")
543
+
544
+ operation = {
545
+ "operationId": self._unique_operation_id(method, path, seen_ids),
546
+ "responses": {
547
+ "200": {"description": "Successful response"},
548
+ },
549
+ }
550
+
551
+ model = getattr(handler, "_swagger_model", None) if handler else None
552
+ ref = None
553
+ if model is not None and hasattr(model, "_fields"):
554
+ models[model.__name__] = model
555
+ ref = f"#/components/schemas/{model.__name__}"
556
+
557
+ self._operation_summary_tags(operation, route, method, handler, used_tags)
558
+
559
+ # Extract metadata from handler decorators
560
+ if handler:
561
+ self._operation_metadata(operation, handler)
562
+ self._operation_request_body(operation, handler, method, ref, ref_schemas)
563
+ self._operation_responses(operation, handler, ref)
564
+ self._operation_response_schemas(operation, handler, ref_schemas)
565
+
566
+ self._operation_parameters(operation, route, handler)
567
+ self._operation_security(operation, route, handler, schemes)
568
+ return operation
569
+
570
+ def _operation_summary_tags(self, operation: dict, route: dict, method: str,
571
+ handler, used_tags: list) -> None:
572
+ """Populate summary + tags (SWAG-SHAPE-DRIFT, ADR-0004) — always present.
573
+
574
+ An explicit @summary/@tags wins; otherwise a "METHOD /path" summary and a
575
+ first-path-segment tag match the other three frameworks' fallback. Each
576
+ used tag is recorded (insertion-ordered) for the top-level tags[] array.
577
+ """
578
+ operation["summary"] = (
579
+ getattr(handler, "_swagger_summary", None) if handler else None
580
+ ) or f"{method.upper()} {route['path']}"
581
+ op_tags = (
582
+ getattr(handler, "_swagger_tags", None) if handler else None
583
+ ) or [self._infer_tag(route["path"])]
584
+ operation["tags"] = op_tags
585
+ for t in op_tags:
586
+ if t not in used_tags:
587
+ used_tags.append(t)
588
+
589
+ @staticmethod
590
+ def _operation_metadata(operation: dict, handler) -> None:
591
+ """Apply @description(+detail) and @deprecated decorator metadata."""
592
+ desc = getattr(handler, "_swagger_description", None)
593
+ detail = getattr(handler, "_swagger_detail", None)
594
+ if desc is not None or detail:
595
+ operation["description"] = "\n\n".join(
596
+ p for p in (desc, detail) if p
597
+ )
598
+ if hasattr(handler, "_swagger_deprecated"):
599
+ operation["deprecated"] = True
600
+
601
+ def _operation_request_body(self, operation: dict, handler, method: str,
602
+ ref, ref_schemas: set) -> None:
603
+ """Build requestBody for write methods — a $ref to a registered request
604
+ schema (preferred), else the model schema, else an inferred schema from
605
+ @example. application/json unless the example declares multipart/form-data."""
606
+ if method not in ("post", "put", "patch"):
607
+ return
608
+ req_schema = getattr(handler, "_swagger_request_schema", None)
609
+ ct = getattr(handler, "_swagger_example_content_type", "application/json")
610
+ ex = getattr(handler, "_swagger_example", None)
611
+ media: dict = {}
612
+ if req_schema is not None:
613
+ sname, sct = req_schema
614
+ ct = sct or ct
615
+ ref_schemas.add(sname)
616
+ media["schema"] = {"$ref": f"#/components/schemas/{sname}"}
617
+ elif ref is not None:
618
+ media["schema"] = {"$ref": ref}
619
+ elif ex is not None:
620
+ media["schema"] = self._infer_schema(ex)
621
+ if ex is not None:
622
+ media["example"] = ex
623
+ if media:
624
+ operation["requestBody"] = {"content": {ct: media}}
625
+
626
+ def _operation_responses(self, operation: dict, handler, ref) -> None:
627
+ """Populate responses: model $ref (single or list), then any per-status
628
+ @example_response entries (explicit wins), then the legacy single-example."""
629
+ if ref is not None:
630
+ is_list = getattr(handler, "_swagger_model_list", False)
631
+ schema = ({"type": "array", "items": {"$ref": ref}} if is_list
632
+ else {"$ref": ref})
633
+ operation["responses"]["200"] = {
634
+ "description": "Successful response",
635
+ "content": {"application/json": {"schema": schema}},
636
+ }
637
+ resp_examples = getattr(handler, "_swagger_example_responses", None)
638
+ if resp_examples:
639
+ for status_code, body in resp_examples.items():
640
+ operation["responses"][str(status_code)] = {
641
+ "description": "Successful response" if str(status_code).startswith("2") else "Response",
642
+ "content": {
643
+ "application/json": {
644
+ "schema": self._infer_schema(body),
645
+ "example": body,
598
646
  }
599
- elif hasattr(handler, "_swagger_example_response") and ref is None:
600
- # Legacy single-example back-compat (no per-status dict).
601
- ex = handler._swagger_example_response
602
- operation["responses"]["200"] = {
603
- "description": "Successful response",
604
- "content": {
605
- "application/json": {
606
- "schema": self._infer_schema(ex),
607
- "example": ex,
608
- }
609
- },
647
+ },
648
+ }
649
+ elif hasattr(handler, "_swagger_example_response") and ref is None:
650
+ # Legacy single-example back-compat (no per-status dict).
651
+ ex = handler._swagger_example_response
652
+ operation["responses"]["200"] = {
653
+ "description": "Successful response",
654
+ "content": {
655
+ "application/json": {
656
+ "schema": self._infer_schema(ex),
657
+ "example": ex,
610
658
  }
659
+ },
660
+ }
611
661
 
612
- # Registered response schemas ($ref) — explicit and authoritative.
613
- resp_schemas = getattr(handler, "_swagger_response_schemas", None)
614
- if resp_schemas:
615
- for status_code, (sname, is_list) in resp_schemas.items():
616
- ref_schemas.add(sname)
617
- sref = f"#/components/schemas/{sname}"
618
- schema = ({"type": "array", "items": {"$ref": sref}} if is_list
619
- else {"$ref": sref})
620
- operation["responses"][str(status_code)] = {
621
- "description": "Successful response" if str(status_code).startswith("2") else "Response",
622
- "content": {"application/json": {"schema": schema}},
623
- }
662
+ @staticmethod
663
+ def _operation_response_schemas(operation: dict, handler, ref_schemas: set) -> None:
664
+ """Apply registered response schemas ($ref) — explicit and authoritative.
665
+ swagger response_schemas: {status: (name, is_list)}."""
666
+ resp_schemas = getattr(handler, "_swagger_response_schemas", None)
667
+ if not resp_schemas:
668
+ return
669
+ for status_code, (sname, is_list) in resp_schemas.items():
670
+ ref_schemas.add(sname)
671
+ sref = f"#/components/schemas/{sname}"
672
+ schema = ({"type": "array", "items": {"$ref": sref}} if is_list
673
+ else {"$ref": sref})
674
+ operation["responses"][str(status_code)] = {
675
+ "description": "Successful response" if str(status_code).startswith("2") else "Response",
676
+ "content": {"application/json": {"schema": schema}},
677
+ }
624
678
 
625
- # Parameters: path params (+ types) then query params from @description(query=)
626
- params = self._extract_path_params(route["path"])
627
- if handler:
628
- pdocs = getattr(handler, "_swagger_params", None) or {}
629
- for p in params:
630
- if p["name"] in pdocs:
631
- p["description"] = str(pdocs[p["name"]])
632
- qdocs = getattr(handler, "_swagger_query", None) or {}
633
- for qname, qdesc in qdocs.items():
634
- params.append({
635
- "name": qname,
636
- "in": "query",
637
- "required": False,
638
- "description": str(qdesc),
639
- "schema": {"type": "string"},
640
- })
641
- if params:
642
- operation["parameters"] = params
643
-
644
- # Auth — explicit @security wins (an empty list = explicitly public);
645
- # otherwise a secured route gets the default scheme.
646
- sec = getattr(handler, "_swagger_security", None) if handler else None
647
- if sec is not None:
648
- operation["security"] = self._sanitize_security(sec, schemes) if sec else []
649
- elif route.get("auth_required", False):
650
- requirements = [{self.default_scheme: []}]
651
- if self.default_scheme == "bearerAuth" and "ssoSession" in schemes:
652
- requirements.append({"ssoSession": []})
653
- operation["security"] = self._sanitize_security(
654
- requirements, schemes
655
- )
656
-
657
- # A secured operation documents a 401 (SWAG-401-SHAPE, ADR-0004,
658
- # OWNER-DECISIONS.md 2026-08-11: "secured swagger ops document a
659
- # 401 (Python adds it)"). PHP/Ruby/Node already did this; Python was
660
- # the gap. setdefault so an explicit @example_response(401, ...) is
661
- # never clobbered.
662
- if operation.get("security"):
663
- operation["responses"].setdefault("401", {"description": "Unauthorized"})
664
-
665
- spec["paths"][path][method] = operation
666
-
667
- # components.schemas from any ORM models referenced by handlers
679
+ def _operation_parameters(self, operation: dict, route: dict, handler) -> None:
680
+ """Build parameters: path params (+ types) then query params from
681
+ @description(query=). Path-param descriptions come from @description(params=)."""
682
+ params = self._extract_path_params(route["path"])
683
+ if handler:
684
+ pdocs = getattr(handler, "_swagger_params", None) or {}
685
+ for p in params:
686
+ if p["name"] in pdocs:
687
+ p["description"] = str(pdocs[p["name"]])
688
+ qdocs = getattr(handler, "_swagger_query", None) or {}
689
+ for qname, qdesc in qdocs.items():
690
+ params.append({
691
+ "name": qname,
692
+ "in": "query",
693
+ "required": False,
694
+ "description": str(qdesc),
695
+ "schema": {"type": "string"},
696
+ })
697
+ if params:
698
+ operation["parameters"] = params
699
+
700
+ def _operation_security(self, operation: dict, route: dict, handler,
701
+ schemes: dict) -> None:
702
+ """Merge the per-route security requirement.
703
+
704
+ Explicit @security wins (an empty list = explicitly public); otherwise a
705
+ secured route gets the default scheme. A secured operation documents a 401
706
+ (SWAG-401-SHAPE, ADR-0004) — setdefault so an explicit
707
+ @example_response(401, ...) is never clobbered.
708
+ """
709
+ sec = getattr(handler, "_swagger_security", None) if handler else None
710
+ if sec is not None:
711
+ operation["security"] = self._sanitize_security(sec, schemes) if sec else []
712
+ elif route.get("auth_required", False):
713
+ requirements = [{self.default_scheme: []}]
714
+ if self.default_scheme == "bearerAuth" and "ssoSession" in schemes:
715
+ requirements.append({"ssoSession": []})
716
+ operation["security"] = self._sanitize_security(
717
+ requirements, schemes
718
+ )
719
+
720
+ if operation.get("security"):
721
+ operation["responses"].setdefault("401", {"description": "Unauthorized"})
722
+
723
+ def _build_component_schemas(self, spec: dict, models: dict,
724
+ ref_schemas: set) -> None:
725
+ """Add components.schemas from referenced ORM models, then any registered
726
+ component schemas referenced via @request_schema/@response_schema."""
668
727
  if models:
669
728
  schemas = spec["components"].setdefault("schemas", {})
670
729
  for name, model_class in models.items():
671
730
  schemas[name] = self._model_schema(model_class)
672
731
 
673
- # Registered component schemas referenced via @request_schema/@response_schema.
674
732
  if ref_schemas:
675
733
  schemas = spec["components"].setdefault("schemas", {})
676
734
  for name in ref_schemas:
677
735
  if name in _REGISTERED_SCHEMAS and name not in schemas:
678
736
  schemas[name] = _REGISTERED_SCHEMAS[name]
679
737
 
680
- # top-level tags[] (name-only is valid OpenAPI; descriptions optional)
681
- if used_tags:
682
- spec["tags"] = [{"name": t} for t in used_tags]
683
-
684
- return spec
685
-
686
738
  def generate_json(self, routes: list[dict]) -> str:
687
739
  """Generate OpenAPI spec as JSON string."""
688
740
  return json.dumps(self.generate(routes), indent=2)