tina4-python 3.13.32__tar.gz → 3.13.33__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.32 → tina4_python-3.13.33}/PKG-INFO +1 -1
- {tina4_python-3.13.32 → tina4_python-3.13.33}/pyproject.toml +1 -1
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue/__init__.py +10 -6
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue/job.py +11 -2
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue/lite_backend.py +147 -70
- {tina4_python-3.13.32 → tina4_python-3.13.33}/.gitignore +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/README.md +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/env.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -34,13 +34,13 @@ from tina4_python.queue.kafka_backend import KafkaBackend
|
|
|
34
34
|
from tina4_python.queue.mongo_backend import MongoBackend
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def _resolve_backend(topic: str, backend: str | None, max_retries: int):
|
|
37
|
+
def _resolve_backend(topic: str, backend: str | None, max_retries: int, retry_backoff: int = 0):
|
|
38
38
|
"""Resolve which backend adapter to use."""
|
|
39
39
|
chosen = backend or os.environ.get("TINA4_QUEUE_BACKEND", "file")
|
|
40
40
|
chosen = chosen.lower().strip()
|
|
41
41
|
|
|
42
42
|
if chosen in ("file", "default", "lite"):
|
|
43
|
-
return LiteBackend(topic, max_retries)
|
|
43
|
+
return LiteBackend(topic, max_retries, retry_backoff)
|
|
44
44
|
elif chosen == "rabbitmq":
|
|
45
45
|
return RabbitMQBackend(topic, max_retries)
|
|
46
46
|
elif chosen == "kafka":
|
|
@@ -63,10 +63,13 @@ class Queue:
|
|
|
63
63
|
"""
|
|
64
64
|
|
|
65
65
|
def __init__(self, topic: str = "default", max_retries: int = 3,
|
|
66
|
-
backend: str | None = None):
|
|
66
|
+
backend: str | None = None, retry_backoff: int = 0):
|
|
67
67
|
self.topic = topic
|
|
68
68
|
self.max_retries = max_retries
|
|
69
|
-
|
|
69
|
+
# Seconds to wait before a failed job is re-attempted (file backend).
|
|
70
|
+
# Default 0 = retry on the very next pop()/consume() iteration.
|
|
71
|
+
self.retry_backoff = retry_backoff
|
|
72
|
+
self._backend = _resolve_backend(topic, backend, max_retries, retry_backoff)
|
|
70
73
|
|
|
71
74
|
def push(self, data: dict, priority: int = 0, delay_seconds: int = 0):
|
|
72
75
|
"""Add a job to the queue. Returns job ID."""
|
|
@@ -208,12 +211,12 @@ class Queue:
|
|
|
208
211
|
|
|
209
212
|
old_topic = self.topic
|
|
210
213
|
self.topic = topic
|
|
211
|
-
self._backend = _resolve_backend(topic, None, self.max_retries)
|
|
214
|
+
self._backend = _resolve_backend(topic, None, self.max_retries, self.retry_backoff)
|
|
212
215
|
try:
|
|
213
216
|
return self.push(data, priority, delay_seconds)
|
|
214
217
|
finally:
|
|
215
218
|
self.topic = old_topic
|
|
216
|
-
self._backend = _resolve_backend(old_topic, None, self.max_retries)
|
|
219
|
+
self._backend = _resolve_backend(old_topic, None, self.max_retries, self.retry_backoff)
|
|
217
220
|
|
|
218
221
|
def consume(self, topic: str = None, job_id: str = None, poll_interval: float = 1.0,
|
|
219
222
|
iterations: int = 0, batch_size: int = 1):
|
|
@@ -309,6 +312,7 @@ class Queue:
|
|
|
309
312
|
data=job_data["data"],
|
|
310
313
|
priority=job_data.get("priority", 0),
|
|
311
314
|
attempts=job_data.get("attempts", 0),
|
|
315
|
+
error=job_data.get("error"),
|
|
312
316
|
)
|
|
313
317
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
314
318
|
continue
|
|
@@ -20,11 +20,20 @@ class Job:
|
|
|
20
20
|
return self.payload
|
|
21
21
|
|
|
22
22
|
def complete(self):
|
|
23
|
-
"""Mark job as completed."""
|
|
23
|
+
"""Mark job as completed. Terminal — the job is done and removed."""
|
|
24
24
|
self.queue._backend.complete(self)
|
|
25
25
|
|
|
26
26
|
def fail(self, error: str = ""):
|
|
27
|
-
"""
|
|
27
|
+
"""Record a failed attempt.
|
|
28
|
+
|
|
29
|
+
Increments ``attempts``. If the job still has retries left
|
|
30
|
+
(``attempts < max_retries``) it is automatically re-enqueued to the
|
|
31
|
+
pending queue, so the next ``pop()``/``consume()`` picks it up again
|
|
32
|
+
(after the queue's ``retry_backoff`` delay, if any). Once it has been
|
|
33
|
+
attempted ``max_retries`` times it is moved to the dead-letter store,
|
|
34
|
+
where ``queue.dead_letters()`` returns it. No manual ``retry_failed()``
|
|
35
|
+
is required.
|
|
36
|
+
"""
|
|
28
37
|
self.queue._backend.fail(self, error)
|
|
29
38
|
|
|
30
39
|
def reject(self, reason: str = ""):
|
|
@@ -30,9 +30,13 @@ class LiteBackend:
|
|
|
30
30
|
Each job is stored as a separate .queue-data JSON file.
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
def __init__(self, topic: str, max_retries: int):
|
|
33
|
+
def __init__(self, topic: str, max_retries: int, retry_backoff: int = 0):
|
|
34
34
|
self._topic = topic
|
|
35
35
|
self._max_retries = max_retries
|
|
36
|
+
# Seconds to delay a job's next attempt when it is automatically
|
|
37
|
+
# re-enqueued by fail(). 0 (the default) means retry immediately —
|
|
38
|
+
# the next pop()/consume() iteration picks it up straight away.
|
|
39
|
+
self._retry_backoff = retry_backoff
|
|
36
40
|
self._base_path = os.environ.get("TINA4_QUEUE_PATH", "data/queue")
|
|
37
41
|
self._lock = threading.Lock()
|
|
38
42
|
self._seq = 0
|
|
@@ -74,29 +78,54 @@ class LiteBackend:
|
|
|
74
78
|
json.dump(job, f, indent=2, default=str)
|
|
75
79
|
return job_id
|
|
76
80
|
|
|
81
|
+
def _available_candidates(self, now: str) -> list:
|
|
82
|
+
"""Return (filename, job_data) for every pending, non-delayed job,
|
|
83
|
+
ordered by the dequeue policy: highest priority first, ties broken
|
|
84
|
+
oldest-first by created_at.
|
|
85
|
+
|
|
86
|
+
The file *name* is no longer the ordering key — the stored
|
|
87
|
+
``priority`` and ``created_at`` fields are. This makes pop()/consume()
|
|
88
|
+
honour priority instead of being pure FIFO.
|
|
89
|
+
"""
|
|
90
|
+
queue_dir = self._queue_dir()
|
|
91
|
+
try:
|
|
92
|
+
filenames = os.listdir(queue_dir)
|
|
93
|
+
except FileNotFoundError:
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
candidates = []
|
|
97
|
+
for filename in filenames:
|
|
98
|
+
if not filename.endswith(".queue-data"):
|
|
99
|
+
continue
|
|
100
|
+
filepath = os.path.join(queue_dir, filename)
|
|
101
|
+
try:
|
|
102
|
+
with open(filepath) as f:
|
|
103
|
+
job_data = json.load(f)
|
|
104
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
105
|
+
continue
|
|
106
|
+
if job_data.get("status") != "pending":
|
|
107
|
+
continue
|
|
108
|
+
if job_data.get("available_at", "") > now:
|
|
109
|
+
continue # still delayed
|
|
110
|
+
candidates.append((filename, job_data))
|
|
111
|
+
|
|
112
|
+
# priority DESC, then created_at ASC (oldest first). created_at is an
|
|
113
|
+
# ISO-8601 string so lexicographic order == chronological order.
|
|
114
|
+
candidates.sort(
|
|
115
|
+
key=lambda item: (
|
|
116
|
+
-int(item[1].get("priority", 0) or 0),
|
|
117
|
+
item[1].get("created_at", "") or "",
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
return candidates
|
|
121
|
+
|
|
77
122
|
def pop(self, queue_ref) -> Job | None:
|
|
78
123
|
now = _now()
|
|
79
124
|
queue_dir = self._queue_dir()
|
|
80
125
|
|
|
81
126
|
with self._lock:
|
|
82
|
-
|
|
83
|
-
files = sorted(f for f in os.listdir(queue_dir) if f.endswith(".queue-data"))
|
|
84
|
-
except FileNotFoundError:
|
|
85
|
-
return None
|
|
86
|
-
|
|
87
|
-
for filename in files:
|
|
127
|
+
for filename, job_data in self._available_candidates(now):
|
|
88
128
|
filepath = os.path.join(queue_dir, filename)
|
|
89
|
-
try:
|
|
90
|
-
with open(filepath) as f:
|
|
91
|
-
job_data = json.load(f)
|
|
92
|
-
except (json.JSONDecodeError, FileNotFoundError):
|
|
93
|
-
continue
|
|
94
|
-
|
|
95
|
-
if job_data.get("status") != "pending":
|
|
96
|
-
continue
|
|
97
|
-
if job_data.get("available_at", "") > now:
|
|
98
|
-
continue
|
|
99
|
-
|
|
100
129
|
# Claim the job by deleting the file
|
|
101
130
|
try:
|
|
102
131
|
os.unlink(filepath)
|
|
@@ -110,37 +139,25 @@ class LiteBackend:
|
|
|
110
139
|
data=job_data["data"],
|
|
111
140
|
priority=job_data.get("priority", 0),
|
|
112
141
|
attempts=job_data.get("attempts", 0),
|
|
142
|
+
error=job_data.get("error"),
|
|
113
143
|
)
|
|
114
144
|
|
|
115
145
|
return None
|
|
116
146
|
|
|
117
147
|
def pop_batch(self, count: int, queue_ref) -> list:
|
|
118
|
-
"""Pop up to count jobs in one operation
|
|
148
|
+
"""Pop up to count jobs in one operation, highest priority first.
|
|
149
|
+
|
|
150
|
+
Returns a partial batch if fewer than ``count`` are available.
|
|
151
|
+
"""
|
|
119
152
|
now = _now()
|
|
120
153
|
queue_dir = self._queue_dir()
|
|
121
154
|
results = []
|
|
122
155
|
|
|
123
156
|
with self._lock:
|
|
124
|
-
|
|
125
|
-
files = sorted(f for f in os.listdir(queue_dir) if f.endswith(".queue-data"))
|
|
126
|
-
except FileNotFoundError:
|
|
127
|
-
return []
|
|
128
|
-
|
|
129
|
-
for filename in files:
|
|
157
|
+
for filename, job_data in self._available_candidates(now):
|
|
130
158
|
if len(results) >= count:
|
|
131
159
|
break
|
|
132
160
|
filepath = os.path.join(queue_dir, filename)
|
|
133
|
-
try:
|
|
134
|
-
with open(filepath) as f:
|
|
135
|
-
job_data = json.load(f)
|
|
136
|
-
except (json.JSONDecodeError, FileNotFoundError):
|
|
137
|
-
continue
|
|
138
|
-
|
|
139
|
-
if job_data.get("status") != "pending":
|
|
140
|
-
continue
|
|
141
|
-
if job_data.get("available_at", "") > now:
|
|
142
|
-
continue
|
|
143
|
-
|
|
144
161
|
try:
|
|
145
162
|
os.unlink(filepath)
|
|
146
163
|
except FileNotFoundError:
|
|
@@ -153,45 +170,57 @@ class LiteBackend:
|
|
|
153
170
|
data=job_data["data"],
|
|
154
171
|
priority=job_data.get("priority", 0),
|
|
155
172
|
attempts=job_data.get("attempts", 0),
|
|
173
|
+
error=job_data.get("error"),
|
|
156
174
|
))
|
|
157
175
|
|
|
158
176
|
return results
|
|
159
177
|
|
|
178
|
+
# Status aliases that live in the failed/ directory (dead-lettered jobs)
|
|
179
|
+
# rather than as pending files in the queue directory.
|
|
180
|
+
_DEAD_STATES = ("failed", "dead", "dead_letter")
|
|
181
|
+
|
|
160
182
|
def size(self, status: str = "pending") -> int:
|
|
161
|
-
|
|
183
|
+
scan_dir = self._failed_dir() if status in self._DEAD_STATES else self._queue_dir()
|
|
162
184
|
count = 0
|
|
163
185
|
try:
|
|
164
|
-
for filename in os.listdir(
|
|
186
|
+
for filename in os.listdir(scan_dir):
|
|
165
187
|
if not filename.endswith(".queue-data"):
|
|
166
188
|
continue
|
|
167
|
-
filepath = os.path.join(
|
|
189
|
+
filepath = os.path.join(scan_dir, filename)
|
|
168
190
|
try:
|
|
169
191
|
with open(filepath) as f:
|
|
170
192
|
job_data = json.load(f)
|
|
171
|
-
if job_data.get("status") == status:
|
|
172
|
-
count += 1
|
|
173
193
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
174
194
|
continue
|
|
195
|
+
if status in self._DEAD_STATES:
|
|
196
|
+
# Every file in failed/ is a dead-letter; count them all
|
|
197
|
+
# regardless of the exact stored status string.
|
|
198
|
+
count += 1
|
|
199
|
+
elif job_data.get("status") == status:
|
|
200
|
+
count += 1
|
|
175
201
|
except FileNotFoundError:
|
|
176
202
|
pass
|
|
177
203
|
return count
|
|
178
204
|
|
|
179
205
|
def purge(self, status: str = "completed") -> int:
|
|
180
|
-
|
|
206
|
+
scan_dir = self._failed_dir() if status in self._DEAD_STATES else self._queue_dir()
|
|
181
207
|
count = 0
|
|
182
208
|
try:
|
|
183
|
-
for filename in os.listdir(
|
|
209
|
+
for filename in os.listdir(scan_dir):
|
|
184
210
|
if not filename.endswith(".queue-data"):
|
|
185
211
|
continue
|
|
186
|
-
filepath = os.path.join(
|
|
212
|
+
filepath = os.path.join(scan_dir, filename)
|
|
187
213
|
try:
|
|
188
214
|
with open(filepath) as f:
|
|
189
215
|
job_data = json.load(f)
|
|
190
|
-
if job_data.get("status") == status:
|
|
191
|
-
os.unlink(filepath)
|
|
192
|
-
count += 1
|
|
193
216
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
194
217
|
continue
|
|
218
|
+
if status in self._DEAD_STATES or job_data.get("status") == status:
|
|
219
|
+
try:
|
|
220
|
+
os.unlink(filepath)
|
|
221
|
+
count += 1
|
|
222
|
+
except FileNotFoundError:
|
|
223
|
+
continue
|
|
195
224
|
except FileNotFoundError:
|
|
196
225
|
pass
|
|
197
226
|
return count
|
|
@@ -212,6 +241,7 @@ class LiteBackend:
|
|
|
212
241
|
if job_data.get("attempts", 0) < limit:
|
|
213
242
|
job_data["status"] = "pending"
|
|
214
243
|
job_data["available_at"] = _now()
|
|
244
|
+
job_data["created_at"] = _now()
|
|
215
245
|
prefix = self._next_prefix()
|
|
216
246
|
new_path = os.path.join(queue_dir, f"{prefix}_{job_data['id']}.queue-data")
|
|
217
247
|
with open(new_path, "w") as f:
|
|
@@ -225,20 +255,28 @@ class LiteBackend:
|
|
|
225
255
|
return count
|
|
226
256
|
|
|
227
257
|
def failed(self) -> list[dict]:
|
|
228
|
-
|
|
258
|
+
"""Jobs that have failed at least once but are still being retried.
|
|
259
|
+
|
|
260
|
+
Under the auto-retry lifecycle a failed-but-retryable job lives in the
|
|
261
|
+
pending queue (not the dead-letter dir), so this scans the queue dir
|
|
262
|
+
for pending jobs with ``attempts > 0`` that have not yet exhausted
|
|
263
|
+
their retries. Dead-lettered jobs are returned by dead_letters().
|
|
264
|
+
"""
|
|
265
|
+
queue_dir = self._queue_dir()
|
|
229
266
|
results = []
|
|
230
267
|
try:
|
|
231
|
-
for filename in sorted(os.listdir(
|
|
268
|
+
for filename in sorted(os.listdir(queue_dir)):
|
|
232
269
|
if not filename.endswith(".queue-data"):
|
|
233
270
|
continue
|
|
234
|
-
filepath = os.path.join(
|
|
271
|
+
filepath = os.path.join(queue_dir, filename)
|
|
235
272
|
try:
|
|
236
273
|
with open(filepath) as f:
|
|
237
274
|
job_data = json.load(f)
|
|
238
|
-
if job_data.get("attempts", 0) < self._max_retries:
|
|
239
|
-
results.append(job_data)
|
|
240
275
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
241
276
|
continue
|
|
277
|
+
attempts = job_data.get("attempts", 0)
|
|
278
|
+
if attempts > 0 and attempts < self._max_retries:
|
|
279
|
+
results.append(job_data)
|
|
242
280
|
except FileNotFoundError:
|
|
243
281
|
pass
|
|
244
282
|
return results
|
|
@@ -264,17 +302,22 @@ class LiteBackend:
|
|
|
264
302
|
return results
|
|
265
303
|
|
|
266
304
|
def retry_job(self, job_id: str, delay_seconds: int = 0) -> bool:
|
|
305
|
+
"""Revive a specific dead-letter job by id back to the pending queue.
|
|
306
|
+
|
|
307
|
+
This is a manual override (Queue.retry(job_id)) — it always revives a
|
|
308
|
+
dead-letter regardless of attempt count, mirroring job.retry(). Returns
|
|
309
|
+
False only if no dead-letter with that id exists.
|
|
310
|
+
"""
|
|
267
311
|
failed_dir = self._failed_dir()
|
|
268
312
|
queue_dir = self._queue_dir()
|
|
269
313
|
filepath = os.path.join(failed_dir, f"{job_id}.queue-data")
|
|
270
314
|
try:
|
|
271
315
|
with open(filepath) as f:
|
|
272
316
|
job_data = json.load(f)
|
|
273
|
-
if job_data.get("attempts", 0) >= self._max_retries:
|
|
274
|
-
return False
|
|
275
317
|
job_data["status"] = "pending"
|
|
276
318
|
job_data["error"] = None
|
|
277
319
|
job_data["attempts"] = job_data.get("attempts", 0) + 1
|
|
320
|
+
job_data["created_at"] = _now()
|
|
278
321
|
if delay_seconds > 0:
|
|
279
322
|
job_data["available_at"] = _future(delay_seconds)
|
|
280
323
|
else:
|
|
@@ -305,41 +348,75 @@ class LiteBackend:
|
|
|
305
348
|
return count
|
|
306
349
|
|
|
307
350
|
def complete(self, job: Job):
|
|
308
|
-
# Job file was already deleted on pop — nothing to do
|
|
351
|
+
# Job file was already deleted on pop — nothing to do. complete() is
|
|
352
|
+
# terminal: the job is done and gone.
|
|
309
353
|
pass
|
|
310
354
|
|
|
311
|
-
def
|
|
312
|
-
job
|
|
355
|
+
def _requeue(self, job: Job, delay_seconds: int = 0, error: str | None = None):
|
|
356
|
+
"""Write the job back to the pending queue (queue dir).
|
|
357
|
+
|
|
358
|
+
Re-enqueued jobs get a fresh ``created_at`` so within a priority tier
|
|
359
|
+
they sort behind jobs that have not yet been attempted. ``attempts``
|
|
360
|
+
already reflects the latest failure count.
|
|
361
|
+
"""
|
|
362
|
+
available = _now() if delay_seconds <= 0 else _future(delay_seconds)
|
|
313
363
|
job_data = {
|
|
314
364
|
"id": job.id,
|
|
315
365
|
"topic": job.topic,
|
|
316
366
|
"data": job.payload,
|
|
317
|
-
"status": "
|
|
367
|
+
"status": "pending",
|
|
318
368
|
"priority": job.priority,
|
|
319
369
|
"attempts": job.attempts,
|
|
320
370
|
"error": error,
|
|
321
|
-
"
|
|
371
|
+
"available_at": available,
|
|
372
|
+
"created_at": _now(),
|
|
322
373
|
}
|
|
323
|
-
|
|
324
|
-
os.
|
|
325
|
-
filepath = os.path.join(failed_dir, f"{job.id}.queue-data")
|
|
374
|
+
prefix = self._next_prefix()
|
|
375
|
+
filepath = os.path.join(self._queue_dir(), f"{prefix}_{job.id}.queue-data")
|
|
326
376
|
with open(filepath, "w") as f:
|
|
327
377
|
json.dump(job_data, f, indent=2, default=str)
|
|
328
378
|
|
|
329
|
-
def
|
|
330
|
-
job.
|
|
331
|
-
|
|
379
|
+
def _dead_letter(self, job: Job, error: str = ""):
|
|
380
|
+
"""Move the job to the dead-letter (failed) directory. Terminal until
|
|
381
|
+
a manual retry_failed()/retry() revives it."""
|
|
332
382
|
job_data = {
|
|
333
383
|
"id": job.id,
|
|
334
384
|
"topic": job.topic,
|
|
335
385
|
"data": job.payload,
|
|
336
|
-
"status": "
|
|
386
|
+
"status": "dead",
|
|
337
387
|
"priority": job.priority,
|
|
338
388
|
"attempts": job.attempts,
|
|
339
|
-
"
|
|
340
|
-
"
|
|
389
|
+
"error": error,
|
|
390
|
+
"failed_at": _now(),
|
|
341
391
|
}
|
|
342
|
-
|
|
343
|
-
|
|
392
|
+
failed_dir = self._failed_dir()
|
|
393
|
+
os.makedirs(failed_dir, exist_ok=True)
|
|
394
|
+
filepath = os.path.join(failed_dir, f"{job.id}.queue-data")
|
|
344
395
|
with open(filepath, "w") as f:
|
|
345
396
|
json.dump(job_data, f, indent=2, default=str)
|
|
397
|
+
|
|
398
|
+
def fail(self, job: Job, error: str = ""):
|
|
399
|
+
"""Record a failed attempt.
|
|
400
|
+
|
|
401
|
+
Increments ``attempts``. If the job still has retries left
|
|
402
|
+
(``attempts < max_retries``) it is automatically re-enqueued to the
|
|
403
|
+
pending queue — so the next pop()/consume() iteration picks it up
|
|
404
|
+
again — after an optional ``retry_backoff`` delay. Once it has been
|
|
405
|
+
attempted ``max_retries`` times (``attempts >= max_retries``) it is
|
|
406
|
+
moved to the dead-letter store, where dead_letters() returns it.
|
|
407
|
+
"""
|
|
408
|
+
job.attempts += 1
|
|
409
|
+
job.error = error
|
|
410
|
+
if job.attempts < self._max_retries:
|
|
411
|
+
self._requeue(job, delay_seconds=self._retry_backoff, error=error)
|
|
412
|
+
else:
|
|
413
|
+
self._dead_letter(job, error)
|
|
414
|
+
|
|
415
|
+
def retry(self, job: Job, delay_seconds: int = 0):
|
|
416
|
+
"""Explicit re-queue requested by the caller (job.retry()).
|
|
417
|
+
|
|
418
|
+
Always re-enqueues regardless of the retry limit — this is a manual
|
|
419
|
+
override, distinct from the automatic fail() path.
|
|
420
|
+
"""
|
|
421
|
+
job.attempts += 1
|
|
422
|
+
self._requeue(job, delay_seconds=delay_seconds, error=None)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.32 → tina4_python-3.13.33}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/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
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/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.32 → tina4_python-3.13.33}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.32 → tina4_python-3.13.33}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|