tina4-python 3.13.104__tar.gz → 3.13.106__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.104 → tina4_python-3.13.106}/PKG-INFO +1 -1
- {tina4_python-3.13.104 → tina4_python-3.13.106}/pyproject.toml +1 -1
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/__init__.py +6 -1
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/cli/__init__.py +2 -7
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/__init__.py +2 -2
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/router.py +28 -5
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/server.py +88 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/orm/model.py +17 -5
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue/__init__.py +42 -7
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue/lite_backend.py +16 -2
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue/mongo_backend.py +67 -9
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/websocket/__init__.py +33 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/.gitignore +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/README.md +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/ai/client.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/context/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/context/chunker.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/port_takeover.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/database_url.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/sql_translator.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/env.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/frond/compiler.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/frond/parser.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/mqtt/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/mqtt/message.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/orm/point.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue/amqp_url.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/realtime/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/realtime/models/Attachment.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/realtime/models/Channel.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/realtime/models/ChannelMember.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/realtime/models/Message.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/realtime/models/Workspace.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/realtime/models/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/realtime/storage.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/session_handlers/memcached_handler.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/sso.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -51,7 +51,7 @@ def _resolve_version() -> str:
|
|
|
51
51
|
#
|
|
52
52
|
# test_version_constant.py now asserts this literal equals the pyproject
|
|
53
53
|
# version, so the release bump cannot leave it behind again.
|
|
54
|
-
return "3.13.
|
|
54
|
+
return "3.13.106"
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
__version__ = _resolve_version()
|
|
@@ -74,6 +74,11 @@ from tina4_python.core.router import ( # noqa: E402, F401
|
|
|
74
74
|
noauth, secured, cached, middleware, template,
|
|
75
75
|
Router, RouteGroup,
|
|
76
76
|
)
|
|
77
|
+
# websocket is not in that tuple. The tina4_python.websocket *subpackage* is
|
|
78
|
+
# itself callable (see tina4_python/websocket/__init__.py) so
|
|
79
|
+
# `from tina4_python import websocket` reaches core.router.websocket without
|
|
80
|
+
# turning `import tina4_python.websocket as ws` into a function. A re-export
|
|
81
|
+
# here would do the latter.
|
|
77
82
|
from tina4_python.core.constants import ( # noqa: E402, F401
|
|
78
83
|
HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, HTTP_NO_CONTENT,
|
|
79
84
|
HTTP_MOVED, HTTP_REDIRECT, HTTP_NOT_MODIFIED,
|
|
@@ -825,13 +825,8 @@ def _seed(args):
|
|
|
825
825
|
|
|
826
826
|
def _routes(args):
|
|
827
827
|
"""List all registered routes."""
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
import importlib
|
|
831
|
-
importlib.import_module("app")
|
|
832
|
-
|
|
833
|
-
from tina4_python.core.router import Router
|
|
834
|
-
routes = Router.get_routes()
|
|
828
|
+
from tina4_python.core import discover_routes
|
|
829
|
+
routes = discover_routes("src/routes")
|
|
835
830
|
if not routes:
|
|
836
831
|
print("No routes registered.")
|
|
837
832
|
return
|
|
@@ -19,7 +19,7 @@ from tina4_python.core.router import (
|
|
|
19
19
|
from tina4_python.core.middleware import CorsMiddleware, RateLimiter
|
|
20
20
|
from tina4_python.core.cache import Cache
|
|
21
21
|
from tina4_python.core.events import on, off, emit, emit_async, once, listeners, events, clear as clear_events
|
|
22
|
-
from tina4_python.core.server import run, resolve_config, handle, start, stop
|
|
22
|
+
from tina4_python.core.server import discover_routes, run, resolve_config, handle, start, stop
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
25
|
"Request", "Response", "Router",
|
|
@@ -28,5 +28,5 @@ __all__ = [
|
|
|
28
28
|
"CorsMiddleware", "RateLimiter",
|
|
29
29
|
"Cache",
|
|
30
30
|
"on", "off", "emit", "emit_async", "once", "listeners", "events", "clear_events",
|
|
31
|
-
"run", "resolve_config", "handle", "start", "stop",
|
|
31
|
+
"discover_routes", "run", "resolve_config", "handle", "start", "stop",
|
|
32
32
|
]
|
|
@@ -817,9 +817,23 @@ def noauth():
|
|
|
817
817
|
def decorator(fn):
|
|
818
818
|
fn._noauth = True
|
|
819
819
|
# If route was already registered (decorator applied after @get/@post),
|
|
820
|
-
# update the route dict directly.
|
|
820
|
+
# update the route dict directly AND log the corrected state. The
|
|
821
|
+
# documented decorator order puts @noauth() ABOVE @get/@post, which
|
|
822
|
+
# means Python's bottom-up application runs @post first — so
|
|
823
|
+
# `_register_route` fires its "auth=required" line for a route that
|
|
824
|
+
# will be public a microsecond later. Without the corrective line
|
|
825
|
+
# below, the earlier log reads as definitive and misleads anyone
|
|
826
|
+
# reading startup output (issue #103: an AI reviewer drafted a false
|
|
827
|
+
# critical-security finding after reading the misleading line).
|
|
821
828
|
if hasattr(fn, "_route_ref"):
|
|
822
|
-
fn._route_ref._route
|
|
829
|
+
route = fn._route_ref._route
|
|
830
|
+
was_required = route.get("auth_required", False)
|
|
831
|
+
route["auth_required"] = False
|
|
832
|
+
if was_required:
|
|
833
|
+
Log.debug(
|
|
834
|
+
f"Route auth updated: {route['method']} {route['path']} "
|
|
835
|
+
f"(auth=public via @noauth)"
|
|
836
|
+
)
|
|
823
837
|
return fn
|
|
824
838
|
return decorator
|
|
825
839
|
|
|
@@ -828,10 +842,19 @@ def secured():
|
|
|
828
842
|
"""Require auth on a GET route (which is public by default)."""
|
|
829
843
|
def decorator(fn):
|
|
830
844
|
fn._secured = True
|
|
831
|
-
#
|
|
832
|
-
#
|
|
845
|
+
# See @noauth() above: emit a corrective log line if we flip the flag
|
|
846
|
+
# on an already-registered route so a reader of the startup log sees
|
|
847
|
+
# the true final auth state after every decorator has applied
|
|
848
|
+
# (issue #103).
|
|
833
849
|
if hasattr(fn, "_route_ref"):
|
|
834
|
-
fn._route_ref._route
|
|
850
|
+
route = fn._route_ref._route
|
|
851
|
+
was_public = not route.get("auth_required", True)
|
|
852
|
+
route["auth_required"] = True
|
|
853
|
+
if was_public:
|
|
854
|
+
Log.debug(
|
|
855
|
+
f"Route auth updated: {route['method']} {route['path']} "
|
|
856
|
+
f"(auth=required via @secured)"
|
|
857
|
+
)
|
|
835
858
|
# Same for a WebSocket route registered by @websocket() below this one.
|
|
836
859
|
if hasattr(fn, "_ws_route_ref"):
|
|
837
860
|
fn._ws_route_ref["auth_required"] = True
|
|
@@ -316,6 +316,79 @@ async def background_tick_loop(callback, interval: float, executor, shutdown):
|
|
|
316
316
|
_discovered_mtimes: dict[str, float] = {}
|
|
317
317
|
|
|
318
318
|
|
|
319
|
+
def _cascade_reload_dependents(reloaded: str, root_pkg: str, visited: set[str]) -> None:
|
|
320
|
+
"""After re-importing ``reloaded``, re-import any other in-scope module that
|
|
321
|
+
still holds a stale ``from <reloaded> import Foo`` reference.
|
|
322
|
+
|
|
323
|
+
Fixes https://github.com/tina4stack/tina4-python/issues/102 (hot reload
|
|
324
|
+
does not re-register ORM field metadata). Root cause: ``_auto_discover``
|
|
325
|
+
reloads the CHANGED module (say ``src.orm.Todo``) by ``del sys.modules[.]``
|
|
326
|
+
+ ``importlib.import_module(.)``, which builds a fresh class object. But a
|
|
327
|
+
route module that did ``from src.orm.Todo import Todo`` at ITS first import
|
|
328
|
+
captured a reference to the OLD class, and unless that module ALSO
|
|
329
|
+
re-executes, its ``Todo(...)`` call still uses the old class -- with the
|
|
330
|
+
old ``_fields`` -- so a newly added column is silently absent from
|
|
331
|
+
``to_dict()`` output. The DATA is written to the DB correctly (the ORM
|
|
332
|
+
knows the field via the new class), and the API just lies about it.
|
|
333
|
+
|
|
334
|
+
Detection: for every in-scope module already in ``sys.modules``, walk its
|
|
335
|
+
namespace and look for any attribute whose ``__module__`` matches the
|
|
336
|
+
reloaded module's name. That's the fingerprint of a ``from X import Y``
|
|
337
|
+
binding into an unchanged file. Re-import that file so its ``from`` binds
|
|
338
|
+
to the fresh class. Recurse (a dependent may itself be depended on).
|
|
339
|
+
|
|
340
|
+
Bounded to the discovery scope (``root_pkg`` and its subpackages) so we
|
|
341
|
+
never delete a framework or third-party module. ``visited`` breaks cycles
|
|
342
|
+
and prevents re-doing a module we already refreshed in this cascade.
|
|
343
|
+
"""
|
|
344
|
+
# Snapshot: mutating sys.modules during the walk would break iteration.
|
|
345
|
+
for other_name in list(sys.modules):
|
|
346
|
+
if other_name in visited:
|
|
347
|
+
continue
|
|
348
|
+
if other_name == reloaded:
|
|
349
|
+
continue
|
|
350
|
+
if other_name != root_pkg and not other_name.startswith(root_pkg + "."):
|
|
351
|
+
continue
|
|
352
|
+
other = sys.modules.get(other_name)
|
|
353
|
+
if other is None or not hasattr(other, "__dict__"):
|
|
354
|
+
continue
|
|
355
|
+
# Any attribute whose __module__ names the reloaded module is a
|
|
356
|
+
# ``from X import Y`` binding into this file -- it is stale.
|
|
357
|
+
try:
|
|
358
|
+
attrs = vars(other)
|
|
359
|
+
except Exception:
|
|
360
|
+
continue
|
|
361
|
+
stale = False
|
|
362
|
+
for attr_name, attr in attrs.items():
|
|
363
|
+
if attr_name.startswith("__"):
|
|
364
|
+
continue
|
|
365
|
+
if getattr(attr, "__module__", None) == reloaded:
|
|
366
|
+
stale = True
|
|
367
|
+
break
|
|
368
|
+
if not stale:
|
|
369
|
+
continue
|
|
370
|
+
try:
|
|
371
|
+
from tina4_python.core.router import Router
|
|
372
|
+
Router.remove_routes_for_module(other_name)
|
|
373
|
+
del sys.modules[other_name]
|
|
374
|
+
importlib.import_module(other_name)
|
|
375
|
+
# Record so the next mtime tick sees this reload as authoritative.
|
|
376
|
+
try:
|
|
377
|
+
other_path = getattr(sys.modules[other_name], "__file__", None)
|
|
378
|
+
if other_path:
|
|
379
|
+
_discovered_mtimes[other_name] = Path(other_path).stat().st_mtime
|
|
380
|
+
except OSError:
|
|
381
|
+
pass
|
|
382
|
+
visited.add(other_name)
|
|
383
|
+
Log.info(f"Cascaded reload of dependent module: {other_name}")
|
|
384
|
+
# A module that imported from `reloaded` may itself be imported
|
|
385
|
+
# from by yet another module. Cascade the same detection on THIS
|
|
386
|
+
# module now that its class objects are fresh.
|
|
387
|
+
_cascade_reload_dependents(other_name, root_pkg, visited)
|
|
388
|
+
except Exception as exc:
|
|
389
|
+
Log.error(f"Cascade reload failed for {other_name}: {exc}")
|
|
390
|
+
|
|
391
|
+
|
|
319
392
|
def _auto_discover(root_dir: str = "src"):
|
|
320
393
|
"""Auto-import all .py files in ``root_dir`` to trigger route decorators.
|
|
321
394
|
|
|
@@ -444,6 +517,14 @@ def _auto_discover(root_dir: str = "src"):
|
|
|
444
517
|
f"Reloaded changed module: {module_name}"
|
|
445
518
|
+ (f" (dropped {dropped} stale route(s))" if dropped else "")
|
|
446
519
|
)
|
|
520
|
+
# Cascade to any in-scope module that captured a
|
|
521
|
+
# ``from <module_name> import Foo`` binding at ITS first
|
|
522
|
+
# import -- that binding still points at the OLD class
|
|
523
|
+
# object we just replaced, so the dependent silently uses
|
|
524
|
+
# stale field metadata / stale globals until it too
|
|
525
|
+
# re-executes. See _cascade_reload_dependents above and
|
|
526
|
+
# issue #102.
|
|
527
|
+
_cascade_reload_dependents(module_name, root_pkg, {module_name})
|
|
447
528
|
else:
|
|
448
529
|
# Out-of-scope module changed — record mtime so we don't
|
|
449
530
|
# keep re-evaluating it, but do not re-import it.
|
|
@@ -468,6 +549,13 @@ def _auto_discover(root_dir: str = "src"):
|
|
|
468
549
|
pass
|
|
469
550
|
|
|
470
551
|
|
|
552
|
+
def discover_routes(root_dir: str = "src/routes"):
|
|
553
|
+
"""Discover application routes without booting the application server."""
|
|
554
|
+
_auto_discover(root_dir)
|
|
555
|
+
from tina4_python.core.router import Router
|
|
556
|
+
return Router.get_routes()
|
|
557
|
+
|
|
558
|
+
|
|
471
559
|
def _record_broken_import(py_file: Path, error: Exception) -> None:
|
|
472
560
|
"""Write a .broken sentinel so /health and the dev dashboard surface
|
|
473
561
|
auto-discover failures instead of burying them in the console."""
|
|
@@ -1363,12 +1363,24 @@ class ORM(metaclass=ORMMeta):
|
|
|
1363
1363
|
def clear_cache(cls) -> None:
|
|
1364
1364
|
"""Invalidate every cached query that touches this model's table.
|
|
1365
1365
|
|
|
1366
|
-
Tag-scoped
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1366
|
+
Tag-scoped in the ORM layer (a cached JOIN on another model that reads
|
|
1367
|
+
this table is busted too because it carries this table's tag; a query
|
|
1368
|
+
that never touches this table is left intact), then cascaded to the
|
|
1369
|
+
DB layer on this model's bound connection so an out-of-band write /
|
|
1370
|
+
deliberate refresh / race-with-another-process cannot leave stale rows
|
|
1371
|
+
in db.fetch()'s persistent cache. Called after every ORM write
|
|
1372
|
+
(save/delete/force_delete/restore) so a read-after-write never serves
|
|
1373
|
+
a stale/deleted row (CACHE-DEC-01). PY-06-22 (3.13.105) added the
|
|
1374
|
+
DB-layer cascade -- previously the two cache layers disagreed
|
|
1375
|
+
under TINA4_AUTO_CACHING=true + TINA4_DB_CACHE=true."""
|
|
1371
1376
|
_query_cache.clear_tag(cls._get_table().lower())
|
|
1377
|
+
try:
|
|
1378
|
+
cls._get_db().cache_clear()
|
|
1379
|
+
except Exception:
|
|
1380
|
+
# A resolvable DB is not guaranteed at every clear_cache call
|
|
1381
|
+
# site (module-import time in odd bootstraps, tests that mutate
|
|
1382
|
+
# bindings); never let a cache-clear crash a save/delete.
|
|
1383
|
+
pass
|
|
1372
1384
|
|
|
1373
1385
|
@classmethod
|
|
1374
1386
|
def clear_rel_cache(cls) -> None:
|
|
@@ -182,7 +182,20 @@ class Queue:
|
|
|
182
182
|
processed += 1
|
|
183
183
|
|
|
184
184
|
def size(self, status: str = "pending") -> int:
|
|
185
|
-
"""Count jobs by status.
|
|
185
|
+
"""Count jobs by status.
|
|
186
|
+
|
|
187
|
+
``"pending"`` counts jobs waiting to be popped -- INCLUDES retryable-
|
|
188
|
+
but-attempted ones, because they live in the pending queue under the
|
|
189
|
+
auto-retry lifecycle (see failed()).
|
|
190
|
+
``"reserved"`` counts jobs a consumer has popped but not yet
|
|
191
|
+
completed/failed (in-flight against the visibility timeout).
|
|
192
|
+
``"completed"`` counts jobs the consumer has finished successfully.
|
|
193
|
+
``"failed"``, ``"dead"``, ``"dead_letter"`` are ALIASES that all count
|
|
194
|
+
the dead-letter store -- jobs whose attempts >= max_retries and that
|
|
195
|
+
have given up. Use dead_letters() to list them. Retryable-but-
|
|
196
|
+
attempted jobs are NOT counted by size("failed"); use failed() to
|
|
197
|
+
list them or size("pending") to include them in a total.
|
|
198
|
+
"""
|
|
186
199
|
return self._backend.size(status)
|
|
187
200
|
|
|
188
201
|
def purge(self, status: str, max_retries: int = None) -> int:
|
|
@@ -194,14 +207,27 @@ class Queue:
|
|
|
194
207
|
return self._backend.retry_failed(max_retries=max_retries if max_retries is not None else self.max_retries)
|
|
195
208
|
|
|
196
209
|
def failed(self) -> list[dict]:
|
|
197
|
-
"""Get jobs that failed but are still
|
|
210
|
+
"""Get jobs that failed at least once but are still being retried
|
|
211
|
+
(0 < attempts < max_retries). These live in the pending queue under
|
|
212
|
+
the auto-retry lifecycle (fail() re-queues them with an incremented
|
|
213
|
+
attempts count and a retry_backoff delay) so pop() picks them up
|
|
214
|
+
again. They are NOT counted by size("failed") -- that alias counts
|
|
215
|
+
the dead-letter store, matching dead_letters(). To include retryable-
|
|
216
|
+
failed jobs in a total, use size("pending"). Terminal failures are
|
|
217
|
+
returned by dead_letters()."""
|
|
198
218
|
return self._backend.failed()
|
|
199
219
|
|
|
200
220
|
def dead_letters(self, max_retries: int = None) -> list[Job]:
|
|
201
|
-
"""Get jobs that exceeded
|
|
221
|
+
"""Get jobs that exceeded max_retries -- terminal failures.
|
|
222
|
+
|
|
223
|
+
Same set counted by size("failed") / size("dead") / size("dead_letter")
|
|
224
|
+
(three aliases for the dead-letter store). To LIST retryable-but-
|
|
225
|
+
attempted jobs (attempts > 0 AND attempts < max_retries) that are
|
|
226
|
+
still being auto-retried, use failed() -- those live in the pending
|
|
227
|
+
queue and are NOT dead letters.
|
|
202
228
|
|
|
203
229
|
Returns ``Job`` objects with the failure reason on ``.error``,
|
|
204
|
-
not raw dicts
|
|
230
|
+
not raw dicts -- so callers can iterate uniformly with the rest
|
|
205
231
|
of the queue API (``for job in queue.consume()``).
|
|
206
232
|
|
|
207
233
|
for job in queue.dead_letters():
|
|
@@ -228,13 +254,22 @@ class Queue:
|
|
|
228
254
|
]
|
|
229
255
|
|
|
230
256
|
def retry(self, job_id: str = None, delay_seconds: int = 0) -> bool:
|
|
231
|
-
"""Retry a failed job by ID, or all dead-letter jobs if no ID given.
|
|
257
|
+
"""Retry a failed job by ID, or all dead-letter jobs if no ID given.
|
|
258
|
+
|
|
259
|
+
The no-arg branch materialises every retry_job() call before returning
|
|
260
|
+
so ALL dead letters are revived. Pre-3.13.105 (PY-12-04) this branch
|
|
261
|
+
used ``any(retry_job(j.id) for j in dead)`` -- Python's short-circuit
|
|
262
|
+
``any`` stopped iterating as soon as the FIRST job revived, silently
|
|
263
|
+
leaving the rest in the dead-letter store.
|
|
264
|
+
"""
|
|
232
265
|
if job_id is None:
|
|
233
266
|
dead = self.dead_letters()
|
|
234
267
|
if not dead:
|
|
235
268
|
return False
|
|
236
|
-
#
|
|
237
|
-
|
|
269
|
+
# Materialise: iterate EVERY dead letter, then reduce -- never a
|
|
270
|
+
# generator inside any().
|
|
271
|
+
results = [self._backend.retry_job(j.id, delay_seconds) for j in dead]
|
|
272
|
+
return any(results)
|
|
238
273
|
return self._backend.retry_job(job_id, delay_seconds)
|
|
239
274
|
|
|
240
275
|
def clear(self) -> int:
|
|
@@ -547,9 +547,23 @@ class LiteBackend:
|
|
|
547
547
|
def retry(self, job: Job, delay_seconds: int = 0):
|
|
548
548
|
"""Explicit re-queue requested by the caller (job.retry()).
|
|
549
549
|
|
|
550
|
-
Always re-enqueues regardless of the retry limit
|
|
551
|
-
override, distinct from the automatic fail() path.
|
|
550
|
+
Always re-enqueues regardless of the retry limit -- this is a manual
|
|
551
|
+
override, distinct from the automatic fail() path. Cleans up BOTH the
|
|
552
|
+
reservation record and any dead-letter file for this id, so a caller
|
|
553
|
+
who iterates dead_letters() and calls .retry() on each doesn't leave
|
|
554
|
+
the dead-letter directory carrying duplicates (PY-12-05, 3.13.105).
|
|
555
|
+
Aligns with retry_job(id) which had always unlinked the dead-letter
|
|
556
|
+
file -- two spellings of the same intent that previously diverged.
|
|
552
557
|
"""
|
|
553
558
|
self._clear_reservation(job.id)
|
|
559
|
+
# Drop any dead-letter file for this id BEFORE the re-queue -- if this
|
|
560
|
+
# job came from dead_letters() it lives in _failed_dir() and would
|
|
561
|
+
# otherwise stay on disk while a fresh pending file appears in
|
|
562
|
+
# _queue_dir(), so the next dead_letters() call reports the job again
|
|
563
|
+
# and a consumer processes it twice.
|
|
564
|
+
try:
|
|
565
|
+
os.unlink(os.path.join(self._failed_dir(), f"{job.id}.queue-data"))
|
|
566
|
+
except FileNotFoundError:
|
|
567
|
+
pass
|
|
554
568
|
job.attempts += 1
|
|
555
569
|
self._requeue(job, delay_seconds=delay_seconds, error=None)
|
|
@@ -93,9 +93,26 @@ class MongoBackend:
|
|
|
93
93
|
return 0
|
|
94
94
|
return self._backend.size(self._topic)
|
|
95
95
|
|
|
96
|
-
def purge(self, status: str = "completed"):
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
def purge(self, status: str = "completed") -> int:
|
|
97
|
+
"""Delete every doc in this queue whose status matches.
|
|
98
|
+
|
|
99
|
+
Pre-3.13.105 (PY-12-08) this returned None (not a count), swallowed
|
|
100
|
+
every status other than ``pending``, and even for ``pending`` called
|
|
101
|
+
``clear()`` which delete_many({"topic": topic}) -- nuking ALL statuses
|
|
102
|
+
under that topic, not just pending. Now every status is honoured, the
|
|
103
|
+
dead-letter states route to the ``.dead_letter`` topic namespace, and
|
|
104
|
+
every path returns ``deleted_count`` so a caller can log/assert what
|
|
105
|
+
was purged.
|
|
106
|
+
"""
|
|
107
|
+
self._backend._ensure_connected()
|
|
108
|
+
if status in ("dead", "dead_letter", "failed"):
|
|
109
|
+
dl_topic = f"{self._topic}.dead_letter"
|
|
110
|
+
result = self._backend._collection.delete_many({"topic": dl_topic})
|
|
111
|
+
else:
|
|
112
|
+
result = self._backend._collection.delete_many(
|
|
113
|
+
{"topic": self._topic, "status": status}
|
|
114
|
+
)
|
|
115
|
+
return result.deleted_count
|
|
99
116
|
|
|
100
117
|
def retry_failed(self, max_retries: int = None) -> int:
|
|
101
118
|
# Accept max_retries to match the LiteBackend contract (Queue passes it
|
|
@@ -154,16 +171,57 @@ class MongoBackend:
|
|
|
154
171
|
for d in docs]
|
|
155
172
|
|
|
156
173
|
def retry_job(self, job_id: str, delay_seconds: int = 0) -> bool:
|
|
157
|
-
"""
|
|
174
|
+
"""Revive a specific dead-letter job by ID back to the pending queue.
|
|
175
|
+
|
|
176
|
+
Pre-3.13.105 this searched
|
|
177
|
+
``{_id: job_id, topic: self._topic, status: "failed"}`` -- three
|
|
178
|
+
separate reasons that filter could never match a dead letter:
|
|
179
|
+
|
|
180
|
+
* ``dead_letter()`` writes a NEW doc with a fresh ``_id`` (the
|
|
181
|
+
original job id is preserved on the doc's ``data.id`` field), so
|
|
182
|
+
``_id: job_id`` never hit.
|
|
183
|
+
* The dead-letter topic is ``f"{topic}.dead_letter"``, not
|
|
184
|
+
``self._topic``.
|
|
185
|
+
* Dead letters carry ``status="dead"`` (the original doc under
|
|
186
|
+
``self._topic`` is ``status="completed"`` after the ack).
|
|
187
|
+
|
|
188
|
+
The corrected flow looks the doc up in the dead-letter namespace by
|
|
189
|
+
the original id, deletes the dead-letter record, and re-enqueues the
|
|
190
|
+
original as pending -- upserting on the small chance the original
|
|
191
|
+
completed doc has been purged, so a retry after a housekeeping run
|
|
192
|
+
still works. Mirrors LiteBackend.retry_job() and matches
|
|
193
|
+
Queue.retry(id) callers' expectation that retry always revives if a
|
|
194
|
+
dead letter exists for the id.
|
|
195
|
+
"""
|
|
158
196
|
self._backend._ensure_connected()
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{"
|
|
197
|
+
dl_topic = f"{self._topic}.dead_letter"
|
|
198
|
+
dl_doc = self._backend._collection.find_one(
|
|
199
|
+
{"topic": dl_topic, "data.id": job_id}
|
|
200
|
+
)
|
|
201
|
+
if dl_doc is None:
|
|
202
|
+
return False
|
|
203
|
+
# Drop the dead-letter doc first so an interrupted retry never leaves
|
|
204
|
+
# both a dead-letter and a fresh pending doc for the same id.
|
|
205
|
+
self._backend._collection.delete_one({"_id": dl_doc["_id"]})
|
|
206
|
+
available = _now() if delay_seconds <= 0 else _future(delay_seconds)
|
|
207
|
+
# dead_letter() stored the ORIGINAL enqueue message under the doc's
|
|
208
|
+
# ``data`` field (see queue_backends/mongo_backend.py::dead_letter);
|
|
209
|
+
# its ``payload`` key is the enqueue payload the consumer needs.
|
|
210
|
+
original = dl_doc.get("data") if isinstance(dl_doc.get("data"), dict) else {}
|
|
211
|
+
payload = original.get("payload", original.get("data", {}))
|
|
212
|
+
priority = original.get("priority", dl_doc.get("priority", 0))
|
|
213
|
+
# Upsert: reset the original doc if it still exists, otherwise
|
|
214
|
+
# re-create it from the dead-letter payload so a purged/gone original
|
|
215
|
+
# is not silently a retry-no-op.
|
|
216
|
+
self._backend._collection.update_one(
|
|
217
|
+
{"_id": job_id, "topic": self._topic},
|
|
162
218
|
{"$set": {"status": "pending", "error": None,
|
|
163
|
-
"available_at": available
|
|
219
|
+
"available_at": available, "reserved_at": None,
|
|
220
|
+
"priority": priority, "payload": payload},
|
|
164
221
|
"$inc": {"attempts": 1}},
|
|
222
|
+
upsert=True,
|
|
165
223
|
)
|
|
166
|
-
return
|
|
224
|
+
return True
|
|
167
225
|
|
|
168
226
|
def clear(self) -> int:
|
|
169
227
|
# The only one of this adapter's six _collection users that forgot to
|
|
@@ -22,6 +22,8 @@ import os
|
|
|
22
22
|
import uuid
|
|
23
23
|
import time
|
|
24
24
|
from typing import Callable
|
|
25
|
+
import sys as _sys
|
|
26
|
+
from types import ModuleType as _ModuleType
|
|
25
27
|
|
|
26
28
|
MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
27
29
|
|
|
@@ -961,3 +963,34 @@ __all__ = [
|
|
|
961
963
|
"OP_TEXT", "OP_BINARY", "OP_CLOSE", "OP_PING", "OP_PONG",
|
|
962
964
|
"CLOSE_NORMAL", "CLOSE_GOING_AWAY", "CLOSE_PROTOCOL_ERROR", "CLOSE_TOO_LARGE",
|
|
963
965
|
]
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
# ── `@websocket("/path")` on the package name ───────────────────────────────
|
|
969
|
+
# `tina4_python.websocket` is two things at once: this RFC 6455 subpackage,
|
|
970
|
+
# and — to every reader of the docs — the route decorator beside @get/@post.
|
|
971
|
+
# Python binds a submodule onto its parent as soon as the submodule loads, so
|
|
972
|
+
#
|
|
973
|
+
# from tina4_python import websocket
|
|
974
|
+
# @websocket("/ws") # TypeError: 'module' object is not callable
|
|
975
|
+
#
|
|
976
|
+
# died at decorate time and auto-discovery dropped every route from that line
|
|
977
|
+
# onward.
|
|
978
|
+
#
|
|
979
|
+
# A re-export in tina4_python/__init__.py AFTER core.server has already loaded
|
|
980
|
+
# this subpackage is stable (importlib setattr happens on first load only).
|
|
981
|
+
# That still fails the other public surface: `import tina4_python.websocket as
|
|
982
|
+
# ws` would then return the function, so `ws.WebSocketServer` breaks. Making
|
|
983
|
+
# this module callable keeps both: the package name decorates, and the
|
|
984
|
+
# subpackage stays a module. Forward *args/**kwargs so this spelling cannot
|
|
985
|
+
# drift from core.router.websocket. Guard sys.modules — a loader that execs
|
|
986
|
+
# this file without registering it must not KeyError.
|
|
987
|
+
class _CallableWebSocketModule(_ModuleType):
|
|
988
|
+
def __call__(self, *args, **kwargs):
|
|
989
|
+
from tina4_python.core.router import websocket as _websocket_route
|
|
990
|
+
|
|
991
|
+
return _websocket_route(*args, **kwargs)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
_self = _sys.modules.get(__name__)
|
|
995
|
+
if _self is not None:
|
|
996
|
+
_self.__class__ = _CallableWebSocketModule
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.104 → tina4_python-3.13.106}/tina4_python/gallery/error-overlay/meta.json
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
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/public/js/tina4-dev-admin.min.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/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
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue_backends/kafka_backend.py
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue_backends/mongo_backend.py
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/realtime/models/ChannelMember.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
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/session_handlers/memcached_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/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.104 → tina4_python-3.13.106}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.104 → tina4_python-3.13.106}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|