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
@@ -0,0 +1,177 @@
1
+ """S3-compatible storage provider (AWS S3, MinIO)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from synapse_sdk.utils.storage.config import S3StorageConfig
9
+ from synapse_sdk.utils.storage.errors import (
10
+ StorageConnectionError,
11
+ StorageNotFoundError,
12
+ StorageUploadError,
13
+ )
14
+ from synapse_sdk.utils.storage.providers.base import _BaseStorageMixin
15
+
16
+ if TYPE_CHECKING:
17
+ from upath import UPath
18
+
19
+
20
+ class S3Storage(_BaseStorageMixin):
21
+ """Storage provider for S3-compatible services (AWS S3, MinIO).
22
+
23
+ Requires: universal-pathlib[s3] (pip install universal-pathlib[s3])
24
+
25
+ Args:
26
+ config: Configuration dict with S3 credentials.
27
+
28
+ Example:
29
+ >>> storage = S3Storage({
30
+ ... 'bucket_name': 'my-bucket',
31
+ ... 'access_key': 'AKIAIOSFODNN7EXAMPLE',
32
+ ... 'secret_key': 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
33
+ ... 'region_name': 'us-east-1',
34
+ ... })
35
+ >>> storage.upload(Path('/tmp/file.txt'), 'data/file.txt')
36
+ 's3://my-bucket/data/file.txt'
37
+ """
38
+
39
+ DEFAULT_REGION = 'us-east-1'
40
+
41
+ def __init__(self, config: dict[str, Any]):
42
+ try:
43
+ from upath import UPath
44
+ except ImportError as e:
45
+ raise ImportError(
46
+ 'S3Storage requires universal-pathlib[s3]. Install with: pip install universal-pathlib[s3]'
47
+ ) from e
48
+
49
+ validated = S3StorageConfig.model_validate(config)
50
+
51
+ self._bucket_name = validated.bucket_name
52
+ client_kwargs: dict[str, Any] = {
53
+ 'region_name': validated.region_name or self.DEFAULT_REGION,
54
+ }
55
+
56
+ if validated.endpoint_url:
57
+ client_kwargs['endpoint_url'] = validated.endpoint_url
58
+
59
+ try:
60
+ self._upath = UPath(
61
+ f's3://{validated.bucket_name}',
62
+ key=validated.access_key,
63
+ secret=validated.secret_key,
64
+ client_kwargs=client_kwargs,
65
+ )
66
+ except Exception as e:
67
+ raise StorageConnectionError(
68
+ f'Failed to connect to S3: {e}',
69
+ details={'bucket': validated.bucket_name},
70
+ ) from e
71
+
72
+ def upload(self, source: Path, target: str) -> str:
73
+ """Upload a file to S3.
74
+
75
+ Args:
76
+ source: Local file path to upload.
77
+ target: Target path in S3 bucket.
78
+
79
+ Returns:
80
+ s3:// URL of uploaded file.
81
+ """
82
+ source_path = Path(source) if isinstance(source, str) else source
83
+
84
+ if not source_path.exists():
85
+ raise StorageNotFoundError(
86
+ f'Source file not found: {source_path}',
87
+ details={'source': str(source_path)},
88
+ )
89
+
90
+ target_path = self._normalize_path(target)
91
+
92
+ try:
93
+ with open(source_path, 'rb') as f:
94
+ (self._upath / target_path).write_bytes(f.read())
95
+ except Exception as e:
96
+ raise StorageUploadError(
97
+ f'Failed to upload to S3: {e}',
98
+ details={'source': str(source_path), 'target': target_path},
99
+ ) from e
100
+
101
+ return self.get_url(target)
102
+
103
+ def exists(self, target: str) -> bool:
104
+ """Check if file exists in S3.
105
+
106
+ Args:
107
+ target: Path to check.
108
+
109
+ Returns:
110
+ True if exists, False otherwise.
111
+ """
112
+ target_path = self._normalize_path(target)
113
+ return (self._upath / target_path).exists()
114
+
115
+ def get_url(self, target: str) -> str:
116
+ """Get s3:// URL for target.
117
+
118
+ Args:
119
+ target: Target path.
120
+
121
+ Returns:
122
+ s3:// URL string.
123
+ """
124
+ target_path = self._normalize_path(target)
125
+ if target_path:
126
+ return f's3://{self._bucket_name}/{target_path}'
127
+ return f's3://{self._bucket_name}'
128
+
129
+ def get_pathlib(self, path: str) -> UPath:
130
+ """Get UPath object for path.
131
+
132
+ Args:
133
+ path: Path relative to bucket root.
134
+
135
+ Returns:
136
+ UPath object.
137
+ """
138
+ normalized = self._normalize_path(path)
139
+ if not normalized:
140
+ return self._upath
141
+ return self._upath / normalized
142
+
143
+ def get_path_file_count(self, pathlib_obj: UPath) -> int:
144
+ """Count files in S3 path.
145
+
146
+ Args:
147
+ pathlib_obj: UPath object from get_pathlib().
148
+
149
+ Returns:
150
+ Number of files.
151
+ """
152
+ return self._count_files(pathlib_obj)
153
+
154
+ def get_path_total_size(self, pathlib_obj: UPath) -> int:
155
+ """Calculate total size of files in S3 path.
156
+
157
+ Args:
158
+ pathlib_obj: UPath object from get_pathlib().
159
+
160
+ Returns:
161
+ Total size in bytes.
162
+ """
163
+ return self._calculate_total_size(pathlib_obj)
164
+
165
+ def glob(self, pattern: str) -> list[UPath]:
166
+ """Glob pattern matching in S3.
167
+
168
+ Args:
169
+ pattern: Glob pattern.
170
+
171
+ Returns:
172
+ List of matching UPath objects.
173
+ """
174
+ return list(self._upath.glob(pattern))
175
+
176
+
177
+ __all__ = ['S3Storage']
@@ -0,0 +1,208 @@
1
+ """SFTP storage provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from synapse_sdk.utils.storage.config import SFTPStorageConfig
9
+ from synapse_sdk.utils.storage.errors import (
10
+ StorageConnectionError,
11
+ StorageNotFoundError,
12
+ StorageUploadError,
13
+ )
14
+ from synapse_sdk.utils.storage.providers.base import _BaseStorageMixin
15
+
16
+ if TYPE_CHECKING:
17
+ from upath import UPath
18
+
19
+
20
+ class SFTPStorage(_BaseStorageMixin):
21
+ """Storage provider for SFTP servers.
22
+
23
+ Requires: universal-pathlib[sftp] (pip install universal-pathlib[sftp])
24
+
25
+ Supports both password and private key authentication.
26
+
27
+ Args:
28
+ config: Configuration dict with SFTP credentials.
29
+
30
+ Example:
31
+ >>> # Password authentication
32
+ >>> storage = SFTPStorage({
33
+ ... 'host': 'sftp.example.com',
34
+ ... 'username': 'user',
35
+ ... 'password': 'secret',
36
+ ... 'root_path': '/data',
37
+ ... })
38
+ >>>
39
+ >>> # Private key authentication
40
+ >>> storage = SFTPStorage({
41
+ ... 'host': 'sftp.example.com',
42
+ ... 'username': 'user',
43
+ ... 'private_key': '/path/to/id_rsa',
44
+ ... 'root_path': '/data',
45
+ ... })
46
+ """
47
+
48
+ def __init__(self, config: dict[str, Any]):
49
+ try:
50
+ from upath import UPath
51
+ except ImportError as e:
52
+ raise ImportError(
53
+ 'SFTPStorage requires universal-pathlib[sftp]. Install with: pip install universal-pathlib[sftp]'
54
+ ) from e
55
+
56
+ validated = SFTPStorageConfig.model_validate(config)
57
+ self._config = validated
58
+
59
+ # Build UPath kwargs based on auth method
60
+ upath_kwargs: dict[str, Any] = {
61
+ 'username': validated.username,
62
+ }
63
+
64
+ if validated.password:
65
+ upath_kwargs['password'] = validated.password
66
+
67
+ if validated.private_key:
68
+ upath_kwargs['key_filename'] = validated.private_key
69
+ if validated.private_key_passphrase:
70
+ upath_kwargs['passphrase'] = validated.private_key_passphrase
71
+
72
+ # Construct base URL with port if non-default
73
+ if validated.port != 22:
74
+ base_url = f'sftp://{validated.host}:{validated.port}'
75
+ else:
76
+ base_url = f'sftp://{validated.host}'
77
+
78
+ try:
79
+ self._base_upath = UPath(base_url, **upath_kwargs)
80
+ self._root_path = validated.root_path.rstrip('/')
81
+ self._host = validated.host
82
+ self._port = validated.port
83
+ except Exception as e:
84
+ raise StorageConnectionError(
85
+ f'Failed to connect to SFTP: {e}',
86
+ details={'host': validated.host, 'port': validated.port},
87
+ ) from e
88
+
89
+ def _get_full_path(self, path: str) -> UPath:
90
+ """Get full UPath including root_path."""
91
+ normalized = self._normalize_path(path)
92
+ if self._root_path:
93
+ full_path = f'{self._root_path}/{normalized}' if normalized else self._root_path
94
+ else:
95
+ full_path = normalized or '/'
96
+ return self._base_upath / full_path.lstrip('/')
97
+
98
+ def upload(self, source: Path, target: str) -> str:
99
+ """Upload a file via SFTP.
100
+
101
+ Args:
102
+ source: Local file path to upload.
103
+ target: Target path on SFTP server.
104
+
105
+ Returns:
106
+ sftp:// URL of uploaded file.
107
+ """
108
+ source_path = Path(source) if isinstance(source, str) else source
109
+
110
+ if not source_path.exists():
111
+ raise StorageNotFoundError(
112
+ f'Source file not found: {source_path}',
113
+ details={'source': str(source_path)},
114
+ )
115
+
116
+ target_upath = self._get_full_path(target)
117
+
118
+ try:
119
+ # Ensure parent directory exists
120
+ target_upath.parent.mkdir(parents=True, exist_ok=True)
121
+
122
+ with open(source_path, 'rb') as f:
123
+ target_upath.write_bytes(f.read())
124
+ except Exception as e:
125
+ raise StorageUploadError(
126
+ f'Failed to upload via SFTP: {e}',
127
+ details={'source': str(source_path), 'target': str(target_upath)},
128
+ ) from e
129
+
130
+ return self.get_url(target)
131
+
132
+ def exists(self, target: str) -> bool:
133
+ """Check if file exists on SFTP server.
134
+
135
+ Args:
136
+ target: Path to check.
137
+
138
+ Returns:
139
+ True if exists, False otherwise.
140
+ """
141
+ return self._get_full_path(target).exists()
142
+
143
+ def get_url(self, target: str) -> str:
144
+ """Get sftp:// URL for target.
145
+
146
+ Args:
147
+ target: Target path.
148
+
149
+ Returns:
150
+ sftp:// URL string.
151
+ """
152
+ normalized = self._normalize_path(target)
153
+ if self._root_path:
154
+ full_path = f'{self._root_path}/{normalized}' if normalized else self._root_path
155
+ else:
156
+ full_path = normalized or '/'
157
+
158
+ if self._port != 22:
159
+ return f'sftp://{self._host}:{self._port}{full_path}'
160
+ return f'sftp://{self._host}{full_path}'
161
+
162
+ def get_pathlib(self, path: str) -> UPath:
163
+ """Get UPath object for path.
164
+
165
+ Args:
166
+ path: Path relative to root_path.
167
+
168
+ Returns:
169
+ UPath object.
170
+ """
171
+ return self._get_full_path(path)
172
+
173
+ def get_path_file_count(self, pathlib_obj: UPath) -> int:
174
+ """Count files in SFTP path.
175
+
176
+ Args:
177
+ pathlib_obj: UPath object from get_pathlib().
178
+
179
+ Returns:
180
+ Number of files.
181
+ """
182
+ return self._count_files(pathlib_obj)
183
+
184
+ def get_path_total_size(self, pathlib_obj: UPath) -> int:
185
+ """Calculate total size of files in SFTP path.
186
+
187
+ Args:
188
+ pathlib_obj: UPath object from get_pathlib().
189
+
190
+ Returns:
191
+ Total size in bytes.
192
+ """
193
+ return self._calculate_total_size(pathlib_obj)
194
+
195
+ def glob(self, pattern: str) -> list[UPath]:
196
+ """Glob pattern matching on SFTP.
197
+
198
+ Args:
199
+ pattern: Glob pattern.
200
+
201
+ Returns:
202
+ List of matching UPath objects.
203
+ """
204
+ base = self._get_full_path('')
205
+ return list(base.glob(pattern))
206
+
207
+
208
+ __all__ = ['SFTPStorage']
@@ -0,0 +1,125 @@
1
+ """Storage provider registry with lazy loading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Callable
6
+
7
+ from synapse_sdk.utils.storage.errors import StorageProviderNotFoundError
8
+
9
+ if TYPE_CHECKING:
10
+ from synapse_sdk.utils.storage import StorageProtocol
11
+
12
+ # Registry stores factory functions for lazy loading
13
+ _PROVIDER_REGISTRY: dict[str, Callable[[], type[StorageProtocol]]] = {}
14
+
15
+
16
+ def _register_builtin_providers() -> None:
17
+ """Register built-in storage providers with lazy loading."""
18
+
19
+ def local_factory() -> type:
20
+ from synapse_sdk.utils.storage.providers.local import LocalStorage
21
+
22
+ return LocalStorage
23
+
24
+ def s3_factory() -> type:
25
+ from synapse_sdk.utils.storage.providers.s3 import S3Storage
26
+
27
+ return S3Storage
28
+
29
+ def gcs_factory() -> type:
30
+ from synapse_sdk.utils.storage.providers.gcs import GCSStorage
31
+
32
+ return GCSStorage
33
+
34
+ def sftp_factory() -> type:
35
+ from synapse_sdk.utils.storage.providers.sftp import SFTPStorage
36
+
37
+ return SFTPStorage
38
+
39
+ def http_factory() -> type:
40
+ from synapse_sdk.utils.storage.providers.http import HTTPStorage
41
+
42
+ return HTTPStorage
43
+
44
+ # Local filesystem
45
+ _PROVIDER_REGISTRY['local'] = local_factory
46
+ _PROVIDER_REGISTRY['file_system'] = local_factory # backward compatibility
47
+
48
+ # S3-compatible
49
+ _PROVIDER_REGISTRY['s3'] = s3_factory
50
+ _PROVIDER_REGISTRY['amazon_s3'] = s3_factory
51
+ _PROVIDER_REGISTRY['minio'] = s3_factory
52
+
53
+ # Google Cloud Storage
54
+ _PROVIDER_REGISTRY['gcs'] = gcs_factory
55
+ _PROVIDER_REGISTRY['gs'] = gcs_factory
56
+ _PROVIDER_REGISTRY['gcp'] = gcs_factory
57
+
58
+ # SFTP
59
+ _PROVIDER_REGISTRY['sftp'] = sftp_factory
60
+
61
+ # HTTP
62
+ _PROVIDER_REGISTRY['http'] = http_factory
63
+ _PROVIDER_REGISTRY['https'] = http_factory
64
+
65
+
66
+ def get_provider_class(provider: str) -> type[StorageProtocol]:
67
+ """Get the storage provider class for the given provider name.
68
+
69
+ Args:
70
+ provider: Provider name (e.g., 's3', 'gcs', 'local').
71
+
72
+ Returns:
73
+ Storage provider class.
74
+
75
+ Raises:
76
+ StorageProviderNotFoundError: If provider is not registered.
77
+ """
78
+ if not _PROVIDER_REGISTRY:
79
+ _register_builtin_providers()
80
+
81
+ factory = _PROVIDER_REGISTRY.get(provider)
82
+ if not factory:
83
+ available = ', '.join(sorted(_PROVIDER_REGISTRY.keys()))
84
+ raise StorageProviderNotFoundError(
85
+ f"Provider '{provider}' not found. Available: {available}",
86
+ details={'provider': provider, 'available': list(_PROVIDER_REGISTRY.keys())},
87
+ )
88
+
89
+ return factory()
90
+
91
+
92
+ def register_provider(name: str, factory: Callable[[], type[StorageProtocol]]) -> None:
93
+ """Register a custom storage provider.
94
+
95
+ Args:
96
+ name: Provider name to register.
97
+ factory: Factory function that returns the provider class.
98
+
99
+ Example:
100
+ >>> def custom_factory():
101
+ ... from my_module import CustomStorage
102
+ ... return CustomStorage
103
+ >>> register_provider('custom', custom_factory)
104
+ """
105
+ if not _PROVIDER_REGISTRY:
106
+ _register_builtin_providers()
107
+ _PROVIDER_REGISTRY[name] = factory
108
+
109
+
110
+ def get_registered_providers() -> list[str]:
111
+ """Get list of registered provider names.
112
+
113
+ Returns:
114
+ List of registered provider names.
115
+ """
116
+ if not _PROVIDER_REGISTRY:
117
+ _register_builtin_providers()
118
+ return list(_PROVIDER_REGISTRY.keys())
119
+
120
+
121
+ __all__ = [
122
+ 'get_provider_class',
123
+ 'register_provider',
124
+ 'get_registered_providers',
125
+ ]
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Generator
5
+
6
+ from synapse_sdk.clients.exceptions import ClientError
7
+
8
+
9
+ def stream_websocket(
10
+ url: str,
11
+ headers: dict[str, str] | None = None,
12
+ timeout: float = 30.0,
13
+ ) -> Generator[dict, None, None]:
14
+ """Stream raw events from a WebSocket connection.
15
+
16
+ Args:
17
+ url: WebSocket URL (ws:// or wss://).
18
+ headers: Optional headers dict.
19
+ timeout: Connection timeout in seconds.
20
+
21
+ Yields:
22
+ Parsed JSON events from the WebSocket.
23
+
24
+ Raises:
25
+ ClientError: On connection or protocol errors.
26
+ """
27
+ try:
28
+ import websocket
29
+ except ImportError:
30
+ raise ClientError(500, 'websocket-client package required for streaming')
31
+
32
+ header_list = [f'{k}: {v}' for k, v in (headers or {}).items()]
33
+
34
+ try:
35
+ ws = websocket.create_connection(url, header=header_list, timeout=timeout)
36
+ except websocket.WebSocketException as e:
37
+ raise ClientError(503, f'WebSocket connection failed: {e}')
38
+ except Exception as e:
39
+ raise ClientError(503, f'Connection error: {e}')
40
+
41
+ try:
42
+ while True:
43
+ try:
44
+ data = ws.recv()
45
+ except websocket.WebSocketTimeoutException:
46
+ break
47
+ except websocket.WebSocketConnectionClosedException:
48
+ break
49
+
50
+ if not data:
51
+ break
52
+
53
+ try:
54
+ event = json.loads(data)
55
+ except json.JSONDecodeError:
56
+ event = {'message': data}
57
+
58
+ yield event
59
+ finally:
60
+ ws.close()
61
+
62
+
63
+ def stream_websocket_logs(
64
+ url: str,
65
+ headers: dict[str, str] | None = None,
66
+ timeout: float = 30.0,
67
+ ) -> Generator[str, None, None]:
68
+ """Stream log messages from a WebSocket connection.
69
+
70
+ Handles the standard log streaming protocol:
71
+ - 'log' events: yields the message
72
+ - 'error' events: raises ClientError
73
+ - 'complete' events: stops iteration
74
+
75
+ Args:
76
+ url: WebSocket URL (ws:// or wss://).
77
+ headers: Optional headers dict.
78
+ timeout: Connection timeout in seconds.
79
+
80
+ Yields:
81
+ Log message strings.
82
+
83
+ Raises:
84
+ ClientError: On connection, protocol, or server errors.
85
+ """
86
+ for event in stream_websocket(url, headers, timeout):
87
+ match event.get('type'):
88
+ case 'error':
89
+ raise ClientError(500, event.get('message', 'Unknown error'))
90
+ case 'complete':
91
+ return
92
+ case _:
93
+ if msg := event.get('message'):
94
+ yield msg
95
+
96
+
97
+ def http_to_ws_url(url: str) -> str:
98
+ """Convert HTTP URL to WebSocket URL."""
99
+ return url.replace('http://', 'ws://').replace('https://', 'wss://')