ergon-framework-python 0.2.5__tar.gz → 0.3.1__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.
Files changed (134) hide show
  1. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/PKG-INFO +3 -3
  2. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/pyproject.toml +3 -3
  3. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/__init__.py +1 -1
  4. ergon_framework_python-0.3.1/src/ergon/connector/ergon_platform/_client.py +52 -0
  5. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/connector.py +4 -0
  6. ergon_framework_python-0.3.1/src/ergon/connector/ergon_platform/workflows/_mode.py +88 -0
  7. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/_operations.py +85 -0
  8. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/async_connector.py +92 -4
  9. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/connector.py +96 -3
  10. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/models.py +34 -1
  11. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/connector.py +5 -0
  12. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/connector.py +5 -0
  13. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/helpers.py +20 -1
  14. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/mixins/consumer.py +71 -2
  15. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/runner.py +114 -70
  16. ergon_framework_python-0.3.1/src/ergon/task/shutdown.py +275 -0
  17. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/utils.py +14 -3
  18. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/PKG-INFO +3 -3
  19. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/SOURCES.txt +2 -0
  20. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/requires.txt +2 -2
  21. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/tests/test_smoke.py +1 -1
  22. ergon_framework_python-0.2.5/src/ergon/connector/ergon_platform/_client.py +0 -26
  23. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/LICENSE +0 -0
  24. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/README.md +0 -0
  25. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/setup.cfg +0 -0
  26. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/__init__.py +0 -0
  27. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +0 -0
  28. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/grafana.yaml +0 -0
  29. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/loki.yaml +0 -0
  30. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +0 -0
  31. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/prometheus.yaml +0 -0
  32. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/_observability/tempo.yaml +0 -0
  33. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
  34. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/main.py +0 -0
  35. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
  36. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/constants.py +0 -0
  37. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
  38. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/config.py +0 -0
  39. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +0 -0
  40. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +0 -0
  41. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +0 -0
  42. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/example_task/task.py +0 -0
  43. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
  44. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
  45. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
  46. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/bootstrap/src/__project__/tasks/settings.py +0 -0
  47. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/cli.py +0 -0
  48. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/__init__.py +0 -0
  49. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/_lazy.py +0 -0
  50. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/connector.py +0 -0
  51. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/__init__.py +0 -0
  52. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/async_connector.py +0 -0
  53. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/__init__.py +0 -0
  54. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/adapters.py +0 -0
  55. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/async_connector.py +0 -0
  56. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/models.py +0 -0
  57. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/__init__.py +0 -0
  58. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/activity.py +0 -0
  59. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/addresses.py +0 -0
  60. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/attachments.py +0 -0
  61. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/messages.py +0 -0
  62. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/records.py +0 -0
  63. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/channels/services/service.py +0 -0
  64. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/connector.py +0 -0
  65. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/models.py +0 -0
  66. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/utils.py +0 -0
  67. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/__init__.py +0 -0
  68. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/ergon_platform/workflows/utils.py +0 -0
  69. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/excel/__init__.py +0 -0
  70. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/excel/connector.py +0 -0
  71. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/excel/models.py +0 -0
  72. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/excel/service.py +0 -0
  73. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/__init__.py +0 -0
  74. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/async_auth_service.py +0 -0
  75. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/async_connector.py +0 -0
  76. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/async_service.py +0 -0
  77. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/auth_service.py +0 -0
  78. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/models.py +0 -0
  79. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/service.py +0 -0
  80. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/nylas/utils.py +0 -0
  81. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/__init__.py +0 -0
  82. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/async_connector.py +0 -0
  83. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/async_service.py +0 -0
  84. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/models.py +0 -0
  85. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/service.py +0 -0
  86. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/outlook/utils.py +0 -0
  87. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/__init__.py +0 -0
  88. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/async_connector.py +0 -0
  89. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/async_service.py +0 -0
  90. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/connector.py +0 -0
  91. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/models.py +0 -0
  92. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/service.py +0 -0
  93. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/pipefy/version.py +0 -0
  94. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/postgres/__init__.py +0 -0
  95. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/postgres/async_connector.py +0 -0
  96. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/postgres/async_service.py +0 -0
  97. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/postgres/models.py +0 -0
  98. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/__init__.py +0 -0
  99. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/async_connector.py +0 -0
  100. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/async_service.py +0 -0
  101. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/connector.py +0 -0
  102. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/helper.py +0 -0
  103. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/models.py +0 -0
  104. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/rabbitmq/service.py +0 -0
  105. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/__init__.py +0 -0
  106. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/async_connector.py +0 -0
  107. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/async_service.py +0 -0
  108. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/connector.py +0 -0
  109. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/models.py +0 -0
  110. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/sqs/service.py +0 -0
  111. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/connector/transaction.py +0 -0
  112. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/py.typed +0 -0
  113. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/service/__init__.py +0 -0
  114. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/service/service.py +0 -0
  115. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/__init__.py +0 -0
  116. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/base.py +0 -0
  117. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/exceptions.py +0 -0
  118. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/liveness.py +0 -0
  119. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/manager.py +0 -0
  120. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/mixins/__init__.py +0 -0
  121. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/mixins/metrics.py +0 -0
  122. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/mixins/producer.py +0 -0
  123. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/task/policies.py +0 -0
  124. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/telemetry/__init__.py +0 -0
  125. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/telemetry/_resource.py +0 -0
  126. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/telemetry/logging.py +0 -0
  127. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/telemetry/metrics.py +0 -0
  128. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/telemetry/tracing.py +0 -0
  129. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/utils/__init__.py +0 -0
  130. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon/utils/env.py +0 -0
  131. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/dependency_links.txt +0 -0
  132. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/entry_points.txt +0 -0
  133. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/src/ergon_framework_python.egg-info/top_level.txt +0 -0
  134. {ergon_framework_python-0.2.5 → ergon_framework_python-0.3.1}/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.2.5
3
+ Version: 0.3.1
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.5,>=0.2.0; extra == "all"
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.5,>=0.2.0; extra == "ergon-platform"
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.2.5"
3
+ version = "0.3.1"
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.2.0,<0.5",
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.2.0,<0.5",
46
+ "ergon-platform-sdk>=0.5.1,<0.6",
47
47
  "httpx>=0.28.0",
48
48
  ]
49
49
  excel = ["openpyxl>=3.1.5"]
@@ -10,4 +10,4 @@ __all__ = [
10
10
  "manager",
11
11
  ]
12
12
 
13
- __version__ = "0.2.5"
13
+ __version__ = "0.3.1"
@@ -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
+ )
@@ -208,6 +208,10 @@ class ErgonPlatformChannelsConnector(Connector):
208
208
  if requeue and transaction.id:
209
209
  self._seen_event_ids.discard(transaction.id)
210
210
 
211
+ def release_transaction(self, transaction: Transaction) -> None:
212
+ """Give back a fetched but unstarted inbox event (worker shutdown): requeued with no delay."""
213
+ self.nack_transaction(transaction, requeue=True, delay_seconds=0)
214
+
211
215
  def download_attachments(
212
216
  self,
213
217
  transaction: Transaction,
@@ -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 not target_phase:
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 asyncio.to_thread(lambda: self.client.workflows.items.route(transaction.id, to_phase_id=target_phase))
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(self, transaction: Transaction, requeue: bool = True, delay_seconds: int = 10) -> None:
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 asyncio.to_thread(lambda: self._operations.release_item(transaction.id, delay_seconds=delay_seconds))
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 not target_phase:
138
+ if target_phase:
139
+ self.client.workflows.items.route(transaction.id, to_phase_id=target_phase)
112
140
  return
113
- self.client.workflows.items.route(transaction.id, to_phase_id=target_phase)
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,40 @@ class ErgonPlatformConnector(Connector):
127
159
  **fields,
128
160
  )
129
161
 
130
- def nack_transaction(self, transaction: Transaction, requeue: bool = True, delay_seconds: int = 10) -> None:
162
+ def release_transaction(self, transaction: Transaction) -> None:
163
+ """Give back a transaction that was fetched but never started (worker shutdown).
164
+
165
+ A plain release: no failure reason, no cooldown, so it is picked up again right away.
166
+ """
167
+ self.release_item(transaction.id)
168
+
169
+ def nack_transaction(
170
+ self,
171
+ transaction: Transaction,
172
+ requeue: bool = True,
173
+ delay_seconds: Optional[int] = None,
174
+ *,
175
+ reason: Optional[str] = None,
176
+ details: Optional[str] = None,
177
+ ) -> None:
178
+ """Hand the item back after a failure.
179
+
180
+ Pull only phase: ``fail`` (counts an attempt; the platform moves the item
181
+ to Exceptions at ``max_attempts``), retried after ``delay_seconds`` or the
182
+ phase retry policy. Standard phase: release with a ``delay_seconds``
183
+ cooldown (default 10 s). ``requeue=False`` routes to ``nack_phase_id`` on both.
184
+ """
131
185
  if requeue:
186
+ if self.is_pull:
187
+ self._operations.fail_item(
188
+ transaction.id,
189
+ reason=reason or DEFAULT_NACK_REASON,
190
+ delay_seconds=delay_seconds,
191
+ details=details,
192
+ )
193
+ return
194
+ if delay_seconds is None:
195
+ delay_seconds = DEFAULT_NACK_RELEASE_DELAY_SECONDS
132
196
  self._operations.release_item(transaction.id, delay_seconds=delay_seconds)
133
197
  return
134
198
 
@@ -139,6 +203,35 @@ class ErgonPlatformConnector(Connector):
139
203
  self.client.workflows.items.route(transaction.id, to_phase_id=target_phase)
140
204
  logger.debug("Item %s moved to nack phase %s", transaction.id, target_phase)
141
205
 
206
+ # -- Pull only outcomes ----------------------------------------------------
207
+
208
+ def complete_transaction(self, transaction: Transaction, *, reason: Optional[str] = None) -> Any:
209
+ """Mark the current stay done; the item stays in the phase and pull skips it."""
210
+ self.is_pull # resolve the mode so the call carries the consumer id
211
+ return self._operations.complete_item(transaction.id, reason=reason)
212
+
213
+ def fail_transaction(
214
+ self,
215
+ transaction: Transaction,
216
+ *,
217
+ reason: str,
218
+ delay_seconds: Optional[int] = None,
219
+ details: Optional[str] = None,
220
+ ) -> Any:
221
+ """Record a failed attempt; visible again after ``delay_seconds`` (or the phase retry policy)."""
222
+ self.is_pull
223
+ return self._operations.fail_item(transaction.id, reason=reason, delay_seconds=delay_seconds, details=details)
224
+
225
+ def defer_transaction(self, transaction: Transaction, *, delay_seconds: int, reason: Optional[str] = None) -> Any:
226
+ """Put the item back without counting an attempt; hidden for ``delay_seconds``."""
227
+ self.is_pull
228
+ return self._operations.defer_item(transaction.id, delay_seconds=delay_seconds, reason=reason)
229
+
230
+ def extend_claim(self, transaction: Transaction, *, seconds: Optional[int] = None) -> Any:
231
+ """Push the claim expiry out; call from long steps so the claim does not lapse."""
232
+ self.is_pull
233
+ return self._operations.extend_claim(transaction.id, seconds=seconds)
234
+
142
235
  def close(self) -> None:
143
236
  self.client.close()
144
237
 
@@ -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):