tina4-python 3.13.15__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.15 → tina4_python-3.13.19}/PKG-INFO +1 -1
- {tina4_python-3.13.15 → tina4_python-3.13.19}/pyproject.toml +1 -1
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/CLAUDE.md +65 -35
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/__init__.py +2 -2
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/response.py +33 -1
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/adapter.py +10 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/postgres.py +6 -1
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/orm/__init__.py +2 -2
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/orm/model.py +53 -15
- {tina4_python-3.13.15 → tina4_python-3.13.19}/.gitignore +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/README.md +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/env.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.15 → 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}")
|
|
@@ -282,8 +282,9 @@ Queue(topic="tasks").push({"action": "send_email"})
|
|
|
282
282
|
- **Macro imports**: `{% from "macros/forms.twig" import field_group %}` — macros do NOT inherit parent context, pass variables explicitly
|
|
283
283
|
4. **DatabaseResult**: `dba.fetch()` returns a `DatabaseResult` object, NOT a plain list
|
|
284
284
|
- Use `.records` to get list of dicts: `result.records`
|
|
285
|
-
- Use `.count` for row count
|
|
285
|
+
- Use `.count` for total row count; `len(result)` also works
|
|
286
286
|
- Iteration works: `for row in result` or `list(result)`
|
|
287
|
+
- Index/slice access works: `result[0]`, `result[1:3]`
|
|
287
288
|
- Other methods: `.to_json()`, `.to_array()`, `.to_csv()`, `.to_paginate()`
|
|
288
289
|
4. **fetch_one()**: Returns a plain dict (or None), NOT a DatabaseResult
|
|
289
290
|
5. **Dict access**: All query results use dict access `row["column"]` not attribute access `row.column`
|
|
@@ -327,7 +328,7 @@ tina4_python/ # Core framework package (v3.0.0)
|
|
|
327
328
|
│ ├── connection.py # Database class (URL-based connection)
|
|
328
329
|
│ ├── adapter.py # DatabaseAdapter, DatabaseResult, SQLTranslator
|
|
329
330
|
│ ├── sqlite.py, postgres.py, mysql.py, mssql.py, firebird.py, odbc.py
|
|
330
|
-
├── orm/ # Active Record ORM (ORM, Field,
|
|
331
|
+
├── orm/ # Active Record ORM (ORM, Field, bind_database)
|
|
331
332
|
│ ├── model.py # ORM base class
|
|
332
333
|
│ └── fields.py # IntegerField, StringField, etc.
|
|
333
334
|
├── frond/ # Template engine (Frond — Jinja2/Twig-compatible)
|
|
@@ -356,7 +357,7 @@ tina4_python/ # Core framework package (v3.0.0)
|
|
|
356
357
|
├── dotenv/ # .env file loader
|
|
357
358
|
├── cli/ # CLI commands
|
|
358
359
|
├── HtmlElement.py # Programmatic HTML builder (HTMLElement, add_html_helpers)
|
|
359
|
-
├── Testing.py # Inline testing framework (tests, assert_equal,
|
|
360
|
+
├── Testing.py # Inline testing framework (tests, assert_equal, run_all)
|
|
360
361
|
├── templates/ # Built-in framework templates (Twig)
|
|
361
362
|
├── public/ # Built-in static assets
|
|
362
363
|
└── scss/ # Built-in SCSS
|
|
@@ -478,6 +479,9 @@ request.session # Session object — use .get(key) and .set(key, value)
|
|
|
478
479
|
|
|
479
480
|
```python
|
|
480
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
|
|
481
485
|
return response("<h1>Hello</h1>") # HTML
|
|
482
486
|
return response("Not found", 404) # With status code
|
|
483
487
|
return response.redirect("/login") # Redirect
|
|
@@ -486,6 +490,8 @@ return response.file("doc.pdf") # Serve a file
|
|
|
486
490
|
return response.stream(generator) # SSE/streaming response (text/event-stream)
|
|
487
491
|
```
|
|
488
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
|
+
|
|
489
495
|
Add custom headers before returning:
|
|
490
496
|
```python
|
|
491
497
|
from tina4_python.core.response import Response
|
|
@@ -820,19 +826,37 @@ class User(ORM):
|
|
|
820
826
|
email = StringField()
|
|
821
827
|
```
|
|
822
828
|
|
|
823
|
-
|
|
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
|
+
|
|
824
835
|
```python
|
|
825
|
-
from tina4_python.orm import
|
|
836
|
+
from tina4_python.orm import bind_database
|
|
826
837
|
from tina4_python.database import Database
|
|
827
838
|
|
|
828
|
-
|
|
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
|
|
829
849
|
```
|
|
830
850
|
|
|
831
851
|
### ORM operations
|
|
832
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
|
+
|
|
833
855
|
```python
|
|
834
856
|
# Create
|
|
835
|
-
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
|
|
836
860
|
user.save()
|
|
837
861
|
|
|
838
862
|
# Load — alias for select_one (class method, returns instance or None)
|
|
@@ -853,17 +877,17 @@ user.save()
|
|
|
853
877
|
user.delete()
|
|
854
878
|
|
|
855
879
|
# Query
|
|
856
|
-
result = User().select(
|
|
880
|
+
result = User().select(sql="name = ?", params=["Alice"], limit=10)
|
|
857
881
|
|
|
858
882
|
# Convert to dict
|
|
859
883
|
user.to_dict()
|
|
860
884
|
|
|
861
|
-
# NoSQL support: to_mongo() generates MongoDB query documents from the same fluent API
|
|
862
|
-
|
|
885
|
+
# NoSQL support: to_mongo() lives on QueryBuilder and generates MongoDB query documents from the same fluent API
|
|
886
|
+
User.query().where("name = ?", ["Alice"]).to_mongo()
|
|
863
887
|
```
|
|
864
888
|
|
|
865
889
|
### Available field types
|
|
866
|
-
`IntegerField`, `StringField`, `TextField`, `DateTimeField`, `NumericField`, `BlobField`, `
|
|
890
|
+
`IntegerField`, `StringField`, `TextField`, `DateTimeField`, `NumericField`, `BlobField`, `BooleanField`, `FloatField`, `ForeignKeyField`
|
|
867
891
|
|
|
868
892
|
Import from `tina4_python` or `tina4_python.orm.fields`.
|
|
869
893
|
|
|
@@ -1187,7 +1211,8 @@ from tina4_python.graphql import GraphQL
|
|
|
1187
1211
|
gql = GraphQL()
|
|
1188
1212
|
gql.schema.from_orm(User)
|
|
1189
1213
|
gql.schema.from_orm(Product)
|
|
1190
|
-
gql.
|
|
1214
|
+
# The engine serves HTTP at gql.endpoint (default "/graphql", set via TINA4_GRAPHQL_ENDPOINT).
|
|
1215
|
+
# To wire it manually, call gql.execute_json(...) from a @post route.
|
|
1191
1216
|
```
|
|
1192
1217
|
|
|
1193
1218
|
`from_orm()` creates: type, single query (`user(id: ID)`), list query (`users(limit, offset)`), create/update/delete mutations.
|
|
@@ -1196,16 +1221,18 @@ gql.register_route("/graphql") # POST = queries, GET = GraphiQL IDE
|
|
|
1196
1221
|
|
|
1197
1222
|
```python
|
|
1198
1223
|
gql.schema.add_type("Widget", {"id": "ID", "name": "String", "price": "Float"})
|
|
1199
|
-
gql.schema.add_query(
|
|
1200
|
-
"
|
|
1201
|
-
|
|
1202
|
-
"
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
"
|
|
1207
|
-
"
|
|
1208
|
-
|
|
1224
|
+
gql.schema.add_query(
|
|
1225
|
+
"widget",
|
|
1226
|
+
args={"id": "ID"},
|
|
1227
|
+
return_type="Widget",
|
|
1228
|
+
resolver=lambda root, args, ctx: {"id": args["id"], "name": "Cog", "price": 5.0},
|
|
1229
|
+
)
|
|
1230
|
+
gql.schema.add_mutation(
|
|
1231
|
+
"deleteWidget",
|
|
1232
|
+
args={"id": "ID!"},
|
|
1233
|
+
return_type="Boolean",
|
|
1234
|
+
resolver=lambda root, args, ctx: True,
|
|
1235
|
+
)
|
|
1209
1236
|
```
|
|
1210
1237
|
|
|
1211
1238
|
### Programmatic usage (no HTTP)
|
|
@@ -1322,8 +1349,8 @@ uv run tina4python test # Discovers @tests in src/**/*.py
|
|
|
1322
1349
|
|
|
1323
1350
|
Or programmatically:
|
|
1324
1351
|
```python
|
|
1325
|
-
from tina4_python.Testing import
|
|
1326
|
-
|
|
1352
|
+
from tina4_python.Testing import run_all
|
|
1353
|
+
run_all(quiet=False, failfast=False)
|
|
1327
1354
|
```
|
|
1328
1355
|
|
|
1329
1356
|
## Events — Decoupled Communication
|
|
@@ -1374,7 +1401,7 @@ clear() # remove all listeners for all events
|
|
|
1374
1401
|
Detect AI coding tools in a project and install Tina4-aware context files so any assistant understands the framework.
|
|
1375
1402
|
|
|
1376
1403
|
```python
|
|
1377
|
-
from tina4_python.ai import detect_ai, detect_ai_names, install_context,
|
|
1404
|
+
from tina4_python.ai import detect_ai, detect_ai_names, install_context, install_selected, status_report
|
|
1378
1405
|
|
|
1379
1406
|
# Detect which AI tools are present
|
|
1380
1407
|
tools = detect_ai(".")
|
|
@@ -1383,12 +1410,15 @@ tools = detect_ai(".")
|
|
|
1383
1410
|
# Just the names of detected tools
|
|
1384
1411
|
names = detect_ai_names(".") # ["claude-code", "cursor"]
|
|
1385
1412
|
|
|
1386
|
-
# Install context
|
|
1387
|
-
created = install_context("."
|
|
1413
|
+
# Install context for ALL known AI tools (tools=None default)
|
|
1414
|
+
created = install_context(".")
|
|
1388
1415
|
# ["CLAUDE.md", ".cursorules"]
|
|
1389
1416
|
|
|
1390
|
-
# Install context for
|
|
1391
|
-
created =
|
|
1417
|
+
# Install context for specific tools only
|
|
1418
|
+
created = install_context(".", tools=["claude-code", "cursor"])
|
|
1419
|
+
|
|
1420
|
+
# Or select by CSV/"all" string
|
|
1421
|
+
created = install_selected(".", "all")
|
|
1392
1422
|
|
|
1393
1423
|
# Human-readable status report
|
|
1394
1424
|
print(status_report("."))
|
|
@@ -1511,7 +1541,7 @@ HTMLElement("img", {"src": "/logo.png"}) # <img src="/logo.png">
|
|
|
1511
1541
|
Attach test assertions directly to functions with the `@tests` decorator. Tests are registered globally and run via CLI or programmatically.
|
|
1512
1542
|
|
|
1513
1543
|
```python
|
|
1514
|
-
from tina4_python.Testing import tests, assert_equal, assert_raises, assert_true, assert_false,
|
|
1544
|
+
from tina4_python.Testing import tests, assert_equal, assert_raises, assert_true, assert_false, run_all
|
|
1515
1545
|
|
|
1516
1546
|
@tests(
|
|
1517
1547
|
assert_equal((5, 3), 8), # add(5, 3) == 8
|
|
@@ -1534,7 +1564,7 @@ uv run tina4python test # Discovers @tests in src/**/*.py
|
|
|
1534
1564
|
|
|
1535
1565
|
```python
|
|
1536
1566
|
# Programmatic execution
|
|
1537
|
-
results =
|
|
1567
|
+
results = run_all(quiet=False, failfast=False)
|
|
1538
1568
|
# {"passed": 5, "failed": 0, "errors": 0, "details": [...]}
|
|
1539
1569
|
```
|
|
1540
1570
|
|
|
@@ -1769,7 +1799,7 @@ async def invite(request, response):
|
|
|
1769
1799
|
queue = Queue(topic="emails")
|
|
1770
1800
|
for job in queue.consume("emails"):
|
|
1771
1801
|
email = job.data
|
|
1772
|
-
html =
|
|
1802
|
+
html = Frond().render(f"emails/{email['template']}.twig", email["data"])
|
|
1773
1803
|
# ... send via SMTP
|
|
1774
1804
|
job.complete()
|
|
1775
1805
|
```
|
|
@@ -1806,7 +1836,7 @@ async def dashboard(request, response):
|
|
|
1806
1836
|
## v3 Features Summary
|
|
1807
1837
|
|
|
1808
1838
|
- **55 built-in features**, zero third-party dependencies
|
|
1809
|
-
- **2,
|
|
1839
|
+
- **2,852 tests** passing across all modules
|
|
1810
1840
|
- **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
|
|
1811
1841
|
- **`tina4python generate`**: model, route, migration, middleware scaffolding
|
|
1812
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":
|
|
@@ -27,6 +27,16 @@ class DatabaseResult:
|
|
|
27
27
|
def __len__(self):
|
|
28
28
|
return self.count
|
|
29
29
|
|
|
30
|
+
def __getitem__(self, index):
|
|
31
|
+
"""Index / slice access into the result rows.
|
|
32
|
+
|
|
33
|
+
v3.13.16: ``result[0]`` (and slicing) are documented (book ch5 §4
|
|
34
|
+
"Index Access") but used to raise ``TypeError: 'DatabaseResult'
|
|
35
|
+
object is not subscriptable``. Delegate to the materialised rows so
|
|
36
|
+
index and slice access behave like a list.
|
|
37
|
+
"""
|
|
38
|
+
return self.records[index]
|
|
39
|
+
|
|
30
40
|
def __bool__(self):
|
|
31
41
|
return self.error is None
|
|
32
42
|
|
|
@@ -535,7 +535,12 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
535
535
|
"""
|
|
536
536
|
sql = SQLTranslator.placeholder_style(sql, "%s")
|
|
537
537
|
sql = SQLTranslator.auto_increment_syntax(sql, "postgresql")
|
|
538
|
-
|
|
538
|
+
# v3.13.16: do NOT run boolean_to_int on PostgreSQL — PG has a native
|
|
539
|
+
# BOOLEAN type, so TRUE/FALSE are valid literals and 1/0 are NOT
|
|
540
|
+
# interchangeable with them. Converting (e.g. `DEFAULT FALSE` -> `DEFAULT 0`,
|
|
541
|
+
# or `WHERE active = TRUE` -> `= 1`) raises "column is of type boolean but
|
|
542
|
+
# expression is of type integer" / "operator does not exist: boolean = integer".
|
|
543
|
+
# boolean_to_int stays for INTEGER/BIT-backed engines (SQLite, Firebird, MSSQL).
|
|
539
544
|
return sql
|
|
540
545
|
|
|
541
546
|
def _supports_returning(self) -> bool:
|
|
@@ -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
|
|
|
@@ -748,7 +758,10 @@ class ORM(metaclass=ORMMeta):
|
|
|
748
758
|
# where it's reliable. SQLite and Firebird stay on INTEGER —
|
|
749
759
|
# SQLite has no native bool, and Firebird's driver round-trip
|
|
750
760
|
# for native BOOLEAN is uneven across versions.
|
|
751
|
-
|
|
761
|
+
# v3.13.16: db.get_database_type() returns "postgresql" (with the -ql),
|
|
762
|
+
# so the old `== "postgres"` check never matched and BooleanField got
|
|
763
|
+
# INTEGER on PG — which then can't accept a Python bool on insert.
|
|
764
|
+
if engine in ("postgres", "postgresql"):
|
|
752
765
|
bool_sql = "BOOLEAN"
|
|
753
766
|
elif engine == "mysql":
|
|
754
767
|
bool_sql = "BOOLEAN" # MySQL alias for TINYINT(1)
|
|
@@ -758,6 +771,16 @@ class ORM(metaclass=ORMMeta):
|
|
|
758
771
|
# sqlite, firebird, odbc, anything else
|
|
759
772
|
bool_sql = "INTEGER"
|
|
760
773
|
|
|
774
|
+
# v3.13.16: DateTimeField was emitted as "DATETIME" unconditionally,
|
|
775
|
+
# but PostgreSQL and Firebird have no DATETIME type — CREATE TABLE blew
|
|
776
|
+
# up with `type "datetime" does not exist`. Emit each engine's real
|
|
777
|
+
# timestamp type. (MySQL/MSSQL/SQLite keep DATETIME: it's valid there,
|
|
778
|
+
# and on MySQL it avoids TIMESTAMP's auto-update + 2038 surprises.)
|
|
779
|
+
if engine in ("postgres", "postgresql", "firebird"):
|
|
780
|
+
datetime_sql = "TIMESTAMP"
|
|
781
|
+
else:
|
|
782
|
+
datetime_sql = "DATETIME"
|
|
783
|
+
|
|
761
784
|
# Don't recreate if table already exists
|
|
762
785
|
if db.table_exists(table):
|
|
763
786
|
return True
|
|
@@ -781,7 +804,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
781
804
|
elif kind == "BooleanField":
|
|
782
805
|
sql_type = bool_sql
|
|
783
806
|
elif kind == "DateTimeField":
|
|
784
|
-
sql_type =
|
|
807
|
+
sql_type = datetime_sql
|
|
785
808
|
elif kind == "BlobField":
|
|
786
809
|
sql_type = "BLOB"
|
|
787
810
|
else:
|
|
@@ -809,7 +832,14 @@ class ORM(metaclass=ORMMeta):
|
|
|
809
832
|
if isinstance(default_val, str):
|
|
810
833
|
parts.append(f"DEFAULT '{default_val}'")
|
|
811
834
|
elif isinstance(default_val, bool):
|
|
812
|
-
|
|
835
|
+
# v3.13.16: a native BOOLEAN column (PG/MySQL) needs
|
|
836
|
+
# TRUE/FALSE; INTEGER- and BIT-backed bools (SQLite,
|
|
837
|
+
# Firebird, MSSQL) need 1/0. `DEFAULT 0` on a PG BOOLEAN
|
|
838
|
+
# raises "default expression is of type integer".
|
|
839
|
+
if bool_sql == "BOOLEAN":
|
|
840
|
+
parts.append(f"DEFAULT {'TRUE' if default_val else 'FALSE'}")
|
|
841
|
+
else:
|
|
842
|
+
parts.append(f"DEFAULT {1 if default_val else 0}")
|
|
813
843
|
else:
|
|
814
844
|
parts.append(f"DEFAULT {default_val}")
|
|
815
845
|
|
|
@@ -821,8 +851,16 @@ class ORM(metaclass=ORMMeta):
|
|
|
821
851
|
engine = db.get_database_type()
|
|
822
852
|
sql = SQLTranslator.auto_increment_syntax(sql, engine)
|
|
823
853
|
|
|
824
|
-
db.execute(
|
|
854
|
+
# v3.13.16: don't claim success when the DDL failed. db.execute()
|
|
855
|
+
# swallows the driver error into get_error() and returns False, so a
|
|
856
|
+
# bad type (or any DDL error) used to leave create_table() returning
|
|
857
|
+
# True while no table was actually created — a silent, misleading pass.
|
|
858
|
+
ok = db.execute(sql)
|
|
825
859
|
db.commit()
|
|
860
|
+
if ok is False:
|
|
861
|
+
from tina4_python.debug import Log
|
|
862
|
+
Log.error(f"create_table failed for {table}: {db.get_error()}", sql=sql)
|
|
863
|
+
return False
|
|
826
864
|
return True
|
|
827
865
|
|
|
828
866
|
# ── Cached Queries ────────────────────────────────────────
|
|
@@ -862,7 +900,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
862
900
|
Resolution order matches _get_db():
|
|
863
901
|
1. cls._db as a Database instance
|
|
864
902
|
2. cls._db as a named string → registry lookup
|
|
865
|
-
3. Global default set via
|
|
903
|
+
3. Global default set via bind_database()
|
|
866
904
|
"""
|
|
867
905
|
return cls._get_db()
|
|
868
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
|
{tina4_python-3.13.15 → 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.15 → 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.15 → 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.15 → tina4_python-3.13.19}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → 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.15 → tina4_python-3.13.19}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → 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.15 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → tina4_python-3.13.19}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.15 → 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
|