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.
- {tina4_python-3.11.23 → tina4_python-3.11.32}/.gitignore +4 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/PKG-INFO +1 -1
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/__init__.py +1 -1
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/connection.py +32 -1
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/dev_admin/__init__.py +231 -1
- tina4_python-3.11.32/tina4_python/docs.py +821 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/mcp/tools.py +31 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/js/tina4-dev-admin.js +121 -121
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/js/tina4-dev-admin.min.js +121 -121
- {tina4_python-3.11.23 → tina4_python-3.11.32}/README.md +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/pyproject.toml +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/Testing.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/events.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/request.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/response.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/router.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/core/server.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.11.23 → tina4_python-3.11.32}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -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
|
|
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="
|
|
1868
|
+
<a href="#" onclick="window.__tina4ToggleOverlay(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard ↗</a>
|
|
1770
1869
|
<span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">✕</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"]
|