tina4-python 3.13.28__tar.gz → 3.13.30__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.28 → tina4_python-3.13.30}/PKG-INFO +1 -1
- {tina4_python-3.13.28 → tina4_python-3.13.30}/pyproject.toml +1 -1
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/CLAUDE.md +1 -1
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/core/router.py +48 -7
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/docs.py +117 -31
- {tina4_python-3.13.28 → tina4_python-3.13.30}/.gitignore +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/README.md +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/env.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -529,7 +529,7 @@ TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookie
|
|
|
529
529
|
|
|
530
530
|
### Authentication & Security
|
|
531
531
|
- Use `Auth.hash_password()` from `tina4_python.auth` to hash passwords — never use hashlib directly.
|
|
532
|
-
- Use `Auth.check_password(
|
|
532
|
+
- Use `Auth.check_password(password, hash)` from `tina4_python.auth` to verify passwords.
|
|
533
533
|
|
|
534
534
|
## Templates (Twig)
|
|
535
535
|
|
|
@@ -171,11 +171,12 @@ class Router:
|
|
|
171
171
|
- ``event`` is ``"open"``, ``"message"``, or ``"close"``
|
|
172
172
|
- ``data`` is the message payload (str for message, None for open/close)
|
|
173
173
|
"""
|
|
174
|
-
pattern, param_names = _compile_pattern(path)
|
|
174
|
+
pattern, param_names, param_types = _compile_pattern(path)
|
|
175
175
|
route = {
|
|
176
176
|
"path": path,
|
|
177
177
|
"pattern": pattern,
|
|
178
178
|
"param_names": param_names,
|
|
179
|
+
"param_types": param_types,
|
|
179
180
|
"handler": handler,
|
|
180
181
|
}
|
|
181
182
|
_ws_routes.append(route)
|
|
@@ -188,8 +189,9 @@ class Router:
|
|
|
188
189
|
m = route["pattern"].match(path)
|
|
189
190
|
if m:
|
|
190
191
|
params = {}
|
|
192
|
+
_types = route.get("param_types", {})
|
|
191
193
|
for i, name in enumerate(route["param_names"]):
|
|
192
|
-
params[name] = m.group(i + 1)
|
|
194
|
+
params[name] = _cast_param(m.group(i + 1), _types.get(name))
|
|
193
195
|
return route, params
|
|
194
196
|
return None, {}
|
|
195
197
|
|
|
@@ -303,7 +305,7 @@ class Router:
|
|
|
303
305
|
combined_mw = list(cls._group_middleware) + list(handler_mw) + route_mw
|
|
304
306
|
effective_middleware = combined_mw or []
|
|
305
307
|
|
|
306
|
-
pattern, param_names = _compile_pattern(path)
|
|
308
|
+
pattern, param_names, param_types = _compile_pattern(path)
|
|
307
309
|
|
|
308
310
|
# Auth default: GET=public, writes=secured (unless custom middleware handles auth)
|
|
309
311
|
m = method.upper()
|
|
@@ -329,6 +331,7 @@ class Router:
|
|
|
329
331
|
"path": path,
|
|
330
332
|
"pattern": pattern,
|
|
331
333
|
"param_names": param_names,
|
|
334
|
+
"param_types": param_types,
|
|
332
335
|
"handler": handler,
|
|
333
336
|
"middleware": effective_middleware,
|
|
334
337
|
"auth_required": auth_required,
|
|
@@ -359,8 +362,9 @@ class Router:
|
|
|
359
362
|
m = route["pattern"].match(path)
|
|
360
363
|
if m:
|
|
361
364
|
params = {}
|
|
365
|
+
_types = route.get("param_types", {})
|
|
362
366
|
for i, name in enumerate(route["param_names"]):
|
|
363
|
-
params[name] = m.group(i + 1)
|
|
367
|
+
params[name] = _cast_param(m.group(i + 1), _types.get(name))
|
|
364
368
|
return route, params
|
|
365
369
|
|
|
366
370
|
# Second pass: HEAD auto-fallback to GET when no HEAD route registered
|
|
@@ -371,8 +375,9 @@ class Router:
|
|
|
371
375
|
m = route["pattern"].match(path)
|
|
372
376
|
if m:
|
|
373
377
|
params = {}
|
|
378
|
+
_types = route.get("param_types", {})
|
|
374
379
|
for i, name in enumerate(route["param_names"]):
|
|
375
|
-
params[name] = m.group(i + 1)
|
|
380
|
+
params[name] = _cast_param(m.group(i + 1), _types.get(name))
|
|
376
381
|
return route, params
|
|
377
382
|
|
|
378
383
|
return None, {}
|
|
@@ -454,8 +459,36 @@ _TYPE_PATTERNS = {
|
|
|
454
459
|
".*": ".+",
|
|
455
460
|
}
|
|
456
461
|
|
|
462
|
+
# Type names whose captured value is coerced from str to a Python scalar before
|
|
463
|
+
# it reaches the handler. Mirrors Ruby's ``cast_param`` (lib/tina4/router.rb):
|
|
464
|
+
# ``int``/``integer`` → ``int``, ``float``/``number`` → ``float``. Every other
|
|
465
|
+
# type (string, alpha, alnum, slug, uuid, path) and untyped params stay ``str``.
|
|
466
|
+
_TYPE_CASTS = {
|
|
467
|
+
"int": int,
|
|
468
|
+
"integer": int,
|
|
469
|
+
"float": float,
|
|
470
|
+
"number": float,
|
|
471
|
+
}
|
|
472
|
+
|
|
457
473
|
|
|
458
|
-
def
|
|
474
|
+
def _cast_param(value: str, type_hint: str | None):
|
|
475
|
+
"""Coerce a captured route param to its declared Python type.
|
|
476
|
+
|
|
477
|
+
The URL regex already guarantees the segment matches the type's pattern
|
|
478
|
+
(e.g. ``{id:int}`` only matches digits), so the cast normally can't fail.
|
|
479
|
+
We still guard it: a coercion failure must never crash routing — fall back
|
|
480
|
+
to the raw string, exactly as Ruby leaves unknown types untouched.
|
|
481
|
+
"""
|
|
482
|
+
caster = _TYPE_CASTS.get(type_hint)
|
|
483
|
+
if caster is None:
|
|
484
|
+
return value
|
|
485
|
+
try:
|
|
486
|
+
return caster(value)
|
|
487
|
+
except (TypeError, ValueError):
|
|
488
|
+
return value
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _compile_pattern(path: str) -> tuple[re.Pattern, list[str], dict[str, str]]:
|
|
459
492
|
"""Convert a route path to a regex pattern.
|
|
460
493
|
|
|
461
494
|
Supports:
|
|
@@ -468,9 +501,16 @@ def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
|
|
|
468
501
|
/api/files/{p:path} → greedy (matches remaining path)
|
|
469
502
|
/api/docs/* → bare-wildcard catch-all (key "*")
|
|
470
503
|
|
|
504
|
+
Returns ``(pattern, param_names, param_types)`` where ``param_types`` maps
|
|
505
|
+
each declared name to its type hint (``"int"``, ``"float"``, …). Untyped
|
|
506
|
+
params and the ``*`` wildcard are absent from the map. The map drives
|
|
507
|
+
coercion in :meth:`Router.match` so typed params arrive at the handler as
|
|
508
|
+
Python scalars (mirrors Ruby's ``cast_param``).
|
|
509
|
+
|
|
471
510
|
Unknown type names raise ``ValueError`` at route registration time.
|
|
472
511
|
"""
|
|
473
512
|
param_names = []
|
|
513
|
+
param_types: dict[str, str] = {}
|
|
474
514
|
regex_parts = []
|
|
475
515
|
|
|
476
516
|
segments = path.strip("/").split("/")
|
|
@@ -490,6 +530,7 @@ def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
|
|
|
490
530
|
f"Valid types: {', '.join(sorted(k for k in _TYPE_PATTERNS if k != '.*'))}."
|
|
491
531
|
)
|
|
492
532
|
regex_parts.append("(" + _TYPE_PATTERNS[type_hint] + ")")
|
|
533
|
+
param_types[name] = type_hint
|
|
493
534
|
else:
|
|
494
535
|
name = inner
|
|
495
536
|
regex_parts.append("([^/]+)")
|
|
@@ -498,7 +539,7 @@ def _compile_pattern(path: str) -> tuple[re.Pattern, list[str]]:
|
|
|
498
539
|
regex_parts.append(re.escape(segment))
|
|
499
540
|
|
|
500
541
|
pattern_str = "^/" + "/".join(regex_parts) + "/?$"
|
|
501
|
-
return re.compile(pattern_str), param_names
|
|
542
|
+
return re.compile(pattern_str), param_names, param_types
|
|
502
543
|
|
|
503
544
|
|
|
504
545
|
# Decorator functions — the public API
|
|
@@ -139,6 +139,62 @@ def _safe_signature(obj) -> str:
|
|
|
139
139
|
return "(...)"
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
# Leading parameter names that are receivers, never passed by callers — dropped
|
|
143
|
+
# from public signatures (covers self/cls and the (cls, instance) pair used by
|
|
144
|
+
# dual class/instance descriptors).
|
|
145
|
+
_RECEIVER_PARAMS = {"self", "cls", "mcs", "metacls", "instance", "owner"}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _render_signature(func, display_name: str) -> str:
|
|
149
|
+
"""Public signature for `display_name`, dropping leading receiver params."""
|
|
150
|
+
try:
|
|
151
|
+
sig = inspect.signature(func)
|
|
152
|
+
except (TypeError, ValueError):
|
|
153
|
+
return f"{display_name}(...)"
|
|
154
|
+
params = list(sig.parameters.values())
|
|
155
|
+
while params and params[0].name in _RECEIVER_PARAMS and params[0].kind in (
|
|
156
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
157
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
158
|
+
):
|
|
159
|
+
params = params[1:]
|
|
160
|
+
try:
|
|
161
|
+
sig = sig.replace(parameters=params)
|
|
162
|
+
except (TypeError, ValueError):
|
|
163
|
+
pass
|
|
164
|
+
return f"{display_name}{sig}"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _unwrap_callable(raw, owner, mname: str):
|
|
168
|
+
"""Resolve a raw class-dict value to (func, is_static, is_class).
|
|
169
|
+
|
|
170
|
+
`func` is the function to introspect for signature/doc/source, or `None`
|
|
171
|
+
for a non-callable class attribute (which the caller skips). Handles plain
|
|
172
|
+
functions, @staticmethod/@classmethod, properties, and custom descriptors
|
|
173
|
+
such as Frond's dual class/instance method (whose wrapper's __qualname__
|
|
174
|
+
would otherwise hide it from the owner check)."""
|
|
175
|
+
if isinstance(raw, staticmethod):
|
|
176
|
+
return raw.__func__, True, False
|
|
177
|
+
if isinstance(raw, classmethod):
|
|
178
|
+
return raw.__func__, False, True
|
|
179
|
+
if isinstance(raw, property):
|
|
180
|
+
return (raw.fget, False, False) if raw.fget else (None, False, False)
|
|
181
|
+
if inspect.isfunction(raw):
|
|
182
|
+
return raw, False, False
|
|
183
|
+
# Custom descriptor — prefer the function it wraps.
|
|
184
|
+
for attr in ("func", "__func__", "__wrapped__"):
|
|
185
|
+
f = getattr(raw, attr, None)
|
|
186
|
+
if inspect.isfunction(f):
|
|
187
|
+
return f, False, False
|
|
188
|
+
# Descriptor that only yields a callable through __get__ (last resort).
|
|
189
|
+
try:
|
|
190
|
+
bound = getattr(owner, mname)
|
|
191
|
+
if inspect.isfunction(bound) or inspect.ismethod(bound):
|
|
192
|
+
return bound, False, False
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
return None, False, False
|
|
196
|
+
|
|
197
|
+
|
|
142
198
|
def _is_private_name(name: str) -> bool:
|
|
143
199
|
"""Dunder stays hidden always. Single-underscore is private (opt-in)."""
|
|
144
200
|
if name.startswith("__") and name.endswith("__"):
|
|
@@ -226,32 +282,16 @@ def _reflect_framework(version: str) -> list[_Entity]:
|
|
|
226
282
|
source="framework",
|
|
227
283
|
))
|
|
228
284
|
|
|
229
|
-
# Methods
|
|
230
|
-
|
|
285
|
+
# Methods defined DIRECTLY on this class (inherited ones are skipped).
|
|
286
|
+
# Membership in obj.__dict__ is the reliable owner test — comparing
|
|
287
|
+
# __qualname__ breaks for methods built by custom descriptors (e.g.
|
|
288
|
+
# Frond's dual class/instance methods), which would silently vanish.
|
|
289
|
+
for mname, raw in list(obj.__dict__.items()):
|
|
231
290
|
if _is_private_name(mname):
|
|
232
291
|
continue
|
|
233
|
-
is_static =
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
raw = obj.__dict__.get(mname, None)
|
|
237
|
-
if isinstance(raw, staticmethod):
|
|
238
|
-
is_static = True
|
|
239
|
-
func = raw.__func__
|
|
240
|
-
elif isinstance(raw, classmethod):
|
|
241
|
-
is_class = True
|
|
242
|
-
func = raw.__func__
|
|
243
|
-
elif inspect.isfunction(mobj) or inspect.ismethod(mobj):
|
|
244
|
-
func = mobj
|
|
245
|
-
else:
|
|
246
|
-
continue
|
|
247
|
-
|
|
248
|
-
# Only include methods actually defined on this class (not inherited).
|
|
249
|
-
if func.__qualname__.split(".")[0] != obj.__qualname__.split(".")[-1]:
|
|
250
|
-
# Heuristic: compare the first qualname segment to class name.
|
|
251
|
-
# If the method's __qualname__ doesn't start with this class, skip.
|
|
252
|
-
owner = func.__qualname__.rsplit(".", 1)[0]
|
|
253
|
-
if owner != obj.__qualname__:
|
|
254
|
-
continue
|
|
292
|
+
func, is_static, is_class = _unwrap_callable(raw, obj, mname)
|
|
293
|
+
if func is None:
|
|
294
|
+
continue # non-callable class attribute
|
|
255
295
|
|
|
256
296
|
try:
|
|
257
297
|
mfile = inspect.getsourcefile(func) or src_file
|
|
@@ -262,7 +302,7 @@ def _reflect_framework(version: str) -> list[_Entity]:
|
|
|
262
302
|
|
|
263
303
|
mdoc = inspect.getdoc(func) or ""
|
|
264
304
|
msummary = _summary_from_doc(mdoc)
|
|
265
|
-
sig =
|
|
305
|
+
sig = _render_signature(func, mname)
|
|
266
306
|
|
|
267
307
|
out.append(_Entity(
|
|
268
308
|
fqn=f"{fqn_class}.{mname}",
|
|
@@ -478,6 +518,24 @@ def _score(entity: _Entity, tokens: list[str], raw_query: str) -> float:
|
|
|
478
518
|
for tok in tokens:
|
|
479
519
|
if tok in name_lower:
|
|
480
520
|
score += 0.5
|
|
521
|
+
# Class-qualified queries ("Frond.add_test", "ORM save"): score the owning
|
|
522
|
+
# class so the qualifier actually steers ranking instead of being dead weight.
|
|
523
|
+
parent_lower = (entity.parent_class or "").lower()
|
|
524
|
+
rq = raw_query.strip().lower()
|
|
525
|
+
if parent_lower:
|
|
526
|
+
# Exact "Class.method" intent — the strongest signal we have.
|
|
527
|
+
if rq == f"{parent_lower}.{name_lower}":
|
|
528
|
+
score += 6.0
|
|
529
|
+
for tok in tokens:
|
|
530
|
+
if tok == parent_lower:
|
|
531
|
+
score += 2.5
|
|
532
|
+
elif parent_lower.startswith(tok):
|
|
533
|
+
score += 1.0
|
|
534
|
+
# Any token that is a whole segment of the fqn (module / class / name).
|
|
535
|
+
fqn_segs = set(re.split(r"[.\s]+", entity.fqn.lower()))
|
|
536
|
+
for tok in tokens:
|
|
537
|
+
if tok in fqn_segs:
|
|
538
|
+
score += 1.0
|
|
481
539
|
if entity.source == "user":
|
|
482
540
|
score *= 1.2
|
|
483
541
|
return score
|
|
@@ -587,14 +645,40 @@ class Docs:
|
|
|
587
645
|
scored.sort(key=lambda p: (-p[1], p[0].fqn))
|
|
588
646
|
return [e.as_hit(s) for (e, s) in scored[:k]]
|
|
589
647
|
|
|
648
|
+
def _resolve_class(self, given: str) -> _Entity | None:
|
|
649
|
+
"""Resolve a class by exact FQN, public import path, or bare name.
|
|
650
|
+
|
|
651
|
+
Callers naturally use the documented import path (`tina4_python.database
|
|
652
|
+
.Database`) or a bare name (`Database`), but the index stores the deep
|
|
653
|
+
defining-module FQN (`tina4_python.database.connection.Database`). Match
|
|
654
|
+
exactly first, then by class name, disambiguating by requiring the given
|
|
655
|
+
dotted segments to appear in the stored FQN (framework + shortest wins)."""
|
|
656
|
+
classes = [
|
|
657
|
+
e for e in (self._framework + self._user) if e.kind == "class"
|
|
658
|
+
]
|
|
659
|
+
for e in classes: # 1. exact
|
|
660
|
+
if e.fqn == given:
|
|
661
|
+
return e
|
|
662
|
+
gsegs = [s for s in given.split(".") if s]
|
|
663
|
+
gname = gsegs[-1] if gsegs else given
|
|
664
|
+
cands = [e for e in classes if e.fqn.split(".")[-1] == gname]
|
|
665
|
+
if len(cands) == 1:
|
|
666
|
+
return cands[0]
|
|
667
|
+
if cands: # 2. disambiguate by segment subset
|
|
668
|
+
subset = [
|
|
669
|
+
e for e in cands
|
|
670
|
+
if all(s in e.fqn.split(".") for s in gsegs)
|
|
671
|
+
]
|
|
672
|
+
pool = subset or cands
|
|
673
|
+
pool.sort(key=lambda e: (e.source != "framework", len(e.fqn), e.fqn))
|
|
674
|
+
return pool[0]
|
|
675
|
+
return None
|
|
676
|
+
|
|
590
677
|
def class_spec(self, fqn: str) -> dict[str, Any] | None:
|
|
591
678
|
"""Full reflection of a class — `None` if not found."""
|
|
592
679
|
self._ensure_index()
|
|
593
680
|
all_entities = self._framework + self._user
|
|
594
|
-
klass =
|
|
595
|
-
(e for e in all_entities if e.kind == "class" and e.fqn == fqn),
|
|
596
|
-
None,
|
|
597
|
-
)
|
|
681
|
+
klass = self._resolve_class(fqn)
|
|
598
682
|
if klass is None:
|
|
599
683
|
return None
|
|
600
684
|
methods = [
|
|
@@ -610,7 +694,7 @@ class Docs:
|
|
|
610
694
|
"source": m.source,
|
|
611
695
|
}
|
|
612
696
|
for m in all_entities
|
|
613
|
-
if m.kind == "method" and m.class_fqn == fqn
|
|
697
|
+
if m.kind == "method" and m.class_fqn == klass.fqn
|
|
614
698
|
]
|
|
615
699
|
return {
|
|
616
700
|
"fqn": klass.fqn,
|
|
@@ -629,8 +713,10 @@ class Docs:
|
|
|
629
713
|
self, class_fqn: str, method_name: str,
|
|
630
714
|
) -> dict[str, Any] | None:
|
|
631
715
|
self._ensure_index()
|
|
716
|
+
klass = self._resolve_class(class_fqn)
|
|
717
|
+
resolved = klass.fqn if klass else class_fqn
|
|
632
718
|
for e in self._framework + self._user:
|
|
633
|
-
if e.kind == "method" and e.class_fqn ==
|
|
719
|
+
if e.kind == "method" and e.class_fqn == resolved and e.name == method_name:
|
|
634
720
|
return {
|
|
635
721
|
"fqn": e.fqn,
|
|
636
722
|
"name": e.name,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.28 → tina4_python-3.13.30}/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.28 → tina4_python-3.13.30}/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.28 → tina4_python-3.13.30}/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.28 → tina4_python-3.13.30}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/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.28 → tina4_python-3.13.30}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.28 → tina4_python-3.13.30}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|