tina4-python 3.11.12__tar.gz → 3.11.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tina4_python-3.11.12 → tina4_python-3.11.14}/PKG-INFO +26 -23
- {tina4_python-3.11.12 → tina4_python-3.11.14}/README.md +25 -22
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/CLAUDE.md +4 -3
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/__init__.py +1 -1
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/ai/__init__.py +6 -3
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/core/router.py +46 -14
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/core/server.py +6 -4
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/database/firebird.py +10 -2
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/database/postgres.py +10 -2
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/dev_admin/__init__.py +557 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/.gitignore +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/pyproject.toml +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/Testing.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/core/events.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/core/request.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/core/response.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.11.12 → tina4_python-3.11.14}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tina4-python
|
|
3
|
-
Version: 3.11.
|
|
3
|
+
Version: 3.11.14
|
|
4
4
|
Summary: Tina4 for Python — 54 built-in features, zero dependencies
|
|
5
5
|
Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -37,13 +37,13 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
<h1 align="center">Tina4 Python</h1>
|
|
38
38
|
|
|
39
39
|
<p align="center">
|
|
40
|
-
|
|
40
|
+
55 built-in features. Zero dependencies. One import, everything works.
|
|
41
41
|
</p>
|
|
42
42
|
|
|
43
43
|
<p align="center">
|
|
44
44
|
<a href="https://pypi.org/project/tina4-python/"><img src="https://img.shields.io/pypi/v/tina4-python?color=7b1fa2&label=PyPI" alt="PyPI"></a>
|
|
45
|
-
<img src="https://img.shields.io/badge/tests-2%
|
|
46
|
-
<img src="https://img.shields.io/badge/features-
|
|
45
|
+
<img src="https://img.shields.io/badge/tests-2%2C281%20passing-brightgreen" alt="Tests">
|
|
46
|
+
<img src="https://img.shields.io/badge/features-55-blue" alt="Features">
|
|
47
47
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
|
|
48
48
|
<a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
|
|
49
49
|
</p>
|
|
@@ -72,10 +72,12 @@ tina4 init python ./my-app
|
|
|
72
72
|
cd my-app && tina4 serve
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
Open http://localhost:
|
|
75
|
+
Open http://localhost:7146 — your app is running.
|
|
76
76
|
|
|
77
77
|
<details>
|
|
78
|
-
<summary><strong>Without the Tina4 CLI</strong></summary>
|
|
78
|
+
<summary><strong>Without the Tina4 CLI (Docker / CI only)</strong></summary>
|
|
79
|
+
|
|
80
|
+
The framework normally refuses to start without the `tina4` Rust CLI (it owns file watching and SCSS compilation). To bypass — e.g. inside a Docker image where you've already built the assets — set `TINA4_OVERRIDE_CLIENT=true` in `.env`:
|
|
79
81
|
|
|
80
82
|
```bash
|
|
81
83
|
# 1. Create project
|
|
@@ -85,24 +87,25 @@ uv init && uv add tina4-python
|
|
|
85
87
|
# 2. Create entry point
|
|
86
88
|
echo 'from tina4_python.core import run; run()' > app.py
|
|
87
89
|
|
|
88
|
-
# 3. Create .env
|
|
90
|
+
# 3. Create .env (note the override)
|
|
89
91
|
echo 'TINA4_DEBUG=true' > .env
|
|
90
92
|
echo 'TINA4_LOG_LEVEL=ALL' >> .env
|
|
93
|
+
echo 'TINA4_OVERRIDE_CLIENT=true' >> .env
|
|
91
94
|
|
|
92
95
|
# 4. Create route directory
|
|
93
96
|
mkdir -p src/routes
|
|
94
97
|
|
|
95
|
-
# 5. Run
|
|
98
|
+
# 5. Run (no file watching, no hot reload in this mode)
|
|
96
99
|
uv run python app.py
|
|
97
100
|
```
|
|
98
101
|
|
|
99
|
-
Open http://localhost:
|
|
102
|
+
Open http://localhost:7146
|
|
100
103
|
|
|
101
104
|
</details>
|
|
102
105
|
|
|
103
106
|
---
|
|
104
107
|
|
|
105
|
-
## What's Built In (
|
|
108
|
+
## What's Built In (55 Features)
|
|
106
109
|
|
|
107
110
|
Every feature is built from scratch -- no pip install, no node_modules, no third-party runtime dependencies in core.
|
|
108
111
|
|
|
@@ -119,7 +122,7 @@ Every feature is built from scratch -- no pip install, no node_modules, no third
|
|
|
119
122
|
| **Developer Tools** (7) | Dev dashboard (11 tabs), dev toolbar, error overlay (Catppuccin Mocha), dev mailbox, hot reload + CSS hot-reload, code metrics (complexity, coupling, maintainability), AI context installer (7 tools) |
|
|
120
123
|
| **Utilities** (7) | DI container (transient + singleton), HtmlElement builder, inline testing (`@tests` decorator), i18n (6 languages), Swagger/OpenAPI auto-generation, CLI scaffolding (`generate model/route/migration/middleware`), structured logging |
|
|
121
124
|
|
|
122
|
-
**2,
|
|
125
|
+
**2,281 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
|
|
123
126
|
|
|
124
127
|
For full documentation visit **[tina4.com](https://tina4.com)**.
|
|
125
128
|
|
|
@@ -198,7 +201,7 @@ async def hello_name(request, response):
|
|
|
198
201
|
return response({"message": f"Hello, {name}!"})
|
|
199
202
|
```
|
|
200
203
|
|
|
201
|
-
Visit `http://localhost:
|
|
204
|
+
Visit `http://localhost:7146/api/hello` -- routes are auto-discovered, no imports needed.
|
|
202
205
|
|
|
203
206
|
### 3. Add a database
|
|
204
207
|
|
|
@@ -602,10 +605,10 @@ cache.tag("users").flush()
|
|
|
602
605
|
|
|
603
606
|
Set `TINA4_DEBUG=true` in `.env` to enable:
|
|
604
607
|
|
|
605
|
-
- **Live reload** -- browser
|
|
606
|
-
- **CSS hot-reload** -- SCSS changes apply without page refresh
|
|
608
|
+
- **Live reload** -- the `tina4` Rust CLI watches `src/`, `migrations/`, `.env` and POSTs `/__dev/api/reload` to the running server; the framework broadcasts to the browser via WebSocket (`/__dev_reload`) with a polling fallback (`GET /__dev/api/mtime`)
|
|
609
|
+
- **CSS hot-reload** -- SCSS changes apply without a full page refresh
|
|
607
610
|
- **Error overlay** -- rich error display in the browser
|
|
608
|
-
- **Dev admin** at `/__dev/` with
|
|
611
|
+
- **Dev admin** at `/__dev/` with tabs: Routes, Queue, Mailbox, Messages, Database, Requests, Errors, WebSocket, System, Tools, Tina4
|
|
609
612
|
|
|
610
613
|
---
|
|
611
614
|
|
|
@@ -613,7 +616,7 @@ Set `TINA4_DEBUG=true` in `.env` to enable:
|
|
|
613
616
|
|
|
614
617
|
```bash
|
|
615
618
|
tina4python init [dir] # Scaffold a new project
|
|
616
|
-
tina4python serve [port]
|
|
619
|
+
tina4python serve [--port P] [--no-browser] [--no-reload] # Dev server (default: 0.0.0.0:7146)
|
|
617
620
|
tina4python serve --production # Auto-install and use best production server (uvicorn)
|
|
618
621
|
tina4python migrate # Run pending migrations
|
|
619
622
|
tina4python migrate:create <desc> # Create a migration file
|
|
@@ -715,13 +718,13 @@ Benchmarked with `wrk` — 5,000 requests, 50 concurrent, median of 3 runs:
|
|
|
715
718
|
|
|
716
719
|
| Framework | JSON req/s | Deps | Features |
|
|
717
720
|
|-----------|-----------|------|----------|
|
|
718
|
-
| **Tina4 Python** | **6,508** | 0 |
|
|
721
|
+
| **Tina4 Python** | **6,508** | 0 | 55 |
|
|
719
722
|
| FastAPI | 12,652 | 12+ | ~8 |
|
|
720
723
|
| Flask | 4,928 | 6+ | ~7 |
|
|
721
724
|
| Bottle | 4,355 | 0 | ~5 |
|
|
722
725
|
| Django | 4,050 | 20+ | ~22 |
|
|
723
726
|
|
|
724
|
-
Tina4 Python delivers competitive throughput with **zero dependencies and
|
|
727
|
+
Tina4 Python delivers competitive throughput with **zero dependencies and 55 features** — frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
|
|
725
728
|
|
|
726
729
|
**Across all 4 Tina4 implementations:**
|
|
727
730
|
|
|
@@ -729,7 +732,7 @@ Tina4 Python delivers competitive throughput with **zero dependencies and 54 fea
|
|
|
729
732
|
|---|--------|-----|------|---------|
|
|
730
733
|
| **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
|
|
731
734
|
| **Dependencies** | 0 | 0 | 0 | 0 |
|
|
732
|
-
| **Features** |
|
|
735
|
+
| **Features** | 55 | 55 | 55 | 55 |
|
|
733
736
|
|
|
734
737
|
Run benchmarks locally: `python benchmarks/benchmark.py --python`
|
|
735
738
|
|
|
@@ -737,15 +740,15 @@ Run benchmarks locally: `python benchmarks/benchmark.py --python`
|
|
|
737
740
|
|
|
738
741
|
## Cross-Framework Parity
|
|
739
742
|
|
|
740
|
-
Tina4 ships identical features across four languages — same architecture, same conventions, same
|
|
743
|
+
Tina4 ships identical features across four languages — same architecture, same conventions, same 55 features:
|
|
741
744
|
|
|
742
745
|
| | Python | PHP | Ruby | Node.js |
|
|
743
746
|
|---|--------|-----|------|---------|
|
|
744
747
|
| **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` | `tina4-nodejs` |
|
|
745
|
-
| **Tests** | 2,
|
|
746
|
-
| **Default port** |
|
|
748
|
+
| **Tests (v3.11.12)** | 2,281 | 2,073 | 2,508 | 2,897 |
|
|
749
|
+
| **Default port** | 7146 | 7145 | 7147 | 7148 |
|
|
747
750
|
|
|
748
|
-
|
|
751
|
+
**~9,700 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
|
|
749
752
|
|
|
750
753
|
---
|
|
751
754
|
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
<h1 align="center">Tina4 Python</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
|
|
8
|
+
55 built-in features. Zero dependencies. One import, everything works.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://pypi.org/project/tina4-python/"><img src="https://img.shields.io/pypi/v/tina4-python?color=7b1fa2&label=PyPI" alt="PyPI"></a>
|
|
13
|
-
<img src="https://img.shields.io/badge/tests-2%
|
|
14
|
-
<img src="https://img.shields.io/badge/features-
|
|
13
|
+
<img src="https://img.shields.io/badge/tests-2%2C281%20passing-brightgreen" alt="Tests">
|
|
14
|
+
<img src="https://img.shields.io/badge/features-55-blue" alt="Features">
|
|
15
15
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
|
|
16
16
|
<a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
|
|
17
17
|
</p>
|
|
@@ -40,10 +40,12 @@ tina4 init python ./my-app
|
|
|
40
40
|
cd my-app && tina4 serve
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Open http://localhost:
|
|
43
|
+
Open http://localhost:7146 — your app is running.
|
|
44
44
|
|
|
45
45
|
<details>
|
|
46
|
-
<summary><strong>Without the Tina4 CLI</strong></summary>
|
|
46
|
+
<summary><strong>Without the Tina4 CLI (Docker / CI only)</strong></summary>
|
|
47
|
+
|
|
48
|
+
The framework normally refuses to start without the `tina4` Rust CLI (it owns file watching and SCSS compilation). To bypass — e.g. inside a Docker image where you've already built the assets — set `TINA4_OVERRIDE_CLIENT=true` in `.env`:
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
51
|
# 1. Create project
|
|
@@ -53,24 +55,25 @@ uv init && uv add tina4-python
|
|
|
53
55
|
# 2. Create entry point
|
|
54
56
|
echo 'from tina4_python.core import run; run()' > app.py
|
|
55
57
|
|
|
56
|
-
# 3. Create .env
|
|
58
|
+
# 3. Create .env (note the override)
|
|
57
59
|
echo 'TINA4_DEBUG=true' > .env
|
|
58
60
|
echo 'TINA4_LOG_LEVEL=ALL' >> .env
|
|
61
|
+
echo 'TINA4_OVERRIDE_CLIENT=true' >> .env
|
|
59
62
|
|
|
60
63
|
# 4. Create route directory
|
|
61
64
|
mkdir -p src/routes
|
|
62
65
|
|
|
63
|
-
# 5. Run
|
|
66
|
+
# 5. Run (no file watching, no hot reload in this mode)
|
|
64
67
|
uv run python app.py
|
|
65
68
|
```
|
|
66
69
|
|
|
67
|
-
Open http://localhost:
|
|
70
|
+
Open http://localhost:7146
|
|
68
71
|
|
|
69
72
|
</details>
|
|
70
73
|
|
|
71
74
|
---
|
|
72
75
|
|
|
73
|
-
## What's Built In (
|
|
76
|
+
## What's Built In (55 Features)
|
|
74
77
|
|
|
75
78
|
Every feature is built from scratch -- no pip install, no node_modules, no third-party runtime dependencies in core.
|
|
76
79
|
|
|
@@ -87,7 +90,7 @@ Every feature is built from scratch -- no pip install, no node_modules, no third
|
|
|
87
90
|
| **Developer Tools** (7) | Dev dashboard (11 tabs), dev toolbar, error overlay (Catppuccin Mocha), dev mailbox, hot reload + CSS hot-reload, code metrics (complexity, coupling, maintainability), AI context installer (7 tools) |
|
|
88
91
|
| **Utilities** (7) | DI container (transient + singleton), HtmlElement builder, inline testing (`@tests` decorator), i18n (6 languages), Swagger/OpenAPI auto-generation, CLI scaffolding (`generate model/route/migration/middleware`), structured logging |
|
|
89
92
|
|
|
90
|
-
**2,
|
|
93
|
+
**2,281 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
|
|
91
94
|
|
|
92
95
|
For full documentation visit **[tina4.com](https://tina4.com)**.
|
|
93
96
|
|
|
@@ -166,7 +169,7 @@ async def hello_name(request, response):
|
|
|
166
169
|
return response({"message": f"Hello, {name}!"})
|
|
167
170
|
```
|
|
168
171
|
|
|
169
|
-
Visit `http://localhost:
|
|
172
|
+
Visit `http://localhost:7146/api/hello` -- routes are auto-discovered, no imports needed.
|
|
170
173
|
|
|
171
174
|
### 3. Add a database
|
|
172
175
|
|
|
@@ -570,10 +573,10 @@ cache.tag("users").flush()
|
|
|
570
573
|
|
|
571
574
|
Set `TINA4_DEBUG=true` in `.env` to enable:
|
|
572
575
|
|
|
573
|
-
- **Live reload** -- browser
|
|
574
|
-
- **CSS hot-reload** -- SCSS changes apply without page refresh
|
|
576
|
+
- **Live reload** -- the `tina4` Rust CLI watches `src/`, `migrations/`, `.env` and POSTs `/__dev/api/reload` to the running server; the framework broadcasts to the browser via WebSocket (`/__dev_reload`) with a polling fallback (`GET /__dev/api/mtime`)
|
|
577
|
+
- **CSS hot-reload** -- SCSS changes apply without a full page refresh
|
|
575
578
|
- **Error overlay** -- rich error display in the browser
|
|
576
|
-
- **Dev admin** at `/__dev/` with
|
|
579
|
+
- **Dev admin** at `/__dev/` with tabs: Routes, Queue, Mailbox, Messages, Database, Requests, Errors, WebSocket, System, Tools, Tina4
|
|
577
580
|
|
|
578
581
|
---
|
|
579
582
|
|
|
@@ -581,7 +584,7 @@ Set `TINA4_DEBUG=true` in `.env` to enable:
|
|
|
581
584
|
|
|
582
585
|
```bash
|
|
583
586
|
tina4python init [dir] # Scaffold a new project
|
|
584
|
-
tina4python serve [port]
|
|
587
|
+
tina4python serve [--port P] [--no-browser] [--no-reload] # Dev server (default: 0.0.0.0:7146)
|
|
585
588
|
tina4python serve --production # Auto-install and use best production server (uvicorn)
|
|
586
589
|
tina4python migrate # Run pending migrations
|
|
587
590
|
tina4python migrate:create <desc> # Create a migration file
|
|
@@ -683,13 +686,13 @@ Benchmarked with `wrk` — 5,000 requests, 50 concurrent, median of 3 runs:
|
|
|
683
686
|
|
|
684
687
|
| Framework | JSON req/s | Deps | Features |
|
|
685
688
|
|-----------|-----------|------|----------|
|
|
686
|
-
| **Tina4 Python** | **6,508** | 0 |
|
|
689
|
+
| **Tina4 Python** | **6,508** | 0 | 55 |
|
|
687
690
|
| FastAPI | 12,652 | 12+ | ~8 |
|
|
688
691
|
| Flask | 4,928 | 6+ | ~7 |
|
|
689
692
|
| Bottle | 4,355 | 0 | ~5 |
|
|
690
693
|
| Django | 4,050 | 20+ | ~22 |
|
|
691
694
|
|
|
692
|
-
Tina4 Python delivers competitive throughput with **zero dependencies and
|
|
695
|
+
Tina4 Python delivers competitive throughput with **zero dependencies and 55 features** — frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
|
|
693
696
|
|
|
694
697
|
**Across all 4 Tina4 implementations:**
|
|
695
698
|
|
|
@@ -697,7 +700,7 @@ Tina4 Python delivers competitive throughput with **zero dependencies and 54 fea
|
|
|
697
700
|
|---|--------|-----|------|---------|
|
|
698
701
|
| **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
|
|
699
702
|
| **Dependencies** | 0 | 0 | 0 | 0 |
|
|
700
|
-
| **Features** |
|
|
703
|
+
| **Features** | 55 | 55 | 55 | 55 |
|
|
701
704
|
|
|
702
705
|
Run benchmarks locally: `python benchmarks/benchmark.py --python`
|
|
703
706
|
|
|
@@ -705,15 +708,15 @@ Run benchmarks locally: `python benchmarks/benchmark.py --python`
|
|
|
705
708
|
|
|
706
709
|
## Cross-Framework Parity
|
|
707
710
|
|
|
708
|
-
Tina4 ships identical features across four languages — same architecture, same conventions, same
|
|
711
|
+
Tina4 ships identical features across four languages — same architecture, same conventions, same 55 features:
|
|
709
712
|
|
|
710
713
|
| | Python | PHP | Ruby | Node.js |
|
|
711
714
|
|---|--------|-----|------|---------|
|
|
712
715
|
| **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` | `tina4-nodejs` |
|
|
713
|
-
| **Tests** | 2,
|
|
714
|
-
| **Default port** |
|
|
716
|
+
| **Tests (v3.11.12)** | 2,281 | 2,073 | 2,508 | 2,897 |
|
|
717
|
+
| **Default port** | 7146 | 7145 | 7147 | 7148 |
|
|
715
718
|
|
|
716
|
-
|
|
719
|
+
**~9,700 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
|
|
717
720
|
|
|
718
721
|
---
|
|
719
722
|
|
|
@@ -632,12 +632,13 @@ Frond.add_test("positive", lambda x: x > 0)
|
|
|
632
632
|
|
|
633
633
|
### The @template() decorator
|
|
634
634
|
|
|
635
|
-
Auto-renders a dict return value through a template:
|
|
635
|
+
Auto-renders a dict return value through a template. **Stack `@template` BELOW the route decorator** — route decorators capture the current function when applied, so `@template` above `@get` is never reached by the router:
|
|
636
|
+
|
|
636
637
|
```python
|
|
637
638
|
from tina4_python.core.router import get, template
|
|
638
639
|
|
|
639
|
-
@template("pages/dashboard.twig")
|
|
640
640
|
@get("/dashboard")
|
|
641
|
+
@template("pages/dashboard.twig")
|
|
641
642
|
async def dashboard(request, response):
|
|
642
643
|
return {"title": "Dashboard", "stats": get_stats()}
|
|
643
644
|
```
|
|
@@ -1779,8 +1780,8 @@ for job in queue.consume("emails"):
|
|
|
1779
1780
|
# src/routes/dashboard.py
|
|
1780
1781
|
from tina4_python.core.router import get, template
|
|
1781
1782
|
|
|
1782
|
-
@template("pages/dashboard.twig")
|
|
1783
1783
|
@get("/dashboard")
|
|
1784
|
+
@template("pages/dashboard.twig")
|
|
1784
1785
|
async def dashboard(request, response):
|
|
1785
1786
|
stats = db.fetch("SELECT count(*) as total FROM orders").to_array()
|
|
1786
1787
|
return {"title": "Dashboard", "stats": stats}
|
|
@@ -20,6 +20,7 @@ AI_TOOLS = [
|
|
|
20
20
|
{"name": "aider", "description": "Aider", "context_file": "CONVENTIONS.md", "config_dir": None},
|
|
21
21
|
{"name": "cline", "description": "Cline", "context_file": ".clinerules", "config_dir": None},
|
|
22
22
|
{"name": "codex", "description": "OpenAI Codex", "context_file": "AGENTS.md", "config_dir": None},
|
|
23
|
+
{"name": "antigravity", "description": "Google Antigravity", "context_file": ".antigravity/context.md", "config_dir": ".antigravity"},
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
|
|
@@ -40,10 +41,11 @@ def show_menu(root: str = ".") -> str:
|
|
|
40
41
|
marker = f" {green}[installed]{reset}" if installed else ""
|
|
41
42
|
print(f" {i}. {tool['description']:<20s} {tool['context_file']}{marker}")
|
|
42
43
|
|
|
43
|
-
# tina4-ai tools option
|
|
44
|
+
# tina4-ai tools option — rendered AFTER the tool list, always the last number
|
|
45
|
+
tina4_ai_num = len(AI_TOOLS) + 1
|
|
44
46
|
tina4_ai_installed = shutil.which("mdview") is not None
|
|
45
47
|
marker = f" {green}[installed]{reset}" if tina4_ai_installed else ""
|
|
46
|
-
print(f"
|
|
48
|
+
print(f" {tina4_ai_num}. Install tina4-ai tools (requires Python){marker}")
|
|
47
49
|
print()
|
|
48
50
|
return input(" Select (comma-separated, or 'all'): ").strip()
|
|
49
51
|
|
|
@@ -56,6 +58,7 @@ def install_selected(root: str, selection: str) -> list[str]:
|
|
|
56
58
|
"""
|
|
57
59
|
root_path = Path(root).resolve()
|
|
58
60
|
created = []
|
|
61
|
+
tina4_ai_num = len(AI_TOOLS) + 1 # keep menu numbering aligned with show_menu
|
|
59
62
|
|
|
60
63
|
if selection.lower() == "all":
|
|
61
64
|
indices = list(range(len(AI_TOOLS)))
|
|
@@ -67,7 +70,7 @@ def install_selected(root: str, selection: str) -> list[str]:
|
|
|
67
70
|
for p in parts:
|
|
68
71
|
try:
|
|
69
72
|
n = int(p)
|
|
70
|
-
if n ==
|
|
73
|
+
if n == tina4_ai_num:
|
|
71
74
|
install_tina4_ai = True
|
|
72
75
|
elif 1 <= n <= len(AI_TOOLS):
|
|
73
76
|
indices.append(n - 1)
|
|
@@ -342,19 +342,49 @@ class Router:
|
|
|
342
342
|
_ws_routes.clear()
|
|
343
343
|
|
|
344
344
|
|
|
345
|
+
# Supported typed-parameter constraints. Keys are the type name written in
|
|
346
|
+
# the route pattern (e.g. ``{id:int}``); values are the regex that the param
|
|
347
|
+
# must match. Mirrored verbatim in PHP/Ruby/Node.js for cross-framework parity.
|
|
348
|
+
#
|
|
349
|
+
# Any type name that isn't in this table raises at route registration time —
|
|
350
|
+
# we never silently fall through to the default matcher, because a typo like
|
|
351
|
+
# ``{id:inetger}`` would otherwise match anything and create a security
|
|
352
|
+
# footgun (see tina4-book#125).
|
|
353
|
+
_TYPE_PATTERNS = {
|
|
354
|
+
"string": "[^/]+", # default, any non-slash segment
|
|
355
|
+
"int": r"\d+",
|
|
356
|
+
"integer": r"\d+",
|
|
357
|
+
"float": r"[\d.]+",
|
|
358
|
+
"number": r"[\d.]+",
|
|
359
|
+
"alpha": "[A-Za-z]+", # letters only
|
|
360
|
+
"alnum": "[A-Za-z0-9]+", # letters + digits
|
|
361
|
+
"slug": "[a-z0-9-]+", # URL slug
|
|
362
|
+
"uuid": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
|
|
363
|
+
"path": ".+", # greedy — matches remaining path
|
|
364
|
+
".*": ".+",
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
|
|
345
368
|
def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
|
|
346
369
|
"""Convert a route path to a regex pattern.
|
|
347
370
|
|
|
348
371
|
Supports:
|
|
349
|
-
/api/users
|
|
350
|
-
/api/users/{id}
|
|
351
|
-
/api/
|
|
372
|
+
/api/users → exact match
|
|
373
|
+
/api/users/{id} → named parameter (any non-slash chars)
|
|
374
|
+
/api/users/{id:int} → digits only
|
|
375
|
+
/api/users/{name:alpha} → letters only
|
|
376
|
+
/api/users/{slug:slug} → URL slug (a-z 0-9 -)
|
|
377
|
+
/api/users/{id:uuid} → UUID v4 format
|
|
378
|
+
/api/files/{p:path} → greedy (matches remaining path)
|
|
379
|
+
/api/docs/* → bare-wildcard catch-all (key "*")
|
|
380
|
+
|
|
381
|
+
Unknown type names raise ``ValueError`` at route registration time.
|
|
352
382
|
"""
|
|
353
383
|
param_names = []
|
|
354
384
|
regex_parts = []
|
|
355
385
|
|
|
356
386
|
segments = path.strip("/").split("/")
|
|
357
|
-
for
|
|
387
|
+
for segment in segments:
|
|
358
388
|
if segment == "*":
|
|
359
389
|
# Wildcard: matches the rest of the path (greedy)
|
|
360
390
|
param_names.append("*")
|
|
@@ -364,14 +394,12 @@ def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
|
|
|
364
394
|
inner = segment[1:-1]
|
|
365
395
|
if ":" in inner:
|
|
366
396
|
name, type_hint = inner.split(":", 1)
|
|
367
|
-
if type_hint
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
else:
|
|
374
|
-
regex_parts.append("([^/]+)")
|
|
397
|
+
if type_hint not in _TYPE_PATTERNS:
|
|
398
|
+
raise ValueError(
|
|
399
|
+
f"Unknown param type {type_hint!r} in route {path!r}. "
|
|
400
|
+
f"Valid types: {', '.join(sorted(k for k in _TYPE_PATTERNS if k != '.*'))}."
|
|
401
|
+
)
|
|
402
|
+
regex_parts.append("(" + _TYPE_PATTERNS[type_hint] + ")")
|
|
375
403
|
else:
|
|
376
404
|
name = inner
|
|
377
405
|
regex_parts.append("([^/]+)")
|
|
@@ -518,9 +546,13 @@ def cached(max_age: int = 60):
|
|
|
518
546
|
def template(template_name: str):
|
|
519
547
|
"""Auto-render a dict return value through a Frond/Twig template.
|
|
520
548
|
|
|
521
|
-
|
|
522
|
-
|
|
549
|
+
IMPORTANT: ``@template`` must sit BELOW the route decorator so the
|
|
550
|
+
wrapper is registered (route decorators capture the current function
|
|
551
|
+
reference when applied — a @template above @get never reaches the
|
|
552
|
+
router). Correct order:
|
|
553
|
+
|
|
523
554
|
@get("/dashboard")
|
|
555
|
+
@template("pages/dashboard.twig")
|
|
524
556
|
async def dashboard(request, response):
|
|
525
557
|
return {"title": "Dashboard", "items": get_items()}
|
|
526
558
|
|
|
@@ -253,7 +253,7 @@ def _is_gallery_deployed(name: str) -> bool:
|
|
|
253
253
|
def _gallery_btn(name: str, try_url: str) -> str:
|
|
254
254
|
"""Render a Try It or View button depending on deployment state."""
|
|
255
255
|
if _is_gallery_deployed(name):
|
|
256
|
-
return f'<button class="try-btn" style="background:#22c55e;" onclick="window.
|
|
256
|
+
return f'<button class="try-btn" style="background:#22c55e;" onclick="window.open(\'{try_url}\',\'_blank\')" data-deployed="1">View ↗</button>'
|
|
257
257
|
return f'<button class="try-btn" onclick="deployGallery(\'{name}\',\'{try_url}\')">Try It</button>'
|
|
258
258
|
|
|
259
259
|
|
|
@@ -437,18 +437,20 @@ function deployGallery(name, tryUrl) {{
|
|
|
437
437
|
btn.style.background = '#22c55e';
|
|
438
438
|
btn.disabled = false;
|
|
439
439
|
btn.dataset.deployed = '1';
|
|
440
|
-
// Wait for the newly deployed route to become reachable
|
|
440
|
+
// Wait for the newly deployed route to become reachable, then
|
|
441
|
+
// open it in a new tab so the dev-admin / gallery home stays
|
|
442
|
+
// open (fixes tina4-book#115).
|
|
441
443
|
var attempts = 0;
|
|
442
444
|
var maxAttempts = 5;
|
|
443
445
|
function pollRoute() {{
|
|
444
446
|
fetch(tryUrl, {{method: 'HEAD'}}).then(function() {{
|
|
445
|
-
window.
|
|
447
|
+
window.open(tryUrl, '_blank');
|
|
446
448
|
}}).catch(function() {{
|
|
447
449
|
attempts++;
|
|
448
450
|
if (attempts < maxAttempts) {{
|
|
449
451
|
setTimeout(pollRoute, 500);
|
|
450
452
|
}} else {{
|
|
451
|
-
window.
|
|
453
|
+
window.open(tryUrl, '_blank');
|
|
452
454
|
}}
|
|
453
455
|
}});
|
|
454
456
|
}}
|
|
@@ -158,7 +158,7 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
158
158
|
|
|
159
159
|
desc = cursor.description
|
|
160
160
|
col_names = [d[0].strip().lower() for d in desc] if desc else []
|
|
161
|
-
rows = [dict(zip(col_names, row)) for row in cursor.fetchall()]
|
|
161
|
+
rows = [self._decode_blobs(dict(zip(col_names, row))) for row in cursor.fetchall()]
|
|
162
162
|
|
|
163
163
|
return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
|
|
164
164
|
|
|
@@ -171,7 +171,15 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
171
171
|
if row is None:
|
|
172
172
|
return None
|
|
173
173
|
col_names = [d[0].strip().lower() for d in desc] if desc else []
|
|
174
|
-
return dict(zip(col_names, row))
|
|
174
|
+
return self._decode_blobs(dict(zip(col_names, row)))
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _decode_blobs(row: dict) -> dict:
|
|
178
|
+
"""Ensure Firebird BLOB columns are proper bytes, not memoryview."""
|
|
179
|
+
for key, value in row.items():
|
|
180
|
+
if isinstance(value, memoryview):
|
|
181
|
+
row[key] = bytes(value)
|
|
182
|
+
return row
|
|
175
183
|
|
|
176
184
|
def insert(self, table: str, data: dict) -> DatabaseResult:
|
|
177
185
|
columns = ", ".join(data.keys())
|
|
@@ -119,7 +119,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
119
119
|
paginated_sql = f"{sql} LIMIT %s OFFSET %s"
|
|
120
120
|
paginated_params = (params or []) + [limit, offset]
|
|
121
121
|
cursor.execute(paginated_sql, paginated_params)
|
|
122
|
-
rows = [dict(row) for row in cursor.fetchall()]
|
|
122
|
+
rows = [self._decode_blobs(dict(row)) for row in cursor.fetchall()]
|
|
123
123
|
|
|
124
124
|
return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
|
|
125
125
|
|
|
@@ -130,7 +130,15 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
130
130
|
cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
|
131
131
|
cursor.execute(sql, params or [])
|
|
132
132
|
row = cursor.fetchone()
|
|
133
|
-
return dict(row) if row else None
|
|
133
|
+
return self._decode_blobs(dict(row)) if row else None
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _decode_blobs(row: dict) -> dict:
|
|
137
|
+
"""Ensure binary columns (bytea) are proper bytes, not memoryview."""
|
|
138
|
+
for key, value in row.items():
|
|
139
|
+
if isinstance(value, memoryview):
|
|
140
|
+
row[key] = bytes(value)
|
|
141
|
+
return row
|
|
134
142
|
|
|
135
143
|
def insert(self, table: str, data: dict) -> DatabaseResult:
|
|
136
144
|
columns = ", ".join(data.keys())
|