synapse-sdk 1.0.0a11__py3-none-any.whl → 2026.1.1b2__py3-none-any.whl

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.

Potentially problematic release.


This version of synapse-sdk might be problematic. Click here for more details.

Files changed (261) hide show
  1. synapse_sdk/__init__.py +24 -0
  2. synapse_sdk/cli/__init__.py +9 -8
  3. synapse_sdk/cli/agent/__init__.py +25 -0
  4. synapse_sdk/cli/agent/config.py +104 -0
  5. synapse_sdk/cli/agent/select.py +197 -0
  6. synapse_sdk/cli/auth.py +104 -0
  7. synapse_sdk/cli/main.py +1025 -0
  8. synapse_sdk/cli/plugin/__init__.py +58 -0
  9. synapse_sdk/cli/plugin/create.py +566 -0
  10. synapse_sdk/cli/plugin/job.py +196 -0
  11. synapse_sdk/cli/plugin/publish.py +322 -0
  12. synapse_sdk/cli/plugin/run.py +131 -0
  13. synapse_sdk/cli/plugin/test.py +200 -0
  14. synapse_sdk/clients/README.md +239 -0
  15. synapse_sdk/clients/__init__.py +5 -0
  16. synapse_sdk/clients/_template.py +266 -0
  17. synapse_sdk/clients/agent/__init__.py +84 -29
  18. synapse_sdk/clients/agent/async_ray.py +289 -0
  19. synapse_sdk/clients/agent/container.py +83 -0
  20. synapse_sdk/clients/agent/plugin.py +101 -0
  21. synapse_sdk/clients/agent/ray.py +296 -39
  22. synapse_sdk/clients/backend/__init__.py +152 -12
  23. synapse_sdk/clients/backend/annotation.py +164 -22
  24. synapse_sdk/clients/backend/core.py +101 -0
  25. synapse_sdk/clients/backend/data_collection.py +292 -0
  26. synapse_sdk/clients/backend/hitl.py +87 -0
  27. synapse_sdk/clients/backend/integration.py +374 -46
  28. synapse_sdk/clients/backend/ml.py +134 -22
  29. synapse_sdk/clients/backend/models.py +247 -0
  30. synapse_sdk/clients/base.py +538 -59
  31. synapse_sdk/clients/exceptions.py +35 -7
  32. synapse_sdk/clients/pipeline/__init__.py +5 -0
  33. synapse_sdk/clients/pipeline/client.py +636 -0
  34. synapse_sdk/clients/protocols.py +178 -0
  35. synapse_sdk/clients/utils.py +86 -8
  36. synapse_sdk/clients/validation.py +58 -0
  37. synapse_sdk/enums.py +76 -0
  38. synapse_sdk/exceptions.py +168 -0
  39. synapse_sdk/integrations/__init__.py +74 -0
  40. synapse_sdk/integrations/_base.py +119 -0
  41. synapse_sdk/integrations/_context.py +53 -0
  42. synapse_sdk/integrations/ultralytics/__init__.py +78 -0
  43. synapse_sdk/integrations/ultralytics/_callbacks.py +126 -0
  44. synapse_sdk/integrations/ultralytics/_patches.py +124 -0
  45. synapse_sdk/loggers.py +476 -95
  46. synapse_sdk/mcp/MCP.md +69 -0
  47. synapse_sdk/mcp/__init__.py +48 -0
  48. synapse_sdk/mcp/__main__.py +6 -0
  49. synapse_sdk/mcp/config.py +349 -0
  50. synapse_sdk/mcp/prompts/__init__.py +4 -0
  51. synapse_sdk/mcp/resources/__init__.py +4 -0
  52. synapse_sdk/mcp/server.py +1352 -0
  53. synapse_sdk/mcp/tools/__init__.py +6 -0
  54. synapse_sdk/plugins/__init__.py +133 -9
  55. synapse_sdk/plugins/action.py +229 -0
  56. synapse_sdk/plugins/actions/__init__.py +82 -0
  57. synapse_sdk/plugins/actions/dataset/__init__.py +37 -0
  58. synapse_sdk/plugins/actions/dataset/action.py +471 -0
  59. synapse_sdk/plugins/actions/export/__init__.py +55 -0
  60. synapse_sdk/plugins/actions/export/action.py +183 -0
  61. synapse_sdk/plugins/actions/export/context.py +59 -0
  62. synapse_sdk/plugins/actions/inference/__init__.py +84 -0
  63. synapse_sdk/plugins/actions/inference/action.py +285 -0
  64. synapse_sdk/plugins/actions/inference/context.py +81 -0
  65. synapse_sdk/plugins/actions/inference/deployment.py +322 -0
  66. synapse_sdk/plugins/actions/inference/serve.py +252 -0
  67. synapse_sdk/plugins/actions/train/__init__.py +54 -0
  68. synapse_sdk/plugins/actions/train/action.py +326 -0
  69. synapse_sdk/plugins/actions/train/context.py +57 -0
  70. synapse_sdk/plugins/actions/upload/__init__.py +49 -0
  71. synapse_sdk/plugins/actions/upload/action.py +165 -0
  72. synapse_sdk/plugins/actions/upload/context.py +61 -0
  73. synapse_sdk/plugins/config.py +98 -0
  74. synapse_sdk/plugins/context/__init__.py +109 -0
  75. synapse_sdk/plugins/context/env.py +113 -0
  76. synapse_sdk/plugins/datasets/__init__.py +113 -0
  77. synapse_sdk/plugins/datasets/converters/__init__.py +76 -0
  78. synapse_sdk/plugins/datasets/converters/base.py +347 -0
  79. synapse_sdk/plugins/datasets/converters/yolo/__init__.py +9 -0
  80. synapse_sdk/plugins/datasets/converters/yolo/from_dm.py +468 -0
  81. synapse_sdk/plugins/datasets/converters/yolo/to_dm.py +381 -0
  82. synapse_sdk/plugins/datasets/formats/__init__.py +82 -0
  83. synapse_sdk/plugins/datasets/formats/dm.py +351 -0
  84. synapse_sdk/plugins/datasets/formats/yolo.py +240 -0
  85. synapse_sdk/plugins/decorators.py +83 -0
  86. synapse_sdk/plugins/discovery.py +790 -0
  87. synapse_sdk/plugins/docs/ACTION_DEV_GUIDE.md +933 -0
  88. synapse_sdk/plugins/docs/ARCHITECTURE.md +1225 -0
  89. synapse_sdk/plugins/docs/LOGGING_SYSTEM.md +683 -0
  90. synapse_sdk/plugins/docs/OVERVIEW.md +531 -0
  91. synapse_sdk/plugins/docs/PIPELINE_GUIDE.md +145 -0
  92. synapse_sdk/plugins/docs/README.md +513 -0
  93. synapse_sdk/plugins/docs/STEP.md +656 -0
  94. synapse_sdk/plugins/enums.py +70 -10
  95. synapse_sdk/plugins/errors.py +92 -0
  96. synapse_sdk/plugins/executors/__init__.py +43 -0
  97. synapse_sdk/plugins/executors/local.py +99 -0
  98. synapse_sdk/plugins/executors/ray/__init__.py +18 -0
  99. synapse_sdk/plugins/executors/ray/base.py +282 -0
  100. synapse_sdk/plugins/executors/ray/job.py +298 -0
  101. synapse_sdk/plugins/executors/ray/jobs_api.py +511 -0
  102. synapse_sdk/plugins/executors/ray/packaging.py +137 -0
  103. synapse_sdk/plugins/executors/ray/pipeline.py +792 -0
  104. synapse_sdk/plugins/executors/ray/task.py +257 -0
  105. synapse_sdk/plugins/models/__init__.py +26 -0
  106. synapse_sdk/plugins/models/logger.py +173 -0
  107. synapse_sdk/plugins/models/pipeline.py +25 -0
  108. synapse_sdk/plugins/pipelines/__init__.py +81 -0
  109. synapse_sdk/plugins/pipelines/action_pipeline.py +417 -0
  110. synapse_sdk/plugins/pipelines/context.py +107 -0
  111. synapse_sdk/plugins/pipelines/display.py +311 -0
  112. synapse_sdk/plugins/runner.py +114 -0
  113. synapse_sdk/plugins/schemas/__init__.py +19 -0
  114. synapse_sdk/plugins/schemas/results.py +152 -0
  115. synapse_sdk/plugins/steps/__init__.py +63 -0
  116. synapse_sdk/plugins/steps/base.py +128 -0
  117. synapse_sdk/plugins/steps/context.py +90 -0
  118. synapse_sdk/plugins/steps/orchestrator.py +128 -0
  119. synapse_sdk/plugins/steps/registry.py +103 -0
  120. synapse_sdk/plugins/steps/utils/__init__.py +20 -0
  121. synapse_sdk/plugins/steps/utils/logging.py +85 -0
  122. synapse_sdk/plugins/steps/utils/timing.py +71 -0
  123. synapse_sdk/plugins/steps/utils/validation.py +68 -0
  124. synapse_sdk/plugins/templates/__init__.py +50 -0
  125. synapse_sdk/plugins/templates/base/.gitignore.j2 +26 -0
  126. synapse_sdk/plugins/templates/base/.synapseignore.j2 +11 -0
  127. synapse_sdk/plugins/templates/base/README.md.j2 +26 -0
  128. synapse_sdk/plugins/templates/base/plugin/__init__.py.j2 +1 -0
  129. synapse_sdk/plugins/templates/base/pyproject.toml.j2 +14 -0
  130. synapse_sdk/plugins/templates/base/requirements.txt.j2 +1 -0
  131. synapse_sdk/plugins/templates/custom/plugin/main.py.j2 +18 -0
  132. synapse_sdk/plugins/templates/data_validation/plugin/validate.py.j2 +32 -0
  133. synapse_sdk/plugins/templates/export/plugin/export.py.j2 +36 -0
  134. synapse_sdk/plugins/templates/neural_net/plugin/inference.py.j2 +36 -0
  135. synapse_sdk/plugins/templates/neural_net/plugin/train.py.j2 +33 -0
  136. synapse_sdk/plugins/templates/post_annotation/plugin/post_annotate.py.j2 +32 -0
  137. synapse_sdk/plugins/templates/pre_annotation/plugin/pre_annotate.py.j2 +32 -0
  138. synapse_sdk/plugins/templates/smart_tool/plugin/auto_label.py.j2 +44 -0
  139. synapse_sdk/plugins/templates/upload/plugin/upload.py.j2 +35 -0
  140. synapse_sdk/plugins/testing/__init__.py +25 -0
  141. synapse_sdk/plugins/testing/sample_actions.py +98 -0
  142. synapse_sdk/plugins/types.py +206 -0
  143. synapse_sdk/plugins/upload.py +595 -64
  144. synapse_sdk/plugins/utils.py +325 -37
  145. synapse_sdk/shared/__init__.py +25 -0
  146. synapse_sdk/utils/__init__.py +1 -0
  147. synapse_sdk/utils/auth.py +74 -0
  148. synapse_sdk/utils/file/__init__.py +58 -0
  149. synapse_sdk/utils/file/archive.py +449 -0
  150. synapse_sdk/utils/file/checksum.py +167 -0
  151. synapse_sdk/utils/file/download.py +286 -0
  152. synapse_sdk/utils/file/io.py +129 -0
  153. synapse_sdk/utils/file/requirements.py +36 -0
  154. synapse_sdk/utils/network.py +168 -0
  155. synapse_sdk/utils/storage/__init__.py +238 -0
  156. synapse_sdk/utils/storage/config.py +188 -0
  157. synapse_sdk/utils/storage/errors.py +52 -0
  158. synapse_sdk/utils/storage/providers/__init__.py +13 -0
  159. synapse_sdk/utils/storage/providers/base.py +76 -0
  160. synapse_sdk/utils/storage/providers/gcs.py +168 -0
  161. synapse_sdk/utils/storage/providers/http.py +250 -0
  162. synapse_sdk/utils/storage/providers/local.py +126 -0
  163. synapse_sdk/utils/storage/providers/s3.py +177 -0
  164. synapse_sdk/utils/storage/providers/sftp.py +208 -0
  165. synapse_sdk/utils/storage/registry.py +125 -0
  166. synapse_sdk/utils/websocket.py +99 -0
  167. synapse_sdk-2026.1.1b2.dist-info/METADATA +715 -0
  168. synapse_sdk-2026.1.1b2.dist-info/RECORD +172 -0
  169. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/WHEEL +1 -1
  170. synapse_sdk-2026.1.1b2.dist-info/licenses/LICENSE +201 -0
  171. locale/en/LC_MESSAGES/messages.mo +0 -0
  172. locale/en/LC_MESSAGES/messages.po +0 -39
  173. locale/ko/LC_MESSAGES/messages.mo +0 -0
  174. locale/ko/LC_MESSAGES/messages.po +0 -34
  175. synapse_sdk/cli/create_plugin.py +0 -10
  176. synapse_sdk/clients/agent/core.py +0 -7
  177. synapse_sdk/clients/agent/service.py +0 -15
  178. synapse_sdk/clients/backend/dataset.py +0 -51
  179. synapse_sdk/clients/ray/__init__.py +0 -6
  180. synapse_sdk/clients/ray/core.py +0 -22
  181. synapse_sdk/clients/ray/serve.py +0 -20
  182. synapse_sdk/i18n.py +0 -35
  183. synapse_sdk/plugins/categories/__init__.py +0 -0
  184. synapse_sdk/plugins/categories/base.py +0 -235
  185. synapse_sdk/plugins/categories/data_validation/__init__.py +0 -0
  186. synapse_sdk/plugins/categories/data_validation/actions/__init__.py +0 -0
  187. synapse_sdk/plugins/categories/data_validation/actions/validation.py +0 -10
  188. synapse_sdk/plugins/categories/data_validation/templates/config.yaml +0 -3
  189. synapse_sdk/plugins/categories/data_validation/templates/plugin/__init__.py +0 -0
  190. synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +0 -5
  191. synapse_sdk/plugins/categories/decorators.py +0 -13
  192. synapse_sdk/plugins/categories/export/__init__.py +0 -0
  193. synapse_sdk/plugins/categories/export/actions/__init__.py +0 -0
  194. synapse_sdk/plugins/categories/export/actions/export.py +0 -10
  195. synapse_sdk/plugins/categories/import/__init__.py +0 -0
  196. synapse_sdk/plugins/categories/import/actions/__init__.py +0 -0
  197. synapse_sdk/plugins/categories/import/actions/import.py +0 -10
  198. synapse_sdk/plugins/categories/neural_net/__init__.py +0 -0
  199. synapse_sdk/plugins/categories/neural_net/actions/__init__.py +0 -0
  200. synapse_sdk/plugins/categories/neural_net/actions/deployment.py +0 -45
  201. synapse_sdk/plugins/categories/neural_net/actions/inference.py +0 -18
  202. synapse_sdk/plugins/categories/neural_net/actions/test.py +0 -10
  203. synapse_sdk/plugins/categories/neural_net/actions/train.py +0 -143
  204. synapse_sdk/plugins/categories/neural_net/templates/config.yaml +0 -12
  205. synapse_sdk/plugins/categories/neural_net/templates/plugin/__init__.py +0 -0
  206. synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +0 -4
  207. synapse_sdk/plugins/categories/neural_net/templates/plugin/test.py +0 -2
  208. synapse_sdk/plugins/categories/neural_net/templates/plugin/train.py +0 -14
  209. synapse_sdk/plugins/categories/post_annotation/__init__.py +0 -0
  210. synapse_sdk/plugins/categories/post_annotation/actions/__init__.py +0 -0
  211. synapse_sdk/plugins/categories/post_annotation/actions/post_annotation.py +0 -10
  212. synapse_sdk/plugins/categories/post_annotation/templates/config.yaml +0 -3
  213. synapse_sdk/plugins/categories/post_annotation/templates/plugin/__init__.py +0 -0
  214. synapse_sdk/plugins/categories/post_annotation/templates/plugin/post_annotation.py +0 -3
  215. synapse_sdk/plugins/categories/pre_annotation/__init__.py +0 -0
  216. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +0 -0
  217. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation.py +0 -10
  218. synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +0 -3
  219. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/__init__.py +0 -0
  220. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/pre_annotation.py +0 -3
  221. synapse_sdk/plugins/categories/registry.py +0 -16
  222. synapse_sdk/plugins/categories/smart_tool/__init__.py +0 -0
  223. synapse_sdk/plugins/categories/smart_tool/actions/__init__.py +0 -0
  224. synapse_sdk/plugins/categories/smart_tool/actions/auto_label.py +0 -37
  225. synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +0 -7
  226. synapse_sdk/plugins/categories/smart_tool/templates/plugin/__init__.py +0 -0
  227. synapse_sdk/plugins/categories/smart_tool/templates/plugin/auto_label.py +0 -11
  228. synapse_sdk/plugins/categories/templates.py +0 -32
  229. synapse_sdk/plugins/cli/__init__.py +0 -21
  230. synapse_sdk/plugins/cli/publish.py +0 -37
  231. synapse_sdk/plugins/cli/run.py +0 -67
  232. synapse_sdk/plugins/exceptions.py +0 -22
  233. synapse_sdk/plugins/models.py +0 -121
  234. synapse_sdk/plugins/templates/cookiecutter.json +0 -11
  235. synapse_sdk/plugins/templates/hooks/post_gen_project.py +0 -3
  236. synapse_sdk/plugins/templates/hooks/pre_prompt.py +0 -21
  237. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
  238. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
  239. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.gitignore +0 -27
  240. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.pre-commit-config.yaml +0 -7
  241. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/README.md +0 -5
  242. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +0 -6
  243. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
  244. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/plugin/__init__.py +0 -0
  245. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/pyproject.toml +0 -13
  246. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +0 -1
  247. synapse_sdk/shared/enums.py +0 -8
  248. synapse_sdk/utils/debug.py +0 -5
  249. synapse_sdk/utils/file.py +0 -87
  250. synapse_sdk/utils/module_loading.py +0 -29
  251. synapse_sdk/utils/pydantic/__init__.py +0 -0
  252. synapse_sdk/utils/pydantic/config.py +0 -4
  253. synapse_sdk/utils/pydantic/errors.py +0 -33
  254. synapse_sdk/utils/pydantic/validators.py +0 -7
  255. synapse_sdk/utils/storage.py +0 -91
  256. synapse_sdk/utils/string.py +0 -11
  257. synapse_sdk-1.0.0a11.dist-info/LICENSE +0 -21
  258. synapse_sdk-1.0.0a11.dist-info/METADATA +0 -43
  259. synapse_sdk-1.0.0a11.dist-info/RECORD +0 -111
  260. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/entry_points.txt +0 -0
  261. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,792 @@
1
+ """Ray Pipeline executor for multi-action pipeline execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any, Literal
11
+
12
+ from synapse_sdk.plugins.context import PluginEnvironment
13
+ from synapse_sdk.plugins.enums import PackageManager
14
+ from synapse_sdk.plugins.errors import ExecutionError
15
+ from synapse_sdk.plugins.executors.ray.base import BaseRayExecutor
16
+ from synapse_sdk.plugins.models.logger import ActionProgress, PipelineProgress
17
+ from synapse_sdk.plugins.models.pipeline import ActionStatus, RunStatus
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import AsyncIterator, Iterator
21
+
22
+ from synapse_sdk.clients.pipeline import PipelineServiceClient
23
+ from synapse_sdk.plugins.action import BaseAction
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def _serialize_paths(obj: Any) -> Any:
29
+ """Recursively convert Path objects to strings for JSON serialization."""
30
+ if isinstance(obj, Path):
31
+ return str(obj)
32
+ if isinstance(obj, dict):
33
+ return {k: _serialize_paths(v) for k, v in obj.items()}
34
+ if isinstance(obj, list):
35
+ return [_serialize_paths(item) for item in obj]
36
+ return obj
37
+
38
+
39
+ @dataclass
40
+ class PipelineDefinition:
41
+ """Definition of a pipeline to execute.
42
+
43
+ Attributes:
44
+ name: Pipeline name.
45
+ actions: List of action classes or entrypoint strings.
46
+ description: Optional description.
47
+ """
48
+
49
+ name: str
50
+ actions: list[type['BaseAction'] | str]
51
+ description: str | None = None
52
+
53
+ def to_api_format(self) -> list[dict[str, Any]]:
54
+ """Convert actions to API format."""
55
+ result = []
56
+ for action in self.actions:
57
+ if isinstance(action, str):
58
+ entrypoint = action
59
+ name = action.rsplit('.', 1)[-1]
60
+ else:
61
+ entrypoint = f'{action.__module__}.{action.__name__}'
62
+ name = action.action_name or action.__name__
63
+ result.append({'name': name, 'entrypoint': entrypoint})
64
+ return result
65
+
66
+
67
+ class RayPipelineExecutor(BaseRayExecutor):
68
+ """Ray-based executor for multi-action pipelines.
69
+
70
+ Submits pipelines as a single Ray actor that executes actions sequentially.
71
+ Integrates with dev-api for progress tracking and checkpointing.
72
+
73
+ Example:
74
+ >>> executor = RayPipelineExecutor(
75
+ ... ray_address='auto',
76
+ ... working_dir='/path/to/plugin',
77
+ ... pipeline_service_url='http://localhost:8100',
78
+ ... )
79
+ >>> pipeline = PipelineDefinition(
80
+ ... name='YOLO Training',
81
+ ... actions=[DownloadAction, ConvertAction, TrainAction],
82
+ ... )
83
+ >>> run_id = executor.submit(pipeline, params={'dataset_id': 123})
84
+ >>> progress = executor.get_progress(run_id)
85
+ >>> result = executor.wait(run_id)
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ env: PluginEnvironment | dict[str, Any] | None = None,
91
+ *,
92
+ ray_address: str = 'auto',
93
+ runtime_env: dict[str, Any] | None = None,
94
+ working_dir: str | Path | None = None,
95
+ requirements_file: str | Path | None = None,
96
+ package_manager: PackageManager | Literal['pip', 'uv'] = PackageManager.PIP,
97
+ package_manager_options: list[str] | None = None,
98
+ wheels_dir: str = 'wheels',
99
+ num_cpus: int | None = None,
100
+ num_gpus: int | None = None,
101
+ include_sdk: bool = False,
102
+ pipeline_service_url: str = 'http://localhost:8100',
103
+ actor_pipeline_service_url: str | None = None,
104
+ ) -> None:
105
+ """Initialize Ray pipeline executor.
106
+
107
+ Args:
108
+ env: Environment config for actions.
109
+ ray_address: Ray cluster address. Defaults to 'auto'.
110
+ runtime_env: Ray runtime environment config.
111
+ working_dir: Plugin working directory.
112
+ requirements_file: Path to requirements.txt.
113
+ package_manager: Package manager to use.
114
+ package_manager_options: Additional package manager options.
115
+ wheels_dir: Directory containing .whl files.
116
+ num_cpus: Number of CPUs to request.
117
+ num_gpus: Number of GPUs to request.
118
+ include_sdk: If True, bundle local SDK with upload.
119
+ pipeline_service_url: URL of the pipeline service API (for local SDK).
120
+ actor_pipeline_service_url: URL for the Ray actor to reach the pipeline
121
+ service. If None, uses pipeline_service_url. Use this when the actor
122
+ runs on a remote cluster that needs a different URL (e.g., via VPN).
123
+ """
124
+ super().__init__(
125
+ env=env,
126
+ runtime_env=runtime_env,
127
+ working_dir=working_dir,
128
+ requirements_file=requirements_file,
129
+ package_manager=package_manager,
130
+ package_manager_options=package_manager_options,
131
+ wheels_dir=wheels_dir,
132
+ ray_address=ray_address,
133
+ include_sdk=include_sdk,
134
+ )
135
+ self._num_cpus = num_cpus
136
+ self._num_gpus = num_gpus
137
+ self._pipeline_service_url = pipeline_service_url
138
+ self._actor_pipeline_service_url = actor_pipeline_service_url or pipeline_service_url
139
+ self._submitted_refs: dict[str, Any] = {} # run_id -> (actor_ref, result_ref)
140
+ self._pipeline_client: PipelineServiceClient | None = None
141
+
142
+ @property
143
+ def pipeline_client(self) -> 'PipelineServiceClient':
144
+ """Get or create the pipeline service client."""
145
+ if self._pipeline_client is None:
146
+ from synapse_sdk.clients.pipeline import PipelineServiceClient
147
+
148
+ self._pipeline_client = PipelineServiceClient(self._pipeline_service_url)
149
+ return self._pipeline_client
150
+
151
+ def submit(
152
+ self,
153
+ pipeline: PipelineDefinition | list[type['BaseAction'] | str],
154
+ params: dict[str, Any],
155
+ *,
156
+ name: str | None = None,
157
+ resume_from: str | None = None,
158
+ ) -> str:
159
+ """Submit a pipeline for execution (non-blocking).
160
+
161
+ Args:
162
+ pipeline: PipelineDefinition or list of action classes/entrypoints.
163
+ params: Initial parameters for the pipeline.
164
+ name: Pipeline name (required if pipeline is a list).
165
+ resume_from: Run ID to resume from. If provided, the pipeline will
166
+ skip completed actions and restore accumulated params from the
167
+ latest checkpoint of that run.
168
+
169
+ Returns:
170
+ Run ID for tracking.
171
+
172
+ Raises:
173
+ ValueError: If pipeline is a list and name is not provided.
174
+ """
175
+ import ray
176
+
177
+ self._ray_init()
178
+
179
+ # Normalize pipeline definition
180
+ if isinstance(pipeline, list):
181
+ if name is None:
182
+ name = 'pipeline'
183
+ pipeline = PipelineDefinition(name=name, actions=pipeline)
184
+
185
+ # Fetch resume checkpoint if resuming
186
+ resume_checkpoint: dict[str, Any] | None = None
187
+ if resume_from:
188
+ resume_checkpoint = self.pipeline_client.get_latest_checkpoint(resume_from)
189
+ if resume_checkpoint:
190
+ logger.info(
191
+ f'Resuming from checkpoint: action={resume_checkpoint.get("action_name")}, '
192
+ f'index={resume_checkpoint.get("action_index")}'
193
+ )
194
+ else:
195
+ logger.warning(f'No checkpoint found for run {resume_from}, starting fresh')
196
+
197
+ # Register pipeline with dev-api
198
+ api_actions = pipeline.to_api_format()
199
+ pipeline_data = self.pipeline_client.create_pipeline(
200
+ name=pipeline.name,
201
+ actions=api_actions,
202
+ description=pipeline.description,
203
+ )
204
+ pipeline_id = pipeline_data['id']
205
+
206
+ # Create run
207
+ run_data = self.pipeline_client.create_run(
208
+ pipeline_id=pipeline_id,
209
+ params=params,
210
+ )
211
+ run_id = run_data['id']
212
+
213
+ # Convert action classes to entrypoint strings
214
+ entrypoints = []
215
+ for action in pipeline.actions:
216
+ if isinstance(action, str):
217
+ entrypoints.append(action)
218
+ else:
219
+ entrypoints.append(f'{action.__module__}.{action.__name__}')
220
+
221
+ # Build remote options
222
+ remote_options: dict[str, Any] = {'runtime_env': self._build_runtime_env()}
223
+ if self._num_cpus is not None:
224
+ remote_options['num_cpus'] = self._num_cpus
225
+ if self._num_gpus is not None:
226
+ remote_options['num_gpus'] = self._num_gpus
227
+
228
+ # Create actor and submit pipeline
229
+ # Use actor_pipeline_service_url for the remote actor
230
+ PipelineActor = ray.remote(**remote_options)(_PipelineExecutorActor)
231
+ actor = PipelineActor.remote(
232
+ run_id=run_id,
233
+ pipeline_id=pipeline_id,
234
+ pipeline_service_url=self._actor_pipeline_service_url,
235
+ )
236
+ result_ref = actor.run_pipeline.remote(entrypoints, params, resume_checkpoint=resume_checkpoint)
237
+
238
+ self._submitted_refs[run_id] = (actor, result_ref)
239
+ logger.info(f'Submitted pipeline run {run_id}')
240
+
241
+ return run_id
242
+
243
+ def get_status(self, run_id: str) -> RunStatus:
244
+ """Get run status from the pipeline service.
245
+
246
+ Args:
247
+ run_id: Run ID from submit().
248
+
249
+ Returns:
250
+ Current run status.
251
+ """
252
+ progress = self.pipeline_client.get_progress(run_id)
253
+ return progress.status
254
+
255
+ def get_progress(self, run_id: str) -> PipelineProgress:
256
+ """Get detailed progress for a run.
257
+
258
+ Args:
259
+ run_id: Run ID from submit().
260
+
261
+ Returns:
262
+ PipelineProgress with current state.
263
+ """
264
+ return self.pipeline_client.get_progress(run_id)
265
+
266
+ def get_result(self, run_id: str, timeout: float | None = None) -> Any:
267
+ """Get pipeline result (blocks until complete).
268
+
269
+ Args:
270
+ run_id: Run ID from submit().
271
+ timeout: Optional timeout in seconds.
272
+
273
+ Returns:
274
+ Final pipeline result.
275
+
276
+ Raises:
277
+ ExecutionError: If pipeline failed or not found.
278
+ """
279
+ import ray
280
+
281
+ refs = self._submitted_refs.get(run_id)
282
+ if refs is None:
283
+ # Try to get from API
284
+ run_data = self.pipeline_client.get_run(run_id)
285
+ if run_data.get('status') == 'completed':
286
+ return run_data.get('result')
287
+ elif run_data.get('status') == 'failed':
288
+ raise ExecutionError(f'Pipeline {run_id} failed: {run_data.get("error")}')
289
+ raise ExecutionError(f'Run {run_id} not found in local cache')
290
+
291
+ _, result_ref = refs
292
+ try:
293
+ return ray.get(result_ref, timeout=timeout)
294
+ except Exception as e:
295
+ raise ExecutionError(f'Pipeline {run_id} failed: {e}') from e
296
+
297
+ def wait(
298
+ self,
299
+ run_id: str,
300
+ timeout_seconds: float = 3600,
301
+ poll_interval: float = 5.0,
302
+ ) -> PipelineProgress:
303
+ """Wait for pipeline to complete, polling for progress.
304
+
305
+ Args:
306
+ run_id: Run ID from submit().
307
+ timeout_seconds: Maximum time to wait.
308
+ poll_interval: Seconds between progress polls.
309
+
310
+ Returns:
311
+ Final PipelineProgress.
312
+
313
+ Raises:
314
+ ExecutionError: If pipeline fails or times out.
315
+ """
316
+ start_time = time.time()
317
+
318
+ while True:
319
+ elapsed = time.time() - start_time
320
+ if elapsed > timeout_seconds:
321
+ raise ExecutionError(f'Pipeline {run_id} timed out after {timeout_seconds}s')
322
+
323
+ progress = self.get_progress(run_id)
324
+
325
+ if progress.status == RunStatus.COMPLETED:
326
+ return progress
327
+ elif progress.status == RunStatus.FAILED:
328
+ raise ExecutionError(f'Pipeline {run_id} failed: {progress.error}')
329
+ elif progress.status == RunStatus.CANCELLED:
330
+ raise ExecutionError(f'Pipeline {run_id} was cancelled')
331
+
332
+ time.sleep(poll_interval)
333
+
334
+ def stream_progress(
335
+ self,
336
+ run_id: str,
337
+ timeout: float = 3600.0,
338
+ ) -> 'Iterator[PipelineProgress]':
339
+ """Stream progress updates via SSE.
340
+
341
+ This method connects to the pipeline service's SSE endpoint and yields
342
+ PipelineProgress objects as updates are received. More efficient than
343
+ polling for long-running pipelines.
344
+
345
+ Args:
346
+ run_id: Run ID from submit().
347
+ timeout: Maximum time to stream in seconds.
348
+
349
+ Yields:
350
+ PipelineProgress objects with current state.
351
+
352
+ Raises:
353
+ ExecutionError: If streaming fails or pipeline errors.
354
+
355
+ Example:
356
+ >>> run_id = executor.submit(pipeline, params)
357
+ >>> for progress in executor.stream_progress(run_id):
358
+ ... print(f"Action: {progress.current_action}, Status: {progress.status}")
359
+ ... if progress.status == RunStatus.COMPLETED:
360
+ ... break
361
+ """
362
+ try:
363
+ yield from self.pipeline_client.stream_progress(run_id, timeout=timeout)
364
+ except Exception as e:
365
+ raise ExecutionError(f'Failed to stream progress for {run_id}: {e}') from e
366
+
367
+ async def stream_progress_async(
368
+ self,
369
+ run_id: str,
370
+ timeout: float = 3600.0,
371
+ ) -> 'AsyncIterator[PipelineProgress]':
372
+ """Stream progress updates via SSE (async version).
373
+
374
+ This method connects to the pipeline service's SSE endpoint and yields
375
+ PipelineProgress objects as updates are received. More efficient than
376
+ polling for long-running pipelines.
377
+
378
+ Args:
379
+ run_id: Run ID from submit().
380
+ timeout: Maximum time to stream in seconds.
381
+
382
+ Yields:
383
+ PipelineProgress objects with current state.
384
+
385
+ Raises:
386
+ ExecutionError: If streaming fails or pipeline errors.
387
+
388
+ Example:
389
+ >>> run_id = executor.submit(pipeline, params)
390
+ >>> async for progress in executor.stream_progress_async(run_id):
391
+ ... print(f"Action: {progress.current_action}, Status: {progress.status}")
392
+ """
393
+ try:
394
+ async for progress in self.pipeline_client.stream_progress_async(run_id, timeout=timeout):
395
+ yield progress
396
+ except Exception as e:
397
+ raise ExecutionError(f'Failed to stream progress for {run_id}: {e}') from e
398
+
399
+ def cancel(self, run_id: str) -> None:
400
+ """Cancel a running pipeline.
401
+
402
+ Args:
403
+ run_id: Run ID to cancel.
404
+ """
405
+ import ray
406
+
407
+ refs = self._submitted_refs.get(run_id)
408
+ if refs is not None:
409
+ actor, result_ref = refs
410
+ try:
411
+ ray.kill(actor)
412
+ except Exception as e:
413
+ logger.warning(f'Failed to kill actor for {run_id}: {e}')
414
+
415
+ # Update status in API
416
+ try:
417
+ self.pipeline_client.update_run(run_id, status='cancelled')
418
+ except Exception as e:
419
+ logger.warning(f'Failed to update cancelled status for {run_id}: {e}')
420
+
421
+ def close(self) -> None:
422
+ """Close the executor and clean up resources."""
423
+ if self._pipeline_client is not None:
424
+ self._pipeline_client.close()
425
+ self._pipeline_client = None
426
+
427
+
428
+ class _PipelineLogger:
429
+ """Logger that wraps ConsoleLogger and reports progress to pipeline service.
430
+
431
+ Forwards progress updates to the dev-api for real-time pipeline monitoring.
432
+ """
433
+
434
+ def __init__(
435
+ self,
436
+ base_logger: Any,
437
+ pipeline_client: Any,
438
+ run_id: str,
439
+ action_index: int,
440
+ action_name: str,
441
+ ) -> None:
442
+ self._base = base_logger
443
+ self._client = pipeline_client
444
+ self._run_id = run_id
445
+ self._action_index = action_index
446
+ self._action_name = action_name
447
+
448
+ def __getattr__(self, name: str) -> Any:
449
+ # Forward all other methods to base logger
450
+ return getattr(self._base, name)
451
+
452
+ def set_progress(self, current: int, total: int, category: str | None = None) -> None:
453
+ """Set progress and report to pipeline service."""
454
+ self._base.set_progress(current, total, category)
455
+
456
+ # Report to pipeline service
457
+ try:
458
+ percent = current / total if total > 0 else 0
459
+ self._client.report_progress(
460
+ run_id=self._run_id,
461
+ current_action_index=self._action_index,
462
+ action_progress=ActionProgress(
463
+ name=self._action_name,
464
+ status=ActionStatus.RUNNING,
465
+ progress=percent,
466
+ progress_category=category,
467
+ message=f'{current}/{total}',
468
+ ),
469
+ )
470
+ except Exception as e:
471
+ print(f'[PipelineLogger] Failed to report progress: {e}')
472
+
473
+ def info(self, message: str) -> None:
474
+ self._base.info(message)
475
+
476
+ def debug(self, message: str) -> None:
477
+ self._base.debug(message)
478
+
479
+ def warning(self, message: str) -> None:
480
+ self._base.warning(message)
481
+
482
+ def error(self, message: str) -> None:
483
+ self._base.error(message)
484
+
485
+ def log(self, event: str, data: dict, file: str | None = None) -> None:
486
+ self._base.log(event, data, file)
487
+
488
+
489
+ class _PipelineExecutorActor:
490
+ """Ray Actor that executes a pipeline of actions.
491
+
492
+ This actor runs within the Ray cluster and executes actions sequentially,
493
+ reporting progress to the pipeline service API after each action.
494
+ """
495
+
496
+ def __init__(
497
+ self,
498
+ run_id: str,
499
+ pipeline_id: str,
500
+ pipeline_service_url: str,
501
+ ) -> None:
502
+ """Initialize the pipeline executor actor.
503
+
504
+ Args:
505
+ run_id: Run identifier.
506
+ pipeline_id: Pipeline identifier.
507
+ pipeline_service_url: URL of the pipeline service.
508
+ """
509
+ self._run_id = run_id
510
+ self._pipeline_id = pipeline_id
511
+ self._pipeline_service_url = pipeline_service_url
512
+ self._client: Any = None
513
+
514
+ @property
515
+ def client(self) -> Any:
516
+ """Get or create the pipeline service client."""
517
+ if self._client is None:
518
+ from synapse_sdk.clients.pipeline import PipelineServiceClient
519
+
520
+ self._client = PipelineServiceClient(self._pipeline_service_url)
521
+ return self._client
522
+
523
+ def run_pipeline(
524
+ self,
525
+ entrypoints: list[str],
526
+ params: dict[str, Any],
527
+ resume_checkpoint: dict[str, Any] | None = None,
528
+ ) -> Any:
529
+ """Execute all actions in the pipeline sequentially.
530
+
531
+ Args:
532
+ entrypoints: List of action entrypoint strings.
533
+ params: Initial parameters.
534
+ resume_checkpoint: Checkpoint data to resume from (optional).
535
+ If provided, actions up to and including the checkpoint's
536
+ action_index will be skipped, and accumulated params will
537
+ be restored from params_snapshot.
538
+
539
+ Returns:
540
+ Result from the final action.
541
+ """
542
+ import importlib
543
+ import logging
544
+ import os
545
+ import sys
546
+ import traceback
547
+
548
+ from pydantic import BaseModel
549
+
550
+ # Configure logging to ensure ConsoleLogger output is visible
551
+ # This is needed because Ray workers don't have logging configured by default
552
+ logging.basicConfig(
553
+ level=logging.INFO,
554
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
555
+ force=True, # Override any existing config
556
+ )
557
+
558
+ from synapse_sdk.loggers import ConsoleLogger
559
+ from synapse_sdk.plugins.context import PluginEnvironment, RuntimeContext
560
+ from synapse_sdk.plugins.models.logger import LogEntry, LogLevel
561
+ from synapse_sdk.plugins.pipelines.context import PipelineContext
562
+
563
+ # Ensure working directory is in sys.path
564
+ cwd = os.getcwd()
565
+ if cwd not in sys.path:
566
+ sys.path.insert(0, cwd)
567
+
568
+ # Create pipeline context for shared working directory
569
+ # TODO: Pass pipeline_ctx to actions via RuntimeContext
570
+ _pipeline_ctx = PipelineContext(
571
+ pipeline_id=self._pipeline_id,
572
+ run_id=self._run_id,
573
+ )
574
+
575
+ # Determine resume index and restore params if resuming
576
+ resume_from_index = -1
577
+ if resume_checkpoint:
578
+ resume_from_index = resume_checkpoint.get('action_index', -1)
579
+ # Restore accumulated params from checkpoint
580
+ if resume_checkpoint.get('params_snapshot'):
581
+ params = resume_checkpoint['params_snapshot']
582
+
583
+ # Report pipeline started
584
+ self.client.report_progress(
585
+ run_id=self._run_id,
586
+ status='running',
587
+ )
588
+
589
+ # Try to create BackendClient from environment
590
+ from synapse_sdk.utils.auth import create_backend_client
591
+
592
+ backend_client = create_backend_client()
593
+
594
+ # Create base runtime context
595
+ base_logger = ConsoleLogger()
596
+ env = PluginEnvironment.from_environ()
597
+
598
+ # Accumulated params (passed between actions)
599
+ accumulated_params = dict(params)
600
+ final_result = None
601
+
602
+ try:
603
+ for idx, entrypoint in enumerate(entrypoints):
604
+ action_name = entrypoint.rsplit('.', 1)[-1]
605
+
606
+ # Skip completed actions when resuming
607
+ if idx <= resume_from_index:
608
+ # Report action as skipped
609
+ self.client.report_progress(
610
+ run_id=self._run_id,
611
+ current_action=action_name,
612
+ current_action_index=idx,
613
+ action_progress=ActionProgress(
614
+ name=action_name,
615
+ status=ActionStatus.SKIPPED,
616
+ progress=1.0,
617
+ completed_at=datetime.utcnow(),
618
+ ),
619
+ )
620
+ self.client.append_logs(
621
+ run_id=self._run_id,
622
+ entries=[
623
+ LogEntry(
624
+ message=f'Skipping action (resumed): {action_name}',
625
+ level=LogLevel.INFO,
626
+ action_name=action_name,
627
+ )
628
+ ],
629
+ )
630
+ continue
631
+
632
+ # Report action started
633
+ self.client.report_progress(
634
+ run_id=self._run_id,
635
+ current_action=action_name,
636
+ current_action_index=idx,
637
+ action_progress=ActionProgress(
638
+ name=action_name,
639
+ status=ActionStatus.RUNNING,
640
+ progress=0.0,
641
+ started_at=datetime.utcnow(),
642
+ ),
643
+ )
644
+
645
+ # Log action start
646
+ self.client.append_logs(
647
+ run_id=self._run_id,
648
+ entries=[
649
+ LogEntry(
650
+ message=f'Starting action: {action_name}',
651
+ level=LogLevel.INFO,
652
+ action_name=action_name,
653
+ )
654
+ ],
655
+ )
656
+
657
+ # Import action class
658
+ try:
659
+ module_path, class_name = entrypoint.rsplit('.', 1)
660
+ module = importlib.import_module(module_path)
661
+ action_cls = getattr(module, class_name)
662
+ except Exception as e:
663
+ raise ExecutionError(f'Failed to import action {entrypoint}: {e}') from e
664
+
665
+ # Validate params
666
+ try:
667
+ validated_params = action_cls.params_model.model_validate(accumulated_params)
668
+ except Exception as e:
669
+ raise ExecutionError(f'Parameter validation failed for {action_name}: {e}') from e
670
+
671
+ # Build context with pipeline logger for progress reporting
672
+ action_logger = _PipelineLogger(
673
+ base_logger=base_logger,
674
+ pipeline_client=self.client,
675
+ run_id=self._run_id,
676
+ action_index=idx,
677
+ action_name=action_name,
678
+ )
679
+ ctx = RuntimeContext(
680
+ logger=action_logger,
681
+ env=env,
682
+ job_id=self._run_id,
683
+ client=backend_client,
684
+ )
685
+
686
+ # Execute action
687
+ action = action_cls(validated_params, ctx)
688
+ try:
689
+ if hasattr(action, 'run'):
690
+ result = action.run()
691
+ else:
692
+ result = action.execute()
693
+ except Exception as e:
694
+ # Report action failed
695
+ self.client.report_progress(
696
+ run_id=self._run_id,
697
+ current_action_index=idx,
698
+ action_progress=ActionProgress(
699
+ name=action_name,
700
+ status=ActionStatus.FAILED,
701
+ error=str(e),
702
+ completed_at=datetime.utcnow(),
703
+ ),
704
+ )
705
+ raise ExecutionError(f'Action {action_name} failed: {e}') from e
706
+
707
+ # Merge result into accumulated params
708
+ # Use mode='json' for Pydantic models to serialize Path objects
709
+ if isinstance(result, BaseModel):
710
+ result_dict = result.model_dump(mode='json')
711
+ elif isinstance(result, dict):
712
+ result_dict = _serialize_paths(result)
713
+ else:
714
+ result_dict = {}
715
+
716
+ accumulated_params.update(result_dict)
717
+ final_result = result
718
+
719
+ # Report action completed
720
+ self.client.report_progress(
721
+ run_id=self._run_id,
722
+ current_action_index=idx,
723
+ action_progress=ActionProgress(
724
+ name=action_name,
725
+ status=ActionStatus.COMPLETED,
726
+ progress=1.0,
727
+ completed_at=datetime.utcnow(),
728
+ ),
729
+ )
730
+
731
+ # Create checkpoint with JSON-serializable data
732
+ self.client.create_checkpoint(
733
+ run_id=self._run_id,
734
+ action_name=action_name,
735
+ action_index=idx,
736
+ status='completed',
737
+ params_snapshot=_serialize_paths(accumulated_params),
738
+ result=result_dict,
739
+ )
740
+
741
+ # Log action complete
742
+ self.client.append_logs(
743
+ run_id=self._run_id,
744
+ entries=[
745
+ LogEntry(
746
+ message=f'Completed action: {action_name}',
747
+ level=LogLevel.INFO,
748
+ action_name=action_name,
749
+ )
750
+ ],
751
+ )
752
+
753
+ # Report pipeline completed
754
+ result_for_api = final_result
755
+ if isinstance(result_for_api, BaseModel):
756
+ result_for_api = result_for_api.model_dump(mode='json')
757
+ elif isinstance(result_for_api, dict):
758
+ result_for_api = _serialize_paths(result_for_api)
759
+
760
+ self.client.update_run(
761
+ run_id=self._run_id,
762
+ status='completed',
763
+ result=result_for_api if isinstance(result_for_api, dict) else None,
764
+ )
765
+
766
+ base_logger.finish()
767
+ return final_result
768
+
769
+ except Exception as e:
770
+ # Report pipeline failed
771
+ error_msg = f'{type(e).__name__}: {str(e)}'
772
+ self.client.update_run(
773
+ run_id=self._run_id,
774
+ status='failed',
775
+ error=error_msg,
776
+ )
777
+
778
+ self.client.append_logs(
779
+ run_id=self._run_id,
780
+ entries=[
781
+ LogEntry(
782
+ message=f'Pipeline failed: {error_msg}\n{traceback.format_exc()}',
783
+ level=LogLevel.ERROR,
784
+ )
785
+ ],
786
+ )
787
+
788
+ base_logger.finish()
789
+ raise
790
+
791
+
792
+ __all__ = ['RayPipelineExecutor', 'PipelineDefinition']