tina4-python 3.9.1__tar.gz → 3.9.2__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.9.2}/PKG-INFO +1 -1
- {tina4_python-3.9.1 → tina4_python-3.9.2}/pyproject.toml +1 -1
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/CLAUDE.md +6 -6
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/__init__.py +1 -1
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/cli/__init__.py +31 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/core/server.py +2 -1
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/query_builder/__init__.py +146 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/session/__init__.py +5 -0
- tina4_python-3.9.2/tina4_python/websocket/backplane.py +121 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/.gitignore +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/README.md +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/Testing.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/core/events.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/core/request.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/core/response.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/core/router.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -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
|
|
|
@@ -1515,8 +1515,8 @@ Key `.env` settings:
|
|
|
1515
1515
|
```bash
|
|
1516
1516
|
# Authentication
|
|
1517
1517
|
SECRET=your-jwt-secret # JWT signing (default uses insecure placeholder)
|
|
1518
|
-
|
|
1519
|
-
|
|
1518
|
+
TINA4_API_KEY=your-api-key # Static bearer token for API auth (API_KEY fallback supported)
|
|
1519
|
+
TINA4_TOKEN_LIMIT=60 # Token lifetime in minutes (default: 60)
|
|
1520
1520
|
|
|
1521
1521
|
# Database
|
|
1522
1522
|
DATABASE_URL=sqlite:///app.db # Connection URL (driver://host:port/database)
|
|
@@ -1525,7 +1525,7 @@ DATABASE_PASSWORD= # DB password
|
|
|
1525
1525
|
|
|
1526
1526
|
# Framework
|
|
1527
1527
|
TINA4_DEBUG=true # Enable dev mode (toolbar, live reload, error overlay)
|
|
1528
|
-
TINA4_LOG_LEVEL=
|
|
1528
|
+
TINA4_LOG_LEVEL=ERROR # Log verbosity: ALL, DEBUG, INFO, WARNING, ERROR (default: ERROR)
|
|
1529
1529
|
TINA4_LOCALE=en # Language for framework messages (en, fr, af, zh, ja, es)
|
|
1530
1530
|
TINA4_DEFAULT_WEBSERVER=FALSE # Set to TRUE to use Tina4's built-in webserver instead of ASGI
|
|
1531
1531
|
HOST_NAME=localhost:7145
|
|
@@ -1534,7 +1534,7 @@ HOST_NAME=localhost:7145
|
|
|
1534
1534
|
TINA4_SESSION_HANDLER=SessionFileHandler # SessionFileHandler, SessionRedisHandler, SessionValkeyHandler, SessionMongoHandler
|
|
1535
1535
|
|
|
1536
1536
|
# Swagger/OpenAPI
|
|
1537
|
-
SWAGGER_TITLE=
|
|
1537
|
+
SWAGGER_TITLE=Tina4 API # API title (default: "Tina4 API")
|
|
1538
1538
|
SWAGGER_VERSION=1.0.0 # API version
|
|
1539
1539
|
SWAGGER_DESCRIPTION= # API description
|
|
1540
1540
|
SWAGGER_CONTACT_TEAM= # Contact name
|
|
@@ -1763,7 +1763,7 @@ async def dashboard(request, response):
|
|
|
1763
1763
|
- **`tina4python generate`**: model, route, migration, middleware scaffolding
|
|
1764
1764
|
- **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), query caching (`TINA4_DB_CACHE=true`, `cache_stats()`, `cache_clear()`)
|
|
1765
1765
|
- **Sessions**: 4 backends (file, Redis/Valkey, MongoDB, database)
|
|
1766
|
-
- **Queue**:
|
|
1766
|
+
- **Queue**: file/RabbitMQ/Kafka/MongoDB backends, configured via env vars
|
|
1767
1767
|
- **Cache**: memory/Redis/file backends
|
|
1768
1768
|
- **Messenger**: .env driven SMTP/IMAP
|
|
1769
1769
|
- **ORM relationships**: `has_many`, `has_one`, `belongs_to` with eager loading (`include=`)
|
|
@@ -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))
|
|
@@ -911,7 +911,8 @@ async def app(scope: dict, receive, send):
|
|
|
911
911
|
sid = request.session.session_id if hasattr(request.session, 'session_id') else getattr(request.session, 'id', None)
|
|
912
912
|
if sid:
|
|
913
913
|
ttl = int(os.environ.get("TINA4_SESSION_TTL", "3600"))
|
|
914
|
-
|
|
914
|
+
samesite = os.environ.get("TINA4_SESSION_SAMESITE", "Lax")
|
|
915
|
+
response.header("set-cookie", f"tina4_session={sid}; Path=/; HttpOnly; SameSite={samesite}; Max-Age={ttl}")
|
|
915
916
|
except Exception:
|
|
916
917
|
pass
|
|
917
918
|
|
|
@@ -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
|
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/gallery/database/src/routes/api/gallery_db.py
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.9.1 → tina4_python-3.9.2}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/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
|
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/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.9.2}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.9.1 → tina4_python-3.9.2}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|