tina4-python 3.13.71__tar.gz → 3.13.72__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.13.71 → tina4_python-3.13.72}/PKG-INFO +1 -1
- {tina4_python-3.13.71 → tina4_python-3.13.72}/pyproject.toml +1 -1
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/cli/__init__.py +26 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/dev_admin/__init__.py +355 -18
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/frond/engine.py +149 -32
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/tina4-dev-admin.js +203 -170
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/tina4-dev-admin.min.js +203 -170
- {tina4_python-3.13.71 → tina4_python-3.13.72}/.gitignore +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/README.md +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/context/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/context/chunker.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/env.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/Attachment.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/Channel.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/ChannelMember.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/Message.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/Workspace.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/models/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/realtime/storage.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.71 → tina4_python-3.13.72}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -513,6 +513,32 @@ def _init(args):
|
|
|
513
513
|
encoding="utf-8",
|
|
514
514
|
)
|
|
515
515
|
|
|
516
|
+
# Create pyproject.toml — a Tina4 project is a uv project. The Dockerfiles
|
|
517
|
+
# already COPY pyproject.toml + uv.lock and `uv sync`, so a fresh project
|
|
518
|
+
# MUST ship a manifest. This is also what makes `deps/install` (uv add) and
|
|
519
|
+
# `uv run pytest` work out of the box. pytest ships in the dev group so the
|
|
520
|
+
# scaffolded tests are runnable from the first commit (tests-first).
|
|
521
|
+
pyproject = target / "pyproject.toml"
|
|
522
|
+
if not pyproject.exists():
|
|
523
|
+
project_name = target.resolve().name.replace(" ", "-").lower() or "tina4-app"
|
|
524
|
+
pyproject.write_text(
|
|
525
|
+
'[project]\n'
|
|
526
|
+
f'name = "{project_name}"\n'
|
|
527
|
+
'version = "0.1.0"\n'
|
|
528
|
+
'description = "A Tina4 application"\n'
|
|
529
|
+
'requires-python = ">=3.11"\n'
|
|
530
|
+
'dependencies = [\n'
|
|
531
|
+
' "tina4-python",\n'
|
|
532
|
+
']\n\n'
|
|
533
|
+
'[dependency-groups]\n'
|
|
534
|
+
'dev = [\n'
|
|
535
|
+
' "pytest",\n'
|
|
536
|
+
']\n\n'
|
|
537
|
+
'[tool.uv]\n'
|
|
538
|
+
'package = false\n',
|
|
539
|
+
encoding="utf-8",
|
|
540
|
+
)
|
|
541
|
+
|
|
516
542
|
# Create .gitignore
|
|
517
543
|
gitignore = target / ".gitignore"
|
|
518
544
|
if not gitignore.exists():
|
|
@@ -500,6 +500,21 @@ def get_api_handlers() -> dict:
|
|
|
500
500
|
# without shelling out from the browser.
|
|
501
501
|
"/__dev/api/scaffold": ("GET", _api_scaffold_list),
|
|
502
502
|
"/__dev/api/scaffold/run": ("POST", _api_scaffold_run),
|
|
503
|
+
# ── Run-chip endpoints (project-level operations) ──
|
|
504
|
+
# The dev-admin SPA's ▶ Migrate / ▶ Test / ▶ Seed chips call
|
|
505
|
+
# these. Each runs the whole-project operation via the framework's
|
|
506
|
+
# own machinery (matching the CLI), NOT by shelling out redundantly.
|
|
507
|
+
# /seed/run is distinct from /seed above (which seeds one named
|
|
508
|
+
# table); /run seeds every discovered ORM model.
|
|
509
|
+
"/__dev/api/migrate": ("POST", _api_migrate),
|
|
510
|
+
"/__dev/api/test": ("POST", _api_test),
|
|
511
|
+
"/__dev/api/seed/run": ("POST", _api_seed_run),
|
|
512
|
+
# ── Framework-grounding (mcp.tina4.com) token config ──
|
|
513
|
+
# Self-contained in Python (writes/reads the project .env
|
|
514
|
+
# directly) — does NOT depend on the Rust agent being up, unlike
|
|
515
|
+
# the Node dev-admin which proxies these to /mcp/status + /mcp/token.
|
|
516
|
+
"/__dev/api/grounding/status": ("GET", _api_grounding_status),
|
|
517
|
+
"/__dev/api/grounding/token": ("POST", _api_grounding_token),
|
|
503
518
|
# ── Live Docs (per plan/v3/22-LIVE-API-RAG.md) ──
|
|
504
519
|
# Thin HTTP wrappers around tina4_python.docs.Docs. Both
|
|
505
520
|
# framework public API and the user's src/ surface are
|
|
@@ -2731,46 +2746,57 @@ async def _api_deps_install(request, response):
|
|
|
2731
2746
|
name = body.get("name", "").strip()
|
|
2732
2747
|
version = body.get("version", "").strip()
|
|
2733
2748
|
registry = body.get("registry", "pypi")
|
|
2749
|
+
# A dev/test dependency (pytest, etc.) goes in the manifest's dev group, not
|
|
2750
|
+
# runtime deps. The install must PERSIST to the manifest — so we prefer the
|
|
2751
|
+
# project's dependency manager (uv/bundle) over a bare pip/gem install.
|
|
2752
|
+
dev = bool(body.get("dev"))
|
|
2734
2753
|
|
|
2735
2754
|
if not name:
|
|
2736
2755
|
return response({"error": "name required"}, 400)
|
|
2737
2756
|
|
|
2757
|
+
import shutil
|
|
2758
|
+
|
|
2738
2759
|
try:
|
|
2739
2760
|
if registry == "pypi":
|
|
2740
2761
|
pkg = f"{name}>={version}" if version else name
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2762
|
+
# uv add persists to pyproject.toml AND installs into the project's
|
|
2763
|
+
# venv. Fall back to pip only when uv or pyproject.toml is absent.
|
|
2764
|
+
if shutil.which("uv") and Path("pyproject.toml").exists():
|
|
2765
|
+
cmd = ["uv", "add", *(["--dev"] if dev else []), pkg]
|
|
2766
|
+
else:
|
|
2767
|
+
cmd = ["pip", "install", pkg]
|
|
2768
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
2745
2769
|
if result.returncode != 0:
|
|
2746
|
-
return response({"error": result.stderr.strip()}, 500)
|
|
2747
|
-
return response({"message": f"Installed {name} {version}".strip(), "output": result.stdout})
|
|
2770
|
+
return response({"error": (result.stderr or result.stdout).strip()}, 500)
|
|
2771
|
+
return response({"message": f"Installed {name} {version}".strip(), "output": result.stdout or result.stderr})
|
|
2748
2772
|
|
|
2749
2773
|
elif registry == "npm":
|
|
2750
2774
|
pkg = f"{name}@{version}" if version else name
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
capture_output=True, text=True, timeout=60
|
|
2754
|
-
)
|
|
2775
|
+
cmd = ["npm", "install", *(["--save-dev"] if dev else []), pkg]
|
|
2776
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
2755
2777
|
if result.returncode != 0:
|
|
2756
2778
|
return response({"error": result.stderr.strip()}, 500)
|
|
2757
2779
|
return response({"message": f"Installed {name}", "output": result.stdout})
|
|
2758
2780
|
|
|
2759
2781
|
elif registry == "packagist":
|
|
2760
2782
|
pkg = f"{name}:{version}" if version else name
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
capture_output=True, text=True, timeout=60
|
|
2764
|
-
)
|
|
2783
|
+
cmd = ["composer", "require", *(["--dev"] if dev else []), pkg]
|
|
2784
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
2765
2785
|
if result.returncode != 0:
|
|
2766
2786
|
return response({"error": result.stderr.strip()}, 500)
|
|
2767
2787
|
return response({"message": f"Installed {name}", "output": result.stdout})
|
|
2768
2788
|
|
|
2769
2789
|
elif registry == "rubygems":
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2790
|
+
# bundle add persists to the Gemfile. Fall back to gem install.
|
|
2791
|
+
if shutil.which("bundle") and Path("Gemfile").exists():
|
|
2792
|
+
cmd = ["bundle", "add", name]
|
|
2793
|
+
if version:
|
|
2794
|
+
cmd += ["--version", version]
|
|
2795
|
+
if dev:
|
|
2796
|
+
cmd += ["--group", "development"]
|
|
2797
|
+
else:
|
|
2798
|
+
cmd = ["gem", "install", name, *(["-v", version] if version else [])]
|
|
2799
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
2774
2800
|
if result.returncode != 0:
|
|
2775
2801
|
return response({"error": result.stderr.strip()}, 500)
|
|
2776
2802
|
return response({"message": f"Installed {name}", "output": result.stdout})
|
|
@@ -3130,6 +3156,317 @@ async def _api_scaffold_run(request, response):
|
|
|
3130
3156
|
return response({"ok": False, "error": str(exc)}, 500)
|
|
3131
3157
|
|
|
3132
3158
|
|
|
3159
|
+
# ─── Run-chip endpoints (migrate / test / seed-all) ────────────────
|
|
3160
|
+
#
|
|
3161
|
+
# Project-level operations the dev-admin SPA's ▶ Migrate / ▶ Test /
|
|
3162
|
+
# ▶ Seed chips fire. Distinct from /scaffold/run (creates one file) and
|
|
3163
|
+
# /seed (seeds one named table): these run the whole-project operation
|
|
3164
|
+
# via the framework's own in-process machinery — the same code paths
|
|
3165
|
+
# the CLI drives — rather than shelling out redundantly. Response shapes
|
|
3166
|
+
# match the shared dev-admin SPA (and the Node dev-admin reference).
|
|
3167
|
+
|
|
3168
|
+
|
|
3169
|
+
def _migration_stem(filename: str) -> str:
|
|
3170
|
+
"""'000001_create_users.sql' → '000001_create_users' (drop the suffix)."""
|
|
3171
|
+
return Path(filename).stem
|
|
3172
|
+
|
|
3173
|
+
|
|
3174
|
+
async def _api_migrate(request, response):
|
|
3175
|
+
"""POST /__dev/api/migrate — apply pending migrations.
|
|
3176
|
+
|
|
3177
|
+
Reuses the exact runner the CLI's ``_migrate`` and the ``_api_tool``
|
|
3178
|
+
'migrate' tool drive (``tina4_python.migration.Migration.migrate``),
|
|
3179
|
+
but in-process so we can report the split result the SPA reads:
|
|
3180
|
+
|
|
3181
|
+
{applied: [...], skipped: [...], failed: [...]}
|
|
3182
|
+
|
|
3183
|
+
- applied: migration ids run during this call
|
|
3184
|
+
- skipped: migration ids already applied before this call
|
|
3185
|
+
- failed: a migration that raised — the runner stops and rolls back
|
|
3186
|
+
at the first failure, so at most one lands here
|
|
3187
|
+
"""
|
|
3188
|
+
import re
|
|
3189
|
+
|
|
3190
|
+
from tina4_python.database import Database
|
|
3191
|
+
from tina4_python.migration import Migration
|
|
3192
|
+
|
|
3193
|
+
body = request.body if hasattr(request, "body") and request.body else {}
|
|
3194
|
+
mig_dir = body.get("dir", "migrations") if isinstance(body, dict) else "migrations"
|
|
3195
|
+
db_url = os.environ.get("TINA4_DATABASE_URL", "sqlite:///data/app.db")
|
|
3196
|
+
db = Database(db_url)
|
|
3197
|
+
try:
|
|
3198
|
+
mig = Migration(db, mig_dir)
|
|
3199
|
+
completed_before = {m["migration_name"] for m in mig.status()["completed"]}
|
|
3200
|
+
applied: list[str] = []
|
|
3201
|
+
failed: list[str] = []
|
|
3202
|
+
try:
|
|
3203
|
+
ran = mig.migrate() # list of applied filenames
|
|
3204
|
+
applied = [_migration_stem(f) for f in ran]
|
|
3205
|
+
except Exception as e:
|
|
3206
|
+
# The runner rolls back + re-raises at the first failing
|
|
3207
|
+
# migration. Recover what actually got applied from the
|
|
3208
|
+
# tracking table, and pull the failed file out of the message
|
|
3209
|
+
# ("Migration failed: <file> — <err>").
|
|
3210
|
+
completed_after = {m["migration_name"] for m in mig.status()["completed"]}
|
|
3211
|
+
applied = sorted(completed_after - completed_before)
|
|
3212
|
+
msg = str(e)
|
|
3213
|
+
match = re.search(r"Migration failed:\s*([^\s—]+)", msg)
|
|
3214
|
+
failed = [_migration_stem(match.group(1))] if match else [msg]
|
|
3215
|
+
skipped = sorted(completed_before)
|
|
3216
|
+
MessageLog.log(
|
|
3217
|
+
"migrate", f"Applied {len(applied)} migration(s)",
|
|
3218
|
+
{"applied": applied, "failed": failed},
|
|
3219
|
+
)
|
|
3220
|
+
return response({"applied": applied, "skipped": skipped, "failed": failed})
|
|
3221
|
+
finally:
|
|
3222
|
+
db.close()
|
|
3223
|
+
|
|
3224
|
+
|
|
3225
|
+
async def _api_test(request, response):
|
|
3226
|
+
"""POST /__dev/api/test — run the project's pytest suite.
|
|
3227
|
+
|
|
3228
|
+
Returns ``{ok, code, output}`` where ``code`` is the pytest process
|
|
3229
|
+
exit code and ``output`` is the combined stdout+stderr. A non-zero
|
|
3230
|
+
exit (failing tests) is a valid, reportable result — NOT a 500 —
|
|
3231
|
+
mirroring the ``_api_tool`` 'test' invocation but in the SPA's chip
|
|
3232
|
+
shape.
|
|
3233
|
+
"""
|
|
3234
|
+
import subprocess
|
|
3235
|
+
import sys
|
|
3236
|
+
|
|
3237
|
+
try:
|
|
3238
|
+
result = subprocess.run(
|
|
3239
|
+
[sys.executable, "-m", "pytest", "tests/", "-q", "--tb=short"],
|
|
3240
|
+
capture_output=True, text=True, timeout=180, cwd=os.getcwd(),
|
|
3241
|
+
)
|
|
3242
|
+
output = result.stdout or ""
|
|
3243
|
+
if result.stderr:
|
|
3244
|
+
output += ("\n" if output else "") + result.stderr
|
|
3245
|
+
MessageLog.log("test", f"pytest exited {result.returncode}",
|
|
3246
|
+
{"code": result.returncode})
|
|
3247
|
+
return response({
|
|
3248
|
+
"ok": result.returncode == 0,
|
|
3249
|
+
"code": result.returncode,
|
|
3250
|
+
"output": output.strip(),
|
|
3251
|
+
})
|
|
3252
|
+
except subprocess.TimeoutExpired:
|
|
3253
|
+
return response({"ok": False, "code": -1,
|
|
3254
|
+
"output": "Test suite timed out after 180 seconds"})
|
|
3255
|
+
except Exception as e:
|
|
3256
|
+
return response({"ok": False, "error": str(e)}, 500)
|
|
3257
|
+
|
|
3258
|
+
|
|
3259
|
+
def _discover_orm_models() -> list:
|
|
3260
|
+
"""Import ORM model modules from ``src/orm/`` (then ``src/models/`` as
|
|
3261
|
+
a fallback) and return the discovered ORM subclasses.
|
|
3262
|
+
|
|
3263
|
+
Same directories the server discovers on startup; importing the
|
|
3264
|
+
modules ensures the classes are registered as ORM subclasses even if
|
|
3265
|
+
nothing has referenced them yet this process. Mirrors the import
|
|
3266
|
+
strategy in ``AutoCrud.discover``.
|
|
3267
|
+
"""
|
|
3268
|
+
import importlib
|
|
3269
|
+
import importlib.util
|
|
3270
|
+
import inspect
|
|
3271
|
+
import sys
|
|
3272
|
+
|
|
3273
|
+
from tina4_python.orm import ORM
|
|
3274
|
+
|
|
3275
|
+
seen: set[int] = set()
|
|
3276
|
+
classes: list = []
|
|
3277
|
+
for models_dir in ("src/orm", "src/models"):
|
|
3278
|
+
if not os.path.isdir(models_dir):
|
|
3279
|
+
continue
|
|
3280
|
+
abs_dir = os.path.abspath(models_dir)
|
|
3281
|
+
parent = os.path.dirname(abs_dir)
|
|
3282
|
+
if parent not in sys.path:
|
|
3283
|
+
sys.path.insert(0, parent)
|
|
3284
|
+
module_prefix = f"{os.path.basename(parent)}.{os.path.basename(abs_dir)}"
|
|
3285
|
+
for filename in sorted(os.listdir(models_dir)):
|
|
3286
|
+
if not filename.endswith(".py") or filename.startswith("_"):
|
|
3287
|
+
continue
|
|
3288
|
+
module_name = filename[:-3]
|
|
3289
|
+
try:
|
|
3290
|
+
mod = importlib.import_module(f"{module_prefix}.{module_name}")
|
|
3291
|
+
except Exception:
|
|
3292
|
+
try:
|
|
3293
|
+
spec = importlib.util.spec_from_file_location(
|
|
3294
|
+
module_name, os.path.join(abs_dir, filename))
|
|
3295
|
+
if spec is None or spec.loader is None:
|
|
3296
|
+
continue
|
|
3297
|
+
mod = importlib.util.module_from_spec(spec)
|
|
3298
|
+
sys.modules[module_name] = mod
|
|
3299
|
+
spec.loader.exec_module(mod)
|
|
3300
|
+
except Exception:
|
|
3301
|
+
continue
|
|
3302
|
+
for attr_name in dir(mod):
|
|
3303
|
+
attr = getattr(mod, attr_name)
|
|
3304
|
+
if (inspect.isclass(attr) and issubclass(attr, ORM)
|
|
3305
|
+
and attr is not ORM and id(attr) not in seen):
|
|
3306
|
+
try:
|
|
3307
|
+
has_table = bool(attr._get_table())
|
|
3308
|
+
except Exception:
|
|
3309
|
+
has_table = False
|
|
3310
|
+
if has_table:
|
|
3311
|
+
seen.add(id(attr))
|
|
3312
|
+
classes.append(attr)
|
|
3313
|
+
return classes
|
|
3314
|
+
|
|
3315
|
+
|
|
3316
|
+
async def _api_seed_run(request, response):
|
|
3317
|
+
"""POST /__dev/api/seed/run — run the whole-project seeder.
|
|
3318
|
+
|
|
3319
|
+
Distinct from ``/__dev/api/seed`` (which seeds one named table): this
|
|
3320
|
+
discovers every ORM model in ``src/orm/`` (``src/models/`` fallback)
|
|
3321
|
+
and seeds them all via the framework's FK-ordered ``seed_models()`` —
|
|
3322
|
+
the same real seeder the CLI uses. Returns ``{seeded, failed}``.
|
|
3323
|
+
"""
|
|
3324
|
+
from tina4_python.seeder import seed_models
|
|
3325
|
+
|
|
3326
|
+
body = request.body if hasattr(request, "body") and request.body else {}
|
|
3327
|
+
count = 10
|
|
3328
|
+
if isinstance(body, dict) and body.get("count") is not None:
|
|
3329
|
+
try:
|
|
3330
|
+
count = int(body.get("count"))
|
|
3331
|
+
except (TypeError, ValueError):
|
|
3332
|
+
count = 10
|
|
3333
|
+
|
|
3334
|
+
try:
|
|
3335
|
+
classes = _discover_orm_models()
|
|
3336
|
+
if not classes:
|
|
3337
|
+
return response(
|
|
3338
|
+
{"seeded": 0, "failed": 0,
|
|
3339
|
+
"error": "No models found in src/orm/ or src/models/"}, 400)
|
|
3340
|
+
summaries = seed_models(classes, count)
|
|
3341
|
+
seeded = 0
|
|
3342
|
+
failed = 0
|
|
3343
|
+
for summary in summaries.values():
|
|
3344
|
+
seeded += getattr(summary, "seeded", 0)
|
|
3345
|
+
failed += getattr(summary, "failed", 0)
|
|
3346
|
+
MessageLog.log(
|
|
3347
|
+
"seed", f"Seeded {seeded} rows across {len(summaries)} model(s)",
|
|
3348
|
+
{"seeded": seeded, "failed": failed},
|
|
3349
|
+
)
|
|
3350
|
+
return response({"seeded": seeded, "failed": failed})
|
|
3351
|
+
except Exception as e:
|
|
3352
|
+
return response({"seeded": 0, "failed": 0, "error": str(e)}, 500)
|
|
3353
|
+
|
|
3354
|
+
|
|
3355
|
+
# ─── Framework-grounding (mcp.tina4.com) token config ──────────────
|
|
3356
|
+
#
|
|
3357
|
+
# Self-contained: reads/writes TINA4_MCP_TOKEN in the process env and
|
|
3358
|
+
# the project .env directly. The Node dev-admin proxies these to the
|
|
3359
|
+
# Rust agent (/mcp/status, /mcp/token); Python keeps them local so the
|
|
3360
|
+
# token panel works with a bare `tina4 serve` — no agent required.
|
|
3361
|
+
|
|
3362
|
+
_MCP_DEFAULT_URL = "https://mcp.tina4.com"
|
|
3363
|
+
|
|
3364
|
+
|
|
3365
|
+
def _env_file_values() -> dict:
|
|
3366
|
+
"""Parse the project ``.env`` into a plain dict (best-effort).
|
|
3367
|
+
|
|
3368
|
+
Missing file or read error → ``{}``. Strips surrounding quotes so a
|
|
3369
|
+
quoted value round-trips to the same string the process env would hold.
|
|
3370
|
+
"""
|
|
3371
|
+
values: dict = {}
|
|
3372
|
+
env_path = Path(".env")
|
|
3373
|
+
if env_path.exists():
|
|
3374
|
+
try:
|
|
3375
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
3376
|
+
s = line.strip()
|
|
3377
|
+
if not s or s.startswith("#") or "=" not in s:
|
|
3378
|
+
continue
|
|
3379
|
+
key, _, val = s.partition("=")
|
|
3380
|
+
values[key.strip()] = val.strip().strip('"').strip("'")
|
|
3381
|
+
except OSError:
|
|
3382
|
+
pass
|
|
3383
|
+
return values
|
|
3384
|
+
|
|
3385
|
+
|
|
3386
|
+
def _resolve_env(key: str, default: str = "") -> str:
|
|
3387
|
+
"""Resolve an env var: process environment first, then project ``.env``."""
|
|
3388
|
+
val = os.environ.get(key)
|
|
3389
|
+
if val:
|
|
3390
|
+
return val
|
|
3391
|
+
return _env_file_values().get(key, default)
|
|
3392
|
+
|
|
3393
|
+
|
|
3394
|
+
def _upsert_env_var(key: str, value: str) -> None:
|
|
3395
|
+
"""Write/upsert ``KEY=value`` into the project ``.env``.
|
|
3396
|
+
|
|
3397
|
+
An empty ``value`` removes the key entirely (clear). Preserves
|
|
3398
|
+
comments, other keys, and a trailing newline. Mirrors the .env-write
|
|
3399
|
+
pattern in ``_api_connections_save``.
|
|
3400
|
+
"""
|
|
3401
|
+
env_path = Path(".env")
|
|
3402
|
+
lines = env_path.read_text(encoding="utf-8").splitlines() if env_path.exists() else []
|
|
3403
|
+
out: list[str] = []
|
|
3404
|
+
found = False
|
|
3405
|
+
for line in lines:
|
|
3406
|
+
s = line.strip()
|
|
3407
|
+
if s.startswith("#") or "=" not in s:
|
|
3408
|
+
out.append(line)
|
|
3409
|
+
continue
|
|
3410
|
+
k = s.split("=", 1)[0].strip()
|
|
3411
|
+
if k == key:
|
|
3412
|
+
found = True
|
|
3413
|
+
if value:
|
|
3414
|
+
out.append(f"{key}={value}")
|
|
3415
|
+
# empty value → drop the line (clear)
|
|
3416
|
+
else:
|
|
3417
|
+
out.append(line)
|
|
3418
|
+
if value and not found:
|
|
3419
|
+
out.append(f"{key}={value}")
|
|
3420
|
+
content = "\n".join(out)
|
|
3421
|
+
if content and not content.endswith("\n"):
|
|
3422
|
+
content += "\n"
|
|
3423
|
+
env_path.write_text(content, encoding="utf-8")
|
|
3424
|
+
|
|
3425
|
+
|
|
3426
|
+
def _grounding_snapshot() -> dict:
|
|
3427
|
+
token = _resolve_env("TINA4_MCP_TOKEN", "")
|
|
3428
|
+
url = _resolve_env("TINA4_MCP_URL", "") or _MCP_DEFAULT_URL
|
|
3429
|
+
return {
|
|
3430
|
+
"configured": bool(token),
|
|
3431
|
+
"last4": token[-4:] if token else "",
|
|
3432
|
+
"url": url,
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
|
|
3436
|
+
async def _api_grounding_status(request, response):
|
|
3437
|
+
"""GET /__dev/api/grounding/status — framework-grounding token config.
|
|
3438
|
+
|
|
3439
|
+
``{configured, last4, url}``. ``configured`` = TINA4_MCP_TOKEN is set
|
|
3440
|
+
(process env or project .env). ``last4`` = last 4 chars of the token
|
|
3441
|
+
(or ""). ``url`` = TINA4_MCP_URL or the mcp.tina4.com default.
|
|
3442
|
+
"""
|
|
3443
|
+
return response(_grounding_snapshot())
|
|
3444
|
+
|
|
3445
|
+
|
|
3446
|
+
async def _api_grounding_token(request, response):
|
|
3447
|
+
"""POST /__dev/api/grounding/token {token} — upsert TINA4_MCP_TOKEN.
|
|
3448
|
+
|
|
3449
|
+
Writes the token into the project ``.env`` (empty token clears it) and
|
|
3450
|
+
updates the running process env so ``/status`` reflects the change
|
|
3451
|
+
immediately, before any restart re-reads ``.env``. Self-contained —
|
|
3452
|
+
never touches the Rust agent. Returns ``{ok, configured, last4}``.
|
|
3453
|
+
"""
|
|
3454
|
+
body = request.body if hasattr(request, "body") and request.body else {}
|
|
3455
|
+
token = str(body.get("token") or "").strip() if isinstance(body, dict) else ""
|
|
3456
|
+
try:
|
|
3457
|
+
_upsert_env_var("TINA4_MCP_TOKEN", token)
|
|
3458
|
+
except OSError as e:
|
|
3459
|
+
return response({"ok": False, "error": str(e)}, 500)
|
|
3460
|
+
if token:
|
|
3461
|
+
os.environ["TINA4_MCP_TOKEN"] = token
|
|
3462
|
+
else:
|
|
3463
|
+
os.environ.pop("TINA4_MCP_TOKEN", None)
|
|
3464
|
+
MessageLog.log("grounding", "Updated TINA4_MCP_TOKEN",
|
|
3465
|
+
{"configured": bool(token)})
|
|
3466
|
+
return response({"ok": True, "configured": bool(token),
|
|
3467
|
+
"last4": token[-4:] if token else ""})
|
|
3468
|
+
|
|
3469
|
+
|
|
3133
3470
|
_DOCS_SINGLETON = None # cached per-process so the framework index
|
|
3134
3471
|
# builds once. User portion still mtime-refreshes
|
|
3135
3472
|
# inside Docs.
|