synapse-sdk 1.0.0a23__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 (228) hide show
  1. synapse_sdk/__init__.py +24 -0
  2. synapse_sdk/cli/__init__.py +310 -5
  3. synapse_sdk/cli/alias/__init__.py +22 -0
  4. synapse_sdk/cli/alias/create.py +36 -0
  5. synapse_sdk/cli/alias/dataclass.py +31 -0
  6. synapse_sdk/cli/alias/default.py +16 -0
  7. synapse_sdk/cli/alias/delete.py +15 -0
  8. synapse_sdk/cli/alias/list.py +19 -0
  9. synapse_sdk/cli/alias/read.py +15 -0
  10. synapse_sdk/cli/alias/update.py +17 -0
  11. synapse_sdk/cli/alias/utils.py +61 -0
  12. synapse_sdk/cli/code_server.py +687 -0
  13. synapse_sdk/cli/config.py +440 -0
  14. synapse_sdk/cli/devtools.py +90 -0
  15. synapse_sdk/cli/plugin/__init__.py +33 -0
  16. synapse_sdk/cli/{create_plugin.py → plugin/create.py} +2 -2
  17. synapse_sdk/{plugins/cli → cli/plugin}/publish.py +23 -15
  18. synapse_sdk/clients/agent/__init__.py +9 -3
  19. synapse_sdk/clients/agent/container.py +143 -0
  20. synapse_sdk/clients/agent/core.py +19 -0
  21. synapse_sdk/clients/agent/ray.py +298 -9
  22. synapse_sdk/clients/backend/__init__.py +30 -12
  23. synapse_sdk/clients/backend/annotation.py +13 -5
  24. synapse_sdk/clients/backend/core.py +31 -4
  25. synapse_sdk/clients/backend/data_collection.py +186 -0
  26. synapse_sdk/clients/backend/hitl.py +17 -0
  27. synapse_sdk/clients/backend/integration.py +16 -1
  28. synapse_sdk/clients/backend/ml.py +5 -1
  29. synapse_sdk/clients/backend/models.py +78 -0
  30. synapse_sdk/clients/base.py +384 -41
  31. synapse_sdk/clients/ray/serve.py +2 -0
  32. synapse_sdk/clients/validators/collections.py +31 -0
  33. synapse_sdk/devtools/config.py +94 -0
  34. synapse_sdk/devtools/server.py +41 -0
  35. synapse_sdk/devtools/streamlit_app/__init__.py +5 -0
  36. synapse_sdk/devtools/streamlit_app/app.py +128 -0
  37. synapse_sdk/devtools/streamlit_app/services/__init__.py +11 -0
  38. synapse_sdk/devtools/streamlit_app/services/job_service.py +233 -0
  39. synapse_sdk/devtools/streamlit_app/services/plugin_service.py +236 -0
  40. synapse_sdk/devtools/streamlit_app/services/serve_service.py +95 -0
  41. synapse_sdk/devtools/streamlit_app/ui/__init__.py +15 -0
  42. synapse_sdk/devtools/streamlit_app/ui/config_tab.py +76 -0
  43. synapse_sdk/devtools/streamlit_app/ui/deployment_tab.py +66 -0
  44. synapse_sdk/devtools/streamlit_app/ui/http_tab.py +125 -0
  45. synapse_sdk/devtools/streamlit_app/ui/jobs_tab.py +573 -0
  46. synapse_sdk/devtools/streamlit_app/ui/serve_tab.py +346 -0
  47. synapse_sdk/devtools/streamlit_app/ui/status_bar.py +118 -0
  48. synapse_sdk/devtools/streamlit_app/utils/__init__.py +40 -0
  49. synapse_sdk/devtools/streamlit_app/utils/json_viewer.py +197 -0
  50. synapse_sdk/devtools/streamlit_app/utils/log_formatter.py +38 -0
  51. synapse_sdk/devtools/streamlit_app/utils/styles.py +241 -0
  52. synapse_sdk/devtools/streamlit_app/utils/ui_components.py +289 -0
  53. synapse_sdk/devtools/streamlit_app.py +10 -0
  54. synapse_sdk/loggers.py +120 -9
  55. synapse_sdk/plugins/README.md +1340 -0
  56. synapse_sdk/plugins/__init__.py +0 -13
  57. synapse_sdk/plugins/categories/base.py +117 -11
  58. synapse_sdk/plugins/categories/data_validation/actions/validation.py +72 -0
  59. synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +33 -5
  60. synapse_sdk/plugins/categories/export/actions/__init__.py +3 -0
  61. synapse_sdk/plugins/categories/export/actions/export/__init__.py +28 -0
  62. synapse_sdk/plugins/categories/export/actions/export/action.py +165 -0
  63. synapse_sdk/plugins/categories/export/actions/export/enums.py +113 -0
  64. synapse_sdk/plugins/categories/export/actions/export/exceptions.py +53 -0
  65. synapse_sdk/plugins/categories/export/actions/export/models.py +74 -0
  66. synapse_sdk/plugins/categories/export/actions/export/run.py +195 -0
  67. synapse_sdk/plugins/categories/export/actions/export/utils.py +187 -0
  68. synapse_sdk/plugins/categories/export/templates/config.yaml +21 -0
  69. synapse_sdk/plugins/categories/export/templates/plugin/__init__.py +390 -0
  70. synapse_sdk/plugins/categories/export/templates/plugin/export.py +160 -0
  71. synapse_sdk/plugins/categories/neural_net/actions/deployment.py +13 -12
  72. synapse_sdk/plugins/categories/neural_net/actions/train.py +1134 -31
  73. synapse_sdk/plugins/categories/neural_net/actions/tune.py +534 -0
  74. synapse_sdk/plugins/categories/neural_net/base/inference.py +1 -1
  75. synapse_sdk/plugins/categories/neural_net/templates/config.yaml +32 -4
  76. synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +26 -10
  77. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  78. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  79. synapse_sdk/plugins/categories/{export/actions/export.py → pre_annotation/actions/pre_annotation/action.py} +4 -4
  80. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  81. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +148 -0
  82. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  83. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  84. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  85. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +100 -0
  86. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +248 -0
  87. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  88. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  89. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +265 -0
  90. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  91. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  92. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +92 -0
  93. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +243 -0
  94. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  95. synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +19 -0
  96. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/to_task.py +40 -0
  97. synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +2 -0
  98. synapse_sdk/plugins/categories/upload/__init__.py +0 -0
  99. synapse_sdk/plugins/categories/upload/actions/__init__.py +0 -0
  100. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +19 -0
  101. synapse_sdk/plugins/categories/upload/actions/upload/action.py +236 -0
  102. synapse_sdk/plugins/categories/upload/actions/upload/context.py +185 -0
  103. synapse_sdk/plugins/categories/upload/actions/upload/enums.py +493 -0
  104. synapse_sdk/plugins/categories/upload/actions/upload/exceptions.py +36 -0
  105. synapse_sdk/plugins/categories/upload/actions/upload/factory.py +138 -0
  106. synapse_sdk/plugins/categories/upload/actions/upload/models.py +214 -0
  107. synapse_sdk/plugins/categories/upload/actions/upload/orchestrator.py +183 -0
  108. synapse_sdk/plugins/categories/upload/actions/upload/registry.py +113 -0
  109. synapse_sdk/plugins/categories/upload/actions/upload/run.py +179 -0
  110. synapse_sdk/plugins/categories/upload/actions/upload/steps/__init__.py +1 -0
  111. synapse_sdk/plugins/categories/upload/actions/upload/steps/base.py +107 -0
  112. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +62 -0
  113. synapse_sdk/plugins/categories/upload/actions/upload/steps/collection.py +63 -0
  114. synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +91 -0
  115. synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +82 -0
  116. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +235 -0
  117. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +201 -0
  118. synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +104 -0
  119. synapse_sdk/plugins/categories/upload/actions/upload/steps/validate.py +71 -0
  120. synapse_sdk/plugins/categories/upload/actions/upload/strategies/__init__.py +1 -0
  121. synapse_sdk/plugins/categories/upload/actions/upload/strategies/base.py +82 -0
  122. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/__init__.py +1 -0
  123. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/batch.py +39 -0
  124. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/single.py +29 -0
  125. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/__init__.py +1 -0
  126. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +300 -0
  127. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +287 -0
  128. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/__init__.py +1 -0
  129. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/excel.py +174 -0
  130. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/none.py +16 -0
  131. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/__init__.py +1 -0
  132. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/sync.py +84 -0
  133. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/__init__.py +1 -0
  134. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +60 -0
  135. synapse_sdk/plugins/categories/upload/actions/upload/utils.py +250 -0
  136. synapse_sdk/plugins/categories/upload/templates/README.md +470 -0
  137. synapse_sdk/plugins/categories/upload/templates/config.yaml +33 -0
  138. synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py +310 -0
  139. synapse_sdk/plugins/categories/upload/templates/plugin/upload.py +102 -0
  140. synapse_sdk/plugins/enums.py +3 -1
  141. synapse_sdk/plugins/models.py +148 -11
  142. synapse_sdk/plugins/templates/plugin-config-schema.json +406 -0
  143. synapse_sdk/plugins/templates/schema.json +491 -0
  144. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +1 -0
  145. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +1 -1
  146. synapse_sdk/plugins/utils/__init__.py +46 -0
  147. synapse_sdk/plugins/utils/actions.py +119 -0
  148. synapse_sdk/plugins/utils/config.py +203 -0
  149. synapse_sdk/plugins/{utils.py → utils/legacy.py} +26 -46
  150. synapse_sdk/plugins/utils/ray_gcs.py +66 -0
  151. synapse_sdk/plugins/utils/registry.py +58 -0
  152. synapse_sdk/shared/__init__.py +25 -0
  153. synapse_sdk/shared/enums.py +93 -0
  154. synapse_sdk/types.py +19 -0
  155. synapse_sdk/utils/converters/__init__.py +240 -0
  156. synapse_sdk/utils/converters/coco/__init__.py +0 -0
  157. synapse_sdk/utils/converters/coco/from_dm.py +322 -0
  158. synapse_sdk/utils/converters/coco/to_dm.py +215 -0
  159. synapse_sdk/utils/converters/dm/__init__.py +57 -0
  160. synapse_sdk/utils/converters/dm/base.py +137 -0
  161. synapse_sdk/utils/converters/dm/from_v1.py +273 -0
  162. synapse_sdk/utils/converters/dm/to_v1.py +321 -0
  163. synapse_sdk/utils/converters/dm/tools/__init__.py +214 -0
  164. synapse_sdk/utils/converters/dm/tools/answer.py +95 -0
  165. synapse_sdk/utils/converters/dm/tools/bounding_box.py +132 -0
  166. synapse_sdk/utils/converters/dm/tools/bounding_box_3d.py +121 -0
  167. synapse_sdk/utils/converters/dm/tools/classification.py +75 -0
  168. synapse_sdk/utils/converters/dm/tools/keypoint.py +117 -0
  169. synapse_sdk/utils/converters/dm/tools/named_entity.py +111 -0
  170. synapse_sdk/utils/converters/dm/tools/polygon.py +122 -0
  171. synapse_sdk/utils/converters/dm/tools/polyline.py +124 -0
  172. synapse_sdk/utils/converters/dm/tools/prompt.py +94 -0
  173. synapse_sdk/utils/converters/dm/tools/relation.py +86 -0
  174. synapse_sdk/utils/converters/dm/tools/segmentation.py +141 -0
  175. synapse_sdk/utils/converters/dm/tools/segmentation_3d.py +83 -0
  176. synapse_sdk/utils/converters/dm/types.py +168 -0
  177. synapse_sdk/utils/converters/dm/utils.py +162 -0
  178. synapse_sdk/utils/converters/dm_legacy/__init__.py +56 -0
  179. synapse_sdk/utils/converters/dm_legacy/from_v1.py +627 -0
  180. synapse_sdk/utils/converters/dm_legacy/to_v1.py +367 -0
  181. synapse_sdk/utils/converters/pascal/__init__.py +0 -0
  182. synapse_sdk/utils/converters/pascal/from_dm.py +244 -0
  183. synapse_sdk/utils/converters/pascal/to_dm.py +214 -0
  184. synapse_sdk/utils/converters/yolo/__init__.py +0 -0
  185. synapse_sdk/utils/converters/yolo/from_dm.py +384 -0
  186. synapse_sdk/utils/converters/yolo/to_dm.py +267 -0
  187. synapse_sdk/utils/dataset.py +46 -0
  188. synapse_sdk/utils/encryption.py +158 -0
  189. synapse_sdk/utils/file/__init__.py +58 -0
  190. synapse_sdk/utils/file/archive.py +32 -0
  191. synapse_sdk/utils/file/checksum.py +56 -0
  192. synapse_sdk/utils/file/chunking.py +31 -0
  193. synapse_sdk/utils/file/download.py +385 -0
  194. synapse_sdk/utils/file/encoding.py +40 -0
  195. synapse_sdk/utils/file/io.py +22 -0
  196. synapse_sdk/utils/file/upload.py +165 -0
  197. synapse_sdk/utils/file/video/__init__.py +29 -0
  198. synapse_sdk/utils/file/video/transcode.py +307 -0
  199. synapse_sdk/utils/file.py.backup +301 -0
  200. synapse_sdk/utils/http.py +138 -0
  201. synapse_sdk/utils/network.py +309 -0
  202. synapse_sdk/utils/storage/__init__.py +72 -0
  203. synapse_sdk/utils/storage/providers/__init__.py +183 -0
  204. synapse_sdk/utils/storage/providers/file_system.py +134 -0
  205. synapse_sdk/utils/storage/providers/gcp.py +13 -0
  206. synapse_sdk/utils/storage/providers/http.py +190 -0
  207. synapse_sdk/utils/storage/providers/s3.py +91 -0
  208. synapse_sdk/utils/storage/providers/sftp.py +47 -0
  209. synapse_sdk/utils/storage/registry.py +17 -0
  210. synapse_sdk-2025.12.3.dist-info/METADATA +123 -0
  211. synapse_sdk-2025.12.3.dist-info/RECORD +279 -0
  212. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/WHEEL +1 -1
  213. synapse_sdk/clients/backend/dataset.py +0 -51
  214. synapse_sdk/plugins/categories/import/actions/import.py +0 -10
  215. synapse_sdk/plugins/cli/__init__.py +0 -21
  216. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
  217. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
  218. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
  219. synapse_sdk/utils/file.py +0 -168
  220. synapse_sdk/utils/storage.py +0 -91
  221. synapse_sdk-1.0.0a23.dist-info/METADATA +0 -44
  222. synapse_sdk-1.0.0a23.dist-info/RECORD +0 -114
  223. /synapse_sdk/{plugins/cli → cli/plugin}/run.py +0 -0
  224. /synapse_sdk/{plugins/categories/import → clients/validators}/__init__.py +0 -0
  225. /synapse_sdk/{plugins/categories/import/actions → devtools}/__init__.py +0 -0
  226. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/entry_points.txt +0 -0
  227. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info/licenses}/LICENSE +0 -0
  228. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/top_level.txt +0 -0
@@ -1,56 +1,157 @@
1
1
  import json
2
- import os
3
- from pathlib import Path
4
2
 
5
3
  import requests
4
+ from requests.adapters import HTTPAdapter
5
+ from urllib3.util.retry import Retry
6
6
 
7
7
  from synapse_sdk.clients.exceptions import ClientError
8
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
+ )
9
15
 
10
16
 
11
17
  class BaseClient:
12
18
  name = None
13
19
  base_url = None
20
+ page_size = 100
14
21
 
15
- def __init__(self, base_url):
16
- self.base_url = base_url
17
- requests_session = requests.Session()
18
- self.requests_session = requests_session
22
+ def __init__(self, base_url, timeout=None):
23
+ self.base_url = base_url.rstrip('/')
24
+ # Set reasonable default timeouts for better UX
25
+ self.timeout = timeout or {
26
+ 'connect': 5, # Connection timeout: 5 seconds
27
+ 'read': 15, # Read timeout: 15 seconds
28
+ }
19
29
 
20
- def _get_url(self, path):
21
- if not path.startswith(self.base_url):
22
- return os.path.join(self.base_url, path)
23
- return path
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()
44
+
45
+ # Configure retry strategy for transient failures
46
+ retry_strategy = Retry(**self._retry_config)
47
+
48
+ adapter = HTTPAdapter(max_retries=retry_strategy)
49
+ session.mount('http://', adapter)
50
+ session.mount('https://', adapter)
51
+
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
63
+
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
24
82
 
25
83
  def _get_headers(self):
26
84
  return {}
27
85
 
28
- def _request(self, method, path, **kwargs):
86
+ def _request(self, method: str, path: str, **kwargs) -> dict | str:
87
+ """Request handler for all HTTP methods.
88
+
89
+ Args:
90
+ method (str): HTTP method to use.
91
+ path (str): URL path to request.
92
+ **kwargs: Additional keyword arguments to pass to the request.
93
+
94
+ Returns:
95
+ dict | str: JSON response or text response.
96
+ """
29
97
  url = self._get_url(path)
30
98
  headers = self._get_headers()
31
99
  headers.update(kwargs.pop('headers', {}))
32
100
 
33
- if method in ['post', 'put', 'patch']:
34
- if kwargs.get('files') is not None:
35
- for name, file in kwargs['files'].items():
36
- if isinstance(file, (str, Path)):
37
- kwargs['files'][name] = Path(str(file)).open(mode='rb')
38
- for name, value in kwargs['data'].items():
39
- if isinstance(value, dict):
40
- kwargs['data'][name] = json.dumps(value)
41
- else:
42
- headers['Content-Type'] = 'application/json'
43
- if 'data' in kwargs:
44
- kwargs['data'] = json.dumps(kwargs['data'])
101
+ # Set timeout if not provided in kwargs
102
+ if 'timeout' not in kwargs:
103
+ kwargs['timeout'] = (self.timeout['connect'], self.timeout['read'])
104
+
105
+ # List to store opened files to close after request
106
+ opened_files = []
45
107
 
46
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
+
127
+ # Send request
47
128
  response = getattr(self.requests_session, method)(url, headers=headers, **kwargs)
48
129
  if not response.ok:
49
130
  raise ClientError(
50
131
  response.status_code, response.json() if response.status_code == 400 else response.reason
51
132
  )
52
- except requests.ConnectionError:
53
- raise ClientError(408, f'{self.name} is not responding')
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
137
+ except requests.exceptions.ConnectTimeout:
138
+ raise ClientError(408, f'{self.name} connection timeout (>{self.timeout["connect"]}s)')
139
+ except requests.exceptions.ReadTimeout:
140
+ raise ClientError(408, f'{self.name} read timeout (>{self.timeout["read"]}s)')
141
+ except requests.exceptions.ConnectionError as e:
142
+ # More specific error handling for different connection issues
143
+ if 'Name or service not known' in str(e) or 'nodename nor servname provided' in str(e):
144
+ raise ClientError(503, f'{self.name} host unreachable')
145
+ elif 'Connection refused' in str(e):
146
+ raise ClientError(503, f'{self.name} connection refused')
147
+ else:
148
+ raise ClientError(503, f'{self.name} connection error: {str(e)[:100]}')
149
+ except requests.exceptions.RequestException as e:
150
+ # Catch all other requests exceptions
151
+ raise ClientError(500, f'{self.name} request failed: {str(e)[:100]}')
152
+ finally:
153
+ # Always close opened files, even if an exception occurred
154
+ close_file_handles(opened_files)
54
155
 
55
156
  return self._post_response(response)
56
157
 
@@ -60,39 +161,281 @@ class BaseClient:
60
161
  except ValueError:
61
162
  return response.text
62
163
 
63
- def _get(self, path, url_conversion=None, **kwargs):
164
+ def _get(self, path, url_conversion=None, response_model=None, **kwargs):
165
+ """Perform a GET request and optionally convert response to a pydantic model.
166
+
167
+ Args:
168
+ path (str): URL path to request.
169
+ url_conversion (dict, optional): Configuration for URL to path conversion.
170
+ request_model (pydantic.BaseModel, optional): Pydantic model to validate the request.
171
+ response_model (pydantic.BaseModel, optional): Pydantic model to validate the response.
172
+ **kwargs: Additional keyword arguments to pass to the request.
173
+
174
+ Returns:
175
+ The response data, optionally converted to a pydantic model.
176
+ """
64
177
  response = self._request('get', path, **kwargs)
178
+
65
179
  if url_conversion:
66
180
  if url_conversion['is_list']:
67
181
  files_url_to_path_from_objs(response['results'], **url_conversion, is_async=True)
68
182
  else:
69
183
  files_url_to_path_from_objs(response, **url_conversion)
184
+
185
+ if response_model:
186
+ return self._validate_response_with_pydantic_model(response, response_model)
187
+
70
188
  return response
71
189
 
72
- def _post(self, path, **kwargs):
73
- return self._request('post', path, **kwargs)
190
+ def _post(self, path, request_model=None, response_model=None, **kwargs):
191
+ """Perform a POST request and optionally convert response to a pydantic model.
74
192
 
75
- def _put(self, path, **kwargs):
76
- return self._request('put', path, **kwargs)
193
+ Args:
194
+ path (str): URL path to request.
195
+ request_model (pydantic.BaseModel, optional): Pydantic model to validate the request.
196
+ response_model (pydantic.BaseModel, optional): Pydantic model to validate the response.
197
+ **kwargs: Additional keyword arguments to pass to the request.
198
+
199
+ Returns:
200
+ The response data, optionally converted to a pydantic model.
201
+ """
202
+ if kwargs.get('data') and request_model:
203
+ kwargs['data'] = self._validate_request_body_with_pydantic_model(kwargs['data'], request_model)
204
+ response = self._request('post', path, **kwargs)
205
+ if response_model:
206
+ return self._validate_response_with_pydantic_model(response, response_model)
207
+ else:
208
+ return response
209
+
210
+ def _put(self, path, request_model=None, response_model=None, **kwargs):
211
+ """Perform a PUT request to the specified path.
212
+
213
+ Args:
214
+ path (str): The URL path for the request.
215
+ request_model (Optional[Type[BaseModel]]): A Pydantic model class to validate the request body against.
216
+ response_model (Optional[Type[BaseModel]]): A Pydantic model class to validate and parse the response.
217
+ **kwargs: Additional arguments to pass to the request method.
218
+ - data: The request body to be sent. If provided along with request_model, it will be validated.
219
+
220
+ Returns:
221
+ Union[dict, BaseModel]:
222
+ If response_model is provided, returns an instance of that model populated with the response data.
223
+ """
224
+ if kwargs.get('data') and request_model:
225
+ kwargs['data'] = self._validate_request_body_with_pydantic_model(kwargs['data'], request_model)
226
+ response = self._request('put', path, **kwargs)
227
+ if response_model:
228
+ return self._validate_response_with_pydantic_model(response, response_model)
229
+ else:
230
+ return response
77
231
 
78
- def _patch(self, path, **kwargs):
79
- return self._request('patch', path, **kwargs)
232
+ def _patch(self, path, request_model=None, response_model=None, **kwargs):
233
+ """Perform a PATCH HTTP request to the specified path.
80
234
 
81
- def _delete(self, path, **kwargs):
82
- return self._request('delete', path, **kwargs)
235
+ Args:
236
+ path (str): The API endpoint path to make the request to.
237
+ request_model (Optional[Type[BaseModel]]): A Pydantic model class used to validate the request body.
238
+ response_model (Optional[Type[BaseModel]]): A Pydantic model class used to validate and parse the response.
239
+ **kwargs: Additional keyword arguments to pass to the request method.
240
+ - data: The request body data. If provided along with request_model, it will be validated.
241
+
242
+ Returns:
243
+ Union[dict, BaseModel]: If response_model is provided, returns an instance of that model.
244
+ Otherwise, returns the raw response data.
245
+ """
246
+ if kwargs.get('data') and request_model:
247
+ kwargs['data'] = self._validate_request_body_with_pydantic_model(kwargs['data'], request_model)
248
+ response = self._request('patch', path, **kwargs)
249
+ if response_model:
250
+ return self._validate_response_with_pydantic_model(response, response_model)
251
+ else:
252
+ return response
253
+
254
+ def _delete(self, path, request_model=None, response_model=None, **kwargs):
255
+ """Performs a DELETE request to the specified path.
256
+
257
+ Args:
258
+ path (str): The API endpoint path to send the DELETE request to.
259
+ request_model (Optional[Type[BaseModel]]): Pydantic model to validate the request data against.
260
+ response_model (Optional[Type[BaseModel]]): Pydantic model to validate and convert the response data.
261
+ **kwargs: Additional keyword arguments passed to the request method.
262
+ - data: Request payload to send. Will be validated against request_model if both are provided.
263
+
264
+ Returns:
265
+ Union[dict, BaseModel]: If response_model is provided, returns an instance of that model.
266
+ Otherwise, returns the raw response data as a dictionary.
267
+ """
268
+ if kwargs.get('data') and request_model:
269
+ kwargs['data'] = self._validate_request_body_with_pydantic_model(kwargs['data'], request_model)
270
+ response = self._request('delete', path, **kwargs)
271
+ if response_model:
272
+ return self._validate_response_with_pydantic_model(response, response_model)
273
+ else:
274
+ return response
275
+
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 = {}
83
324
 
84
- def _list(self, path, url_conversion=None, list_all=False, **kwargs):
85
- response = self._get(path, **kwargs)
86
325
  if list_all:
87
- 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')
88
328
  else:
329
+ response = self._get(path, params=params, **kwargs)
89
330
  return response
90
331
 
91
332
  def _list_all(self, path, url_conversion=None, params=None, **kwargs):
92
- response = self._get(path, url_conversion, params=params, **kwargs)
93
- yield from response['results']
94
- if response['next']:
95
- yield from self._list_all(response['next'], url_conversion, **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')
96
409
 
97
410
  def exists(self, api, *args, **kwargs):
98
411
  return getattr(self, api)(*args, **kwargs)['count'] > 0
412
+
413
+ def _validate_response_with_pydantic_model(self, response, pydantic_model):
414
+ """Validate a response with a pydantic model."""
415
+ # Check if model is a pydantic model (has the __pydantic_model__ attribute)
416
+ if (
417
+ hasattr(pydantic_model, '__pydantic_model__')
418
+ or hasattr(pydantic_model, 'model_validate')
419
+ or hasattr(pydantic_model, 'parse_obj')
420
+ ):
421
+ pydantic_model.model_validate(response)
422
+ return response
423
+ else:
424
+ # Not a pydantic model
425
+ raise TypeError('The provided model is not a pydantic model')
426
+
427
+ def _validate_request_body_with_pydantic_model(self, request_body, pydantic_model):
428
+ """Validate a request body with a pydantic model."""
429
+ # Check if model is a pydantic model (has the __pydantic_model__ attribute)
430
+ if (
431
+ hasattr(pydantic_model, '__pydantic_model__')
432
+ or hasattr(pydantic_model, 'model_validate')
433
+ or hasattr(pydantic_model, 'parse_obj')
434
+ ):
435
+ # Validate the request body and convert to model instance
436
+ model_instance = pydantic_model.model_validate(request_body)
437
+ # Convert model to dict and remove None values
438
+ return {k: v for k, v in model_instance.model_dump().items() if v is not None}
439
+ else:
440
+ # Not a pydantic model
441
+ raise TypeError('The provided model is not a pydantic model')
@@ -8,6 +8,7 @@ class ServeClientMixin(BaseClient):
8
8
  response = self._get(path, params=params)
9
9
  for key, item in response['applications'].items():
10
10
  response['applications'][key]['deployments'] = list(item['deployments'].values())
11
+ response['applications'][key]['route_prefix'] = item['route_prefix']
11
12
  return list(response['applications'].values())
12
13
 
13
14
  def get_serve_application(self, pk, params=None):
@@ -15,6 +16,7 @@ class ServeClientMixin(BaseClient):
15
16
  response = self._get(path, params=params)
16
17
  try:
17
18
  response['applications'][pk]['deployments'] = list(response['applications'][pk]['deployments'].values())
19
+ response['applications'][pk]['route_prefix'] = response['applications'][pk]['route_prefix']
18
20
  return response['applications'][pk]
19
21
  except KeyError:
20
22
  raise ClientError(404, 'Serve Application Not Found')
@@ -0,0 +1,31 @@
1
+ class FileSpecificationValidator:
2
+ """File specification validator class for synapse backend collection.
3
+
4
+ Args:
5
+ file_spec_template (list):
6
+ * List of dictionaries containing file specification template
7
+ * This is from synapse-backend file specification data.
8
+ organized_files (list): List of dictionaries containing organized files.
9
+ """
10
+
11
+ def __init__(self, file_spec_template, organized_files):
12
+ self.file_spec_template = file_spec_template
13
+ self.organized_files = organized_files
14
+
15
+ def validate(self):
16
+ """Validate the file specification template with organized files.
17
+
18
+ Returns:
19
+ bool: True if the file specification template is valid, False otherwise.
20
+ """
21
+ for spec in self.file_spec_template:
22
+ spec_name = spec['name']
23
+ is_required = spec['is_required']
24
+
25
+ for file_group in self.organized_files:
26
+ files = file_group['files']
27
+ if is_required and spec_name not in files:
28
+ return False
29
+ if spec_name in files and not files[spec_name]:
30
+ return False
31
+ return True
@@ -0,0 +1,94 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Dict, Optional
4
+
5
+ CONFIG_DIR = Path.home() / '.config' / 'synapse'
6
+ DEVTOOLS_CONFIG_FILE = CONFIG_DIR / 'devtools.json'
7
+
8
+
9
+ def ensure_config_dir():
10
+ """Ensure the config directory exists"""
11
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
12
+
13
+
14
+ def load_devtools_config() -> Dict:
15
+ """Load devtools configuration from file"""
16
+ ensure_config_dir()
17
+
18
+ # Handle both Path and string types for testing
19
+ config_file = Path(DEVTOOLS_CONFIG_FILE) if isinstance(DEVTOOLS_CONFIG_FILE, str) else DEVTOOLS_CONFIG_FILE
20
+
21
+ if not config_file.exists():
22
+ return {}
23
+
24
+ try:
25
+ with open(config_file, 'r') as f:
26
+ return json.load(f)
27
+ except (json.JSONDecodeError, IOError):
28
+ return {}
29
+
30
+
31
+ def save_devtools_config(config: Dict):
32
+ """Save devtools configuration to file"""
33
+ ensure_config_dir()
34
+
35
+ # Handle both Path and string types for testing
36
+ config_file = Path(DEVTOOLS_CONFIG_FILE) if isinstance(DEVTOOLS_CONFIG_FILE, str) else DEVTOOLS_CONFIG_FILE
37
+
38
+ try:
39
+ with open(config_file, 'w') as f:
40
+ json.dump(config, f, indent=2)
41
+ except IOError:
42
+ pass
43
+
44
+
45
+ def get_backend_config() -> Optional[Dict]:
46
+ """Get backend configuration (host and token)"""
47
+ config = load_devtools_config()
48
+ backend = config.get('backend', {})
49
+
50
+ host = backend.get('host')
51
+ token = backend.get('token')
52
+
53
+ if host and token:
54
+ return {'host': host, 'token': token}
55
+
56
+ return None
57
+
58
+
59
+ def set_backend_config(host: str, token: str):
60
+ """Set backend configuration"""
61
+ config = load_devtools_config()
62
+ config['backend'] = {'host': host, 'token': token}
63
+ save_devtools_config(config)
64
+
65
+
66
+ def clear_backend_config():
67
+ """Clear backend configuration"""
68
+ config = load_devtools_config()
69
+ if 'backend' in config:
70
+ del config['backend']
71
+ save_devtools_config(config)
72
+
73
+
74
+ def get_server_config() -> Dict:
75
+ """Get server configuration (host and port)"""
76
+ config = load_devtools_config()
77
+ server = config.get('server', {})
78
+
79
+ return {'host': server.get('host', '0.0.0.0'), 'port': server.get('port', 8080)}
80
+
81
+
82
+ def set_server_config(host: str = None, port: int = None):
83
+ """Set server configuration"""
84
+ config = load_devtools_config()
85
+
86
+ if 'server' not in config:
87
+ config['server'] = {}
88
+
89
+ if host is not None:
90
+ config['server']['host'] = host
91
+ if port is not None:
92
+ config['server']['port'] = port
93
+
94
+ save_devtools_config(config)
@@ -0,0 +1,41 @@
1
+ """
2
+ Legacy server module - DEPRECATED
3
+
4
+ This module is kept for backwards compatibility only.
5
+ The devtools now use Streamlit exclusively.
6
+
7
+ All functionality has been moved to streamlit_app.py
8
+ """
9
+
10
+ import logging
11
+ from pathlib import Path
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class DevtoolsServer:
17
+ """Legacy DevtoolsServer class - DEPRECATED
18
+
19
+ This class is kept only for backwards compatibility.
20
+ Use streamlit_app.py instead.
21
+ """
22
+
23
+ def __init__(self, host: str = '0.0.0.0', port: int = 8080, plugin_directory: str = None):
24
+ logger.warning("DevtoolsServer is deprecated. Use 'synapse devtools' command which runs Streamlit instead.")
25
+ self.host = host
26
+ self.port = port
27
+ self.plugin_directory = Path(plugin_directory) if plugin_directory else Path.cwd()
28
+
29
+ def start_server(self):
30
+ """Legacy method - DEPRECATED"""
31
+ logger.error("FastAPI server is no longer supported. Use 'synapse devtools' command to run Streamlit app.")
32
+ raise RuntimeError("FastAPI server is deprecated. Use 'synapse devtools' command to run the Streamlit app.")
33
+
34
+
35
+ def create_devtools_server(host: str = '0.0.0.0', port: int = 8080, plugin_directory: str = None) -> DevtoolsServer:
36
+ """Legacy function - DEPRECATED
37
+
38
+ This function is kept only for backwards compatibility.
39
+ Use 'synapse devtools' command instead.
40
+ """
41
+ return DevtoolsServer(host=host, port=port, plugin_directory=plugin_directory)
@@ -0,0 +1,5 @@
1
+ """Streamlit-based Synapse DevTools Application."""
2
+
3
+ from .app import DevToolsApp, main
4
+
5
+ __all__ = ['DevToolsApp', 'main']