tina4-python 3.13.16__tar.gz → 3.13.19__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.19}/PKG-INFO +1 -1
- {tina4_python-3.13.16 → tina4_python-3.13.19}/pyproject.toml +1 -1
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/CLAUDE.md +31 -8
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/__init__.py +2 -2
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/response.py +33 -1
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/orm/__init__.py +2 -2
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/orm/model.py +21 -11
- {tina4_python-3.13.16 → tina4_python-3.13.19}/.gitignore +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/README.md +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/env.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.16 → tina4_python-3.13.19}/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)
|
|
@@ -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,852 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.19"
|
|
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,
|
|
@@ -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":
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/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.19}/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.19}/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.19}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/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.19}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/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.19}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.16 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|