tina4-python 3.11.23__tar.gz → 3.11.32__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 (153) hide show
  1. {tina4_python-3.11.23 → tina4_python-3.11.32}/.gitignore +4 -0
  2. {tina4_python-3.11.23 → tina4_python-3.11.32}/PKG-INFO +1 -1
  3. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/connection.py +32 -1
  5. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/dev_admin/__init__.py +231 -1
  6. tina4_python-3.11.32/tina4_python/docs.py +821 -0
  7. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/mcp/tools.py +31 -0
  8. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/js/tina4-dev-admin.js +121 -121
  9. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/js/tina4-dev-admin.min.js +121 -121
  10. {tina4_python-3.11.23 → tina4_python-3.11.32}/README.md +0 -0
  11. {tina4_python-3.11.23 → tina4_python-3.11.32}/pyproject.toml +0 -0
  12. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/CLAUDE.md +0 -0
  13. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/HtmlElement.py +0 -0
  14. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/Testing.py +0 -0
  15. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/ai/__init__.py +0 -0
  16. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/api/__init__.py +0 -0
  17. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/auth/__init__.py +0 -0
  18. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/cache/__init__.py +0 -0
  19. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/cli/__init__.py +0 -0
  20. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/container/__init__.py +0 -0
  21. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/__init__.py +0 -0
  22. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/cache.py +0 -0
  23. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/constants.py +0 -0
  24. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/events.py +0 -0
  25. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/middleware.py +0 -0
  26. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/rate_limiter.py +0 -0
  27. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/request.py +0 -0
  28. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/response.py +0 -0
  29. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/router.py +0 -0
  30. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/server.py +0 -0
  31. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/crud/__init__.py +0 -0
  32. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/__init__.py +0 -0
  33. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/adapter.py +0 -0
  34. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/firebird.py +0 -0
  35. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/mongodb.py +0 -0
  36. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/mssql.py +0 -0
  37. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/mysql.py +0 -0
  38. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/odbc.py +0 -0
  39. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/postgres.py +0 -0
  40. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/sqlite.py +0 -0
  41. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/debug/__init__.py +0 -0
  42. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/debug/error_overlay.py +0 -0
  43. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/dev_admin/metrics.py +0 -0
  44. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/dev_admin/plan.py +0 -0
  45. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/dev_admin/project_index.py +0 -0
  46. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/dotenv/__init__.py +0 -0
  47. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/frond/FROND.md +0 -0
  48. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/frond/__init__.py +0 -0
  49. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/frond/engine.py +0 -0
  50. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/auth/meta.json +0 -0
  51. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  52. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/database/meta.json +0 -0
  53. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  54. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/error-overlay/meta.json +0 -0
  55. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  56. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/orm/meta.json +0 -0
  57. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  58. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  59. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/queue/meta.json +0 -0
  60. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  61. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/rest-api/meta.json +0 -0
  62. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  63. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/templates/meta.json +0 -0
  64. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  65. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  66. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/graphql/__init__.py +0 -0
  67. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/i18n/__init__.py +0 -0
  68. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/mcp/__init__.py +0 -0
  69. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/mcp/protocol.py +0 -0
  70. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/messenger/__init__.py +0 -0
  71. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/migration/__init__.py +0 -0
  72. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/migration/runner.py +0 -0
  73. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/orm/__init__.py +0 -0
  74. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/orm/fields.py +0 -0
  75. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/orm/model.py +0 -0
  76. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/js/frond.min.js +0 -0
  82. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/js/tina4.min.js +0 -0
  83. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/js/tina4js.min.js +0 -0
  84. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/swagger/index.html +0 -0
  85. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  86. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/query_builder/__init__.py +0 -0
  87. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/__init__.py +0 -0
  88. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/job.py +0 -0
  89. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/kafka_backend.py +0 -0
  90. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/lite_backend.py +0 -0
  91. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/mongo_backend.py +0 -0
  92. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/rabbitmq_backend.py +0 -0
  93. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue_backends/__init__.py +0 -0
  94. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue_backends/kafka_backend.py +0 -0
  95. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue_backends/mongo_backend.py +0 -0
  96. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  97. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/__init__.py +0 -0
  98. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  99. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_badges.scss +0 -0
  100. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  101. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_cards.scss +0 -0
  102. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_forms.scss +0 -0
  103. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_grid.scss +0 -0
  104. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_modals.scss +0 -0
  105. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_nav.scss +0 -0
  106. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_reset.scss +0 -0
  107. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_tables.scss +0 -0
  108. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_typography.scss +0 -0
  109. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  110. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_variables.scss +0 -0
  111. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/base.scss +0 -0
  112. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/colors.scss +0 -0
  113. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/tina4.scss +0 -0
  114. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/seeder/__init__.py +0 -0
  115. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/service/__init__.py +0 -0
  116. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/session/__init__.py +0 -0
  117. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/session_handlers/__init__.py +0 -0
  118. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  119. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/session_handlers/redis_handler.py +0 -0
  120. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/session_handlers/valkey_handler.py +0 -0
  121. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/swagger/__init__.py +0 -0
  122. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/components/crud.twig +0 -0
  123. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  124. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  125. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/docker/python/Dockerfile +0 -0
  126. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  127. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/302.twig +0 -0
  128. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/401.twig +0 -0
  129. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/403.twig +0 -0
  130. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/404.twig +0 -0
  131. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/500.twig +0 -0
  132. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/502.twig +0 -0
  133. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/503.twig +0 -0
  134. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/base.twig +0 -0
  135. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/frontend/README.md +0 -0
  136. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/readme.md +0 -0
  137. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/test_client/__init__.py +0 -0
  138. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  139. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  140. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  141. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  142. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/validator/__init__.py +0 -0
  151. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/websocket/__init__.py +0 -0
  152. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/websocket/backplane.py +0 -0
  153. {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/wsdl/__init__.py +0 -0
@@ -74,3 +74,7 @@ example/store/__pycache__/
74
74
  example/store/src/**/__pycache__/
75
75
  .claude/settings.local.json
76
76
  .claude/worktrees/
77
+ /nonexistent_path/
78
+ /data/store.db
79
+ /example/store/
80
+ /example/uv.lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.11.23
3
+ Version: 3.11.32
4
4
  Summary: Tina4 for Python — 54 built-in features, zero dependencies
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -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.11.23"
11
+ __version__ = "3.11.32"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -166,6 +166,11 @@ class Database:
166
166
  self._adapter: DatabaseAdapter = self._create_adapter()
167
167
  self._adapter.connect(self._connection_path(), username=self.username, password=self.password, **kwargs)
168
168
 
169
+ # Per-thread transaction adapter pin. While set, every operation
170
+ # on this thread routes to the same adapter — so the round-robin
171
+ # pool can't rotate mid-transaction and silently break atomicity.
172
+ self._tx_local = threading.local()
173
+
169
174
  # Query cache — off by default, opt-in via TINA4_DB_CACHE=true
170
175
  from tina4_python.dotenv import is_truthy
171
176
  self._cache_enabled: bool = is_truthy(os.environ.get("TINA4_DB_CACHE", "false"))
@@ -308,7 +313,25 @@ class Database:
308
313
  # ── Pool-aware adapter access ─────────────────────────────
309
314
 
310
315
  def _get_adapter(self) -> DatabaseAdapter:
311
- """Get an adapter from pool (round-robin) or single connection."""
316
+ """Get an adapter for the next operation.
317
+
318
+ With pooling enabled, ordinary calls round-robin through the pool.
319
+ Inside a transaction, however, all calls must land on the SAME
320
+ adapter — otherwise start_transaction(), execute() and commit()
321
+ each rotate to a different connection and the transaction is
322
+ meaningless (executes autocommit on whatever adapter they hit;
323
+ the final commit lands on yet another adapter that has nothing
324
+ to commit; rollback() is silently no-op'd).
325
+
326
+ We pin the adapter to the calling thread for the duration of the
327
+ transaction. start_transaction() sets the pin, commit()/rollback()
328
+ clear it. While pinned, _get_adapter() returns that same adapter
329
+ for every call so the whole transaction is atomic on one
330
+ connection.
331
+ """
332
+ pinned = getattr(self._tx_local, "adapter", None)
333
+ if pinned is not None:
334
+ return pinned
312
335
  if self._pool is not None:
313
336
  return self._pool.checkout()
314
337
  return self._adapter
@@ -422,16 +445,24 @@ class Database:
422
445
  return adapter.delete(table, filter_sql, params)
423
446
 
424
447
  def start_transaction(self):
448
+ """Begin a transaction. Pins the adapter to this thread for the
449
+ whole transaction so executes and the final commit/rollback all
450
+ run on the same connection."""
425
451
  adapter = self._get_adapter()
452
+ self._tx_local.adapter = adapter
426
453
  adapter.start_transaction()
427
454
 
428
455
  def commit(self):
456
+ """Commit the current transaction and release the adapter pin."""
429
457
  adapter = self._get_adapter()
430
458
  adapter.commit()
459
+ self._tx_local.adapter = None
431
460
 
432
461
  def rollback(self):
462
+ """Roll back the current transaction and release the adapter pin."""
433
463
  adapter = self._get_adapter()
434
464
  adapter.rollback()
465
+ self._tx_local.adapter = None
435
466
 
436
467
  def table_exists(self, name: str) -> bool:
437
468
  adapter = self._get_adapter()
@@ -288,6 +288,94 @@ def register():
288
288
  Router.get(path, handler)
289
289
  else:
290
290
  Router.post(path, handler)
291
+ # Auto-discovery: drop `.tina4/mcp.json` so MCP-aware AI tools
292
+ # (Claude Code, Cursor, etc.) discover the local Live Docs +
293
+ # MCP server without the user authoring config. Idempotent.
294
+ write_mcp_discovery_file()
295
+
296
+
297
+ def write_mcp_discovery_file() -> None:
298
+ """Drop `.tina4/mcp.json` and append `.tina4/` to `.gitignore`.
299
+
300
+ Both are idempotent — running twice is a no-op when the state is
301
+ already correct. Skipped silently outside debug mode and on
302
+ filesystem errors (read-only project dir, etc.) — discovery is
303
+ a convenience, not a requirement.
304
+
305
+ See plan/v3/22-LIVE-API-RAG.md §"Auto-discovery file" for the
306
+ JSON shape.
307
+ """
308
+ import json
309
+ import os
310
+
311
+ is_dev = os.environ.get("TINA4_DEBUG", "false").lower() in ("1", "true", "yes")
312
+ if not is_dev:
313
+ return
314
+ root = os.getcwd()
315
+ tina4_dir = os.path.join(root, ".tina4")
316
+ mcp_file = os.path.join(tina4_dir, "mcp.json")
317
+ port = (os.environ.get("TINA4_PORT")
318
+ or os.environ.get("PORT")
319
+ or "7146")
320
+ expected = {
321
+ "mcpServers": {
322
+ "tina4-live-docs": {
323
+ "url": f"http://localhost:{port}/__dev/api/mcp",
324
+ "description": "Live API docs for this Tina4 project (framework + user code)",
325
+ }
326
+ }
327
+ }
328
+ expected_json = json.dumps(expected, indent=2) + "\n"
329
+
330
+ try:
331
+ if os.path.isfile(mcp_file):
332
+ with open(mcp_file, "r", encoding="utf-8") as f:
333
+ existing = f.read()
334
+ if existing.strip() == expected_json.strip():
335
+ _ensure_gitignore(root)
336
+ return
337
+ os.makedirs(tina4_dir, exist_ok=True)
338
+ with open(mcp_file, "w", encoding="utf-8") as f:
339
+ f.write(expected_json)
340
+ _ensure_gitignore(root)
341
+ except OSError:
342
+ # Read-only fs, permission denied, etc. Silently skip —
343
+ # discovery is convenience.
344
+ return
345
+
346
+
347
+ def _ensure_gitignore(root: str) -> None:
348
+ """Append `.tina4/` to `.gitignore` if not already excluded.
349
+
350
+ Tolerates leading slashes, trailing slashes, and existing comment
351
+ lines so we never duplicate. Only touches `.gitignore` if `.git/`
352
+ exists (don't pollute non-git projects).
353
+ """
354
+ import os
355
+
356
+ if not os.path.isdir(os.path.join(root, ".git")):
357
+ return
358
+ gi_path = os.path.join(root, ".gitignore")
359
+ existing = ""
360
+ if os.path.isfile(gi_path):
361
+ try:
362
+ with open(gi_path, "r", encoding="utf-8") as f:
363
+ existing = f.read()
364
+ except OSError:
365
+ return
366
+ for raw in existing.splitlines():
367
+ line = raw.strip()
368
+ if not line or line.startswith("#"):
369
+ continue
370
+ normal = line.strip("/").strip()
371
+ if normal == ".tina4":
372
+ return # already excluded
373
+ suffix = "" if existing.endswith("\n") or existing == "" else "\n"
374
+ try:
375
+ with open(gi_path, "a", encoding="utf-8") as f:
376
+ f.write(suffix + ".tina4/\n")
377
+ except OSError:
378
+ pass
291
379
 
292
380
 
293
381
  def get_api_handlers() -> dict:
@@ -388,6 +476,17 @@ def get_api_handlers() -> dict:
388
476
  # without shelling out from the browser.
389
477
  "/__dev/api/scaffold": ("GET", _api_scaffold_list),
390
478
  "/__dev/api/scaffold/run": ("POST", _api_scaffold_run),
479
+ # ── Live Docs (per plan/v3/22-LIVE-API-RAG.md) ──
480
+ # Thin HTTP wrappers around tina4_python.docs.Docs. Both
481
+ # framework public API and the user's src/ surface are
482
+ # returned, tagged with `source = framework | user`. AI tools
483
+ # (Claude Code, Cursor, dev-admin chat) hit these for ground-
484
+ # truth introspection instead of guessing from training data.
485
+ "/__dev/api/docs/search": ("GET", _api_docs_search),
486
+ "/__dev/api/docs/class": ("GET", _api_docs_class),
487
+ "/__dev/api/docs/method": ("GET", _api_docs_method),
488
+ "/__dev/api/docs/index": ("GET", _api_docs_index),
489
+ "/__dev/api/docs/.well-known.json": ("GET", _api_docs_well_known),
391
490
  }
392
491
 
393
492
 
@@ -1766,10 +1865,56 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
1766
1865
  <span style="color:#ffeb3b;">req:{request_id}</span>
1767
1866
  <span style="color:#90caf9;">{route_count} routes</span>
1768
1867
  <span style="color:#888;">Python {python_version}</span>
1769
- <a href="#" onclick="(function(e){{e.preventDefault();var p=document.getElementById('tina4-dev-panel');if(p){{p.style.display=p.style.display==='none'?'block':'none';return;}}var c=document.createElement('div');c.id='tina4-dev-panel';c.style.cssText='position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all 0.2s';var f=document.createElement('iframe');f.src='/__dev';f.style.cssText='width:100%;height:100%;border:1px solid #3572A5;border-radius:0.5rem;box-shadow:0 8px 32px rgba(0,0,0,0.5);background:#0f172a';c.appendChild(f);document.body.appendChild(c);}})(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard &#8599;</a>
1868
+ <a href="#" onclick="window.__tina4ToggleOverlay(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard &#8599;</a>
1770
1869
  <span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">&#10005;</span>
1771
1870
  </div>
1772
1871
  <script>
1872
+ // Overlay open/toggle helper + auto-restore. Persist the dev-admin
1873
+ // iframe's open/closed state across parent reloads so saving a file
1874
+ // (which kicks the watcher → location.reload) doesn't lose the
1875
+ // user's dev-admin chat / plan / file tree. Cross-framework parity
1876
+ // with PHP / Ruby / Node — same localStorage key, same shape.
1877
+ (function(){{
1878
+ var STATE_KEY = 'tina4_dev_overlay_open';
1879
+ function buildOverlay() {{
1880
+ var c = document.createElement('div');
1881
+ c.id = 'tina4-dev-panel';
1882
+ c.style.cssText = 'position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all 0.2s';
1883
+ var f = document.createElement('iframe');
1884
+ f.src = '/__dev';
1885
+ f.style.cssText = 'width:100%;height:100%;border:1px solid #3572A5;border-radius:0.5rem;box-shadow:0 8px 32px rgba(0,0,0,0.5);background:#0f172a';
1886
+ c.appendChild(f);
1887
+ document.body.appendChild(c);
1888
+ return c;
1889
+ }}
1890
+ window.__tina4ToggleOverlay = function(e) {{
1891
+ if (e) e.preventDefault();
1892
+ var p = document.getElementById('tina4-dev-panel');
1893
+ if (p) {{
1894
+ var hide = p.style.display !== 'none';
1895
+ p.style.display = hide ? 'none' : 'block';
1896
+ try {{ localStorage.setItem(STATE_KEY, hide ? '0' : '1'); }} catch (_) {{}}
1897
+ return;
1898
+ }}
1899
+ buildOverlay();
1900
+ try {{ localStorage.setItem(STATE_KEY, '1'); }} catch (_) {{}}
1901
+ }};
1902
+ function restoreIfOpen() {{
1903
+ try {{
1904
+ if (location.pathname.indexOf('/__dev') === 0) return;
1905
+ if (localStorage.getItem(STATE_KEY) === '1' && !document.getElementById('tina4-dev-panel')) {{
1906
+ buildOverlay();
1907
+ }}
1908
+ }} catch (_) {{}}
1909
+ }}
1910
+ if (document.readyState === 'loading') {{
1911
+ document.addEventListener('DOMContentLoaded', restoreIfOpen);
1912
+ }} else {{
1913
+ restoreIfOpen();
1914
+ }}
1915
+ }})();
1916
+ </script>
1917
+ <script>
1773
1918
  {'(function(){})();' if no_reload else f"""(function(){{
1774
1919
  var _t4_mtime=0,_t4_css_exts=['.css','.scss'],_t4_debounce=null;
1775
1920
  var _t4_interval=parseInt('{poll_interval_ms}')||3000;
@@ -2582,5 +2727,90 @@ async def _api_scaffold_run(request, response):
2582
2727
  return response({"ok": False, "error": str(exc)}, 500)
2583
2728
 
2584
2729
 
2730
+ _DOCS_SINGLETON = None # cached per-process so the framework index
2731
+ # builds once. User portion still mtime-refreshes
2732
+ # inside Docs.
2733
+
2734
+ def _docs_instance():
2735
+ """Lazy singleton for the Live Docs module — bound to the project
2736
+ cwd at first call. Subsequent calls reuse the same Docs instance,
2737
+ which keeps the framework index hot across requests while still
2738
+ refreshing the user portion when src/ files change."""
2739
+ global _DOCS_SINGLETON
2740
+ if _DOCS_SINGLETON is None:
2741
+ import os
2742
+ from tina4_python.docs import Docs
2743
+ _DOCS_SINGLETON = Docs(project_root=os.getcwd())
2744
+ return _DOCS_SINGLETON
2745
+
2746
+
2747
+ async def _api_docs_search(request, response):
2748
+ """GET /__dev/api/docs/search?q=...&k=...&source=...&include_private=..."""
2749
+ q = (request.query.get("q") or "").strip() if hasattr(request, "query") else ""
2750
+ if not q:
2751
+ return response({"ok": False, "error": "missing required 'q' param"}, 400)
2752
+ try:
2753
+ k = int(request.query.get("k", 5))
2754
+ except (TypeError, ValueError):
2755
+ k = 5
2756
+ source = request.query.get("source", "all")
2757
+ include_private = (request.query.get("include_private", "")
2758
+ or "").lower() in ("1", "true", "yes")
2759
+ import time
2760
+ t0 = time.perf_counter()
2761
+ hits = _docs_instance().search(q, k=k, source=source, include_private=include_private)
2762
+ took_ms = int((time.perf_counter() - t0) * 1000)
2763
+ return response({"ok": True, "query": q, "results": hits, "took_ms": took_ms})
2764
+
2765
+
2766
+ async def _api_docs_class(request, response):
2767
+ """GET /__dev/api/docs/class?name=<fqn>"""
2768
+ name = (request.query.get("name") or "").strip()
2769
+ if not name:
2770
+ return response({"ok": False, "error": "missing required 'name' param"}, 400)
2771
+ spec = _docs_instance().class_spec(name)
2772
+ if spec is None:
2773
+ return response({"ok": False, "error": f"class not found: {name}"}, 404)
2774
+ return response({"ok": True, "class": spec})
2775
+
2776
+
2777
+ async def _api_docs_method(request, response):
2778
+ """GET /__dev/api/docs/method?class=<fqn>&name=<method>"""
2779
+ cls = (request.query.get("class") or "").strip()
2780
+ name = (request.query.get("name") or "").strip()
2781
+ if not cls or not name:
2782
+ return response({"ok": False, "error": "both 'class' and 'name' params are required"}, 400)
2783
+ spec = _docs_instance().method_spec(cls, name)
2784
+ if spec is None:
2785
+ return response({"ok": False, "error": f"method not found: {cls}.{name}"}, 404)
2786
+ return response({"ok": True, "method": spec})
2787
+
2788
+
2789
+ async def _api_docs_index(request, response):
2790
+ """GET /__dev/api/docs/index?source=<framework|user|all>"""
2791
+ source = request.query.get("source", "all")
2792
+ entities = _docs_instance().index()
2793
+ if source != "all":
2794
+ entities = [e for e in entities if e.get("source") == source]
2795
+ return response({"ok": True, "count": len(entities), "entities": entities})
2796
+
2797
+
2798
+ async def _api_docs_well_known(request, response):
2799
+ """Public well-known doc — describes what the docs surface offers
2800
+ so non-MCP AI tools know what endpoints to call."""
2801
+ return response({
2802
+ "ok": True,
2803
+ "service": "tina4-live-docs",
2804
+ "version": "1",
2805
+ "endpoints": {
2806
+ "search": "/__dev/api/docs/search?q={query}&k={int}&source={framework|user|all}",
2807
+ "class": "/__dev/api/docs/class?name={fqn}",
2808
+ "method": "/__dev/api/docs/method?class={fqn}&name={method}",
2809
+ "index": "/__dev/api/docs/index?source={framework|user|all}",
2810
+ },
2811
+ "description": "Live API reflection for this Tina4 project — framework + user code combined.",
2812
+ })
2813
+
2814
+
2585
2815
  __all__ = ["MessageLog", "RequestInspector", "BrokenTracker",
2586
2816
  "get_api_handlers", "render_dev_toolbar"]