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,511 @@
1
+ """Ray Jobs API executor for plugin actions with runtime env log streaming.
2
+
3
+ This executor uses Ray's Jobs API (JobSubmissionClient) instead of ray.remote,
4
+ which provides access to runtime environment setup logs and proper job lifecycle.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import tempfile
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator, Literal
13
+
14
+ from synapse_sdk.plugins.context import PluginEnvironment
15
+ from synapse_sdk.plugins.enums import PackageManager
16
+ from synapse_sdk.plugins.errors import ExecutionError
17
+ from synapse_sdk.plugins.executors.ray.base import BaseRayExecutor
18
+
19
+ if TYPE_CHECKING:
20
+ from ray.job_submission import JobStatus
21
+
22
+ from synapse_sdk.plugins.action import BaseAction
23
+
24
+
25
+ # Default template for the job entrypoint script
26
+ _ENTRYPOINT_SCRIPT_TEMPLATE = '''#!/usr/bin/env python
27
+ """Auto-generated entrypoint script for Ray Jobs API."""
28
+ import importlib
29
+ import json
30
+ import os
31
+ import sys
32
+ import logging
33
+
34
+ def main():
35
+ # Configure logging to ensure ConsoleLogger output is visible
36
+ logging.basicConfig(
37
+ level=logging.INFO,
38
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
39
+ force=True,
40
+ )
41
+
42
+ # Add working directory to path
43
+ cwd = os.getcwd()
44
+ if cwd not in sys.path:
45
+ sys.path.insert(0, cwd)
46
+
47
+ # Load params from environment or file
48
+ params_json = os.environ.get("SYNAPSE_ACTION_PARAMS")
49
+ if not params_json:
50
+ params_file = os.environ.get("SYNAPSE_ACTION_PARAMS_FILE")
51
+ if params_file and os.path.exists(params_file):
52
+ with open(params_file) as f:
53
+ params_json = f.read()
54
+
55
+ if not params_json:
56
+ raise ValueError("No params provided via SYNAPSE_ACTION_PARAMS or SYNAPSE_ACTION_PARAMS_FILE")
57
+
58
+ params = json.loads(params_json)
59
+
60
+ # Get entrypoint
61
+ entrypoint = os.environ.get("SYNAPSE_ACTION_ENTRYPOINT")
62
+ if not entrypoint:
63
+ raise ValueError("SYNAPSE_ACTION_ENTRYPOINT not set")
64
+
65
+ # Import action class
66
+ module_path, class_name = entrypoint.rsplit(".", 1)
67
+ module = importlib.import_module(module_path)
68
+ action_cls = getattr(module, class_name)
69
+
70
+ # Validate params
71
+ validated_params = action_cls.params_model.model_validate(params)
72
+
73
+ # Create context
74
+ from synapse_sdk.loggers import ConsoleLogger
75
+ from synapse_sdk.plugins.context import RuntimeContext
76
+ from synapse_sdk.utils.auth import create_backend_client
77
+
78
+ client = create_backend_client()
79
+ logger = ConsoleLogger()
80
+ ctx = RuntimeContext(
81
+ logger=logger,
82
+ env=dict(os.environ),
83
+ job_id=os.environ.get("RAY_JOB_ID"),
84
+ client=client,
85
+ )
86
+
87
+ # Execute action
88
+ action = action_cls(validated_params, ctx)
89
+ result = action.execute()
90
+
91
+ logger.finish()
92
+
93
+ # Output result as JSON for capture
94
+ print("__SYNAPSE_RESULT_START__")
95
+ print(json.dumps(result if isinstance(result, dict) else {"result": result}))
96
+ print("__SYNAPSE_RESULT_END__")
97
+
98
+ return result
99
+
100
+ if __name__ == "__main__":
101
+ main()
102
+ '''
103
+
104
+
105
+ class RayJobsApiExecutor(BaseRayExecutor):
106
+ """Ray Jobs API based execution with log streaming support.
107
+
108
+ Uses Ray's JobSubmissionClient for job management, which provides:
109
+ - Access to runtime environment setup logs
110
+ - Real-time log streaming via tail_job_logs
111
+ - Proper job lifecycle management
112
+
113
+ Example:
114
+ >>> executor = RayJobsApiExecutor(
115
+ ... dashboard_address='http://localhost:8265',
116
+ ... working_dir='/path/to/plugin',
117
+ ... )
118
+ >>> job_id = executor.submit(TrainAction, {'epochs': 100})
119
+ >>>
120
+ >>> # Stream logs including runtime env setup
121
+ >>> for log_line in executor.stream_logs(job_id):
122
+ ... print(log_line, end='')
123
+ >>>
124
+ >>> result = executor.get_result(job_id)
125
+ """
126
+
127
+ def __init__(
128
+ self,
129
+ env: PluginEnvironment | dict[str, Any] | None = None,
130
+ *,
131
+ dashboard_address: str = 'http://localhost:8265',
132
+ runtime_env: dict[str, Any] | None = None,
133
+ working_dir: str | Path | None = None,
134
+ requirements_file: str | Path | None = None,
135
+ package_manager: PackageManager | Literal['pip', 'uv'] = PackageManager.PIP,
136
+ package_manager_options: list[str] | None = None,
137
+ wheels_dir: str = 'wheels',
138
+ num_cpus: int | float | None = None,
139
+ num_gpus: int | float | None = None,
140
+ memory: int | None = None,
141
+ include_sdk: bool = False,
142
+ ) -> None:
143
+ """Initialize Ray Jobs API executor.
144
+
145
+ Args:
146
+ env: Environment config for the action. If None, loads from os.environ.
147
+ dashboard_address: Ray Dashboard HTTP address (e.g., 'http://localhost:8265').
148
+ runtime_env: Ray runtime environment config.
149
+ working_dir: Plugin working directory.
150
+ requirements_file: Path to requirements.txt.
151
+ package_manager: Package manager to use ('pip' or 'uv').
152
+ package_manager_options: Additional options for the package manager.
153
+ wheels_dir: Directory containing .whl files relative to working_dir.
154
+ num_cpus: Number of CPUs for the entrypoint.
155
+ num_gpus: Number of GPUs for the entrypoint.
156
+ memory: Memory in bytes for the entrypoint.
157
+ include_sdk: If True, bundle local SDK with upload (for development).
158
+ """
159
+ # Use 'auto' for ray_address since we're using the Jobs API
160
+ super().__init__(
161
+ env=env,
162
+ runtime_env=runtime_env,
163
+ working_dir=working_dir,
164
+ requirements_file=requirements_file,
165
+ package_manager=package_manager,
166
+ package_manager_options=package_manager_options,
167
+ wheels_dir=wheels_dir,
168
+ ray_address='auto',
169
+ include_sdk=include_sdk,
170
+ )
171
+ self._dashboard_address = dashboard_address
172
+ self._num_cpus = num_cpus
173
+ self._num_gpus = num_gpus
174
+ self._memory = memory
175
+ self._client: Any | None = None
176
+ self._job_results: dict[str, Any] = {} # job_id -> parsed result
177
+
178
+ def _get_client(self) -> Any:
179
+ """Get or create JobSubmissionClient."""
180
+ if self._client is None:
181
+ from ray.job_submission import JobSubmissionClient
182
+
183
+ self._client = JobSubmissionClient(self._dashboard_address)
184
+ return self._client
185
+
186
+ def _build_jobs_api_runtime_env(self) -> dict[str, Any]:
187
+ """Build runtime environment for Jobs API.
188
+
189
+ The Jobs API requires working_dir to be a local path that it will upload,
190
+ or a remote URI (gcs://, s3://, etc.).
191
+ """
192
+ runtime_env = self._build_runtime_env()
193
+
194
+ # For Jobs API, include SDK in py_modules if requested
195
+ if self._include_sdk:
196
+ import synapse_sdk
197
+
198
+ sdk_path = str(Path(synapse_sdk.__file__).parent)
199
+ runtime_env.setdefault('py_modules', [])
200
+ if sdk_path not in runtime_env['py_modules']:
201
+ runtime_env['py_modules'].append(sdk_path)
202
+
203
+ # Ensure working_dir is set for Jobs API
204
+ if self._working_dir and 'working_dir' not in runtime_env:
205
+ runtime_env['working_dir'] = str(self._working_dir)
206
+
207
+ return runtime_env
208
+
209
+ def submit(
210
+ self,
211
+ action_cls: type[BaseAction] | str,
212
+ params: dict[str, Any],
213
+ *,
214
+ job_id: str | None = None,
215
+ ) -> str:
216
+ """Submit action as a Ray Job (non-blocking).
217
+
218
+ Args:
219
+ action_cls: BaseAction subclass or entrypoint string.
220
+ params: Parameters dict for the action.
221
+ job_id: Optional job identifier. If None, Ray generates one.
222
+
223
+ Returns:
224
+ Job ID for tracking.
225
+ """
226
+ import json
227
+
228
+ client = self._get_client()
229
+
230
+ # Convert class to entrypoint string
231
+ if isinstance(action_cls, str):
232
+ entrypoint = action_cls
233
+ else:
234
+ entrypoint = f'{action_cls.__module__}.{action_cls.__name__}'
235
+
236
+ # Build runtime env
237
+ runtime_env = self._build_jobs_api_runtime_env()
238
+
239
+ # Add params and entrypoint to env vars
240
+ runtime_env.setdefault('env_vars', {})
241
+ runtime_env['env_vars']['SYNAPSE_ACTION_ENTRYPOINT'] = entrypoint
242
+
243
+ # For small params, use env var directly. For large params, use a temp file.
244
+ params_json = json.dumps(params)
245
+ if len(params_json) < 32000: # Safe limit for env var size
246
+ runtime_env['env_vars']['SYNAPSE_ACTION_PARAMS'] = params_json
247
+ else:
248
+ # Write params to a temp file in working_dir
249
+ if self._working_dir:
250
+ params_file = Path(self._working_dir) / '.synapse_params.json'
251
+ params_file.write_text(params_json)
252
+ runtime_env['env_vars']['SYNAPSE_ACTION_PARAMS_FILE'] = '.synapse_params.json'
253
+ else:
254
+ # Fallback: use temp directory
255
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
256
+ f.write(params_json)
257
+ runtime_env['env_vars']['SYNAPSE_ACTION_PARAMS_FILE'] = f.name
258
+
259
+ # Create entrypoint script in working_dir
260
+ if self._working_dir:
261
+ entrypoint_script = Path(self._working_dir) / '_synapse_entrypoint.py'
262
+ entrypoint_script.write_text(_ENTRYPOINT_SCRIPT_TEMPLATE)
263
+ entrypoint_cmd = 'python _synapse_entrypoint.py'
264
+ else:
265
+ # Write script inline via python -c
266
+ # This is less ideal but works without a working_dir
267
+ entrypoint_cmd = f'python -c "{_ENTRYPOINT_SCRIPT_TEMPLATE.replace(chr(34), chr(92) + chr(34))}"'
268
+
269
+ # Submit job
270
+ submit_kwargs: dict[str, Any] = {
271
+ 'entrypoint': entrypoint_cmd,
272
+ 'runtime_env': runtime_env,
273
+ }
274
+
275
+ if job_id:
276
+ submit_kwargs['submission_id'] = job_id
277
+
278
+ if self._num_cpus is not None:
279
+ submit_kwargs['entrypoint_num_cpus'] = self._num_cpus
280
+ if self._num_gpus is not None:
281
+ submit_kwargs['entrypoint_num_gpus'] = self._num_gpus
282
+ if self._memory is not None:
283
+ submit_kwargs['entrypoint_memory'] = self._memory
284
+
285
+ return client.submit_job(**submit_kwargs)
286
+
287
+ def get_status(self, job_id: str) -> JobStatus:
288
+ """Get job status.
289
+
290
+ Args:
291
+ job_id: Job ID from submit().
292
+
293
+ Returns:
294
+ JobStatus enum (PENDING, RUNNING, SUCCEEDED, FAILED, STOPPED).
295
+ """
296
+ client = self._get_client()
297
+ return client.get_job_status(job_id)
298
+
299
+ def get_logs(self, job_id: str) -> str:
300
+ """Get all job logs (includes runtime env setup logs).
301
+
302
+ Args:
303
+ job_id: Job ID from submit().
304
+
305
+ Returns:
306
+ Full job logs as a string.
307
+ """
308
+ client = self._get_client()
309
+ return client.get_job_logs(job_id)
310
+
311
+ def stream_logs(
312
+ self,
313
+ job_id: str,
314
+ *,
315
+ timeout: float = 3600.0,
316
+ ) -> Iterator[str]:
317
+ """Stream job logs synchronously (includes runtime env setup logs).
318
+
319
+ This is a synchronous wrapper around the async tail_job_logs method.
320
+ Streams logs in real-time, including runtime environment setup progress.
321
+
322
+ Args:
323
+ job_id: Job ID from submit().
324
+ timeout: Maximum time to stream logs in seconds.
325
+
326
+ Yields:
327
+ Log lines as they become available.
328
+
329
+ Example:
330
+ >>> for line in executor.stream_logs(job_id):
331
+ ... print(line, end='')
332
+ """
333
+
334
+ async def _stream() -> AsyncIterator[str]:
335
+ client = self._get_client()
336
+ async for lines in client.tail_job_logs(job_id):
337
+ yield lines
338
+
339
+ # Run async generator in sync context
340
+ loop = asyncio.new_event_loop()
341
+ try:
342
+ agen = _stream()
343
+ while True:
344
+ try:
345
+ future = asyncio.wait_for(
346
+ agen.__anext__(), # type: ignore[union-attr]
347
+ timeout=timeout,
348
+ )
349
+ yield loop.run_until_complete(future)
350
+ except StopAsyncIteration:
351
+ break
352
+ except asyncio.TimeoutError:
353
+ break
354
+ finally:
355
+ loop.close()
356
+
357
+ async def stream_logs_async(
358
+ self,
359
+ job_id: str,
360
+ ) -> AsyncIterator[str]:
361
+ """Stream job logs asynchronously (includes runtime env setup logs).
362
+
363
+ Streams logs in real-time, including runtime environment setup progress.
364
+
365
+ Args:
366
+ job_id: Job ID from submit().
367
+
368
+ Yields:
369
+ Log lines as they become available.
370
+
371
+ Example:
372
+ >>> async for line in executor.stream_logs_async(job_id):
373
+ ... print(line, end='')
374
+ """
375
+ client = self._get_client()
376
+ async for lines in client.tail_job_logs(job_id):
377
+ yield lines
378
+
379
+ def get_result(self, job_id: str, timeout: float | None = None) -> Any:
380
+ """Get job result (blocks until complete).
381
+
382
+ Parses the result from the job output logs.
383
+
384
+ Args:
385
+ job_id: Job ID from submit().
386
+ timeout: Optional timeout in seconds.
387
+
388
+ Returns:
389
+ Action result parsed from job output.
390
+
391
+ Raises:
392
+ ExecutionError: If job failed or result cannot be parsed.
393
+ """
394
+ import time
395
+
396
+ client = self._get_client()
397
+ start_time = time.time()
398
+
399
+ while True:
400
+ status = client.get_job_status(job_id)
401
+
402
+ if status.is_terminal():
403
+ if str(status) in ('SUCCEEDED', 'JobStatus.SUCCEEDED'):
404
+ # Parse result from logs
405
+ logs = client.get_job_logs(job_id)
406
+ return self._parse_result_from_logs(logs, job_id)
407
+ else:
408
+ logs = client.get_job_logs(job_id)
409
+ raise ExecutionError(f'Job {job_id} failed with status {status}. Logs:\n{logs}')
410
+
411
+ if timeout is not None:
412
+ elapsed = time.time() - start_time
413
+ if elapsed >= timeout:
414
+ raise ExecutionError(f'Job {job_id} timed out after {timeout}s')
415
+
416
+ time.sleep(1)
417
+
418
+ def _parse_result_from_logs(self, logs: str, job_id: str) -> Any:
419
+ """Parse result from job output logs.
420
+
421
+ Args:
422
+ logs: Full job logs.
423
+ job_id: Job ID for error messages.
424
+
425
+ Returns:
426
+ Parsed result dict.
427
+
428
+ Raises:
429
+ ExecutionError: If result markers not found or parsing fails.
430
+ """
431
+ import json
432
+
433
+ start_marker = '__SYNAPSE_RESULT_START__'
434
+ end_marker = '__SYNAPSE_RESULT_END__'
435
+
436
+ start_idx = logs.find(start_marker)
437
+ end_idx = logs.find(end_marker)
438
+
439
+ if start_idx == -1 or end_idx == -1:
440
+ raise ExecutionError(
441
+ f'Could not parse result from job {job_id} logs. Result markers not found. Logs:\n{logs}'
442
+ )
443
+
444
+ result_json = logs[start_idx + len(start_marker) : end_idx].strip()
445
+ try:
446
+ return json.loads(result_json)
447
+ except json.JSONDecodeError as e:
448
+ raise ExecutionError(f'Failed to parse result JSON from job {job_id}: {e}') from e
449
+
450
+ def wait(
451
+ self,
452
+ job_id: str,
453
+ timeout_seconds: float = 300,
454
+ poll_interval: float = 1.0,
455
+ ) -> str:
456
+ """Wait for job to complete.
457
+
458
+ Args:
459
+ job_id: Job ID from submit().
460
+ timeout_seconds: Maximum time to wait.
461
+ poll_interval: Time between status checks.
462
+
463
+ Returns:
464
+ Final job status as string.
465
+
466
+ Raises:
467
+ ExecutionError: If job fails or times out.
468
+ """
469
+ import time
470
+
471
+ client = self._get_client()
472
+ start_time = time.time()
473
+
474
+ while True:
475
+ status = client.get_job_status(job_id)
476
+
477
+ if status.is_terminal():
478
+ return str(status)
479
+
480
+ elapsed = time.time() - start_time
481
+ if elapsed >= timeout_seconds:
482
+ raise ExecutionError(f'Job {job_id} timed out after {timeout_seconds}s')
483
+
484
+ time.sleep(poll_interval)
485
+
486
+ def stop(self, job_id: str) -> bool:
487
+ """Stop a running job.
488
+
489
+ Args:
490
+ job_id: Job ID from submit().
491
+
492
+ Returns:
493
+ True if stop was successful.
494
+ """
495
+ client = self._get_client()
496
+ return client.stop_job(job_id)
497
+
498
+ def delete(self, job_id: str) -> bool:
499
+ """Delete job info (only for terminal jobs).
500
+
501
+ Args:
502
+ job_id: Job ID from submit().
503
+
504
+ Returns:
505
+ True if deletion was successful.
506
+ """
507
+ client = self._get_client()
508
+ return client.delete_job(job_id)
509
+
510
+
511
+ __all__ = ['RayJobsApiExecutor']
@@ -0,0 +1,137 @@
1
+ """Utilities for packaging and uploading working directories to Ray GCS."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+
9
+ def upload_working_dir_to_gcs(working_dir: str | Path) -> str:
10
+ """Package a local directory and upload to Ray's Global Control Store.
11
+
12
+ Ray's working_dir with remote clusters requires the directory to be
13
+ uploaded to a location Ray workers can access. This function:
14
+ 1. Creates a zip archive of the directory
15
+ 2. Uploads it to Ray's GCS (content-addressable storage)
16
+ 3. Returns the gcs:// URI for use in runtime_env
17
+
18
+ Args:
19
+ working_dir: Local directory path to package and upload.
20
+
21
+ Returns:
22
+ gcs:// URI that Ray can use for working_dir.
23
+ Example: "gcs://_ray_pkg_abc123def456.zip"
24
+
25
+ Raises:
26
+ RuntimeError: If Ray is not initialized or not connected.
27
+ FileNotFoundError: If working_dir doesn't exist.
28
+
29
+ Example:
30
+ >>> ray.init('ray://10.0.0.4:10001')
31
+ >>> gcs_uri = upload_working_dir_to_gcs('/path/to/plugin')
32
+ >>> runtime_env = {'working_dir': gcs_uri}
33
+ """
34
+ try:
35
+ import ray
36
+ except ImportError:
37
+ raise RuntimeError('Ray is not installed. Install with: pip install ray')
38
+
39
+ if not ray.is_initialized():
40
+ raise RuntimeError(
41
+ 'Ray must be initialized before uploading to GCS. Call ray.init() or connect to a cluster first.'
42
+ )
43
+
44
+ from ray._private.runtime_env.packaging import (
45
+ get_uri_for_package,
46
+ package_exists,
47
+ upload_package_to_gcs,
48
+ )
49
+
50
+ working_dir = Path(working_dir).resolve()
51
+ if not working_dir.exists():
52
+ raise FileNotFoundError(f'Working directory not found: {working_dir}')
53
+
54
+ # Import archive utilities
55
+ from synapse_sdk.utils.file.archive import ArchiveFilter, create_archive
56
+
57
+ # Create zip in temporary location
58
+ with tempfile.TemporaryDirectory() as temp_dir:
59
+ archive_path = Path(temp_dir) / 'working_dir.zip'
60
+
61
+ # Create archive with default excludes
62
+ archive_filter = ArchiveFilter.from_patterns()
63
+ create_archive(working_dir, archive_path, filter=archive_filter)
64
+
65
+ # Generate content-addressable gcs:// URI
66
+ gcs_uri = get_uri_for_package(archive_path)
67
+
68
+ # Upload if not already present (deduplication)
69
+ if not package_exists(gcs_uri):
70
+ upload_package_to_gcs(gcs_uri, archive_path.read_bytes())
71
+
72
+ return gcs_uri
73
+
74
+
75
+ def upload_module_to_gcs(module_dir: str | Path) -> str:
76
+ """Package a Python module directory and upload to Ray's GCS.
77
+
78
+ Unlike upload_working_dir_to_gcs which archives directory contents,
79
+ this preserves the module name in the archive so it can be imported.
80
+
81
+ Args:
82
+ module_dir: Path to the module directory (e.g., /path/to/synapse_sdk).
83
+
84
+ Returns:
85
+ gcs:// URI for use in runtime_env py_modules.
86
+
87
+ Example:
88
+ >>> gcs_uri = upload_module_to_gcs('/path/to/synapse_sdk')
89
+ >>> runtime_env = {'py_modules': [gcs_uri]}
90
+ >>> # Remote can now `import synapse_sdk`
91
+ """
92
+ import zipfile
93
+
94
+ try:
95
+ import ray
96
+ except ImportError:
97
+ raise RuntimeError('Ray is not installed. Install with: pip install ray')
98
+
99
+ if not ray.is_initialized():
100
+ raise RuntimeError('Ray must be initialized before uploading to GCS.')
101
+
102
+ from ray._private.runtime_env.packaging import (
103
+ get_uri_for_package,
104
+ package_exists,
105
+ upload_package_to_gcs,
106
+ )
107
+
108
+ module_dir = Path(module_dir).resolve()
109
+ if not module_dir.exists():
110
+ raise FileNotFoundError(f'Module directory not found: {module_dir}')
111
+
112
+ module_name = module_dir.name # e.g., 'synapse_sdk'
113
+
114
+ from synapse_sdk.utils.file.archive import ArchiveFilter
115
+
116
+ archive_filter = ArchiveFilter.from_patterns()
117
+
118
+ with tempfile.TemporaryDirectory() as temp_dir:
119
+ archive_path = Path(temp_dir) / 'module.zip'
120
+
121
+ # Create archive with module name as root directory
122
+ with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zf:
123
+ for file_path in module_dir.rglob('*'):
124
+ if file_path.is_file() and archive_filter.should_include(file_path, module_dir):
125
+ # Prefix with module name so synapse_sdk/... structure is preserved
126
+ arcname = f'{module_name}/{file_path.relative_to(module_dir)}'
127
+ zf.write(file_path, arcname)
128
+
129
+ gcs_uri = get_uri_for_package(archive_path)
130
+
131
+ if not package_exists(gcs_uri):
132
+ upload_package_to_gcs(gcs_uri, archive_path.read_bytes())
133
+
134
+ return gcs_uri
135
+
136
+
137
+ __all__ = ['upload_module_to_gcs', 'upload_working_dir_to_gcs']