synapse-sdk 1.0.0b5__py3-none-any.whl → 2025.12.3__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.
- synapse_sdk/__init__.py +24 -0
- synapse_sdk/cli/code_server.py +305 -33
- synapse_sdk/clients/agent/__init__.py +2 -1
- synapse_sdk/clients/agent/container.py +143 -0
- synapse_sdk/clients/agent/ray.py +296 -38
- synapse_sdk/clients/backend/annotation.py +1 -1
- synapse_sdk/clients/backend/core.py +31 -4
- synapse_sdk/clients/backend/data_collection.py +82 -7
- synapse_sdk/clients/backend/hitl.py +1 -1
- synapse_sdk/clients/backend/ml.py +1 -1
- synapse_sdk/clients/base.py +211 -61
- synapse_sdk/loggers.py +46 -0
- synapse_sdk/plugins/README.md +1340 -0
- synapse_sdk/plugins/categories/base.py +59 -9
- synapse_sdk/plugins/categories/export/actions/__init__.py +3 -0
- synapse_sdk/plugins/categories/export/actions/export/__init__.py +28 -0
- synapse_sdk/plugins/categories/export/actions/export/action.py +165 -0
- synapse_sdk/plugins/categories/export/actions/export/enums.py +113 -0
- synapse_sdk/plugins/categories/export/actions/export/exceptions.py +53 -0
- synapse_sdk/plugins/categories/export/actions/export/models.py +74 -0
- synapse_sdk/plugins/categories/export/actions/export/run.py +195 -0
- synapse_sdk/plugins/categories/export/actions/export/utils.py +187 -0
- synapse_sdk/plugins/categories/export/templates/config.yaml +19 -1
- synapse_sdk/plugins/categories/export/templates/plugin/__init__.py +390 -0
- synapse_sdk/plugins/categories/export/templates/plugin/export.py +153 -177
- synapse_sdk/plugins/categories/neural_net/actions/train.py +1130 -32
- synapse_sdk/plugins/categories/neural_net/actions/tune.py +157 -4
- synapse_sdk/plugins/categories/neural_net/templates/config.yaml +7 -4
- synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/action.py +10 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +148 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +100 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +248 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +265 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +92 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +243 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
- synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +19 -0
- synapse_sdk/plugins/categories/upload/actions/upload/action.py +236 -0
- synapse_sdk/plugins/categories/upload/actions/upload/context.py +185 -0
- synapse_sdk/plugins/categories/upload/actions/upload/enums.py +493 -0
- synapse_sdk/plugins/categories/upload/actions/upload/exceptions.py +36 -0
- synapse_sdk/plugins/categories/upload/actions/upload/factory.py +138 -0
- synapse_sdk/plugins/categories/upload/actions/upload/models.py +214 -0
- synapse_sdk/plugins/categories/upload/actions/upload/orchestrator.py +183 -0
- synapse_sdk/plugins/categories/upload/actions/upload/registry.py +113 -0
- synapse_sdk/plugins/categories/upload/actions/upload/run.py +179 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/base.py +107 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +62 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/collection.py +63 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +91 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +82 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +235 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +201 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +104 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/validate.py +71 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/base.py +82 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/batch.py +39 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/single.py +29 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +300 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +287 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/excel.py +174 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/none.py +16 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/sync.py +84 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +60 -0
- synapse_sdk/plugins/categories/upload/actions/upload/utils.py +250 -0
- synapse_sdk/plugins/categories/upload/templates/README.md +470 -0
- synapse_sdk/plugins/categories/upload/templates/config.yaml +28 -2
- synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py +310 -0
- synapse_sdk/plugins/categories/upload/templates/plugin/upload.py +82 -20
- synapse_sdk/plugins/models.py +111 -9
- synapse_sdk/plugins/templates/plugin-config-schema.json +7 -0
- synapse_sdk/plugins/templates/schema.json +7 -0
- synapse_sdk/plugins/utils/__init__.py +3 -0
- synapse_sdk/plugins/utils/ray_gcs.py +66 -0
- synapse_sdk/shared/__init__.py +25 -0
- synapse_sdk/utils/converters/dm/__init__.py +42 -41
- synapse_sdk/utils/converters/dm/base.py +137 -0
- synapse_sdk/utils/converters/dm/from_v1.py +208 -562
- synapse_sdk/utils/converters/dm/to_v1.py +258 -304
- synapse_sdk/utils/converters/dm/tools/__init__.py +214 -0
- synapse_sdk/utils/converters/dm/tools/answer.py +95 -0
- synapse_sdk/utils/converters/dm/tools/bounding_box.py +132 -0
- synapse_sdk/utils/converters/dm/tools/bounding_box_3d.py +121 -0
- synapse_sdk/utils/converters/dm/tools/classification.py +75 -0
- synapse_sdk/utils/converters/dm/tools/keypoint.py +117 -0
- synapse_sdk/utils/converters/dm/tools/named_entity.py +111 -0
- synapse_sdk/utils/converters/dm/tools/polygon.py +122 -0
- synapse_sdk/utils/converters/dm/tools/polyline.py +124 -0
- synapse_sdk/utils/converters/dm/tools/prompt.py +94 -0
- synapse_sdk/utils/converters/dm/tools/relation.py +86 -0
- synapse_sdk/utils/converters/dm/tools/segmentation.py +141 -0
- synapse_sdk/utils/converters/dm/tools/segmentation_3d.py +83 -0
- synapse_sdk/utils/converters/dm/types.py +168 -0
- synapse_sdk/utils/converters/dm/utils.py +162 -0
- synapse_sdk/utils/converters/dm_legacy/__init__.py +56 -0
- synapse_sdk/utils/converters/dm_legacy/from_v1.py +627 -0
- synapse_sdk/utils/converters/dm_legacy/to_v1.py +367 -0
- synapse_sdk/utils/file/__init__.py +58 -0
- synapse_sdk/utils/file/archive.py +32 -0
- synapse_sdk/utils/file/checksum.py +56 -0
- synapse_sdk/utils/file/chunking.py +31 -0
- synapse_sdk/utils/file/download.py +385 -0
- synapse_sdk/utils/file/encoding.py +40 -0
- synapse_sdk/utils/file/io.py +22 -0
- synapse_sdk/utils/file/upload.py +165 -0
- synapse_sdk/utils/file/video/__init__.py +29 -0
- synapse_sdk/utils/file/video/transcode.py +307 -0
- synapse_sdk/utils/{file.py → file.py.backup} +77 -0
- synapse_sdk/utils/network.py +272 -0
- synapse_sdk/utils/storage/__init__.py +6 -2
- synapse_sdk/utils/storage/providers/file_system.py +6 -0
- {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/METADATA +19 -2
- {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/RECORD +134 -74
- synapse_sdk/devtools/docs/.gitignore +0 -20
- synapse_sdk/devtools/docs/README.md +0 -41
- synapse_sdk/devtools/docs/blog/2019-05-28-first-blog-post.md +0 -12
- synapse_sdk/devtools/docs/blog/2019-05-29-long-blog-post.md +0 -44
- synapse_sdk/devtools/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -24
- synapse_sdk/devtools/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- synapse_sdk/devtools/docs/blog/2021-08-26-welcome/index.md +0 -29
- synapse_sdk/devtools/docs/blog/authors.yml +0 -25
- synapse_sdk/devtools/docs/blog/tags.yml +0 -19
- synapse_sdk/devtools/docs/docusaurus.config.ts +0 -138
- synapse_sdk/devtools/docs/package-lock.json +0 -17455
- synapse_sdk/devtools/docs/package.json +0 -47
- synapse_sdk/devtools/docs/sidebars.ts +0 -44
- synapse_sdk/devtools/docs/src/components/HomepageFeatures/index.tsx +0 -71
- synapse_sdk/devtools/docs/src/components/HomepageFeatures/styles.module.css +0 -11
- synapse_sdk/devtools/docs/src/css/custom.css +0 -30
- synapse_sdk/devtools/docs/src/pages/index.module.css +0 -23
- synapse_sdk/devtools/docs/src/pages/index.tsx +0 -21
- synapse_sdk/devtools/docs/src/pages/markdown-page.md +0 -7
- synapse_sdk/devtools/docs/static/.nojekyll +0 -0
- synapse_sdk/devtools/docs/static/img/docusaurus-social-card.jpg +0 -0
- synapse_sdk/devtools/docs/static/img/docusaurus.png +0 -0
- synapse_sdk/devtools/docs/static/img/favicon.ico +0 -0
- synapse_sdk/devtools/docs/static/img/logo.png +0 -0
- synapse_sdk/devtools/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
- synapse_sdk/devtools/docs/static/img/undraw_docusaurus_react.svg +0 -170
- synapse_sdk/devtools/docs/static/img/undraw_docusaurus_tree.svg +0 -40
- synapse_sdk/devtools/docs/tsconfig.json +0 -8
- synapse_sdk/plugins/categories/export/actions/export.py +0 -346
- synapse_sdk/plugins/categories/export/enums.py +0 -7
- synapse_sdk/plugins/categories/neural_net/actions/gradio.py +0 -151
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task.py +0 -943
- synapse_sdk/plugins/categories/upload/actions/upload.py +0 -954
- {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/WHEEL +0 -0
- {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/entry_points.txt +0 -0
- {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/licenses/LICENSE +0 -0
- {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/top_level.txt +0 -0
synapse_sdk/clients/agent/ray.py
CHANGED
|
@@ -1,10 +1,86 @@
|
|
|
1
|
-
import
|
|
1
|
+
import weakref
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
2
3
|
|
|
3
4
|
from synapse_sdk.clients.base import BaseClient
|
|
4
5
|
from synapse_sdk.clients.exceptions import ClientError
|
|
6
|
+
from synapse_sdk.utils.network import (
|
|
7
|
+
HTTPStreamManager,
|
|
8
|
+
StreamLimits,
|
|
9
|
+
WebSocketStreamManager,
|
|
10
|
+
http_to_websocket_url,
|
|
11
|
+
sanitize_error_message,
|
|
12
|
+
validate_resource_id,
|
|
13
|
+
validate_timeout,
|
|
14
|
+
)
|
|
5
15
|
|
|
6
16
|
|
|
7
17
|
class RayClientMixin(BaseClient):
|
|
18
|
+
"""Mixin class providing Ray cluster management and monitoring functionality.
|
|
19
|
+
|
|
20
|
+
This mixin extends BaseClient with Ray-specific operations for interacting with
|
|
21
|
+
Apache Ray distributed computing clusters. It provides comprehensive job management,
|
|
22
|
+
node monitoring, task tracking, and Ray Serve application control capabilities.
|
|
23
|
+
|
|
24
|
+
Key Features:
|
|
25
|
+
- Job lifecycle management (list, get, monitor)
|
|
26
|
+
- Real-time log streaming via WebSocket and HTTP protocols
|
|
27
|
+
- Node and task monitoring
|
|
28
|
+
- Ray Serve application deployment and management
|
|
29
|
+
- Robust error handling with input validation
|
|
30
|
+
- Resource management with automatic cleanup
|
|
31
|
+
|
|
32
|
+
Streaming Capabilities:
|
|
33
|
+
- WebSocket streaming for real-time log tailing
|
|
34
|
+
- HTTP streaming as fallback protocol
|
|
35
|
+
- Configurable timeouts and stream limits
|
|
36
|
+
- Automatic protocol validation and error recovery
|
|
37
|
+
|
|
38
|
+
Resource Management:
|
|
39
|
+
- Thread pool for concurrent operations (5 workers)
|
|
40
|
+
- WeakSet for tracking active connections
|
|
41
|
+
- Automatic cleanup on object destruction
|
|
42
|
+
- Stream limits to prevent resource exhaustion
|
|
43
|
+
|
|
44
|
+
Usage Examples:
|
|
45
|
+
Basic job operations:
|
|
46
|
+
>>> client = RayClient(base_url="http://ray-head:8265")
|
|
47
|
+
>>> jobs = client.list_jobs()
|
|
48
|
+
>>> job = client.get_job('job-12345')
|
|
49
|
+
|
|
50
|
+
Real-time log streaming:
|
|
51
|
+
>>> # WebSocket streaming (preferred)
|
|
52
|
+
>>> for log_line in client.tail_job_logs('job-12345', protocol='websocket'):
|
|
53
|
+
... print(log_line)
|
|
54
|
+
|
|
55
|
+
>>> # HTTP streaming (fallback)
|
|
56
|
+
>>> for log_line in client.tail_job_logs('job-12345', protocol='stream'):
|
|
57
|
+
... print(log_line)
|
|
58
|
+
|
|
59
|
+
Node and task monitoring:
|
|
60
|
+
>>> nodes = client.list_nodes()
|
|
61
|
+
>>> tasks = client.list_tasks()
|
|
62
|
+
>>> node_details = client.get_node('node-id')
|
|
63
|
+
|
|
64
|
+
Ray Serve management:
|
|
65
|
+
>>> apps = client.list_serve_applications()
|
|
66
|
+
>>> client.delete_serve_application('app-id')
|
|
67
|
+
|
|
68
|
+
Note:
|
|
69
|
+
This class is designed as a mixin and should be combined with other
|
|
70
|
+
client classes that provide authentication and base functionality.
|
|
71
|
+
It requires the BaseClient foundation for HTTP operations.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, *args, **kwargs):
|
|
75
|
+
super().__init__(*args, **kwargs)
|
|
76
|
+
self._thread_pool = ThreadPoolExecutor(max_workers=5, thread_name_prefix='ray_client_')
|
|
77
|
+
self._active_connections = weakref.WeakSet()
|
|
78
|
+
|
|
79
|
+
# Initialize stream managers
|
|
80
|
+
stream_limits = StreamLimits()
|
|
81
|
+
self._websocket_manager = WebSocketStreamManager(self._thread_pool, stream_limits)
|
|
82
|
+
self._http_manager = HTTPStreamManager(self.requests_session, stream_limits)
|
|
83
|
+
|
|
8
84
|
def get_job(self, pk):
|
|
9
85
|
path = f'jobs/{pk}/'
|
|
10
86
|
return self._get(path)
|
|
@@ -17,48 +93,180 @@ class RayClientMixin(BaseClient):
|
|
|
17
93
|
path = f'jobs/{pk}/logs/'
|
|
18
94
|
return self._get(path)
|
|
19
95
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
96
|
+
def websocket_tail_job_logs(self, pk, stream_timeout=10):
|
|
97
|
+
"""Stream job logs in real-time using WebSocket protocol.
|
|
98
|
+
|
|
99
|
+
Establishes a WebSocket connection to stream job logs as they are generated.
|
|
100
|
+
This method provides the lowest latency for real-time log monitoring and is
|
|
101
|
+
the preferred protocol when available.
|
|
23
102
|
|
|
24
|
-
|
|
25
|
-
|
|
103
|
+
Args:
|
|
104
|
+
pk (str): Job primary key or identifier. Must be alphanumeric with
|
|
105
|
+
optional hyphens/underscores, max 100 characters.
|
|
106
|
+
stream_timeout (float, optional): Maximum time in seconds to wait for
|
|
107
|
+
log data. Defaults to 10. Must be positive
|
|
108
|
+
and cannot exceed 300 seconds.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Generator[str, None, None]: A generator yielding log lines as strings.
|
|
112
|
+
Each line includes a newline character.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ClientError:
|
|
116
|
+
- 400: If long polling is enabled (incompatible)
|
|
117
|
+
- 400: If pk is empty, contains invalid characters, or too long
|
|
118
|
+
- 400: If stream_timeout is not positive or exceeds maximum
|
|
119
|
+
- 500: If WebSocket library is unavailable
|
|
120
|
+
- 503: If connection to Ray cluster fails
|
|
121
|
+
- 408: If connection timeout occurs
|
|
122
|
+
- 429: If stream limits are exceeded (lines, size, messages)
|
|
123
|
+
|
|
124
|
+
Usage:
|
|
125
|
+
>>> # Basic log streaming
|
|
126
|
+
>>> for log_line in client.websocket_tail_job_logs('job-12345'):
|
|
127
|
+
... print(log_line.strip())
|
|
128
|
+
|
|
129
|
+
>>> # With custom timeout
|
|
130
|
+
>>> for log_line in client.websocket_tail_job_logs('job-12345', stream_timeout=30):
|
|
131
|
+
... if 'ERROR' in log_line:
|
|
132
|
+
... break
|
|
133
|
+
|
|
134
|
+
Technical Notes:
|
|
135
|
+
- Uses WebSocketStreamManager for connection management
|
|
136
|
+
- Automatic input validation and sanitization
|
|
137
|
+
- Resource cleanup handled by WeakSet tracking
|
|
138
|
+
- Stream limits prevent memory exhaustion
|
|
139
|
+
- Thread pool manages WebSocket operations
|
|
140
|
+
|
|
141
|
+
See Also:
|
|
142
|
+
stream_tail_job_logs: HTTP-based alternative
|
|
143
|
+
tail_job_logs: Protocol-agnostic wrapper method
|
|
144
|
+
"""
|
|
145
|
+
if hasattr(self, 'long_poll_handler') and self.long_poll_handler:
|
|
146
|
+
raise ClientError(400, '"websocket_tail_job_logs" does not support long polling')
|
|
147
|
+
|
|
148
|
+
# Validate inputs using network utilities
|
|
149
|
+
validated_pk = validate_resource_id(pk, 'job')
|
|
150
|
+
validated_timeout = validate_timeout(stream_timeout)
|
|
151
|
+
|
|
152
|
+
# Build WebSocket URL
|
|
153
|
+
path = f'ray/jobs/{validated_pk}/logs/ws/'
|
|
154
|
+
url = self._get_url(path, trailing_slash=True)
|
|
155
|
+
ws_url = http_to_websocket_url(url)
|
|
156
|
+
|
|
157
|
+
# Get headers and use WebSocket manager
|
|
26
158
|
headers = self._get_headers()
|
|
159
|
+
headers['Agent-Token'] = f'Token {self.agent_token}'
|
|
160
|
+
context = f'job {validated_pk}'
|
|
161
|
+
|
|
162
|
+
return self._websocket_manager.stream_logs(ws_url, headers, validated_timeout, context)
|
|
163
|
+
|
|
164
|
+
def stream_tail_job_logs(self, pk, stream_timeout=10):
|
|
165
|
+
"""Stream job logs in real-time using HTTP chunked transfer encoding.
|
|
166
|
+
|
|
167
|
+
Establishes an HTTP connection with chunked transfer encoding to stream
|
|
168
|
+
job logs as they are generated. This method serves as a reliable fallback
|
|
169
|
+
when WebSocket connections are not available or suitable.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
pk (str): Job primary key or identifier. Must be alphanumeric with
|
|
173
|
+
optional hyphens/underscores, max 100 characters.
|
|
174
|
+
stream_timeout (float, optional): Maximum time in seconds to wait for
|
|
175
|
+
log data. Defaults to 10. Must be positive
|
|
176
|
+
and cannot exceed 300 seconds.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Generator[str, None, None]: A generator yielding log lines as strings.
|
|
180
|
+
Each line includes a newline character.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ClientError:
|
|
184
|
+
- 400: If long polling is enabled (incompatible)
|
|
185
|
+
- 400: If pk is empty, contains invalid characters, or too long
|
|
186
|
+
- 400: If stream_timeout is not positive or exceeds maximum
|
|
187
|
+
- 503: If connection to Ray cluster fails
|
|
188
|
+
- 408: If connection or read timeout occurs
|
|
189
|
+
- 404: If job is not found
|
|
190
|
+
- 429: If stream limits are exceeded (lines, size, messages)
|
|
191
|
+
- 500: If unexpected streaming error occurs
|
|
192
|
+
|
|
193
|
+
Usage:
|
|
194
|
+
>>> # Basic HTTP log streaming
|
|
195
|
+
>>> for log_line in client.stream_tail_job_logs('job-12345'):
|
|
196
|
+
... print(log_line.strip())
|
|
197
|
+
|
|
198
|
+
>>> # With error handling and custom timeout
|
|
199
|
+
>>> try:
|
|
200
|
+
... for log_line in client.stream_tail_job_logs('job-12345', stream_timeout=60):
|
|
201
|
+
... if 'COMPLETED' in log_line:
|
|
202
|
+
... break
|
|
203
|
+
... except ClientError as e:
|
|
204
|
+
... print(f"Streaming failed: {e}")
|
|
205
|
+
|
|
206
|
+
Technical Notes:
|
|
207
|
+
- Uses HTTPStreamManager for connection management
|
|
208
|
+
- Automatic input validation and sanitization
|
|
209
|
+
- Proper HTTP response cleanup on completion/error
|
|
210
|
+
- Stream limits prevent memory exhaustion
|
|
211
|
+
- Filters out oversized lines (>10KB) automatically
|
|
212
|
+
- Connection reuse through requests session
|
|
213
|
+
|
|
214
|
+
See Also:
|
|
215
|
+
websocket_tail_job_logs: WebSocket-based alternative (preferred)
|
|
216
|
+
tail_job_logs: Protocol-agnostic wrapper method
|
|
217
|
+
"""
|
|
218
|
+
if hasattr(self, 'long_poll_handler') and self.long_poll_handler:
|
|
219
|
+
raise ClientError(400, '"stream_tail_job_logs" does not support long polling')
|
|
220
|
+
|
|
221
|
+
# Validate inputs using network utilities
|
|
222
|
+
validated_pk = validate_resource_id(pk, 'job')
|
|
223
|
+
validated_timeout = validate_timeout(stream_timeout)
|
|
224
|
+
|
|
225
|
+
# Build HTTP URL and prepare request
|
|
226
|
+
path = f'ray/jobs/{validated_pk}/logs/stream/'
|
|
227
|
+
url = self._get_url(path, trailing_slash=True)
|
|
228
|
+
headers = self._get_headers()
|
|
229
|
+
headers['Agent-Token'] = f'Token {self.agent_token}'
|
|
230
|
+
timeout = (self.timeout['connect'], validated_timeout)
|
|
231
|
+
context = f'job {validated_pk}'
|
|
232
|
+
|
|
233
|
+
return self._http_manager.stream_logs(url, headers, timeout, context)
|
|
234
|
+
|
|
235
|
+
def tail_job_logs(self, pk, stream_timeout=10, protocol='stream'):
|
|
236
|
+
"""Tail job logs using either WebSocket or HTTP streaming.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
pk: Job primary key
|
|
240
|
+
stream_timeout: Timeout for streaming operations
|
|
241
|
+
protocol: 'websocket' or 'stream' (default: 'stream')
|
|
242
|
+
"""
|
|
243
|
+
# Validate protocol first
|
|
244
|
+
if protocol not in ('websocket', 'stream'):
|
|
245
|
+
raise ClientError(400, f'Unsupported protocol: {protocol}. Use "websocket" or "stream"')
|
|
246
|
+
|
|
247
|
+
# Pre-validate common inputs using network utilities
|
|
248
|
+
validate_resource_id(pk, 'job')
|
|
249
|
+
validate_timeout(stream_timeout)
|
|
27
250
|
|
|
28
251
|
try:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# Set up streaming with timeout handling
|
|
36
|
-
try:
|
|
37
|
-
for line in response.iter_lines(decode_unicode=True, chunk_size=1024):
|
|
38
|
-
if line:
|
|
39
|
-
yield f'{line}\n'
|
|
40
|
-
except requests.exceptions.ChunkedEncodingError:
|
|
41
|
-
# Connection was interrupted during streaming
|
|
42
|
-
raise ClientError(503, f'Log stream for job {pk} was interrupted')
|
|
43
|
-
except requests.exceptions.ReadTimeout:
|
|
44
|
-
# Read timeout during streaming
|
|
45
|
-
raise ClientError(408, f'Log stream for job {pk} timed out after {stream_timeout}s')
|
|
46
|
-
|
|
47
|
-
except requests.exceptions.ConnectTimeout:
|
|
48
|
-
raise ClientError(
|
|
49
|
-
408, f'Failed to connect to log stream for job {pk} (timeout: {self.timeout["connect"]}s)'
|
|
50
|
-
)
|
|
51
|
-
except requests.exceptions.ReadTimeout:
|
|
52
|
-
raise ClientError(408, f'Log stream for job {pk} read timeout ({stream_timeout}s)')
|
|
53
|
-
except requests.exceptions.ConnectionError as e:
|
|
54
|
-
if 'Connection refused' in str(e):
|
|
55
|
-
raise ClientError(503, f'Agent connection refused for job {pk}')
|
|
56
|
-
else:
|
|
57
|
-
raise ClientError(503, f'Agent connection error for job {pk}: {str(e)[:100]}')
|
|
58
|
-
except requests.exceptions.HTTPError as e:
|
|
59
|
-
raise ClientError(e.response.status_code, f'HTTP error streaming logs for job {pk}: {e}')
|
|
252
|
+
if protocol == 'websocket':
|
|
253
|
+
return self.websocket_tail_job_logs(pk, stream_timeout)
|
|
254
|
+
else: # protocol == 'stream'
|
|
255
|
+
return self.stream_tail_job_logs(pk, stream_timeout)
|
|
256
|
+
except ClientError:
|
|
257
|
+
raise
|
|
60
258
|
except Exception as e:
|
|
61
|
-
|
|
259
|
+
# Fallback error handling using network utility
|
|
260
|
+
sanitized_error = sanitize_error_message(str(e), f'job {pk}')
|
|
261
|
+
raise ClientError(500, f'Protocol {protocol} failed: {sanitized_error}')
|
|
262
|
+
|
|
263
|
+
def __del__(self):
|
|
264
|
+
"""Cleanup resources when object is destroyed."""
|
|
265
|
+
try:
|
|
266
|
+
if hasattr(self, '_thread_pool'):
|
|
267
|
+
self._thread_pool.shutdown(wait=False)
|
|
268
|
+
except Exception:
|
|
269
|
+
pass # Ignore cleanup errors during destruction
|
|
62
270
|
|
|
63
271
|
def get_node(self, pk):
|
|
64
272
|
path = f'nodes/{pk}/'
|
|
@@ -87,3 +295,53 @@ class RayClientMixin(BaseClient):
|
|
|
87
295
|
def delete_serve_application(self, pk):
|
|
88
296
|
path = f'serve_applications/{pk}/'
|
|
89
297
|
return self._delete(path)
|
|
298
|
+
|
|
299
|
+
def stop_job(self, pk):
|
|
300
|
+
"""Stop a running job gracefully.
|
|
301
|
+
|
|
302
|
+
Uses Ray's stop_job() API to request graceful termination of the job.
|
|
303
|
+
This preserves job state and allows for potential resubmission later.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
pk (str): Job primary key or identifier. Must be alphanumeric with
|
|
307
|
+
optional hyphens/underscores, max 100 characters.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
dict: Response containing job status and stop details.
|
|
311
|
+
|
|
312
|
+
Raises:
|
|
313
|
+
ClientError:
|
|
314
|
+
- 400: If pk is empty, contains invalid characters, or too long
|
|
315
|
+
- 400: If job is already in terminal state (STOPPED, FAILED, etc.)
|
|
316
|
+
- 404: If job is not found
|
|
317
|
+
- 503: If connection to Ray cluster fails
|
|
318
|
+
- 500: If unexpected error occurs during stop
|
|
319
|
+
|
|
320
|
+
Usage:
|
|
321
|
+
>>> # Stop a running job
|
|
322
|
+
>>> result = client.stop_job('job-12345')
|
|
323
|
+
>>> print(result['status']) # Should show 'STOPPING' or similar
|
|
324
|
+
|
|
325
|
+
>>> # Handle stop errors
|
|
326
|
+
>>> try:
|
|
327
|
+
... client.stop_job('job-12345')
|
|
328
|
+
... except ClientError as e:
|
|
329
|
+
... print(f"Stop failed: {e}")
|
|
330
|
+
|
|
331
|
+
Technical Notes:
|
|
332
|
+
- Uses Ray's stop_job() API for graceful termination
|
|
333
|
+
- Validates job state before attempting stop
|
|
334
|
+
- Maintains consistency with existing SDK patterns
|
|
335
|
+
- Provides detailed error messages for debugging
|
|
336
|
+
|
|
337
|
+
See Also:
|
|
338
|
+
resume_job: Method for restarting stopped jobs
|
|
339
|
+
"""
|
|
340
|
+
# Validate inputs using network utilities
|
|
341
|
+
validated_pk = validate_resource_id(pk, 'job')
|
|
342
|
+
|
|
343
|
+
# Build API path for job stop
|
|
344
|
+
path = f'jobs/{validated_pk}/stop/'
|
|
345
|
+
|
|
346
|
+
# Use _post method with empty data to match Ray's API pattern
|
|
347
|
+
return self._post(path)
|
|
@@ -24,7 +24,7 @@ class AnnotationClientMixin(BaseClient):
|
|
|
24
24
|
return self._list(path, params=params)
|
|
25
25
|
|
|
26
26
|
def list_tasks(self, params=None, url_conversion=None, list_all=False):
|
|
27
|
-
path = 'tasks/'
|
|
27
|
+
path = 'sdk/tasks/'
|
|
28
28
|
url_conversion = get_default_url_conversion(url_conversion, files_fields=['files'])
|
|
29
29
|
return self._list(path, params=params, url_conversion=url_conversion, list_all=list_all)
|
|
30
30
|
|
|
@@ -3,15 +3,42 @@ import os
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from synapse_sdk.clients.base import BaseClient
|
|
6
|
+
from synapse_sdk.utils.file import read_file_in_chunks
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class CoreClientMixin(BaseClient):
|
|
9
10
|
def create_chunked_upload(self, file_path):
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
while chunk := file.read(chunk_size):
|
|
13
|
-
yield chunk
|
|
11
|
+
"""
|
|
12
|
+
Upload a file using chunked upload for efficient handling of large files.
|
|
14
13
|
|
|
14
|
+
This method breaks the file into chunks and uploads them sequentially to the server.
|
|
15
|
+
It calculates an MD5 hash of the entire file to ensure data integrity during upload.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
file_path (str | Path): Path to the file to upload
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
dict: Response from the server after successful upload completion,
|
|
22
|
+
typically containing upload confirmation and file metadata
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
FileNotFoundError: If the specified file doesn't exist
|
|
26
|
+
PermissionError: If the file can't be read due to permissions
|
|
27
|
+
ClientError: If there's an error during the upload process
|
|
28
|
+
OSError: If there's an OS-level error accessing the file
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
```python
|
|
32
|
+
client = CoreClientMixin(base_url='https://api.example.com')
|
|
33
|
+
result = client.create_chunked_upload('/path/to/large_file.zip')
|
|
34
|
+
print(f"Upload completed: {result}")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Note:
|
|
38
|
+
- Uses 50MB chunks by default for optimal upload performance
|
|
39
|
+
- Automatically resumes from the last successfully uploaded chunk
|
|
40
|
+
- Verifies upload integrity using MD5 checksum
|
|
41
|
+
"""
|
|
15
42
|
file_path = Path(file_path)
|
|
16
43
|
size = os.path.getsize(file_path)
|
|
17
44
|
hash_md5 = hashlib.md5()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from multiprocessing import Pool
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Dict, Optional
|
|
3
|
+
from typing import Dict, Optional, Union
|
|
4
4
|
|
|
5
5
|
from tqdm import tqdm
|
|
6
6
|
|
|
@@ -9,6 +9,17 @@ from synapse_sdk.clients.utils import get_batched_list, get_default_url_conversi
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class DataCollectionClientMixin(BaseClient):
|
|
12
|
+
"""Mixin class for data collection operations.
|
|
13
|
+
|
|
14
|
+
Provides methods for managing data collections, files, and units
|
|
15
|
+
in the Synapse backend. Supports both regular file uploads and
|
|
16
|
+
chunked uploads for large files.
|
|
17
|
+
|
|
18
|
+
This mixin extends BaseClient with data collection-specific functionality
|
|
19
|
+
including file upload capabilities, data unit management, and batch processing
|
|
20
|
+
operations for efficient data collection workflows.
|
|
21
|
+
"""
|
|
22
|
+
|
|
12
23
|
def list_data_collection(self):
|
|
13
24
|
path = 'data_collections/'
|
|
14
25
|
return self._list(path)
|
|
@@ -22,14 +33,66 @@ class DataCollectionClientMixin(BaseClient):
|
|
|
22
33
|
path = f'data_collections/{data_collection_id}/?expand=file_specifications'
|
|
23
34
|
return self._get(path)
|
|
24
35
|
|
|
25
|
-
def create_data_file(
|
|
26
|
-
|
|
36
|
+
def create_data_file(
|
|
37
|
+
self, file_path: Path, use_chunked_upload: bool = False
|
|
38
|
+
) -> Union[Dict[str, Union[str, int]], str]:
|
|
39
|
+
"""Create and upload a data file to the Synapse backend.
|
|
40
|
+
|
|
41
|
+
This method supports two upload strategies:
|
|
42
|
+
1. Direct file upload for smaller files (default)
|
|
43
|
+
2. Chunked upload for large files (automatic when flag is enabled)
|
|
27
44
|
|
|
28
45
|
Args:
|
|
29
|
-
file_path:
|
|
46
|
+
file_path: Path object pointing to the file to upload.
|
|
47
|
+
Must be a valid file path that exists on the filesystem.
|
|
48
|
+
use_chunked_upload: Boolean flag to enable chunked upload for the file.
|
|
49
|
+
When True, automatically creates a chunked upload for the file
|
|
50
|
+
instead of uploading it directly. Defaults to False.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Dictionary containing the created data file information including:
|
|
54
|
+
- id: The unique identifier of the created data file
|
|
55
|
+
- checksum: The MD5 checksum of the uploaded file
|
|
56
|
+
- size: The file size in bytes
|
|
57
|
+
- created_at: Timestamp of creation
|
|
58
|
+
- Additional metadata fields from the backend
|
|
59
|
+
Or a string response in case of non-JSON response.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
FileNotFoundError: If the specified file doesn't exist (for direct upload)
|
|
63
|
+
PermissionError: If the file can't be read due to permissions
|
|
64
|
+
ClientError: If the backend returns an error response
|
|
65
|
+
ValueError: If file_path is not a valid Path object
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
Direct file upload for small files:
|
|
69
|
+
```python
|
|
70
|
+
client = DataCollectionClientMixin(base_url='https://api.example.com')
|
|
71
|
+
file_path = Path('/path/to/small_file.csv')
|
|
72
|
+
result = client.create_data_file(file_path)
|
|
73
|
+
print(f"File uploaded with ID: {result['id']}")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Using chunked upload for large files:
|
|
77
|
+
```python
|
|
78
|
+
# Automatically create chunked upload for large file
|
|
79
|
+
file_path = Path('/path/to/large_file.zip')
|
|
80
|
+
result = client.create_data_file(file_path, use_chunked_upload=True)
|
|
81
|
+
print(f"Large file uploaded with ID: {result['id']}")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Note:
|
|
85
|
+
- For files larger than 100MB, consider using chunked upload
|
|
86
|
+
- The chunked upload will be automatically created when the flag is enabled
|
|
87
|
+
- Chunked uploads provide better reliability for large files
|
|
30
88
|
"""
|
|
31
89
|
path = 'data_files/'
|
|
32
|
-
|
|
90
|
+
if use_chunked_upload:
|
|
91
|
+
chunked_upload = self.create_chunked_upload(file_path)
|
|
92
|
+
data = {'chunked_upload': chunked_upload['id'], 'meta': {'filename': file_path.name}}
|
|
93
|
+
return self._post(path, data=data)
|
|
94
|
+
else:
|
|
95
|
+
return self._post(path, files={'file': file_path})
|
|
33
96
|
|
|
34
97
|
def get_data_unit(self, data_unit_id: int, params=None):
|
|
35
98
|
path = f'data_units/{data_unit_id}/'
|
|
@@ -49,6 +112,16 @@ class DataCollectionClientMixin(BaseClient):
|
|
|
49
112
|
url_conversion = get_default_url_conversion(url_conversion, files_fields=['files'])
|
|
50
113
|
return self._list(path, params=params, url_conversion=url_conversion, list_all=list_all)
|
|
51
114
|
|
|
115
|
+
def data_files_verify_checksums(self, checksums: list[str]):
|
|
116
|
+
"""Check checksums from files are exists.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
checksums: A list of MD5 checksums to verify.
|
|
120
|
+
"""
|
|
121
|
+
path = 'data_files/verify_checksums/'
|
|
122
|
+
data = {'checksums': checksums}
|
|
123
|
+
return self._post(path, data=data)
|
|
124
|
+
|
|
52
125
|
def upload_data_collection(
|
|
53
126
|
self,
|
|
54
127
|
data_collection_id: int,
|
|
@@ -91,7 +164,7 @@ class DataCollectionClientMixin(BaseClient):
|
|
|
91
164
|
|
|
92
165
|
self.create_tasks(tasks_data)
|
|
93
166
|
|
|
94
|
-
def upload_data_file(self, data: Dict, data_collection_id: int) -> Dict:
|
|
167
|
+
def upload_data_file(self, data: Dict, data_collection_id: int, use_chunked_upload: bool = False) -> Dict:
|
|
95
168
|
"""Upload files to synapse-backend.
|
|
96
169
|
|
|
97
170
|
Args:
|
|
@@ -100,12 +173,14 @@ class DataCollectionClientMixin(BaseClient):
|
|
|
100
173
|
- files: The files to upload. (key: file name, value: file pathlib object)
|
|
101
174
|
- meta: The meta data to upload.
|
|
102
175
|
data_collection_id: The data_collection id to upload the data to.
|
|
176
|
+
use_chunked_upload: Whether to use chunked upload for large files.(default False)
|
|
177
|
+
Automatically determined based on file size threshold in upload plugin (default 50MB).
|
|
103
178
|
|
|
104
179
|
Returns:
|
|
105
180
|
Dict: The result of the upload.
|
|
106
181
|
"""
|
|
107
182
|
for name, path in data['files'].items():
|
|
108
|
-
data_file = self.create_data_file(path)
|
|
183
|
+
data_file = self.create_data_file(path, use_chunked_upload)
|
|
109
184
|
data['data_collection'] = data_collection_id
|
|
110
185
|
data['files'][name] = {'checksum': data_file['checksum'], 'path': str(path)}
|
|
111
186
|
return data
|
|
@@ -8,7 +8,7 @@ class HITLClientMixin(BaseClient):
|
|
|
8
8
|
return self._get(path)
|
|
9
9
|
|
|
10
10
|
def list_assignments(self, params=None, url_conversion=None, list_all=False):
|
|
11
|
-
path = 'assignments/'
|
|
11
|
+
path = 'sdk/assignments/'
|
|
12
12
|
url_conversion = get_default_url_conversion(url_conversion, files_fields=['files'])
|
|
13
13
|
return self._list(path, params=params, url_conversion=url_conversion, list_all=list_all)
|
|
14
14
|
|
|
@@ -19,7 +19,7 @@ class MLClientMixin(BaseClient):
|
|
|
19
19
|
return self._post(path, data=data)
|
|
20
20
|
|
|
21
21
|
def list_ground_truth_events(self, params=None, url_conversion=None, list_all=False):
|
|
22
|
-
path = 'ground_truth_events/'
|
|
22
|
+
path = 'sdk/ground_truth_events/'
|
|
23
23
|
url_conversion = get_default_url_conversion(url_conversion, files_fields=['files'])
|
|
24
24
|
return self._list(path, params=params, url_conversion=url_conversion, list_all=list_all)
|
|
25
25
|
|