tina4-python 3.13.2__tar.gz → 3.13.4__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.2 → tina4_python-3.13.4}/PKG-INFO +1 -1
- {tina4_python-3.13.2 → tina4_python-3.13.4}/pyproject.toml +1 -1
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/__init__.py +4 -1
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/core/request.py +52 -1
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/core/router.py +11 -6
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/core/server.py +87 -3
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/debug/__init__.py +51 -0
- tina4_python-3.13.4/tina4_python/env.py +111 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/.gitignore +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/README.md +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
|
|
|
8
8
|
|
|
9
9
|
One import, everything works.
|
|
10
10
|
"""
|
|
11
|
-
__version__ = "3.13.
|
|
11
|
+
__version__ = "3.13.4"
|
|
12
12
|
|
|
13
13
|
# ── Route decorators ──
|
|
14
14
|
from tina4_python.core.router import ( # noqa: E402, F401
|
|
@@ -42,6 +42,9 @@ from tina4_python.orm import ( # noqa: E402, F401
|
|
|
42
42
|
has_many, has_one, belongs_to, # relationship descriptors
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
+
# ── Env (typed env-var helpers) ──
|
|
46
|
+
from tina4_python.env import Env # noqa: E402, F401
|
|
47
|
+
|
|
45
48
|
# ── Auth ──
|
|
46
49
|
from tina4_python.auth import Auth # noqa: E402, F401
|
|
47
50
|
|
|
@@ -15,6 +15,57 @@ class PayloadTooLarge(Exception):
|
|
|
15
15
|
pass
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class CaseInsensitiveDict(dict):
|
|
19
|
+
"""Dict subclass for HTTP headers — string keys are case-insensitive.
|
|
20
|
+
|
|
21
|
+
HTTP header field-names are case-insensitive per RFC 7230 §3.2.
|
|
22
|
+
With this class, ``request.headers["Content-Type"]``,
|
|
23
|
+
``request.headers.get("content-type")``, and
|
|
24
|
+
``request.headers.get("CONTENT-TYPE")`` all return the same value.
|
|
25
|
+
|
|
26
|
+
Keys are stored lowercase internally. Cross-framework parity:
|
|
27
|
+
same behaviour now ships in tina4-php (Tina4\\Request),
|
|
28
|
+
tina4-ruby (Tina4::Request), tina4-nodejs (Tina4Request) so the
|
|
29
|
+
chapter 10 documented examples (``headers.get("Content-Type")``)
|
|
30
|
+
finally do what they read like. tina4-book#141 PY-10-03.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, *args, **kwargs):
|
|
34
|
+
super().__init__()
|
|
35
|
+
if args or kwargs:
|
|
36
|
+
self.update(dict(*args, **kwargs))
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _norm(key):
|
|
40
|
+
return key.lower() if isinstance(key, str) else key
|
|
41
|
+
|
|
42
|
+
def __setitem__(self, key, value):
|
|
43
|
+
super().__setitem__(self._norm(key), value)
|
|
44
|
+
|
|
45
|
+
def __getitem__(self, key):
|
|
46
|
+
return super().__getitem__(self._norm(key))
|
|
47
|
+
|
|
48
|
+
def __contains__(self, key):
|
|
49
|
+
return super().__contains__(self._norm(key))
|
|
50
|
+
|
|
51
|
+
def __delitem__(self, key):
|
|
52
|
+
super().__delitem__(self._norm(key))
|
|
53
|
+
|
|
54
|
+
def get(self, key, default=None):
|
|
55
|
+
return super().get(self._norm(key), default)
|
|
56
|
+
|
|
57
|
+
def setdefault(self, key, default=None):
|
|
58
|
+
return super().setdefault(self._norm(key), default)
|
|
59
|
+
|
|
60
|
+
def pop(self, key, *args):
|
|
61
|
+
return super().pop(self._norm(key), *args)
|
|
62
|
+
|
|
63
|
+
def update(self, *args, **kwargs):
|
|
64
|
+
# Route through __setitem__ so every key gets normalised.
|
|
65
|
+
for k, v in dict(*args, **kwargs).items():
|
|
66
|
+
self[k] = v
|
|
67
|
+
|
|
68
|
+
|
|
18
69
|
class Request:
|
|
19
70
|
"""Parsed HTTP request — everything a route handler needs."""
|
|
20
71
|
|
|
@@ -31,7 +82,7 @@ class Request:
|
|
|
31
82
|
self.query_string: str = ""
|
|
32
83
|
self.params: dict = {} # Query string + route params merged
|
|
33
84
|
self.query: dict = {} # Query string params only (separate from route params)
|
|
34
|
-
self.headers: dict =
|
|
85
|
+
self.headers: dict = CaseInsensitiveDict() # Case-insensitive HTTP headers
|
|
35
86
|
self.body: dict | str | None = None # Parsed body
|
|
36
87
|
self.raw_body: bytes = b""
|
|
37
88
|
self.cookies: dict = {}
|
|
@@ -687,7 +687,14 @@ def secured():
|
|
|
687
687
|
# ── Middleware Decorator ───────────────────────────────────────
|
|
688
688
|
|
|
689
689
|
def middleware(*middleware_classes):
|
|
690
|
-
"""Attach middleware classes to a route handler.
|
|
690
|
+
"""Attach middleware classes or functions to a route handler.
|
|
691
|
+
|
|
692
|
+
Middleware is purely additive — it does NOT change the route's auth
|
|
693
|
+
requirement. POST/PUT/PATCH/DELETE routes stay Bearer-token-gated by
|
|
694
|
+
default. Use ``@noauth()`` to open a write route, ``@secured()`` to
|
|
695
|
+
lock a read route. This rule is the same across all four Tina4
|
|
696
|
+
frameworks (tina4-book#141, PY-10-02).
|
|
697
|
+
"""
|
|
691
698
|
def decorator(fn):
|
|
692
699
|
fn._middleware = list(middleware_classes)
|
|
693
700
|
# If route was already registered (decorator applied after @get/@post),
|
|
@@ -695,11 +702,9 @@ def middleware(*middleware_classes):
|
|
|
695
702
|
if hasattr(fn, "_route_ref"):
|
|
696
703
|
existing = fn._route_ref._route.get("middleware", [])
|
|
697
704
|
fn._route_ref._route["middleware"] = list(middleware_classes) + existing
|
|
698
|
-
#
|
|
699
|
-
#
|
|
700
|
-
route
|
|
701
|
-
if not hasattr(fn, "_secured") and route.get("auth_required"):
|
|
702
|
-
route["auth_required"] = False
|
|
705
|
+
# Intentionally does NOT touch route["auth_required"]. Prior
|
|
706
|
+
# behaviour silently flipped it to False, creating an undocumented
|
|
707
|
+
# auth bypass for any write route that added custom middleware.
|
|
703
708
|
return fn
|
|
704
709
|
return decorator
|
|
705
710
|
|
|
@@ -1151,10 +1151,40 @@ def _check_auth(request: Request, response: Response, route: dict) -> bool:
|
|
|
1151
1151
|
return False
|
|
1152
1152
|
|
|
1153
1153
|
|
|
1154
|
+
def _is_function_middleware(mw) -> bool:
|
|
1155
|
+
"""A function-style middleware is a non-class callable that takes 3+ args.
|
|
1156
|
+
|
|
1157
|
+
Class-based middleware uses ``before_*`` / ``after_*`` method-name
|
|
1158
|
+
dispatch. Function-based middleware uses Express/FastAPI-style
|
|
1159
|
+
``async def mw(request, response, next_handler)`` with a continuation
|
|
1160
|
+
callable that invokes the next layer (or the route handler).
|
|
1161
|
+
Documented in chapter 10 for 8+ examples; before tina4-book#141
|
|
1162
|
+
PY-10-01 the framework silently ignored the function bodies.
|
|
1163
|
+
"""
|
|
1164
|
+
import inspect
|
|
1165
|
+
if isinstance(mw, type):
|
|
1166
|
+
return False # Class — class-based before_*/after_* dispatch
|
|
1167
|
+
if not callable(mw):
|
|
1168
|
+
return False
|
|
1169
|
+
try:
|
|
1170
|
+
sig = inspect.signature(mw)
|
|
1171
|
+
# The third (or later) parameter is the next_handler continuation.
|
|
1172
|
+
return len(sig.parameters) >= 3
|
|
1173
|
+
except (TypeError, ValueError):
|
|
1174
|
+
return False
|
|
1175
|
+
|
|
1176
|
+
|
|
1154
1177
|
def _run_before_middleware(request: Request, response: Response, route: dict) -> tuple[Request, Response, bool]:
|
|
1155
|
-
"""Run before_* middleware methods. Returns (request, response, skip_handler).
|
|
1178
|
+
"""Run class-based before_* middleware methods. Returns (request, response, skip_handler).
|
|
1179
|
+
|
|
1180
|
+
Function-style ``async def mw(req, resp, next_handler)`` middleware is
|
|
1181
|
+
skipped here — it's handled by ``_invoke_handler_with_middleware``
|
|
1182
|
+
which wraps the route handler with each function's continuation.
|
|
1183
|
+
"""
|
|
1156
1184
|
skip = False
|
|
1157
1185
|
for _mw_cls in route.get("middleware", []):
|
|
1186
|
+
if _is_function_middleware(_mw_cls):
|
|
1187
|
+
continue # Handled by the continuation wrapper instead
|
|
1158
1188
|
_mw_inst = _mw_cls() if isinstance(_mw_cls, type) else _mw_cls
|
|
1159
1189
|
for _attr_name in dir(_mw_inst):
|
|
1160
1190
|
if _attr_name.startswith("before_"):
|
|
@@ -1172,8 +1202,15 @@ def _run_before_middleware(request: Request, response: Response, route: dict) ->
|
|
|
1172
1202
|
|
|
1173
1203
|
|
|
1174
1204
|
def _run_after_middleware(request: Request, response: Response, route: dict) -> tuple[Request, Response]:
|
|
1175
|
-
"""Run after_* middleware methods.
|
|
1205
|
+
"""Run class-based after_* middleware methods.
|
|
1206
|
+
|
|
1207
|
+
Function-style middleware that wraps the handler (with next_handler)
|
|
1208
|
+
handles its own "after" code on the return path — no separate pass
|
|
1209
|
+
needed for it here.
|
|
1210
|
+
"""
|
|
1176
1211
|
for _mw_cls in route.get("middleware", []):
|
|
1212
|
+
if _is_function_middleware(_mw_cls):
|
|
1213
|
+
continue
|
|
1177
1214
|
_mw_inst = _mw_cls() if isinstance(_mw_cls, type) else _mw_cls
|
|
1178
1215
|
for _attr_name in dir(_mw_inst):
|
|
1179
1216
|
if _attr_name.startswith("after_"):
|
|
@@ -1185,6 +1222,53 @@ def _run_after_middleware(request: Request, response: Response, route: dict) ->
|
|
|
1185
1222
|
return request, response
|
|
1186
1223
|
|
|
1187
1224
|
|
|
1225
|
+
def _make_mw_continuation(mw, inner_next):
|
|
1226
|
+
"""Build a `next_handler` continuation for a function-style middleware.
|
|
1227
|
+
|
|
1228
|
+
Captures `mw` and the next layer (`inner_next`) in a closure. When
|
|
1229
|
+
invoked, calls `mw(req, resp, next_handler=inner_next)`. The
|
|
1230
|
+
middleware is responsible for awaiting `inner_next(req, resp)` if it
|
|
1231
|
+
wants the chain to continue — otherwise it short-circuits.
|
|
1232
|
+
"""
|
|
1233
|
+
async def wrapper(req, resp):
|
|
1234
|
+
return await mw(req, resp, inner_next)
|
|
1235
|
+
return wrapper
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
async def _invoke_handler_with_middleware(request: Request, response: Response, route: dict, params: dict) -> Response:
|
|
1239
|
+
"""Invoke the route handler, wrapping with any function-style middleware.
|
|
1240
|
+
|
|
1241
|
+
Function-style middleware (``async def mw(req, resp, next_handler)``)
|
|
1242
|
+
forms a Russian-doll continuation chain — the first declared
|
|
1243
|
+
middleware is the outermost layer; it calls ``next_handler`` to
|
|
1244
|
+
descend, and the route handler is the innermost. Class-based
|
|
1245
|
+
middleware is dispatched separately by ``_run_before_middleware`` /
|
|
1246
|
+
``_run_after_middleware`` and does not go through this wrapper.
|
|
1247
|
+
"""
|
|
1248
|
+
fn_middlewares = [
|
|
1249
|
+
mw for mw in route.get("middleware", [])
|
|
1250
|
+
if _is_function_middleware(mw)
|
|
1251
|
+
]
|
|
1252
|
+
|
|
1253
|
+
if not fn_middlewares:
|
|
1254
|
+
return await _invoke_handler(request, response, route, params)
|
|
1255
|
+
|
|
1256
|
+
# Build the chain from the inside out: innermost wraps the handler
|
|
1257
|
+
# first, then each outer layer wraps that. The result is a callable
|
|
1258
|
+
# `next_handler` that, when invoked, runs the whole chain.
|
|
1259
|
+
async def call_route_handler(req, resp):
|
|
1260
|
+
return await _invoke_handler(req, resp, route, params)
|
|
1261
|
+
|
|
1262
|
+
next_handler = call_route_handler
|
|
1263
|
+
for mw in reversed(fn_middlewares):
|
|
1264
|
+
next_handler = _make_mw_continuation(mw, next_handler)
|
|
1265
|
+
|
|
1266
|
+
result = await next_handler(request, response)
|
|
1267
|
+
if isinstance(result, Response):
|
|
1268
|
+
return result
|
|
1269
|
+
return response
|
|
1270
|
+
|
|
1271
|
+
|
|
1188
1272
|
async def _invoke_handler(request: Request, response: Response, route: dict, params: dict) -> Response:
|
|
1189
1273
|
"""Call the route handler with the correct arguments."""
|
|
1190
1274
|
import inspect
|
|
@@ -1430,7 +1514,7 @@ async def handle(request: Request) -> Response:
|
|
|
1430
1514
|
if not skip:
|
|
1431
1515
|
request, response, skip = _run_before_middleware(request, response, route)
|
|
1432
1516
|
if not skip:
|
|
1433
|
-
response = await
|
|
1517
|
+
response = await _invoke_handler_with_middleware(request, response, route, params)
|
|
1434
1518
|
request, response = _run_after_middleware(request, response, route)
|
|
1435
1519
|
except Exception as e:
|
|
1436
1520
|
response = _handle_route_error(e, request, response, request_id, _is_dev)
|
|
@@ -320,10 +320,57 @@ class Log:
|
|
|
320
320
|
now = datetime.now(timezone.utc)
|
|
321
321
|
return now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{now.microsecond // 1000:03d}Z"
|
|
322
322
|
|
|
323
|
+
# Frame names that belong to Log itself — walk past them when looking
|
|
324
|
+
# for the real caller. Kept as a class attribute so subclasses can
|
|
325
|
+
# extend if they layer extra wrappers (and so tests can introspect).
|
|
326
|
+
_OWN_FRAMES = frozenset({
|
|
327
|
+
"_caller_name", "_format_line", "_format", "_log",
|
|
328
|
+
"debug", "info", "warning", "error", "critical",
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def _caller_name(cls) -> str | None:
|
|
333
|
+
"""Return the function name that called Log.{debug,info,warning,error}.
|
|
334
|
+
|
|
335
|
+
Active only when ``TINA4_LOG_FUNC=true`` — the lookup uses
|
|
336
|
+
``inspect.currentframe()`` which is ~5% overhead per log call.
|
|
337
|
+
Walks past Log's own frames (``_OWN_FRAMES``) to land on the
|
|
338
|
+
real caller, so the count is robust whether the test calls
|
|
339
|
+
``_format_line`` directly or goes through ``Log.info`` →
|
|
340
|
+
``_log`` → ``_format_line``. Returns ``None`` if the stack is
|
|
341
|
+
too shallow (e.g. log called from module import) or if anything
|
|
342
|
+
goes wrong — never raises. Parity feature #41 across all four
|
|
343
|
+
Tina4 frameworks.
|
|
344
|
+
"""
|
|
345
|
+
import os
|
|
346
|
+
if os.environ.get("TINA4_LOG_FUNC", "").strip().lower() not in (
|
|
347
|
+
"1", "true", "on", "yes", "y", "t"
|
|
348
|
+
):
|
|
349
|
+
return None
|
|
350
|
+
try:
|
|
351
|
+
import inspect
|
|
352
|
+
frame = inspect.currentframe()
|
|
353
|
+
# Walk past Log's own frames. Cap the walk at 16 to defend
|
|
354
|
+
# against any pathological recursion / wrapper stack.
|
|
355
|
+
for _ in range(16):
|
|
356
|
+
if frame is None:
|
|
357
|
+
return None
|
|
358
|
+
name = frame.f_code.co_name
|
|
359
|
+
if name not in cls._OWN_FRAMES:
|
|
360
|
+
# Filter out anonymous / module-level callers — they're noise.
|
|
361
|
+
if name in ("<module>", "<lambda>", "<genexpr>", "<listcomp>", "<setcomp>", "<dictcomp>"):
|
|
362
|
+
return None
|
|
363
|
+
return name
|
|
364
|
+
frame = frame.f_back
|
|
365
|
+
return None
|
|
366
|
+
except Exception:
|
|
367
|
+
return None
|
|
368
|
+
|
|
323
369
|
@classmethod
|
|
324
370
|
def _format_line(cls, level: str, message: str, **kwargs) -> str:
|
|
325
371
|
timestamp = cls._timestamp()
|
|
326
372
|
request_id = get_request_id()
|
|
373
|
+
caller = cls._caller_name()
|
|
327
374
|
|
|
328
375
|
# JSON format wins whenever the explicit env opt-in is set,
|
|
329
376
|
# regardless of production flag (so dev devs can ship JSON to
|
|
@@ -336,6 +383,8 @@ class Log:
|
|
|
336
383
|
}
|
|
337
384
|
if request_id:
|
|
338
385
|
entry["request_id"] = request_id
|
|
386
|
+
if caller:
|
|
387
|
+
entry["function"] = caller
|
|
339
388
|
if kwargs:
|
|
340
389
|
entry["context"] = {k: v for k, v in kwargs.items()}
|
|
341
390
|
return json.dumps(entry, default=str)
|
|
@@ -345,6 +394,8 @@ class Log:
|
|
|
345
394
|
parts = [timestamp, f"[{level_str}]"]
|
|
346
395
|
if request_id:
|
|
347
396
|
parts.append(f"[{request_id}]")
|
|
397
|
+
if caller:
|
|
398
|
+
parts.append(f"[{caller}]")
|
|
348
399
|
parts.append(message)
|
|
349
400
|
if kwargs:
|
|
350
401
|
parts.append(json.dumps(kwargs, default=str))
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Tina4 Env — Typed environment-variable helpers.
|
|
2
|
+
# Copyright 2007 - present Tina4
|
|
3
|
+
# License: MIT
|
|
4
|
+
"""Typed environment-variable helpers — zero-deps.
|
|
5
|
+
|
|
6
|
+
Reading env vars by hand gets old fast: every boolean flag becomes a
|
|
7
|
+
``os.getenv("TINA4_DEBUG", "false").lower() in ("1", "true", "on", "yes")``
|
|
8
|
+
incantation. Every numeric tuning knob needs a try/except around int().
|
|
9
|
+
Env centralises that. Same API across all four Tina4 frameworks
|
|
10
|
+
(``Tina4\\Env`` in PHP, ``Tina4::Env`` in Ruby, ``Env`` in Node.js).
|
|
11
|
+
|
|
12
|
+
from tina4_python import Env
|
|
13
|
+
|
|
14
|
+
debug = Env.bool("TINA4_DEBUG", default=False)
|
|
15
|
+
workers = Env.int("WORKERS", default=4)
|
|
16
|
+
rate = Env.float("RATE_LIMIT", default=10.0)
|
|
17
|
+
region = Env.str("AWS_REGION", default="us-east-1")
|
|
18
|
+
|
|
19
|
+
Values are accepted case-insensitively after ``strip()``. Truthy:
|
|
20
|
+
``1 / true / on / yes / y / t``. Falsy: ``0 / false / off / no / n / f``.
|
|
21
|
+
Any other value triggers the ``default``. Unparseable ints and floats
|
|
22
|
+
log a warning via ``Log`` and fall back to ``default`` — never raise.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
|
|
28
|
+
# Lazy import so ``import tina4_python.env`` doesn't drag the whole Log
|
|
29
|
+
# chain in for callers who don't want it (CLI scaffolding, etc).
|
|
30
|
+
_TRUTHY = frozenset({"1", "true", "on", "yes", "y", "t"})
|
|
31
|
+
_FALSY = frozenset({"0", "false", "off", "no", "n", "f", ""})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _log_warning(message: str) -> None:
|
|
35
|
+
"""Emit a warning via Log without creating a circular import at module load."""
|
|
36
|
+
try:
|
|
37
|
+
from tina4_python.debug import Log
|
|
38
|
+
Log.warning(message)
|
|
39
|
+
except Exception:
|
|
40
|
+
# Log not yet wired (e.g. very early bootstrap) — silently skip.
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Env:
|
|
45
|
+
"""Typed environment-variable helpers.
|
|
46
|
+
|
|
47
|
+
All methods are classmethods so callers can write ``Env.bool(...)``
|
|
48
|
+
without instantiating anything — matching the PHP/Ruby/Node ports.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def bool(cls, name: str, default: bool = False) -> bool:
|
|
53
|
+
"""Read ``name`` and coerce to bool.
|
|
54
|
+
|
|
55
|
+
Truthy values (case-insensitive after strip): ``1``, ``true``,
|
|
56
|
+
``on``, ``yes``, ``y``, ``t``. Falsy: ``0``, ``false``, ``off``,
|
|
57
|
+
``no``, ``n``, ``f``, empty string. Anything else returns the
|
|
58
|
+
``default`` — never raises.
|
|
59
|
+
"""
|
|
60
|
+
raw = os.environ.get(name)
|
|
61
|
+
if raw is None:
|
|
62
|
+
return default
|
|
63
|
+
token = raw.strip().lower()
|
|
64
|
+
if token in _TRUTHY:
|
|
65
|
+
return True
|
|
66
|
+
if token in _FALSY:
|
|
67
|
+
return False
|
|
68
|
+
return default
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def int(cls, name: str, default: int = 0) -> int:
|
|
72
|
+
"""Read ``name`` and coerce to int. Returns ``default`` on parse failure."""
|
|
73
|
+
raw = os.environ.get(name)
|
|
74
|
+
if raw is None:
|
|
75
|
+
return default
|
|
76
|
+
try:
|
|
77
|
+
return int(raw.strip())
|
|
78
|
+
except (ValueError, TypeError):
|
|
79
|
+
_log_warning(
|
|
80
|
+
f"Env.int({name!r}): could not parse {raw!r} as int — "
|
|
81
|
+
f"using default {default!r}"
|
|
82
|
+
)
|
|
83
|
+
return default
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def float(cls, name: str, default: float = 0.0) -> float:
|
|
87
|
+
"""Read ``name`` and coerce to float. Returns ``default`` on parse failure."""
|
|
88
|
+
raw = os.environ.get(name)
|
|
89
|
+
if raw is None:
|
|
90
|
+
return default
|
|
91
|
+
try:
|
|
92
|
+
return float(raw.strip())
|
|
93
|
+
except (ValueError, TypeError):
|
|
94
|
+
_log_warning(
|
|
95
|
+
f"Env.float({name!r}): could not parse {raw!r} as float — "
|
|
96
|
+
f"using default {default!r}"
|
|
97
|
+
)
|
|
98
|
+
return default
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def str(cls, name: str, default: str = "") -> str:
|
|
102
|
+
"""Read ``name`` as a string. Returns ``default`` if unset.
|
|
103
|
+
|
|
104
|
+
Whitespace is preserved — this is a pass-through for the raw env
|
|
105
|
+
value. ``Env.str("PATH")`` is exactly ``os.environ.get("PATH", "")``
|
|
106
|
+
with a more discoverable name.
|
|
107
|
+
"""
|
|
108
|
+
raw = os.environ.get(name)
|
|
109
|
+
if raw is None:
|
|
110
|
+
return default
|
|
111
|
+
return raw
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.2 → tina4_python-3.13.4}/tina4_python/gallery/auth/src/routes/api/gallery_auth.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.2 → tina4_python-3.13.4}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.2 → tina4_python-3.13.4}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/templates/docker/distroless/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
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.2 → tina4_python-3.13.4}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|