synapse-sdk 1.0.0a11__py3-none-any.whl → 2026.1.1b2__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.

Potentially problematic release.


This version of synapse-sdk might be problematic. Click here for more details.

Files changed (261) hide show
  1. synapse_sdk/__init__.py +24 -0
  2. synapse_sdk/cli/__init__.py +9 -8
  3. synapse_sdk/cli/agent/__init__.py +25 -0
  4. synapse_sdk/cli/agent/config.py +104 -0
  5. synapse_sdk/cli/agent/select.py +197 -0
  6. synapse_sdk/cli/auth.py +104 -0
  7. synapse_sdk/cli/main.py +1025 -0
  8. synapse_sdk/cli/plugin/__init__.py +58 -0
  9. synapse_sdk/cli/plugin/create.py +566 -0
  10. synapse_sdk/cli/plugin/job.py +196 -0
  11. synapse_sdk/cli/plugin/publish.py +322 -0
  12. synapse_sdk/cli/plugin/run.py +131 -0
  13. synapse_sdk/cli/plugin/test.py +200 -0
  14. synapse_sdk/clients/README.md +239 -0
  15. synapse_sdk/clients/__init__.py +5 -0
  16. synapse_sdk/clients/_template.py +266 -0
  17. synapse_sdk/clients/agent/__init__.py +84 -29
  18. synapse_sdk/clients/agent/async_ray.py +289 -0
  19. synapse_sdk/clients/agent/container.py +83 -0
  20. synapse_sdk/clients/agent/plugin.py +101 -0
  21. synapse_sdk/clients/agent/ray.py +296 -39
  22. synapse_sdk/clients/backend/__init__.py +152 -12
  23. synapse_sdk/clients/backend/annotation.py +164 -22
  24. synapse_sdk/clients/backend/core.py +101 -0
  25. synapse_sdk/clients/backend/data_collection.py +292 -0
  26. synapse_sdk/clients/backend/hitl.py +87 -0
  27. synapse_sdk/clients/backend/integration.py +374 -46
  28. synapse_sdk/clients/backend/ml.py +134 -22
  29. synapse_sdk/clients/backend/models.py +247 -0
  30. synapse_sdk/clients/base.py +538 -59
  31. synapse_sdk/clients/exceptions.py +35 -7
  32. synapse_sdk/clients/pipeline/__init__.py +5 -0
  33. synapse_sdk/clients/pipeline/client.py +636 -0
  34. synapse_sdk/clients/protocols.py +178 -0
  35. synapse_sdk/clients/utils.py +86 -8
  36. synapse_sdk/clients/validation.py +58 -0
  37. synapse_sdk/enums.py +76 -0
  38. synapse_sdk/exceptions.py +168 -0
  39. synapse_sdk/integrations/__init__.py +74 -0
  40. synapse_sdk/integrations/_base.py +119 -0
  41. synapse_sdk/integrations/_context.py +53 -0
  42. synapse_sdk/integrations/ultralytics/__init__.py +78 -0
  43. synapse_sdk/integrations/ultralytics/_callbacks.py +126 -0
  44. synapse_sdk/integrations/ultralytics/_patches.py +124 -0
  45. synapse_sdk/loggers.py +476 -95
  46. synapse_sdk/mcp/MCP.md +69 -0
  47. synapse_sdk/mcp/__init__.py +48 -0
  48. synapse_sdk/mcp/__main__.py +6 -0
  49. synapse_sdk/mcp/config.py +349 -0
  50. synapse_sdk/mcp/prompts/__init__.py +4 -0
  51. synapse_sdk/mcp/resources/__init__.py +4 -0
  52. synapse_sdk/mcp/server.py +1352 -0
  53. synapse_sdk/mcp/tools/__init__.py +6 -0
  54. synapse_sdk/plugins/__init__.py +133 -9
  55. synapse_sdk/plugins/action.py +229 -0
  56. synapse_sdk/plugins/actions/__init__.py +82 -0
  57. synapse_sdk/plugins/actions/dataset/__init__.py +37 -0
  58. synapse_sdk/plugins/actions/dataset/action.py +471 -0
  59. synapse_sdk/plugins/actions/export/__init__.py +55 -0
  60. synapse_sdk/plugins/actions/export/action.py +183 -0
  61. synapse_sdk/plugins/actions/export/context.py +59 -0
  62. synapse_sdk/plugins/actions/inference/__init__.py +84 -0
  63. synapse_sdk/plugins/actions/inference/action.py +285 -0
  64. synapse_sdk/plugins/actions/inference/context.py +81 -0
  65. synapse_sdk/plugins/actions/inference/deployment.py +322 -0
  66. synapse_sdk/plugins/actions/inference/serve.py +252 -0
  67. synapse_sdk/plugins/actions/train/__init__.py +54 -0
  68. synapse_sdk/plugins/actions/train/action.py +326 -0
  69. synapse_sdk/plugins/actions/train/context.py +57 -0
  70. synapse_sdk/plugins/actions/upload/__init__.py +49 -0
  71. synapse_sdk/plugins/actions/upload/action.py +165 -0
  72. synapse_sdk/plugins/actions/upload/context.py +61 -0
  73. synapse_sdk/plugins/config.py +98 -0
  74. synapse_sdk/plugins/context/__init__.py +109 -0
  75. synapse_sdk/plugins/context/env.py +113 -0
  76. synapse_sdk/plugins/datasets/__init__.py +113 -0
  77. synapse_sdk/plugins/datasets/converters/__init__.py +76 -0
  78. synapse_sdk/plugins/datasets/converters/base.py +347 -0
  79. synapse_sdk/plugins/datasets/converters/yolo/__init__.py +9 -0
  80. synapse_sdk/plugins/datasets/converters/yolo/from_dm.py +468 -0
  81. synapse_sdk/plugins/datasets/converters/yolo/to_dm.py +381 -0
  82. synapse_sdk/plugins/datasets/formats/__init__.py +82 -0
  83. synapse_sdk/plugins/datasets/formats/dm.py +351 -0
  84. synapse_sdk/plugins/datasets/formats/yolo.py +240 -0
  85. synapse_sdk/plugins/decorators.py +83 -0
  86. synapse_sdk/plugins/discovery.py +790 -0
  87. synapse_sdk/plugins/docs/ACTION_DEV_GUIDE.md +933 -0
  88. synapse_sdk/plugins/docs/ARCHITECTURE.md +1225 -0
  89. synapse_sdk/plugins/docs/LOGGING_SYSTEM.md +683 -0
  90. synapse_sdk/plugins/docs/OVERVIEW.md +531 -0
  91. synapse_sdk/plugins/docs/PIPELINE_GUIDE.md +145 -0
  92. synapse_sdk/plugins/docs/README.md +513 -0
  93. synapse_sdk/plugins/docs/STEP.md +656 -0
  94. synapse_sdk/plugins/enums.py +70 -10
  95. synapse_sdk/plugins/errors.py +92 -0
  96. synapse_sdk/plugins/executors/__init__.py +43 -0
  97. synapse_sdk/plugins/executors/local.py +99 -0
  98. synapse_sdk/plugins/executors/ray/__init__.py +18 -0
  99. synapse_sdk/plugins/executors/ray/base.py +282 -0
  100. synapse_sdk/plugins/executors/ray/job.py +298 -0
  101. synapse_sdk/plugins/executors/ray/jobs_api.py +511 -0
  102. synapse_sdk/plugins/executors/ray/packaging.py +137 -0
  103. synapse_sdk/plugins/executors/ray/pipeline.py +792 -0
  104. synapse_sdk/plugins/executors/ray/task.py +257 -0
  105. synapse_sdk/plugins/models/__init__.py +26 -0
  106. synapse_sdk/plugins/models/logger.py +173 -0
  107. synapse_sdk/plugins/models/pipeline.py +25 -0
  108. synapse_sdk/plugins/pipelines/__init__.py +81 -0
  109. synapse_sdk/plugins/pipelines/action_pipeline.py +417 -0
  110. synapse_sdk/plugins/pipelines/context.py +107 -0
  111. synapse_sdk/plugins/pipelines/display.py +311 -0
  112. synapse_sdk/plugins/runner.py +114 -0
  113. synapse_sdk/plugins/schemas/__init__.py +19 -0
  114. synapse_sdk/plugins/schemas/results.py +152 -0
  115. synapse_sdk/plugins/steps/__init__.py +63 -0
  116. synapse_sdk/plugins/steps/base.py +128 -0
  117. synapse_sdk/plugins/steps/context.py +90 -0
  118. synapse_sdk/plugins/steps/orchestrator.py +128 -0
  119. synapse_sdk/plugins/steps/registry.py +103 -0
  120. synapse_sdk/plugins/steps/utils/__init__.py +20 -0
  121. synapse_sdk/plugins/steps/utils/logging.py +85 -0
  122. synapse_sdk/plugins/steps/utils/timing.py +71 -0
  123. synapse_sdk/plugins/steps/utils/validation.py +68 -0
  124. synapse_sdk/plugins/templates/__init__.py +50 -0
  125. synapse_sdk/plugins/templates/base/.gitignore.j2 +26 -0
  126. synapse_sdk/plugins/templates/base/.synapseignore.j2 +11 -0
  127. synapse_sdk/plugins/templates/base/README.md.j2 +26 -0
  128. synapse_sdk/plugins/templates/base/plugin/__init__.py.j2 +1 -0
  129. synapse_sdk/plugins/templates/base/pyproject.toml.j2 +14 -0
  130. synapse_sdk/plugins/templates/base/requirements.txt.j2 +1 -0
  131. synapse_sdk/plugins/templates/custom/plugin/main.py.j2 +18 -0
  132. synapse_sdk/plugins/templates/data_validation/plugin/validate.py.j2 +32 -0
  133. synapse_sdk/plugins/templates/export/plugin/export.py.j2 +36 -0
  134. synapse_sdk/plugins/templates/neural_net/plugin/inference.py.j2 +36 -0
  135. synapse_sdk/plugins/templates/neural_net/plugin/train.py.j2 +33 -0
  136. synapse_sdk/plugins/templates/post_annotation/plugin/post_annotate.py.j2 +32 -0
  137. synapse_sdk/plugins/templates/pre_annotation/plugin/pre_annotate.py.j2 +32 -0
  138. synapse_sdk/plugins/templates/smart_tool/plugin/auto_label.py.j2 +44 -0
  139. synapse_sdk/plugins/templates/upload/plugin/upload.py.j2 +35 -0
  140. synapse_sdk/plugins/testing/__init__.py +25 -0
  141. synapse_sdk/plugins/testing/sample_actions.py +98 -0
  142. synapse_sdk/plugins/types.py +206 -0
  143. synapse_sdk/plugins/upload.py +595 -64
  144. synapse_sdk/plugins/utils.py +325 -37
  145. synapse_sdk/shared/__init__.py +25 -0
  146. synapse_sdk/utils/__init__.py +1 -0
  147. synapse_sdk/utils/auth.py +74 -0
  148. synapse_sdk/utils/file/__init__.py +58 -0
  149. synapse_sdk/utils/file/archive.py +449 -0
  150. synapse_sdk/utils/file/checksum.py +167 -0
  151. synapse_sdk/utils/file/download.py +286 -0
  152. synapse_sdk/utils/file/io.py +129 -0
  153. synapse_sdk/utils/file/requirements.py +36 -0
  154. synapse_sdk/utils/network.py +168 -0
  155. synapse_sdk/utils/storage/__init__.py +238 -0
  156. synapse_sdk/utils/storage/config.py +188 -0
  157. synapse_sdk/utils/storage/errors.py +52 -0
  158. synapse_sdk/utils/storage/providers/__init__.py +13 -0
  159. synapse_sdk/utils/storage/providers/base.py +76 -0
  160. synapse_sdk/utils/storage/providers/gcs.py +168 -0
  161. synapse_sdk/utils/storage/providers/http.py +250 -0
  162. synapse_sdk/utils/storage/providers/local.py +126 -0
  163. synapse_sdk/utils/storage/providers/s3.py +177 -0
  164. synapse_sdk/utils/storage/providers/sftp.py +208 -0
  165. synapse_sdk/utils/storage/registry.py +125 -0
  166. synapse_sdk/utils/websocket.py +99 -0
  167. synapse_sdk-2026.1.1b2.dist-info/METADATA +715 -0
  168. synapse_sdk-2026.1.1b2.dist-info/RECORD +172 -0
  169. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/WHEEL +1 -1
  170. synapse_sdk-2026.1.1b2.dist-info/licenses/LICENSE +201 -0
  171. locale/en/LC_MESSAGES/messages.mo +0 -0
  172. locale/en/LC_MESSAGES/messages.po +0 -39
  173. locale/ko/LC_MESSAGES/messages.mo +0 -0
  174. locale/ko/LC_MESSAGES/messages.po +0 -34
  175. synapse_sdk/cli/create_plugin.py +0 -10
  176. synapse_sdk/clients/agent/core.py +0 -7
  177. synapse_sdk/clients/agent/service.py +0 -15
  178. synapse_sdk/clients/backend/dataset.py +0 -51
  179. synapse_sdk/clients/ray/__init__.py +0 -6
  180. synapse_sdk/clients/ray/core.py +0 -22
  181. synapse_sdk/clients/ray/serve.py +0 -20
  182. synapse_sdk/i18n.py +0 -35
  183. synapse_sdk/plugins/categories/__init__.py +0 -0
  184. synapse_sdk/plugins/categories/base.py +0 -235
  185. synapse_sdk/plugins/categories/data_validation/__init__.py +0 -0
  186. synapse_sdk/plugins/categories/data_validation/actions/__init__.py +0 -0
  187. synapse_sdk/plugins/categories/data_validation/actions/validation.py +0 -10
  188. synapse_sdk/plugins/categories/data_validation/templates/config.yaml +0 -3
  189. synapse_sdk/plugins/categories/data_validation/templates/plugin/__init__.py +0 -0
  190. synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +0 -5
  191. synapse_sdk/plugins/categories/decorators.py +0 -13
  192. synapse_sdk/plugins/categories/export/__init__.py +0 -0
  193. synapse_sdk/plugins/categories/export/actions/__init__.py +0 -0
  194. synapse_sdk/plugins/categories/export/actions/export.py +0 -10
  195. synapse_sdk/plugins/categories/import/__init__.py +0 -0
  196. synapse_sdk/plugins/categories/import/actions/__init__.py +0 -0
  197. synapse_sdk/plugins/categories/import/actions/import.py +0 -10
  198. synapse_sdk/plugins/categories/neural_net/__init__.py +0 -0
  199. synapse_sdk/plugins/categories/neural_net/actions/__init__.py +0 -0
  200. synapse_sdk/plugins/categories/neural_net/actions/deployment.py +0 -45
  201. synapse_sdk/plugins/categories/neural_net/actions/inference.py +0 -18
  202. synapse_sdk/plugins/categories/neural_net/actions/test.py +0 -10
  203. synapse_sdk/plugins/categories/neural_net/actions/train.py +0 -143
  204. synapse_sdk/plugins/categories/neural_net/templates/config.yaml +0 -12
  205. synapse_sdk/plugins/categories/neural_net/templates/plugin/__init__.py +0 -0
  206. synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +0 -4
  207. synapse_sdk/plugins/categories/neural_net/templates/plugin/test.py +0 -2
  208. synapse_sdk/plugins/categories/neural_net/templates/plugin/train.py +0 -14
  209. synapse_sdk/plugins/categories/post_annotation/__init__.py +0 -0
  210. synapse_sdk/plugins/categories/post_annotation/actions/__init__.py +0 -0
  211. synapse_sdk/plugins/categories/post_annotation/actions/post_annotation.py +0 -10
  212. synapse_sdk/plugins/categories/post_annotation/templates/config.yaml +0 -3
  213. synapse_sdk/plugins/categories/post_annotation/templates/plugin/__init__.py +0 -0
  214. synapse_sdk/plugins/categories/post_annotation/templates/plugin/post_annotation.py +0 -3
  215. synapse_sdk/plugins/categories/pre_annotation/__init__.py +0 -0
  216. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +0 -0
  217. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation.py +0 -10
  218. synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +0 -3
  219. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/__init__.py +0 -0
  220. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/pre_annotation.py +0 -3
  221. synapse_sdk/plugins/categories/registry.py +0 -16
  222. synapse_sdk/plugins/categories/smart_tool/__init__.py +0 -0
  223. synapse_sdk/plugins/categories/smart_tool/actions/__init__.py +0 -0
  224. synapse_sdk/plugins/categories/smart_tool/actions/auto_label.py +0 -37
  225. synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +0 -7
  226. synapse_sdk/plugins/categories/smart_tool/templates/plugin/__init__.py +0 -0
  227. synapse_sdk/plugins/categories/smart_tool/templates/plugin/auto_label.py +0 -11
  228. synapse_sdk/plugins/categories/templates.py +0 -32
  229. synapse_sdk/plugins/cli/__init__.py +0 -21
  230. synapse_sdk/plugins/cli/publish.py +0 -37
  231. synapse_sdk/plugins/cli/run.py +0 -67
  232. synapse_sdk/plugins/exceptions.py +0 -22
  233. synapse_sdk/plugins/models.py +0 -121
  234. synapse_sdk/plugins/templates/cookiecutter.json +0 -11
  235. synapse_sdk/plugins/templates/hooks/post_gen_project.py +0 -3
  236. synapse_sdk/plugins/templates/hooks/pre_prompt.py +0 -21
  237. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
  238. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
  239. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.gitignore +0 -27
  240. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.pre-commit-config.yaml +0 -7
  241. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/README.md +0 -5
  242. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +0 -6
  243. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
  244. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/plugin/__init__.py +0 -0
  245. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/pyproject.toml +0 -13
  246. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +0 -1
  247. synapse_sdk/shared/enums.py +0 -8
  248. synapse_sdk/utils/debug.py +0 -5
  249. synapse_sdk/utils/file.py +0 -87
  250. synapse_sdk/utils/module_loading.py +0 -29
  251. synapse_sdk/utils/pydantic/__init__.py +0 -0
  252. synapse_sdk/utils/pydantic/config.py +0 -4
  253. synapse_sdk/utils/pydantic/errors.py +0 -33
  254. synapse_sdk/utils/pydantic/validators.py +0 -7
  255. synapse_sdk/utils/storage.py +0 -91
  256. synapse_sdk/utils/string.py +0 -11
  257. synapse_sdk-1.0.0a11.dist-info/LICENSE +0 -21
  258. synapse_sdk-1.0.0a11.dist-info/METADATA +0 -43
  259. synapse_sdk-1.0.0a11.dist-info/RECORD +0 -111
  260. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/entry_points.txt +0 -0
  261. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/top_level.txt +0 -0
@@ -1,93 +1,572 @@
1
+ """Base HTTP client classes for sync and async operations.
2
+
3
+ This module provides BaseClient (sync) and AsyncBaseClient (async) classes
4
+ that serve as the foundation for all API clients in the SDK.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
1
9
  import json
2
- import os
10
+ from contextlib import ExitStack
3
11
  from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any
4
13
 
14
+ import httpx
5
15
  import requests
16
+ from requests.adapters import HTTPAdapter
17
+ from urllib3.util.retry import Retry
6
18
 
7
- from synapse_sdk.clients.exceptions import ClientError
19
+ from synapse_sdk.clients.utils import build_url, extract_error_detail, parse_json_response
20
+ from synapse_sdk.clients.validation import ValidationMixin
21
+ from synapse_sdk.exceptions import (
22
+ ClientConnectionError,
23
+ ClientError,
24
+ ClientTimeoutError,
25
+ ServerError,
26
+ raise_for_status,
27
+ )
8
28
  from synapse_sdk.utils.file import files_url_to_path_from_objs
9
29
 
30
+ if TYPE_CHECKING:
31
+ from pydantic import BaseModel
32
+
33
+
34
+ class BaseClient(ValidationMixin):
35
+ """Synchronous HTTP client base using requests.
36
+
37
+ This class provides a foundation for building API clients with
38
+ session management, retry logic, and request/response handling.
10
39
 
11
- class BaseClient:
12
- name = None
13
- base_url = None
40
+ Attributes:
41
+ name: Client name for error messages.
42
+ page_size: Default page size for paginated requests.
43
+ """
14
44
 
15
- def __init__(self, base_url):
16
- self.base_url = base_url
17
- requests_session = requests.Session()
18
- self.requests_session = requests_session
45
+ name: str | None = None
46
+ page_size: int = 100
19
47
 
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
48
+ def __init__(self, base_url: str, timeout: dict[str, int] | None = None):
49
+ """Initialize the base client.
24
50
 
25
- def _get_headers(self):
51
+ Args:
52
+ base_url: The base URL for all API requests.
53
+ timeout: Optional timeout configuration with 'connect' and 'read' keys.
54
+ """
55
+ self.base_url = base_url.rstrip('/')
56
+ self.timeout = timeout or {
57
+ 'connect': 5,
58
+ 'read': 15,
59
+ }
60
+ self._session: requests.Session | None = None
61
+ self._retry_config = {
62
+ 'total': 3,
63
+ 'backoff_factor': 1,
64
+ 'status_forcelist': [502, 503, 504],
65
+ 'allowed_methods': ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
66
+ }
67
+
68
+ def _create_session(self) -> requests.Session:
69
+ """Create a new requests session with retry strategy."""
70
+ session = requests.Session()
71
+ retry_strategy = Retry(**self._retry_config)
72
+ adapter = HTTPAdapter(max_retries=retry_strategy)
73
+ session.mount('http://', adapter)
74
+ session.mount('https://', adapter)
75
+ return session
76
+
77
+ @property
78
+ def requests_session(self) -> requests.Session:
79
+ """Get or create the requests session."""
80
+ if self._session is None:
81
+ self._session = self._create_session()
82
+ return self._session
83
+
84
+ def _get_headers(self) -> dict[str, str]:
85
+ """Return headers for requests. Override in subclasses."""
26
86
  return {}
27
87
 
28
- def _request(self, method, path, **kwargs):
29
- url = self._get_url(path)
88
+ def _request(self, method: str, path: str, **kwargs) -> dict | str | None:
89
+ """Request handler for all HTTP methods.
90
+
91
+ Args:
92
+ method: HTTP method (get, post, put, patch, delete).
93
+ path: URL path to request.
94
+ **kwargs: Additional arguments passed to requests.
95
+
96
+ Returns:
97
+ Parsed response data.
98
+
99
+ Raises:
100
+ ClientTimeoutError: If the request times out.
101
+ ClientConnectionError: If connection fails.
102
+ HTTPError subclasses: For HTTP error responses.
103
+ """
104
+ url = build_url(self.base_url, path)
30
105
  headers = self._get_headers()
106
+ headers.update(kwargs.pop('headers', {}))
107
+
108
+ if 'timeout' not in kwargs:
109
+ kwargs['timeout'] = (self.timeout['connect'], self.timeout['read'])
110
+
111
+ with ExitStack() as stack:
112
+ if method in ('post', 'put', 'patch'):
113
+ self._prepare_request_body(kwargs, headers, stack)
31
114
 
32
- if method in ['post', 'put', 'patch']:
33
- if kwargs.get('files') is not None:
34
- for name, file in kwargs['files'].items():
35
- kwargs['files'][name] = Path(str(file)).open(mode='rb')
115
+ try:
116
+ response = getattr(self.requests_session, method)(url, headers=headers, **kwargs)
117
+ if not response.ok:
118
+ raise_for_status(response.status_code, extract_error_detail(response))
119
+ except (
120
+ ClientError,
121
+ ClientTimeoutError,
122
+ ClientConnectionError,
123
+ ):
124
+ raise
125
+ except requests.exceptions.ConnectTimeout:
126
+ raise ClientTimeoutError(f'{self.name} connection timeout (>{self.timeout["connect"]}s)')
127
+ except requests.exceptions.ReadTimeout:
128
+ raise ClientTimeoutError(f'{self.name} read timeout (>{self.timeout["read"]}s)')
129
+ except requests.exceptions.ConnectionError as e:
130
+ error_str = str(e)
131
+ if 'Name or service not known' in error_str or 'nodename nor servname provided' in error_str:
132
+ raise ClientConnectionError(f'{self.name} host unreachable')
133
+ elif 'Connection refused' in error_str:
134
+ raise ClientConnectionError(f'{self.name} connection refused')
135
+ else:
136
+ raise ClientConnectionError(f'{self.name} connection error: {error_str[:100]}')
137
+ except requests.exceptions.RequestException as e:
138
+ raise ServerError(500, f'{self.name} request failed: {str(e)[:100]}')
139
+
140
+ return parse_json_response(response)
141
+
142
+ def _prepare_request_body(self, kwargs: dict, headers: dict, stack: ExitStack) -> None:
143
+ """Prepare request body, handling files and JSON serialization."""
144
+ if kwargs.get('files') is not None:
145
+ for name, file in kwargs['files'].items():
146
+ if isinstance(file, (str, Path)):
147
+ file = Path(file)
148
+ opened_file = stack.enter_context(file.open(mode='rb'))
149
+ kwargs['files'][name] = (file.name, opened_file)
150
+ if 'data' in kwargs:
36
151
  for name, value in kwargs['data'].items():
37
152
  if isinstance(value, dict):
38
153
  kwargs['data'][name] = json.dumps(value)
154
+ else:
155
+ headers['Content-Type'] = 'application/json'
156
+ if 'data' in kwargs:
157
+ kwargs['data'] = json.dumps(kwargs['data'])
158
+
159
+ def _get(
160
+ self,
161
+ path: str,
162
+ url_conversion: dict | None = None,
163
+ response_model: type[BaseModel] | None = None,
164
+ **kwargs,
165
+ ) -> Any:
166
+ """Perform a GET request.
167
+
168
+ Args:
169
+ path: URL path to request.
170
+ url_conversion: Optional URL conversion config for file paths.
171
+ response_model: Optional Pydantic model for response validation.
172
+ **kwargs: Additional arguments passed to requests.
173
+
174
+ Returns:
175
+ Parsed and optionally validated response data.
176
+ """
177
+ response = self._request('get', path, **kwargs)
178
+
179
+ if url_conversion and isinstance(response, dict):
180
+ is_list = url_conversion.get('is_list', False)
181
+ if is_list:
182
+ files_url_to_path_from_objs(response['results'], **url_conversion, is_async=True)
39
183
  else:
40
- headers['Content-Type'] = 'application/json'
41
- if 'data' in kwargs:
42
- kwargs['data'] = json.dumps(kwargs['data'])
184
+ files_url_to_path_from_objs(response, **url_conversion)
43
185
 
44
- try:
45
- response = getattr(self.requests_session, method)(url, headers=headers, **kwargs)
46
- if not response.ok:
47
- raise ClientError(
48
- response.status_code, response.json() if response.status_code == 400 else response.reason
49
- )
50
- except requests.ConnectionError:
51
- raise ClientError(408, f'{self.name} is not responding')
186
+ if response_model:
187
+ return self._validate_response(response, response_model)
188
+ return response
189
+
190
+ def _mutate(
191
+ self,
192
+ method: str,
193
+ path: str,
194
+ request_model: type[BaseModel] | None = None,
195
+ response_model: type[BaseModel] | None = None,
196
+ **kwargs,
197
+ ) -> Any:
198
+ """Perform a mutating request (POST, PUT, PATCH, DELETE).
199
+
200
+ Args:
201
+ method: HTTP method.
202
+ path: URL path to request.
203
+ request_model: Optional Pydantic model for request validation.
204
+ response_model: Optional Pydantic model for response validation.
205
+ **kwargs: Additional arguments passed to requests.
206
+
207
+ Returns:
208
+ Parsed and optionally validated response data.
209
+ """
210
+ if kwargs.get('data') and request_model:
211
+ kwargs['data'] = self._validate_request(kwargs['data'], request_model)
212
+
213
+ response = self._request(method, path, **kwargs)
214
+
215
+ if response_model:
216
+ return self._validate_response(response, response_model)
217
+ return response
218
+
219
+ def _post(
220
+ self,
221
+ path: str,
222
+ request_model: type[BaseModel] | None = None,
223
+ response_model: type[BaseModel] | None = None,
224
+ **kwargs,
225
+ ) -> Any:
226
+ """Perform a POST request."""
227
+ return self._mutate('post', path, request_model, response_model, **kwargs)
228
+
229
+ def _put(
230
+ self,
231
+ path: str,
232
+ request_model: type[BaseModel] | None = None,
233
+ response_model: type[BaseModel] | None = None,
234
+ **kwargs,
235
+ ) -> Any:
236
+ """Perform a PUT request."""
237
+ return self._mutate('put', path, request_model, response_model, **kwargs)
238
+
239
+ def _patch(
240
+ self,
241
+ path: str,
242
+ request_model: type[BaseModel] | None = None,
243
+ response_model: type[BaseModel] | None = None,
244
+ **kwargs,
245
+ ) -> Any:
246
+ """Perform a PATCH request."""
247
+ return self._mutate('patch', path, request_model, response_model, **kwargs)
248
+
249
+ def _delete(
250
+ self,
251
+ path: str,
252
+ request_model: type[BaseModel] | None = None,
253
+ response_model: type[BaseModel] | None = None,
254
+ **kwargs,
255
+ ) -> Any:
256
+ """Perform a DELETE request."""
257
+ return self._mutate('delete', path, request_model, response_model, **kwargs)
258
+
259
+ def _list(
260
+ self,
261
+ path: str,
262
+ url_conversion: dict | None = None,
263
+ list_all: bool = False,
264
+ params: dict | None = None,
265
+ **kwargs,
266
+ ) -> dict | tuple[Any, int]:
267
+ """List resources from a paginated API endpoint.
268
+
269
+ Args:
270
+ path: URL path to request.
271
+ url_conversion: Optional URL conversion config for file paths.
272
+ list_all: If True, return a generator for all pages.
273
+ params: Optional query parameters.
274
+ **kwargs: Additional arguments passed to requests.
275
+
276
+ Returns:
277
+ Response dict, or tuple of (generator, count) if list_all=True.
278
+ """
279
+ if params is None:
280
+ params = {}
281
+
282
+ if list_all:
283
+ response = self._get(path, params=params, **kwargs)
284
+ return self._list_all(path, url_conversion, params=params, **kwargs), response.get('count')
285
+ return self._get(path, params=params, **kwargs)
286
+
287
+ def _list_all(self, path: str, url_conversion: dict | None = None, params: dict | None = None, **kwargs):
288
+ """Generator yielding all results from a paginated endpoint."""
289
+ if params is None:
290
+ params = {}
291
+
292
+ request_params = params.copy()
293
+ if 'page_size' not in request_params:
294
+ request_params['page_size'] = self.page_size
295
+
296
+ next_url = path
297
+ is_first_request = True
298
+
299
+ while next_url:
300
+ if is_first_request:
301
+ response = self._get(next_url, url_conversion, params=request_params, **kwargs)
302
+ is_first_request = False
303
+ else:
304
+ response = self._get(next_url, url_conversion, **kwargs)
305
+
306
+ yield from response['results']
307
+ next_url = response.get('next')
308
+
309
+ def exists(self, api: str, *args, **kwargs) -> bool:
310
+ """Check if any results exist for the given API method."""
311
+ return getattr(self, api)(*args, **kwargs)['count'] > 0
312
+
313
+
314
+ class AsyncBaseClient(ValidationMixin):
315
+ """Asynchronous HTTP client base using httpx.
316
+
317
+ This class provides a foundation for building async API clients with
318
+ connection management and request/response handling.
319
+
320
+ Attributes:
321
+ name: Client name for error messages.
322
+ page_size: Default page size for paginated requests.
323
+ """
324
+
325
+ name: str | None = None
326
+ page_size: int = 100
327
+
328
+ def __init__(
329
+ self,
330
+ base_url: str,
331
+ timeout: float | httpx.Timeout | None = None,
332
+ ):
333
+ """Initialize the async base client.
334
+
335
+ Args:
336
+ base_url: The base URL for all API requests.
337
+ timeout: Optional timeout configuration.
338
+ """
339
+ self.base_url = base_url.rstrip('/')
340
+ self.timeout = timeout if timeout is not None else httpx.Timeout(15.0, connect=5.0)
341
+ self._client: httpx.AsyncClient | None = None
52
342
 
53
- return self._post_response(response)
343
+ async def _get_client(self) -> httpx.AsyncClient:
344
+ """Get or create the async HTTP client."""
345
+ if self._client is None or self._client.is_closed:
346
+ self._client = httpx.AsyncClient(
347
+ base_url=self.base_url,
348
+ timeout=self.timeout,
349
+ )
350
+ return self._client
351
+
352
+ async def close(self) -> None:
353
+ """Close the HTTP client."""
354
+ if self._client and not self._client.is_closed:
355
+ await self._client.aclose()
356
+ self._client = None
357
+
358
+ async def __aenter__(self) -> AsyncBaseClient:
359
+ return self
360
+
361
+ async def __aexit__(self, *args) -> None:
362
+ await self.close()
363
+
364
+ def _get_headers(self) -> dict[str, str]:
365
+ """Return headers for requests. Override in subclasses."""
366
+ return {}
367
+
368
+ async def _request(self, method: str, path: str, **kwargs) -> dict | str | None:
369
+ """Request handler for all HTTP methods.
370
+
371
+ Args:
372
+ method: HTTP method (GET, POST, PUT, PATCH, DELETE).
373
+ path: URL path to request.
374
+ **kwargs: Additional arguments passed to httpx.
375
+
376
+ Returns:
377
+ Parsed response data.
378
+
379
+ Raises:
380
+ ClientTimeoutError: If the request times out.
381
+ ClientConnectionError: If connection fails.
382
+ HTTPError subclasses: For HTTP error responses.
383
+ """
384
+ client = await self._get_client()
385
+ # Handle full URLs (e.g., pagination next links) vs relative paths
386
+ if path.startswith(('http://', 'https://')):
387
+ url = path
388
+ else:
389
+ url = path.lstrip('/')
390
+ headers = self._get_headers()
391
+ headers.update(kwargs.pop('headers', {}))
54
392
 
55
- def _post_response(self, response):
56
393
  try:
57
- return response.json()
58
- except ValueError:
59
- return response.text
394
+ response = await client.request(method, url, headers=headers, **kwargs)
395
+ if not response.is_success:
396
+ raise_for_status(response.status_code, extract_error_detail(response))
397
+ except (
398
+ ClientError,
399
+ ClientTimeoutError,
400
+ ClientConnectionError,
401
+ ):
402
+ raise
403
+ except httpx.ConnectTimeout:
404
+ raise ClientTimeoutError(f'{self.name} connection timeout')
405
+ except httpx.ReadTimeout:
406
+ raise ClientTimeoutError(f'{self.name} read timeout')
407
+ except httpx.ConnectError as e:
408
+ raise ClientConnectionError(f'{self.name} connection error: {str(e)[:100]}')
409
+ except httpx.HTTPStatusError as e:
410
+ raise_for_status(e.response.status_code, extract_error_detail(e.response))
411
+ raise # unreachable, but helps type checker
412
+ except httpx.HTTPError as e:
413
+ raise ServerError(500, f'{self.name} request failed: {str(e)[:100]}')
60
414
 
61
- def _get(self, path, url_conversion=None, **kwargs):
62
- response = self._request('get', path, **kwargs)
63
- if url_conversion:
64
- if url_conversion['is_list']:
65
- files_url_to_path_from_objs(response['results'], **url_conversion)
415
+ return parse_json_response(response)
416
+
417
+ async def _get(
418
+ self,
419
+ path: str,
420
+ url_conversion: dict | None = None,
421
+ response_model: type[BaseModel] | None = None,
422
+ **kwargs,
423
+ ) -> Any:
424
+ """Perform a GET request.
425
+
426
+ Args:
427
+ path: URL path to request.
428
+ url_conversion: Optional URL conversion config for file paths.
429
+ response_model: Optional Pydantic model for response validation.
430
+ **kwargs: Additional arguments passed to httpx.
431
+
432
+ Returns:
433
+ Parsed and optionally validated response data.
434
+ """
435
+ response = await self._request('GET', path, **kwargs)
436
+
437
+ if url_conversion and isinstance(response, dict):
438
+ is_list = url_conversion.get('is_list', False)
439
+ if is_list:
440
+ files_url_to_path_from_objs(response['results'], **url_conversion, is_async=True)
66
441
  else:
67
442
  files_url_to_path_from_objs(response, **url_conversion)
443
+
444
+ if response_model:
445
+ return self._validate_response(response, response_model)
446
+ return response
447
+
448
+ async def _mutate(
449
+ self,
450
+ method: str,
451
+ path: str,
452
+ request_model: type[BaseModel] | None = None,
453
+ response_model: type[BaseModel] | None = None,
454
+ **kwargs,
455
+ ) -> Any:
456
+ """Perform a mutating request (POST, PUT, PATCH, DELETE).
457
+
458
+ Args:
459
+ method: HTTP method.
460
+ path: URL path to request.
461
+ request_model: Optional Pydantic model for request validation.
462
+ response_model: Optional Pydantic model for response validation.
463
+ **kwargs: Additional arguments passed to httpx.
464
+
465
+ Returns:
466
+ Parsed and optionally validated response data.
467
+ """
468
+ if kwargs.get('json') and request_model:
469
+ kwargs['json'] = self._validate_request(kwargs['json'], request_model)
470
+
471
+ response = await self._request(method, path, **kwargs)
472
+
473
+ if response_model:
474
+ return self._validate_response(response, response_model)
68
475
  return response
69
476
 
70
- def _post(self, path, **kwargs):
71
- return self._request('post', path, **kwargs)
477
+ async def _post(
478
+ self,
479
+ path: str,
480
+ request_model: type[BaseModel] | None = None,
481
+ response_model: type[BaseModel] | None = None,
482
+ **kwargs,
483
+ ) -> Any:
484
+ """Perform a POST request."""
485
+ return await self._mutate('POST', path, request_model, response_model, **kwargs)
486
+
487
+ async def _put(
488
+ self,
489
+ path: str,
490
+ request_model: type[BaseModel] | None = None,
491
+ response_model: type[BaseModel] | None = None,
492
+ **kwargs,
493
+ ) -> Any:
494
+ """Perform a PUT request."""
495
+ return await self._mutate('PUT', path, request_model, response_model, **kwargs)
496
+
497
+ async def _patch(
498
+ self,
499
+ path: str,
500
+ request_model: type[BaseModel] | None = None,
501
+ response_model: type[BaseModel] | None = None,
502
+ **kwargs,
503
+ ) -> Any:
504
+ """Perform a PATCH request."""
505
+ return await self._mutate('PATCH', path, request_model, response_model, **kwargs)
72
506
 
73
- def _put(self, path, **kwargs):
74
- return self._request('put', path, **kwargs)
507
+ async def _delete(
508
+ self,
509
+ path: str,
510
+ request_model: type[BaseModel] | None = None,
511
+ response_model: type[BaseModel] | None = None,
512
+ **kwargs,
513
+ ) -> Any:
514
+ """Perform a DELETE request."""
515
+ return await self._mutate('DELETE', path, request_model, response_model, **kwargs)
75
516
 
76
- def _patch(self, path, **kwargs):
77
- return self._request('patch', path, **kwargs)
517
+ async def _list(
518
+ self,
519
+ path: str,
520
+ url_conversion: dict | None = None,
521
+ list_all: bool = False,
522
+ params: dict | None = None,
523
+ **kwargs,
524
+ ) -> dict | tuple[Any, int]:
525
+ """List resources from a paginated API endpoint.
526
+
527
+ Args:
528
+ path: URL path to request.
529
+ url_conversion: Optional URL conversion config for file paths.
530
+ list_all: If True, return a generator for all pages.
531
+ params: Optional query parameters.
532
+ **kwargs: Additional arguments passed to httpx.
533
+
534
+ Returns:
535
+ Response dict, or tuple of (generator, count) if list_all=True.
536
+ """
537
+ if params is None:
538
+ params = {}
78
539
 
79
- def _list(self, path, url_conversion=None, list_all=False, **kwargs):
80
- response = self._get(path, url_conversion, **kwargs)
81
540
  if list_all:
82
- return self._list_all(path, url_conversion, **kwargs), response['count']
83
- else:
84
- return response
541
+ response = await self._get(path, params=params, **kwargs)
542
+ return self._list_all(path, url_conversion, params=params, **kwargs), response.get('count')
543
+ return await self._get(path, params=params, **kwargs)
85
544
 
86
- def _list_all(self, path, url_conversion=None, **kwargs):
87
- response = self._get(path, url_conversion, **kwargs)
88
- yield from response['results']
89
- if response['next']:
90
- yield from self._list_all(response['next'], url_conversion, **kwargs)
545
+ async def _list_all(
546
+ self,
547
+ path: str,
548
+ url_conversion: dict | None = None,
549
+ params: dict | None = None,
550
+ **kwargs,
551
+ ):
552
+ """Async generator yielding all results from a paginated endpoint."""
553
+ if params is None:
554
+ params = {}
91
555
 
92
- def exists(self, api, *args, **kwargs):
93
- return getattr(self, api)(*args, **kwargs)['count'] > 0
556
+ request_params = params.copy()
557
+ if 'page_size' not in request_params:
558
+ request_params['page_size'] = self.page_size
559
+
560
+ next_url: str | None = path
561
+ is_first_request = True
562
+
563
+ while next_url:
564
+ if is_first_request:
565
+ response = await self._get(next_url, url_conversion, params=request_params, **kwargs)
566
+ is_first_request = False
567
+ else:
568
+ response = await self._get(next_url, url_conversion, **kwargs)
569
+
570
+ for item in response['results']:
571
+ yield item
572
+ next_url = response.get('next')
@@ -1,8 +1,36 @@
1
- class ClientError(Exception):
2
- status = None
3
- reason = None
1
+ from __future__ import annotations
4
2
 
5
- def __init__(self, status, reason, *args):
6
- self.status = status
7
- self.reason = reason
8
- super().__init__(status, reason, *args)
3
+ # Re-export from the shared exceptions module for backwards compatibility
4
+ from synapse_sdk.exceptions import (
5
+ AuthenticationError,
6
+ AuthorizationError,
7
+ ClientConnectionError,
8
+ ClientError,
9
+ ClientTimeoutError,
10
+ HTTPError,
11
+ NotFoundError,
12
+ RateLimitError,
13
+ ServerError,
14
+ StreamError,
15
+ StreamLimitExceededError,
16
+ ValidationError,
17
+ WebSocketError,
18
+ raise_for_status,
19
+ )
20
+
21
+ __all__ = [
22
+ 'ClientError',
23
+ 'ClientConnectionError',
24
+ 'ClientTimeoutError',
25
+ 'HTTPError',
26
+ 'AuthenticationError',
27
+ 'AuthorizationError',
28
+ 'NotFoundError',
29
+ 'ValidationError',
30
+ 'RateLimitError',
31
+ 'ServerError',
32
+ 'StreamError',
33
+ 'StreamLimitExceededError',
34
+ 'WebSocketError',
35
+ 'raise_for_status',
36
+ ]
@@ -0,0 +1,5 @@
1
+ """Pipeline service client for dev-api communication."""
2
+
3
+ from synapse_sdk.clients.pipeline.client import PipelineServiceClient
4
+
5
+ __all__ = ['PipelineServiceClient']