tina4-python 3.13.35__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.35 → tina4_python-3.13.36}/PKG-INFO +1 -1
- {tina4_python-3.13.35 → tina4_python-3.13.36}/pyproject.toml +1 -1
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/CLAUDE.md +11 -2
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/core/router.py +14 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/core/server.py +82 -5
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/dev_admin/__init__.py +56 -7
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/js/tina4-dev-admin.js +44 -42
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/js/tina4-dev-admin.min.js +44 -42
- {tina4_python-3.13.35 → tina4_python-3.13.36}/.gitignore +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/README.md +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/env.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.35 → tina4_python-3.13.36}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.35 → 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:
|
|
@@ -1979,6 +1979,25 @@ async def _api_reload(request, response):
|
|
|
1979
1979
|
except Exception as e:
|
|
1980
1980
|
Log.error(f"Re-discover on reload failed: {e}")
|
|
1981
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
|
+
|
|
1982
2001
|
return response({"ok": True, "type": reload_type})
|
|
1983
2002
|
|
|
1984
2003
|
|
|
@@ -2150,16 +2169,22 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
|
|
|
2150
2169
|
</script>
|
|
2151
2170
|
<script>
|
|
2152
2171
|
{'(function(){})();' if no_reload else f"""(function(){{
|
|
2153
|
-
|
|
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;
|
|
2154
2177
|
var _t4_interval=parseInt('{poll_interval_ms}')||3000;
|
|
2178
|
+
var _t4_ws=null,_t4_poll_timer=null,_t4_mtime=null;
|
|
2155
2179
|
function _t4_apply(d){{
|
|
2156
|
-
|
|
2157
|
-
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)}});
|
|
2158
2183
|
if(isCss){{
|
|
2159
2184
|
var links=document.querySelectorAll('link[rel="stylesheet"]');
|
|
2160
2185
|
links.forEach(function(l){{
|
|
2161
2186
|
var href=l.getAttribute('href');
|
|
2162
|
-
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()))}}
|
|
2163
2188
|
}});
|
|
2164
2189
|
}}else{{
|
|
2165
2190
|
location.reload();
|
|
@@ -2167,15 +2192,39 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
|
|
|
2167
2192
|
}}
|
|
2168
2193
|
function _t4_poll(){{
|
|
2169
2194
|
fetch('/__dev/api/mtime').then(function(r){{return r.json()}}).then(function(d){{
|
|
2170
|
-
if(
|
|
2171
|
-
if(d.mtime
|
|
2195
|
+
if(_t4_mtime===null){{_t4_mtime=d.mtime;return;}}
|
|
2196
|
+
if(d.mtime!==_t4_mtime){{
|
|
2172
2197
|
_t4_mtime=d.mtime;
|
|
2173
2198
|
if(_t4_debounce)clearTimeout(_t4_debounce);
|
|
2174
2199
|
_t4_debounce=setTimeout(function(){{_t4_apply(d);}},500);
|
|
2175
2200
|
}}
|
|
2176
2201
|
}}).catch(function(){{}});
|
|
2177
2202
|
}}
|
|
2178
|
-
|
|
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();
|
|
2179
2228
|
}})();"""}
|
|
2180
2229
|
function tina4VersionModal(){{
|
|
2181
2230
|
var m=document.getElementById('tina4-ver-modal');
|