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.
Files changed (167) hide show
  1. synapse_sdk/__init__.py +24 -0
  2. synapse_sdk/cli/code_server.py +305 -33
  3. synapse_sdk/clients/agent/__init__.py +2 -1
  4. synapse_sdk/clients/agent/container.py +143 -0
  5. synapse_sdk/clients/agent/ray.py +296 -38
  6. synapse_sdk/clients/backend/annotation.py +1 -1
  7. synapse_sdk/clients/backend/core.py +31 -4
  8. synapse_sdk/clients/backend/data_collection.py +82 -7
  9. synapse_sdk/clients/backend/hitl.py +1 -1
  10. synapse_sdk/clients/backend/ml.py +1 -1
  11. synapse_sdk/clients/base.py +211 -61
  12. synapse_sdk/loggers.py +46 -0
  13. synapse_sdk/plugins/README.md +1340 -0
  14. synapse_sdk/plugins/categories/base.py +59 -9
  15. synapse_sdk/plugins/categories/export/actions/__init__.py +3 -0
  16. synapse_sdk/plugins/categories/export/actions/export/__init__.py +28 -0
  17. synapse_sdk/plugins/categories/export/actions/export/action.py +165 -0
  18. synapse_sdk/plugins/categories/export/actions/export/enums.py +113 -0
  19. synapse_sdk/plugins/categories/export/actions/export/exceptions.py +53 -0
  20. synapse_sdk/plugins/categories/export/actions/export/models.py +74 -0
  21. synapse_sdk/plugins/categories/export/actions/export/run.py +195 -0
  22. synapse_sdk/plugins/categories/export/actions/export/utils.py +187 -0
  23. synapse_sdk/plugins/categories/export/templates/config.yaml +19 -1
  24. synapse_sdk/plugins/categories/export/templates/plugin/__init__.py +390 -0
  25. synapse_sdk/plugins/categories/export/templates/plugin/export.py +153 -177
  26. synapse_sdk/plugins/categories/neural_net/actions/train.py +1130 -32
  27. synapse_sdk/plugins/categories/neural_net/actions/tune.py +157 -4
  28. synapse_sdk/plugins/categories/neural_net/templates/config.yaml +7 -4
  29. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  30. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  31. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/action.py +10 -0
  32. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  33. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +148 -0
  34. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  35. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  36. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  37. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +100 -0
  38. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +248 -0
  39. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  40. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  41. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +265 -0
  42. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  43. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  44. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +92 -0
  45. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +243 -0
  46. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  47. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +19 -0
  48. synapse_sdk/plugins/categories/upload/actions/upload/action.py +236 -0
  49. synapse_sdk/plugins/categories/upload/actions/upload/context.py +185 -0
  50. synapse_sdk/plugins/categories/upload/actions/upload/enums.py +493 -0
  51. synapse_sdk/plugins/categories/upload/actions/upload/exceptions.py +36 -0
  52. synapse_sdk/plugins/categories/upload/actions/upload/factory.py +138 -0
  53. synapse_sdk/plugins/categories/upload/actions/upload/models.py +214 -0
  54. synapse_sdk/plugins/categories/upload/actions/upload/orchestrator.py +183 -0
  55. synapse_sdk/plugins/categories/upload/actions/upload/registry.py +113 -0
  56. synapse_sdk/plugins/categories/upload/actions/upload/run.py +179 -0
  57. synapse_sdk/plugins/categories/upload/actions/upload/steps/__init__.py +1 -0
  58. synapse_sdk/plugins/categories/upload/actions/upload/steps/base.py +107 -0
  59. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +62 -0
  60. synapse_sdk/plugins/categories/upload/actions/upload/steps/collection.py +63 -0
  61. synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +91 -0
  62. synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +82 -0
  63. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +235 -0
  64. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +201 -0
  65. synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +104 -0
  66. synapse_sdk/plugins/categories/upload/actions/upload/steps/validate.py +71 -0
  67. synapse_sdk/plugins/categories/upload/actions/upload/strategies/__init__.py +1 -0
  68. synapse_sdk/plugins/categories/upload/actions/upload/strategies/base.py +82 -0
  69. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/__init__.py +1 -0
  70. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/batch.py +39 -0
  71. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/single.py +29 -0
  72. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/__init__.py +1 -0
  73. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +300 -0
  74. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +287 -0
  75. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/__init__.py +1 -0
  76. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/excel.py +174 -0
  77. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/none.py +16 -0
  78. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/__init__.py +1 -0
  79. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/sync.py +84 -0
  80. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/__init__.py +1 -0
  81. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +60 -0
  82. synapse_sdk/plugins/categories/upload/actions/upload/utils.py +250 -0
  83. synapse_sdk/plugins/categories/upload/templates/README.md +470 -0
  84. synapse_sdk/plugins/categories/upload/templates/config.yaml +28 -2
  85. synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py +310 -0
  86. synapse_sdk/plugins/categories/upload/templates/plugin/upload.py +82 -20
  87. synapse_sdk/plugins/models.py +111 -9
  88. synapse_sdk/plugins/templates/plugin-config-schema.json +7 -0
  89. synapse_sdk/plugins/templates/schema.json +7 -0
  90. synapse_sdk/plugins/utils/__init__.py +3 -0
  91. synapse_sdk/plugins/utils/ray_gcs.py +66 -0
  92. synapse_sdk/shared/__init__.py +25 -0
  93. synapse_sdk/utils/converters/dm/__init__.py +42 -41
  94. synapse_sdk/utils/converters/dm/base.py +137 -0
  95. synapse_sdk/utils/converters/dm/from_v1.py +208 -562
  96. synapse_sdk/utils/converters/dm/to_v1.py +258 -304
  97. synapse_sdk/utils/converters/dm/tools/__init__.py +214 -0
  98. synapse_sdk/utils/converters/dm/tools/answer.py +95 -0
  99. synapse_sdk/utils/converters/dm/tools/bounding_box.py +132 -0
  100. synapse_sdk/utils/converters/dm/tools/bounding_box_3d.py +121 -0
  101. synapse_sdk/utils/converters/dm/tools/classification.py +75 -0
  102. synapse_sdk/utils/converters/dm/tools/keypoint.py +117 -0
  103. synapse_sdk/utils/converters/dm/tools/named_entity.py +111 -0
  104. synapse_sdk/utils/converters/dm/tools/polygon.py +122 -0
  105. synapse_sdk/utils/converters/dm/tools/polyline.py +124 -0
  106. synapse_sdk/utils/converters/dm/tools/prompt.py +94 -0
  107. synapse_sdk/utils/converters/dm/tools/relation.py +86 -0
  108. synapse_sdk/utils/converters/dm/tools/segmentation.py +141 -0
  109. synapse_sdk/utils/converters/dm/tools/segmentation_3d.py +83 -0
  110. synapse_sdk/utils/converters/dm/types.py +168 -0
  111. synapse_sdk/utils/converters/dm/utils.py +162 -0
  112. synapse_sdk/utils/converters/dm_legacy/__init__.py +56 -0
  113. synapse_sdk/utils/converters/dm_legacy/from_v1.py +627 -0
  114. synapse_sdk/utils/converters/dm_legacy/to_v1.py +367 -0
  115. synapse_sdk/utils/file/__init__.py +58 -0
  116. synapse_sdk/utils/file/archive.py +32 -0
  117. synapse_sdk/utils/file/checksum.py +56 -0
  118. synapse_sdk/utils/file/chunking.py +31 -0
  119. synapse_sdk/utils/file/download.py +385 -0
  120. synapse_sdk/utils/file/encoding.py +40 -0
  121. synapse_sdk/utils/file/io.py +22 -0
  122. synapse_sdk/utils/file/upload.py +165 -0
  123. synapse_sdk/utils/file/video/__init__.py +29 -0
  124. synapse_sdk/utils/file/video/transcode.py +307 -0
  125. synapse_sdk/utils/{file.py → file.py.backup} +77 -0
  126. synapse_sdk/utils/network.py +272 -0
  127. synapse_sdk/utils/storage/__init__.py +6 -2
  128. synapse_sdk/utils/storage/providers/file_system.py +6 -0
  129. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/METADATA +19 -2
  130. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/RECORD +134 -74
  131. synapse_sdk/devtools/docs/.gitignore +0 -20
  132. synapse_sdk/devtools/docs/README.md +0 -41
  133. synapse_sdk/devtools/docs/blog/2019-05-28-first-blog-post.md +0 -12
  134. synapse_sdk/devtools/docs/blog/2019-05-29-long-blog-post.md +0 -44
  135. synapse_sdk/devtools/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -24
  136. synapse_sdk/devtools/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  137. synapse_sdk/devtools/docs/blog/2021-08-26-welcome/index.md +0 -29
  138. synapse_sdk/devtools/docs/blog/authors.yml +0 -25
  139. synapse_sdk/devtools/docs/blog/tags.yml +0 -19
  140. synapse_sdk/devtools/docs/docusaurus.config.ts +0 -138
  141. synapse_sdk/devtools/docs/package-lock.json +0 -17455
  142. synapse_sdk/devtools/docs/package.json +0 -47
  143. synapse_sdk/devtools/docs/sidebars.ts +0 -44
  144. synapse_sdk/devtools/docs/src/components/HomepageFeatures/index.tsx +0 -71
  145. synapse_sdk/devtools/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  146. synapse_sdk/devtools/docs/src/css/custom.css +0 -30
  147. synapse_sdk/devtools/docs/src/pages/index.module.css +0 -23
  148. synapse_sdk/devtools/docs/src/pages/index.tsx +0 -21
  149. synapse_sdk/devtools/docs/src/pages/markdown-page.md +0 -7
  150. synapse_sdk/devtools/docs/static/.nojekyll +0 -0
  151. synapse_sdk/devtools/docs/static/img/docusaurus-social-card.jpg +0 -0
  152. synapse_sdk/devtools/docs/static/img/docusaurus.png +0 -0
  153. synapse_sdk/devtools/docs/static/img/favicon.ico +0 -0
  154. synapse_sdk/devtools/docs/static/img/logo.png +0 -0
  155. synapse_sdk/devtools/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  156. synapse_sdk/devtools/docs/static/img/undraw_docusaurus_react.svg +0 -170
  157. synapse_sdk/devtools/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  158. synapse_sdk/devtools/docs/tsconfig.json +0 -8
  159. synapse_sdk/plugins/categories/export/actions/export.py +0 -346
  160. synapse_sdk/plugins/categories/export/enums.py +0 -7
  161. synapse_sdk/plugins/categories/neural_net/actions/gradio.py +0 -151
  162. synapse_sdk/plugins/categories/pre_annotation/actions/to_task.py +0 -943
  163. synapse_sdk/plugins/categories/upload/actions/upload.py +0 -954
  164. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/WHEEL +0 -0
  165. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/entry_points.txt +0 -0
  166. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/licenses/LICENSE +0 -0
  167. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,86 @@
1
- import requests
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 tail_job_logs(self, pk, stream_timeout=10):
21
- if self.long_poll_handler:
22
- raise ClientError(400, '"tail_job_logs" does not support long polling')
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
- path = f'jobs/{pk}/tail_logs/'
25
- url = self._get_url(path)
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
- # Use shorter timeout for streaming to prevent hanging
30
- response = self.requests_session.get(
31
- url, headers=headers, stream=True, timeout=(self.timeout['connect'], stream_timeout)
32
- )
33
- response.raise_for_status()
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
- raise ClientError(500, f'Unexpected error streaming logs for job {pk}: {str(e)[:100]}')
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
- def read_file_in_chunks(file_path, chunk_size=1024 * 1024 * 50):
11
- with open(file_path, 'rb') as file:
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(self, file_path: Path):
26
- """Create data file to synapse-backend.
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: The file pathlib object to upload.
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
- return self._post(path, files={'file': file_path})
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