tina4-python 3.13.14__tar.gz → 3.13.16__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.14 → tina4_python-3.13.16}/PKG-INFO +1 -1
- {tina4_python-3.13.14 → tina4_python-3.13.16}/pyproject.toml +1 -1
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/CLAUDE.md +34 -27
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/adapter.py +10 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/postgres.py +46 -2
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/orm/model.py +32 -4
- {tina4_python-3.13.14 → tina4_python-3.13.16}/.gitignore +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/README.md +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/env.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -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`
|
|
@@ -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
|
|
@@ -853,17 +854,17 @@ user.save()
|
|
|
853
854
|
user.delete()
|
|
854
855
|
|
|
855
856
|
# Query
|
|
856
|
-
result = User().select(
|
|
857
|
+
result = User().select(sql="name = ?", params=["Alice"], limit=10)
|
|
857
858
|
|
|
858
859
|
# Convert to dict
|
|
859
860
|
user.to_dict()
|
|
860
861
|
|
|
861
|
-
# NoSQL support: to_mongo() generates MongoDB query documents from the same fluent API
|
|
862
|
-
|
|
862
|
+
# NoSQL support: to_mongo() lives on QueryBuilder and generates MongoDB query documents from the same fluent API
|
|
863
|
+
User.query().where("name = ?", ["Alice"]).to_mongo()
|
|
863
864
|
```
|
|
864
865
|
|
|
865
866
|
### Available field types
|
|
866
|
-
`IntegerField`, `StringField`, `TextField`, `DateTimeField`, `NumericField`, `BlobField`, `
|
|
867
|
+
`IntegerField`, `StringField`, `TextField`, `DateTimeField`, `NumericField`, `BlobField`, `BooleanField`, `FloatField`, `ForeignKeyField`
|
|
867
868
|
|
|
868
869
|
Import from `tina4_python` or `tina4_python.orm.fields`.
|
|
869
870
|
|
|
@@ -1187,7 +1188,8 @@ from tina4_python.graphql import GraphQL
|
|
|
1187
1188
|
gql = GraphQL()
|
|
1188
1189
|
gql.schema.from_orm(User)
|
|
1189
1190
|
gql.schema.from_orm(Product)
|
|
1190
|
-
gql.
|
|
1191
|
+
# The engine serves HTTP at gql.endpoint (default "/graphql", set via TINA4_GRAPHQL_ENDPOINT).
|
|
1192
|
+
# To wire it manually, call gql.execute_json(...) from a @post route.
|
|
1191
1193
|
```
|
|
1192
1194
|
|
|
1193
1195
|
`from_orm()` creates: type, single query (`user(id: ID)`), list query (`users(limit, offset)`), create/update/delete mutations.
|
|
@@ -1196,16 +1198,18 @@ gql.register_route("/graphql") # POST = queries, GET = GraphiQL IDE
|
|
|
1196
1198
|
|
|
1197
1199
|
```python
|
|
1198
1200
|
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
|
-
|
|
1201
|
+
gql.schema.add_query(
|
|
1202
|
+
"widget",
|
|
1203
|
+
args={"id": "ID"},
|
|
1204
|
+
return_type="Widget",
|
|
1205
|
+
resolver=lambda root, args, ctx: {"id": args["id"], "name": "Cog", "price": 5.0},
|
|
1206
|
+
)
|
|
1207
|
+
gql.schema.add_mutation(
|
|
1208
|
+
"deleteWidget",
|
|
1209
|
+
args={"id": "ID!"},
|
|
1210
|
+
return_type="Boolean",
|
|
1211
|
+
resolver=lambda root, args, ctx: True,
|
|
1212
|
+
)
|
|
1209
1213
|
```
|
|
1210
1214
|
|
|
1211
1215
|
### Programmatic usage (no HTTP)
|
|
@@ -1322,8 +1326,8 @@ uv run tina4python test # Discovers @tests in src/**/*.py
|
|
|
1322
1326
|
|
|
1323
1327
|
Or programmatically:
|
|
1324
1328
|
```python
|
|
1325
|
-
from tina4_python.Testing import
|
|
1326
|
-
|
|
1329
|
+
from tina4_python.Testing import run_all
|
|
1330
|
+
run_all(quiet=False, failfast=False)
|
|
1327
1331
|
```
|
|
1328
1332
|
|
|
1329
1333
|
## Events — Decoupled Communication
|
|
@@ -1374,7 +1378,7 @@ clear() # remove all listeners for all events
|
|
|
1374
1378
|
Detect AI coding tools in a project and install Tina4-aware context files so any assistant understands the framework.
|
|
1375
1379
|
|
|
1376
1380
|
```python
|
|
1377
|
-
from tina4_python.ai import detect_ai, detect_ai_names, install_context,
|
|
1381
|
+
from tina4_python.ai import detect_ai, detect_ai_names, install_context, install_selected, status_report
|
|
1378
1382
|
|
|
1379
1383
|
# Detect which AI tools are present
|
|
1380
1384
|
tools = detect_ai(".")
|
|
@@ -1383,12 +1387,15 @@ tools = detect_ai(".")
|
|
|
1383
1387
|
# Just the names of detected tools
|
|
1384
1388
|
names = detect_ai_names(".") # ["claude-code", "cursor"]
|
|
1385
1389
|
|
|
1386
|
-
# Install context
|
|
1387
|
-
created = install_context("."
|
|
1390
|
+
# Install context for ALL known AI tools (tools=None default)
|
|
1391
|
+
created = install_context(".")
|
|
1388
1392
|
# ["CLAUDE.md", ".cursorules"]
|
|
1389
1393
|
|
|
1390
|
-
# Install context for
|
|
1391
|
-
created =
|
|
1394
|
+
# Install context for specific tools only
|
|
1395
|
+
created = install_context(".", tools=["claude-code", "cursor"])
|
|
1396
|
+
|
|
1397
|
+
# Or select by CSV/"all" string
|
|
1398
|
+
created = install_selected(".", "all")
|
|
1392
1399
|
|
|
1393
1400
|
# Human-readable status report
|
|
1394
1401
|
print(status_report("."))
|
|
@@ -1511,7 +1518,7 @@ HTMLElement("img", {"src": "/logo.png"}) # <img src="/logo.png">
|
|
|
1511
1518
|
Attach test assertions directly to functions with the `@tests` decorator. Tests are registered globally and run via CLI or programmatically.
|
|
1512
1519
|
|
|
1513
1520
|
```python
|
|
1514
|
-
from tina4_python.Testing import tests, assert_equal, assert_raises, assert_true, assert_false,
|
|
1521
|
+
from tina4_python.Testing import tests, assert_equal, assert_raises, assert_true, assert_false, run_all
|
|
1515
1522
|
|
|
1516
1523
|
@tests(
|
|
1517
1524
|
assert_equal((5, 3), 8), # add(5, 3) == 8
|
|
@@ -1534,7 +1541,7 @@ uv run tina4python test # Discovers @tests in src/**/*.py
|
|
|
1534
1541
|
|
|
1535
1542
|
```python
|
|
1536
1543
|
# Programmatic execution
|
|
1537
|
-
results =
|
|
1544
|
+
results = run_all(quiet=False, failfast=False)
|
|
1538
1545
|
# {"passed": 5, "failed": 0, "errors": 0, "details": [...]}
|
|
1539
1546
|
```
|
|
1540
1547
|
|
|
@@ -1769,7 +1776,7 @@ async def invite(request, response):
|
|
|
1769
1776
|
queue = Queue(topic="emails")
|
|
1770
1777
|
for job in queue.consume("emails"):
|
|
1771
1778
|
email = job.data
|
|
1772
|
-
html =
|
|
1779
|
+
html = Frond().render(f"emails/{email['template']}.twig", email["data"])
|
|
1773
1780
|
# ... send via SMTP
|
|
1774
1781
|
job.complete()
|
|
1775
1782
|
```
|
|
@@ -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
|
|
|
@@ -207,6 +207,35 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
207
207
|
except Exception:
|
|
208
208
|
pass
|
|
209
209
|
|
|
210
|
+
def _end_read_txn(self):
|
|
211
|
+
"""v3.13.15 (issue #51): close the implicit transaction a read
|
|
212
|
+
opens, so the connection doesn't leak as ``idle in transaction``.
|
|
213
|
+
|
|
214
|
+
psycopg2 runs with ``autocommit = False`` (the framework's safe
|
|
215
|
+
default — writes must be committed explicitly). A bare ``SELECT``
|
|
216
|
+
therefore opens a transaction too, and with nothing to commit it
|
|
217
|
+
sits ``idle in transaction`` for the life of the connection,
|
|
218
|
+
holding a pool slot and any locks it touched. Short-lived boot
|
|
219
|
+
connections (the migration runner's ``MAX(batch)`` lookup) leak
|
|
220
|
+
one each, eventually exhausting ``max_connections`` — at which
|
|
221
|
+
point module autodiscovery fails mid-boot and every route 404s
|
|
222
|
+
while ``/health-check`` still passes ("ready but broken").
|
|
223
|
+
|
|
224
|
+
A read has nothing to persist, so a ROLLBACK is the safe close.
|
|
225
|
+
Inside an explicit ``start_transaction()`` the caller owns the
|
|
226
|
+
transaction, so we defer (mirrors the heal / error-path guards).
|
|
227
|
+
Writes go through :meth:`execute`, which is deliberately left
|
|
228
|
+
untouched — with autocommit off they must be committed explicitly,
|
|
229
|
+
so auto-closing them here would silently drop data.
|
|
230
|
+
"""
|
|
231
|
+
if self._in_transaction or self._conn is None:
|
|
232
|
+
return
|
|
233
|
+
try:
|
|
234
|
+
self._conn.rollback()
|
|
235
|
+
except Exception:
|
|
236
|
+
# Never let bookkeeping mask the rows we already fetched.
|
|
237
|
+
pass
|
|
238
|
+
|
|
210
239
|
def connect(self, connection_string: str, username: str = "", password: str = "", **kwargs):
|
|
211
240
|
"""Connect to PostgreSQL.
|
|
212
241
|
|
|
@@ -357,6 +386,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
357
386
|
self._exec_with_handling(cursor, paginated_sql, paginated_params)
|
|
358
387
|
rows = [self._decode_blobs(dict(row)) for row in cursor.fetchall()]
|
|
359
388
|
|
|
389
|
+
# v3.13.15 (#51): rows are materialised above — close the implicit
|
|
390
|
+
# read transaction so the connection doesn't sit idle-in-transaction.
|
|
391
|
+
self._end_read_txn()
|
|
392
|
+
|
|
360
393
|
return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
|
|
361
394
|
|
|
362
395
|
def fetch_one(self, sql: str, params: list = None) -> dict | None:
|
|
@@ -368,7 +401,13 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
368
401
|
cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
|
369
402
|
self._exec_with_handling(cursor, sql, params)
|
|
370
403
|
row = cursor.fetchone()
|
|
371
|
-
|
|
404
|
+
result = self._decode_blobs(dict(row)) if row else None
|
|
405
|
+
# v3.13.15 (#51): close the implicit read transaction so the
|
|
406
|
+
# connection doesn't sit 'idle in transaction' (psycopg2 autocommit
|
|
407
|
+
# is off). This is the path the migration runner's MAX(batch)
|
|
408
|
+
# lookup leaked through.
|
|
409
|
+
self._end_read_txn()
|
|
410
|
+
return result
|
|
372
411
|
|
|
373
412
|
@staticmethod
|
|
374
413
|
def _decode_blobs(row: dict) -> dict:
|
|
@@ -496,7 +535,12 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
496
535
|
"""
|
|
497
536
|
sql = SQLTranslator.placeholder_style(sql, "%s")
|
|
498
537
|
sql = SQLTranslator.auto_increment_syntax(sql, "postgresql")
|
|
499
|
-
|
|
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).
|
|
500
544
|
return sql
|
|
501
545
|
|
|
502
546
|
def _supports_returning(self) -> bool:
|
|
@@ -748,7 +748,10 @@ class ORM(metaclass=ORMMeta):
|
|
|
748
748
|
# where it's reliable. SQLite and Firebird stay on INTEGER —
|
|
749
749
|
# SQLite has no native bool, and Firebird's driver round-trip
|
|
750
750
|
# for native BOOLEAN is uneven across versions.
|
|
751
|
-
|
|
751
|
+
# v3.13.16: db.get_database_type() returns "postgresql" (with the -ql),
|
|
752
|
+
# so the old `== "postgres"` check never matched and BooleanField got
|
|
753
|
+
# INTEGER on PG — which then can't accept a Python bool on insert.
|
|
754
|
+
if engine in ("postgres", "postgresql"):
|
|
752
755
|
bool_sql = "BOOLEAN"
|
|
753
756
|
elif engine == "mysql":
|
|
754
757
|
bool_sql = "BOOLEAN" # MySQL alias for TINYINT(1)
|
|
@@ -758,6 +761,16 @@ class ORM(metaclass=ORMMeta):
|
|
|
758
761
|
# sqlite, firebird, odbc, anything else
|
|
759
762
|
bool_sql = "INTEGER"
|
|
760
763
|
|
|
764
|
+
# v3.13.16: DateTimeField was emitted as "DATETIME" unconditionally,
|
|
765
|
+
# but PostgreSQL and Firebird have no DATETIME type — CREATE TABLE blew
|
|
766
|
+
# up with `type "datetime" does not exist`. Emit each engine's real
|
|
767
|
+
# timestamp type. (MySQL/MSSQL/SQLite keep DATETIME: it's valid there,
|
|
768
|
+
# and on MySQL it avoids TIMESTAMP's auto-update + 2038 surprises.)
|
|
769
|
+
if engine in ("postgres", "postgresql", "firebird"):
|
|
770
|
+
datetime_sql = "TIMESTAMP"
|
|
771
|
+
else:
|
|
772
|
+
datetime_sql = "DATETIME"
|
|
773
|
+
|
|
761
774
|
# Don't recreate if table already exists
|
|
762
775
|
if db.table_exists(table):
|
|
763
776
|
return True
|
|
@@ -781,7 +794,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
781
794
|
elif kind == "BooleanField":
|
|
782
795
|
sql_type = bool_sql
|
|
783
796
|
elif kind == "DateTimeField":
|
|
784
|
-
sql_type =
|
|
797
|
+
sql_type = datetime_sql
|
|
785
798
|
elif kind == "BlobField":
|
|
786
799
|
sql_type = "BLOB"
|
|
787
800
|
else:
|
|
@@ -809,7 +822,14 @@ class ORM(metaclass=ORMMeta):
|
|
|
809
822
|
if isinstance(default_val, str):
|
|
810
823
|
parts.append(f"DEFAULT '{default_val}'")
|
|
811
824
|
elif isinstance(default_val, bool):
|
|
812
|
-
|
|
825
|
+
# v3.13.16: a native BOOLEAN column (PG/MySQL) needs
|
|
826
|
+
# TRUE/FALSE; INTEGER- and BIT-backed bools (SQLite,
|
|
827
|
+
# Firebird, MSSQL) need 1/0. `DEFAULT 0` on a PG BOOLEAN
|
|
828
|
+
# raises "default expression is of type integer".
|
|
829
|
+
if bool_sql == "BOOLEAN":
|
|
830
|
+
parts.append(f"DEFAULT {'TRUE' if default_val else 'FALSE'}")
|
|
831
|
+
else:
|
|
832
|
+
parts.append(f"DEFAULT {1 if default_val else 0}")
|
|
813
833
|
else:
|
|
814
834
|
parts.append(f"DEFAULT {default_val}")
|
|
815
835
|
|
|
@@ -821,8 +841,16 @@ class ORM(metaclass=ORMMeta):
|
|
|
821
841
|
engine = db.get_database_type()
|
|
822
842
|
sql = SQLTranslator.auto_increment_syntax(sql, engine)
|
|
823
843
|
|
|
824
|
-
db.execute(
|
|
844
|
+
# v3.13.16: don't claim success when the DDL failed. db.execute()
|
|
845
|
+
# swallows the driver error into get_error() and returns False, so a
|
|
846
|
+
# bad type (or any DDL error) used to leave create_table() returning
|
|
847
|
+
# True while no table was actually created — a silent, misleading pass.
|
|
848
|
+
ok = db.execute(sql)
|
|
825
849
|
db.commit()
|
|
850
|
+
if ok is False:
|
|
851
|
+
from tina4_python.debug import Log
|
|
852
|
+
Log.error(f"create_table failed for {table}: {db.get_error()}", sql=sql)
|
|
853
|
+
return False
|
|
826
854
|
return True
|
|
827
855
|
|
|
828
856
|
# ── Cached Queries ────────────────────────────────────────
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.14 → tina4_python-3.13.16}/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.14 → tina4_python-3.13.16}/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.14 → tina4_python-3.13.16}/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.14 → tina4_python-3.13.16}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/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.14 → tina4_python-3.13.16}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.14 → tina4_python-3.13.16}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|