tina4-python 3.12.5__tar.gz → 3.12.8__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.12.5 → tina4_python-3.12.8}/PKG-INFO +1 -1
- {tina4_python-3.12.5 → tina4_python-3.12.8}/pyproject.toml +1 -1
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/__init__.py +1 -1
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/core/router.py +93 -3
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/core/server.py +37 -1
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/database/postgres.py +26 -4
- {tina4_python-3.12.5 → tina4_python-3.12.8}/.gitignore +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/README.md +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/Testing.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/core/events.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/core/request.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/core/response.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/docs.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -246,6 +246,33 @@ class Router:
|
|
|
246
246
|
"""Register a DELETE route (imperative, non-decorator style)."""
|
|
247
247
|
return cls.add("DELETE", path, handler, middleware=middleware, swagger_meta=swagger_meta, template=template, **options)
|
|
248
248
|
|
|
249
|
+
@classmethod
|
|
250
|
+
def head(cls, path: str, handler, middleware: list = None, swagger_meta: dict = None, template: str = None, **options) -> "RouteRef":
|
|
251
|
+
"""Register an explicit HEAD route.
|
|
252
|
+
|
|
253
|
+
By default the framework auto-handles HEAD by falling back to the GET
|
|
254
|
+
route and stripping the body (RFC 9110 §9.3.2). Use this method only
|
|
255
|
+
when you need a HEAD handler that does something different from GET —
|
|
256
|
+
e.g. cheaper existence-check logic, custom validator headers without
|
|
257
|
+
the cost of building the body.
|
|
258
|
+
|
|
259
|
+
The framework still strips the response body for you on the way out —
|
|
260
|
+
HEAD MUST NOT return content, even if your handler does, so we
|
|
261
|
+
enforce that unconditionally rather than relying on developer care.
|
|
262
|
+
"""
|
|
263
|
+
return cls.add("HEAD", path, handler, middleware=middleware, swagger_meta=swagger_meta, template=template, **options)
|
|
264
|
+
|
|
265
|
+
@classmethod
|
|
266
|
+
def options(cls, path: str, handler, middleware: list = None, swagger_meta: dict = None, template: str = None, **options) -> "RouteRef":
|
|
267
|
+
"""Register an explicit OPTIONS route.
|
|
268
|
+
|
|
269
|
+
By default the framework auto-handles OPTIONS by building an Allow
|
|
270
|
+
header from every method registered for the path and returning 204
|
|
271
|
+
(RFC 9110 §9.3.7). Use this method to take over that behaviour —
|
|
272
|
+
e.g. to return a richer OPTIONS payload describing the resource.
|
|
273
|
+
"""
|
|
274
|
+
return cls.add("OPTIONS", path, handler, middleware=middleware, swagger_meta=swagger_meta, template=template, **options)
|
|
275
|
+
|
|
249
276
|
@classmethod
|
|
250
277
|
def any(cls, path: str, handler, middleware: list = None, swagger_meta: dict = None, template: str = None, **options) -> "RouteRef":
|
|
251
278
|
"""Register a route for any HTTP method (imperative, non-decorator style)."""
|
|
@@ -291,7 +318,11 @@ class Router:
|
|
|
291
318
|
# Route has custom middleware — developer handles auth themselves
|
|
292
319
|
auth_required = False
|
|
293
320
|
else:
|
|
294
|
-
|
|
321
|
+
# GET, HEAD, OPTIONS, and ANY are public by default. HEAD and
|
|
322
|
+
# OPTIONS are safe/idempotent introspection methods (RFC 9110
|
|
323
|
+
# §9.2.1) — requiring auth on them breaks cache validators
|
|
324
|
+
# and CORS preflight probes.
|
|
325
|
+
auth_required = m not in ("GET", "HEAD", "OPTIONS", "ANY")
|
|
295
326
|
|
|
296
327
|
route = {
|
|
297
328
|
"method": m,
|
|
@@ -312,9 +343,18 @@ class Router:
|
|
|
312
343
|
|
|
313
344
|
@staticmethod
|
|
314
345
|
def match(method: str, path: str) -> tuple[dict | None, dict]:
|
|
315
|
-
"""Find a route matching method + path. Returns (route, params).
|
|
346
|
+
"""Find a route matching method + path. Returns (route, params).
|
|
347
|
+
|
|
348
|
+
RFC 9110 §9.3.2: HEAD is identical to GET except the response carries
|
|
349
|
+
no body. If the app didn't register a dedicated HEAD route, we
|
|
350
|
+
transparently match the GET route; the dispatcher strips the body on
|
|
351
|
+
the way out, so the handler doesn't need to know HEAD even happened.
|
|
352
|
+
"""
|
|
353
|
+
method_upper = method.upper()
|
|
354
|
+
|
|
355
|
+
# First pass: exact method match (covers HEAD → explicit HEAD route too)
|
|
316
356
|
for route in _routes:
|
|
317
|
-
if route["method"] not in (
|
|
357
|
+
if route["method"] not in (method_upper, "ANY"):
|
|
318
358
|
continue
|
|
319
359
|
m = route["pattern"].match(path)
|
|
320
360
|
if m:
|
|
@@ -323,8 +363,58 @@ class Router:
|
|
|
323
363
|
params[name] = m.group(i + 1)
|
|
324
364
|
return route, params
|
|
325
365
|
|
|
366
|
+
# Second pass: HEAD auto-fallback to GET when no HEAD route registered
|
|
367
|
+
if method_upper == "HEAD":
|
|
368
|
+
for route in _routes:
|
|
369
|
+
if route["method"] not in ("GET", "ANY"):
|
|
370
|
+
continue
|
|
371
|
+
m = route["pattern"].match(path)
|
|
372
|
+
if m:
|
|
373
|
+
params = {}
|
|
374
|
+
for i, name in enumerate(route["param_names"]):
|
|
375
|
+
params[name] = m.group(i + 1)
|
|
376
|
+
return route, params
|
|
377
|
+
|
|
326
378
|
return None, {}
|
|
327
379
|
|
|
380
|
+
@staticmethod
|
|
381
|
+
def methods_allowed_for_path(path: str) -> list[str]:
|
|
382
|
+
"""Return the list of HTTP methods registered for ``path``, in the
|
|
383
|
+
order GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS. Used by
|
|
384
|
+
the dispatcher to build the ``Allow:`` header on 405 / OPTIONS
|
|
385
|
+
responses (RFC 9110 §10.2.1, §9.3.7).
|
|
386
|
+
|
|
387
|
+
If GET is registered, HEAD is appended implicitly (the framework
|
|
388
|
+
auto-falls-back HEAD to GET). OPTIONS is appended whenever the
|
|
389
|
+
path has any registered method (the framework auto-handles OPTIONS).
|
|
390
|
+
"""
|
|
391
|
+
# ANY routes count for every method but we don't enumerate them
|
|
392
|
+
# individually — flag whether ANY matched and union it with the
|
|
393
|
+
# concrete-method matches.
|
|
394
|
+
method_order = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS")
|
|
395
|
+
seen: set[str] = set()
|
|
396
|
+
any_matched = False
|
|
397
|
+
|
|
398
|
+
for route in _routes:
|
|
399
|
+
if not route["pattern"].match(path):
|
|
400
|
+
continue
|
|
401
|
+
m = route["method"]
|
|
402
|
+
if m == "ANY":
|
|
403
|
+
any_matched = True
|
|
404
|
+
elif m in method_order:
|
|
405
|
+
seen.add(m)
|
|
406
|
+
|
|
407
|
+
if any_matched:
|
|
408
|
+
seen.update(method_order)
|
|
409
|
+
|
|
410
|
+
# GET implies HEAD; any registered method implies OPTIONS.
|
|
411
|
+
if seen:
|
|
412
|
+
if "GET" in seen:
|
|
413
|
+
seen.add("HEAD")
|
|
414
|
+
seen.add("OPTIONS")
|
|
415
|
+
|
|
416
|
+
return [m for m in method_order if m in seen]
|
|
417
|
+
|
|
328
418
|
@staticmethod
|
|
329
419
|
def get_routes() -> list[dict]:
|
|
330
420
|
"""Return all registered routes."""
|
|
@@ -1338,7 +1338,43 @@ async def handle(request: Request) -> Response:
|
|
|
1338
1338
|
except Exception as e:
|
|
1339
1339
|
response = _handle_route_error(e, request, response, request_id, _is_dev)
|
|
1340
1340
|
else:
|
|
1341
|
-
|
|
1341
|
+
# RFC 9110 conformance — before falling through to 404 / static / template,
|
|
1342
|
+
# check whether the PATH is known to the router under any OTHER method.
|
|
1343
|
+
# If yes:
|
|
1344
|
+
# - OPTIONS request → 204 No Content with Allow listing the methods
|
|
1345
|
+
# (RFC 9110 §9.3.7). Generic OPTIONS handler.
|
|
1346
|
+
# - Any other method (PUT on GET-only route, TRACE, CONNECT, etc.)
|
|
1347
|
+
# → 405 Method Not Allowed with Allow header (§15.5.6 + §10.2.1).
|
|
1348
|
+
allowed = Router.methods_allowed_for_path(request.path)
|
|
1349
|
+
if allowed:
|
|
1350
|
+
allow_header = ", ".join(allowed)
|
|
1351
|
+
if request.method.upper() == "OPTIONS":
|
|
1352
|
+
response.header("Allow", allow_header)
|
|
1353
|
+
response.status(204)
|
|
1354
|
+
else:
|
|
1355
|
+
response.header("Allow", allow_header)
|
|
1356
|
+
response.status(405).json({
|
|
1357
|
+
"error": "Method Not Allowed",
|
|
1358
|
+
"path": request.path,
|
|
1359
|
+
"method": request.method,
|
|
1360
|
+
"allow": allowed,
|
|
1361
|
+
"status": 405,
|
|
1362
|
+
})
|
|
1363
|
+
else:
|
|
1364
|
+
response = _handle_no_route(request, response, request_id)
|
|
1365
|
+
|
|
1366
|
+
# RFC 9110 §9.3.2: a HEAD response MUST NOT include content. Strip the
|
|
1367
|
+
# body unconditionally (even for explicit Router.head() handlers that
|
|
1368
|
+
# accidentally returned one) and record what Content-Length the GET
|
|
1369
|
+
# would have sent — cache validators / link checkers / monitoring
|
|
1370
|
+
# probes rely on that header to size estimates.
|
|
1371
|
+
if request.method.upper() == "HEAD":
|
|
1372
|
+
body = response.content if response.content is not None else b""
|
|
1373
|
+
if isinstance(body, str):
|
|
1374
|
+
body = body.encode("utf-8")
|
|
1375
|
+
if body:
|
|
1376
|
+
response.header("Content-Length", str(len(body)))
|
|
1377
|
+
response.content = b""
|
|
1342
1378
|
|
|
1343
1379
|
return _finalize_response(request, response, route, request_id, _is_dev, _req_start)
|
|
1344
1380
|
|
|
@@ -20,6 +20,28 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
20
20
|
self._conn = None
|
|
21
21
|
self._in_transaction: bool = False
|
|
22
22
|
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _safe_execute(cursor, sql: str, params=None):
|
|
25
|
+
"""Wrapper for ``cursor.execute`` that side-steps psycopg2's
|
|
26
|
+
always-on ``%`` substitution when no params are needed.
|
|
27
|
+
|
|
28
|
+
Issue #40: psycopg2 interprets ``%`` characters in the SQL
|
|
29
|
+
as parameter placeholders WHENEVER the ``params`` argument is
|
|
30
|
+
supplied — even an empty list ``[]``. So a migration body
|
|
31
|
+
containing ``RAISE EXCEPTION 'thing % conflicts with %', a, b``
|
|
32
|
+
(perfectly valid PL/pgSQL) blows up with the misleading
|
|
33
|
+
``list index out of range`` because psycopg2 thinks ``%`` is a
|
|
34
|
+
placeholder and there are no values to substitute.
|
|
35
|
+
|
|
36
|
+
Pass ``None`` (or omit the second arg) and psycopg2 skips the
|
|
37
|
+
substitution pass entirely — literal ``%`` flows through
|
|
38
|
+
untouched. So we route empty/None params through that path.
|
|
39
|
+
"""
|
|
40
|
+
if params:
|
|
41
|
+
cursor.execute(sql, params)
|
|
42
|
+
else:
|
|
43
|
+
cursor.execute(sql)
|
|
44
|
+
|
|
23
45
|
def connect(self, connection_string: str, username: str = "", password: str = "", **kwargs):
|
|
24
46
|
"""Connect to PostgreSQL.
|
|
25
47
|
|
|
@@ -63,7 +85,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
63
85
|
)
|
|
64
86
|
|
|
65
87
|
cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
|
66
|
-
|
|
88
|
+
self._safe_execute(cursor, sql, params)
|
|
67
89
|
|
|
68
90
|
records = []
|
|
69
91
|
last_id = None
|
|
@@ -124,7 +146,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
124
146
|
# Count total rows
|
|
125
147
|
count_sql = f"SELECT COUNT(*) AS cnt FROM ({sql}) AS _count_subquery"
|
|
126
148
|
try:
|
|
127
|
-
|
|
149
|
+
self._safe_execute(cursor, count_sql, params)
|
|
128
150
|
total = cursor.fetchone()["cnt"]
|
|
129
151
|
except Exception:
|
|
130
152
|
total = 0
|
|
@@ -132,7 +154,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
132
154
|
# Apply pagination
|
|
133
155
|
paginated_sql = f"{sql} LIMIT %s OFFSET %s"
|
|
134
156
|
paginated_params = (params or []) + [limit, offset]
|
|
135
|
-
|
|
157
|
+
self._safe_execute(cursor, paginated_sql, paginated_params)
|
|
136
158
|
rows = [self._decode_blobs(dict(row)) for row in cursor.fetchall()]
|
|
137
159
|
|
|
138
160
|
return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
|
|
@@ -142,7 +164,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
142
164
|
|
|
143
165
|
sql = self._translate_sql(sql)
|
|
144
166
|
cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
|
145
|
-
|
|
167
|
+
self._safe_execute(cursor, sql, params)
|
|
146
168
|
row = cursor.fetchone()
|
|
147
169
|
return self._decode_blobs(dict(row)) if row else None
|
|
148
170
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.12.5 → tina4_python-3.12.8}/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
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/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.12.5 → tina4_python-3.12.8}/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.12.5 → tina4_python-3.12.8}/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
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.12.5 → tina4_python-3.12.8}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|