tina4-python 3.10.97__tar.gz → 3.11.0__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.10.97 → tina4_python-3.11.0}/.gitignore +11 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/PKG-INFO +23 -1
- {tina4_python-3.10.97 → tina4_python-3.11.0}/README.md +22 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/CLAUDE.md +2 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/__init__.py +2 -2
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/server.py +56 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/frond/engine.py +49 -6
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/orm/model.py +17 -12
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/__init__.py +7 -2
- {tina4_python-3.10.97 → tina4_python-3.11.0}/pyproject.toml +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/Testing.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/events.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/request.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/response.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/core/router.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -61,5 +61,16 @@ __pycache__/
|
|
|
61
61
|
build/
|
|
62
62
|
.pytest_cache/
|
|
63
63
|
example/.env
|
|
64
|
+
example/data/*.db
|
|
65
|
+
example/data/*.db-*
|
|
66
|
+
example/store/data/*.db
|
|
67
|
+
example/store/data/*.db-*
|
|
68
|
+
example/store/data/broken/
|
|
69
|
+
example/store/data/.broken/
|
|
70
|
+
example/store/data/mailbox/
|
|
71
|
+
example/store/data/sessions/
|
|
72
|
+
example/store/.tina4/
|
|
73
|
+
example/store/__pycache__/
|
|
74
|
+
example/store/src/**/__pycache__/
|
|
64
75
|
.claude/settings.local.json
|
|
65
76
|
.claude/worktrees/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tina4-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.11.0
|
|
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
|
|
@@ -749,6 +749,28 @@ Tina4 ships identical features across four languages — same architecture, same
|
|
|
749
749
|
|
|
750
750
|
---
|
|
751
751
|
|
|
752
|
+
## Demo Store
|
|
753
|
+
|
|
754
|
+
A complete e-commerce app lives in `example/`. It demonstrates every framework feature through a real-world use case.
|
|
755
|
+
|
|
756
|
+
```bash
|
|
757
|
+
cd example
|
|
758
|
+
bash setup.sh # macOS/Linux
|
|
759
|
+
# or: setup.bat # Windows
|
|
760
|
+
.venv/bin/python app.py
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
Open http://localhost:7146
|
|
764
|
+
|
|
765
|
+
| Role | Email | Password |
|
|
766
|
+
|------|-------|----------|
|
|
767
|
+
| Admin | admin@tina4store.com | admin123 |
|
|
768
|
+
| Customer | alice@example.com | customer123 |
|
|
769
|
+
|
|
770
|
+
See [`example/README.md`](example/README.md) for full details.
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
752
774
|
## Documentation
|
|
753
775
|
|
|
754
776
|
Full guides, API reference, and examples at **[tina4.com](https://tina4.com)**.
|
|
@@ -717,6 +717,28 @@ Tina4 ships identical features across four languages — same architecture, same
|
|
|
717
717
|
|
|
718
718
|
---
|
|
719
719
|
|
|
720
|
+
## Demo Store
|
|
721
|
+
|
|
722
|
+
A complete e-commerce app lives in `example/`. It demonstrates every framework feature through a real-world use case.
|
|
723
|
+
|
|
724
|
+
```bash
|
|
725
|
+
cd example
|
|
726
|
+
bash setup.sh # macOS/Linux
|
|
727
|
+
# or: setup.bat # Windows
|
|
728
|
+
.venv/bin/python app.py
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
Open http://localhost:7146
|
|
732
|
+
|
|
733
|
+
| Role | Email | Password |
|
|
734
|
+
|------|-------|----------|
|
|
735
|
+
| Admin | admin@tina4store.com | admin123 |
|
|
736
|
+
| Customer | alice@example.com | customer123 |
|
|
737
|
+
|
|
738
|
+
See [`example/README.md`](example/README.md) for full details.
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
720
742
|
## Documentation
|
|
721
743
|
|
|
722
744
|
Full guides, API reference, and examples at **[tina4.com](https://tina4.com)**.
|
|
@@ -245,6 +245,7 @@ Tina4 provides a full toolkit. Before writing custom code, check if the framewor
|
|
|
245
245
|
| Dependency injection | `Container` from `tina4_python.container` |
|
|
246
246
|
| Structured logging | `Log` from `tina4_python.debug` |
|
|
247
247
|
| Error overlay (dev) | `render_error_overlay`, `is_debug_mode` from `tina4_python.debug.error_overlay` |
|
|
248
|
+
| Periodic background work | `background()` from `tina4_python.core.server` |
|
|
248
249
|
|
|
249
250
|
**Bad — writing a custom queue:**
|
|
250
251
|
```python
|
|
@@ -289,6 +290,7 @@ Queue(topic="tasks").push({"action": "send_email"})
|
|
|
289
290
|
6. **Connection strings**: v3 uses standard URL format: `driver://host:port/database` with separate `username` and `password` parameters. Example: `Database("firebird://localhost:3050//path/to/db", "SYSDBA", "masterkey")`. Environment variable: `DATABASE_URL`.
|
|
290
291
|
7. **Running the app**: `uv run python app.py <port> <name>` — port and name are CLI args handled by tina4_python
|
|
291
292
|
8. **SCSS**: Files in `src/scss/` are auto-compiled to `src/public/css/` on startup
|
|
293
|
+
12. **Background tasks**: Use `background(fn, interval)` from `tina4_python.core.server` — never use `threading.Thread` for periodic work. The `background()` function runs tasks cooperatively in the asyncio event loop with proper shutdown handling.
|
|
292
294
|
|
|
293
295
|
|
|
294
296
|
---
|
|
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
|
|
|
8
8
|
|
|
9
9
|
One import, everything works.
|
|
10
10
|
"""
|
|
11
|
-
__version__ = "3.
|
|
11
|
+
__version__ = "3.11.0"
|
|
12
12
|
|
|
13
13
|
# ── Route decorators ──
|
|
14
14
|
from tina4_python.core.router import ( # noqa: E402, F401
|
|
@@ -60,4 +60,4 @@ from tina4_python.cache import ( # noqa: E402, F401
|
|
|
60
60
|
from tina4_python.container import Container # noqa: E402, F401
|
|
61
61
|
|
|
62
62
|
# ── Server ──
|
|
63
|
-
from tina4_python.core.server import run # noqa: E402, F401
|
|
63
|
+
from tina4_python.core.server import run, background # noqa: E402, F401
|
|
@@ -32,6 +32,21 @@ _ai_port_ctx: contextvars.ContextVar[bool] = contextvars.ContextVar("_ai_port_ct
|
|
|
32
32
|
# Track startup time
|
|
33
33
|
_start_time: float = 0
|
|
34
34
|
|
|
35
|
+
# ── Background tasks registry ────────────────────────────────────────────
|
|
36
|
+
_background_tasks: list[dict] = []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def background(callback, interval: float = 1.0):
|
|
40
|
+
"""Register a background task that runs periodically in the server event loop.
|
|
41
|
+
|
|
42
|
+
Matches PHP's $app->background(fn, interval) pattern.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
callback: Function to call (sync or async, no arguments).
|
|
46
|
+
interval: Seconds between invocations (default: 1.0).
|
|
47
|
+
"""
|
|
48
|
+
_background_tasks.append({"callback": callback, "interval": interval})
|
|
49
|
+
|
|
35
50
|
|
|
36
51
|
def _auto_discover(root_dir: str = "src"):
|
|
37
52
|
"""Auto-import all .py files in src/ to trigger route decorators."""
|
|
@@ -1812,7 +1827,48 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1812
1827
|
except NotImplementedError:
|
|
1813
1828
|
pass # Windows
|
|
1814
1829
|
|
|
1830
|
+
# Start registered background tasks as asyncio tasks.
|
|
1831
|
+
# Sync callbacks run in a thread pool so they CANNOT block the event loop.
|
|
1832
|
+
# A timeout (2x interval, min 5s) cancels runaway tasks.
|
|
1833
|
+
import concurrent.futures
|
|
1834
|
+
_executor = concurrent.futures.ThreadPoolExecutor(
|
|
1835
|
+
max_workers=max(len(_background_tasks), 2),
|
|
1836
|
+
thread_name_prefix="tina4_bg",
|
|
1837
|
+
)
|
|
1838
|
+
bg_tasks = []
|
|
1839
|
+
for task_def in _background_tasks:
|
|
1840
|
+
cb = task_def["callback"]
|
|
1841
|
+
iv = task_def["interval"]
|
|
1842
|
+
|
|
1843
|
+
async def _tick_loop(_cb=cb, _iv=iv):
|
|
1844
|
+
timeout = max(_iv * 2, 5.0)
|
|
1845
|
+
while not shutdown.is_set():
|
|
1846
|
+
try:
|
|
1847
|
+
if asyncio.iscoroutinefunction(_cb):
|
|
1848
|
+
await asyncio.wait_for(_cb(), timeout=timeout)
|
|
1849
|
+
else:
|
|
1850
|
+
# Run sync callback in thread pool — never blocks the event loop
|
|
1851
|
+
await asyncio.wait_for(
|
|
1852
|
+
loop.run_in_executor(_executor, _cb),
|
|
1853
|
+
timeout=timeout,
|
|
1854
|
+
)
|
|
1855
|
+
except asyncio.TimeoutError:
|
|
1856
|
+
Log.warning(
|
|
1857
|
+
f"Background task exceeded {timeout:.1f}s timeout and was interrupted. "
|
|
1858
|
+
f"Use non-blocking calls (e.g. queue.pop() instead of queue.consume())."
|
|
1859
|
+
)
|
|
1860
|
+
except Exception as e:
|
|
1861
|
+
Log.error(f"Background task error: {e}")
|
|
1862
|
+
await asyncio.sleep(_iv)
|
|
1863
|
+
|
|
1864
|
+
bg_tasks.append(asyncio.create_task(_tick_loop()))
|
|
1865
|
+
|
|
1815
1866
|
await shutdown.wait()
|
|
1867
|
+
|
|
1868
|
+
# Cancel background tasks
|
|
1869
|
+
for t in bg_tasks:
|
|
1870
|
+
t.cancel()
|
|
1871
|
+
|
|
1816
1872
|
if ai_server:
|
|
1817
1873
|
ai_server.close()
|
|
1818
1874
|
await ai_server.wait_closed()
|
|
@@ -914,8 +914,40 @@ def _parse_filter_chain(expr: str) -> tuple[str, list[tuple[str, list[str]]]]:
|
|
|
914
914
|
return variable, filters
|
|
915
915
|
|
|
916
916
|
|
|
917
|
-
def
|
|
918
|
-
"""
|
|
917
|
+
def _coerce_arg(s: str):
|
|
918
|
+
"""Try to convert a string argument to a Python object (dict, list, number, bool).
|
|
919
|
+
|
|
920
|
+
Returns the original string unchanged if no conversion applies.
|
|
921
|
+
"""
|
|
922
|
+
if s.startswith("{") and s.endswith("}"):
|
|
923
|
+
try:
|
|
924
|
+
return json.loads(s)
|
|
925
|
+
except Exception:
|
|
926
|
+
pass
|
|
927
|
+
if s.startswith("[") and s.endswith("]"):
|
|
928
|
+
try:
|
|
929
|
+
return json.loads(s)
|
|
930
|
+
except Exception:
|
|
931
|
+
pass
|
|
932
|
+
if s in ("true", "True"):
|
|
933
|
+
return True
|
|
934
|
+
if s in ("false", "False"):
|
|
935
|
+
return False
|
|
936
|
+
if s in ("null", "None", "none"):
|
|
937
|
+
return None
|
|
938
|
+
try:
|
|
939
|
+
return int(s)
|
|
940
|
+
except (ValueError, TypeError):
|
|
941
|
+
pass
|
|
942
|
+
try:
|
|
943
|
+
return float(s)
|
|
944
|
+
except (ValueError, TypeError):
|
|
945
|
+
pass
|
|
946
|
+
return s
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def _parse_args(raw: str) -> list:
|
|
950
|
+
"""Parse filter arguments, respecting quoted strings, braces, and backslash escapes."""
|
|
919
951
|
args = []
|
|
920
952
|
current = ""
|
|
921
953
|
in_quote = None
|
|
@@ -926,9 +958,9 @@ def _parse_args(raw: str) -> list[str]:
|
|
|
926
958
|
in_quote = ch
|
|
927
959
|
elif ch == in_quote:
|
|
928
960
|
in_quote = None
|
|
929
|
-
elif ch
|
|
961
|
+
elif ch in ("(", "{", "[") and not in_quote:
|
|
930
962
|
depth += 1
|
|
931
|
-
elif ch
|
|
963
|
+
elif ch in (")", "}", "]") and not in_quote:
|
|
932
964
|
depth -= 1
|
|
933
965
|
elif ch == "," and not in_quote and depth == 0:
|
|
934
966
|
args.append(_strip_outer_quotes(current.strip()))
|
|
@@ -939,7 +971,7 @@ def _parse_args(raw: str) -> list[str]:
|
|
|
939
971
|
if current.strip():
|
|
940
972
|
args.append(_strip_outer_quotes(current.strip()))
|
|
941
973
|
|
|
942
|
-
return args
|
|
974
|
+
return [_coerce_arg(a) for a in args]
|
|
943
975
|
|
|
944
976
|
|
|
945
977
|
def _strip_outer_quotes(s: str) -> str:
|
|
@@ -964,6 +996,13 @@ def _strip_outer_quotes(s: str) -> str:
|
|
|
964
996
|
return s
|
|
965
997
|
|
|
966
998
|
|
|
999
|
+
def _dict_replace(s: str, mapping: dict) -> str:
|
|
1000
|
+
"""Apply multiple replacements from a dict (Twig-style replace filter)."""
|
|
1001
|
+
for old, new in mapping.items():
|
|
1002
|
+
s = s.replace(str(old), str(new))
|
|
1003
|
+
return s
|
|
1004
|
+
|
|
1005
|
+
|
|
967
1006
|
# Built-in filters
|
|
968
1007
|
_BUILTIN_FILTERS = {
|
|
969
1008
|
"upper": lambda v, *a: str(v).upper(),
|
|
@@ -981,7 +1020,11 @@ _BUILTIN_FILTERS = {
|
|
|
981
1020
|
"last": lambda v, *a: v[-1] if v else None,
|
|
982
1021
|
"join": lambda v, *a: (a[0] if a else ", ").join(str(i) for i in v) if isinstance(v, list) else str(v),
|
|
983
1022
|
"split": lambda v, *a: str(v).split(a[0] if a else " "),
|
|
984
|
-
"replace": lambda v, *a:
|
|
1023
|
+
"replace": lambda v, *a: (
|
|
1024
|
+
_dict_replace(str(v), a[0]) if len(a) == 1 and isinstance(a[0], dict)
|
|
1025
|
+
else str(v).replace(a[0], a[1]) if len(a) >= 2
|
|
1026
|
+
else str(v)
|
|
1027
|
+
),
|
|
985
1028
|
"default": lambda v, *a: v if v is not None and v != "" else (a[0] if a else ""),
|
|
986
1029
|
"raw": lambda v, *a: v, # Mark as safe (no escaping)
|
|
987
1030
|
"safe": lambda v, *a: v,
|
|
@@ -168,7 +168,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
168
168
|
table_name: str = ""
|
|
169
169
|
soft_delete: bool = False # Set True to enable soft delete
|
|
170
170
|
field_mapping: dict[str, str] = {} # {"python_attribute": "db_column"}
|
|
171
|
-
auto_map: bool =
|
|
171
|
+
auto_map: bool = True # No-op in Python (snake_case matches DB); exists for cross-language parity
|
|
172
172
|
auto_crud: bool = False # Set True to auto-register CRUD routes
|
|
173
173
|
_db: str | object | None = None # Per-model database override
|
|
174
174
|
_fields: dict[str, Field] = {}
|
|
@@ -933,14 +933,18 @@ class ORM(metaclass=ORMMeta):
|
|
|
933
933
|
|
|
934
934
|
# ── Serialization ───────────────────────────────────────────
|
|
935
935
|
|
|
936
|
-
def to_dict(self, include: list[str] = None) -> dict:
|
|
936
|
+
def to_dict(self, include: list[str] = None, case: str = "snake") -> dict:
|
|
937
937
|
"""Convert to dict (field values only, optionally with relationships).
|
|
938
938
|
|
|
939
939
|
Args:
|
|
940
940
|
include: List of relationship names to include. Supports dot notation
|
|
941
941
|
for nested relationships (e.g., ["posts.comments"]).
|
|
942
|
+
case: Key casing — 'snake' (default for Python), 'camel' (matches PHP).
|
|
942
943
|
"""
|
|
943
|
-
|
|
944
|
+
if case == "camel":
|
|
945
|
+
result = {snake_to_camel(name): getattr(self, name) for name in self._fields}
|
|
946
|
+
else:
|
|
947
|
+
result = {name: getattr(self, name) for name in self._fields}
|
|
944
948
|
|
|
945
949
|
if include:
|
|
946
950
|
# Group includes: top-level and nested
|
|
@@ -957,27 +961,28 @@ class ORM(metaclass=ORMMeta):
|
|
|
957
961
|
if rel_name in self._relationships:
|
|
958
962
|
# Access the relationship (triggers lazy load if not cached)
|
|
959
963
|
related = getattr(self, rel_name)
|
|
964
|
+
key = snake_to_camel(rel_name) if case == "camel" else rel_name
|
|
960
965
|
if related is None:
|
|
961
|
-
result[
|
|
966
|
+
result[key] = None
|
|
962
967
|
elif isinstance(related, list):
|
|
963
|
-
result[
|
|
964
|
-
r.to_dict(include=nested if nested else None)
|
|
968
|
+
result[key] = [
|
|
969
|
+
r.to_dict(include=nested if nested else None, case=case)
|
|
965
970
|
for r in related
|
|
966
971
|
]
|
|
967
972
|
else:
|
|
968
|
-
result[
|
|
969
|
-
include=nested if nested else None
|
|
973
|
+
result[key] = related.to_dict(
|
|
974
|
+
include=nested if nested else None, case=case
|
|
970
975
|
)
|
|
971
976
|
|
|
972
977
|
return result
|
|
973
978
|
|
|
974
|
-
def to_assoc(self, include: list[str] = None) -> dict:
|
|
979
|
+
def to_assoc(self, include: list[str] = None, case: str = "snake") -> dict:
|
|
975
980
|
"""Convert to an associative dict (alias for to_dict)."""
|
|
976
|
-
return self.to_dict(include=include)
|
|
981
|
+
return self.to_dict(include=include, case=case)
|
|
977
982
|
|
|
978
|
-
def to_object(self) -> dict:
|
|
983
|
+
def to_object(self, case: str = "snake") -> dict:
|
|
979
984
|
"""Convert to an object/dict (alias for to_dict)."""
|
|
980
|
-
return self.to_dict()
|
|
985
|
+
return self.to_dict(case=case)
|
|
981
986
|
|
|
982
987
|
def to_array(self) -> list:
|
|
983
988
|
"""Convert to a list of values."""
|
|
@@ -62,10 +62,15 @@ def compile_scss(scss_dir: str = "src/scss", output: str = "public/css/default.c
|
|
|
62
62
|
if minify:
|
|
63
63
|
css = _minify(css)
|
|
64
64
|
|
|
65
|
-
# Write output
|
|
65
|
+
# Write output only if content changed (avoids triggering DevReload loops)
|
|
66
66
|
out_path = Path(output)
|
|
67
67
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
-
|
|
68
|
+
try:
|
|
69
|
+
existing = out_path.read_text(encoding="utf-8")
|
|
70
|
+
except (FileNotFoundError, OSError):
|
|
71
|
+
existing = None
|
|
72
|
+
if existing != css:
|
|
73
|
+
out_path.write_text(css, encoding="utf-8")
|
|
69
74
|
|
|
70
75
|
return css
|
|
71
76
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/public/swagger/oauth2-redirect.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/templates/docker/python/Dockerfile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.97 → tina4_python-3.11.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|