tina4-python 3.9.1__tar.gz → 3.10.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.9.1 → tina4_python-3.10.0}/PKG-INFO +1 -1
- {tina4_python-3.9.1 → tina4_python-3.10.0}/pyproject.toml +1 -1
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/CLAUDE.md +17 -8
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/__init__.py +1 -1
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/cli/__init__.py +31 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/core/response.py +75 -21
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/core/server.py +14 -18
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/messenger/__init__.py +2 -3
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/query_builder/__init__.py +146 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/session/__init__.py +5 -0
- tina4_python-3.10.0/tina4_python/websocket/backplane.py +121 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/.gitignore +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/README.md +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/Testing.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/core/events.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/core/request.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/core/router.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -233,7 +233,7 @@ Tina4 provides a full toolkit. Before writing custom code, check if the framewor
|
|
|
233
233
|
| GraphQL API | `GraphQL` from `tina4_python.graphql` |
|
|
234
234
|
| SOAP/WSDL services | `WSDL` from `tina4_python.wsdl` |
|
|
235
235
|
| Database migrations | `migrate`, `create_migration` from `tina4_python.migration` |
|
|
236
|
-
| WebSockets | `WebSocketServer` from `tina4_python.websocket` |
|
|
236
|
+
| WebSockets | `WebSocketServer` from `tina4_python.websocket`. Backplane via Redis pub/sub (`TINA4_WS_BACKPLANE`, `TINA4_WS_BACKPLANE_URL`) |
|
|
237
237
|
| SCSS compilation | Drop `.scss` in `src/scss/` — auto-compiled |
|
|
238
238
|
| Static file serving | Put files in `src/public/` — auto-served |
|
|
239
239
|
| Translations / i18n | `I18n` from `tina4_python.i18n` |
|
|
@@ -336,7 +336,7 @@ tina4_python/ # Core framework package (v3.0.0)
|
|
|
336
336
|
├── migration/ # SQL-file migrations (migrate, create_migration, rollback)
|
|
337
337
|
│ └── runner.py # Migration runner
|
|
338
338
|
├── session/ # Pluggable sessions (Session, FileSessionHandler, DatabaseSessionHandler)
|
|
339
|
-
├── websocket/ # RFC 6455 WebSocket server (WebSocketServer, WebSocketConnection)
|
|
339
|
+
├── websocket/ # RFC 6455 WebSocket server (WebSocketServer, WebSocketConnection, backplane)
|
|
340
340
|
├── graphql/ # Zero-dep GraphQL engine (GraphQL, Schema)
|
|
341
341
|
├── wsdl/ # SOAP 1.1 / WSDL server (WSDL, wsdl_operation)
|
|
342
342
|
├── crud/ # Auto-CRUD REST endpoint generator (AutoCrud)
|
|
@@ -480,7 +480,7 @@ Response.add_header("X-Custom", "value")
|
|
|
480
480
|
|
|
481
481
|
## Sessions
|
|
482
482
|
|
|
483
|
-
|
|
483
|
+
TINA4_TOKEN_LIMIT is used to set the session time, default 60 minutes
|
|
484
484
|
|
|
485
485
|
### Session Backends
|
|
486
486
|
|
|
@@ -504,6 +504,7 @@ TINA4_SESSION_MONGO_USERNAME= # optional
|
|
|
504
504
|
TINA4_SESSION_MONGO_PASSWORD= # optional
|
|
505
505
|
TINA4_SESSION_MONGO_DB=tina4_sessions # default database
|
|
506
506
|
TINA4_SESSION_MONGO_COLLECTION=sessions # default collection
|
|
507
|
+
TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookies (default: Lax)
|
|
507
508
|
```
|
|
508
509
|
|
|
509
510
|
### Authentication & Security
|
|
@@ -836,6 +837,9 @@ result = User().select(filter="name = ?", params=["Alice"], limit=10)
|
|
|
836
837
|
|
|
837
838
|
# Convert to dict
|
|
838
839
|
user.to_dict()
|
|
840
|
+
|
|
841
|
+
# NoSQL support: to_mongo() generates MongoDB query documents from the same fluent API
|
|
842
|
+
result.to_mongo()
|
|
839
843
|
```
|
|
840
844
|
|
|
841
845
|
### Available field types
|
|
@@ -1515,8 +1519,8 @@ Key `.env` settings:
|
|
|
1515
1519
|
```bash
|
|
1516
1520
|
# Authentication
|
|
1517
1521
|
SECRET=your-jwt-secret # JWT signing (default uses insecure placeholder)
|
|
1518
|
-
|
|
1519
|
-
|
|
1522
|
+
TINA4_API_KEY=your-api-key # Static bearer token for API auth (API_KEY fallback supported)
|
|
1523
|
+
TINA4_TOKEN_LIMIT=60 # Token lifetime in minutes (default: 60)
|
|
1520
1524
|
|
|
1521
1525
|
# Database
|
|
1522
1526
|
DATABASE_URL=sqlite:///app.db # Connection URL (driver://host:port/database)
|
|
@@ -1525,16 +1529,17 @@ DATABASE_PASSWORD= # DB password
|
|
|
1525
1529
|
|
|
1526
1530
|
# Framework
|
|
1527
1531
|
TINA4_DEBUG=true # Enable dev mode (toolbar, live reload, error overlay)
|
|
1528
|
-
TINA4_LOG_LEVEL=
|
|
1532
|
+
TINA4_LOG_LEVEL=ERROR # Log verbosity: ALL, DEBUG, INFO, WARNING, ERROR (default: ERROR)
|
|
1529
1533
|
TINA4_LOCALE=en # Language for framework messages (en, fr, af, zh, ja, es)
|
|
1530
1534
|
TINA4_DEFAULT_WEBSERVER=FALSE # Set to TRUE to use Tina4's built-in webserver instead of ASGI
|
|
1531
1535
|
HOST_NAME=localhost:7145
|
|
1532
1536
|
|
|
1533
1537
|
# Sessions
|
|
1534
1538
|
TINA4_SESSION_HANDLER=SessionFileHandler # SessionFileHandler, SessionRedisHandler, SessionValkeyHandler, SessionMongoHandler
|
|
1539
|
+
TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookies (default: Lax)
|
|
1535
1540
|
|
|
1536
1541
|
# Swagger/OpenAPI
|
|
1537
|
-
SWAGGER_TITLE=
|
|
1542
|
+
SWAGGER_TITLE=Tina4 API # API title (default: "Tina4 API")
|
|
1538
1543
|
SWAGGER_VERSION=1.0.0 # API version
|
|
1539
1544
|
SWAGGER_DESCRIPTION= # API description
|
|
1540
1545
|
SWAGGER_CONTACT_TEAM= # Contact name
|
|
@@ -1763,11 +1768,15 @@ async def dashboard(request, response):
|
|
|
1763
1768
|
- **`tina4python generate`**: model, route, migration, middleware scaffolding
|
|
1764
1769
|
- **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), query caching (`TINA4_DB_CACHE=true`, `cache_stats()`, `cache_clear()`)
|
|
1765
1770
|
- **Sessions**: 4 backends (file, Redis/Valkey, MongoDB, database)
|
|
1766
|
-
- **Queue**:
|
|
1771
|
+
- **Queue**: file/RabbitMQ/Kafka/MongoDB backends, configured via env vars
|
|
1767
1772
|
- **Cache**: memory/Redis/file backends
|
|
1768
1773
|
- **Messenger**: .env driven SMTP/IMAP
|
|
1769
1774
|
- **ORM relationships**: `has_many`, `has_one`, `belongs_to` with eager loading (`include=`)
|
|
1770
1775
|
- **Frond pre-compilation**: 2.8x template render improvement, `Frond.clear_cache()`
|
|
1776
|
+
- **QueryBuilder** with NoSQL/MongoDB support (`to_mongo()`)
|
|
1777
|
+
- **WebSocket backplane** (Redis pub/sub) for horizontal scaling
|
|
1778
|
+
- **SameSite=Lax** default on session cookies (`TINA4_SESSION_SAMESITE`)
|
|
1779
|
+
- **`tina4 init`** generates Dockerfile and .dockerignore
|
|
1771
1780
|
- **Gallery**: 7 interactive examples with Try It deploy at `/__dev/`
|
|
1772
1781
|
|
|
1773
1782
|
|
|
@@ -141,6 +141,37 @@ def _init(args):
|
|
|
141
141
|
if not dst_file.exists() and src_file.exists():
|
|
142
142
|
dst_file.write_text(src_file.read_text(encoding="utf-8"), encoding="utf-8")
|
|
143
143
|
|
|
144
|
+
# Create root Dockerfile (uv variant) if it doesn't exist
|
|
145
|
+
root_dockerfile = target / "Dockerfile"
|
|
146
|
+
if not root_dockerfile.exists():
|
|
147
|
+
root_dockerfile.write_text(
|
|
148
|
+
'FROM python:3.13-slim AS build\n'
|
|
149
|
+
'WORKDIR /app\n'
|
|
150
|
+
'COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv\n'
|
|
151
|
+
'COPY pyproject.toml uv.lock* ./\n'
|
|
152
|
+
'RUN uv sync --frozen --no-dev\n'
|
|
153
|
+
'COPY . .\n'
|
|
154
|
+
'\n'
|
|
155
|
+
'FROM python:3.13-slim\n'
|
|
156
|
+
'WORKDIR /app\n'
|
|
157
|
+
'COPY --from=build /app .\n'
|
|
158
|
+
'COPY --from=build /usr/local/bin/uv /usr/local/bin/uv\n'
|
|
159
|
+
'ENV PATH="/app/.venv/bin:$PATH"\n'
|
|
160
|
+
'ENV HOST=0.0.0.0\n'
|
|
161
|
+
'ENV PORT=7145\n'
|
|
162
|
+
'EXPOSE 7145\n'
|
|
163
|
+
'CMD ["python", "app.py"]\n',
|
|
164
|
+
encoding="utf-8",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Create root .dockerignore if it doesn't exist
|
|
168
|
+
root_dockerignore = target / ".dockerignore"
|
|
169
|
+
if not root_dockerignore.exists():
|
|
170
|
+
root_dockerignore.write_text(
|
|
171
|
+
".venv\n__pycache__\n.git\n.claude\n.env\n*.log\ntests\ntmp\n",
|
|
172
|
+
encoding="utf-8",
|
|
173
|
+
)
|
|
174
|
+
|
|
144
175
|
# Auto-detect AI tools and install context
|
|
145
176
|
from tina4_python.ai import detect_ai_names, install_context, install_all
|
|
146
177
|
detected = detect_ai_names(str(target))
|
|
@@ -22,6 +22,54 @@ import mimetypes
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Global Frond template engine registry
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
_global_frond = None
|
|
29
|
+
_framework_frond = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_frond():
|
|
33
|
+
"""Return the global Frond engine, creating a default if needed."""
|
|
34
|
+
global _global_frond
|
|
35
|
+
if _global_frond is None:
|
|
36
|
+
from tina4_python.frond.engine import Frond
|
|
37
|
+
_global_frond = Frond("src/templates")
|
|
38
|
+
return _global_frond
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_framework_frond():
|
|
42
|
+
"""Return the singleton Frond engine for built-in framework templates."""
|
|
43
|
+
global _framework_frond
|
|
44
|
+
framework_dir = Path(__file__).resolve().parent.parent / "templates"
|
|
45
|
+
if _framework_frond is None and framework_dir.is_dir():
|
|
46
|
+
from tina4_python.frond.engine import Frond
|
|
47
|
+
_framework_frond = Frond(str(framework_dir))
|
|
48
|
+
# Sync custom filters/globals from the user engine
|
|
49
|
+
if _framework_frond is not None:
|
|
50
|
+
user_engine = get_frond()
|
|
51
|
+
_framework_frond._filters.update(user_engine._filters)
|
|
52
|
+
_framework_frond._globals.update(user_engine._globals)
|
|
53
|
+
return _framework_frond
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_frond(engine):
|
|
57
|
+
"""Register a pre-configured Frond engine for response.render().
|
|
58
|
+
|
|
59
|
+
Call this at startup after registering custom filters and globals:
|
|
60
|
+
|
|
61
|
+
from tina4_python.frond import Frond
|
|
62
|
+
from tina4_python.core.response import set_frond
|
|
63
|
+
|
|
64
|
+
engine = Frond("src/templates")
|
|
65
|
+
engine.add_filter("money", my_money_filter)
|
|
66
|
+
engine.add_global("APP_VERSION", "1.0")
|
|
67
|
+
set_frond(engine)
|
|
68
|
+
"""
|
|
69
|
+
global _global_frond
|
|
70
|
+
_global_frond = engine
|
|
71
|
+
|
|
72
|
+
|
|
25
73
|
class Response:
|
|
26
74
|
"""HTTP response builder with compression and ETag support."""
|
|
27
75
|
|
|
@@ -175,27 +223,33 @@ class Response:
|
|
|
175
223
|
return self
|
|
176
224
|
|
|
177
225
|
def render(self, template: str, data: dict = None) -> "Response":
|
|
178
|
-
"""Render a Frond/Twig template with data.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
"""Render a Frond/Twig template with data.
|
|
227
|
+
|
|
228
|
+
Uses the global Frond engine (registered via set_frond()) so that
|
|
229
|
+
custom filters and globals are available in all templates.
|
|
230
|
+
Falls back to framework templates if not found in user dir.
|
|
231
|
+
"""
|
|
232
|
+
engine = get_frond()
|
|
233
|
+
|
|
234
|
+
# Try user templates first (the global engine's directory)
|
|
235
|
+
try:
|
|
236
|
+
html = engine.render(template, data or {})
|
|
237
|
+
return self.html(html)
|
|
238
|
+
except FileNotFoundError:
|
|
239
|
+
pass
|
|
240
|
+
except Exception as e:
|
|
241
|
+
return self.html(f"<pre>Template error: {e}</pre>", 500)
|
|
242
|
+
|
|
243
|
+
# Fallback: framework templates (singleton, filters/globals synced)
|
|
244
|
+
fw_engine = get_framework_frond()
|
|
245
|
+
if fw_engine is not None:
|
|
246
|
+
try:
|
|
247
|
+
html = fw_engine.render(template, data or {})
|
|
248
|
+
return self.html(html)
|
|
249
|
+
except FileNotFoundError:
|
|
250
|
+
pass
|
|
251
|
+
except Exception as e:
|
|
252
|
+
return self.html(f"<pre>Template error: {e}</pre>", 500)
|
|
199
253
|
|
|
200
254
|
return self.html(f"<pre>Template not found: {template}</pre>", 404)
|
|
201
255
|
|
|
@@ -105,7 +105,7 @@ def _render_error_page(status_code: int, path: str, request_id: str, error_messa
|
|
|
105
105
|
|
|
106
106
|
Returns rendered HTML string, or None if no template found.
|
|
107
107
|
"""
|
|
108
|
-
from tina4_python.
|
|
108
|
+
from tina4_python.core.response import get_frond, get_framework_frond
|
|
109
109
|
|
|
110
110
|
template_name = f"errors/{status_code}.twig"
|
|
111
111
|
data = {
|
|
@@ -115,21 +115,17 @@ def _render_error_page(status_code: int, path: str, request_id: str, error_messa
|
|
|
115
115
|
"status_code": status_code,
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
# 1. Try user override
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return engine.render(template_name, data)
|
|
124
|
-
except Exception:
|
|
125
|
-
pass
|
|
118
|
+
# 1. Try user override (singleton engine with custom filters/globals)
|
|
119
|
+
try:
|
|
120
|
+
return get_frond().render(template_name, data)
|
|
121
|
+
except (FileNotFoundError, Exception):
|
|
122
|
+
pass
|
|
126
123
|
|
|
127
|
-
# 2. Try framework default
|
|
128
|
-
|
|
129
|
-
if
|
|
124
|
+
# 2. Try framework default (singleton, filters/globals synced)
|
|
125
|
+
fw_engine = get_framework_frond()
|
|
126
|
+
if fw_engine is not None:
|
|
130
127
|
try:
|
|
131
|
-
|
|
132
|
-
return engine.render(template_name, data)
|
|
128
|
+
return fw_engine.render(template_name, data)
|
|
133
129
|
except Exception:
|
|
134
130
|
pass
|
|
135
131
|
|
|
@@ -848,9 +844,8 @@ async def app(scope: dict, receive, send):
|
|
|
848
844
|
# Try serving a template file (e.g. /hello -> src/templates/hello.twig or hello.html)
|
|
849
845
|
tpl_file = _resolve_template(request.path)
|
|
850
846
|
if tpl_file:
|
|
851
|
-
from tina4_python.
|
|
852
|
-
|
|
853
|
-
html = frond.render(tpl_file, {})
|
|
847
|
+
from tina4_python.core.response import get_frond
|
|
848
|
+
html = get_frond().render(tpl_file, {})
|
|
854
849
|
response.html(html)
|
|
855
850
|
elif request.path == "/":
|
|
856
851
|
response.html(_render_landing_page())
|
|
@@ -911,7 +906,8 @@ async def app(scope: dict, receive, send):
|
|
|
911
906
|
sid = request.session.session_id if hasattr(request.session, 'session_id') else getattr(request.session, 'id', None)
|
|
912
907
|
if sid:
|
|
913
908
|
ttl = int(os.environ.get("TINA4_SESSION_TTL", "3600"))
|
|
914
|
-
|
|
909
|
+
samesite = os.environ.get("TINA4_SESSION_SAMESITE", "Lax")
|
|
910
|
+
response.header("set-cookie", f"tina4_session={sid}; Path=/; HttpOnly; SameSite={samesite}; Max-Age={ttl}")
|
|
915
911
|
except Exception:
|
|
916
912
|
pass
|
|
917
913
|
|
|
@@ -203,9 +203,8 @@ class Messenger:
|
|
|
203
203
|
**kwargs: Passed to send() (cc, bcc, attachments, etc.)
|
|
204
204
|
"""
|
|
205
205
|
try:
|
|
206
|
-
from tina4_python.
|
|
207
|
-
|
|
208
|
-
body = engine.render_string(template, data or {})
|
|
206
|
+
from tina4_python.core.response import get_frond
|
|
207
|
+
body = get_frond().render_string(template, data or {})
|
|
209
208
|
except ImportError:
|
|
210
209
|
body = template
|
|
211
210
|
|
|
@@ -257,6 +257,152 @@ class QueryBuilder:
|
|
|
257
257
|
"""
|
|
258
258
|
return self.count() > 0
|
|
259
259
|
|
|
260
|
+
def to_mongo(self) -> dict:
|
|
261
|
+
"""Convert the fluent builder state into a MongoDB-compatible query.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
A dict with keys: filter, projection, sort, limit, skip.
|
|
265
|
+
Only non-empty keys are included.
|
|
266
|
+
"""
|
|
267
|
+
result = {}
|
|
268
|
+
|
|
269
|
+
# -- projection --
|
|
270
|
+
if self._columns != ["*"]:
|
|
271
|
+
result["projection"] = {col.strip(): 1 for col in self._columns}
|
|
272
|
+
|
|
273
|
+
# -- filter --
|
|
274
|
+
if self._wheres:
|
|
275
|
+
param_index = 0
|
|
276
|
+
and_conditions = []
|
|
277
|
+
or_conditions = []
|
|
278
|
+
|
|
279
|
+
for i, (connector, condition) in enumerate(self._wheres):
|
|
280
|
+
mongo_cond, param_index = self._parse_condition_to_mongo(
|
|
281
|
+
condition, param_index
|
|
282
|
+
)
|
|
283
|
+
if i == 0 or connector == "AND":
|
|
284
|
+
and_conditions.append(mongo_cond)
|
|
285
|
+
else:
|
|
286
|
+
or_conditions.append(mongo_cond)
|
|
287
|
+
|
|
288
|
+
if or_conditions:
|
|
289
|
+
# Merge AND conditions into a single dict, then $or with OR ones
|
|
290
|
+
and_merged = self._merge_mongo_conditions(and_conditions)
|
|
291
|
+
all_branches = [and_merged] + or_conditions
|
|
292
|
+
result["filter"] = {"$or": all_branches}
|
|
293
|
+
else:
|
|
294
|
+
result["filter"] = self._merge_mongo_conditions(and_conditions)
|
|
295
|
+
|
|
296
|
+
# -- sort --
|
|
297
|
+
if self._order_by_cols:
|
|
298
|
+
sort_list = []
|
|
299
|
+
for expr in self._order_by_cols:
|
|
300
|
+
parts = expr.strip().split()
|
|
301
|
+
field = parts[0]
|
|
302
|
+
direction = -1 if len(parts) > 1 and parts[1].upper() == "DESC" else 1
|
|
303
|
+
sort_list.append((field, direction))
|
|
304
|
+
result["sort"] = sort_list
|
|
305
|
+
|
|
306
|
+
# -- limit / skip --
|
|
307
|
+
if self._limit_val is not None:
|
|
308
|
+
result["limit"] = self._limit_val
|
|
309
|
+
if self._offset_val is not None:
|
|
310
|
+
result["skip"] = self._offset_val
|
|
311
|
+
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
def _parse_condition_to_mongo(self, condition: str, param_index: int) -> tuple[dict, int]:
|
|
315
|
+
"""Parse a single SQL condition string into a MongoDB filter dict.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
(mongo_filter_dict, updated_param_index)
|
|
319
|
+
"""
|
|
320
|
+
import re
|
|
321
|
+
|
|
322
|
+
cond = condition.strip()
|
|
323
|
+
|
|
324
|
+
# IS NOT NULL
|
|
325
|
+
match = re.match(r"^(\w+)\s+IS\s+NOT\s+NULL$", cond, re.IGNORECASE)
|
|
326
|
+
if match:
|
|
327
|
+
return {match.group(1): {"$exists": True, "$ne": None}}, param_index
|
|
328
|
+
|
|
329
|
+
# IS NULL
|
|
330
|
+
match = re.match(r"^(\w+)\s+IS\s+NULL$", cond, re.IGNORECASE)
|
|
331
|
+
if match:
|
|
332
|
+
return {match.group(1): {"$exists": False}}, param_index
|
|
333
|
+
|
|
334
|
+
# NOT IN
|
|
335
|
+
match = re.match(r"^(\w+)\s+NOT\s+IN\s*\(\s*\?\s*\)$", cond, re.IGNORECASE)
|
|
336
|
+
if match:
|
|
337
|
+
val = self._params[param_index] if param_index < len(self._params) else []
|
|
338
|
+
values = val if isinstance(val, list) else [val]
|
|
339
|
+
return {match.group(1): {"$nin": values}}, param_index + 1
|
|
340
|
+
|
|
341
|
+
# IN
|
|
342
|
+
match = re.match(r"^(\w+)\s+IN\s*\(\s*\?\s*\)$", cond, re.IGNORECASE)
|
|
343
|
+
if match:
|
|
344
|
+
val = self._params[param_index] if param_index < len(self._params) else []
|
|
345
|
+
values = val if isinstance(val, list) else [val]
|
|
346
|
+
return {match.group(1): {"$in": values}}, param_index + 1
|
|
347
|
+
|
|
348
|
+
# LIKE
|
|
349
|
+
match = re.match(r"^(\w+)\s+LIKE\s+\?$", cond, re.IGNORECASE)
|
|
350
|
+
if match:
|
|
351
|
+
val = self._params[param_index] if param_index < len(self._params) else ""
|
|
352
|
+
# Convert SQL LIKE pattern to regex: % -> .*, _ -> .
|
|
353
|
+
pattern = str(val).replace("%", ".*").replace("_", ".")
|
|
354
|
+
return {match.group(1): {"$regex": pattern, "$options": "i"}}, param_index + 1
|
|
355
|
+
|
|
356
|
+
# Comparison operators: >=, <=, <>, !=, >, <, =
|
|
357
|
+
match = re.match(r"^(\w+)\s*(>=|<=|<>|!=|>|<|=)\s*\?$", cond)
|
|
358
|
+
if match:
|
|
359
|
+
field = match.group(1)
|
|
360
|
+
op = match.group(2)
|
|
361
|
+
val = self._params[param_index] if param_index < len(self._params) else None
|
|
362
|
+
op_map = {
|
|
363
|
+
"=": None,
|
|
364
|
+
"!=": "$ne",
|
|
365
|
+
"<>": "$ne",
|
|
366
|
+
">": "$gt",
|
|
367
|
+
">=": "$gte",
|
|
368
|
+
"<": "$lt",
|
|
369
|
+
"<=": "$lte",
|
|
370
|
+
}
|
|
371
|
+
mongo_op = op_map.get(op)
|
|
372
|
+
if mongo_op is None:
|
|
373
|
+
return {field: val}, param_index + 1
|
|
374
|
+
return {field: {mongo_op: val}}, param_index + 1
|
|
375
|
+
|
|
376
|
+
# Fallback: return condition as-is in $where (raw JS expression)
|
|
377
|
+
return {"$where": cond}, param_index
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def _merge_mongo_conditions(conditions: list[dict]) -> dict:
|
|
381
|
+
"""Merge a list of single-field mongo condition dicts into one dict.
|
|
382
|
+
|
|
383
|
+
If the same field appears in multiple conditions, wraps them in $and.
|
|
384
|
+
"""
|
|
385
|
+
if len(conditions) == 1:
|
|
386
|
+
return conditions[0]
|
|
387
|
+
|
|
388
|
+
merged = {}
|
|
389
|
+
conflicts = []
|
|
390
|
+
for cond in conditions:
|
|
391
|
+
for key, val in cond.items():
|
|
392
|
+
if key in merged:
|
|
393
|
+
conflicts.append(cond)
|
|
394
|
+
break
|
|
395
|
+
else:
|
|
396
|
+
merged[key] = val
|
|
397
|
+
else:
|
|
398
|
+
continue
|
|
399
|
+
# If we broke out due to conflict, don't add to merged
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
if conflicts:
|
|
403
|
+
return {"$and": conditions}
|
|
404
|
+
return merged
|
|
405
|
+
|
|
260
406
|
# -- Private helpers --
|
|
261
407
|
|
|
262
408
|
def _build_where(self) -> str:
|
|
@@ -265,6 +265,11 @@ class Session:
|
|
|
265
265
|
self.unset(flash_key)
|
|
266
266
|
return val
|
|
267
267
|
|
|
268
|
+
def cookie_header(self, cookie_name: str = "tina4_session") -> str:
|
|
269
|
+
"""Return a Set-Cookie header value for this session."""
|
|
270
|
+
samesite = os.environ.get("TINA4_SESSION_SAMESITE", "Lax")
|
|
271
|
+
return f"{cookie_name}={self._session_id}; Path=/; HttpOnly; SameSite={samesite}; Max-Age={self._ttl}"
|
|
272
|
+
|
|
268
273
|
def gc(self):
|
|
269
274
|
"""Run garbage collection on the backend."""
|
|
270
275
|
self._handler.gc(self._ttl)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket Backplane Abstraction for Tina4 Python.
|
|
3
|
+
|
|
4
|
+
Enables broadcasting WebSocket messages across multiple server instances
|
|
5
|
+
using a shared pub/sub channel (e.g. Redis). Without a backplane configured,
|
|
6
|
+
broadcast() only reaches connections on the local process.
|
|
7
|
+
|
|
8
|
+
Configuration via environment variables:
|
|
9
|
+
TINA4_WS_BACKPLANE — Backend type: "redis", "nats", or "" (default: none)
|
|
10
|
+
TINA4_WS_BACKPLANE_URL — Connection string (default: redis://localhost:6379)
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
backplane = create_backplane()
|
|
14
|
+
if backplane:
|
|
15
|
+
backplane.subscribe("chat", lambda msg: relay_to_local(msg))
|
|
16
|
+
backplane.publish("chat", '{"user": "A", "text": "hello"}')
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import threading
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WebSocketBackplane:
|
|
27
|
+
"""Base backplane interface for scaling WebSocket broadcast across instances.
|
|
28
|
+
|
|
29
|
+
Subclasses implement publish/subscribe over a shared message bus so that
|
|
30
|
+
every server instance receives every broadcast, not just the originator.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def publish(self, channel: str, message: str) -> None:
|
|
34
|
+
"""Publish a message to all instances listening on *channel*."""
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
def subscribe(self, channel: str, callback) -> None:
|
|
38
|
+
"""Subscribe to *channel*. *callback(message: str)* is invoked for
|
|
39
|
+
each incoming message. Runs in a background thread."""
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
def unsubscribe(self, channel: str) -> None:
|
|
43
|
+
"""Stop listening on *channel*."""
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
def close(self) -> None:
|
|
47
|
+
"""Tear down connections and background threads."""
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RedisBackplane(WebSocketBackplane):
|
|
52
|
+
"""Redis pub/sub backplane.
|
|
53
|
+
|
|
54
|
+
Requires the ``redis`` package (``pip install redis``). The import is
|
|
55
|
+
deferred so the rest of Tina4 works fine without it installed — an error
|
|
56
|
+
is raised only when this class is actually instantiated.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, url: str | None = None):
|
|
60
|
+
try:
|
|
61
|
+
import redis
|
|
62
|
+
except ImportError:
|
|
63
|
+
raise ImportError(
|
|
64
|
+
"The 'redis' package is required for RedisBackplane. "
|
|
65
|
+
"Install it with: pip install redis"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self._url = url or os.environ.get(
|
|
69
|
+
"TINA4_WS_BACKPLANE_URL", "redis://localhost:6379"
|
|
70
|
+
)
|
|
71
|
+
self._redis = redis.Redis.from_url(self._url)
|
|
72
|
+
self._pubsub = self._redis.pubsub()
|
|
73
|
+
self._threads: dict[str, threading.Thread] = {}
|
|
74
|
+
self._running = True
|
|
75
|
+
logger.info("RedisBackplane connected to %s", self._url)
|
|
76
|
+
|
|
77
|
+
def publish(self, channel: str, message: str) -> None:
|
|
78
|
+
self._redis.publish(channel, message)
|
|
79
|
+
|
|
80
|
+
def subscribe(self, channel: str, callback) -> None:
|
|
81
|
+
self._pubsub.subscribe(**{channel: lambda raw: callback(raw["data"].decode() if isinstance(raw["data"], bytes) else raw["data"])})
|
|
82
|
+
thread = self._pubsub.run_in_thread(sleep_time=0.01, daemon=True)
|
|
83
|
+
self._threads[channel] = thread
|
|
84
|
+
logger.info("RedisBackplane subscribed to channel '%s'", channel)
|
|
85
|
+
|
|
86
|
+
def unsubscribe(self, channel: str) -> None:
|
|
87
|
+
self._pubsub.unsubscribe(channel)
|
|
88
|
+
thread = self._threads.pop(channel, None)
|
|
89
|
+
if thread:
|
|
90
|
+
thread.stop()
|
|
91
|
+
logger.info("RedisBackplane unsubscribed from channel '%s'", channel)
|
|
92
|
+
|
|
93
|
+
def close(self) -> None:
|
|
94
|
+
self._running = False
|
|
95
|
+
for thread in self._threads.values():
|
|
96
|
+
thread.stop()
|
|
97
|
+
self._threads.clear()
|
|
98
|
+
self._pubsub.close()
|
|
99
|
+
self._redis.close()
|
|
100
|
+
logger.info("RedisBackplane closed")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def create_backplane(url: str | None = None) -> WebSocketBackplane | None:
|
|
104
|
+
"""Factory that reads TINA4_WS_BACKPLANE and returns the appropriate
|
|
105
|
+
backplane instance, or *None* if no backplane is configured.
|
|
106
|
+
|
|
107
|
+
This keeps backplane usage entirely optional — callers simply check
|
|
108
|
+
``if backplane:`` before publishing.
|
|
109
|
+
"""
|
|
110
|
+
backend = os.environ.get("TINA4_WS_BACKPLANE", "").strip().lower()
|
|
111
|
+
|
|
112
|
+
if backend == "redis":
|
|
113
|
+
return RedisBackplane(url=url)
|
|
114
|
+
elif backend == "nats":
|
|
115
|
+
raise NotImplementedError(
|
|
116
|
+
"NATS backplane is on the roadmap but not yet implemented."
|
|
117
|
+
)
|
|
118
|
+
elif backend == "":
|
|
119
|
+
return None
|
|
120
|
+
else:
|
|
121
|
+
raise ValueError(f"Unknown TINA4_WS_BACKPLANE value: '{backend}'")
|
|
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.9.1 → tina4_python-3.10.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.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
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/gallery/templates/src/routes/gallery_page.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
|
|
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.9.1 → tina4_python-3.10.0}/tina4_python/templates/docker/distroless/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
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.10.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|