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,89 +1,620 @@
1
+ """Plugin upload utilities for archiving and uploading plugins to storage."""
2
+
3
+ from __future__ import annotations
4
+
1
5
  import subprocess
2
6
  import tempfile
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass, field
9
+ from enum import StrEnum
3
10
  from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from synapse_sdk.plugins.errors import ArchiveError, BuildError, PluginUploadError
14
+ from synapse_sdk.utils.file.archive import create_archive_from_git, get_archive_size
15
+ from synapse_sdk.utils.file.checksum import calculate_checksum
16
+
17
+ if TYPE_CHECKING:
18
+ from synapse_sdk.utils.storage import StorageProtocol
19
+
20
+
21
+ # Progress callback signature: (stage, current, total)
22
+ UploadProgressCallback = Callable[[str, int, int], None]
23
+
24
+
25
+ class PackageManager(StrEnum):
26
+ """Supported package managers for building wheels."""
27
+
28
+ UV = 'uv'
29
+ POETRY = 'poetry'
30
+ PIP = 'pip'
31
+
32
+
33
+ class UploadStage(StrEnum):
34
+ """Upload operation stages for progress tracking."""
35
+
36
+ ARCHIVING = 'archiving'
37
+ CHECKSUMMING = 'checksumming'
38
+ BUILDING = 'building'
39
+ UPLOADING = 'uploading'
40
+ VERIFYING = 'verifying'
41
+
42
+
43
+ @dataclass
44
+ class UploadResult:
45
+ """Result of a plugin upload operation.
46
+
47
+ Attributes:
48
+ url: Storage URL of uploaded file.
49
+ checksum: MD5 checksum of uploaded file.
50
+ filename: Name of uploaded file.
51
+ size: Size in bytes.
52
+ is_cached: True if file already existed in storage.
53
+ """
54
+
55
+ url: str
56
+ checksum: str
57
+ filename: str
58
+ size: int
59
+ is_cached: bool = False
60
+
61
+
62
+ @dataclass
63
+ class BuildConfig:
64
+ """Configuration for wheel building.
65
+
66
+ Attributes:
67
+ package_manager: Build tool to use (uv, poetry, pip).
68
+ python_path: Path to Python interpreter (auto-detected if None).
69
+ extra_args: Additional arguments to pass to build command.
70
+ """
71
+
72
+ package_manager: PackageManager = PackageManager.UV
73
+ python_path: Path | None = None
74
+ extra_args: list[str] = field(default_factory=list)
75
+
76
+
77
+ def _get_storage(storage: StorageProtocol | dict[str, Any]) -> StorageProtocol:
78
+ """Convert storage config to StorageProtocol instance.
79
+
80
+ Args:
81
+ storage: StorageProtocol instance or config dict.
82
+
83
+ Returns:
84
+ StorageProtocol instance.
85
+
86
+ Raises:
87
+ PluginUploadError: If storage configuration is invalid.
88
+ """
89
+ # Check if it's already a StorageProtocol instance
90
+ # We check for the presence of required methods
91
+ if hasattr(storage, 'upload') and hasattr(storage, 'exists') and hasattr(storage, 'get_url'):
92
+ return storage # type: ignore[return-value]
93
+
94
+ # Convert dict config to storage instance
95
+ if isinstance(storage, dict):
96
+ from synapse_sdk.utils.storage import get_storage
97
+
98
+ try:
99
+ return get_storage(storage)
100
+ except Exception as e:
101
+ raise PluginUploadError(
102
+ f'Invalid storage configuration: {e}',
103
+ details={'config': storage},
104
+ ) from e
105
+
106
+ raise PluginUploadError(
107
+ 'Invalid storage type. Expected StorageProtocol or dict.',
108
+ details={'type': type(storage).__name__},
109
+ )
110
+
111
+
112
+ def _report_progress(
113
+ callback: UploadProgressCallback | None,
114
+ stage: UploadStage,
115
+ current: int,
116
+ total: int,
117
+ ) -> None:
118
+ """Report progress if callback is provided."""
119
+ if callback:
120
+ callback(stage.value, current, total)
121
+
122
+
123
+ def _get_build_command(config: BuildConfig, source_path: Path) -> list[str]:
124
+ """Generate build command based on package manager.
125
+
126
+ Args:
127
+ config: Build configuration.
128
+ source_path: Plugin source directory.
129
+
130
+ Returns:
131
+ Command as list of strings.
132
+
133
+ Raises:
134
+ BuildError: If package manager is not supported.
135
+ """
136
+ python = str(config.python_path) if config.python_path else 'python'
137
+
138
+ match config.package_manager:
139
+ case PackageManager.UV:
140
+ return ['uv', 'build', '--wheel', *config.extra_args]
141
+ case PackageManager.POETRY:
142
+ return ['poetry', 'build', '--format', 'wheel', *config.extra_args]
143
+ case PackageManager.PIP:
144
+ return [python, '-m', 'build', '--wheel', *config.extra_args]
145
+ case _:
146
+ raise BuildError(
147
+ f'Unsupported package manager: {config.package_manager}',
148
+ details={'package_manager': config.package_manager},
149
+ )
150
+
151
+
152
+ def archive_plugin(
153
+ source_path: str | Path,
154
+ archive_path: str | Path | None = None,
155
+ *,
156
+ use_git: bool = True,
157
+ progress_callback: UploadProgressCallback | None = None,
158
+ ) -> tuple[Path, str]:
159
+ """Archive a plugin directory.
160
+
161
+ Creates a ZIP archive of the plugin source code. When use_git=True,
162
+ uses git ls-files to determine which files to include.
163
+
164
+ Args:
165
+ source_path: Plugin source directory.
166
+ archive_path: Output path (auto-generated in temp dir if None).
167
+ use_git: Use git ls-files for file selection.
168
+ progress_callback: Optional progress callback.
169
+
170
+ Returns:
171
+ Tuple of (archive_path, checksum).
172
+
173
+ Raises:
174
+ ArchiveError: If archiving fails.
175
+ FileNotFoundError: If source_path does not exist.
176
+
177
+ Example:
178
+ >>> archive_path, checksum = archive_plugin('/path/to/plugin')
179
+ >>> print(f'Created {archive_path} with checksum {checksum}')
180
+ """
181
+ source = Path(source_path).resolve()
182
+
183
+ if not source.exists():
184
+ raise FileNotFoundError(f'Source path not found: {source}')
185
+
186
+ # Generate archive path if not provided
187
+ if archive_path is None:
188
+ temp_dir = tempfile.mkdtemp()
189
+ archive = Path(temp_dir) / 'archive.zip'
190
+ else:
191
+ archive = Path(archive_path).resolve()
192
+
193
+ _report_progress(progress_callback, UploadStage.ARCHIVING, 0, 100)
194
+
195
+ try:
196
+ if use_git:
197
+ create_archive_from_git(source, archive)
198
+ else:
199
+ from synapse_sdk.utils.file.archive import create_archive
200
+
201
+ create_archive(source, archive)
202
+ except Exception as e:
203
+ raise ArchiveError(
204
+ f'Failed to create archive: {e}',
205
+ details={'source': str(source), 'archive': str(archive)},
206
+ ) from e
207
+
208
+ _report_progress(progress_callback, UploadStage.ARCHIVING, 100, 100)
209
+
210
+ # Calculate checksum
211
+ _report_progress(progress_callback, UploadStage.CHECKSUMMING, 0, 1)
212
+ checksum = calculate_checksum(archive)
213
+ _report_progress(progress_callback, UploadStage.CHECKSUMMING, 1, 1)
4
214
 
5
- from synapse_sdk.i18n import gettext as _
6
- from synapse_sdk.utils.file import calculate_checksum, download_file
7
- from synapse_sdk.utils.storage import get_storage
215
+ return archive, checksum
8
216
 
9
217
 
10
- def archive(source_path, archive_path):
11
- archive_path.parent.mkdir(parents=True, exist_ok=True)
12
- command = f'git ls-files --others --exclude-standard --cached | zip -q --names-stdin {archive_path}'
13
- subprocess.run(command, cwd=source_path, shell=True, check=True, stdout=subprocess.DEVNULL)
218
+ def archive_and_upload(
219
+ source_path: str | Path,
220
+ storage: StorageProtocol | dict[str, Any],
221
+ *,
222
+ target_prefix: str = '',
223
+ use_git: bool = True,
224
+ skip_existing: bool = True,
225
+ progress_callback: UploadProgressCallback | None = None,
226
+ ) -> UploadResult:
227
+ """Archive plugin and upload to storage.
14
228
 
229
+ Creates a ZIP archive with checksum-based naming (dev-{checksum}.zip).
230
+ If skip_existing=True and file exists in storage, returns cached URL.
15
231
 
16
- def download_and_upload(source_url, url):
17
- storage = get_storage(url)
18
- with tempfile.TemporaryDirectory() as temp_path:
19
- file_path = str(download_file(source_url, temp_path))
20
- checksum = calculate_checksum(file_path)
21
- # TODO 중복 체크
22
- return storage.upload(file_path, f'dev-{checksum}.zip')
232
+ Args:
233
+ source_path: Plugin source directory.
234
+ storage: Storage provider or config dict.
235
+ target_prefix: Optional prefix for target path.
236
+ use_git: Use git ls-files for file selection.
237
+ skip_existing: Skip upload if file exists in storage.
238
+ progress_callback: Optional progress callback.
23
239
 
240
+ Returns:
241
+ UploadResult with URL, checksum, and metadata.
24
242
 
25
- def archive_and_upload(source_path, url):
26
- storage = get_storage(url)
27
- dist_path = Path(source_path, 'dist')
28
- archive_path = dist_path / 'archive.zip'
243
+ Raises:
244
+ ArchiveError: If archiving fails.
245
+ PluginUploadError: If upload fails.
29
246
 
30
- archive(source_path, archive_path)
31
- checksum = calculate_checksum(archive_path)
32
- checksum_archive_path = dist_path / f'dev-{checksum}.zip'
247
+ Example:
248
+ >>> result = archive_and_upload(
249
+ ... '/path/to/plugin',
250
+ ... {'provider': 's3', 'configuration': {...}},
251
+ ... )
252
+ >>> print(result.url)
253
+ """
254
+ storage_provider = _get_storage(storage)
255
+ source = Path(source_path).resolve()
33
256
 
34
- if checksum_archive_path.exists():
35
- # TODO 실제 스토리지 있는지 확인
36
- return storage.get_url(checksum_archive_path.name)
257
+ # Create archive and get checksum
258
+ archive_path, checksum = archive_plugin(
259
+ source,
260
+ use_git=use_git,
261
+ progress_callback=progress_callback,
262
+ )
263
+
264
+ # Build target filename with checksum
265
+ filename = f'dev-{checksum}.zip'
266
+ target_path = f'{target_prefix}{filename}' if target_prefix else filename
267
+
268
+ try:
269
+ # Check if already exists in storage
270
+ if skip_existing and storage_provider.exists(target_path):
271
+ url = storage_provider.get_url(target_path)
272
+ return UploadResult(
273
+ url=url,
274
+ checksum=checksum,
275
+ filename=filename,
276
+ size=get_archive_size(archive_path),
277
+ is_cached=True,
278
+ )
279
+
280
+ # Upload to storage
281
+ _report_progress(progress_callback, UploadStage.UPLOADING, 0, 100)
282
+ url = storage_provider.upload(archive_path, target_path)
283
+ _report_progress(progress_callback, UploadStage.UPLOADING, 100, 100)
284
+
285
+ return UploadResult(
286
+ url=url,
287
+ checksum=checksum,
288
+ filename=filename,
289
+ size=get_archive_size(archive_path),
290
+ is_cached=False,
291
+ )
292
+
293
+ except Exception as e:
294
+ if isinstance(e, PluginUploadError):
295
+ raise
296
+ raise PluginUploadError(
297
+ f'Failed to upload archive: {e}',
298
+ details={'target': target_path},
299
+ ) from e
300
+ finally:
301
+ # Clean up temp archive
302
+ if archive_path.exists():
303
+ archive_path.unlink(missing_ok=True)
304
+
305
+
306
+ def modify_wheel_build_tag(
307
+ wheel_path: str | Path,
308
+ build_tag: str,
309
+ ) -> Path:
310
+ """Modify wheel filename to embed build tag (checksum).
311
+
312
+ Converts: package-1.0.0-py3-none-any.whl
313
+ To: package-1.0.0+{build_tag}-py3-none-any.whl
314
+
315
+ Args:
316
+ wheel_path: Path to wheel file.
317
+ build_tag: Build tag to embed (typically checksum).
318
+
319
+ Returns:
320
+ Path to renamed wheel file.
321
+
322
+ Raises:
323
+ ValueError: If wheel filename format is invalid.
324
+
325
+ Example:
326
+ >>> new_path = modify_wheel_build_tag('/path/to/pkg-1.0.0-py3-none-any.whl', 'abc123')
327
+ >>> print(new_path.name)
328
+ 'pkg-1.0.0+abc123-py3-none-any.whl'
329
+ """
330
+ path = Path(wheel_path)
37
331
 
38
- archive_path.rename(checksum_archive_path)
39
- for file_path in dist_path.glob('*.zip'):
40
- if file_path.name != checksum_archive_path.name:
41
- file_path.unlink()
42
- return storage.upload(str(checksum_archive_path), checksum_archive_path.name)
332
+ # Wheel filename format: {name}-{version}[-{build}]-{python}-{abi}-{platform}.whl
333
+ # Minimum components: name-version-python-abi-platform.whl = 5 parts
334
+ parts = path.stem.split('-')
43
335
 
336
+ if len(parts) < 5:
337
+ raise ValueError(f'Invalid wheel filename format: {path.name}')
44
338
 
45
- def build_and_upload(source_path, url, virtualenv_path='.venv'):
46
- storage = get_storage(url)
47
- dist_path = Path(source_path, 'dist')
48
- archive_path = dist_path / 'archive.zip'
339
+ # Version is always the second part
340
+ # It may already contain a build tag (after +)
341
+ version = parts[1].split('+')[0]
49
342
 
50
- archive(source_path, archive_path)
51
- checksum = calculate_checksum(archive_path)
52
- checksum_archive_path = dist_path / f'dev-{checksum}.zip'
343
+ # Insert build tag into version
344
+ parts[1] = f'{version}+{build_tag}'
53
345
 
54
- if checksum_archive_path.exists():
55
- # TODO 실제 스토리지 있는지 확인
56
- wheel_path = next(dist_path.glob('*.whl'), None)
57
- return storage.get_url(wheel_path.name)
346
+ # Reconstruct filename
347
+ new_name = '-'.join(parts) + '.whl'
348
+ new_path = path.parent / new_name
58
349
 
59
- # wheel file 빌드 진행
60
- for file_path in dist_path.glob('*.whl'):
61
- file_path.unlink()
350
+ # Rename the file
351
+ path.rename(new_path)
62
352
 
63
- print(_('Building {}...').format(Path(source_path).name))
64
- subprocess.run(
65
- f'{virtualenv_path}/bin/python -m build --wheel',
66
- cwd=source_path,
67
- shell=True,
68
- check=True,
69
- stdout=subprocess.DEVNULL,
353
+ return new_path
354
+
355
+
356
+ def build_and_upload(
357
+ source_path: str | Path,
358
+ storage: StorageProtocol | dict[str, Any],
359
+ *,
360
+ build_config: BuildConfig | None = None,
361
+ target_prefix: str = '',
362
+ skip_existing: bool = True,
363
+ progress_callback: UploadProgressCallback | None = None,
364
+ ) -> UploadResult:
365
+ """Build wheel and upload to storage.
366
+
367
+ Creates archive, calculates checksum, builds wheel, embeds checksum
368
+ in wheel filename build tag, and uploads to storage.
369
+
370
+ Args:
371
+ source_path: Plugin source directory with pyproject.toml.
372
+ storage: Storage provider or config dict.
373
+ build_config: Build configuration (defaults to uv).
374
+ target_prefix: Optional prefix for target path.
375
+ skip_existing: Skip upload if file exists in storage.
376
+ progress_callback: Optional progress callback.
377
+
378
+ Returns:
379
+ UploadResult with wheel URL, checksum, and metadata.
380
+
381
+ Raises:
382
+ BuildError: If wheel build fails.
383
+ PluginUploadError: If upload fails.
384
+
385
+ Example:
386
+ >>> result = build_and_upload(
387
+ ... '/path/to/plugin',
388
+ ... {'provider': 's3', 'configuration': {...}},
389
+ ... build_config=BuildConfig(package_manager=PackageManager.UV),
390
+ ... )
391
+ >>> print(result.url)
392
+ """
393
+ storage_provider = _get_storage(storage)
394
+ source = Path(source_path).resolve()
395
+
396
+ if build_config is None:
397
+ build_config = BuildConfig()
398
+
399
+ # Check for pyproject.toml
400
+ if not (source / 'pyproject.toml').exists():
401
+ raise BuildError(
402
+ 'No pyproject.toml found in source directory',
403
+ details={'source': str(source)},
404
+ )
405
+
406
+ # Create archive and get checksum
407
+ archive_path, checksum = archive_plugin(
408
+ source,
409
+ use_git=True,
410
+ progress_callback=progress_callback,
70
411
  )
71
- wheel_path = next(dist_path.glob('*.whl'), None)
72
412
 
73
- # whl 파일 버전이 동일한 이슈를 해결하기 위해 checksum으로 build명 변경
74
- checksum_wheel_path = wheel_path.with_name(change_build_from_whl_name(wheel_path.name, checksum))
75
- wheel_path.rename(checksum_wheel_path)
413
+ # Check if already exists in storage (use checksum-based wheel name pattern)
414
+ # The wheel name will contain the checksum as build tag
415
+ if skip_existing:
416
+ # Try to find existing wheel with this checksum
417
+ # Format: name-version+checksum-py3-none-any.whl
418
+ # We can't know the exact name without building, so we check after build
419
+ pass
420
+
421
+ # Build wheel
422
+ _report_progress(progress_callback, UploadStage.BUILDING, 0, 100)
423
+
424
+ dist_dir = source / 'dist'
425
+ dist_dir.mkdir(exist_ok=True)
426
+
427
+ # Clean existing wheel files
428
+ for whl_file in dist_dir.glob('*.whl'):
429
+ whl_file.unlink()
430
+
431
+ build_cmd = _get_build_command(build_config, source)
432
+
433
+ try:
434
+ subprocess.run(
435
+ build_cmd,
436
+ cwd=source,
437
+ check=True,
438
+ capture_output=True,
439
+ text=True,
440
+ )
441
+ except subprocess.CalledProcessError as e:
442
+ raise BuildError(
443
+ f'Wheel build failed: {e.stderr}',
444
+ details={
445
+ 'command': ' '.join(build_cmd),
446
+ 'returncode': e.returncode,
447
+ 'stdout': e.stdout,
448
+ 'stderr': e.stderr,
449
+ },
450
+ ) from e
451
+ except FileNotFoundError as e:
452
+ raise BuildError(
453
+ f'Build command not found: {build_cmd[0]}. Is it installed?',
454
+ details={'command': build_cmd[0]},
455
+ ) from e
456
+
457
+ _report_progress(progress_callback, UploadStage.BUILDING, 100, 100)
458
+
459
+ # Find built wheel
460
+ wheel_files = list(dist_dir.glob('*.whl'))
461
+ if not wheel_files:
462
+ raise BuildError(
463
+ 'No wheel file found after build',
464
+ details={'dist_dir': str(dist_dir)},
465
+ )
466
+
467
+ wheel_path = wheel_files[0]
468
+
469
+ # Embed checksum in wheel filename
470
+ wheel_path = modify_wheel_build_tag(wheel_path, checksum)
471
+
472
+ filename = wheel_path.name
473
+ target_path = f'{target_prefix}{filename}' if target_prefix else filename
474
+
475
+ try:
476
+ # Check if already exists
477
+ if skip_existing and storage_provider.exists(target_path):
478
+ url = storage_provider.get_url(target_path)
479
+ return UploadResult(
480
+ url=url,
481
+ checksum=checksum,
482
+ filename=filename,
483
+ size=wheel_path.stat().st_size,
484
+ is_cached=True,
485
+ )
486
+
487
+ # Upload wheel
488
+ _report_progress(progress_callback, UploadStage.UPLOADING, 0, 100)
489
+ url = storage_provider.upload(wheel_path, target_path)
490
+ _report_progress(progress_callback, UploadStage.UPLOADING, 100, 100)
491
+
492
+ return UploadResult(
493
+ url=url,
494
+ checksum=checksum,
495
+ filename=filename,
496
+ size=wheel_path.stat().st_size,
497
+ is_cached=False,
498
+ )
499
+
500
+ except Exception as e:
501
+ if isinstance(e, (PluginUploadError, BuildError)):
502
+ raise
503
+ raise PluginUploadError(
504
+ f'Failed to upload wheel: {e}',
505
+ details={'target': target_path},
506
+ ) from e
507
+ finally:
508
+ # Clean up temp archive
509
+ if archive_path.exists():
510
+ archive_path.unlink(missing_ok=True)
511
+
512
+
513
+ def download_and_upload(
514
+ source_url: str,
515
+ storage: StorageProtocol | dict[str, Any],
516
+ *,
517
+ target_prefix: str = '',
518
+ skip_existing: bool = True,
519
+ progress_callback: UploadProgressCallback | None = None,
520
+ ) -> UploadResult:
521
+ """Download file from URL and upload to storage.
522
+
523
+ Downloads the file, calculates checksum, and re-uploads with
524
+ checksum-based naming to the target storage.
525
+
526
+ Args:
527
+ source_url: URL to download from.
528
+ storage: Storage provider or config dict.
529
+ target_prefix: Optional prefix for target path.
530
+ skip_existing: Skip upload if file exists in storage.
531
+ progress_callback: Optional progress callback.
532
+
533
+ Returns:
534
+ UploadResult with storage URL, checksum, and metadata.
535
+
536
+ Raises:
537
+ PluginUploadError: If download or upload fails.
538
+
539
+ Example:
540
+ >>> result = download_and_upload(
541
+ ... 'https://example.com/plugin.zip',
542
+ ... {'provider': 's3', 'configuration': {...}},
543
+ ... )
544
+ >>> print(result.url)
545
+ """
546
+ from synapse_sdk.utils.file.download import download_file
547
+
548
+ storage_provider = _get_storage(storage)
549
+
550
+ with tempfile.TemporaryDirectory() as temp_dir:
551
+ temp_path = Path(temp_dir)
552
+
553
+ # Download file
554
+ try:
555
+ downloaded_path = download_file(source_url, temp_path)
556
+ except Exception as e:
557
+ raise PluginUploadError(
558
+ f'Failed to download file: {e}',
559
+ details={'url': source_url},
560
+ ) from e
561
+
562
+ # Calculate checksum
563
+ _report_progress(progress_callback, UploadStage.CHECKSUMMING, 0, 1)
564
+ checksum = calculate_checksum(downloaded_path)
565
+ _report_progress(progress_callback, UploadStage.CHECKSUMMING, 1, 1)
566
+
567
+ # Build target filename with checksum
568
+ filename = f'dev-{checksum}.zip'
569
+ target_path = f'{target_prefix}{filename}' if target_prefix else filename
570
+
571
+ try:
572
+ # Check if already exists
573
+ if skip_existing and storage_provider.exists(target_path):
574
+ url = storage_provider.get_url(target_path)
575
+ return UploadResult(
576
+ url=url,
577
+ checksum=checksum,
578
+ filename=filename,
579
+ size=downloaded_path.stat().st_size,
580
+ is_cached=True,
581
+ )
582
+
583
+ # Upload to storage
584
+ _report_progress(progress_callback, UploadStage.UPLOADING, 0, 100)
585
+ url = storage_provider.upload(downloaded_path, target_path)
586
+ _report_progress(progress_callback, UploadStage.UPLOADING, 100, 100)
76
587
 
77
- archive_path.rename(checksum_archive_path)
588
+ return UploadResult(
589
+ url=url,
590
+ checksum=checksum,
591
+ filename=filename,
592
+ size=downloaded_path.stat().st_size,
593
+ is_cached=False,
594
+ )
78
595
 
79
- for file_path in dist_path.glob('*.zip'):
80
- if file_path.name != checksum_archive_path.name:
81
- file_path.unlink()
82
- return storage.upload(str(checksum_wheel_path), checksum_wheel_path.name)
596
+ except Exception as e:
597
+ if isinstance(e, PluginUploadError):
598
+ raise
599
+ raise PluginUploadError(
600
+ f'Failed to upload file: {e}',
601
+ details={'target': target_path},
602
+ ) from e
83
603
 
84
604
 
85
- def change_build_from_whl_name(whl_name, build):
86
- components = whl_name.split('-')
87
- version = components[1].split('+')[0]
88
- components[1] = f'{version}+{build}'
89
- return '-'.join(components)
605
+ __all__ = [
606
+ # Enums
607
+ 'PackageManager',
608
+ 'UploadStage',
609
+ # Dataclasses
610
+ 'UploadResult',
611
+ 'BuildConfig',
612
+ # Type aliases
613
+ 'UploadProgressCallback',
614
+ # Functions
615
+ 'archive_plugin',
616
+ 'archive_and_upload',
617
+ 'build_and_upload',
618
+ 'download_and_upload',
619
+ 'modify_wheel_build_tag',
620
+ ]