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/base.py
CHANGED
|
@@ -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
|
-
#
|
|
26
|
-
|
|
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
|
-
|
|
38
|
-
|
|
49
|
+
session.mount('http://', adapter)
|
|
50
|
+
session.mount('https://', adapter)
|
|
39
51
|
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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:
|