tina4-python 3.13.102__tar.gz → 3.13.104__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.102 → tina4_python-3.13.104}/PKG-INFO +1 -1
- {tina4_python-3.13.102 → tina4_python-3.13.104}/pyproject.toml +1 -1
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/__init__.py +8 -1
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/server.py +13 -1
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/__init__.py +6 -2
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/adapter.py +3 -2
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/sql_translator.py +122 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/dev_admin/metrics.py +1 -1
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/frond/engine.py +167 -234
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/orm/__init__.py +6 -3
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/orm/fields.py +125 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/orm/model.py +195 -2
- tina4_python-3.13.104/tina4_python/orm/point.py +421 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/js/tina4-dev-admin.min.js +2 -2
- tina4_python-3.13.104/tina4_python/query_builder/__init__.py +757 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session/__init__.py +4 -1
- tina4_python-3.13.104/tina4_python/sso.py +319 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/swagger/__init__.py +13 -1
- tina4_python-3.13.102/tina4_python/query_builder/__init__.py +0 -445
- {tina4_python-3.13.102 → tina4_python-3.13.104}/.gitignore +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/README.md +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/ai/client.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/context/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/context/chunker.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/port_takeover.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/database_url.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/env.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/frond/compiler.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/frond/parser.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/mqtt/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/mqtt/message.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/amqp_url.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/Attachment.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/Channel.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/ChannelMember.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/Message.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/Workspace.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/models/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/realtime/storage.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session_handlers/memcached_handler.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.102 → tina4_python-3.13.104}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -51,7 +51,7 @@ def _resolve_version() -> str:
|
|
|
51
51
|
#
|
|
52
52
|
# test_version_constant.py now asserts this literal equals the pyproject
|
|
53
53
|
# version, so the release bump cannot leave it behind again.
|
|
54
|
-
return "3.13.
|
|
54
|
+
return "3.13.104"
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
__version__ = _resolve_version()
|
|
@@ -108,6 +108,9 @@ _LAZY: dict[str, tuple[str, str]] = {
|
|
|
108
108
|
"NumericField": ("tina4_python.orm", "NumericField"),
|
|
109
109
|
"DecimalField": ("tina4_python.orm", "DecimalField"),
|
|
110
110
|
"JSONField": ("tina4_python.orm", "JSONField"),
|
|
111
|
+
"PointField": ("tina4_python.orm", "PointField"),
|
|
112
|
+
"Point": ("tina4_python.orm", "Point"),
|
|
113
|
+
"feature_collection": ("tina4_python.orm", "feature_collection"),
|
|
111
114
|
"ForeignKeyField": ("tina4_python.orm", "ForeignKeyField"),
|
|
112
115
|
"IntField": ("tina4_python.orm", "IntField"),
|
|
113
116
|
"StrField": ("tina4_python.orm", "StrField"),
|
|
@@ -141,6 +144,10 @@ _LAZY: dict[str, tuple[str, str]] = {
|
|
|
141
144
|
"AiHTTPError": ("tina4_python.ai", "AiHTTPError"),
|
|
142
145
|
"AiTimeoutError": ("tina4_python.ai", "AiTimeoutError"),
|
|
143
146
|
"AiParseError": ("tina4_python.ai", "AiParseError"),
|
|
147
|
+
# Provider-neutral OpenID Connect SSO (ADR-0056)
|
|
148
|
+
"Sso": ("tina4_python.sso", "Sso"),
|
|
149
|
+
"SSO": ("tina4_python.sso", "SSO"),
|
|
150
|
+
"SsoError": ("tina4_python.sso", "SsoError"),
|
|
144
151
|
# SOAP / WSDL
|
|
145
152
|
"WSDL": ("tina4_python.wsdl", "WSDL"),
|
|
146
153
|
"wsdl_operation": ("tina4_python.wsdl", "wsdl_operation"),
|
|
@@ -1691,8 +1691,16 @@ def _check_auth(request: Request, response: Response, route: dict) -> bool:
|
|
|
1691
1691
|
if not _auth_ok:
|
|
1692
1692
|
_session = getattr(request, "session", None)
|
|
1693
1693
|
if _session:
|
|
1694
|
+
# A provider-verified OIDC identity is handed into the SAME auth
|
|
1695
|
+
# gate as JWT. Provider credentials stay in the reserved Session
|
|
1696
|
+
# value and never enter request.user.
|
|
1697
|
+
_sso = _session.get("_tina4_sso")
|
|
1698
|
+
_sso_identity = _sso.get("identity") if isinstance(_sso, dict) else None
|
|
1699
|
+
if isinstance(_sso_identity, dict) and _sso_identity.get("issuer") and _sso_identity.get("subject"):
|
|
1700
|
+
_auth_ok = True
|
|
1701
|
+
request.user = _sso_identity
|
|
1694
1702
|
_session_token = _session.get("token") if _session else ""
|
|
1695
|
-
if _session_token:
|
|
1703
|
+
if not _auth_ok and _session_token:
|
|
1696
1704
|
try:
|
|
1697
1705
|
from tina4_python.auth import Auth
|
|
1698
1706
|
_payload = Auth.valid_token_static(_session_token)
|
|
@@ -3669,6 +3677,10 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
3669
3677
|
|
|
3670
3678
|
# Auto-discover routes
|
|
3671
3679
|
_auto_discover("src")
|
|
3680
|
+
# Configuration-first OIDC: canonical routes appear only when configured,
|
|
3681
|
+
# after app discovery so collisions fail loudly rather than overwrite.
|
|
3682
|
+
from tina4_python.sso import Sso as _Sso
|
|
3683
|
+
_Sso.mount_configured()
|
|
3672
3684
|
route_count = len(Router.get_routes())
|
|
3673
3685
|
Log.info(f"Discovered {route_count} routes")
|
|
3674
3686
|
|
|
@@ -11,7 +11,11 @@ SQL-first database layer. One interface, many drivers.
|
|
|
11
11
|
row = db.fetch_one("SELECT * FROM users WHERE id = ?", [42])
|
|
12
12
|
db.execute("INSERT INTO users (name) VALUES (?)", ["Alice"])
|
|
13
13
|
"""
|
|
14
|
-
from tina4_python.database.adapter import DatabaseAdapter, DatabaseResult
|
|
14
|
+
from tina4_python.database.adapter import DatabaseAdapter, DatabaseResult
|
|
15
|
+
from tina4_python.database.sql_translator import SQLTranslator, SpatialNotSupportedError
|
|
15
16
|
from tina4_python.database.connection import Database
|
|
16
17
|
|
|
17
|
-
__all__ = [
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Database", "DatabaseAdapter", "DatabaseResult", "SQLTranslator",
|
|
20
|
+
"SpatialNotSupportedError",
|
|
21
|
+
]
|
|
@@ -1131,9 +1131,10 @@ class SqlCrudMixin:
|
|
|
1131
1131
|
# ── SQL Translation Rules ──────────────────────────────────────
|
|
1132
1132
|
# Reusable translation functions for common cross-engine quirks.
|
|
1133
1133
|
|
|
1134
|
-
|
|
1135
1134
|
# SQLTranslator moved to sql_translator.py (feature 3: the adapter module is the
|
|
1136
1135
|
# adapter contract and nothing else). Re-exported here because the sqlite,
|
|
1137
1136
|
# firebird, mssql and odbc adapters import it from this module, and a file move
|
|
1138
1137
|
# is not the place to churn their imports.
|
|
1139
|
-
from tina4_python.database.sql_translator import
|
|
1138
|
+
from tina4_python.database.sql_translator import ( # noqa: E402,F401
|
|
1139
|
+
SQLTranslator, SpatialNotSupportedError,
|
|
1140
|
+
)
|
|
@@ -16,6 +16,10 @@ by engine; moving it here would flatten that override.
|
|
|
16
16
|
import re
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
class SpatialNotSupportedError(NotImplementedError):
|
|
20
|
+
"""The selected database engine cannot honor the GIS contract."""
|
|
21
|
+
|
|
22
|
+
|
|
19
23
|
class SQLTranslator:
|
|
20
24
|
"""Cross-engine SQL translator.
|
|
21
25
|
|
|
@@ -23,6 +27,124 @@ class SQLTranslator:
|
|
|
23
27
|
and stateless — just string transforms.
|
|
24
28
|
"""
|
|
25
29
|
|
|
30
|
+
SPATIAL_ENGINES = ("postgres", "postgresql")
|
|
31
|
+
_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$")
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def supports_spatial(cls, engine: str) -> bool:
|
|
35
|
+
"""Return whether Tina4 has a spatial provider for ``engine``."""
|
|
36
|
+
name = (engine or "").lower()
|
|
37
|
+
name = cls.ENGINE_ALIASES.get(name, name)
|
|
38
|
+
return name in cls.SPATIAL_ENGINES
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def require_spatial(cls, engine: str, feature: str) -> str:
|
|
42
|
+
"""Return the normalized spatial engine or fail with an actionable error."""
|
|
43
|
+
name = (engine or "unknown").lower()
|
|
44
|
+
name = cls.ENGINE_ALIASES.get(name, name)
|
|
45
|
+
if cls.supports_spatial(name):
|
|
46
|
+
return name
|
|
47
|
+
raise SpatialNotSupportedError(
|
|
48
|
+
f"{feature} is not supported on the '{name}' database engine. "
|
|
49
|
+
"Tina4 GIS support is PostGIS-first: use PostgreSQL with the "
|
|
50
|
+
"PostGIS extension (CREATE EXTENSION postgis). Tina4 will not "
|
|
51
|
+
"replace a spatial query with an approximate latitude/longitude query. "
|
|
52
|
+
"If the application needs storage only and no GIS behavior, declare "
|
|
53
|
+
"separate longitude and latitude FloatField columns instead."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def identifier(cls, name: str, what: str = "column") -> str:
|
|
58
|
+
"""Validate an identifier before placing it in spatial SQL."""
|
|
59
|
+
if not isinstance(name, str) or not cls._IDENTIFIER.fullmatch(name):
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Spatial {what} name {name!r} is not a valid SQL identifier"
|
|
62
|
+
)
|
|
63
|
+
return name
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _srid(srid) -> int:
|
|
67
|
+
"""Coerce the interpolated SRID to an integer."""
|
|
68
|
+
if isinstance(srid, bool):
|
|
69
|
+
raise ValueError(f"Spatial SRID must be an integer, got {srid!r}")
|
|
70
|
+
try:
|
|
71
|
+
value = int(srid)
|
|
72
|
+
except (TypeError, ValueError) as error:
|
|
73
|
+
raise ValueError(f"Spatial SRID must be an integer, got {srid!r}") from error
|
|
74
|
+
if value <= 0:
|
|
75
|
+
raise ValueError(f"Spatial SRID must be positive, got {value}")
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def point_column_type(cls, engine: str, srid: int = 4326) -> str:
|
|
80
|
+
"""Return PostGIS geography Point DDL."""
|
|
81
|
+
cls.require_spatial(engine, "PointField")
|
|
82
|
+
return f"geography(Point,{cls._srid(srid)})"
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def spatial_index(cls, engine: str, table: str, column: str) -> str:
|
|
86
|
+
"""Return idempotent PostGIS GiST-index DDL."""
|
|
87
|
+
cls.require_spatial(engine, "spatial index creation")
|
|
88
|
+
table = cls.identifier(table, "table")
|
|
89
|
+
column = cls.identifier(column)
|
|
90
|
+
index = f"{table.replace('.', '_')}_{column}_gist"
|
|
91
|
+
return f"CREATE INDEX IF NOT EXISTS {index} ON {table} USING GIST ({column})"
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def point_literal(cls, engine: str, srid: int = 4326) -> str:
|
|
95
|
+
"""Return a bound PostGIS point expression in longitude/latitude order."""
|
|
96
|
+
cls.require_spatial(engine, "spatial predicates")
|
|
97
|
+
return f"ST_SetSRID(ST_MakePoint(?, ?), {cls._srid(srid)})::geography"
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def within_distance(cls, engine: str, column: str, srid: int = 4326) -> str:
|
|
101
|
+
"""Return a bound radius predicate whose distance uses metres."""
|
|
102
|
+
cls.require_spatial(engine, "within_distance()")
|
|
103
|
+
column = cls.identifier(column)
|
|
104
|
+
return f"ST_DWithin({column}, {cls.point_literal(engine, srid)}, ?)"
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def distance(cls, engine: str, column: str, srid: int = 4326) -> str:
|
|
108
|
+
"""Return a bound spheroid-distance expression in metres."""
|
|
109
|
+
cls.require_spatial(engine, "order_by_distance()")
|
|
110
|
+
column = cls.identifier(column)
|
|
111
|
+
return f"ST_Distance({column}, {cls.point_literal(engine, srid)})"
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def distance_as(cls, engine: str, column: str, alias: str, srid: int = 4326) -> str:
|
|
115
|
+
"""Return an aliased bound distance expression for a SELECT list."""
|
|
116
|
+
alias = cls.identifier(alias, "result alias")
|
|
117
|
+
return f"{cls.distance(engine, column, srid)} AS {alias}"
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def geometry_literal(cls, engine: str, form: str = "ewkt", srid: int = 4326) -> str:
|
|
121
|
+
"""Return a one-parameter PostGIS geometry expression."""
|
|
122
|
+
cls.require_spatial(engine, "spatial predicates")
|
|
123
|
+
if form == "ewkt":
|
|
124
|
+
return "ST_GeogFromText(?)"
|
|
125
|
+
if form == "geojson":
|
|
126
|
+
return f"ST_SetSRID(ST_GeomFromGeoJSON(?), {cls._srid(srid)})::geography"
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"Spatial geometry form {form!r} is not supported; use 'ewkt' or 'geojson'"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def intersects(cls, engine: str, column: str, form: str = "ewkt",
|
|
133
|
+
srid: int = 4326) -> str:
|
|
134
|
+
"""Return a bound PostGIS intersection predicate."""
|
|
135
|
+
column = cls.identifier(column)
|
|
136
|
+
return f"ST_Intersects({column}, {cls.geometry_literal(engine, form, srid)})"
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def bbox(cls, engine: str, column: str, srid: int = 4326) -> str:
|
|
140
|
+
"""Return a bound PostGIS bounding-box predicate."""
|
|
141
|
+
cls.require_spatial(engine, "bbox()")
|
|
142
|
+
column = cls.identifier(column)
|
|
143
|
+
return (
|
|
144
|
+
f"ST_Intersects({column}, "
|
|
145
|
+
f"ST_MakeEnvelope(?, ?, ?, ?, {cls._srid(srid)})::geography)"
|
|
146
|
+
)
|
|
147
|
+
|
|
26
148
|
@staticmethod
|
|
27
149
|
def limit_to_rows(sql: str) -> str:
|
|
28
150
|
"""Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
|
|
@@ -12,7 +12,7 @@ class MetricsEngineError(RuntimeError):
|
|
|
12
12
|
|
|
13
13
|
_TIMEOUT_SECONDS = 60
|
|
14
14
|
_SUMMARY_KEYS = ("files_analyzed", "total_functions", "avg_complexity", "avg_maintainability")
|
|
15
|
-
_FILE_KEYS = ("path", "loc", "avg_complexity", "maintainability", "
|
|
15
|
+
_FILE_KEYS = ("path", "loc", "avg_complexity", "maintainability", "has_referencing_test")
|
|
16
16
|
_FUNCTION_KEYS = ("name", "file", "line", "complexity", "loc")
|
|
17
17
|
_INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli"
|
|
18
18
|
_last_scan_root = ""
|