tina4-python 3.13.12__tar.gz → 3.13.15__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.12 → tina4_python-3.13.15}/PKG-INFO +1 -1
- {tina4_python-3.13.12 → tina4_python-3.13.15}/pyproject.toml +1 -1
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/core/middleware.py +9 -4
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/core/server.py +37 -1
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/database/adapter.py +21 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/database/mssql.py +16 -5
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/database/mysql.py +8 -2
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/database/postgres.py +70 -11
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/database/sqlite.py +16 -3
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/debug/__init__.py +12 -4
- {tina4_python-3.13.12 → tina4_python-3.13.15}/.gitignore +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/README.md +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/env.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -361,9 +361,15 @@ class RequestLoggerMiddleware:
|
|
|
361
361
|
"""Request logger — stamps start time before the handler and logs elapsed time after.
|
|
362
362
|
|
|
363
363
|
Mirrors the PHP, Ruby and Node.js RequestLogger classes.
|
|
364
|
+
|
|
365
|
+
v3.13.14: routes through the Tina4 ``Log`` class (was stdlib
|
|
366
|
+
``logging.getLogger``, whose ``info()`` is silently dropped by an
|
|
367
|
+
unconfigured root logger — so these lines never reached stdout).
|
|
368
|
+
The dev server also logs every request globally (see
|
|
369
|
+
``server._finalize_response``); this middleware remains for callers
|
|
370
|
+
that want per-route request logging.
|
|
364
371
|
"""
|
|
365
372
|
|
|
366
|
-
_logger = logging.getLogger("tina4.request")
|
|
367
373
|
_start_times: dict = {}
|
|
368
374
|
_lock = threading.Lock()
|
|
369
375
|
|
|
@@ -385,7 +391,6 @@ class RequestLoggerMiddleware:
|
|
|
385
391
|
method = getattr(request, "method", "?")
|
|
386
392
|
path = getattr(request, "url", None) or getattr(request, "path", "/")
|
|
387
393
|
status = getattr(response, "status_code", None) or getattr(response, "status", 0)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
)
|
|
394
|
+
from tina4_python.debug import Log
|
|
395
|
+
Log.info(f"{method} {path} -> {status} ({elapsed_ms}ms)")
|
|
391
396
|
return request, response
|
|
@@ -1404,6 +1404,21 @@ def _handle_no_route(request: Request, response: Response, request_id: str) -> R
|
|
|
1404
1404
|
return response
|
|
1405
1405
|
|
|
1406
1406
|
|
|
1407
|
+
def _request_logging_enabled(is_dev: bool) -> bool:
|
|
1408
|
+
"""Whether to emit a per-request log line (v3.13.14).
|
|
1409
|
+
|
|
1410
|
+
``TINA4_LOG_REQUESTS`` is the explicit control (true/false). When unset,
|
|
1411
|
+
request logging follows dev mode: on under ``TINA4_DEBUG``, off in
|
|
1412
|
+
production (so prod doesn't pay the per-request logging cost unless the
|
|
1413
|
+
operator opts in). Same contract across all four frameworks.
|
|
1414
|
+
"""
|
|
1415
|
+
from tina4_python.dotenv import is_truthy
|
|
1416
|
+
val = os.environ.get("TINA4_LOG_REQUESTS")
|
|
1417
|
+
if val is not None and val != "":
|
|
1418
|
+
return is_truthy(val)
|
|
1419
|
+
return is_dev
|
|
1420
|
+
|
|
1421
|
+
|
|
1407
1422
|
def _finalize_response(
|
|
1408
1423
|
request: Request, response: Response, route: dict | None,
|
|
1409
1424
|
request_id: str, is_dev: bool, req_start: float,
|
|
@@ -1444,6 +1459,22 @@ def _finalize_response(
|
|
|
1444
1459
|
except Exception:
|
|
1445
1460
|
pass
|
|
1446
1461
|
|
|
1462
|
+
# Request log line (v3.13.14). The dev dashboard's RequestInspector above
|
|
1463
|
+
# only feeds the /__dev UI — it never reached stdout, so `tina4 serve`
|
|
1464
|
+
# printed the startup banner then went silent even as requests flowed.
|
|
1465
|
+
# Emit a per-request line through the Tina4 Log so it lands on stdout
|
|
1466
|
+
# (docker logs / k8s) like every other log. On by default in dev; opt-in
|
|
1467
|
+
# in production via TINA4_LOG_REQUESTS to avoid per-request overhead.
|
|
1468
|
+
if _request_logging_enabled(is_dev):
|
|
1469
|
+
try:
|
|
1470
|
+
import time as _time
|
|
1471
|
+
elapsed_ms = round((_time.perf_counter() - req_start) * 1000, 3)
|
|
1472
|
+
Log.info(
|
|
1473
|
+
f"{request.method} {request.path} -> {response.status_code} ({elapsed_ms}ms)"
|
|
1474
|
+
)
|
|
1475
|
+
except Exception:
|
|
1476
|
+
pass
|
|
1477
|
+
|
|
1447
1478
|
# Session save + cookie.
|
|
1448
1479
|
# Skip saving if this was a brand-new session that the route never wrote to —
|
|
1449
1480
|
# that prevents empty orphaned session files accumulating on disk.
|
|
@@ -2081,7 +2112,12 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
2081
2112
|
|
|
2082
2113
|
# Init logger
|
|
2083
2114
|
is_production = os.environ.get("TINA4_ENV", "development") == "production"
|
|
2084
|
-
|
|
2115
|
+
# v3.13.14: default level is INFO (was ERROR). ERROR-by-default meant a
|
|
2116
|
+
# deployed app that logged at info/debug appeared silent — operators
|
|
2117
|
+
# "weren't getting logs". INFO shows request/startup/warn/error without
|
|
2118
|
+
# debug noise, and matches PHP/Ruby/Node defaults. Override per-deploy
|
|
2119
|
+
# with TINA4_LOG_LEVEL.
|
|
2120
|
+
log_level = os.environ.get("TINA4_LOG_LEVEL", "info")
|
|
2085
2121
|
Log.configure(level=log_level, production=is_production)
|
|
2086
2122
|
|
|
2087
2123
|
# Install a top-level exception hook so uncaught exceptions bubbling
|
|
@@ -375,6 +375,27 @@ class DatabaseAdapter:
|
|
|
375
375
|
stripped = stripped[:-1].rstrip()
|
|
376
376
|
return stripped
|
|
377
377
|
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _split_schema(name: str) -> tuple[str | None, str]:
|
|
380
|
+
"""Split a possibly-qualified table name into (schema, table).
|
|
381
|
+
|
|
382
|
+
v3.13.14 (#48): a model whose ``table_name`` is qualified —
|
|
383
|
+
PostgreSQL ``gift_cards.gift_card``, MSSQL ``dbo.widget``,
|
|
384
|
+
MySQL ``otherdb.table``, SQLite ``attached.table`` — lives in
|
|
385
|
+
that schema/catalog, not the default. Each adapter's
|
|
386
|
+
``table_exists`` / ``get_columns`` use this to query the right
|
|
387
|
+
namespace instead of matching the whole dotted string as one
|
|
388
|
+
flat table name. Returns ``(None, name)`` for a bare name so
|
|
389
|
+
callers default to the engine's current namespace. Splits on the
|
|
390
|
+
first dot; quoted identifiers with embedded dots aren't supported
|
|
391
|
+
(they weren't before either). Firebird has no schemas, so its
|
|
392
|
+
adapter ignores this.
|
|
393
|
+
"""
|
|
394
|
+
if "." in name:
|
|
395
|
+
schema, _, table = name.partition(".")
|
|
396
|
+
return schema, table
|
|
397
|
+
return None, name
|
|
398
|
+
|
|
378
399
|
def fetch(self, sql: str, params: list = None,
|
|
379
400
|
limit: int = 100, offset: int = 0) -> DatabaseResult:
|
|
380
401
|
"""Execute a read query and return multiple rows."""
|
|
@@ -190,10 +190,17 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
190
190
|
self._in_transaction = False
|
|
191
191
|
|
|
192
192
|
def table_exists(self, name: str) -> bool:
|
|
193
|
+
# v3.13.14 (#48): honour a schema-qualified name ("dbo.widget",
|
|
194
|
+
# "sales.orders"). Schemas are everyday in SQL Server; pre-fix this
|
|
195
|
+
# had no schema filter and matched the whole dotted string as a flat
|
|
196
|
+
# TABLE_NAME, so any qualified table_name was invisible. A bare name
|
|
197
|
+
# still matches in any schema (unchanged behaviour).
|
|
198
|
+
schema, tbl = self._split_schema(name)
|
|
193
199
|
row = self.fetch_one(
|
|
194
200
|
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES "
|
|
195
|
-
"WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = %s"
|
|
196
|
-
|
|
201
|
+
"WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = %s "
|
|
202
|
+
"AND (%s IS NULL OR TABLE_SCHEMA = %s)",
|
|
203
|
+
[tbl, schema, schema],
|
|
197
204
|
)
|
|
198
205
|
return row is not None
|
|
199
206
|
|
|
@@ -206,6 +213,9 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
206
213
|
return [r["TABLE_NAME"] for r in result.records]
|
|
207
214
|
|
|
208
215
|
def get_columns(self, table: str) -> list[dict]:
|
|
216
|
+
# v3.13.14 (#48): honour a schema-qualified name; a bare name matches
|
|
217
|
+
# in any schema (unchanged).
|
|
218
|
+
schema, tbl = self._split_schema(table)
|
|
209
219
|
sql = """
|
|
210
220
|
SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT,
|
|
211
221
|
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS is_primary
|
|
@@ -215,12 +225,13 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
215
225
|
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
216
226
|
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku
|
|
217
227
|
ON tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
|
|
218
|
-
WHERE tc.TABLE_NAME = %s AND tc.
|
|
228
|
+
WHERE tc.TABLE_NAME = %s AND (%s IS NULL OR tc.TABLE_SCHEMA = %s)
|
|
229
|
+
AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
|
219
230
|
) pk ON c.COLUMN_NAME = pk.COLUMN_NAME
|
|
220
|
-
WHERE c.TABLE_NAME = %s
|
|
231
|
+
WHERE c.TABLE_NAME = %s AND (%s IS NULL OR c.TABLE_SCHEMA = %s)
|
|
221
232
|
ORDER BY c.ORDINAL_POSITION
|
|
222
233
|
"""
|
|
223
|
-
result = self.fetch(sql, [
|
|
234
|
+
result = self.fetch(sql, [tbl, schema, schema, tbl, schema, schema], limit=10000)
|
|
224
235
|
return [
|
|
225
236
|
{
|
|
226
237
|
"name": r["COLUMN_NAME"],
|
|
@@ -174,10 +174,16 @@ class MySQLAdapter(DatabaseAdapter):
|
|
|
174
174
|
self._in_transaction = False
|
|
175
175
|
|
|
176
176
|
def table_exists(self, name: str) -> bool:
|
|
177
|
+
# v3.13.14 (#48): honour a database-qualified name ("otherdb.table").
|
|
178
|
+
# In MySQL "schema" == database; default to the connected database.
|
|
179
|
+
# Pre-fix this matched the whole dotted string as a flat TABLE_NAME
|
|
180
|
+
# under DATABASE() only, so a cross-database qualified name was never
|
|
181
|
+
# found and create_table()/migrations misfired.
|
|
182
|
+
schema, tbl = self._split_schema(name)
|
|
177
183
|
row = self.fetch_one(
|
|
178
184
|
"SELECT TABLE_NAME FROM information_schema.TABLES "
|
|
179
|
-
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s",
|
|
180
|
-
[
|
|
185
|
+
"WHERE TABLE_SCHEMA = COALESCE(%s, DATABASE()) AND TABLE_NAME = %s",
|
|
186
|
+
[schema, tbl],
|
|
181
187
|
)
|
|
182
188
|
return row is not None
|
|
183
189
|
|
|
@@ -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:
|
|
@@ -423,20 +462,40 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
423
462
|
self._in_transaction = False
|
|
424
463
|
|
|
425
464
|
def table_exists(self, name: str) -> bool:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
465
|
+
# v3.13.x (#48): support schema-qualified names ("schema.table") and the
|
|
466
|
+
# connection search_path. to_regclass() resolves a (possibly qualified)
|
|
467
|
+
# relation name to its OID using the SAME rules Postgres applies to a
|
|
468
|
+
# FROM clause, returning NULL when absent. Pre-fix this hardcoded
|
|
469
|
+
# schemaname='public' AND matched the whole dotted string as a flat
|
|
470
|
+
# tablename, so a table in a non-public schema was invisible —
|
|
471
|
+
# table_exists() always returned False, so create_table()/migrations
|
|
472
|
+
# misfired and ORM introspection saw nothing.
|
|
473
|
+
row = self.fetch_one("SELECT to_regclass(%s) AS oid", [name])
|
|
474
|
+
return bool(row and row.get("oid") is not None)
|
|
431
475
|
|
|
432
476
|
def get_tables(self) -> list[str]:
|
|
477
|
+
# v3.13.x (#48): list tables from every user schema, not just public.
|
|
478
|
+
# public tables stay bare ("users"); tables in other schemas are
|
|
479
|
+
# returned schema-qualified ("gift_cards.gift_card") so the names round
|
|
480
|
+
# -trip back through table_exists()/get_columns().
|
|
433
481
|
result = self.fetch(
|
|
434
|
-
"
|
|
482
|
+
"""
|
|
483
|
+
SELECT schemaname, tablename FROM pg_tables
|
|
484
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
485
|
+
ORDER BY schemaname, tablename
|
|
486
|
+
""",
|
|
435
487
|
limit=10000,
|
|
436
488
|
)
|
|
437
|
-
return [
|
|
489
|
+
return [
|
|
490
|
+
r["tablename"] if r["schemaname"] == "public"
|
|
491
|
+
else f"{r['schemaname']}.{r['tablename']}"
|
|
492
|
+
for r in result.records
|
|
493
|
+
]
|
|
438
494
|
|
|
439
495
|
def get_columns(self, table: str) -> list[dict]:
|
|
496
|
+
# v3.13.x (#48): honour a schema-qualified name; default to public.
|
|
497
|
+
schema, tbl = self._split_schema(table)
|
|
498
|
+
schema = schema or "public"
|
|
440
499
|
sql = """
|
|
441
500
|
SELECT c.column_name, c.data_type, c.is_nullable, c.column_default,
|
|
442
501
|
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END AS is_primary
|
|
@@ -446,12 +505,12 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
446
505
|
FROM information_schema.table_constraints tc
|
|
447
506
|
JOIN information_schema.key_column_usage ku
|
|
448
507
|
ON tc.constraint_name = ku.constraint_name
|
|
449
|
-
WHERE tc.table_name = %s AND tc.constraint_type = 'PRIMARY KEY'
|
|
508
|
+
WHERE tc.table_name = %s AND tc.table_schema = %s AND tc.constraint_type = 'PRIMARY KEY'
|
|
450
509
|
) pk ON c.column_name = pk.column_name
|
|
451
|
-
WHERE c.table_name = %s AND c.table_schema =
|
|
510
|
+
WHERE c.table_name = %s AND c.table_schema = %s
|
|
452
511
|
ORDER BY c.ordinal_position
|
|
453
512
|
"""
|
|
454
|
-
result = self.fetch(sql, [
|
|
513
|
+
result = self.fetch(sql, [tbl, schema, tbl, schema], limit=10000)
|
|
455
514
|
return [
|
|
456
515
|
{
|
|
457
516
|
"name": r["column_name"],
|
|
@@ -179,9 +179,16 @@ class SQLiteAdapter(DatabaseAdapter):
|
|
|
179
179
|
self._in_transaction = False
|
|
180
180
|
|
|
181
181
|
def table_exists(self, name: str) -> bool:
|
|
182
|
+
# v3.13.14 (#48): honour an attached-database prefix ("attached.table").
|
|
183
|
+
# SQLite's "schema" is an ATTACH alias; each has its own sqlite_master.
|
|
184
|
+
# Bare names use the main database (unchanged). The alias must be a
|
|
185
|
+
# plain identifier (it's developer-supplied, but guard against the dot
|
|
186
|
+
# being part of an odd name).
|
|
187
|
+
schema, tbl = self._split_schema(name)
|
|
188
|
+
master = f"{schema}.sqlite_master" if schema and schema.isidentifier() else "sqlite_master"
|
|
182
189
|
row = self.fetch_one(
|
|
183
|
-
"SELECT name FROM
|
|
184
|
-
[name]
|
|
190
|
+
f"SELECT name FROM {master} WHERE type='table' AND name=?",
|
|
191
|
+
[tbl if (schema and schema.isidentifier()) else name]
|
|
185
192
|
)
|
|
186
193
|
return row is not None
|
|
187
194
|
|
|
@@ -193,7 +200,13 @@ class SQLiteAdapter(DatabaseAdapter):
|
|
|
193
200
|
return [r["name"] for r in result.records]
|
|
194
201
|
|
|
195
202
|
def get_columns(self, table: str) -> list[dict]:
|
|
196
|
-
|
|
203
|
+
# v3.13.14 (#48): honour an attached-database prefix via
|
|
204
|
+
# `PRAGMA <schema>.table_info(<table>)`.
|
|
205
|
+
schema, tbl = self._split_schema(table)
|
|
206
|
+
if schema and schema.isidentifier() and tbl.isidentifier():
|
|
207
|
+
cursor = self._conn.execute(f"PRAGMA {schema}.table_info({tbl})")
|
|
208
|
+
else:
|
|
209
|
+
cursor = self._conn.execute(f"PRAGMA table_info({table})")
|
|
197
210
|
return [
|
|
198
211
|
{
|
|
199
212
|
"name": row["name"],
|
|
@@ -412,10 +412,18 @@ class Log:
|
|
|
412
412
|
# File always gets ALL levels (no filtering for file output)
|
|
413
413
|
line = cls._format_line(level, message, **kwargs)
|
|
414
414
|
|
|
415
|
-
# Console output respects TINA4_LOG_LEVEL and the stdout toggle
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
415
|
+
# Console output respects TINA4_LOG_LEVEL and the stdout toggle.
|
|
416
|
+
# v3.13.14: stdout is NOT suppressed in production — containers
|
|
417
|
+
# treat stdout as the canonical log sink (docker logs / k8s read
|
|
418
|
+
# PID 1 stdout), and the pre-v3.13.14 `not _is_production` gate
|
|
419
|
+
# meant deployed containers got nothing. Production still emits
|
|
420
|
+
# JSON (see _format_line / _is_production), just on stdout now.
|
|
421
|
+
# flush=True so logs appear immediately on a non-TTY pipe rather
|
|
422
|
+
# than sitting in Python's block buffer until the process exits.
|
|
423
|
+
if cls._stdout_enabled and cls._should_log(level):
|
|
424
|
+
color = "" if cls._is_production else cls.COLORS.get(level, "")
|
|
425
|
+
reset = "" if cls._is_production else cls.RESET
|
|
426
|
+
print(f"{color}{line}{reset}", flush=True)
|
|
419
427
|
|
|
420
428
|
# Always write ALL levels to the main file (raw log, no filtering)
|
|
421
429
|
if cls._writer:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.12 → tina4_python-3.13.15}/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.12 → tina4_python-3.13.15}/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.12 → tina4_python-3.13.15}/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.12 → tina4_python-3.13.15}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/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.12 → tina4_python-3.13.15}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.12 → tina4_python-3.13.15}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|