pulse-engine 0.2.7__tar.gz → 0.2.9__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 (151) hide show
  1. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/PKG-INFO +1 -1
  2. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/pyproject.toml +1 -1
  3. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/runners/prefect_pipeline_flow.py +93 -3
  4. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/README.md +0 -0
  5. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/__init__.py +0 -0
  6. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/__init__.py +0 -0
  7. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/audio_transcription.py +0 -0
  8. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/batcher.py +0 -0
  9. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/digital_news.py +0 -0
  10. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/digital_news_metadata.py +0 -0
  11. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/exceptions.py +0 -0
  12. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/models.py +0 -0
  13. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/opensearch_storage.py +0 -0
  14. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/speech_content.py +0 -0
  15. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/speech_metadata.py +0 -0
  16. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/twitter.py +0 -0
  17. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/youtube_downloader.py +0 -0
  18. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/adapters/youtube_metadata.py +0 -0
  19. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/api/__init__.py +0 -0
  20. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/api/v1/__init__.py +0 -0
  21. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/api/v1/auth.py +0 -0
  22. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/api/v1/health.py +0 -0
  23. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/api/v1/router.py +0 -0
  24. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/chain_recovery.py +0 -0
  25. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/__init__.py +0 -0
  26. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/main.py +0 -0
  27. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/templates/cookiecutter.json +0 -0
  28. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/.gitignore +0 -0
  29. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/Dockerfile +0 -0
  30. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pipeline.yaml +0 -0
  31. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pyproject.toml +0 -0
  32. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/src/pulse_{{cookiecutter.product_slug}}/__init__.py +0 -0
  33. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/__init__.py +0 -0
  34. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/__init__.py +0 -0
  35. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/test_manifest.py +0 -0
  36. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/client.py +0 -0
  37. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/config.py +0 -0
  38. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/core/__init__.py +0 -0
  39. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/core/error_handlers.py +0 -0
  40. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/core/exceptions.py +0 -0
  41. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/core/job_token.py +0 -0
  42. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/core/logging.py +0 -0
  43. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/core/scope.py +0 -0
  44. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/core/security.py +0 -0
  45. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/database.py +0 -0
  46. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/dependencies.py +0 -0
  47. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/__init__.py +0 -0
  48. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/backend_deployment_repository.py +0 -0
  49. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/backends/__init__.py +0 -0
  50. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/backends/base.py +0 -0
  51. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/backends/exceptions.py +0 -0
  52. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/backends/native_lambda.py +0 -0
  53. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/backends/prefect_ecs.py +0 -0
  54. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/backends/prefect_k8s.py +0 -0
  55. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/backends/registry.py +0 -0
  56. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/infra_provisioner.py +0 -0
  57. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/job_launcher.py +0 -0
  58. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/models.py +0 -0
  59. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/repository.py +0 -0
  60. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/router.py +0 -0
  61. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/schemas.py +0 -0
  62. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/deployment/service.py +0 -0
  63. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/__init__.py +0 -0
  64. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/adapters/__init__.py +0 -0
  65. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/base.py +0 -0
  66. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/models.py +0 -0
  67. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/orchestrator/__init__.py +0 -0
  68. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/orchestrator/base.py +0 -0
  69. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/orchestrator/noop.py +0 -0
  70. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/orchestrator/prefect.py +0 -0
  71. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/repository.py +0 -0
  72. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/router.py +0 -0
  73. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/schemas.py +0 -0
  74. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/service.py +0 -0
  75. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/stage_models.py +0 -0
  76. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/extractor/stage_repository.py +0 -0
  77. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/main.py +0 -0
  78. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/mcp/__init__.py +0 -0
  79. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/mcp/__main__.py +0 -0
  80. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/mcp/server.py +0 -0
  81. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/mcp/tools_jobs.py +0 -0
  82. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/mcp/tools_kb.py +0 -0
  83. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/mcp/tools_modules.py +0 -0
  84. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/mcp/tools_pipelines.py +0 -0
  85. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/mcp/tools_processor.py +0 -0
  86. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/middleware/__init__.py +0 -0
  87. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/middleware/rate_limit.py +0 -0
  88. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/middleware/request_id.py +0 -0
  89. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/middleware/security_headers.py +0 -0
  90. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/middleware/tenant.py +0 -0
  91. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/notifications/__init__.py +0 -0
  92. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/notifications/apprise_notifier.py +0 -0
  93. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/__init__.py +0 -0
  94. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/config_parser.py +0 -0
  95. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/expression.py +0 -0
  96. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/models.py +0 -0
  97. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/repositories.py +0 -0
  98. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/router_modules.py +0 -0
  99. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/router_pipelines.py +0 -0
  100. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/schemas.py +0 -0
  101. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/service.py +0 -0
  102. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/translators/__init__.py +0 -0
  103. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/translators/airflow_status.py +0 -0
  104. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/translators/airflow_translator.py +0 -0
  105. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/translators/base.py +0 -0
  106. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/translators/prefect_status.py +0 -0
  107. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/pipeline/translators/prefect_translator.py +0 -0
  108. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/__init__.py +0 -0
  109. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/base.py +0 -0
  110. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/core/__init__.py +0 -0
  111. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/core/analysis.py +0 -0
  112. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/core/chunking.py +0 -0
  113. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/core/prompts.py +0 -0
  114. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/core/topic_splitter.py +0 -0
  115. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/defaults/__init__.py +0 -0
  116. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/defaults/core_processor.py +0 -0
  117. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/defaults/postprocessor.py +0 -0
  118. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/defaults/preprocessor.py +0 -0
  119. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/llm/__init__.py +0 -0
  120. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/llm/provider.py +0 -0
  121. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/ocr/gemini.py +0 -0
  122. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/pipeline.py +0 -0
  123. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/postprocessor/__init__.py +0 -0
  124. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/postprocessor/embeddings.py +0 -0
  125. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/postprocessor/tasks.py +0 -0
  126. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/preprocessor/__init__.py +0 -0
  127. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/preprocessor/tasks.py +0 -0
  128. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/router.py +0 -0
  129. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/processor/schemas.py +0 -0
  130. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/registry.py +0 -0
  131. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/runners/__init__.py +0 -0
  132. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/runners/lambda_runner.py +0 -0
  133. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/runners/pipeline_runner.py +0 -0
  134. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/runners/prefect_runner.py +0 -0
  135. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/s3.py +0 -0
  136. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/secrets.py +0 -0
  137. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/services/__init__.py +0 -0
  138. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/services/bootstrap.py +0 -0
  139. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/services/opensearch.py +0 -0
  140. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/storage/__init__.py +0 -0
  141. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/storage/connectors/__init__.py +0 -0
  142. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/storage/connectors/athena.py +0 -0
  143. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/storage/connectors/base.py +0 -0
  144. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/storage/connectors/opensearch.py +0 -0
  145. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/storage/knowledge_base.py +0 -0
  146. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/storage/router.py +0 -0
  147. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/storage/schemas.py +0 -0
  148. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/testing/__init__.py +0 -0
  149. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/testing/fixtures.py +0 -0
  150. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/testing/mocks.py +0 -0
  151. {pulse_engine-0.2.7 → pulse_engine-0.2.9}/src/pulse_engine/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pulse-engine
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: Pulse Engine — Hybrid framework for building Pulse products
5
5
  Author: Pulse Team
6
6
  Requires-Python: >=3.11,<3.13
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pulse-engine"
3
- version = "0.2.7"
3
+ version = "0.2.9"
4
4
  description = "Pulse Engine — Hybrid framework for building Pulse products"
5
5
  authors = ["Pulse Team"]
6
6
  readme = "README.md"
@@ -143,6 +143,40 @@ def _is_s3_ref(value: Any) -> bool:
143
143
  return isinstance(value, dict) and value.get("_s3_ref") is True
144
144
 
145
145
 
146
+ def _fetch_s3_ref(ref: dict[str, Any]) -> Any:
147
+ """Download and deserialise a single S3 ref. Used for when-expression evaluation."""
148
+ import boto3
149
+
150
+ region = boto3.session.Session().region_name or "us-east-1"
151
+ s3 = boto3.client("s3", region_name=region)
152
+ obj = s3.get_object(Bucket=ref["bucket"], Key=ref["key"])
153
+ return json.loads(obj["Body"].read().decode())
154
+
155
+
156
+ def _resolve_step_results_for_when(
157
+ when_expr: str, step_results: dict[str, Any]
158
+ ) -> dict[str, Any]:
159
+ """Copy of step_results with S3 refs expanded for steps in the when expression."""
160
+ import re
161
+
162
+ refs = re.findall(r"\$steps\.([\w-]+)\.", when_expr)
163
+ if not refs:
164
+ return step_results
165
+ resolved = dict(step_results)
166
+ for step in set(refs):
167
+ val = resolved.get(step)
168
+ if isinstance(val, dict) and _is_s3_ref(val):
169
+ try:
170
+ resolved[step] = _fetch_s3_ref(val)
171
+ except Exception:
172
+ logger.warning(
173
+ "Could not fetch S3 ref for step '%s' during when evaluation",
174
+ step,
175
+ exc_info=True,
176
+ )
177
+ return resolved
178
+
179
+
146
180
  def _merge_s3_refs(
147
181
  refs: list[dict[str, Any]],
148
182
  s3_bucket: str,
@@ -546,7 +580,10 @@ async def _dispatch_vm(
546
580
 
547
581
  # Embed all values as Python literals to avoid shell quoting issues entirely.
548
582
  remote_script = (
549
- f"import subprocess, sys, json\n"
583
+ f"import os, subprocess, sys, json\n"
584
+ # Inject resolved engine credentials into the VM process environment so that
585
+ # subprocess calls (aws ecr get-login-password, docker pull) inherit them.
586
+ f"os.environ.update({repr(_aws_env_inject)})\n"
550
587
  f"env = {repr(env)}\n"
551
588
  f"image = {repr(image)}\n"
552
589
  f"ecr_registry = {repr(ecr_registry)}\n"
@@ -1147,7 +1184,8 @@ def _execute_layer(
1147
1184
 
1148
1185
  # Gate 2: step-level when (only when expression doesn't reference $item.*)
1149
1186
  if when_expr and "$item." not in when_expr:
1150
- if not evaluate_when(when_expr, step_results, step_statuses):
1187
+ _when_results = _resolve_step_results_for_when(when_expr, step_results)
1188
+ if not evaluate_when(when_expr, _when_results, step_statuses):
1151
1189
  logger.info("Step '%s' skipped — when condition false", step_name)
1152
1190
  step_statuses[step_name] = "skipped"
1153
1191
  step_results[step_name] = {}
@@ -1155,8 +1193,60 @@ def _execute_layer(
1155
1193
 
1156
1194
  if step_config.get("for_each"):
1157
1195
  expr = parse_expression(step_config["for_each"])
1196
+ # Fetch S3 refs before fan-out so field access (e.g. .batches) works
1197
+ resolved_for_each = step_results
1198
+ if (
1199
+ expr.source == "steps"
1200
+ and expr.step
1201
+ and _is_s3_ref(step_results.get(expr.step))
1202
+ ):
1203
+ try:
1204
+ resolved_for_each = {
1205
+ **step_results,
1206
+ expr.step: _fetch_s3_ref(step_results[expr.step]),
1207
+ }
1208
+ except Exception:
1209
+ logger.warning(
1210
+ "Could not fetch S3 ref for for_each step '%s'",
1211
+ expr.step,
1212
+ exc_info=True,
1213
+ )
1214
+ elif expr.source == "collect" and collect_from:
1215
+ # $collect.field — resolve S3 refs for all collect_from steps
1216
+ overrides: dict[str, Any] = {}
1217
+ for _cname in collect_from:
1218
+ _val = step_results.get(_cname)
1219
+ if _is_s3_ref(_val) and _val is not None:
1220
+ try:
1221
+ overrides[_cname] = _fetch_s3_ref(_val)
1222
+ except Exception:
1223
+ logger.warning(
1224
+ "Could not fetch S3 ref for collect step '%s'",
1225
+ _cname,
1226
+ exc_info=True,
1227
+ )
1228
+ elif isinstance(_val, list):
1229
+ # fan-out: list of S3 refs — fetch and flatten
1230
+ fetched: list[Any] = []
1231
+ for _ref in _val:
1232
+ if _is_s3_ref(_ref):
1233
+ try:
1234
+ fetched.append(_fetch_s3_ref(_ref))
1235
+ except Exception:
1236
+ logger.warning(
1237
+ "Could not fetch S3 ref in collect list"
1238
+ " for step '%s'",
1239
+ _cname,
1240
+ exc_info=True,
1241
+ )
1242
+ else:
1243
+ fetched.append(_ref)
1244
+ if fetched:
1245
+ overrides[_cname] = fetched
1246
+ if overrides:
1247
+ resolved_for_each = {**step_results, **overrides}
1158
1248
  items = resolve_for_each_items(
1159
- expr, module_args, step_results, collect_from or None
1249
+ expr, module_args, resolved_for_each, collect_from or None
1160
1250
  )
1161
1251
 
1162
1252
  # Item-level when filter (fan-out only)
File without changes