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.
- synapse_sdk/__init__.py +24 -0
- synapse_sdk/cli/__init__.py +9 -8
- synapse_sdk/cli/agent/__init__.py +25 -0
- synapse_sdk/cli/agent/config.py +104 -0
- synapse_sdk/cli/agent/select.py +197 -0
- synapse_sdk/cli/auth.py +104 -0
- synapse_sdk/cli/main.py +1025 -0
- synapse_sdk/cli/plugin/__init__.py +58 -0
- synapse_sdk/cli/plugin/create.py +566 -0
- synapse_sdk/cli/plugin/job.py +196 -0
- synapse_sdk/cli/plugin/publish.py +322 -0
- synapse_sdk/cli/plugin/run.py +131 -0
- synapse_sdk/cli/plugin/test.py +200 -0
- synapse_sdk/clients/README.md +239 -0
- synapse_sdk/clients/__init__.py +5 -0
- synapse_sdk/clients/_template.py +266 -0
- synapse_sdk/clients/agent/__init__.py +84 -29
- synapse_sdk/clients/agent/async_ray.py +289 -0
- synapse_sdk/clients/agent/container.py +83 -0
- synapse_sdk/clients/agent/plugin.py +101 -0
- synapse_sdk/clients/agent/ray.py +296 -39
- synapse_sdk/clients/backend/__init__.py +152 -12
- synapse_sdk/clients/backend/annotation.py +164 -22
- synapse_sdk/clients/backend/core.py +101 -0
- synapse_sdk/clients/backend/data_collection.py +292 -0
- synapse_sdk/clients/backend/hitl.py +87 -0
- synapse_sdk/clients/backend/integration.py +374 -46
- synapse_sdk/clients/backend/ml.py +134 -22
- synapse_sdk/clients/backend/models.py +247 -0
- synapse_sdk/clients/base.py +538 -59
- synapse_sdk/clients/exceptions.py +35 -7
- synapse_sdk/clients/pipeline/__init__.py +5 -0
- synapse_sdk/clients/pipeline/client.py +636 -0
- synapse_sdk/clients/protocols.py +178 -0
- synapse_sdk/clients/utils.py +86 -8
- synapse_sdk/clients/validation.py +58 -0
- synapse_sdk/enums.py +76 -0
- synapse_sdk/exceptions.py +168 -0
- synapse_sdk/integrations/__init__.py +74 -0
- synapse_sdk/integrations/_base.py +119 -0
- synapse_sdk/integrations/_context.py +53 -0
- synapse_sdk/integrations/ultralytics/__init__.py +78 -0
- synapse_sdk/integrations/ultralytics/_callbacks.py +126 -0
- synapse_sdk/integrations/ultralytics/_patches.py +124 -0
- synapse_sdk/loggers.py +476 -95
- synapse_sdk/mcp/MCP.md +69 -0
- synapse_sdk/mcp/__init__.py +48 -0
- synapse_sdk/mcp/__main__.py +6 -0
- synapse_sdk/mcp/config.py +349 -0
- synapse_sdk/mcp/prompts/__init__.py +4 -0
- synapse_sdk/mcp/resources/__init__.py +4 -0
- synapse_sdk/mcp/server.py +1352 -0
- synapse_sdk/mcp/tools/__init__.py +6 -0
- synapse_sdk/plugins/__init__.py +133 -9
- synapse_sdk/plugins/action.py +229 -0
- synapse_sdk/plugins/actions/__init__.py +82 -0
- synapse_sdk/plugins/actions/dataset/__init__.py +37 -0
- synapse_sdk/plugins/actions/dataset/action.py +471 -0
- synapse_sdk/plugins/actions/export/__init__.py +55 -0
- synapse_sdk/plugins/actions/export/action.py +183 -0
- synapse_sdk/plugins/actions/export/context.py +59 -0
- synapse_sdk/plugins/actions/inference/__init__.py +84 -0
- synapse_sdk/plugins/actions/inference/action.py +285 -0
- synapse_sdk/plugins/actions/inference/context.py +81 -0
- synapse_sdk/plugins/actions/inference/deployment.py +322 -0
- synapse_sdk/plugins/actions/inference/serve.py +252 -0
- synapse_sdk/plugins/actions/train/__init__.py +54 -0
- synapse_sdk/plugins/actions/train/action.py +326 -0
- synapse_sdk/plugins/actions/train/context.py +57 -0
- synapse_sdk/plugins/actions/upload/__init__.py +49 -0
- synapse_sdk/plugins/actions/upload/action.py +165 -0
- synapse_sdk/plugins/actions/upload/context.py +61 -0
- synapse_sdk/plugins/config.py +98 -0
- synapse_sdk/plugins/context/__init__.py +109 -0
- synapse_sdk/plugins/context/env.py +113 -0
- synapse_sdk/plugins/datasets/__init__.py +113 -0
- synapse_sdk/plugins/datasets/converters/__init__.py +76 -0
- synapse_sdk/plugins/datasets/converters/base.py +347 -0
- synapse_sdk/plugins/datasets/converters/yolo/__init__.py +9 -0
- synapse_sdk/plugins/datasets/converters/yolo/from_dm.py +468 -0
- synapse_sdk/plugins/datasets/converters/yolo/to_dm.py +381 -0
- synapse_sdk/plugins/datasets/formats/__init__.py +82 -0
- synapse_sdk/plugins/datasets/formats/dm.py +351 -0
- synapse_sdk/plugins/datasets/formats/yolo.py +240 -0
- synapse_sdk/plugins/decorators.py +83 -0
- synapse_sdk/plugins/discovery.py +790 -0
- synapse_sdk/plugins/docs/ACTION_DEV_GUIDE.md +933 -0
- synapse_sdk/plugins/docs/ARCHITECTURE.md +1225 -0
- synapse_sdk/plugins/docs/LOGGING_SYSTEM.md +683 -0
- synapse_sdk/plugins/docs/OVERVIEW.md +531 -0
- synapse_sdk/plugins/docs/PIPELINE_GUIDE.md +145 -0
- synapse_sdk/plugins/docs/README.md +513 -0
- synapse_sdk/plugins/docs/STEP.md +656 -0
- synapse_sdk/plugins/enums.py +70 -10
- synapse_sdk/plugins/errors.py +92 -0
- synapse_sdk/plugins/executors/__init__.py +43 -0
- synapse_sdk/plugins/executors/local.py +99 -0
- synapse_sdk/plugins/executors/ray/__init__.py +18 -0
- synapse_sdk/plugins/executors/ray/base.py +282 -0
- synapse_sdk/plugins/executors/ray/job.py +298 -0
- synapse_sdk/plugins/executors/ray/jobs_api.py +511 -0
- synapse_sdk/plugins/executors/ray/packaging.py +137 -0
- synapse_sdk/plugins/executors/ray/pipeline.py +792 -0
- synapse_sdk/plugins/executors/ray/task.py +257 -0
- synapse_sdk/plugins/models/__init__.py +26 -0
- synapse_sdk/plugins/models/logger.py +173 -0
- synapse_sdk/plugins/models/pipeline.py +25 -0
- synapse_sdk/plugins/pipelines/__init__.py +81 -0
- synapse_sdk/plugins/pipelines/action_pipeline.py +417 -0
- synapse_sdk/plugins/pipelines/context.py +107 -0
- synapse_sdk/plugins/pipelines/display.py +311 -0
- synapse_sdk/plugins/runner.py +114 -0
- synapse_sdk/plugins/schemas/__init__.py +19 -0
- synapse_sdk/plugins/schemas/results.py +152 -0
- synapse_sdk/plugins/steps/__init__.py +63 -0
- synapse_sdk/plugins/steps/base.py +128 -0
- synapse_sdk/plugins/steps/context.py +90 -0
- synapse_sdk/plugins/steps/orchestrator.py +128 -0
- synapse_sdk/plugins/steps/registry.py +103 -0
- synapse_sdk/plugins/steps/utils/__init__.py +20 -0
- synapse_sdk/plugins/steps/utils/logging.py +85 -0
- synapse_sdk/plugins/steps/utils/timing.py +71 -0
- synapse_sdk/plugins/steps/utils/validation.py +68 -0
- synapse_sdk/plugins/templates/__init__.py +50 -0
- synapse_sdk/plugins/templates/base/.gitignore.j2 +26 -0
- synapse_sdk/plugins/templates/base/.synapseignore.j2 +11 -0
- synapse_sdk/plugins/templates/base/README.md.j2 +26 -0
- synapse_sdk/plugins/templates/base/plugin/__init__.py.j2 +1 -0
- synapse_sdk/plugins/templates/base/pyproject.toml.j2 +14 -0
- synapse_sdk/plugins/templates/base/requirements.txt.j2 +1 -0
- synapse_sdk/plugins/templates/custom/plugin/main.py.j2 +18 -0
- synapse_sdk/plugins/templates/data_validation/plugin/validate.py.j2 +32 -0
- synapse_sdk/plugins/templates/export/plugin/export.py.j2 +36 -0
- synapse_sdk/plugins/templates/neural_net/plugin/inference.py.j2 +36 -0
- synapse_sdk/plugins/templates/neural_net/plugin/train.py.j2 +33 -0
- synapse_sdk/plugins/templates/post_annotation/plugin/post_annotate.py.j2 +32 -0
- synapse_sdk/plugins/templates/pre_annotation/plugin/pre_annotate.py.j2 +32 -0
- synapse_sdk/plugins/templates/smart_tool/plugin/auto_label.py.j2 +44 -0
- synapse_sdk/plugins/templates/upload/plugin/upload.py.j2 +35 -0
- synapse_sdk/plugins/testing/__init__.py +25 -0
- synapse_sdk/plugins/testing/sample_actions.py +98 -0
- synapse_sdk/plugins/types.py +206 -0
- synapse_sdk/plugins/upload.py +595 -64
- synapse_sdk/plugins/utils.py +325 -37
- synapse_sdk/shared/__init__.py +25 -0
- synapse_sdk/utils/__init__.py +1 -0
- synapse_sdk/utils/auth.py +74 -0
- synapse_sdk/utils/file/__init__.py +58 -0
- synapse_sdk/utils/file/archive.py +449 -0
- synapse_sdk/utils/file/checksum.py +167 -0
- synapse_sdk/utils/file/download.py +286 -0
- synapse_sdk/utils/file/io.py +129 -0
- synapse_sdk/utils/file/requirements.py +36 -0
- synapse_sdk/utils/network.py +168 -0
- synapse_sdk/utils/storage/__init__.py +238 -0
- synapse_sdk/utils/storage/config.py +188 -0
- synapse_sdk/utils/storage/errors.py +52 -0
- synapse_sdk/utils/storage/providers/__init__.py +13 -0
- synapse_sdk/utils/storage/providers/base.py +76 -0
- synapse_sdk/utils/storage/providers/gcs.py +168 -0
- synapse_sdk/utils/storage/providers/http.py +250 -0
- synapse_sdk/utils/storage/providers/local.py +126 -0
- synapse_sdk/utils/storage/providers/s3.py +177 -0
- synapse_sdk/utils/storage/providers/sftp.py +208 -0
- synapse_sdk/utils/storage/registry.py +125 -0
- synapse_sdk/utils/websocket.py +99 -0
- synapse_sdk-2026.1.1b2.dist-info/METADATA +715 -0
- synapse_sdk-2026.1.1b2.dist-info/RECORD +172 -0
- {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/WHEEL +1 -1
- synapse_sdk-2026.1.1b2.dist-info/licenses/LICENSE +201 -0
- locale/en/LC_MESSAGES/messages.mo +0 -0
- locale/en/LC_MESSAGES/messages.po +0 -39
- locale/ko/LC_MESSAGES/messages.mo +0 -0
- locale/ko/LC_MESSAGES/messages.po +0 -34
- synapse_sdk/cli/create_plugin.py +0 -10
- synapse_sdk/clients/agent/core.py +0 -7
- synapse_sdk/clients/agent/service.py +0 -15
- synapse_sdk/clients/backend/dataset.py +0 -51
- synapse_sdk/clients/ray/__init__.py +0 -6
- synapse_sdk/clients/ray/core.py +0 -22
- synapse_sdk/clients/ray/serve.py +0 -20
- synapse_sdk/i18n.py +0 -35
- synapse_sdk/plugins/categories/__init__.py +0 -0
- synapse_sdk/plugins/categories/base.py +0 -235
- synapse_sdk/plugins/categories/data_validation/__init__.py +0 -0
- synapse_sdk/plugins/categories/data_validation/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/data_validation/actions/validation.py +0 -10
- synapse_sdk/plugins/categories/data_validation/templates/config.yaml +0 -3
- synapse_sdk/plugins/categories/data_validation/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +0 -5
- synapse_sdk/plugins/categories/decorators.py +0 -13
- synapse_sdk/plugins/categories/export/__init__.py +0 -0
- synapse_sdk/plugins/categories/export/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/export/actions/export.py +0 -10
- synapse_sdk/plugins/categories/import/__init__.py +0 -0
- synapse_sdk/plugins/categories/import/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/import/actions/import.py +0 -10
- synapse_sdk/plugins/categories/neural_net/__init__.py +0 -0
- synapse_sdk/plugins/categories/neural_net/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/neural_net/actions/deployment.py +0 -45
- synapse_sdk/plugins/categories/neural_net/actions/inference.py +0 -18
- synapse_sdk/plugins/categories/neural_net/actions/test.py +0 -10
- synapse_sdk/plugins/categories/neural_net/actions/train.py +0 -143
- synapse_sdk/plugins/categories/neural_net/templates/config.yaml +0 -12
- synapse_sdk/plugins/categories/neural_net/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +0 -4
- synapse_sdk/plugins/categories/neural_net/templates/plugin/test.py +0 -2
- synapse_sdk/plugins/categories/neural_net/templates/plugin/train.py +0 -14
- synapse_sdk/plugins/categories/post_annotation/__init__.py +0 -0
- synapse_sdk/plugins/categories/post_annotation/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/post_annotation/actions/post_annotation.py +0 -10
- synapse_sdk/plugins/categories/post_annotation/templates/config.yaml +0 -3
- synapse_sdk/plugins/categories/post_annotation/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/post_annotation/templates/plugin/post_annotation.py +0 -3
- synapse_sdk/plugins/categories/pre_annotation/__init__.py +0 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation.py +0 -10
- synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +0 -3
- synapse_sdk/plugins/categories/pre_annotation/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/pre_annotation/templates/plugin/pre_annotation.py +0 -3
- synapse_sdk/plugins/categories/registry.py +0 -16
- synapse_sdk/plugins/categories/smart_tool/__init__.py +0 -0
- synapse_sdk/plugins/categories/smart_tool/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/smart_tool/actions/auto_label.py +0 -37
- synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +0 -7
- synapse_sdk/plugins/categories/smart_tool/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/smart_tool/templates/plugin/auto_label.py +0 -11
- synapse_sdk/plugins/categories/templates.py +0 -32
- synapse_sdk/plugins/cli/__init__.py +0 -21
- synapse_sdk/plugins/cli/publish.py +0 -37
- synapse_sdk/plugins/cli/run.py +0 -67
- synapse_sdk/plugins/exceptions.py +0 -22
- synapse_sdk/plugins/models.py +0 -121
- synapse_sdk/plugins/templates/cookiecutter.json +0 -11
- synapse_sdk/plugins/templates/hooks/post_gen_project.py +0 -3
- synapse_sdk/plugins/templates/hooks/pre_prompt.py +0 -21
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.gitignore +0 -27
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.pre-commit-config.yaml +0 -7
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/README.md +0 -5
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +0 -6
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/plugin/__init__.py +0 -0
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/pyproject.toml +0 -13
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +0 -1
- synapse_sdk/shared/enums.py +0 -8
- synapse_sdk/utils/debug.py +0 -5
- synapse_sdk/utils/file.py +0 -87
- synapse_sdk/utils/module_loading.py +0 -29
- synapse_sdk/utils/pydantic/__init__.py +0 -0
- synapse_sdk/utils/pydantic/config.py +0 -4
- synapse_sdk/utils/pydantic/errors.py +0 -33
- synapse_sdk/utils/pydantic/validators.py +0 -7
- synapse_sdk/utils/storage.py +0 -91
- synapse_sdk/utils/string.py +0 -11
- synapse_sdk-1.0.0a11.dist-info/LICENSE +0 -21
- synapse_sdk-1.0.0a11.dist-info/METADATA +0 -43
- synapse_sdk-1.0.0a11.dist-info/RECORD +0 -111
- {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/entry_points.txt +0 -0
- {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']
|