tina4-python 3.13.34__tar.gz → 3.13.36__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.34 → tina4_python-3.13.36}/PKG-INFO +1 -1
- {tina4_python-3.13.34 → tina4_python-3.13.36}/pyproject.toml +1 -1
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/CLAUDE.md +11 -2
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/core/router.py +14 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/core/server.py +82 -5
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/dev_admin/__init__.py +102 -7
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/mcp/tools.py +1 -1
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/js/tina4-dev-admin.js +44 -42
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/js/tina4-dev-admin.min.js +44 -42
- {tina4_python-3.13.34 → tina4_python-3.13.36}/.gitignore +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/README.md +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/env.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.34 → tina4_python-3.13.36}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -409,9 +409,18 @@ Set `TINA4_DEBUG=true` in `.env` to enable development features:
|
|
|
409
409
|
- **CSS hot-reload** — SCSS/CSS changes refresh stylesheets without full page reload
|
|
410
410
|
- **SCSS auto-compile** — `.scss` files in `src/scss/` are compiled to `src/public/css/` on save
|
|
411
411
|
- **Error overlay** — Runtime errors display a rich, syntax-highlighted overlay in the browser
|
|
412
|
-
- **Route re-discovery** — `POST /__dev/api/reload` re-runs auto-discover
|
|
412
|
+
- **Route re-discovery & hot-reload** — `POST /__dev/api/reload` re-runs auto-discover. New files in `src/routes/`, `src/orm/`, or `src/app/` register without a server restart, AND changed `.py` files under `src/` are re-imported in-process (mtime-tracked) so editing an existing route hot-reloads its handler live — no restart needed. The Router replaces a re-registered `(method, path)` in place, so the fresh handler wins instead of being shadowed by the stale one. Only `src/` modules are ever re-imported; framework (`tina4_python.*`) modules are never touched. Honest caveat: a symbol imported BY NAME into another *unchanged* module (e.g. `from src.orm.User import User` inside a route file that itself didn't change) or an ORM class identity captured across modules may still need a restart — the edited file re-executes, but a different module that grabbed the old reference earlier keeps it until it too re-imports.
|
|
413
413
|
|
|
414
|
-
DevReload
|
|
414
|
+
### How DevReload works (WebSocket-primary)
|
|
415
|
+
|
|
416
|
+
DevReload is **WebSocket-primary** — the reload is instant, not polled:
|
|
417
|
+
|
|
418
|
+
1. The `tina4` Rust CLI watches `src/`, `migrations/`, `.env` and, on a real change, POSTs `/__dev/api/reload` to the **running** server. The CLI does **not** restart the worker process.
|
|
419
|
+
2. The server re-runs auto-discover — registering new `src/` files and re-importing changed ones **in-process** (mtime-tracked, `src/` only; framework modules never), so the worker keeps the same PID — then bumps its reload counter.
|
|
420
|
+
3. The server **broadcasts** a JSON message `{type, file, mtime}` to every browser connected on the `/__dev_reload` WebSocket (`type` is `"css"` for stylesheet changes, else `"reload"`). The dev-toolbar client and the dev-admin dashboard both connect here and act on it instantly: CSS changes swap `<link rel=stylesheet>` hrefs with a cache-bust query; everything else does a full `location.reload()`.
|
|
421
|
+
4. **Poll is fallback only.** The injected toolbar client stops polling the moment the socket connects, and only restarts the `/__dev/api/mtime` poll (every 3 s) when the socket drops — reconnecting after ~2 s. In normal operation there is no polling.
|
|
422
|
+
|
|
423
|
+
The `/__dev_reload` WebSocket route is registered automatically when `TINA4_DEBUG=true`. No configuration needed. Running without the Rust CLI (e.g. Docker, `TINA4_OVERRIDE_CLIENT=true`) means no automatic reload.
|
|
415
424
|
|
|
416
425
|
## Routing
|
|
417
426
|
|
|
@@ -340,6 +340,20 @@ class Router:
|
|
|
340
340
|
"swagger_meta": swagger_meta or options.get("swagger_meta", {}),
|
|
341
341
|
"template": template or options.get("template"),
|
|
342
342
|
}
|
|
343
|
+
# Replace semantics: re-registering the same (method, path) overwrites
|
|
344
|
+
# the existing entry in place rather than appending a second one.
|
|
345
|
+
# This is what makes dev hot-reload work — when a changed module is
|
|
346
|
+
# re-imported, its @get("/x") decorator runs again with a fresh handler,
|
|
347
|
+
# and ``match()`` returns the FIRST match, so a stale leftover would
|
|
348
|
+
# otherwise shadow the new handler forever. Overwriting keeps the
|
|
349
|
+
# registry free of duplicates and ensures the latest handler wins.
|
|
350
|
+
# Distinct (method, path) pairs are untouched — only an exact dup
|
|
351
|
+
# collapses onto the prior slot, preserving its position/order.
|
|
352
|
+
for i, existing in enumerate(_routes):
|
|
353
|
+
if existing["method"] == m and existing["path"] == path:
|
|
354
|
+
_routes[i] = route
|
|
355
|
+
Log.debug(f"Route replaced: {m} {path} (auth={'required' if auth_required else 'public'})")
|
|
356
|
+
return RouteRef(route)
|
|
343
357
|
_routes.append(route)
|
|
344
358
|
Log.debug(f"Route registered: {m} {path} (auth={'required' if auth_required else 'public'})")
|
|
345
359
|
return RouteRef(route)
|
|
@@ -48,12 +48,30 @@ def background(callback, interval: float = 1.0):
|
|
|
48
48
|
_background_tasks.append({"callback": callback, "interval": interval})
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
# module_name → source-file mtime at the last (re)import. Drives the
|
|
52
|
+
# changed-file detection in ``_auto_discover``: a file whose mtime is newer
|
|
53
|
+
# than the recorded value is re-executed in place, so editing an existing
|
|
54
|
+
# route hot-reloads on /__dev/api/reload without a server restart. Files we
|
|
55
|
+
# have never imported are absent from the map.
|
|
56
|
+
_discovered_mtimes: dict[str, float] = {}
|
|
57
|
+
|
|
58
|
+
|
|
51
59
|
def _auto_discover(root_dir: str = "src"):
|
|
52
60
|
"""Auto-import all .py files in ``root_dir`` to trigger route decorators.
|
|
53
61
|
|
|
54
|
-
Idempotent and re-runnable
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
Idempotent and re-runnable so re-discovery on /__dev/api/reload is cheap:
|
|
63
|
+
|
|
64
|
+
* **New** module (not in ``sys.modules``) → import it, record its mtime.
|
|
65
|
+
* **Changed** module (in ``sys.modules`` and its source mtime is newer than
|
|
66
|
+
the recorded value) → re-execute it (``del sys.modules`` + re-import) so
|
|
67
|
+
edits to an existing route take effect. The Router replaces same-(method,
|
|
68
|
+
path) registrations, so the re-imported handler wins instead of being
|
|
69
|
+
shadowed by the stale one.
|
|
70
|
+
* **Unchanged** module → skipped (keeps the re-runnable property cheap).
|
|
71
|
+
|
|
72
|
+
Only modules discovered under ``root_dir`` are ever re-imported — framework
|
|
73
|
+
(``tina4_python.*``) and third-party modules are never deleted/re-imported,
|
|
74
|
+
which would be catastrophic for shared singletons and class identity.
|
|
57
75
|
|
|
58
76
|
Import failures are recorded to ``data/.broken/`` so /health surfaces them
|
|
59
77
|
instead of swallowing them into a console line nobody reads.
|
|
@@ -62,6 +80,14 @@ def _auto_discover(root_dir: str = "src"):
|
|
|
62
80
|
if not root.is_dir():
|
|
63
81
|
return
|
|
64
82
|
|
|
83
|
+
# The package prefix every discovered module shares (e.g. "src"). The
|
|
84
|
+
# del+reimport path is gated on this so we can never evict a framework or
|
|
85
|
+
# third-party module from sys.modules even if a name somehow collides.
|
|
86
|
+
try:
|
|
87
|
+
root_pkg = root.relative_to(Path.cwd()).parts[0]
|
|
88
|
+
except (ValueError, IndexError):
|
|
89
|
+
root_pkg = root.name
|
|
90
|
+
|
|
65
91
|
# Folders to skip — non-Python sub-trees inside src/.
|
|
66
92
|
skip = {"public", "templates", "scss", "locales", "icons"}
|
|
67
93
|
# Routes folder is special-cased so the user gets a clear warning when
|
|
@@ -91,9 +117,32 @@ def _auto_discover(root_dir: str = "src"):
|
|
|
91
117
|
try:
|
|
92
118
|
rel = py_file.relative_to(Path.cwd()).with_suffix("")
|
|
93
119
|
module_name = ".".join(rel.parts)
|
|
120
|
+
try:
|
|
121
|
+
current_mtime = py_file.stat().st_mtime
|
|
122
|
+
except OSError:
|
|
123
|
+
current_mtime = 0.0
|
|
124
|
+
|
|
94
125
|
if module_name not in sys.modules:
|
|
126
|
+
# New module — import and remember its mtime.
|
|
95
127
|
importlib.import_module(module_name)
|
|
128
|
+
_discovered_mtimes[module_name] = current_mtime
|
|
96
129
|
Log.debug(f"Loaded: {module_name}")
|
|
130
|
+
elif current_mtime > _discovered_mtimes.get(module_name, 0.0):
|
|
131
|
+
# Changed module — re-execute so edits to an existing route
|
|
132
|
+
# take effect in-process. Scope guard: only ever evict a module
|
|
133
|
+
# that lives under our discovery package. Deleting a
|
|
134
|
+
# tina4_python.* / third-party module would break shared
|
|
135
|
+
# singletons and class identity — never do that here.
|
|
136
|
+
if module_name == root_pkg or module_name.startswith(root_pkg + "."):
|
|
137
|
+
del sys.modules[module_name]
|
|
138
|
+
importlib.import_module(module_name)
|
|
139
|
+
_discovered_mtimes[module_name] = current_mtime
|
|
140
|
+
Log.info(f"Reloaded changed module: {module_name}")
|
|
141
|
+
else:
|
|
142
|
+
# Out-of-scope module changed — record mtime so we don't
|
|
143
|
+
# keep re-evaluating it, but do not re-import it.
|
|
144
|
+
_discovered_mtimes[module_name] = current_mtime
|
|
145
|
+
# Unchanged module → skip (keeps re-discovery cheap/idempotent).
|
|
97
146
|
except Exception as e:
|
|
98
147
|
Log.error(f"Failed to load {py_file}: {e}")
|
|
99
148
|
_record_broken_import(py_file, e)
|
|
@@ -634,6 +683,30 @@ from tina4_python.websocket import WebSocketConnection, WebSocketManager
|
|
|
634
683
|
_ws_manager = WebSocketManager()
|
|
635
684
|
|
|
636
685
|
|
|
686
|
+
async def _dev_reload_ws(connection, event, data):
|
|
687
|
+
"""WebSocket handler for the dev-reload channel (/__dev_reload).
|
|
688
|
+
|
|
689
|
+
Connections are kept open and held by ``_ws_manager`` on the
|
|
690
|
+
``/__dev_reload`` path so ``POST /__dev/api/reload`` can broadcast an
|
|
691
|
+
instant reload to every browser. The framework never pushes anything from
|
|
692
|
+
the client side — incoming frames are ignored; the open socket is the
|
|
693
|
+
whole point. This restores the documented WebSocket-primary DevReload
|
|
694
|
+
design (the dashboard SPA and the injected dev-toolbar both connect here).
|
|
695
|
+
"""
|
|
696
|
+
return
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
_dev_reload_ws_registered = [False]
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _register_dev_reload_ws() -> None:
|
|
703
|
+
"""Register the /__dev_reload WebSocket route once (debug mode only)."""
|
|
704
|
+
if _dev_reload_ws_registered[0]:
|
|
705
|
+
return
|
|
706
|
+
Router.websocket("/__dev_reload", _dev_reload_ws)
|
|
707
|
+
_dev_reload_ws_registered[0] = True
|
|
708
|
+
|
|
709
|
+
|
|
637
710
|
async def _handle_asgi_websocket(scope: dict, receive, send):
|
|
638
711
|
"""Handle ASGI WebSocket connections, dispatching to registered routes."""
|
|
639
712
|
path = scope.get("path", "/")
|
|
@@ -2183,8 +2256,12 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
2183
2256
|
is_debug = is_truthy(os.environ.get("TINA4_DEBUG", ""))
|
|
2184
2257
|
|
|
2185
2258
|
# File watching is handled by the Rust CLI (tina4 serve). The framework
|
|
2186
|
-
# only needs to receive POST /__dev/api/reload
|
|
2187
|
-
#
|
|
2259
|
+
# only needs to receive POST /__dev/api/reload, re-import the changed
|
|
2260
|
+
# module in-process, and push an instant reload over the /__dev_reload
|
|
2261
|
+
# WebSocket. The mtime counter at /__dev/api/mtime is the polling
|
|
2262
|
+
# fallback for when that socket is down. No internal watcher.
|
|
2263
|
+
if is_debug:
|
|
2264
|
+
_register_dev_reload_ws()
|
|
2188
2265
|
|
|
2189
2266
|
prod = None
|
|
2190
2267
|
if not is_debug:
|
|
@@ -485,6 +485,12 @@ def get_api_handlers() -> dict:
|
|
|
485
485
|
# @mcp_tool decorator appear in both immediately.
|
|
486
486
|
"/__dev/api/mcp/tools": ("GET", _api_mcp_tools),
|
|
487
487
|
"/__dev/api/mcp/call": ("POST", _api_mcp_call),
|
|
488
|
+
# JSON-RPC + SSE surface for real MCP clients (Claude Desktop/Code).
|
|
489
|
+
# Same registry as the REST shim above; this is what makes /__dev/mcp
|
|
490
|
+
# actually reachable as an MCP server (previously defined but unmounted).
|
|
491
|
+
"/__dev/mcp": ("POST", _api_mcp_rpc),
|
|
492
|
+
"/__dev/mcp/message": ("POST", _api_mcp_rpc),
|
|
493
|
+
"/__dev/mcp/sse": ("GET", _api_mcp_sse),
|
|
488
494
|
# ── Scaffold REST shim ──
|
|
489
495
|
# Wraps the tina4python CLI's `generate <kind> <name>` so the
|
|
490
496
|
# + Route / + Model / + Migration / + Middleware buttons work
|
|
@@ -1973,6 +1979,25 @@ async def _api_reload(request, response):
|
|
|
1973
1979
|
except Exception as e:
|
|
1974
1980
|
Log.error(f"Re-discover on reload failed: {e}")
|
|
1975
1981
|
|
|
1982
|
+
# WebSocket-primary reload: push an instant message to every browser
|
|
1983
|
+
# connected on /__dev_reload. The toolbar client (and the dev-admin
|
|
1984
|
+
# dashboard) act on this immediately — the mtime poll above is only a
|
|
1985
|
+
# fallback for when the socket is down. CSS changes swap stylesheets;
|
|
1986
|
+
# everything else triggers a full page reload. We normalise the wire
|
|
1987
|
+
# `type` to "css"/"reload" so both clients react (the dashboard only
|
|
1988
|
+
# listens for reload/change/css), but the HTTP response still echoes the
|
|
1989
|
+
# caller's original type. Wrapped so a broadcast failure (or zero
|
|
1990
|
+
# clients) never 500s the reload endpoint.
|
|
1991
|
+
ws_type = "css" if reload_type == "css" else "reload"
|
|
1992
|
+
try:
|
|
1993
|
+
from tina4_python.core.server import _ws_manager
|
|
1994
|
+
await _ws_manager.broadcast(
|
|
1995
|
+
json.dumps({"type": ws_type, "file": _reload_file[0], "mtime": _reload_mtime[0]}),
|
|
1996
|
+
path="/__dev_reload",
|
|
1997
|
+
)
|
|
1998
|
+
except Exception as e:
|
|
1999
|
+
Log.error(f"Dev-reload WebSocket broadcast failed: {e}")
|
|
2000
|
+
|
|
1976
2001
|
return response({"ok": True, "type": reload_type})
|
|
1977
2002
|
|
|
1978
2003
|
|
|
@@ -2144,16 +2169,22 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
|
|
|
2144
2169
|
</script>
|
|
2145
2170
|
<script>
|
|
2146
2171
|
{'(function(){})();' if no_reload else f"""(function(){{
|
|
2147
|
-
|
|
2172
|
+
// WebSocket-primary dev reloader. The running server re-imports changed
|
|
2173
|
+
// src/ modules in-process and pushes a {{type,file,mtime}} message over
|
|
2174
|
+
// /__dev_reload — no respawn, instant refresh. The mtime poll below is a
|
|
2175
|
+
// FALLBACK only, started when the socket is down and stopped on connect.
|
|
2176
|
+
var _t4_css_exts=['.css','.scss'],_t4_debounce=null;
|
|
2148
2177
|
var _t4_interval=parseInt('{poll_interval_ms}')||3000;
|
|
2178
|
+
var _t4_ws=null,_t4_poll_timer=null,_t4_mtime=null;
|
|
2149
2179
|
function _t4_apply(d){{
|
|
2150
|
-
|
|
2151
|
-
var
|
|
2180
|
+
d=d||{{}};
|
|
2181
|
+
var f=d.file||'',t=d.type||'';
|
|
2182
|
+
var isCss=t==='css'||_t4_css_exts.some(function(e){{return f.endsWith(e)}});
|
|
2152
2183
|
if(isCss){{
|
|
2153
2184
|
var links=document.querySelectorAll('link[rel="stylesheet"]');
|
|
2154
2185
|
links.forEach(function(l){{
|
|
2155
2186
|
var href=l.getAttribute('href');
|
|
2156
|
-
if(href){{l.setAttribute('href',href.split('?')[0]+'?_t4='+d.mtime)}}
|
|
2187
|
+
if(href){{l.setAttribute('href',href.split('?')[0]+'?_t4='+(d.mtime||Date.now()))}}
|
|
2157
2188
|
}});
|
|
2158
2189
|
}}else{{
|
|
2159
2190
|
location.reload();
|
|
@@ -2161,15 +2192,39 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
|
|
|
2161
2192
|
}}
|
|
2162
2193
|
function _t4_poll(){{
|
|
2163
2194
|
fetch('/__dev/api/mtime').then(function(r){{return r.json()}}).then(function(d){{
|
|
2164
|
-
if(
|
|
2165
|
-
if(d.mtime
|
|
2195
|
+
if(_t4_mtime===null){{_t4_mtime=d.mtime;return;}}
|
|
2196
|
+
if(d.mtime!==_t4_mtime){{
|
|
2166
2197
|
_t4_mtime=d.mtime;
|
|
2167
2198
|
if(_t4_debounce)clearTimeout(_t4_debounce);
|
|
2168
2199
|
_t4_debounce=setTimeout(function(){{_t4_apply(d);}},500);
|
|
2169
2200
|
}}
|
|
2170
2201
|
}}).catch(function(){{}});
|
|
2171
2202
|
}}
|
|
2172
|
-
|
|
2203
|
+
function _t4_startPoll(){{
|
|
2204
|
+
if(_t4_poll_timer)return;
|
|
2205
|
+
_t4_mtime=null;
|
|
2206
|
+
_t4_poll_timer=setInterval(_t4_poll,_t4_interval);
|
|
2207
|
+
}}
|
|
2208
|
+
function _t4_stopPoll(){{
|
|
2209
|
+
if(_t4_poll_timer){{clearInterval(_t4_poll_timer);_t4_poll_timer=null;}}
|
|
2210
|
+
}}
|
|
2211
|
+
function _t4_connect(){{
|
|
2212
|
+
var url=(location.protocol==='https:'?'wss':'ws')+'://'+location.host+'/__dev_reload';
|
|
2213
|
+
try{{_t4_ws=new WebSocket(url);}}catch(_){{_t4_startPoll();return;}}
|
|
2214
|
+
_t4_ws.addEventListener('open',function(){{_t4_stopPoll();}});
|
|
2215
|
+
_t4_ws.addEventListener('message',function(ev){{
|
|
2216
|
+
var d=null;
|
|
2217
|
+
try{{d=typeof ev.data==='string'?JSON.parse(ev.data):null;}}catch(_){{}}
|
|
2218
|
+
if(!d)return;
|
|
2219
|
+
if(d.type==='reload'||d.type==='change'||d.type==='css'){{
|
|
2220
|
+
if(_t4_debounce)clearTimeout(_t4_debounce);
|
|
2221
|
+
_t4_debounce=setTimeout(function(){{_t4_apply(d);}},150);
|
|
2222
|
+
}}
|
|
2223
|
+
}});
|
|
2224
|
+
_t4_ws.addEventListener('close',function(){{_t4_ws=null;_t4_startPoll();setTimeout(_t4_connect,2000);}});
|
|
2225
|
+
_t4_ws.addEventListener('error',function(){{try{{_t4_ws&&_t4_ws.close();}}catch(_){{}}}});
|
|
2226
|
+
}}
|
|
2227
|
+
_t4_connect();
|
|
2173
2228
|
}})();"""}
|
|
2174
2229
|
function tina4VersionModal(){{
|
|
2175
2230
|
var m=document.getElementById('tina4-ver-modal');
|
|
@@ -2861,6 +2916,46 @@ async def _api_mcp_call(request, response):
|
|
|
2861
2916
|
return response({"ok": False, "name": name, "error": str(exc)}, 500)
|
|
2862
2917
|
|
|
2863
2918
|
|
|
2919
|
+
# ─── MCP JSON-RPC + SSE endpoint ───────────────────────────────────
|
|
2920
|
+
#
|
|
2921
|
+
# The protocol surface real MCP clients (Claude Desktop/Code) speak —
|
|
2922
|
+
# JSON-RPC 2.0 over the HTTP+SSE transport. Mounted on the running dev
|
|
2923
|
+
# server so each `tina4 serve`d project exposes its OWN endpoint, giving
|
|
2924
|
+
# an AI agent live access scoped to that project. Shares the same
|
|
2925
|
+
# `_default_server` tool registry as the REST shim above, so every
|
|
2926
|
+
# @mcp_tool shows up on both surfaces.
|
|
2927
|
+
|
|
2928
|
+
async def _api_mcp_rpc(request, response):
|
|
2929
|
+
"""POST — the JSON-RPC endpoint. Mounted at /__dev/mcp and
|
|
2930
|
+
/__dev/mcp/message. Forwards to the default MCP server's
|
|
2931
|
+
handle_message() and returns the JSON-RPC response; notifications
|
|
2932
|
+
(no id) yield an empty 204.
|
|
2933
|
+
"""
|
|
2934
|
+
import json as _json
|
|
2935
|
+
from tina4_python.mcp import _get_default_server, is_enabled
|
|
2936
|
+
if not is_enabled():
|
|
2937
|
+
return response({"error": "MCP disabled"}, 404)
|
|
2938
|
+
server = _get_default_server()
|
|
2939
|
+
body = request.body
|
|
2940
|
+
raw = body if isinstance(body, (dict, str)) else str(body)
|
|
2941
|
+
result = server.handle_message(raw)
|
|
2942
|
+
if not result:
|
|
2943
|
+
return response("", 204)
|
|
2944
|
+
return response(_json.loads(result))
|
|
2945
|
+
|
|
2946
|
+
|
|
2947
|
+
async def _api_mcp_sse(request, response):
|
|
2948
|
+
"""GET — SSE handshake. Emits the `endpoint` event telling the client
|
|
2949
|
+
where to POST JSON-RPC messages, per the MCP HTTP+SSE transport.
|
|
2950
|
+
"""
|
|
2951
|
+
from tina4_python.mcp import is_enabled
|
|
2952
|
+
if not is_enabled():
|
|
2953
|
+
return response({"error": "MCP disabled"}, 404)
|
|
2954
|
+
base = request.path.rsplit("/sse", 1)[0]
|
|
2955
|
+
sse = f"event: endpoint\ndata: {base}/message\n\n"
|
|
2956
|
+
return response(sse, 200, "text/event-stream")
|
|
2957
|
+
|
|
2958
|
+
|
|
2864
2959
|
# ─── Scaffold REST shim ────────────────────────────────────────────
|
|
2865
2960
|
#
|
|
2866
2961
|
# Wraps the tina4python `generate <kind> <name>` CLI commands so the
|
|
@@ -259,7 +259,7 @@ def register_dev_tools(server):
|
|
|
259
259
|
"""List all registered routes with methods and paths."""
|
|
260
260
|
from tina4_python.core.router import Router
|
|
261
261
|
routes = []
|
|
262
|
-
for route in Router.
|
|
262
|
+
for route in Router.get_routes():
|
|
263
263
|
routes.append({
|
|
264
264
|
"method": route.get("method", ""),
|
|
265
265
|
"path": route.get("path", ""),
|