tina4-python 3.13.52__tar.gz → 3.13.54__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.52 → tina4_python-3.13.54}/PKG-INFO +1 -1
- {tina4_python-3.13.52 → tina4_python-3.13.54}/pyproject.toml +1 -1
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/CLAUDE.md +3 -1
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/graphql/__init__.py +2 -1
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/migration/runner.py +48 -6
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/orm/__init__.py +2 -2
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/orm/fields.py +86 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/orm/model.py +23 -2
- {tina4_python-3.13.52 → tina4_python-3.13.54}/.gitignore +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/README.md +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/env.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -901,7 +901,9 @@ User.query().where("name = ?", ["Alice"]).to_mongo()
|
|
|
901
901
|
```
|
|
902
902
|
|
|
903
903
|
### Available field types
|
|
904
|
-
`IntegerField`, `StringField`, `TextField`, `DateTimeField`, `NumericField`, `BlobField`, `BooleanField`, `FloatField`, `ForeignKeyField`
|
|
904
|
+
`IntegerField`, `StringField`, `TextField`, `DateTimeField`, `NumericField`, `BlobField`, `BooleanField`, `FloatField`, `JSONField`, `ForeignKeyField`
|
|
905
|
+
|
|
906
|
+
`JSONField()` stores a `dict`/`list` as JSON. You work with a plain Python object; the ORM serializes to a JSON string on save and parses it back on load, so `model.field` is always a `dict`/`list`. Engine-aware column: `JSONB` (PostgreSQL), `JSON` (MySQL), `NVARCHAR(MAX)` (MSSQL), `TEXT` (SQLite), `BLOB SUB_TYPE TEXT` (Firebird). Use it for a JSON column inside a relational row; use DocStore for whole-document storage with Mongo-style queries.
|
|
905
907
|
|
|
906
908
|
Import from `tina4_python` or `tina4_python.orm.fields`.
|
|
907
909
|
|
|
@@ -36,7 +36,7 @@ from tina4_python.database import Database # noqa: E402, F401
|
|
|
36
36
|
from tina4_python.orm import ( # noqa: E402, F401
|
|
37
37
|
ORM, bind_database, Field,
|
|
38
38
|
IntegerField, StringField, BooleanField, FloatField,
|
|
39
|
-
DateTimeField, TextField, BlobField, NumericField,
|
|
39
|
+
DateTimeField, TextField, BlobField, NumericField, JSONField,
|
|
40
40
|
ForeignKeyField,
|
|
41
41
|
IntField, StrField, BoolField, # short aliases
|
|
42
42
|
has_many, has_one, belongs_to, # relationship descriptors
|
|
@@ -382,7 +382,8 @@ class Schema:
|
|
|
382
382
|
gql_type = "Float"
|
|
383
383
|
elif ftype == "BooleanField":
|
|
384
384
|
gql_type = "Boolean"
|
|
385
|
-
elif ftype in ("StringField", "TextField", "DateTimeField"):
|
|
385
|
+
elif ftype in ("StringField", "TextField", "DateTimeField", "JSONField"):
|
|
386
|
+
# JSONField serializes to a JSON string in the GraphQL payload.
|
|
386
387
|
gql_type = "String"
|
|
387
388
|
else:
|
|
388
389
|
gql_type = "String"
|
|
@@ -192,6 +192,16 @@ def _upgrade_v2_to_v3(db, migration_folder: str | None) -> None:
|
|
|
192
192
|
"Detected legacy v2 tina4_migration schema — performing in-place upgrade to v3"
|
|
193
193
|
)
|
|
194
194
|
|
|
195
|
+
# A v2 table has no id generator; on Firebird, ids for rows recorded after
|
|
196
|
+
# the upgrade come from GEN_TINA4_MIGRATION_ID (see _record_migration), so
|
|
197
|
+
# make sure it exists. Fresh v3 tables get it in _create_v3_table.
|
|
198
|
+
if _is_firebird(db):
|
|
199
|
+
try:
|
|
200
|
+
db.execute("CREATE GENERATOR GEN_TINA4_MIGRATION_ID")
|
|
201
|
+
db.commit()
|
|
202
|
+
except Exception:
|
|
203
|
+
pass # Generator may already exist
|
|
204
|
+
|
|
195
205
|
cols = _column_names(db)
|
|
196
206
|
|
|
197
207
|
if "migration_id" not in cols:
|
|
@@ -331,6 +341,27 @@ def _normalize_quotes(sql: str) -> str:
|
|
|
331
341
|
return _SMART_QUOTE_RE.sub(lambda m: _SMART_QUOTES[m.group(0)], sql)
|
|
332
342
|
|
|
333
343
|
|
|
344
|
+
_SET_TERM_RE = re.compile(r"^SET\s+TERM\s+(\S+)$", re.IGNORECASE)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _parse_set_term(statement: str) -> str | None:
|
|
348
|
+
"""Return the new terminator from a ``SET TERM <new> <current>`` directive.
|
|
349
|
+
|
|
350
|
+
``SET TERM`` is a script-level directive (recognised by isql and other
|
|
351
|
+
InterBase/Firebird tooling, not run by the engine) that changes the
|
|
352
|
+
terminator separating statements. Recognising it lets a statement whose own
|
|
353
|
+
body contains the default ``;`` terminator — a trigger, stored procedure or
|
|
354
|
+
``EXECUTE BLOCK`` — be kept intact rather than split on those inner ``;``.
|
|
355
|
+
The terminator may be more than one character (e.g. ``!!``).
|
|
356
|
+
|
|
357
|
+
:param statement: A single, already-stripped statement.
|
|
358
|
+
:returns: The new terminator, or ``None`` when *statement* is not a
|
|
359
|
+
``SET TERM`` directive.
|
|
360
|
+
"""
|
|
361
|
+
match = _SET_TERM_RE.match(statement.strip())
|
|
362
|
+
return match.group(1) if match else None
|
|
363
|
+
|
|
364
|
+
|
|
334
365
|
def _split_statements(sql: str, delimiter: str = ";") -> list[str]:
|
|
335
366
|
"""Split SQL into individual statements with a single-pass, quote- and
|
|
336
367
|
comment-aware scanner.
|
|
@@ -356,6 +387,10 @@ def _split_statements(sql: str, delimiter: str = ";") -> list[str]:
|
|
|
356
387
|
a delimiter or comment, so it is preserved untouched.
|
|
357
388
|
- The **delimiter** ends a statement only when reached outside all of the
|
|
358
389
|
above. Empty statements are dropped; each kept statement is trimmed.
|
|
390
|
+
- A **SET TERM directive** (``SET TERM <new> <current>``) switches the active
|
|
391
|
+
terminator and is consumed, never emitted, so a statement whose own body
|
|
392
|
+
contains the default terminator survives as one. Multi-character
|
|
393
|
+
terminators (e.g. ``!!``) are supported.
|
|
359
394
|
|
|
360
395
|
Smart/curly quotes are normalized to straight ASCII first. Mirrors the
|
|
361
396
|
tina4-php ``Migration::splitStatements`` scanner for cross-framework parity.
|
|
@@ -367,7 +402,6 @@ def _split_statements(sql: str, delimiter: str = ";") -> list[str]:
|
|
|
367
402
|
statements: list[str] = []
|
|
368
403
|
current: list[str] = []
|
|
369
404
|
n = len(sql)
|
|
370
|
-
dlen = len(delimiter)
|
|
371
405
|
i = 0
|
|
372
406
|
in_dollar_block = False
|
|
373
407
|
in_slash_block = False
|
|
@@ -448,20 +482,28 @@ def _split_statements(sql: str, delimiter: str = ";") -> list[str]:
|
|
|
448
482
|
i += 1
|
|
449
483
|
continue
|
|
450
484
|
|
|
451
|
-
# Statement delimiter — only reached outside blocks/comments/strings.
|
|
485
|
+
# Statement delimiter — only reached outside blocks/comments/strings. A
|
|
486
|
+
# SET TERM directive switches the active terminator and is consumed
|
|
487
|
+
# (never emitted); any other completed statement is collected.
|
|
452
488
|
if delimiter and sql.startswith(delimiter, i):
|
|
489
|
+
i += len(delimiter)
|
|
453
490
|
stmt = "".join(current).strip()
|
|
454
|
-
if stmt:
|
|
455
|
-
statements.append(stmt)
|
|
456
491
|
current = []
|
|
457
|
-
|
|
492
|
+
if stmt:
|
|
493
|
+
new_term = _parse_set_term(stmt)
|
|
494
|
+
if new_term is not None:
|
|
495
|
+
delimiter = new_term
|
|
496
|
+
else:
|
|
497
|
+
statements.append(stmt)
|
|
458
498
|
continue
|
|
459
499
|
|
|
460
500
|
current.append(ch)
|
|
461
501
|
i += 1
|
|
462
502
|
|
|
503
|
+
# Trailing statement (may not end with a delimiter). A trailing SET TERM
|
|
504
|
+
# directive is a no-op — consume it, don't emit it.
|
|
463
505
|
stmt = "".join(current).strip()
|
|
464
|
-
if stmt:
|
|
506
|
+
if stmt and _parse_set_term(stmt) is None:
|
|
465
507
|
statements.append(stmt)
|
|
466
508
|
return statements
|
|
467
509
|
|
|
@@ -19,7 +19,7 @@ Active Record ORM with SQL-first paradigm.
|
|
|
19
19
|
from tina4_python.orm.fields import (
|
|
20
20
|
Field,
|
|
21
21
|
IntegerField, StringField, BooleanField, FloatField,
|
|
22
|
-
DateTimeField, TextField, BlobField, NumericField,
|
|
22
|
+
DateTimeField, TextField, BlobField, NumericField, JSONField,
|
|
23
23
|
ForeignKeyField,
|
|
24
24
|
IntField, StrField, BoolField, # short aliases
|
|
25
25
|
has_many, has_one, belongs_to, # relationship descriptors
|
|
@@ -31,7 +31,7 @@ __all__ = [
|
|
|
31
31
|
"Field",
|
|
32
32
|
# Verbose (preferred)
|
|
33
33
|
"IntegerField", "StringField", "BooleanField", "FloatField",
|
|
34
|
-
"DateTimeField", "TextField", "BlobField", "NumericField",
|
|
34
|
+
"DateTimeField", "TextField", "BlobField", "NumericField", "JSONField",
|
|
35
35
|
"ForeignKeyField",
|
|
36
36
|
# Short aliases (backwards compat)
|
|
37
37
|
"IntField", "StrField", "BoolField",
|
|
@@ -9,6 +9,8 @@ Fields define column types, constraints, and defaults for ORM models.
|
|
|
9
9
|
role = Field(str, choices=["admin", "user", "guest"])
|
|
10
10
|
active = Field(bool, default=True)
|
|
11
11
|
"""
|
|
12
|
+
import copy as _copy
|
|
13
|
+
import json as _json
|
|
12
14
|
import re as _re
|
|
13
15
|
from datetime import datetime
|
|
14
16
|
|
|
@@ -165,6 +167,16 @@ class Field:
|
|
|
165
167
|
|
|
166
168
|
return value
|
|
167
169
|
|
|
170
|
+
def to_db(self, value):
|
|
171
|
+
"""Serialize an in-memory value to the form the driver should store.
|
|
172
|
+
|
|
173
|
+
Identity by default — the value already round-trips through the driver
|
|
174
|
+
as-is. Overridden by fields whose Python representation differs from
|
|
175
|
+
their column representation (e.g. ``JSONField`` stores a JSON string).
|
|
176
|
+
Called by ``ORM.save()`` when building the row for INSERT/UPDATE.
|
|
177
|
+
"""
|
|
178
|
+
return value
|
|
179
|
+
|
|
168
180
|
def __repr__(self):
|
|
169
181
|
parts = [self.field_type.__name__]
|
|
170
182
|
if self.primary_key:
|
|
@@ -212,6 +224,80 @@ def BlobField(**kwargs):
|
|
|
212
224
|
def NumericField(**kwargs):
|
|
213
225
|
return _make_field(float, "NumericField", **kwargs)
|
|
214
226
|
|
|
227
|
+
class JSONField(Field):
|
|
228
|
+
"""A column that stores a JSON document (a ``dict`` or a ``list``).
|
|
229
|
+
|
|
230
|
+
You work with a plain Python ``dict``/``list``; the ORM serializes it to a
|
|
231
|
+
JSON string on save and parses it back on load. The column DDL is
|
|
232
|
+
engine-aware (see :meth:`ORM.create_table`): ``JSONB`` on PostgreSQL,
|
|
233
|
+
``JSON`` on MySQL, ``NVARCHAR(MAX)`` on MSSQL, ``TEXT`` on SQLite/ODBC,
|
|
234
|
+
``BLOB SUB_TYPE TEXT`` on Firebird.
|
|
235
|
+
|
|
236
|
+
Read normalization: a native-JSON driver (PostgreSQL JSONB, MySQL JSON) may
|
|
237
|
+
hand back an already-parsed object, while a string-backed engine (SQLite)
|
|
238
|
+
hands back a JSON string. :meth:`validate` normalizes both to a Python
|
|
239
|
+
object, so ``model.data`` is always a ``dict``/``list`` regardless of engine.
|
|
240
|
+
|
|
241
|
+
Usage::
|
|
242
|
+
|
|
243
|
+
class Event(ORM):
|
|
244
|
+
id = IntegerField(primary_key=True, auto_increment=True)
|
|
245
|
+
payload = JSONField()
|
|
246
|
+
|
|
247
|
+
Event({"payload": {"type": "click", "tags": ["a", "b"]}}).save()
|
|
248
|
+
e = Event.find(1)
|
|
249
|
+
e.payload["type"] # "click" — a real dict, not a string
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, **kwargs):
|
|
253
|
+
super().__init__(dict, **kwargs)
|
|
254
|
+
self.kind = "JSONField"
|
|
255
|
+
|
|
256
|
+
def _resolve_default(self):
|
|
257
|
+
# Deep-copy a mutable JSON default (``default={}`` / ``default=[]``) so
|
|
258
|
+
# separate instances never alias the same object — the classic
|
|
259
|
+
# shared-default footgun. This runs on BOTH the __init__ default path
|
|
260
|
+
# (which calls _resolve_default directly) and the validate() None path.
|
|
261
|
+
d = super()._resolve_default()
|
|
262
|
+
return _copy.deepcopy(d) if isinstance(d, (dict, list)) else d
|
|
263
|
+
|
|
264
|
+
def validate(self, value):
|
|
265
|
+
# None -> required check + default (mirrors base Field).
|
|
266
|
+
if value is None:
|
|
267
|
+
if self.required and self.default is None:
|
|
268
|
+
raise ValueError(f"Field '{self.name}' is required")
|
|
269
|
+
return self._resolve_default()
|
|
270
|
+
|
|
271
|
+
# A driver returns a JSON string (SQLite/TEXT) or an already-parsed
|
|
272
|
+
# dict/list (PostgreSQL JSONB, MySQL JSON). Normalize to a Python object.
|
|
273
|
+
if isinstance(value, (bytes, bytearray)):
|
|
274
|
+
value = value.decode("utf-8")
|
|
275
|
+
if isinstance(value, str):
|
|
276
|
+
try:
|
|
277
|
+
value = _json.loads(value)
|
|
278
|
+
except (ValueError, TypeError) as e:
|
|
279
|
+
raise ValueError(f"Field '{self.name}': value is not valid JSON") from e
|
|
280
|
+
if not isinstance(value, (dict, list)):
|
|
281
|
+
raise ValueError(
|
|
282
|
+
f"Field '{self.name}': expected a dict or list, got {type(value).__name__}"
|
|
283
|
+
)
|
|
284
|
+
if self.validator is not None:
|
|
285
|
+
self.validator(value)
|
|
286
|
+
return value
|
|
287
|
+
|
|
288
|
+
def to_db(self, value):
|
|
289
|
+
# Serialize to a JSON string for the driver. A TEXT column stores it
|
|
290
|
+
# verbatim; a native JSON column (JSONB/JSON) casts the string literal.
|
|
291
|
+
if value is None:
|
|
292
|
+
return None
|
|
293
|
+
if isinstance(value, str):
|
|
294
|
+
return value # already serialized (defensive)
|
|
295
|
+
try:
|
|
296
|
+
return _json.dumps(value)
|
|
297
|
+
except (TypeError, ValueError) as e:
|
|
298
|
+
raise ValueError(f"Field '{self.name}': value is not JSON-serializable") from e
|
|
299
|
+
|
|
300
|
+
|
|
215
301
|
class ForeignKeyField(Field):
|
|
216
302
|
"""Integer field that references another model's primary key.
|
|
217
303
|
|
|
@@ -390,7 +390,9 @@ class ORM(metaclass=ORMMeta):
|
|
|
390
390
|
if value is not None or not field.auto_increment:
|
|
391
391
|
# Use field_mapping for the column name, fall back to field.column
|
|
392
392
|
db_col = self.field_mapping.get(name, field.column)
|
|
393
|
-
|
|
393
|
+
# Serialize to the column's storage form: identity for most
|
|
394
|
+
# fields, JSON string for JSONField (see Field.to_db).
|
|
395
|
+
data[db_col] = field.to_db(value)
|
|
394
396
|
|
|
395
397
|
# v3.13.11 (issue #50): pick INSERT vs UPDATE on row existence
|
|
396
398
|
# for non-auto-increment PKs. Auto-increment keeps the legacy
|
|
@@ -840,6 +842,23 @@ class ORM(metaclass=ORMMeta):
|
|
|
840
842
|
else:
|
|
841
843
|
datetime_sql = "DATETIME"
|
|
842
844
|
|
|
845
|
+
# JSONField -> the engine's native JSON type where it has one, else a
|
|
846
|
+
# text column. PostgreSQL JSONB (binary, indexable, canonical form);
|
|
847
|
+
# MySQL JSON; MSSQL has no JSON type so NVARCHAR(MAX) (its documented
|
|
848
|
+
# JSON storage); SQLite/ODBC TEXT; Firebird has no TEXT/JSON type so
|
|
849
|
+
# BLOB SUB_TYPE TEXT. The ORM stores a JSON string in every case, so a
|
|
850
|
+
# text-backed column round-trips identically to a native one.
|
|
851
|
+
if engine in ("postgres", "postgresql"):
|
|
852
|
+
json_sql = "JSONB"
|
|
853
|
+
elif engine == "mysql":
|
|
854
|
+
json_sql = "JSON"
|
|
855
|
+
elif engine == "mssql":
|
|
856
|
+
json_sql = "NVARCHAR(MAX)"
|
|
857
|
+
elif engine == "firebird":
|
|
858
|
+
json_sql = "BLOB SUB_TYPE TEXT"
|
|
859
|
+
else:
|
|
860
|
+
json_sql = "TEXT"
|
|
861
|
+
|
|
843
862
|
# Don't recreate if table already exists
|
|
844
863
|
if db.table_exists(table):
|
|
845
864
|
return True
|
|
@@ -866,6 +885,8 @@ class ORM(metaclass=ORMMeta):
|
|
|
866
885
|
sql_type = datetime_sql
|
|
867
886
|
elif kind == "BlobField":
|
|
868
887
|
sql_type = "BLOB"
|
|
888
|
+
elif kind == "JSONField":
|
|
889
|
+
sql_type = json_sql
|
|
869
890
|
else:
|
|
870
891
|
# Fallback based on field_type
|
|
871
892
|
ft = field_obj.field_type
|
|
@@ -886,7 +907,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
886
907
|
parts.append("AUTOINCREMENT")
|
|
887
908
|
if field_obj.required and not field_obj.primary_key:
|
|
888
909
|
parts.append("NOT NULL")
|
|
889
|
-
if field_obj.default is not None and not field_obj.auto_increment:
|
|
910
|
+
if field_obj.default is not None and not field_obj.auto_increment and kind != "JSONField":
|
|
890
911
|
default_val = field_obj.default
|
|
891
912
|
if isinstance(default_val, str):
|
|
892
913
|
parts.append(f"DEFAULT '{default_val}'")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.52 → tina4_python-3.13.54}/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.52 → tina4_python-3.13.54}/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.52 → tina4_python-3.13.54}/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.52 → tina4_python-3.13.54}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/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.52 → tina4_python-3.13.54}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.52 → tina4_python-3.13.54}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|