ergon-framework-python 0.2.5__tar.gz → 0.3.0__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.
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/PKG-INFO +3 -3
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/pyproject.toml +3 -3
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/__init__.py +1 -1
- ergon_framework_python-0.3.0/src/ergon/connector/ergon_platform/_client.py +52 -0
- ergon_framework_python-0.3.0/src/ergon/connector/ergon_platform/workflows/_mode.py +88 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/workflows/_operations.py +85 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/workflows/async_connector.py +92 -4
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/workflows/connector.py +89 -3
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/workflows/models.py +34 -1
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon_framework_python.egg-info/PKG-INFO +3 -3
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon_framework_python.egg-info/SOURCES.txt +1 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon_framework_python.egg-info/requires.txt +2 -2
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/tests/test_smoke.py +1 -1
- ergon_framework_python-0.2.5/src/ergon/connector/ergon_platform/_client.py +0 -26
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/LICENSE +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/README.md +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/setup.cfg +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/_observability/grafana.yaml +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/_observability/loki.yaml +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/_observability/prometheus.yaml +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/_observability/tempo.yaml +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/main.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/constants.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/example_task/config.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/example_task/task.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/bootstrap/src/__project__/tasks/settings.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/cli.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/_lazy.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/async_connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/adapters.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/async_connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/models.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/services/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/services/activity.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/services/addresses.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/services/attachments.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/services/messages.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/services/records.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/channels/services/service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/models.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/utils.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/workflows/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/ergon_platform/workflows/utils.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/excel/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/excel/connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/excel/models.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/excel/service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/nylas/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/nylas/async_auth_service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/nylas/async_connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/nylas/async_service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/nylas/auth_service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/nylas/connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/nylas/models.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/nylas/service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/nylas/utils.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/outlook/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/outlook/async_connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/outlook/async_service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/outlook/connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/outlook/models.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/outlook/service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/outlook/utils.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/pipefy/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/pipefy/async_connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/pipefy/async_service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/pipefy/connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/pipefy/models.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/pipefy/service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/pipefy/version.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/postgres/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/postgres/async_connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/postgres/async_service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/postgres/models.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/rabbitmq/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/rabbitmq/async_connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/rabbitmq/async_service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/rabbitmq/connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/rabbitmq/helper.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/rabbitmq/models.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/rabbitmq/service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/sqs/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/sqs/async_connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/sqs/async_service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/sqs/connector.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/sqs/models.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/sqs/service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/connector/transaction.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/py.typed +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/service/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/service/service.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/base.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/exceptions.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/helpers.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/liveness.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/manager.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/mixins/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/mixins/consumer.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/mixins/metrics.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/mixins/producer.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/policies.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/runner.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/task/utils.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/telemetry/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/telemetry/_resource.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/telemetry/logging.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/telemetry/metrics.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/telemetry/tracing.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/utils/__init__.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon/utils/env.py +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon_framework_python.egg-info/dependency_links.txt +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon_framework_python.egg-info/entry_points.txt +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/src/ergon_framework_python.egg-info/top_level.txt +0 -0
- {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.0}/tests/test_optional_connectors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ergon-framework-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Ergon internal task-oriented project bootstrapper
|
|
5
5
|
Author-email: Ergondata Technologies <anza.vossos@protonmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,7 +29,7 @@ Requires-Dist: aio-pika>=9.0.0; extra == "all"
|
|
|
29
29
|
Requires-Dist: aiobotocore>=2.13.0; extra == "all"
|
|
30
30
|
Requires-Dist: asyncpg>=0.29.0; extra == "all"
|
|
31
31
|
Requires-Dist: boto3>=1.35.0; extra == "all"
|
|
32
|
-
Requires-Dist: ergon-platform-sdk<0.
|
|
32
|
+
Requires-Dist: ergon-platform-sdk<0.6,>=0.5.1; extra == "all"
|
|
33
33
|
Requires-Dist: httpx>=0.28.0; extra == "all"
|
|
34
34
|
Requires-Dist: msal>=1.37.0; extra == "all"
|
|
35
35
|
Requires-Dist: nylas>=6.0.0; extra == "all"
|
|
@@ -37,7 +37,7 @@ Requires-Dist: openpyxl>=3.1.5; extra == "all"
|
|
|
37
37
|
Requires-Dist: pika>=1.3.0; extra == "all"
|
|
38
38
|
Requires-Dist: requests>=2.32.5; extra == "all"
|
|
39
39
|
Provides-Extra: ergon-platform
|
|
40
|
-
Requires-Dist: ergon-platform-sdk<0.
|
|
40
|
+
Requires-Dist: ergon-platform-sdk<0.6,>=0.5.1; extra == "ergon-platform"
|
|
41
41
|
Requires-Dist: httpx>=0.28.0; extra == "ergon-platform"
|
|
42
42
|
Provides-Extra: excel
|
|
43
43
|
Requires-Dist: openpyxl>=3.1.5; extra == "excel"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ergon-framework-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Ergon internal task-oriented project bootstrapper"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -34,7 +34,7 @@ all = [
|
|
|
34
34
|
"aiobotocore>=2.13.0",
|
|
35
35
|
"asyncpg>=0.29.0",
|
|
36
36
|
"boto3>=1.35.0",
|
|
37
|
-
"ergon-platform-sdk>=0.
|
|
37
|
+
"ergon-platform-sdk>=0.5.1,<0.6",
|
|
38
38
|
"httpx>=0.28.0",
|
|
39
39
|
"msal>=1.37.0",
|
|
40
40
|
"nylas>=6.0.0",
|
|
@@ -43,7 +43,7 @@ all = [
|
|
|
43
43
|
"requests>=2.32.5",
|
|
44
44
|
]
|
|
45
45
|
"ergon-platform" = [
|
|
46
|
-
"ergon-platform-sdk>=0.
|
|
46
|
+
"ergon-platform-sdk>=0.5.1,<0.6",
|
|
47
47
|
"httpx>=0.28.0",
|
|
48
48
|
]
|
|
49
49
|
excel = ["openpyxl>=3.1.5"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import socket
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .models import ErgonPlatformClient
|
|
6
|
+
|
|
7
|
+
_ERGON_PLATFORM_IMPORT_ERROR = (
|
|
8
|
+
"The 'ergon-platform' connector dependencies are not installed. "
|
|
9
|
+
"Install them with: pip install 'ergon-framework-python[ergon-platform]'"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_ergon_client():
|
|
14
|
+
try:
|
|
15
|
+
from ergon_platform import ErgonClient # type: ignore[reportMissingImports]
|
|
16
|
+
except ImportError as exc:
|
|
17
|
+
raise ImportError(_ERGON_PLATFORM_IMPORT_ERROR) from exc
|
|
18
|
+
return ErgonClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def default_consumer_id() -> str:
|
|
22
|
+
"""One identity per worker process: ``<hostname>:<pid>``.
|
|
23
|
+
|
|
24
|
+
Pull only phases fence item claims by this value; two processes must never
|
|
25
|
+
share it, two batches of the same process should.
|
|
26
|
+
"""
|
|
27
|
+
host = socket.gethostname().split(".")[0] or "worker"
|
|
28
|
+
safe_host = "".join(ch if (ch.isalnum() or ch in "._:@-") else "-" for ch in host)
|
|
29
|
+
return f"{safe_host}:{os.getpid()}"[:128]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_ergon_client(config: ErgonPlatformClient, *, consumer_id: Optional[str] = None):
|
|
33
|
+
"""Build the SDK client. ``consumer_id`` adds ``X-Ergon-Consumer-Id`` to every request."""
|
|
34
|
+
ErgonClient = _get_ergon_client()
|
|
35
|
+
if consumer_id is None:
|
|
36
|
+
return ErgonClient(
|
|
37
|
+
client_id=config.client_id,
|
|
38
|
+
client_secret=config.client_secret,
|
|
39
|
+
base_url=config.base_url,
|
|
40
|
+
company_id=config.company_id,
|
|
41
|
+
timeout=config.timeout,
|
|
42
|
+
max_retries=config.max_retries,
|
|
43
|
+
)
|
|
44
|
+
return ErgonClient(
|
|
45
|
+
client_id=config.client_id,
|
|
46
|
+
client_secret=config.client_secret,
|
|
47
|
+
base_url=config.base_url,
|
|
48
|
+
company_id=config.company_id,
|
|
49
|
+
consumer_id=consumer_id,
|
|
50
|
+
timeout=config.timeout,
|
|
51
|
+
max_retries=config.max_retries,
|
|
52
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Consumer mode resolution shared by the sync and async workflow connectors.
|
|
2
|
+
|
|
3
|
+
A connector consumes one phase. Standard phases are served by the historical
|
|
4
|
+
list-and-claim loop; *Pull only* phases by ``POST /phases/{id}/items/pull``
|
|
5
|
+
under a consumer id. ``mode="auto"`` reads the phase kind once, on the first
|
|
6
|
+
call that needs it, and the connector then behaves like the matching kind for
|
|
7
|
+
the rest of its life. Phase kind is fixed at creation, so there is nothing to
|
|
8
|
+
re-check later.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, Callable, Optional
|
|
13
|
+
|
|
14
|
+
from .._client import create_ergon_client, default_consumer_id
|
|
15
|
+
from ..models import ErgonPlatformClient
|
|
16
|
+
from ._operations import ErgonPlatformWorkflowsService
|
|
17
|
+
from .models import ErgonPlatformConsumerConfig
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
DEFAULT_NACK_RELEASE_DELAY_SECONDS = 10
|
|
22
|
+
DEFAULT_NACK_REASON = "nack_transaction"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConsumerModeState:
|
|
26
|
+
"""Lazily decides list-claim vs pull for one connector and swaps in the fenced client."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
client_config: ErgonPlatformClient,
|
|
31
|
+
consumer_config: Optional[ErgonPlatformConsumerConfig],
|
|
32
|
+
service: ErgonPlatformWorkflowsService,
|
|
33
|
+
set_client: Callable[[Any], None],
|
|
34
|
+
) -> None:
|
|
35
|
+
self._client_config = client_config
|
|
36
|
+
self._consumer_config = consumer_config
|
|
37
|
+
self._service = service
|
|
38
|
+
self._set_client = set_client
|
|
39
|
+
self._pull: Optional[bool] = None
|
|
40
|
+
self.consumer_id: Optional[str] = None
|
|
41
|
+
if consumer_config is not None and consumer_config.mode == "pull":
|
|
42
|
+
self._enable_pull()
|
|
43
|
+
self._pull = True
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def resolved(self) -> Optional[bool]:
|
|
47
|
+
return self._pull
|
|
48
|
+
|
|
49
|
+
def is_pull(self) -> bool:
|
|
50
|
+
if self._pull is not None:
|
|
51
|
+
return self._pull
|
|
52
|
+
config = self._consumer_config
|
|
53
|
+
if config is None:
|
|
54
|
+
return False
|
|
55
|
+
if config.mode == "list_claim":
|
|
56
|
+
pull = False
|
|
57
|
+
else:
|
|
58
|
+
try:
|
|
59
|
+
pull = self._service.is_pull_phase(config.workflow_id, config.phase_id)
|
|
60
|
+
except Exception:
|
|
61
|
+
logger.warning(
|
|
62
|
+
"Could not read the kind of phase %s; consuming it as a standard phase. "
|
|
63
|
+
"Set consumer_config.mode explicitly to silence this.",
|
|
64
|
+
config.phase_id,
|
|
65
|
+
exc_info=True,
|
|
66
|
+
)
|
|
67
|
+
pull = False
|
|
68
|
+
if pull:
|
|
69
|
+
self._enable_pull()
|
|
70
|
+
self._pull = pull
|
|
71
|
+
return pull
|
|
72
|
+
|
|
73
|
+
def _enable_pull(self) -> None:
|
|
74
|
+
config = self._consumer_config
|
|
75
|
+
assert config is not None
|
|
76
|
+
self.consumer_id = config.consumer_id or default_consumer_id()
|
|
77
|
+
fenced = create_ergon_client(self._client_config, consumer_id=self.consumer_id)
|
|
78
|
+
previous = self._service.client
|
|
79
|
+
self._service.client = fenced
|
|
80
|
+
self._set_client(fenced)
|
|
81
|
+
if previous is not fenced:
|
|
82
|
+
close = getattr(previous, "close", None)
|
|
83
|
+
if callable(close):
|
|
84
|
+
try:
|
|
85
|
+
close()
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.debug("Closing the unfenced client failed", exc_info=True)
|
|
88
|
+
logger.info("Phase %s is Pull only; consuming with consumer id %s", config.phase_id, self.consumer_id)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import math
|
|
2
3
|
import mimetypes
|
|
4
|
+
import warnings
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Any, Dict, List, Optional
|
|
5
7
|
|
|
@@ -21,6 +23,15 @@ from .utils import (
|
|
|
21
23
|
latest_status_entry,
|
|
22
24
|
)
|
|
23
25
|
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
RELEASE_DELAY_DEPRECATION = (
|
|
29
|
+
"release_item(delay_seconds=...) relies on the item override "
|
|
30
|
+
"visibility_timeout_on_release_minutes, which the platform removes on 2026-11-27 "
|
|
31
|
+
"(ergon-platform #664). Make the phase 'Pull only' and the same call becomes a defer, "
|
|
32
|
+
"or call defer_transaction / fail_transaction directly."
|
|
33
|
+
)
|
|
34
|
+
|
|
24
35
|
|
|
25
36
|
class ErgonPlatformWorkflowsService:
|
|
26
37
|
"""Workflows HTTP, pagination, claim, and attachment service."""
|
|
@@ -142,10 +153,16 @@ class ErgonPlatformWorkflowsService:
|
|
|
142
153
|
delay_seconds: Optional[int] = None,
|
|
143
154
|
**fields: Any,
|
|
144
155
|
) -> Any:
|
|
156
|
+
"""Release a claim on a standard phase; ``delay_seconds`` hides the item once.
|
|
157
|
+
|
|
158
|
+
The delay uses the deprecated item override; see :data:`RELEASE_DELAY_DEPRECATION`.
|
|
159
|
+
"""
|
|
145
160
|
if delay_seconds is not None:
|
|
146
161
|
if delay_seconds < 0:
|
|
147
162
|
raise ValueError("delay_seconds must be a non-negative integer")
|
|
148
163
|
if delay_seconds > 0:
|
|
164
|
+
warnings.warn(RELEASE_DELAY_DEPRECATION, DeprecationWarning, stacklevel=3)
|
|
165
|
+
logger.warning(RELEASE_DELAY_DEPRECATION)
|
|
149
166
|
release_minutes = math.ceil(delay_seconds / 60)
|
|
150
167
|
self.client.workflows.items.update(
|
|
151
168
|
item_id,
|
|
@@ -153,6 +170,74 @@ class ErgonPlatformWorkflowsService:
|
|
|
153
170
|
)
|
|
154
171
|
return self.client.workflows.items.release(item_id, {**(data or {}), **fields})
|
|
155
172
|
|
|
173
|
+
# -- Pull only phases ---------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def get_phase(self, workflow_id: str, phase_id: str) -> Any:
|
|
176
|
+
"""The phase row from ``GET /workflows/{id}/phases`` (there is no single-phase GET)."""
|
|
177
|
+
phases = extract_items(self.client.workflows.workflow(workflow_id).phases())
|
|
178
|
+
for phase in phases:
|
|
179
|
+
if str(get_value(phase, "id", "")) == str(phase_id):
|
|
180
|
+
return phase
|
|
181
|
+
raise LookupError(f"Phase {phase_id} not found in workflow {workflow_id}")
|
|
182
|
+
|
|
183
|
+
def is_pull_phase(self, workflow_id: str, phase_id: str) -> bool:
|
|
184
|
+
return str(get_value(self.get_phase(workflow_id, phase_id), "kind", "")) == "pull"
|
|
185
|
+
|
|
186
|
+
def pull_items(
|
|
187
|
+
self,
|
|
188
|
+
workflow_id: str,
|
|
189
|
+
phase_id: str,
|
|
190
|
+
*,
|
|
191
|
+
limit: int,
|
|
192
|
+
filters: Optional[List[Dict[str, Any]]] = None,
|
|
193
|
+
wait_seconds: Optional[int] = None,
|
|
194
|
+
) -> List[Transaction]:
|
|
195
|
+
"""``POST /phases/{id}/items/pull``: atomically claim up to ``limit`` visible items."""
|
|
196
|
+
body: Dict[str, Any] = {"max": max(1, min(int(limit), 100))}
|
|
197
|
+
if filters:
|
|
198
|
+
body["filter"] = {"filters": list(filters)}
|
|
199
|
+
if wait_seconds:
|
|
200
|
+
body["wait_seconds"] = int(wait_seconds)
|
|
201
|
+
response = self.client.workflows.phases.pull_items(phase_id, body)
|
|
202
|
+
return [item_to_transaction(item, workflow_id) for item in extract_items(response)]
|
|
203
|
+
|
|
204
|
+
def complete_item(self, item_id: str, *, reason: Optional[str] = None) -> Any:
|
|
205
|
+
body: Dict[str, Any] = {}
|
|
206
|
+
if reason:
|
|
207
|
+
body["reason"] = reason
|
|
208
|
+
return self.client.workflows.items.complete(item_id, body)
|
|
209
|
+
|
|
210
|
+
def fail_item(
|
|
211
|
+
self,
|
|
212
|
+
item_id: str,
|
|
213
|
+
*,
|
|
214
|
+
reason: str,
|
|
215
|
+
delay_seconds: Optional[int] = None,
|
|
216
|
+
details: Optional[str] = None,
|
|
217
|
+
) -> Any:
|
|
218
|
+
body: Dict[str, Any] = {"reason": reason}
|
|
219
|
+
if delay_seconds is not None:
|
|
220
|
+
if delay_seconds < 0:
|
|
221
|
+
raise ValueError("delay_seconds must be a non-negative integer")
|
|
222
|
+
body["delay_seconds"] = int(delay_seconds)
|
|
223
|
+
if details:
|
|
224
|
+
body["details"] = details
|
|
225
|
+
return self.client.workflows.items.fail(item_id, body)
|
|
226
|
+
|
|
227
|
+
def defer_item(self, item_id: str, *, delay_seconds: int, reason: Optional[str] = None) -> Any:
|
|
228
|
+
if delay_seconds < 1:
|
|
229
|
+
raise ValueError("defer requires delay_seconds >= 1")
|
|
230
|
+
body: Dict[str, Any] = {"delay_seconds": int(delay_seconds)}
|
|
231
|
+
if reason:
|
|
232
|
+
body["reason"] = reason
|
|
233
|
+
return self.client.workflows.items.defer(item_id, body)
|
|
234
|
+
|
|
235
|
+
def extend_claim(self, item_id: str, *, seconds: Optional[int] = None) -> Any:
|
|
236
|
+
body: Dict[str, Any] = {}
|
|
237
|
+
if seconds is not None:
|
|
238
|
+
body["seconds"] = int(seconds)
|
|
239
|
+
return self.client.workflows.items.extend_claim(item_id, body)
|
|
240
|
+
|
|
156
241
|
def get_pipeline_result(
|
|
157
242
|
self,
|
|
158
243
|
workflow_id: str,
|
|
@@ -6,6 +6,7 @@ from ...connector import AsyncConnector
|
|
|
6
6
|
from ...transaction import Transaction
|
|
7
7
|
from .._client import create_ergon_client
|
|
8
8
|
from ..models import ErgonPlatformClient
|
|
9
|
+
from ._mode import DEFAULT_NACK_REASON, DEFAULT_NACK_RELEASE_DELAY_SECONDS, ConsumerModeState
|
|
9
10
|
from ._operations import ErgonPlatformWorkflowsService
|
|
10
11
|
from .models import (
|
|
11
12
|
CreateItemPayload,
|
|
@@ -29,6 +30,21 @@ class AsyncErgonPlatformConnector(AsyncConnector):
|
|
|
29
30
|
self._operations = self.service
|
|
30
31
|
self._consumer_config = consumer_config
|
|
31
32
|
self._producer_config = producer_config or ErgonPlatformProducerConfig()
|
|
33
|
+
self._mode = ConsumerModeState(client, consumer_config, self.service, self._set_client)
|
|
34
|
+
|
|
35
|
+
def _set_client(self, client: Any) -> None:
|
|
36
|
+
self.client = client
|
|
37
|
+
|
|
38
|
+
async def is_pull_async(self) -> bool:
|
|
39
|
+
"""True when the consumed phase is Pull only (resolved once, on first use)."""
|
|
40
|
+
if self._mode.resolved is not None:
|
|
41
|
+
return self._mode.resolved
|
|
42
|
+
return await asyncio.to_thread(self._mode.is_pull)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def consumer_id(self) -> Optional[str]:
|
|
46
|
+
"""The ``X-Ergon-Consumer-Id`` this connector claims under; None on standard phases."""
|
|
47
|
+
return self._mode.consumer_id
|
|
32
48
|
|
|
33
49
|
async def fetch_transactions_async(
|
|
34
50
|
self,
|
|
@@ -41,6 +57,19 @@ class AsyncErgonPlatformConnector(AsyncConnector):
|
|
|
41
57
|
|
|
42
58
|
config = self._consumer_config
|
|
43
59
|
limit = batch_size or config.batch_size
|
|
60
|
+
if await self.is_pull_async():
|
|
61
|
+
if kwargs:
|
|
62
|
+
logger.debug("Ignoring list params %s on Pull only phase %s", sorted(kwargs), config.phase_id)
|
|
63
|
+
return await asyncio.to_thread(
|
|
64
|
+
lambda: self._operations.pull_items(
|
|
65
|
+
config.workflow_id,
|
|
66
|
+
config.phase_id,
|
|
67
|
+
limit=limit,
|
|
68
|
+
filters=config.pull_filters,
|
|
69
|
+
wait_seconds=config.pull_wait_seconds,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
44
73
|
params: Dict[str, Any] = {**config.list_params, **kwargs}
|
|
45
74
|
if config.unassigned:
|
|
46
75
|
params.pop("assigned_to", None)
|
|
@@ -117,12 +146,15 @@ class AsyncErgonPlatformConnector(AsyncConnector):
|
|
|
117
146
|
)
|
|
118
147
|
|
|
119
148
|
async def ack_transaction(self, transaction: Transaction, phase_id: Optional[str] = None) -> None:
|
|
149
|
+
"""Route to ``phase_id`` / ``ack_phase_id``; on a Pull only phase with no target, complete the stay."""
|
|
120
150
|
target_phase = phase_id
|
|
121
151
|
if target_phase is None and self._consumer_config is not None:
|
|
122
152
|
target_phase = self._consumer_config.ack_phase_id
|
|
123
|
-
if
|
|
153
|
+
if target_phase:
|
|
154
|
+
await asyncio.to_thread(lambda: self.client.workflows.items.route(transaction.id, to_phase_id=target_phase))
|
|
124
155
|
return
|
|
125
|
-
await
|
|
156
|
+
if await self.is_pull_async():
|
|
157
|
+
await asyncio.to_thread(lambda: self._operations.complete_item(transaction.id))
|
|
126
158
|
|
|
127
159
|
async def release_item(
|
|
128
160
|
self,
|
|
@@ -132,6 +164,9 @@ class AsyncErgonPlatformConnector(AsyncConnector):
|
|
|
132
164
|
delay_seconds: Optional[int] = None,
|
|
133
165
|
**fields: Any,
|
|
134
166
|
) -> Any:
|
|
167
|
+
"""Give the item back. ``delay_seconds`` > 0 is a defer on Pull only phases."""
|
|
168
|
+
if delay_seconds and await self.is_pull_async():
|
|
169
|
+
return await asyncio.to_thread(lambda: self._operations.defer_item(item_id, delay_seconds=delay_seconds))
|
|
135
170
|
return await asyncio.to_thread(
|
|
136
171
|
lambda: self._operations.release_item(
|
|
137
172
|
item_id,
|
|
@@ -141,9 +176,29 @@ class AsyncErgonPlatformConnector(AsyncConnector):
|
|
|
141
176
|
)
|
|
142
177
|
)
|
|
143
178
|
|
|
144
|
-
async def nack_transaction(
|
|
179
|
+
async def nack_transaction(
|
|
180
|
+
self,
|
|
181
|
+
transaction: Transaction,
|
|
182
|
+
requeue: bool = True,
|
|
183
|
+
delay_seconds: Optional[int] = None,
|
|
184
|
+
*,
|
|
185
|
+
reason: Optional[str] = None,
|
|
186
|
+
details: Optional[str] = None,
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Hand the item back after a failure; see :meth:`ErgonPlatformConnector.nack_transaction`."""
|
|
145
189
|
if requeue:
|
|
146
|
-
await
|
|
190
|
+
if await self.is_pull_async():
|
|
191
|
+
await asyncio.to_thread(
|
|
192
|
+
lambda: self._operations.fail_item(
|
|
193
|
+
transaction.id,
|
|
194
|
+
reason=reason or DEFAULT_NACK_REASON,
|
|
195
|
+
delay_seconds=delay_seconds,
|
|
196
|
+
details=details,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return
|
|
200
|
+
resolved_delay = DEFAULT_NACK_RELEASE_DELAY_SECONDS if delay_seconds is None else delay_seconds
|
|
201
|
+
await asyncio.to_thread(lambda: self._operations.release_item(transaction.id, delay_seconds=resolved_delay))
|
|
147
202
|
return
|
|
148
203
|
|
|
149
204
|
target_phase = self._consumer_config.nack_phase_id if self._consumer_config is not None else None
|
|
@@ -152,6 +207,39 @@ class AsyncErgonPlatformConnector(AsyncConnector):
|
|
|
152
207
|
|
|
153
208
|
await asyncio.to_thread(lambda: self.client.workflows.items.route(transaction.id, to_phase_id=target_phase))
|
|
154
209
|
|
|
210
|
+
# -- Pull only outcomes ----------------------------------------------------
|
|
211
|
+
|
|
212
|
+
async def complete_transaction(self, transaction: Transaction, *, reason: Optional[str] = None) -> Any:
|
|
213
|
+
await self.is_pull_async()
|
|
214
|
+
return await asyncio.to_thread(lambda: self._operations.complete_item(transaction.id, reason=reason))
|
|
215
|
+
|
|
216
|
+
async def fail_transaction(
|
|
217
|
+
self,
|
|
218
|
+
transaction: Transaction,
|
|
219
|
+
*,
|
|
220
|
+
reason: str,
|
|
221
|
+
delay_seconds: Optional[int] = None,
|
|
222
|
+
details: Optional[str] = None,
|
|
223
|
+
) -> Any:
|
|
224
|
+
await self.is_pull_async()
|
|
225
|
+
return await asyncio.to_thread(
|
|
226
|
+
lambda: self._operations.fail_item(
|
|
227
|
+
transaction.id, reason=reason, delay_seconds=delay_seconds, details=details
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
async def defer_transaction(
|
|
232
|
+
self, transaction: Transaction, *, delay_seconds: int, reason: Optional[str] = None
|
|
233
|
+
) -> Any:
|
|
234
|
+
await self.is_pull_async()
|
|
235
|
+
return await asyncio.to_thread(
|
|
236
|
+
lambda: self._operations.defer_item(transaction.id, delay_seconds=delay_seconds, reason=reason)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
async def extend_claim(self, transaction: Transaction, *, seconds: Optional[int] = None) -> Any:
|
|
240
|
+
await self.is_pull_async()
|
|
241
|
+
return await asyncio.to_thread(lambda: self._operations.extend_claim(transaction.id, seconds=seconds))
|
|
242
|
+
|
|
155
243
|
async def close(self) -> None:
|
|
156
244
|
await asyncio.to_thread(self.client.close)
|
|
157
245
|
|
|
@@ -5,6 +5,7 @@ from ...connector import Connector
|
|
|
5
5
|
from ...transaction import Transaction
|
|
6
6
|
from .._client import create_ergon_client
|
|
7
7
|
from ..models import ErgonPlatformClient
|
|
8
|
+
from ._mode import DEFAULT_NACK_REASON, DEFAULT_NACK_RELEASE_DELAY_SECONDS, ConsumerModeState
|
|
8
9
|
from ._operations import ErgonPlatformWorkflowsService
|
|
9
10
|
from .models import (
|
|
10
11
|
CreateItemPayload,
|
|
@@ -28,6 +29,20 @@ class ErgonPlatformConnector(Connector):
|
|
|
28
29
|
self._operations = self.service
|
|
29
30
|
self._consumer_config = consumer_config
|
|
30
31
|
self._producer_config = producer_config or ErgonPlatformProducerConfig()
|
|
32
|
+
self._mode = ConsumerModeState(client, consumer_config, self.service, self._set_client)
|
|
33
|
+
|
|
34
|
+
def _set_client(self, client: Any) -> None:
|
|
35
|
+
self.client = client
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_pull(self) -> bool:
|
|
39
|
+
"""True when the consumed phase is Pull only (resolved once, on first use)."""
|
|
40
|
+
return self._mode.is_pull()
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def consumer_id(self) -> Optional[str]:
|
|
44
|
+
"""The ``X-Ergon-Consumer-Id`` this connector claims under; None on standard phases."""
|
|
45
|
+
return self._mode.consumer_id
|
|
31
46
|
|
|
32
47
|
def fetch_transactions(
|
|
33
48
|
self,
|
|
@@ -40,6 +55,17 @@ class ErgonPlatformConnector(Connector):
|
|
|
40
55
|
|
|
41
56
|
config = self._consumer_config
|
|
42
57
|
limit = batch_size or config.batch_size
|
|
58
|
+
if self.is_pull:
|
|
59
|
+
if kwargs:
|
|
60
|
+
logger.debug("Ignoring list params %s on Pull only phase %s", sorted(kwargs), config.phase_id)
|
|
61
|
+
return self._operations.pull_items(
|
|
62
|
+
config.workflow_id,
|
|
63
|
+
config.phase_id,
|
|
64
|
+
limit=limit,
|
|
65
|
+
filters=config.pull_filters,
|
|
66
|
+
wait_seconds=config.pull_wait_seconds,
|
|
67
|
+
)
|
|
68
|
+
|
|
43
69
|
params: Dict[str, Any] = {**config.list_params, **kwargs}
|
|
44
70
|
if config.unassigned:
|
|
45
71
|
params.pop("assigned_to", None)
|
|
@@ -105,12 +131,15 @@ class ErgonPlatformConnector(Connector):
|
|
|
105
131
|
)
|
|
106
132
|
|
|
107
133
|
def ack_transaction(self, transaction: Transaction, phase_id: Optional[str] = None) -> None:
|
|
134
|
+
"""Route to ``phase_id`` / ``ack_phase_id``; on a Pull only phase with no target, complete the stay."""
|
|
108
135
|
target_phase = phase_id
|
|
109
136
|
if target_phase is None and self._consumer_config is not None:
|
|
110
137
|
target_phase = self._consumer_config.ack_phase_id
|
|
111
|
-
if
|
|
138
|
+
if target_phase:
|
|
139
|
+
self.client.workflows.items.route(transaction.id, to_phase_id=target_phase)
|
|
112
140
|
return
|
|
113
|
-
self.
|
|
141
|
+
if self.is_pull:
|
|
142
|
+
self._operations.complete_item(transaction.id)
|
|
114
143
|
|
|
115
144
|
def release_item(
|
|
116
145
|
self,
|
|
@@ -120,6 +149,9 @@ class ErgonPlatformConnector(Connector):
|
|
|
120
149
|
delay_seconds: Optional[int] = None,
|
|
121
150
|
**fields: Any,
|
|
122
151
|
) -> Any:
|
|
152
|
+
"""Give the item back. ``delay_seconds`` > 0 is a defer on Pull only phases."""
|
|
153
|
+
if self.is_pull and delay_seconds:
|
|
154
|
+
return self._operations.defer_item(item_id, delay_seconds=delay_seconds)
|
|
123
155
|
return self._operations.release_item(
|
|
124
156
|
item_id,
|
|
125
157
|
data,
|
|
@@ -127,8 +159,33 @@ class ErgonPlatformConnector(Connector):
|
|
|
127
159
|
**fields,
|
|
128
160
|
)
|
|
129
161
|
|
|
130
|
-
def nack_transaction(
|
|
162
|
+
def nack_transaction(
|
|
163
|
+
self,
|
|
164
|
+
transaction: Transaction,
|
|
165
|
+
requeue: bool = True,
|
|
166
|
+
delay_seconds: Optional[int] = None,
|
|
167
|
+
*,
|
|
168
|
+
reason: Optional[str] = None,
|
|
169
|
+
details: Optional[str] = None,
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Hand the item back after a failure.
|
|
172
|
+
|
|
173
|
+
Pull only phase: ``fail`` (counts an attempt; the platform moves the item
|
|
174
|
+
to Exceptions at ``max_attempts``), retried after ``delay_seconds`` or the
|
|
175
|
+
phase retry policy. Standard phase: release with a ``delay_seconds``
|
|
176
|
+
cooldown (default 10 s). ``requeue=False`` routes to ``nack_phase_id`` on both.
|
|
177
|
+
"""
|
|
131
178
|
if requeue:
|
|
179
|
+
if self.is_pull:
|
|
180
|
+
self._operations.fail_item(
|
|
181
|
+
transaction.id,
|
|
182
|
+
reason=reason or DEFAULT_NACK_REASON,
|
|
183
|
+
delay_seconds=delay_seconds,
|
|
184
|
+
details=details,
|
|
185
|
+
)
|
|
186
|
+
return
|
|
187
|
+
if delay_seconds is None:
|
|
188
|
+
delay_seconds = DEFAULT_NACK_RELEASE_DELAY_SECONDS
|
|
132
189
|
self._operations.release_item(transaction.id, delay_seconds=delay_seconds)
|
|
133
190
|
return
|
|
134
191
|
|
|
@@ -139,6 +196,35 @@ class ErgonPlatformConnector(Connector):
|
|
|
139
196
|
self.client.workflows.items.route(transaction.id, to_phase_id=target_phase)
|
|
140
197
|
logger.debug("Item %s moved to nack phase %s", transaction.id, target_phase)
|
|
141
198
|
|
|
199
|
+
# -- Pull only outcomes ----------------------------------------------------
|
|
200
|
+
|
|
201
|
+
def complete_transaction(self, transaction: Transaction, *, reason: Optional[str] = None) -> Any:
|
|
202
|
+
"""Mark the current stay done; the item stays in the phase and pull skips it."""
|
|
203
|
+
self.is_pull # resolve the mode so the call carries the consumer id
|
|
204
|
+
return self._operations.complete_item(transaction.id, reason=reason)
|
|
205
|
+
|
|
206
|
+
def fail_transaction(
|
|
207
|
+
self,
|
|
208
|
+
transaction: Transaction,
|
|
209
|
+
*,
|
|
210
|
+
reason: str,
|
|
211
|
+
delay_seconds: Optional[int] = None,
|
|
212
|
+
details: Optional[str] = None,
|
|
213
|
+
) -> Any:
|
|
214
|
+
"""Record a failed attempt; visible again after ``delay_seconds`` (or the phase retry policy)."""
|
|
215
|
+
self.is_pull
|
|
216
|
+
return self._operations.fail_item(transaction.id, reason=reason, delay_seconds=delay_seconds, details=details)
|
|
217
|
+
|
|
218
|
+
def defer_transaction(self, transaction: Transaction, *, delay_seconds: int, reason: Optional[str] = None) -> Any:
|
|
219
|
+
"""Put the item back without counting an attempt; hidden for ``delay_seconds``."""
|
|
220
|
+
self.is_pull
|
|
221
|
+
return self._operations.defer_item(transaction.id, delay_seconds=delay_seconds, reason=reason)
|
|
222
|
+
|
|
223
|
+
def extend_claim(self, transaction: Transaction, *, seconds: Optional[int] = None) -> Any:
|
|
224
|
+
"""Push the claim expiry out; call from long steps so the claim does not lapse."""
|
|
225
|
+
self.is_pull
|
|
226
|
+
return self._operations.extend_claim(transaction.id, seconds=seconds)
|
|
227
|
+
|
|
142
228
|
def close(self) -> None:
|
|
143
229
|
self.client.close()
|
|
144
230
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from typing import Any, Dict, Optional, Union
|
|
1
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
+
ConsumerMode = Literal["auto", "list_claim", "pull"]
|
|
6
|
+
|
|
5
7
|
# Status buckets returned by the Ergon Platform attachment pipeline.
|
|
6
8
|
SUCCESS_STATUSES = {"completed", "complete", "succeeded", "success", "processed", "ready"}
|
|
7
9
|
FAILURE_STATUSES = {"failed", "error", "errored", "skipped", "oversized"}
|
|
@@ -41,6 +43,37 @@ class ErgonPlatformConsumerConfig(BaseModel):
|
|
|
41
43
|
default_factory=dict,
|
|
42
44
|
description="Extra query params forwarded to the items listing endpoint",
|
|
43
45
|
)
|
|
46
|
+
mode: ConsumerMode = Field(
|
|
47
|
+
default="auto",
|
|
48
|
+
description=(
|
|
49
|
+
"How items are taken from the phase. 'list_claim' lists and claims one by one "
|
|
50
|
+
"(standard phases). 'pull' uses POST /phases/{id}/items/pull with a consumer id "
|
|
51
|
+
"(Pull only phases): atomic claim with expiry, fail/defer/complete outcomes, "
|
|
52
|
+
"attempt limit handled by the platform. 'auto' (default) reads the phase kind "
|
|
53
|
+
"once, on the first fetch, and picks the matching behaviour."
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
pull_filters: Optional[List[Dict[str, Any]]] = Field(
|
|
57
|
+
default=None,
|
|
58
|
+
description=(
|
|
59
|
+
"Pull mode only. Field filters a pulled item must match, same shape as the "
|
|
60
|
+
"'filters' list of POST /items/query (e.g. [{'field_id': ..., 'op': 'eq', 'value': ...}])."
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
pull_wait_seconds: Optional[int] = Field(
|
|
64
|
+
default=None,
|
|
65
|
+
ge=0,
|
|
66
|
+
le=30,
|
|
67
|
+
description="Pull mode only. Long-poll up to this many seconds when the phase has no visible item.",
|
|
68
|
+
)
|
|
69
|
+
consumer_id: Optional[str] = Field(
|
|
70
|
+
default=None,
|
|
71
|
+
description=(
|
|
72
|
+
"Pull mode only. Worker identity sent as X-Ergon-Consumer-Id; claims taken under it "
|
|
73
|
+
"expire and are fenced against other consumers. Defaults to '<hostname>:<pid>'. "
|
|
74
|
+
"Allowed: [A-Za-z0-9._:@-]{1,128}."
|
|
75
|
+
),
|
|
76
|
+
)
|
|
44
77
|
|
|
45
78
|
|
|
46
79
|
class ErgonPlatformProducerConfig(BaseModel):
|