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,5 +1,4 @@
1
1
  import json
2
- from pathlib import Path
3
2
 
4
3
  import requests
5
4
  from requests.adapters import HTTPAdapter
@@ -7,6 +6,12 @@ from urllib3.util.retry import Retry
7
6
 
8
7
  from synapse_sdk.clients.exceptions import ClientError
9
8
  from synapse_sdk.utils.file import files_url_to_path_from_objs
9
+ from synapse_sdk.utils.file.upload import (
10
+ FileProcessingError,
11
+ FileValidationError,
12
+ close_file_handles,
13
+ process_files_for_upload,
14
+ )
10
15
 
11
16
 
12
17
  class BaseClient:
@@ -22,27 +27,58 @@ class BaseClient:
22
27
  'read': 15, # Read timeout: 15 seconds
23
28
  }
24
29
 
25
- # Create session with retry strategy
26
- requests_session = requests.Session()
30
+ # Session is created on first use
31
+ self._session = None
32
+
33
+ # Store retry configuration for creating sessions
34
+ self._retry_config = {
35
+ 'total': 3, # Total retries
36
+ 'backoff_factor': 1, # Backoff factor between retries
37
+ 'status_forcelist': [502, 503, 504], # HTTP status codes to retry
38
+ 'allowed_methods': ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
39
+ }
40
+
41
+ def _create_session(self):
42
+ """Create a new requests session with retry strategy."""
43
+ session = requests.Session()
27
44
 
28
45
  # Configure retry strategy for transient failures
29
- retry_strategy = Retry(
30
- total=3, # Total retries
31
- backoff_factor=1, # Backoff factor between retries
32
- status_forcelist=[502, 503, 504], # HTTP status codes to retry
33
- allowed_methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
34
- )
46
+ retry_strategy = Retry(**self._retry_config)
35
47
 
36
48
  adapter = HTTPAdapter(max_retries=retry_strategy)
37
- requests_session.mount('http://', adapter)
38
- requests_session.mount('https://', adapter)
49
+ session.mount('http://', adapter)
50
+ session.mount('https://', adapter)
39
51
 
40
- self.requests_session = requests_session
52
+ return session
53
+
54
+ @property
55
+ def requests_session(self):
56
+ """Get the requests session.
57
+
58
+ Returns a session instance, creating one if it doesn't exist.
59
+ """
60
+ if self._session is None:
61
+ self._session = self._create_session()
62
+ return self._session
41
63
 
42
- def _get_url(self, path):
43
- if not path.startswith('http'):
44
- return f'{self.base_url}/{path.lstrip("/")}'
45
- return path
64
+ def _get_url(self, path, trailing_slash=False):
65
+ """Construct a full URL from a path.
66
+
67
+ Args:
68
+ path (str): URL path or full URL
69
+ trailing_slash (bool): Whether to ensure URL ends with trailing slash
70
+
71
+ Returns:
72
+ str: Complete URL
73
+ """
74
+ # Use the path as-is if it's already a full URL, otherwise construct from base_url and path
75
+ url = path if path.startswith(('http://', 'https://')) else f'{self.base_url}/{path.lstrip("/")}'
76
+
77
+ # Add trailing slash if requested and not present
78
+ if trailing_slash and not url.endswith('/'):
79
+ url += '/'
80
+
81
+ return url
46
82
 
47
83
  def _get_headers(self):
48
84
  return {}
@@ -69,33 +105,35 @@ class BaseClient:
69
105
  # List to store opened files to close after request
70
106
  opened_files = []
71
107
 
72
- if method in ['post', 'put', 'patch']:
73
- # If files are included in the request, open them as binary files
74
- if kwargs.get('files') is not None:
75
- for name, file in kwargs['files'].items():
76
- # Handle both string and Path object cases
77
- if isinstance(file, str):
78
- file = Path(file)
79
- if isinstance(file, Path):
80
- opened_file = file.open(mode='rb')
81
- kwargs['files'][name] = (file.name, opened_file)
82
- opened_files.append(opened_file)
83
- if 'data' in kwargs:
84
- for name, value in kwargs['data'].items():
85
- if isinstance(value, dict):
86
- kwargs['data'][name] = json.dumps(value)
87
- else:
88
- headers['Content-Type'] = 'application/json'
89
- if 'data' in kwargs:
90
- kwargs['data'] = json.dumps(kwargs['data'])
91
-
92
108
  try:
109
+ if method in ['post', 'put', 'patch']:
110
+ # Process files if present using the utility function
111
+ # TODO: File handling logic using 'files' key is naive. Need to establish and document
112
+ # a clear convention for including file information in request bodies across Synapse SDK.
113
+ if kwargs.get('files') is not None:
114
+ kwargs['files'], opened_files = process_files_for_upload(kwargs['files'])
115
+
116
+ # Handle data serialization when files are present
117
+ if 'data' in kwargs:
118
+ for name, value in kwargs['data'].items():
119
+ if isinstance(value, dict):
120
+ kwargs['data'][name] = json.dumps(value)
121
+ else:
122
+ # No files - use JSON content type
123
+ headers['Content-Type'] = 'application/json'
124
+ if 'data' in kwargs:
125
+ kwargs['data'] = json.dumps(kwargs['data'])
126
+
93
127
  # Send request
94
128
  response = getattr(self.requests_session, method)(url, headers=headers, **kwargs)
95
129
  if not response.ok:
96
130
  raise ClientError(
97
131
  response.status_code, response.json() if response.status_code == 400 else response.reason
98
132
  )
133
+
134
+ except (FileValidationError, FileProcessingError) as e:
135
+ # Catch file validation and processing errors from the utility
136
+ raise ClientError(400, str(e)) from e
99
137
  except requests.exceptions.ConnectTimeout:
100
138
  raise ClientError(408, f'{self.name} connection timeout (>{self.timeout["connect"]}s)')
101
139
  except requests.exceptions.ReadTimeout:
@@ -111,10 +149,9 @@ class BaseClient:
111
149
  except requests.exceptions.RequestException as e:
112
150
  # Catch all other requests exceptions
113
151
  raise ClientError(500, f'{self.name} request failed: {str(e)[:100]}')
114
-
115
- # Close all opened files
116
- for opened_file in opened_files:
117
- opened_file.close()
152
+ finally:
153
+ # Always close opened files, even if an exception occurred
154
+ close_file_handles(opened_files)
118
155
 
119
156
  return self._post_response(response)
120
157
 
@@ -125,8 +162,7 @@ class BaseClient:
125
162
  return response.text
126
163
 
127
164
  def _get(self, path, url_conversion=None, response_model=None, **kwargs):
128
- """
129
- Perform a GET request and optionally convert response to a pydantic model.
165
+ """Perform a GET request and optionally convert response to a pydantic model.
130
166
 
131
167
  Args:
132
168
  path (str): URL path to request.
@@ -152,8 +188,7 @@ class BaseClient:
152
188
  return response
153
189
 
154
190
  def _post(self, path, request_model=None, response_model=None, **kwargs):
155
- """
156
- Perform a POST request and optionally convert response to a pydantic model.
191
+ """Perform a POST request and optionally convert response to a pydantic model.
157
192
 
158
193
  Args:
159
194
  path (str): URL path to request.
@@ -173,8 +208,7 @@ class BaseClient:
173
208
  return response
174
209
 
175
210
  def _put(self, path, request_model=None, response_model=None, **kwargs):
176
- """
177
- Perform a PUT request to the specified path.
211
+ """Perform a PUT request to the specified path.
178
212
 
179
213
  Args:
180
214
  path (str): The URL path for the request.
@@ -196,8 +230,7 @@ class BaseClient:
196
230
  return response
197
231
 
198
232
  def _patch(self, path, request_model=None, response_model=None, **kwargs):
199
- """
200
- Perform a PATCH HTTP request to the specified path.
233
+ """Perform a PATCH HTTP request to the specified path.
201
234
 
202
235
  Args:
203
236
  path (str): The API endpoint path to make the request to.
@@ -210,7 +243,6 @@ class BaseClient:
210
243
  Union[dict, BaseModel]: If response_model is provided, returns an instance of that model.
211
244
  Otherwise, returns the raw response data.
212
245
  """
213
-
214
246
  if kwargs.get('data') and request_model:
215
247
  kwargs['data'] = self._validate_request_body_with_pydantic_model(kwargs['data'], request_model)
216
248
  response = self._request('patch', path, **kwargs)
@@ -220,8 +252,7 @@ class BaseClient:
220
252
  return response
221
253
 
222
254
  def _delete(self, path, request_model=None, response_model=None, **kwargs):
223
- """
224
- Performs a DELETE request to the specified path.
255
+ """Performs a DELETE request to the specified path.
225
256
 
226
257
  Args:
227
258
  path (str): The API endpoint path to send the DELETE request to.
@@ -234,7 +265,6 @@ class BaseClient:
234
265
  Union[dict, BaseModel]: If response_model is provided, returns an instance of that model.
235
266
  Otherwise, returns the raw response data as a dictionary.
236
267
  """
237
-
238
268
  if kwargs.get('data') and request_model:
239
269
  kwargs['data'] = self._validate_request_body_with_pydantic_model(kwargs['data'], request_model)
240
270
  response = self._request('delete', path, **kwargs)
@@ -243,19 +273,139 @@ class BaseClient:
243
273
  else:
244
274
  return response
245
275
 
246
- def _list(self, path, url_conversion=None, list_all=False, **kwargs):
247
- response = self._get(path, **kwargs)
276
+ def _list(self, path, url_conversion=None, list_all=False, params=None, **kwargs):
277
+ """List resources from a paginated API endpoint.
278
+
279
+ Args:
280
+ path (str): URL path to request.
281
+ url_conversion (dict, optional): Configuration for URL to path conversion.
282
+ Used to convert file URLs to local paths in the response.
283
+ Example: {'files_fields': ['files'], 'is_list': True}
284
+ This will convert file URLs in the 'files' field of each result.
285
+ list_all (bool): If True, returns a generator yielding all results across all pages.
286
+ Default is False, which returns only the first page.
287
+ params (dict, optional): Query parameters to pass to the request.
288
+ Example: {'status': 'active', 'project': 123}
289
+ **kwargs: Additional keyword arguments to pass to the request.
290
+
291
+ Returns:
292
+ If list_all is False: dict response from the API containing:
293
+ - 'results': list of items on the current page
294
+ - 'count': total number of items
295
+ - 'next': URL to the next page (or None)
296
+ - 'previous': URL to the previous page (or None)
297
+ If list_all is True: tuple of (generator, count) where:
298
+ - generator: yields individual items from all pages
299
+ - count: total number of items across all pages
300
+
301
+ Examples:
302
+ Get first page only:
303
+ >>> response = client._list('api/tasks/')
304
+ >>> tasks = response['results'] # List of tasks on first page
305
+ >>> total_count = response['count'] # Total number of tasks
306
+
307
+ Get all results across all pages:
308
+ >>> generator, count = client._list('api/tasks/', list_all=True)
309
+ >>> all_tasks = list(generator) # Fetches all pages
310
+
311
+ With filters and url_conversion:
312
+ >>> url_conversion = {'files_fields': ['files'], 'is_list': True}
313
+ >>> params = {'status': 'active'}
314
+ >>> generator, count = client._list(
315
+ ... 'api/data_units/',
316
+ ... url_conversion=url_conversion,
317
+ ... list_all=True,
318
+ ... params=params
319
+ ... )
320
+ >>> active_units = list(generator) # All active units with file URLs converted
321
+ """
322
+ if params is None:
323
+ params = {}
324
+
248
325
  if list_all:
249
- return self._list_all(path, url_conversion, **kwargs), response['count']
326
+ response = self._get(path, params=params, **kwargs)
327
+ return self._list_all(path, url_conversion, params=params, **kwargs), response.get('count')
250
328
  else:
329
+ response = self._get(path, params=params, **kwargs)
251
330
  return response
252
331
 
253
- def _list_all(self, path, url_conversion=None, params={}, **kwargs):
254
- params['page_size'] = self.page_size
255
- response = self._get(path, url_conversion, params=params, **kwargs)
256
- yield from response['results']
257
- if response['next']:
258
- yield from self._list_all(response['next'], url_conversion, **kwargs)
332
+ def _list_all(self, path, url_conversion=None, params=None, **kwargs):
333
+ """Generator that yields all results from a paginated API endpoint.
334
+
335
+ This method handles pagination automatically by following the 'next' URLs
336
+ returned by the API until all pages have been fetched. It uses an iterative
337
+ approach (while loop) instead of recursion to avoid stack overflow with
338
+ deep pagination.
339
+
340
+ Args:
341
+ path (str): Initial URL path to request.
342
+ url_conversion (dict, optional): Configuration for URL to path conversion.
343
+ Applied to all pages. Common structure:
344
+ - 'files_fields': List of field names containing file URLs
345
+ - 'is_list': Whether the response is a list (True for paginated results)
346
+ Example: {'files_fields': ['files', 'images'], 'is_list': True}
347
+ params (dict, optional): Query parameters for the first request only.
348
+ Subsequent requests use the 'next' URL which already includes
349
+ all necessary parameters. If 'page_size' is not specified,
350
+ it defaults to self.page_size (100).
351
+ Example: {'status': 'active', 'page_size': 50}
352
+ **kwargs: Additional keyword arguments to pass to requests.
353
+ Example: timeout, headers, etc.
354
+
355
+ Yields:
356
+ dict: Individual result items from all pages. Each item is yielded
357
+ as soon as it's fetched, allowing for memory-efficient processing
358
+ of large datasets.
359
+
360
+ Examples:
361
+ Basic usage - fetch all tasks:
362
+ >>> for task in client._list_all('api/tasks/'):
363
+ ... process_task(task)
364
+
365
+ With filters:
366
+ >>> params = {'status': 'pending', 'priority': 'high'}
367
+ >>> for task in client._list_all('api/tasks/', params=params):
368
+ ... print(task['id'])
369
+
370
+ With url_conversion for file fields:
371
+ >>> url_conversion = {'files_fields': ['files'], 'is_list': True}
372
+ >>> for data_unit in client._list_all('api/data_units/', url_conversion):
373
+ ... # File URLs in 'files' field are converted to local paths
374
+ ... print(data_unit['files'])
375
+
376
+ Collecting results into a list:
377
+ >>> all_tasks = list(client._list_all('api/tasks/'))
378
+ >>> print(f"Total tasks: {len(all_tasks)}")
379
+
380
+ Note:
381
+ - This is a generator function, so results are fetched lazily as you iterate
382
+ - The first page is fetched with the provided params
383
+ - Subsequent pages use the 'next' URL from the API response
384
+ - No duplicate page_size parameters are added to subsequent requests
385
+ - Memory efficient: processes one item at a time rather than loading all at once
386
+ """
387
+ if params is None:
388
+ params = {}
389
+
390
+ # Set page_size only if not already specified by user
391
+ request_params = params.copy()
392
+ if 'page_size' not in request_params:
393
+ request_params['page_size'] = self.page_size
394
+
395
+ next_url = path
396
+ is_first_request = True
397
+
398
+ while next_url:
399
+ # First request uses params, subsequent requests use next URL directly
400
+ if is_first_request:
401
+ response = self._get(next_url, url_conversion, params=request_params, **kwargs)
402
+ is_first_request = False
403
+ else:
404
+ # next URL already contains all necessary query parameters
405
+ response = self._get(next_url, url_conversion, **kwargs)
406
+
407
+ yield from response['results']
408
+ next_url = response.get('next')
259
409
 
260
410
  def exists(self, api, *args, **kwargs):
261
411
  return getattr(self, api)(*args, **kwargs)['count'] > 0
synapse_sdk/loggers.py CHANGED
@@ -64,6 +64,37 @@ class BaseLogger:
64
64
  else:
65
65
  self.progress_record.update(current_progress)
66
66
 
67
+ def set_progress_failed(self, category: str | None = None):
68
+ """Mark progress as failed with elapsed time but no completion.
69
+
70
+ This method should be called when an operation fails to indicate that
71
+ no progress was made, but still track how long the operation ran before failing.
72
+
73
+ Args:
74
+ category(str | None): progress category
75
+ """
76
+ assert category is not None or 'categories' not in self.progress_record
77
+
78
+ # Calculate elapsed time if start time was recorded
79
+ elapsed_time = None
80
+ if category in self.time_begin_per_category:
81
+ elapsed_time = time.time() - self.time_begin_per_category[category]
82
+ elapsed_time = round(elapsed_time, 2)
83
+
84
+ # Progress is 0% (not completed), no time remaining, but track elapsed time
85
+ failed_progress = {
86
+ 'percent': 0.0,
87
+ 'time_remaining': None,
88
+ 'elapsed_time': elapsed_time,
89
+ 'status': 'failed',
90
+ }
91
+
92
+ if category:
93
+ self.current_progress_category = category
94
+ self.progress_record['categories'][category].update(failed_progress)
95
+ else:
96
+ self.progress_record.update(failed_progress)
97
+
67
98
  def get_current_progress(self):
68
99
  categories = self.progress_record.get('categories')
69
100
 
@@ -121,6 +152,10 @@ class ConsoleLogger(BaseLogger):
121
152
  super().set_progress(current, total, category=category)
122
153
  print(self.get_current_progress())
123
154
 
155
+ def set_progress_failed(self, category: str | None = None):
156
+ super().set_progress_failed(category=category)
157
+ print(self.get_current_progress())
158
+
124
159
  def set_metrics(self, value: Dict[Any, Any], category: str):
125
160
  super().set_metrics(value, category)
126
161
  print(self.metrics_record)
@@ -150,6 +185,17 @@ class BackendLogger(BaseLogger):
150
185
  except ClientError:
151
186
  pass
152
187
 
188
+ def set_progress_failed(self, category: str | None = None):
189
+ super().set_progress_failed(category=category)
190
+ try:
191
+ progress_record = {
192
+ 'record': self.progress_record,
193
+ 'current_progress': self.get_current_progress(),
194
+ }
195
+ self.client.update_job(self.job_id, data={'progress_record': progress_record})
196
+ except ClientError:
197
+ pass
198
+
153
199
  def set_metrics(self, value: Dict[Any, Any], category: str):
154
200
  super().set_metrics(value, category)
155
201
  try: