tina4-python 3.13.102__tar.gz → 3.13.104__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 (163) hide show
  1. {tina4_python-3.13.102 → tina4_python-3.13.104}/PKG-INFO +1 -1
  2. {tina4_python-3.13.102 → tina4_python-3.13.104}/pyproject.toml +1 -1
  3. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/__init__.py +8 -1
  4. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/server.py +13 -1
  5. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/__init__.py +6 -2
  6. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/adapter.py +3 -2
  7. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/sql_translator.py +122 -0
  8. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/dev_admin/metrics.py +1 -1
  9. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/frond/engine.py +167 -234
  10. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/orm/__init__.py +6 -3
  11. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/orm/fields.py +125 -0
  12. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/orm/model.py +195 -2
  13. tina4_python-3.13.104/tina4_python/orm/point.py +421 -0
  14. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/js/tina4-dev-admin.min.js +2 -2
  15. tina4_python-3.13.104/tina4_python/query_builder/__init__.py +757 -0
  16. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session/__init__.py +4 -1
  17. tina4_python-3.13.104/tina4_python/sso.py +319 -0
  18. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/swagger/__init__.py +13 -1
  19. tina4_python-3.13.102/tina4_python/query_builder/__init__.py +0 -445
  20. {tina4_python-3.13.102 → tina4_python-3.13.104}/.gitignore +0 -0
  21. {tina4_python-3.13.102 → tina4_python-3.13.104}/README.md +0 -0
  22. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/CLAUDE.md +0 -0
  23. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/HtmlElement.py +0 -0
  24. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/Testing.py +0 -0
  25. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/ai/__init__.py +0 -0
  26. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/ai/client.py +0 -0
  27. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/api/__init__.py +0 -0
  28. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/auth/__init__.py +0 -0
  29. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/cache/__init__.py +0 -0
  30. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/cli/__init__.py +0 -0
  31. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/container/__init__.py +0 -0
  32. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/context/__init__.py +0 -0
  33. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/context/chunker.py +0 -0
  34. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/__init__.py +0 -0
  35. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/cache.py +0 -0
  36. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/constants.py +0 -0
  37. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/events.py +0 -0
  38. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/middleware.py +0 -0
  39. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/port_takeover.py +0 -0
  40. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/rate_limiter.py +0 -0
  41. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/request.py +0 -0
  42. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/response.py +0 -0
  43. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/router.py +0 -0
  44. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/crud/__init__.py +0 -0
  45. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/connection.py +0 -0
  46. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/database_url.py +0 -0
  47. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/firebird.py +0 -0
  48. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/mongodb.py +0 -0
  49. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/mssql.py +0 -0
  50. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/mysql.py +0 -0
  51. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/odbc.py +0 -0
  52. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/postgres.py +0 -0
  53. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/sqlite.py +0 -0
  54. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/debug/__init__.py +0 -0
  55. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/debug/error_overlay.py +0 -0
  56. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/dev_admin/__init__.py +0 -0
  57. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/dev_admin/plan.py +0 -0
  58. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/dev_admin/project_index.py +0 -0
  59. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/docs.py +0 -0
  60. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/docstore/__init__.py +0 -0
  61. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/dotenv/__init__.py +0 -0
  62. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/env.py +0 -0
  63. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/frond/FROND.md +0 -0
  64. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/frond/__init__.py +0 -0
  65. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/frond/compiler.py +0 -0
  66. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/frond/parser.py +0 -0
  67. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/auth/meta.json +0 -0
  68. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  69. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/database/meta.json +0 -0
  70. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  71. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/error-overlay/meta.json +0 -0
  72. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  73. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/orm/meta.json +0 -0
  74. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  75. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  76. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/queue/meta.json +0 -0
  77. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  78. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/rest-api/meta.json +0 -0
  79. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  80. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/templates/meta.json +0 -0
  81. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  82. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  83. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/graphql/__init__.py +0 -0
  84. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/i18n/__init__.py +0 -0
  85. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/mcp/__init__.py +0 -0
  86. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/mcp/protocol.py +0 -0
  87. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/mcp/tools.py +0 -0
  88. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/messenger/__init__.py +0 -0
  89. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/migration/__init__.py +0 -0
  90. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/migration/runner.py +0 -0
  91. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/mqtt/__init__.py +0 -0
  92. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/mqtt/message.py +0 -0
  93. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/__feedback/widget.js +0 -0
  94. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/css/tina4.css +0 -0
  95. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/css/tina4.min.css +0 -0
  96. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/favicon.ico +0 -0
  97. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/images/logo.svg +0 -0
  98. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  99. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/js/frond.js +0 -0
  100. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/js/frond.min.js +0 -0
  101. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/js/tina4.min.js +0 -0
  102. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/js/tina4js.min.js +0 -0
  103. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/swagger/index.html +0 -0
  104. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  105. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/__init__.py +0 -0
  106. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/amqp_url.py +0 -0
  107. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/job.py +0 -0
  108. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/kafka_backend.py +0 -0
  109. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/lite_backend.py +0 -0
  110. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/mongo_backend.py +0 -0
  111. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/rabbitmq_backend.py +0 -0
  112. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue_backends/__init__.py +0 -0
  113. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue_backends/kafka_backend.py +0 -0
  114. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue_backends/mongo_backend.py +0 -0
  115. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  116. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/__init__.py +0 -0
  117. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/Attachment.py +0 -0
  118. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/Channel.py +0 -0
  119. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/ChannelMember.py +0 -0
  120. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/Message.py +0 -0
  121. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/Workspace.py +0 -0
  122. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/__init__.py +0 -0
  123. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/storage.py +0 -0
  124. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/seeder/__init__.py +0 -0
  125. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/service/__init__.py +0 -0
  126. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session_handlers/__init__.py +0 -0
  127. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session_handlers/memcached_handler.py +0 -0
  128. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  129. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session_handlers/redis_handler.py +0 -0
  130. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session_handlers/valkey_handler.py +0 -0
  131. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/components/crud.twig +0 -0
  132. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  133. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  134. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/docker/python/Dockerfile +0 -0
  135. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  136. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/302.twig +0 -0
  137. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/401.twig +0 -0
  138. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/403.twig +0 -0
  139. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/404.twig +0 -0
  140. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/500.twig +0 -0
  141. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/502.twig +0 -0
  142. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/503.twig +0 -0
  143. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/base.twig +0 -0
  144. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/frontend/README.md +0 -0
  145. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/readme.md +0 -0
  146. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/test/__init__.py +0 -0
  147. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/test_client/__init__.py +0 -0
  148. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  155. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  156. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  157. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  158. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  159. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  160. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/validator/__init__.py +0 -0
  161. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/websocket/__init__.py +0 -0
  162. {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/websocket/backplane.py +0 -0
  163. {tina4_python-3.13.102 → tina4_python-3.13.104}/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.102
3
+ Version: 3.13.104
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.102"
3
+ version = "3.13.104"
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.101"
54
+ return "3.13.104"
55
55
 
56
56
 
57
57
  __version__ = _resolve_version()
@@ -108,6 +108,9 @@ _LAZY: dict[str, tuple[str, str]] = {
108
108
  "NumericField": ("tina4_python.orm", "NumericField"),
109
109
  "DecimalField": ("tina4_python.orm", "DecimalField"),
110
110
  "JSONField": ("tina4_python.orm", "JSONField"),
111
+ "PointField": ("tina4_python.orm", "PointField"),
112
+ "Point": ("tina4_python.orm", "Point"),
113
+ "feature_collection": ("tina4_python.orm", "feature_collection"),
111
114
  "ForeignKeyField": ("tina4_python.orm", "ForeignKeyField"),
112
115
  "IntField": ("tina4_python.orm", "IntField"),
113
116
  "StrField": ("tina4_python.orm", "StrField"),
@@ -141,6 +144,10 @@ _LAZY: dict[str, tuple[str, str]] = {
141
144
  "AiHTTPError": ("tina4_python.ai", "AiHTTPError"),
142
145
  "AiTimeoutError": ("tina4_python.ai", "AiTimeoutError"),
143
146
  "AiParseError": ("tina4_python.ai", "AiParseError"),
147
+ # Provider-neutral OpenID Connect SSO (ADR-0056)
148
+ "Sso": ("tina4_python.sso", "Sso"),
149
+ "SSO": ("tina4_python.sso", "SSO"),
150
+ "SsoError": ("tina4_python.sso", "SsoError"),
144
151
  # SOAP / WSDL
145
152
  "WSDL": ("tina4_python.wsdl", "WSDL"),
146
153
  "wsdl_operation": ("tina4_python.wsdl", "wsdl_operation"),
@@ -1691,8 +1691,16 @@ def _check_auth(request: Request, response: Response, route: dict) -> bool:
1691
1691
  if not _auth_ok:
1692
1692
  _session = getattr(request, "session", None)
1693
1693
  if _session:
1694
+ # A provider-verified OIDC identity is handed into the SAME auth
1695
+ # gate as JWT. Provider credentials stay in the reserved Session
1696
+ # value and never enter request.user.
1697
+ _sso = _session.get("_tina4_sso")
1698
+ _sso_identity = _sso.get("identity") if isinstance(_sso, dict) else None
1699
+ if isinstance(_sso_identity, dict) and _sso_identity.get("issuer") and _sso_identity.get("subject"):
1700
+ _auth_ok = True
1701
+ request.user = _sso_identity
1694
1702
  _session_token = _session.get("token") if _session else ""
1695
- if _session_token:
1703
+ if not _auth_ok and _session_token:
1696
1704
  try:
1697
1705
  from tina4_python.auth import Auth
1698
1706
  _payload = Auth.valid_token_static(_session_token)
@@ -3669,6 +3677,10 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
3669
3677
 
3670
3678
  # Auto-discover routes
3671
3679
  _auto_discover("src")
3680
+ # Configuration-first OIDC: canonical routes appear only when configured,
3681
+ # after app discovery so collisions fail loudly rather than overwrite.
3682
+ from tina4_python.sso import Sso as _Sso
3683
+ _Sso.mount_configured()
3672
3684
  route_count = len(Router.get_routes())
3673
3685
  Log.info(f"Discovered {route_count} routes")
3674
3686
 
@@ -11,7 +11,11 @@ SQL-first database layer. One interface, many drivers.
11
11
  row = db.fetch_one("SELECT * FROM users WHERE id = ?", [42])
12
12
  db.execute("INSERT INTO users (name) VALUES (?)", ["Alice"])
13
13
  """
14
- from tina4_python.database.adapter import DatabaseAdapter, DatabaseResult, SQLTranslator
14
+ from tina4_python.database.adapter import DatabaseAdapter, DatabaseResult
15
+ from tina4_python.database.sql_translator import SQLTranslator, SpatialNotSupportedError
15
16
  from tina4_python.database.connection import Database
16
17
 
17
- __all__ = ["Database", "DatabaseAdapter", "DatabaseResult", "SQLTranslator"]
18
+ __all__ = [
19
+ "Database", "DatabaseAdapter", "DatabaseResult", "SQLTranslator",
20
+ "SpatialNotSupportedError",
21
+ ]
@@ -1131,9 +1131,10 @@ class SqlCrudMixin:
1131
1131
  # ── SQL Translation Rules ──────────────────────────────────────
1132
1132
  # Reusable translation functions for common cross-engine quirks.
1133
1133
 
1134
-
1135
1134
  # SQLTranslator moved to sql_translator.py (feature 3: the adapter module is the
1136
1135
  # adapter contract and nothing else). Re-exported here because the sqlite,
1137
1136
  # firebird, mssql and odbc adapters import it from this module, and a file move
1138
1137
  # is not the place to churn their imports.
1139
- from tina4_python.database.sql_translator import SQLTranslator # noqa: E402,F401
1138
+ from tina4_python.database.sql_translator import ( # noqa: E402,F401
1139
+ SQLTranslator, SpatialNotSupportedError,
1140
+ )
@@ -16,6 +16,10 @@ by engine; moving it here would flatten that override.
16
16
  import re
17
17
 
18
18
 
19
+ class SpatialNotSupportedError(NotImplementedError):
20
+ """The selected database engine cannot honor the GIS contract."""
21
+
22
+
19
23
  class SQLTranslator:
20
24
  """Cross-engine SQL translator.
21
25
 
@@ -23,6 +27,124 @@ class SQLTranslator:
23
27
  and stateless — just string transforms.
24
28
  """
25
29
 
30
+ SPATIAL_ENGINES = ("postgres", "postgresql")
31
+ _IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$")
32
+
33
+ @classmethod
34
+ def supports_spatial(cls, engine: str) -> bool:
35
+ """Return whether Tina4 has a spatial provider for ``engine``."""
36
+ name = (engine or "").lower()
37
+ name = cls.ENGINE_ALIASES.get(name, name)
38
+ return name in cls.SPATIAL_ENGINES
39
+
40
+ @classmethod
41
+ def require_spatial(cls, engine: str, feature: str) -> str:
42
+ """Return the normalized spatial engine or fail with an actionable error."""
43
+ name = (engine or "unknown").lower()
44
+ name = cls.ENGINE_ALIASES.get(name, name)
45
+ if cls.supports_spatial(name):
46
+ return name
47
+ raise SpatialNotSupportedError(
48
+ f"{feature} is not supported on the '{name}' database engine. "
49
+ "Tina4 GIS support is PostGIS-first: use PostgreSQL with the "
50
+ "PostGIS extension (CREATE EXTENSION postgis). Tina4 will not "
51
+ "replace a spatial query with an approximate latitude/longitude query. "
52
+ "If the application needs storage only and no GIS behavior, declare "
53
+ "separate longitude and latitude FloatField columns instead."
54
+ )
55
+
56
+ @classmethod
57
+ def identifier(cls, name: str, what: str = "column") -> str:
58
+ """Validate an identifier before placing it in spatial SQL."""
59
+ if not isinstance(name, str) or not cls._IDENTIFIER.fullmatch(name):
60
+ raise ValueError(
61
+ f"Spatial {what} name {name!r} is not a valid SQL identifier"
62
+ )
63
+ return name
64
+
65
+ @staticmethod
66
+ def _srid(srid) -> int:
67
+ """Coerce the interpolated SRID to an integer."""
68
+ if isinstance(srid, bool):
69
+ raise ValueError(f"Spatial SRID must be an integer, got {srid!r}")
70
+ try:
71
+ value = int(srid)
72
+ except (TypeError, ValueError) as error:
73
+ raise ValueError(f"Spatial SRID must be an integer, got {srid!r}") from error
74
+ if value <= 0:
75
+ raise ValueError(f"Spatial SRID must be positive, got {value}")
76
+ return value
77
+
78
+ @classmethod
79
+ def point_column_type(cls, engine: str, srid: int = 4326) -> str:
80
+ """Return PostGIS geography Point DDL."""
81
+ cls.require_spatial(engine, "PointField")
82
+ return f"geography(Point,{cls._srid(srid)})"
83
+
84
+ @classmethod
85
+ def spatial_index(cls, engine: str, table: str, column: str) -> str:
86
+ """Return idempotent PostGIS GiST-index DDL."""
87
+ cls.require_spatial(engine, "spatial index creation")
88
+ table = cls.identifier(table, "table")
89
+ column = cls.identifier(column)
90
+ index = f"{table.replace('.', '_')}_{column}_gist"
91
+ return f"CREATE INDEX IF NOT EXISTS {index} ON {table} USING GIST ({column})"
92
+
93
+ @classmethod
94
+ def point_literal(cls, engine: str, srid: int = 4326) -> str:
95
+ """Return a bound PostGIS point expression in longitude/latitude order."""
96
+ cls.require_spatial(engine, "spatial predicates")
97
+ return f"ST_SetSRID(ST_MakePoint(?, ?), {cls._srid(srid)})::geography"
98
+
99
+ @classmethod
100
+ def within_distance(cls, engine: str, column: str, srid: int = 4326) -> str:
101
+ """Return a bound radius predicate whose distance uses metres."""
102
+ cls.require_spatial(engine, "within_distance()")
103
+ column = cls.identifier(column)
104
+ return f"ST_DWithin({column}, {cls.point_literal(engine, srid)}, ?)"
105
+
106
+ @classmethod
107
+ def distance(cls, engine: str, column: str, srid: int = 4326) -> str:
108
+ """Return a bound spheroid-distance expression in metres."""
109
+ cls.require_spatial(engine, "order_by_distance()")
110
+ column = cls.identifier(column)
111
+ return f"ST_Distance({column}, {cls.point_literal(engine, srid)})"
112
+
113
+ @classmethod
114
+ def distance_as(cls, engine: str, column: str, alias: str, srid: int = 4326) -> str:
115
+ """Return an aliased bound distance expression for a SELECT list."""
116
+ alias = cls.identifier(alias, "result alias")
117
+ return f"{cls.distance(engine, column, srid)} AS {alias}"
118
+
119
+ @classmethod
120
+ def geometry_literal(cls, engine: str, form: str = "ewkt", srid: int = 4326) -> str:
121
+ """Return a one-parameter PostGIS geometry expression."""
122
+ cls.require_spatial(engine, "spatial predicates")
123
+ if form == "ewkt":
124
+ return "ST_GeogFromText(?)"
125
+ if form == "geojson":
126
+ return f"ST_SetSRID(ST_GeomFromGeoJSON(?), {cls._srid(srid)})::geography"
127
+ raise ValueError(
128
+ f"Spatial geometry form {form!r} is not supported; use 'ewkt' or 'geojson'"
129
+ )
130
+
131
+ @classmethod
132
+ def intersects(cls, engine: str, column: str, form: str = "ewkt",
133
+ srid: int = 4326) -> str:
134
+ """Return a bound PostGIS intersection predicate."""
135
+ column = cls.identifier(column)
136
+ return f"ST_Intersects({column}, {cls.geometry_literal(engine, form, srid)})"
137
+
138
+ @classmethod
139
+ def bbox(cls, engine: str, column: str, srid: int = 4326) -> str:
140
+ """Return a bound PostGIS bounding-box predicate."""
141
+ cls.require_spatial(engine, "bbox()")
142
+ column = cls.identifier(column)
143
+ return (
144
+ f"ST_Intersects({column}, "
145
+ f"ST_MakeEnvelope(?, ?, ?, ?, {cls._srid(srid)})::geography)"
146
+ )
147
+
26
148
  @staticmethod
27
149
  def limit_to_rows(sql: str) -> str:
28
150
  """Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
@@ -12,7 +12,7 @@ class MetricsEngineError(RuntimeError):
12
12
 
13
13
  _TIMEOUT_SECONDS = 60
14
14
  _SUMMARY_KEYS = ("files_analyzed", "total_functions", "avg_complexity", "avg_maintainability")
15
- _FILE_KEYS = ("path", "loc", "avg_complexity", "maintainability", "has_tests")
15
+ _FILE_KEYS = ("path", "loc", "avg_complexity", "maintainability", "has_referencing_test")
16
16
  _FUNCTION_KEYS = ("name", "file", "line", "complexity", "loc")
17
17
  _INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli"
18
18
  _last_scan_root = ""