tina4-python 3.13.41__tar.gz → 3.13.43__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.41 → tina4_python-3.13.43}/PKG-INFO +1 -1
- {tina4_python-3.13.41 → tina4_python-3.13.43}/pyproject.toml +1 -1
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/CLAUDE.md +31 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/queue/__init__.py +31 -8
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/queue/kafka_backend.py +18 -8
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/queue/mongo_backend.py +20 -7
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/queue/rabbitmq_backend.py +18 -8
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/queue_backends/mongo_backend.py +23 -1
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/swagger/__init__.py +260 -12
- {tina4_python-3.13.41 → tina4_python-3.13.43}/.gitignore +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/README.md +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/env.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.41 → tina4_python-3.13.43}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -1657,6 +1657,37 @@ TINA4_SWAGGER_LICENSE= # SPDX license name (e.g. MIT) for info.licens
|
|
|
1657
1657
|
TINA4_SWAGGER_SERVERS= # comma-separated server URLs for the OpenAPI servers[] block; falls back to SWAGGER_DEV_URL
|
|
1658
1658
|
TINA4_SWAGGER_UI_CDN= # base URL for the Swagger UI assets (default jsdelivr); point at a self-hosted mirror for air-gapped use
|
|
1659
1659
|
SWAGGER_DEV_URL=http://localhost:7145 # single dev-server URL (used when TINA4_SWAGGER_SERVERS is unset)
|
|
1660
|
+
TINA4_SWAGGER_OPENAPI=3.0.3 # OpenAPI version: 3.0.3 (default) or 3.1 (-> emits 3.1.0)
|
|
1661
|
+
TINA4_SWAGGER_BEARER_FORMAT=JWT # bearerFormat on the built-in bearerAuth scheme (e.g. opaque for sk_live_ keys)
|
|
1662
|
+
TINA4_SWAGGER_API_KEY_NAME= # if set, emit an apiKeyAuth scheme with this header/query name (e.g. X-Api-Key)
|
|
1663
|
+
TINA4_SWAGGER_API_KEY_IN=header # where the apiKey lives: header (default) | query | cookie
|
|
1664
|
+
TINA4_SWAGGER_DEFAULT_SCHEME=bearerAuth # scheme secured routes use when no @security is set
|
|
1665
|
+
TINA4_SWAGGER_INCLUDE= # comma-separated path prefixes to include (allow-list; only these documented)
|
|
1666
|
+
TINA4_SWAGGER_EXCLUDE= # comma-separated path prefixes to drop (/swagger + /__dev are always excluded)
|
|
1667
|
+
```
|
|
1668
|
+
|
|
1669
|
+
**Per-route security + reusable schemas (v3.13.42).** Configure named security
|
|
1670
|
+
schemes (configurable `bearerFormat`, an optional `apiKey` scheme, or register
|
|
1671
|
+
arbitrary schemes incl. `oauth2` with scopes via `Swagger.add_security_scheme(name, def)`),
|
|
1672
|
+
then declare them per route with `@security`:
|
|
1673
|
+
|
|
1674
|
+
```python
|
|
1675
|
+
from tina4_python.swagger import security, request_schema, response_schema, Swagger
|
|
1676
|
+
|
|
1677
|
+
@security("oauth2", scopes=["read:users"]) # scopes kept only for oauth2/openIdConnect
|
|
1678
|
+
@get("/api/v1/users")
|
|
1679
|
+
async def list_users(request, response): ...
|
|
1680
|
+
|
|
1681
|
+
@security("public") # explicitly public (overrides write-secure-by-default)
|
|
1682
|
+
@post("/api/v1/webhook")
|
|
1683
|
+
async def webhook(request, response): ...
|
|
1684
|
+
|
|
1685
|
+
# Reusable component schemas referenced by $ref (beyond ORM-model auto-schemas):
|
|
1686
|
+
Swagger.add_schema("CreateUser", {"type": "object", "properties": {"email": {"type": "string"}}})
|
|
1687
|
+
@request_schema("CreateUser")
|
|
1688
|
+
@response_schema("User", status=200)
|
|
1689
|
+
@post("/api/v1/users")
|
|
1690
|
+
async def create_user(request, response): ...
|
|
1660
1691
|
```
|
|
1661
1692
|
|
|
1662
1693
|
The spec is OpenAPI 3.0.3. ORM models registered via AutoCrud become reusable
|
|
@@ -61,7 +61,7 @@ def _resolve_backend(topic: str, backend: str | None, max_retries: int,
|
|
|
61
61
|
# Consumer-group offsets manage redelivery — framework timeout N/A.
|
|
62
62
|
return KafkaBackend(topic, max_retries)
|
|
63
63
|
elif chosen in ("mongodb", "mongo"):
|
|
64
|
-
return MongoBackend(topic, max_retries, visibility_timeout)
|
|
64
|
+
return MongoBackend(topic, max_retries, visibility_timeout, retry_backoff)
|
|
65
65
|
else:
|
|
66
66
|
raise ValueError(f"Unknown queue backend: {chosen!r}. Use 'file', 'rabbitmq', 'kafka', or 'mongodb'.")
|
|
67
67
|
|
|
@@ -82,9 +82,12 @@ class Queue:
|
|
|
82
82
|
visibility_timeout: float | None = None):
|
|
83
83
|
self.topic = topic
|
|
84
84
|
self.max_retries = max_retries
|
|
85
|
-
# Seconds to wait before a failed job is re-attempted
|
|
85
|
+
# Seconds to wait before a failed job is re-attempted.
|
|
86
86
|
# Default 0 = retry on the very next pop()/consume() iteration.
|
|
87
87
|
self.retry_backoff = retry_backoff
|
|
88
|
+
# Remember an explicit backend= so retargeting a topic (produce/consume)
|
|
89
|
+
# does not silently fall back to the TINA4_QUEUE_BACKEND env default.
|
|
90
|
+
self._backend_choice = backend
|
|
88
91
|
# Reservation/visibility timeout (seconds). A popped job is reserved for
|
|
89
92
|
# this long; if the consumer dies before complete()/fail() the next
|
|
90
93
|
# pop() reclaims it (at-least-once delivery). Falls back to
|
|
@@ -96,6 +99,24 @@ class Queue:
|
|
|
96
99
|
self._backend = _resolve_backend(topic, backend, max_retries, retry_backoff,
|
|
97
100
|
self.visibility_timeout)
|
|
98
101
|
|
|
102
|
+
def _retarget(self, topic: str) -> None:
|
|
103
|
+
"""Point this queue (and its backend) at ``topic`` in place.
|
|
104
|
+
|
|
105
|
+
produce()/consume()/process() call this so a topic argument actually
|
|
106
|
+
changes which topic is read or written. Without it the argument was
|
|
107
|
+
accepted but ignored on the read path — pop() always used the
|
|
108
|
+
construction-time topic, so ``consume("other")`` silently drained the
|
|
109
|
+
wrong queue. Reuses the explicit backend choice (``_backend_choice``)
|
|
110
|
+
so an explicit ``backend=`` is preserved across the switch.
|
|
111
|
+
"""
|
|
112
|
+
if topic == self.topic:
|
|
113
|
+
return
|
|
114
|
+
self.topic = topic
|
|
115
|
+
self._backend = _resolve_backend(
|
|
116
|
+
topic, self._backend_choice, self.max_retries,
|
|
117
|
+
self.retry_backoff, self.visibility_timeout,
|
|
118
|
+
)
|
|
119
|
+
|
|
99
120
|
def push(self, data: dict, priority: int = 0, delay_seconds: int = 0):
|
|
100
121
|
"""Add a job to the queue. Returns job ID."""
|
|
101
122
|
return self._backend.push(data, priority, delay_seconds)
|
|
@@ -134,6 +155,8 @@ class Queue:
|
|
|
134
155
|
batch_size: Number of jobs to pass to handler at once (default 1).
|
|
135
156
|
When > 1, handler receives a list of Jobs.
|
|
136
157
|
"""
|
|
158
|
+
if topic is not None:
|
|
159
|
+
self._retarget(topic)
|
|
137
160
|
processed = 0
|
|
138
161
|
while max_jobs is None or processed < max_jobs:
|
|
139
162
|
if batch_size > 1:
|
|
@@ -235,15 +258,11 @@ class Queue:
|
|
|
235
258
|
delay_seconds = max(0, offset)
|
|
236
259
|
|
|
237
260
|
old_topic = self.topic
|
|
238
|
-
self.topic
|
|
239
|
-
self._backend = _resolve_backend(topic, None, self.max_retries, self.retry_backoff,
|
|
240
|
-
self.visibility_timeout)
|
|
261
|
+
self._retarget(topic)
|
|
241
262
|
try:
|
|
242
263
|
return self.push(data, priority, delay_seconds)
|
|
243
264
|
finally:
|
|
244
|
-
self.
|
|
245
|
-
self._backend = _resolve_backend(old_topic, None, self.max_retries, self.retry_backoff,
|
|
246
|
-
self.visibility_timeout)
|
|
265
|
+
self._retarget(old_topic)
|
|
247
266
|
|
|
248
267
|
def consume(self, topic: str = None, job_id: str = None, poll_interval: float = 1.0,
|
|
249
268
|
iterations: int = 0, batch_size: int = 1):
|
|
@@ -282,6 +301,10 @@ class Queue:
|
|
|
282
301
|
import time
|
|
283
302
|
|
|
284
303
|
topic = topic or self.topic
|
|
304
|
+
# Honor the topic argument: point the queue (and the backend that
|
|
305
|
+
# pop()/job.complete()/job.fail() route through) at it. Previously the
|
|
306
|
+
# argument was ignored and consume() drained the construction-time topic.
|
|
307
|
+
self._retarget(topic)
|
|
285
308
|
|
|
286
309
|
if job_id is not None:
|
|
287
310
|
# Consume a specific job by ID — single yield, no polling
|
|
@@ -54,16 +54,21 @@ class KafkaBackend:
|
|
|
54
54
|
def purge(self, status: str = "completed"):
|
|
55
55
|
pass # Kafka does not support purging
|
|
56
56
|
|
|
57
|
-
def retry_failed(self) -> int:
|
|
58
|
-
jobs = self.failed()
|
|
57
|
+
def retry_failed(self, max_retries: int = None) -> int:
|
|
58
|
+
jobs = self.failed(max_retries)
|
|
59
59
|
count = 0
|
|
60
60
|
for job in jobs:
|
|
61
61
|
if self.retry_job(job.get("id", "")):
|
|
62
62
|
count += 1
|
|
63
63
|
return count
|
|
64
64
|
|
|
65
|
-
def failed(self) -> list[dict]:
|
|
66
|
-
"""Consume dead_letter topic, republish, return jobs under max_retries.
|
|
65
|
+
def failed(self, max_retries: int = None) -> list[dict]:
|
|
66
|
+
"""Consume dead_letter topic, republish, return jobs under max_retries.
|
|
67
|
+
|
|
68
|
+
Accepts max_retries to match the LiteBackend contract — Queue.retry_failed()
|
|
69
|
+
passes it as a kwarg, so without this signature the call raised TypeError.
|
|
70
|
+
"""
|
|
71
|
+
mr = max_retries if max_retries is not None else self._max_retries
|
|
67
72
|
dl_topic = f"{self._topic}.dead_letter"
|
|
68
73
|
results = []
|
|
69
74
|
requeue = []
|
|
@@ -73,7 +78,7 @@ class KafkaBackend:
|
|
|
73
78
|
break
|
|
74
79
|
payload = msg.get("payload", msg)
|
|
75
80
|
attempts = msg.get("attempts", 0)
|
|
76
|
-
if attempts <
|
|
81
|
+
if attempts < mr:
|
|
77
82
|
results.append({"id": msg.get("id"), "data": payload,
|
|
78
83
|
"attempts": attempts, "error": msg.get("error")})
|
|
79
84
|
requeue.append(msg)
|
|
@@ -81,8 +86,13 @@ class KafkaBackend:
|
|
|
81
86
|
self._backend.enqueue(dl_topic, msg)
|
|
82
87
|
return results
|
|
83
88
|
|
|
84
|
-
def dead_letters(self) -> list[dict]:
|
|
85
|
-
"""Consume dead_letter topic, republish, return jobs at/over max_retries.
|
|
89
|
+
def dead_letters(self, max_retries: int = None) -> list[dict]:
|
|
90
|
+
"""Consume dead_letter topic, republish, return jobs at/over max_retries.
|
|
91
|
+
|
|
92
|
+
Accepts max_retries to match the LiteBackend contract — Queue.dead_letters()
|
|
93
|
+
passes it as a kwarg, so without this signature the call raised TypeError.
|
|
94
|
+
"""
|
|
95
|
+
mr = max_retries if max_retries is not None else self._max_retries
|
|
86
96
|
dl_topic = f"{self._topic}.dead_letter"
|
|
87
97
|
results = []
|
|
88
98
|
requeue = []
|
|
@@ -92,7 +102,7 @@ class KafkaBackend:
|
|
|
92
102
|
break
|
|
93
103
|
payload = msg.get("payload", msg)
|
|
94
104
|
attempts = msg.get("attempts", 0)
|
|
95
|
-
if attempts >=
|
|
105
|
+
if attempts >= mr:
|
|
96
106
|
results.append({"id": msg.get("id"), "data": payload,
|
|
97
107
|
"attempts": attempts, "error": msg.get("error")})
|
|
98
108
|
requeue.append(msg)
|
|
@@ -23,11 +23,12 @@ def _future(seconds: int) -> str:
|
|
|
23
23
|
class MongoBackend:
|
|
24
24
|
"""Backend adapter wrapping MongoBackend for the unified Queue API."""
|
|
25
25
|
|
|
26
|
-
def __init__(self, topic: str, max_retries: int, visibility_timeout: float = 300.0
|
|
26
|
+
def __init__(self, topic: str, max_retries: int, visibility_timeout: float = 300.0,
|
|
27
|
+
retry_backoff: float = 0):
|
|
27
28
|
from tina4_python.queue_backends import MongoConnector as _MongoBackend
|
|
28
29
|
|
|
29
30
|
url = os.environ.get("TINA4_QUEUE_URL", "")
|
|
30
|
-
config = {"visibility_timeout": visibility_timeout}
|
|
31
|
+
config = {"visibility_timeout": visibility_timeout, "retry_backoff": retry_backoff}
|
|
31
32
|
if url:
|
|
32
33
|
config["uri"] = url
|
|
33
34
|
self._backend = _MongoBackend(**config)
|
|
@@ -70,12 +71,19 @@ class MongoBackend:
|
|
|
70
71
|
if status == "pending":
|
|
71
72
|
self._backend.clear(self._topic)
|
|
72
73
|
|
|
73
|
-
def retry_failed(self) -> int:
|
|
74
|
+
def retry_failed(self, max_retries: int = None) -> int:
|
|
75
|
+
# Accept max_retries to match the LiteBackend contract (Queue passes it
|
|
76
|
+
# as a kwarg) — without this signature, Queue.retry_failed() raised
|
|
77
|
+
# TypeError on MongoDB.
|
|
78
|
+
mr = max_retries if max_retries is not None else self._max_retries
|
|
74
79
|
self._backend._ensure_connected()
|
|
75
80
|
result = self._backend._collection.update_many(
|
|
76
81
|
{"topic": self._topic, "status": "failed",
|
|
77
|
-
"attempts": {"$lt":
|
|
78
|
-
|
|
82
|
+
"attempts": {"$lt": mr}},
|
|
83
|
+
# Reset available_at so re-queued failed jobs are visible again
|
|
84
|
+
# (they were reserved with available_at in the future at dequeue).
|
|
85
|
+
{"$set": {"status": "pending", "error": None, "available_at": _now(),
|
|
86
|
+
"reserved_at": None}},
|
|
79
87
|
)
|
|
80
88
|
return result.modified_count
|
|
81
89
|
|
|
@@ -90,8 +98,13 @@ class MongoBackend:
|
|
|
90
98
|
"attempts": d.get("attempts", 0), "error": d.get("error")}
|
|
91
99
|
for d in docs]
|
|
92
100
|
|
|
93
|
-
def dead_letters(self) -> list[dict]:
|
|
94
|
-
"""Query the dead_letter collection in MongoDB.
|
|
101
|
+
def dead_letters(self, max_retries: int = None) -> list[dict]:
|
|
102
|
+
"""Query the dead_letter collection in MongoDB.
|
|
103
|
+
|
|
104
|
+
Accepts max_retries (unused — dead letters are terminal) to match the
|
|
105
|
+
LiteBackend contract; Queue.dead_letters() passes it as a kwarg, so
|
|
106
|
+
without this parameter the call raised TypeError on MongoDB.
|
|
107
|
+
"""
|
|
95
108
|
self._backend._ensure_connected()
|
|
96
109
|
dl_topic = f"{self._topic}.dead_letter"
|
|
97
110
|
docs = self._backend._collection.find({"topic": dl_topic})
|
|
@@ -82,8 +82,8 @@ class RabbitMQBackend:
|
|
|
82
82
|
if status == "pending":
|
|
83
83
|
self._backend.clear(self._topic)
|
|
84
84
|
|
|
85
|
-
def retry_failed(self) -> int:
|
|
86
|
-
jobs = self.failed()
|
|
85
|
+
def retry_failed(self, max_retries: int = None) -> int:
|
|
86
|
+
jobs = self.failed(max_retries)
|
|
87
87
|
count = 0
|
|
88
88
|
for job in jobs:
|
|
89
89
|
job_id = job.get("id", "")
|
|
@@ -91,8 +91,13 @@ class RabbitMQBackend:
|
|
|
91
91
|
count += 1
|
|
92
92
|
return count
|
|
93
93
|
|
|
94
|
-
def failed(self) -> list[dict]:
|
|
95
|
-
"""Drain the dead_letter queue, re-enqueue, and return jobs still under max_retries.
|
|
94
|
+
def failed(self, max_retries: int = None) -> list[dict]:
|
|
95
|
+
"""Drain the dead_letter queue, re-enqueue, and return jobs still under max_retries.
|
|
96
|
+
|
|
97
|
+
Accepts max_retries to match the LiteBackend contract — Queue.retry_failed()
|
|
98
|
+
passes it as a kwarg, so without this signature the call raised TypeError.
|
|
99
|
+
"""
|
|
100
|
+
mr = max_retries if max_retries is not None else self._max_retries
|
|
96
101
|
dl_topic = f"{self._topic}.dead_letter"
|
|
97
102
|
results = []
|
|
98
103
|
requeue = []
|
|
@@ -102,7 +107,7 @@ class RabbitMQBackend:
|
|
|
102
107
|
break
|
|
103
108
|
payload = msg.get("payload", msg)
|
|
104
109
|
attempts = msg.get("attempts", 0)
|
|
105
|
-
if attempts <
|
|
110
|
+
if attempts < mr:
|
|
106
111
|
results.append({"id": msg.get("id"), "data": payload,
|
|
107
112
|
"attempts": attempts, "error": msg.get("error")})
|
|
108
113
|
requeue.append(msg)
|
|
@@ -110,8 +115,13 @@ class RabbitMQBackend:
|
|
|
110
115
|
self._backend.enqueue(dl_topic, msg)
|
|
111
116
|
return results
|
|
112
117
|
|
|
113
|
-
def dead_letters(self) -> list[dict]:
|
|
114
|
-
"""Drain the dead_letter queue, re-enqueue, and return jobs at/over max_retries.
|
|
118
|
+
def dead_letters(self, max_retries: int = None) -> list[dict]:
|
|
119
|
+
"""Drain the dead_letter queue, re-enqueue, and return jobs at/over max_retries.
|
|
120
|
+
|
|
121
|
+
Accepts max_retries to match the LiteBackend contract — Queue.dead_letters()
|
|
122
|
+
passes it as a kwarg, so without this signature the call raised TypeError.
|
|
123
|
+
"""
|
|
124
|
+
mr = max_retries if max_retries is not None else self._max_retries
|
|
115
125
|
dl_topic = f"{self._topic}.dead_letter"
|
|
116
126
|
results = []
|
|
117
127
|
requeue = []
|
|
@@ -121,7 +131,7 @@ class RabbitMQBackend:
|
|
|
121
131
|
break
|
|
122
132
|
payload = msg.get("payload", msg)
|
|
123
133
|
attempts = msg.get("attempts", 0)
|
|
124
|
-
if attempts >=
|
|
134
|
+
if attempts >= mr:
|
|
125
135
|
results.append({"id": msg.get("id"), "data": payload,
|
|
126
136
|
"attempts": attempts, "error": msg.get("error")})
|
|
127
137
|
requeue.append(msg)
|
|
@@ -56,6 +56,14 @@ class MongoConnector:
|
|
|
56
56
|
))
|
|
57
57
|
except (TypeError, ValueError):
|
|
58
58
|
self._visibility_timeout = 300.0
|
|
59
|
+
# Seconds to delay a requeued (rejected/retried) job before it is
|
|
60
|
+
# eligible again. Default 0 = available on the very next dequeue, so a
|
|
61
|
+
# fail()'d job retries immediately (matching the file backend) instead
|
|
62
|
+
# of waiting out the visibility window.
|
|
63
|
+
try:
|
|
64
|
+
self._retry_backoff = float(config.get("retry_backoff", 0))
|
|
65
|
+
except (TypeError, ValueError):
|
|
66
|
+
self._retry_backoff = 0.0
|
|
59
67
|
|
|
60
68
|
self._pymongo = None
|
|
61
69
|
self._client = None
|
|
@@ -143,6 +151,13 @@ class MongoConnector:
|
|
|
143
151
|
|
|
144
152
|
result = doc.get("data", {})
|
|
145
153
|
result["id"] = doc["_id"]
|
|
154
|
+
# Surface the LIVE document-level attempts/priority, not the push-time
|
|
155
|
+
# snapshot stored inside ``data``. reclaim_expired()/reject() increment
|
|
156
|
+
# the top-level ``attempts``; without this the consumer always saw the
|
|
157
|
+
# original 0, so fail()'s ``attempts >= max_retries`` check never tripped
|
|
158
|
+
# and a job could be retried forever instead of dead-lettering.
|
|
159
|
+
result["attempts"] = doc.get("attempts", result.get("attempts", 0))
|
|
160
|
+
result["priority"] = doc.get("priority", result.get("priority", 0))
|
|
146
161
|
return result
|
|
147
162
|
|
|
148
163
|
def reclaim_expired(self, topic: str, max_retries: int) -> int:
|
|
@@ -201,9 +216,16 @@ class MongoConnector:
|
|
|
201
216
|
"""Reject a message. Optionally requeue it."""
|
|
202
217
|
self._ensure_connected()
|
|
203
218
|
if requeue:
|
|
219
|
+
# Reset available_at so the requeued job is visible again right away
|
|
220
|
+
# (or after retry_backoff). dequeue() pushed available_at out to the
|
|
221
|
+
# reservation expiry; leaving it there stranded a fail()'d job for the
|
|
222
|
+
# full visibility window instead of retrying it on the next pop().
|
|
223
|
+
available = _future(self._retry_backoff) if self._retry_backoff > 0 else _now()
|
|
204
224
|
self._collection.update_one(
|
|
205
225
|
{"_id": message_id, "topic": topic},
|
|
206
|
-
{"$set": {"status": "pending"
|
|
226
|
+
{"$set": {"status": "pending", "available_at": available,
|
|
227
|
+
"reserved_at": None},
|
|
228
|
+
"$inc": {"attempts": 1}},
|
|
207
229
|
)
|
|
208
230
|
else:
|
|
209
231
|
self._collection.update_one(
|