tina4-python 3.13.16__tar.gz → 3.13.21__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tina4_python-3.13.16 → tina4_python-3.13.21}/PKG-INFO +1 -1
- {tina4_python-3.13.16 → tina4_python-3.13.21}/pyproject.toml +1 -1
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/CLAUDE.md +32 -9
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/__init__.py +2 -2
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/auth/__init__.py +42 -20
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/core/middleware.py +4 -4
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/core/response.py +33 -1
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/frond/engine.py +2 -2
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/orm/__init__.py +2 -2
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/orm/model.py +21 -11
- {tina4_python-3.13.16 → tina4_python-3.13.21}/.gitignore +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/README.md +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/env.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -128,13 +128,13 @@ Rules:
|
|
|
128
128
|
```python
|
|
129
129
|
# app.py
|
|
130
130
|
from tina4_python.core import run
|
|
131
|
-
from tina4_python.orm import
|
|
131
|
+
from tina4_python.orm import bind_database
|
|
132
132
|
from tina4_python.frond import Frond
|
|
133
133
|
from tina4_python.database import Database
|
|
134
134
|
|
|
135
135
|
# 1. Database & ORM
|
|
136
136
|
db = Database("sqlite:///app.db")
|
|
137
|
-
|
|
137
|
+
bind_database(db)
|
|
138
138
|
|
|
139
139
|
# 2. Custom Twig filters
|
|
140
140
|
Frond.add_filter("money", lambda v: f"{float(v or 0):,.2f}")
|
|
@@ -328,7 +328,7 @@ tina4_python/ # Core framework package (v3.0.0)
|
|
|
328
328
|
│ ├── connection.py # Database class (URL-based connection)
|
|
329
329
|
│ ├── adapter.py # DatabaseAdapter, DatabaseResult, SQLTranslator
|
|
330
330
|
│ ├── sqlite.py, postgres.py, mysql.py, mssql.py, firebird.py, odbc.py
|
|
331
|
-
├── orm/ # Active Record ORM (ORM, Field,
|
|
331
|
+
├── orm/ # Active Record ORM (ORM, Field, bind_database)
|
|
332
332
|
│ ├── model.py # ORM base class
|
|
333
333
|
│ └── fields.py # IntegerField, StringField, etc.
|
|
334
334
|
├── frond/ # Template engine (Frond — Jinja2/Twig-compatible)
|
|
@@ -479,6 +479,9 @@ request.session # Session object — use .get(key) and .set(key, value)
|
|
|
479
479
|
|
|
480
480
|
```python
|
|
481
481
|
return response({"data": []}) # JSON (auto-detected from dict/list)
|
|
482
|
+
return response(User.find(1)) # ORM model -> JSON object
|
|
483
|
+
return response(User.all()) # list of models -> JSON array
|
|
484
|
+
return response(db.fetch("SELECT * FROM users")) # DatabaseResult -> JSON array
|
|
482
485
|
return response("<h1>Hello</h1>") # HTML
|
|
483
486
|
return response("Not found", 404) # With status code
|
|
484
487
|
return response.redirect("/login") # Redirect
|
|
@@ -487,6 +490,8 @@ return response.file("doc.pdf") # Serve a file
|
|
|
487
490
|
return response.stream(generator) # SSE/streaming response (text/event-stream)
|
|
488
491
|
```
|
|
489
492
|
|
|
493
|
+
`response()` auto-serializes a domain object — an ORM model, a list of models, or a `DatabaseResult` — to JSON with no manual `to_dict()` / `to_json()`. A single model becomes a JSON object; a list of models or a `DatabaseResult` becomes a JSON array. Dicts, lists and strings behave exactly as before (purely additive).
|
|
494
|
+
|
|
490
495
|
Add custom headers before returning:
|
|
491
496
|
```python
|
|
492
497
|
from tina4_python.core.response import Response
|
|
@@ -821,19 +826,37 @@ class User(ORM):
|
|
|
821
826
|
email = StringField()
|
|
822
827
|
```
|
|
823
828
|
|
|
824
|
-
|
|
829
|
+
**Binding the database:**
|
|
830
|
+
|
|
831
|
+
- **`.env` default (no call needed)** — models auto-bind to `TINA4_DATABASE_URL` when it is set, so most apps need no binding call at all.
|
|
832
|
+
- **`bind_database(db)`** — override the default explicitly; assigns the connection to all ORM subclasses that don't select a named one.
|
|
833
|
+
- **`bind_database(db, name="analytics")` + `_db = "analytics"`** — register a named/secondary connection and point a model at it. A missing named connection raises a clear error.
|
|
834
|
+
|
|
825
835
|
```python
|
|
826
|
-
from tina4_python.orm import
|
|
836
|
+
from tina4_python.orm import bind_database
|
|
827
837
|
from tina4_python.database import Database
|
|
828
838
|
|
|
829
|
-
|
|
839
|
+
# Most apps: nothing to do — the .env default (TINA4_DATABASE_URL) is auto-bound.
|
|
840
|
+
|
|
841
|
+
# Override the default explicitly:
|
|
842
|
+
bind_database(Database("sqlite:///app.db")) # Assigns DB to all ORM subclasses
|
|
843
|
+
|
|
844
|
+
# Register a NAMED connection and point a model at it:
|
|
845
|
+
bind_database(Database("postgres://…/analytics", "u", "p"), name="analytics")
|
|
846
|
+
|
|
847
|
+
class Visit(ORM):
|
|
848
|
+
_db = "analytics" # this model uses the analytics connection
|
|
830
849
|
```
|
|
831
850
|
|
|
832
851
|
### ORM operations
|
|
833
852
|
|
|
853
|
+
The constructor accepts a dict, a JSON object string, or keyword args. Passing a list/array raises a clear `TypeError` (map over the list to build many records).
|
|
854
|
+
|
|
834
855
|
```python
|
|
835
856
|
# Create
|
|
836
|
-
user = User({"name": "Alice", "email": "alice@example.com"})
|
|
857
|
+
user = User({"name": "Alice", "email": "alice@example.com"}) # dict
|
|
858
|
+
user = User('{"name": "Alice", "email": "alice@example.com"}') # JSON object string
|
|
859
|
+
user = User(name="Alice", email="alice@example.com") # keyword args
|
|
837
860
|
user.save()
|
|
838
861
|
|
|
839
862
|
# Load — alias for select_one (class method, returns instance or None)
|
|
@@ -1567,7 +1590,7 @@ Key `.env` settings:
|
|
|
1567
1590
|
|
|
1568
1591
|
```bash
|
|
1569
1592
|
# Authentication
|
|
1570
|
-
|
|
1593
|
+
TINA4_SECRET=your-jwt-secret # JWT signing (warns + uses a blank, insecure secret if unset)
|
|
1571
1594
|
TINA4_API_KEY=your-api-key # Static bearer token for API auth (API_KEY fallback supported)
|
|
1572
1595
|
TINA4_TOKEN_LIMIT=60 # Token lifetime in minutes (default: 60)
|
|
1573
1596
|
|
|
@@ -1813,7 +1836,7 @@ async def dashboard(request, response):
|
|
|
1813
1836
|
## v3 Features Summary
|
|
1814
1837
|
|
|
1815
1838
|
- **55 built-in features**, zero third-party dependencies
|
|
1816
|
-
- **2,
|
|
1839
|
+
- **2,857 tests** passing across all modules
|
|
1817
1840
|
- **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
|
|
1818
1841
|
- **`tina4python generate`**: model, route, migration, middleware scaffolding
|
|
1819
1842
|
- **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), query caching (`TINA4_DB_CACHE=true`, `cache_stats()`, `cache_clear()`)
|
|
@@ -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.13.
|
|
11
|
+
__version__ = "3.13.21"
|
|
12
12
|
|
|
13
13
|
# ── Route decorators ──
|
|
14
14
|
from tina4_python.core.router import ( # noqa: E402, F401
|
|
@@ -34,7 +34,7 @@ from tina4_python.database import Database # noqa: E402, F401
|
|
|
34
34
|
|
|
35
35
|
# ── ORM ──
|
|
36
36
|
from tina4_python.orm import ( # noqa: E402, F401
|
|
37
|
-
ORM,
|
|
37
|
+
ORM, bind_database, Field,
|
|
38
38
|
IntegerField, StringField, BooleanField, FloatField,
|
|
39
39
|
DateTimeField, TextField, BlobField, NumericField,
|
|
40
40
|
ForeignKeyField,
|
|
@@ -29,7 +29,7 @@ import functools
|
|
|
29
29
|
class _DualMethod:
|
|
30
30
|
"""Descriptor that makes a method callable on both the class and an instance.
|
|
31
31
|
|
|
32
|
-
- ``Auth.get_token(payload)`` — creates a temporary Auth() using env
|
|
32
|
+
- ``Auth.get_token(payload)`` — creates a temporary Auth() using env TINA4_SECRET
|
|
33
33
|
- ``auth.get_token(payload)`` — uses the instance's own secret / settings
|
|
34
34
|
"""
|
|
35
35
|
|
|
@@ -42,13 +42,35 @@ class _DualMethod:
|
|
|
42
42
|
# Called on the class: Auth.get_token(payload, ...)
|
|
43
43
|
@functools.wraps(self._func)
|
|
44
44
|
def _class_call(*args, **kwargs):
|
|
45
|
-
inst = cls() # Auth() reads
|
|
45
|
+
inst = cls() # Auth() reads TINA4_SECRET from env
|
|
46
46
|
return self._func(inst, *args, **kwargs)
|
|
47
47
|
return _class_call
|
|
48
48
|
# Called on an instance: auth_instance.get_token(payload, ...)
|
|
49
49
|
return functools.partial(self._func, obj)
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def _resolve_secret(secret: str = None) -> str:
|
|
53
|
+
"""Resolve the JWT signing secret.
|
|
54
|
+
|
|
55
|
+
Reads ``TINA4_SECRET`` from the environment. When neither an explicit
|
|
56
|
+
secret nor ``TINA4_SECRET`` is set, warns loudly and returns a blank
|
|
57
|
+
secret — parity with the PHP/Node frameworks. Tina4 never silently signs
|
|
58
|
+
with a guessable built-in default, which would make tokens forgeable.
|
|
59
|
+
"""
|
|
60
|
+
if secret:
|
|
61
|
+
return secret
|
|
62
|
+
env_secret = os.environ.get("TINA4_SECRET", "")
|
|
63
|
+
if not env_secret:
|
|
64
|
+
try:
|
|
65
|
+
from tina4_python.debug import Log
|
|
66
|
+
Log.warning("Auth: TINA4_SECRET not set in .env — using blank secret (insecure)")
|
|
67
|
+
except Exception:
|
|
68
|
+
import sys
|
|
69
|
+
print("Auth: TINA4_SECRET not set in .env — using blank secret (insecure)", file=sys.stderr)
|
|
70
|
+
return ""
|
|
71
|
+
return env_secret
|
|
72
|
+
|
|
73
|
+
|
|
52
74
|
class Auth:
|
|
53
75
|
"""JWT authentication and password hashing — zero dependencies."""
|
|
54
76
|
|
|
@@ -56,11 +78,11 @@ class Auth:
|
|
|
56
78
|
expires_in: int = None):
|
|
57
79
|
"""
|
|
58
80
|
Args:
|
|
59
|
-
secret: Signing secret (falls back to
|
|
81
|
+
secret: Signing secret (falls back to the TINA4_SECRET env var).
|
|
60
82
|
algorithm: JWT algorithm (default HS256).
|
|
61
83
|
expires_in: Token lifetime in seconds (default 3600).
|
|
62
84
|
"""
|
|
63
|
-
self.secret = secret
|
|
85
|
+
self.secret = secret if secret is not None else _resolve_secret()
|
|
64
86
|
self.algorithm = algorithm
|
|
65
87
|
# JWT expiry env var:
|
|
66
88
|
# - TINA4_TOKEN_EXPIRES_IN (preferred — matches docs and the
|
|
@@ -80,7 +102,7 @@ class Auth:
|
|
|
80
102
|
|
|
81
103
|
Args:
|
|
82
104
|
expires_in: Lifetime in minutes (default: self.expires_in).
|
|
83
|
-
secret: Override signing secret (default: self.secret /
|
|
105
|
+
secret: Override signing secret (default: self.secret / TINA4_SECRET env var).
|
|
84
106
|
|
|
85
107
|
Returns: header.payload.signature
|
|
86
108
|
"""
|
|
@@ -176,36 +198,36 @@ class Auth:
|
|
|
176
198
|
|
|
177
199
|
# get_token and valid_token are the primary names
|
|
178
200
|
|
|
179
|
-
# ── Class-level convenience methods (read
|
|
201
|
+
# ── Class-level convenience methods (read TINA4_SECRET from env) ────
|
|
180
202
|
|
|
181
203
|
@classmethod
|
|
182
204
|
def get_token_static(cls, payload: dict, expires_in: int = 60) -> str:
|
|
183
|
-
"""Create a JWT without instantiating Auth — reads
|
|
184
|
-
secret =
|
|
205
|
+
"""Create a JWT without instantiating Auth — reads TINA4_SECRET from env."""
|
|
206
|
+
secret = _resolve_secret()
|
|
185
207
|
auth = cls(secret=secret, expires_in=expires_in)
|
|
186
208
|
return auth.get_token(payload)
|
|
187
209
|
|
|
188
210
|
@classmethod
|
|
189
211
|
def valid_token_static(cls, token: str) -> "dict | None":
|
|
190
|
-
"""Validate a JWT without instantiating Auth — reads
|
|
212
|
+
"""Validate a JWT without instantiating Auth — reads TINA4_SECRET from env.
|
|
191
213
|
|
|
192
214
|
Returns the decoded payload on success, ``None`` on failure.
|
|
193
215
|
"""
|
|
194
|
-
secret =
|
|
216
|
+
secret = _resolve_secret()
|
|
195
217
|
auth = cls(secret=secret)
|
|
196
218
|
return auth.valid_token(token)
|
|
197
219
|
|
|
198
220
|
@classmethod
|
|
199
221
|
def get_payload_static(cls, token: str) -> dict | None:
|
|
200
222
|
"""Decode payload (no validation) without instantiating Auth."""
|
|
201
|
-
secret =
|
|
223
|
+
secret = _resolve_secret()
|
|
202
224
|
auth = cls(secret=secret)
|
|
203
225
|
return auth.get_payload(token)
|
|
204
226
|
|
|
205
227
|
@classmethod
|
|
206
228
|
def refresh_token_static(cls, token: str, expires_in: int = 60) -> str | None:
|
|
207
|
-
"""Refresh a JWT without instantiating Auth — reads
|
|
208
|
-
secret =
|
|
229
|
+
"""Refresh a JWT without instantiating Auth — reads TINA4_SECRET from env."""
|
|
230
|
+
secret = _resolve_secret()
|
|
209
231
|
auth = cls(secret=secret, expires_in=expires_in)
|
|
210
232
|
return auth.refresh_token(token)
|
|
211
233
|
|
|
@@ -213,10 +235,10 @@ class Auth:
|
|
|
213
235
|
def authenticate_request_static(cls, headers: dict) -> dict | None:
|
|
214
236
|
"""Extract and validate auth from request headers without instantiating Auth.
|
|
215
237
|
|
|
216
|
-
Reads
|
|
238
|
+
Reads TINA4_SECRET from env. Checks: Bearer JWT, Bearer API key, Basic auth.
|
|
217
239
|
Returns payload dict on success, None on failure.
|
|
218
240
|
"""
|
|
219
|
-
secret =
|
|
241
|
+
secret = _resolve_secret()
|
|
220
242
|
auth = cls(secret=secret)
|
|
221
243
|
return auth.authenticate_request(headers)
|
|
222
244
|
|
|
@@ -281,7 +303,7 @@ class Auth:
|
|
|
281
303
|
"""Extract and validate auth from request headers.
|
|
282
304
|
|
|
283
305
|
Args:
|
|
284
|
-
secret: Override signing secret (default: self.secret /
|
|
306
|
+
secret: Override signing secret (default: self.secret / TINA4_SECRET env var).
|
|
285
307
|
algorithm: JWT algorithm override (default: "HS256").
|
|
286
308
|
|
|
287
309
|
Checks: Bearer JWT, Bearer API key, Basic auth.
|
|
@@ -324,14 +346,14 @@ def _b64url_decode(s: str) -> bytes:
|
|
|
324
346
|
# ── Module-level convenience functions (use static methods) ────
|
|
325
347
|
|
|
326
348
|
def get_token(payload: dict, expires_in: int = 60, secret: str = None) -> str:
|
|
327
|
-
"""Create a JWT — reads
|
|
349
|
+
"""Create a JWT — reads TINA4_SECRET from env. Shortcut for Auth.get_token_static()."""
|
|
328
350
|
if secret is not None:
|
|
329
351
|
return Auth(secret=secret).get_token(payload, expires_in=expires_in)
|
|
330
352
|
return Auth.get_token_static(payload, expires_in=expires_in)
|
|
331
353
|
|
|
332
354
|
|
|
333
355
|
def valid_token(token: str) -> "dict | None":
|
|
334
|
-
"""Validate a JWT signature and expiry — reads
|
|
356
|
+
"""Validate a JWT signature and expiry — reads TINA4_SECRET from env.
|
|
335
357
|
|
|
336
358
|
Returns the decoded payload dict on success, ``None`` on failure.
|
|
337
359
|
The truthy/falsy split keeps legacy ``if valid_token(t):`` code working
|
|
@@ -346,12 +368,12 @@ def get_payload(token: str) -> dict | None:
|
|
|
346
368
|
|
|
347
369
|
|
|
348
370
|
def refresh_token(token: str, expires_in: int = 60) -> str | None:
|
|
349
|
-
"""Refresh a JWT — reads
|
|
371
|
+
"""Refresh a JWT — reads TINA4_SECRET from env. Shortcut for Auth.refresh_token_static()."""
|
|
350
372
|
return Auth.refresh_token_static(token, expires_in=expires_in)
|
|
351
373
|
|
|
352
374
|
|
|
353
375
|
def authenticate_request(headers: dict, secret: str = None, algorithm: str = "HS256") -> dict | None:
|
|
354
|
-
"""Validate auth from request headers — reads
|
|
376
|
+
"""Validate auth from request headers — reads TINA4_SECRET from env."""
|
|
355
377
|
if secret is not None:
|
|
356
378
|
return Auth(secret=secret).authenticate_request(headers, algorithm=algorithm)
|
|
357
379
|
return Auth.authenticate_request_static(headers)
|
|
@@ -286,8 +286,8 @@ class CsrfMiddleware:
|
|
|
286
286
|
if auth_header.startswith("Bearer "):
|
|
287
287
|
bearer_token = auth_header[7:].strip()
|
|
288
288
|
if bearer_token:
|
|
289
|
-
from tina4_python.auth import Auth as _CsrfAuth
|
|
290
|
-
secret =
|
|
289
|
+
from tina4_python.auth import Auth as _CsrfAuth, _resolve_secret
|
|
290
|
+
secret = _resolve_secret()
|
|
291
291
|
auth = _CsrfAuth(secret=secret)
|
|
292
292
|
if auth.valid_token(bearer_token):
|
|
293
293
|
return request, response
|
|
@@ -325,8 +325,8 @@ class CsrfMiddleware:
|
|
|
325
325
|
)
|
|
326
326
|
|
|
327
327
|
# Validate the token
|
|
328
|
-
from tina4_python.auth import Auth as _CsrfAuth
|
|
329
|
-
secret =
|
|
328
|
+
from tina4_python.auth import Auth as _CsrfAuth, _resolve_secret
|
|
329
|
+
secret = _resolve_secret()
|
|
330
330
|
auth = _CsrfAuth(secret=secret)
|
|
331
331
|
if not auth.valid_token(token):
|
|
332
332
|
return request, response.error(
|
|
@@ -70,6 +70,34 @@ def set_frond(engine):
|
|
|
70
70
|
_global_frond = engine
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def _to_jsonable(data):
|
|
74
|
+
"""Normalise domain objects into JSON-serialisable structures.
|
|
75
|
+
|
|
76
|
+
Lets a route hand domain objects straight to ``response(...)`` /
|
|
77
|
+
``response.json(...)`` without calling ``.to_dict()``/``.to_json()`` by hand:
|
|
78
|
+
|
|
79
|
+
return response(user) # ORM model -> dict
|
|
80
|
+
return response(User.all()) # list[ORM] -> list[dict]
|
|
81
|
+
return response(db.fetch(sql)) # DatabaseResult -> list[dict]
|
|
82
|
+
|
|
83
|
+
Plain ``dict`` / ``str`` / ``bytes`` / ``None`` pass through unchanged, so
|
|
84
|
+
existing handlers behave exactly as before.
|
|
85
|
+
"""
|
|
86
|
+
if data is None or isinstance(data, (dict, str, bytes)):
|
|
87
|
+
return data
|
|
88
|
+
# Query result (DatabaseResult): exposes both a ``records`` list and a
|
|
89
|
+
# ``to_array`` method — the pair distinguishes it from a plain {"records": …} dict.
|
|
90
|
+
if isinstance(getattr(data, "records", None), list) and callable(getattr(data, "to_array", None)):
|
|
91
|
+
return data.records
|
|
92
|
+
# ORM model: duck-typed on a callable ``to_dict``.
|
|
93
|
+
if callable(getattr(data, "to_dict", None)):
|
|
94
|
+
return data.to_dict()
|
|
95
|
+
# Collections: normalise each element (list of models -> list of dicts).
|
|
96
|
+
if isinstance(data, (list, tuple)):
|
|
97
|
+
return [_to_jsonable(item) for item in data]
|
|
98
|
+
return data
|
|
99
|
+
|
|
100
|
+
|
|
73
101
|
class Response:
|
|
74
102
|
"""HTTP response builder with compression and ETag support."""
|
|
75
103
|
|
|
@@ -107,6 +135,10 @@ class Response:
|
|
|
107
135
|
for k, v in headers.items():
|
|
108
136
|
self._headers.append((k, v))
|
|
109
137
|
|
|
138
|
+
# Normalise ORM models / collections / query results so handlers can
|
|
139
|
+
# `return response(model)` without serialising by hand.
|
|
140
|
+
data = _to_jsonable(data)
|
|
141
|
+
|
|
110
142
|
if content_type:
|
|
111
143
|
# Explicit content type provided
|
|
112
144
|
self.content_type = content_type
|
|
@@ -232,7 +264,7 @@ class Response:
|
|
|
232
264
|
if status_code:
|
|
233
265
|
self.status_code = status_code
|
|
234
266
|
self.content_type = "application/json"
|
|
235
|
-
self.content = json.dumps(data, default=str, separators=(",", ":")).encode()
|
|
267
|
+
self.content = json.dumps(_to_jsonable(data), default=str, separators=(",", ":")).encode()
|
|
236
268
|
return self
|
|
237
269
|
|
|
238
270
|
def html(self, content: str, status_code: int = None) -> "Response":
|
|
@@ -15,7 +15,7 @@ from functools import lru_cache
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from datetime import datetime
|
|
17
17
|
|
|
18
|
-
from tina4_python.auth import Auth as _FrondAuth
|
|
18
|
+
from tina4_python.auth import Auth as _FrondAuth, _resolve_secret
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class SafeString(str):
|
|
@@ -1173,7 +1173,7 @@ def _generate_form_jwt(descriptor: str = "", session_id: str = "") -> str:
|
|
|
1173
1173
|
if sid:
|
|
1174
1174
|
payload["session_id"] = sid
|
|
1175
1175
|
|
|
1176
|
-
secret =
|
|
1176
|
+
secret = _resolve_secret()
|
|
1177
1177
|
ttl = int(os.environ.get("TINA4_TOKEN_EXPIRES_IN", "60"))
|
|
1178
1178
|
auth = _FrondAuth(secret=secret, expires_in=ttl)
|
|
1179
1179
|
return auth.get_token(payload)
|
|
@@ -24,10 +24,10 @@ from tina4_python.orm.fields import (
|
|
|
24
24
|
IntField, StrField, BoolField, # short aliases
|
|
25
25
|
has_many, has_one, belongs_to, # relationship descriptors
|
|
26
26
|
)
|
|
27
|
-
from tina4_python.orm.model import ORM,
|
|
27
|
+
from tina4_python.orm.model import ORM, bind_database
|
|
28
28
|
|
|
29
29
|
__all__ = [
|
|
30
|
-
"ORM", "
|
|
30
|
+
"ORM", "bind_database",
|
|
31
31
|
"Field",
|
|
32
32
|
# Verbose (preferred)
|
|
33
33
|
"IntegerField", "StringField", "BooleanField", "FloatField",
|
|
@@ -17,13 +17,13 @@ from tina4_python.core.cache import Cache
|
|
|
17
17
|
# Module-level query cache — shared across all ORM models
|
|
18
18
|
_query_cache = Cache(default_ttl=0, max_size=500)
|
|
19
19
|
|
|
20
|
-
# Global database reference — set via
|
|
20
|
+
# Global database reference — set via bind_database()
|
|
21
21
|
_database = None
|
|
22
22
|
# Named database connections registry
|
|
23
23
|
_databases: dict[str, object] = {}
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def bind_database(db, name: str = None):
|
|
27
27
|
"""Bind a Database instance to ORM models.
|
|
28
28
|
|
|
29
29
|
Args:
|
|
@@ -32,11 +32,11 @@ def orm_bind(db, name: str = None):
|
|
|
32
32
|
If None, sets the global default used by all models without _db.
|
|
33
33
|
|
|
34
34
|
Usage:
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
bind_database(db_main) # default for all models
|
|
36
|
+
bind_database(db_audit, name="audit") # named connection
|
|
37
37
|
|
|
38
38
|
# Decorator style — class is returned unchanged:
|
|
39
|
-
@
|
|
39
|
+
@bind_database(db)
|
|
40
40
|
class User(ORM):
|
|
41
41
|
...
|
|
42
42
|
|
|
@@ -49,7 +49,7 @@ def orm_bind(db, name: str = None):
|
|
|
49
49
|
else:
|
|
50
50
|
_databases[name] = db
|
|
51
51
|
|
|
52
|
-
# Return a pass-through decorator so @
|
|
52
|
+
# Return a pass-through decorator so @bind_database(db) syntax works.
|
|
53
53
|
# Without this the decorator would set the class to None.
|
|
54
54
|
def _decorator(cls):
|
|
55
55
|
return cls
|
|
@@ -191,6 +191,16 @@ class ORM(metaclass=ORMMeta):
|
|
|
191
191
|
import json
|
|
192
192
|
data = json.loads(data)
|
|
193
193
|
|
|
194
|
+
# A single model is one record — reject a list/array with a clear
|
|
195
|
+
# message instead of a cryptic "'list' object has no attribute 'items'".
|
|
196
|
+
if data is not None and not isinstance(data, dict):
|
|
197
|
+
raise TypeError(
|
|
198
|
+
f"{type(self).__name__}() expects a dict, a JSON object string, or "
|
|
199
|
+
f"keyword args for one record — got {type(data).__name__}. To build "
|
|
200
|
+
f"many records, map over the list: "
|
|
201
|
+
f"[{type(self).__name__}(row) for row in rows]."
|
|
202
|
+
)
|
|
203
|
+
|
|
194
204
|
# Populate from dict or kwargs
|
|
195
205
|
if data:
|
|
196
206
|
self._populate(data)
|
|
@@ -269,7 +279,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
269
279
|
Resolution order:
|
|
270
280
|
1. cls._db as a Database instance (direct assignment)
|
|
271
281
|
2. cls._db as a string name → look up in _databases registry
|
|
272
|
-
3. Global _database (set via
|
|
282
|
+
3. Global _database (set via bind_database(db))
|
|
273
283
|
"""
|
|
274
284
|
if cls._db is not None:
|
|
275
285
|
if isinstance(cls._db, str):
|
|
@@ -277,7 +287,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
277
287
|
if db is None:
|
|
278
288
|
raise RuntimeError(
|
|
279
289
|
f"Named database '{cls._db}' not found. "
|
|
280
|
-
f"Call
|
|
290
|
+
f"Call bind_database(db, name='{cls._db}') first."
|
|
281
291
|
)
|
|
282
292
|
return db
|
|
283
293
|
return cls._db # Direct Database instance
|
|
@@ -291,10 +301,10 @@ class ORM(metaclass=ORMMeta):
|
|
|
291
301
|
username = os.environ.get("TINA4_DATABASE_USERNAME", "")
|
|
292
302
|
password = os.environ.get("TINA4_DATABASE_PASSWORD", "")
|
|
293
303
|
db = Database(url, username, password)
|
|
294
|
-
|
|
304
|
+
bind_database(db)
|
|
295
305
|
return db
|
|
296
306
|
raise RuntimeError(
|
|
297
|
-
"No database bound. Call
|
|
307
|
+
"No database bound. Call bind_database(db) or set TINA4_DATABASE_URL in .env"
|
|
298
308
|
)
|
|
299
309
|
return _database
|
|
300
310
|
|
|
@@ -890,7 +900,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
890
900
|
Resolution order matches _get_db():
|
|
891
901
|
1. cls._db as a Database instance
|
|
892
902
|
2. cls._db as a named string → registry lookup
|
|
893
|
-
3. Global default set via
|
|
903
|
+
3. Global default set via bind_database()
|
|
894
904
|
"""
|
|
895
905
|
return cls._get_db()
|
|
896
906
|
|
|
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
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/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
|
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/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.13.16 → tina4_python-3.13.21}/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
|
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/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
|
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.21}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|