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,790 @@
1
+ """Plugin discovery and introspection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import inspect
7
+ from pathlib import Path
8
+ from types import ModuleType
9
+ from typing import TYPE_CHECKING, Any, Callable
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from synapse_sdk.plugins.config import ActionConfig, PluginConfig
14
+ from synapse_sdk.plugins.enums import PluginCategory, RunMethod
15
+ from synapse_sdk.plugins.errors import ActionNotFoundError
16
+ from synapse_sdk.plugins.utils import pydantic_to_ui_schema
17
+
18
+ if TYPE_CHECKING:
19
+ from synapse_sdk.plugins.action import BaseAction
20
+
21
+
22
+ class PluginDiscovery:
23
+ """Plugin discovery and introspection.
24
+
25
+ Provides methods to discover actions from configuration files or Python modules.
26
+ Supports both class-based (BaseAction subclasses) and function-based (@action decorator)
27
+ action definitions.
28
+
29
+ Example from config:
30
+ >>> discovery = PluginDiscovery.from_path('/path/to/plugin')
31
+ >>> discovery.list_actions()
32
+ ['train', 'inference', 'export']
33
+ >>> action_cls = discovery.get_action_class('train')
34
+
35
+ Example from module:
36
+ >>> import my_plugin
37
+ >>> discovery = PluginDiscovery.from_module(my_plugin)
38
+ >>> discovery.list_actions()
39
+ ['train', 'export']
40
+ """
41
+
42
+ def __init__(self, config: PluginConfig) -> None:
43
+ """Initialize discovery with a plugin configuration.
44
+
45
+ Args:
46
+ config: Validated PluginConfig instance
47
+ """
48
+ self.config = config
49
+ self._action_cache: dict[str, type[BaseAction] | Callable] = {}
50
+
51
+ @classmethod
52
+ def from_path(cls, path: Path | str) -> PluginDiscovery:
53
+ """Load plugin from config.yaml path.
54
+
55
+ Args:
56
+ path: Path to config.yaml file or directory containing it
57
+
58
+ Returns:
59
+ PluginDiscovery instance
60
+
61
+ Raises:
62
+ FileNotFoundError: If config.yaml doesn't exist
63
+ ValueError: If config.yaml is invalid
64
+ """
65
+ import yaml
66
+
67
+ path = Path(path)
68
+ if path.is_dir():
69
+ path = path / 'config.yaml'
70
+
71
+ if not path.exists():
72
+ raise FileNotFoundError(f'Config file not found: {path}')
73
+
74
+ with path.open() as f:
75
+ data = yaml.safe_load(f)
76
+
77
+ # Convert actions dict to ActionConfig objects if needed
78
+ if 'actions' in data and isinstance(data['actions'], dict):
79
+ actions = {}
80
+ for action_name, action_data in data['actions'].items():
81
+ if isinstance(action_data, dict):
82
+ action_data.setdefault('name', action_name)
83
+ actions[action_name] = ActionConfig(**action_data)
84
+ elif isinstance(action_data, ActionConfig):
85
+ actions[action_name] = action_data
86
+ data['actions'] = actions
87
+
88
+ config = PluginConfig(**data)
89
+ return cls(config)
90
+
91
+ @classmethod
92
+ def from_module(
93
+ cls,
94
+ module: ModuleType,
95
+ *,
96
+ name: str | None = None,
97
+ category: PluginCategory = PluginCategory.CUSTOM,
98
+ ) -> PluginDiscovery:
99
+ """Discover plugin from Python module by introspection.
100
+
101
+ Scans module for:
102
+ - Functions decorated with @action
103
+ - Classes that subclass BaseAction
104
+
105
+ Args:
106
+ module: Python module to introspect
107
+ name: Plugin name (defaults to module name)
108
+ category: Plugin category
109
+
110
+ Returns:
111
+ PluginDiscovery instance with discovered actions
112
+ """
113
+ from synapse_sdk.plugins.action import BaseAction
114
+
115
+ actions: dict[str, ActionConfig] = {}
116
+
117
+ for attr_name in dir(module):
118
+ if attr_name.startswith('_'):
119
+ continue
120
+
121
+ obj = getattr(module, attr_name)
122
+
123
+ # Check for @action decorated functions
124
+ if callable(obj) and hasattr(obj, '_is_action') and obj._is_action:
125
+ action_name = getattr(obj, '_action_name', attr_name)
126
+ actions[action_name] = ActionConfig(
127
+ name=action_name,
128
+ description=getattr(obj, '_action_description', ''),
129
+ entrypoint=f'{module.__name__}:{attr_name}',
130
+ method=RunMethod.TASK,
131
+ params_schema=getattr(obj, '_action_params', None),
132
+ )
133
+
134
+ # Check for BaseAction subclasses
135
+ elif (
136
+ inspect.isclass(obj)
137
+ and issubclass(obj, BaseAction)
138
+ and obj is not BaseAction
139
+ and hasattr(obj, 'action_name')
140
+ ):
141
+ action_name = obj.action_name
142
+ actions[action_name] = ActionConfig(
143
+ name=action_name,
144
+ description=getattr(obj, 'description', ''),
145
+ entrypoint=f'{module.__name__}:{attr_name}',
146
+ method=getattr(obj, 'method', RunMethod.TASK),
147
+ params_schema=getattr(obj, 'params_model', None),
148
+ )
149
+
150
+ module_name = name or getattr(module, '__name__', 'unknown')
151
+ config = PluginConfig(
152
+ name=module_name,
153
+ code=module_name.replace('.', '-'),
154
+ category=category,
155
+ actions=actions,
156
+ )
157
+ return cls(config)
158
+
159
+ def list_actions(self) -> list[str]:
160
+ """Get available action names.
161
+
162
+ Returns:
163
+ List of action names
164
+ """
165
+ return list(self.config.actions.keys())
166
+
167
+ def get_action_config(self, name: str) -> ActionConfig:
168
+ """Get configuration for a specific action.
169
+
170
+ Args:
171
+ name: Action name
172
+
173
+ Returns:
174
+ ActionConfig instance
175
+
176
+ Raises:
177
+ ActionNotFoundError: If action doesn't exist
178
+ """
179
+ if name not in self.config.actions:
180
+ raise ActionNotFoundError(
181
+ f"Action '{name}' not found",
182
+ details={'available': self.list_actions()},
183
+ )
184
+ return self.config.actions[name]
185
+
186
+ def get_action_method(self, name: str) -> RunMethod:
187
+ """Get execution method for an action.
188
+
189
+ Args:
190
+ name: Action name
191
+
192
+ Returns:
193
+ RunMethod enum value
194
+ """
195
+ return self.get_action_config(name).method
196
+
197
+ def get_action_class(self, name: str) -> type[BaseAction] | Callable:
198
+ """Load action class/function from entrypoint.
199
+
200
+ Injects action_name and category from config if not defined on the class.
201
+ This allows plugin developers to write minimal action classes without
202
+ redundant metadata when using config.yaml-based discovery.
203
+
204
+ Args:
205
+ name: Action name
206
+
207
+ Returns:
208
+ Action class (BaseAction subclass) or decorated function
209
+
210
+ Raises:
211
+ ActionNotFoundError: If action doesn't exist or has no entrypoint
212
+ """
213
+ if name in self._action_cache:
214
+ return self._action_cache[name]
215
+
216
+ action_config = self.get_action_config(name)
217
+ entrypoint = action_config.entrypoint
218
+
219
+ if not entrypoint:
220
+ raise ActionNotFoundError(
221
+ f"Action '{name}' has no entrypoint defined",
222
+ details={'action': name},
223
+ )
224
+
225
+ action_cls = _load_entrypoint(entrypoint)
226
+
227
+ # Inject action_name from config key if not defined on class
228
+ if not getattr(action_cls, 'action_name', None):
229
+ action_cls.action_name = name
230
+
231
+ # Inject category from plugin config if not defined on class
232
+ if not getattr(action_cls, 'category', None):
233
+ action_cls.category = str(self.config.category)
234
+
235
+ self._action_cache[name] = action_cls
236
+ return action_cls
237
+
238
+ def has_action(self, name: str) -> bool:
239
+ """Check if an action exists.
240
+
241
+ Args:
242
+ name: Action name
243
+
244
+ Returns:
245
+ True if action exists, False otherwise
246
+ """
247
+ return name in self.config.actions
248
+
249
+ def get_action_entrypoint(self, name: str) -> str:
250
+ """Get action entrypoint string without loading the class.
251
+
252
+ Useful for remote execution where the class shouldn't be imported locally.
253
+
254
+ Args:
255
+ name: Action name
256
+
257
+ Returns:
258
+ Entrypoint string (e.g., 'plugin.train.TrainAction')
259
+
260
+ Raises:
261
+ ActionNotFoundError: If action doesn't exist or has no entrypoint
262
+ """
263
+ action_config = self.get_action_config(name)
264
+ entrypoint = action_config.entrypoint
265
+
266
+ if not entrypoint:
267
+ raise ActionNotFoundError(
268
+ f"Action '{name}' has no entrypoint defined",
269
+ details={'action': name},
270
+ )
271
+
272
+ # Normalize entrypoint format: 'module:Class' -> 'module.Class'
273
+ if ':' in entrypoint:
274
+ module_path, class_name = entrypoint.rsplit(':', 1)
275
+ return f'{module_path}.{class_name}'
276
+ return entrypoint
277
+
278
+ def get_action_params_model(self, name: str) -> type[BaseModel] | None:
279
+ """Get the params model for an action.
280
+
281
+ Args:
282
+ name: Action name
283
+
284
+ Returns:
285
+ Pydantic model class for parameters, or None if not defined
286
+ """
287
+ action_config = self.get_action_config(name)
288
+
289
+ # First check if params_schema is set on ActionConfig
290
+ if action_config.params_schema:
291
+ return action_config.params_schema
292
+
293
+ # Otherwise try to load from action class
294
+ try:
295
+ action_cls = self.get_action_class(name)
296
+ return getattr(action_cls, 'params_model', None)
297
+ except Exception:
298
+ return None
299
+
300
+ def get_action_ui_schema(self, name: str) -> list[dict[str, Any]]:
301
+ """Get UI schema for an action's parameters.
302
+
303
+ Auto-generates FormKit-compatible UI schema from the action's params_model.
304
+
305
+ Args:
306
+ name: Action name
307
+
308
+ Returns:
309
+ List of FormKit schema items, or empty list if no params_model
310
+
311
+ Example:
312
+ >>> discovery = PluginDiscovery.from_path('/path/to/plugin')
313
+ >>> schema = discovery.get_action_ui_schema('train')
314
+ >>> schema
315
+ [{'$formkit': 'number', 'name': 'epochs', 'label': 'Epochs', ...}]
316
+ """
317
+ params_model = self.get_action_params_model(name)
318
+ if params_model is None:
319
+ return []
320
+ return pydantic_to_ui_schema(params_model)
321
+
322
+ def get_action_result_model(self, name: str) -> type[BaseModel] | None:
323
+ """Get the result model for an action.
324
+
325
+ Returns the Pydantic model class used for output validation.
326
+ Returns None if no result model is defined (NoResult sentinel).
327
+
328
+ Args:
329
+ name: Action name
330
+
331
+ Returns:
332
+ Pydantic model class for result validation, or None if not defined
333
+
334
+ Example:
335
+ >>> discovery = PluginDiscovery.from_path('/path/to/plugin')
336
+ >>> result_model = discovery.get_action_result_model('train')
337
+ >>> if result_model:
338
+ ... print(result_model.model_json_schema())
339
+ """
340
+ from synapse_sdk.plugins.action import NoResult
341
+
342
+ try:
343
+ action_cls = self.get_action_class(name)
344
+ result_model = getattr(action_cls, 'result_model', NoResult)
345
+ if result_model is NoResult:
346
+ return None
347
+ return result_model
348
+ except Exception:
349
+ return None
350
+
351
+ def get_action_result_ui_schema(self, name: str) -> list[dict[str, Any]]:
352
+ """Get UI schema for an action's result type.
353
+
354
+ Auto-generates FormKit-compatible UI schema from the action's result_model.
355
+ Useful for displaying expected output format in UIs.
356
+
357
+ Args:
358
+ name: Action name
359
+
360
+ Returns:
361
+ List of FormKit schema items, or empty list if no result_model
362
+
363
+ Example:
364
+ >>> discovery = PluginDiscovery.from_path('/path/to/plugin')
365
+ >>> schema = discovery.get_action_result_ui_schema('train')
366
+ >>> schema
367
+ [{'$formkit': 'text', 'name': 'weights_path', 'label': 'Weights Path', ...}]
368
+ """
369
+ result_model = self.get_action_result_model(name)
370
+ if result_model is None:
371
+ return []
372
+ return pydantic_to_ui_schema(result_model)
373
+
374
+ def get_action_input_type(self, name: str) -> str | None:
375
+ """Get the semantic input type for an action.
376
+
377
+ Extracts the input_type from the action class's DataType declaration.
378
+
379
+ Args:
380
+ name: Action name
381
+
382
+ Returns:
383
+ Input type name string, or None if not defined
384
+
385
+ Example:
386
+ >>> discovery = PluginDiscovery.from_path('/path/to/plugin')
387
+ >>> discovery.get_action_input_type('train')
388
+ 'yolo_dataset'
389
+ """
390
+ try:
391
+ action_cls = self.get_action_class(name)
392
+ input_type = getattr(action_cls, 'input_type', None)
393
+ if input_type is not None and hasattr(input_type, 'name'):
394
+ return input_type.name
395
+ return None
396
+ except Exception:
397
+ return None
398
+
399
+ def get_action_output_type(self, name: str) -> str | None:
400
+ """Get the semantic output type for an action.
401
+
402
+ Extracts the output_type from the action class's DataType declaration.
403
+
404
+ Args:
405
+ name: Action name
406
+
407
+ Returns:
408
+ Output type name string, or None if not defined
409
+
410
+ Example:
411
+ >>> discovery = PluginDiscovery.from_path('/path/to/plugin')
412
+ >>> discovery.get_action_output_type('train')
413
+ 'model_weights'
414
+ """
415
+ try:
416
+ action_cls = self.get_action_class(name)
417
+ output_type = getattr(action_cls, 'output_type', None)
418
+ if output_type is not None and hasattr(output_type, 'name'):
419
+ return output_type.name
420
+ return None
421
+ except Exception:
422
+ return None
423
+
424
+ def to_config_dict(self, *, include_ui_schemas: bool = True) -> dict[str, Any]:
425
+ """Export plugin configuration as a dictionary.
426
+
427
+ Generates a config dict compatible with the backend API format,
428
+ with optional auto-generation of UI schemas from params_model.
429
+
430
+ Args:
431
+ include_ui_schemas: If True, auto-generate train_ui_schemas
432
+ from each action's params_model
433
+
434
+ Returns:
435
+ Config dictionary ready for serialization or API submission
436
+
437
+ Example:
438
+ >>> discovery = PluginDiscovery.from_module(my_plugin)
439
+ >>> config = discovery.to_config_dict()
440
+ >>> # config['actions']['train']['hyperparameters']['train_ui_schemas']
441
+ >>> # is auto-populated from TrainParams model
442
+ """
443
+ config_dict: dict[str, Any] = {
444
+ 'name': self.config.name,
445
+ 'code': self.config.code,
446
+ 'version': self.config.version,
447
+ 'category': str(self.config.category.value),
448
+ 'description': self.config.description,
449
+ 'readme': self.config.readme,
450
+ }
451
+
452
+ # Add optional fields
453
+ if self.config.data_type:
454
+ config_dict['data_type'] = str(self.config.data_type.value)
455
+ if self.config.tasks:
456
+ config_dict['tasks'] = self.config.tasks
457
+
458
+ # Build actions dict
459
+ actions_dict: dict[str, Any] = {}
460
+ for action_name, action_config in self.config.actions.items():
461
+ action_dict: dict[str, Any] = {
462
+ 'entrypoint': action_config.entrypoint,
463
+ 'method': str(action_config.method.value),
464
+ }
465
+
466
+ if action_config.description:
467
+ action_dict['description'] = action_config.description
468
+
469
+ # Add semantic types (extracted from action class)
470
+ input_type = self.get_action_input_type(action_name)
471
+ output_type = self.get_action_output_type(action_name)
472
+ if input_type:
473
+ action_dict['input_type'] = input_type
474
+ if output_type:
475
+ action_dict['output_type'] = output_type
476
+
477
+ # Auto-generate UI schemas from params_model
478
+ if include_ui_schemas:
479
+ ui_schema = self.get_action_ui_schema(action_name)
480
+ if ui_schema:
481
+ action_dict['hyperparameters'] = {
482
+ 'train_ui_schemas': ui_schema,
483
+ }
484
+
485
+ # Add result schema if defined
486
+ result_schema = self.get_action_result_ui_schema(action_name)
487
+ if result_schema:
488
+ action_dict['result_schema'] = result_schema
489
+
490
+ actions_dict[action_name] = action_dict
491
+
492
+ config_dict['actions'] = actions_dict
493
+ return config_dict
494
+
495
+ def to_yaml(self, *, include_ui_schemas: bool = True) -> str:
496
+ """Export plugin configuration as YAML string.
497
+
498
+ Args:
499
+ include_ui_schemas: If True, auto-generate train_ui_schemas
500
+
501
+ Returns:
502
+ YAML-formatted configuration string
503
+ """
504
+ import yaml
505
+
506
+ config_dict = self.to_config_dict(include_ui_schemas=include_ui_schemas)
507
+ return yaml.dump(config_dict, default_flow_style=False, allow_unicode=True, sort_keys=False)
508
+
509
+ def sync_config_file(self, config_path: Path | str) -> dict[str, str]:
510
+ """Sync actions and types from code to config.yaml.
511
+
512
+ Discovers actions from plugin source files and updates config.yaml:
513
+ - Adds new actions found in code but not in config
514
+ - Updates entrypoints if they changed
515
+ - Updates input_type/output_type from code
516
+
517
+ This should be called during plugin publish to ensure config.yaml
518
+ reflects the code-defined actions and types.
519
+
520
+ Args:
521
+ config_path: Path to config.yaml file
522
+
523
+ Returns:
524
+ Dict of action_name -> changes made (for logging)
525
+
526
+ Example:
527
+ >>> discovery = PluginDiscovery.from_path('/path/to/plugin')
528
+ >>> changes = discovery.sync_config_file('/path/to/plugin/config.yaml')
529
+ >>> print(changes)
530
+ {'train': 'added (entrypoint=plugin.train.TrainAction, input_type=yolo_dataset)'}
531
+ """
532
+ import yaml
533
+
534
+ config_path = Path(config_path)
535
+ if not config_path.exists():
536
+ raise FileNotFoundError(f'Config file not found: {config_path}')
537
+
538
+ plugin_dir = config_path.parent
539
+
540
+ # Read existing config
541
+ with config_path.open() as f:
542
+ config_data = yaml.safe_load(f)
543
+
544
+ if config_data is None:
545
+ config_data = {}
546
+
547
+ changes: dict[str, str] = {}
548
+
549
+ # Ensure actions dict exists
550
+ if 'actions' not in config_data:
551
+ config_data['actions'] = {}
552
+
553
+ # Discover actions from source files
554
+ discovered = self.discover_actions(plugin_dir)
555
+
556
+ # Process discovered actions
557
+ for action_name, action_info in discovered.items():
558
+ entrypoint = action_info['entrypoint']
559
+ input_type = action_info['input_type']
560
+ output_type = action_info['output_type']
561
+
562
+ if action_name not in config_data['actions']:
563
+ # New action - add it
564
+ action_data: dict[str, Any] = {'entrypoint': entrypoint}
565
+ if input_type:
566
+ action_data['input_type'] = input_type
567
+ if output_type:
568
+ action_data['output_type'] = output_type
569
+
570
+ config_data['actions'][action_name] = action_data
571
+
572
+ parts = [f'entrypoint={entrypoint}']
573
+ if input_type:
574
+ parts.append(f'input_type={input_type}')
575
+ if output_type:
576
+ parts.append(f'output_type={output_type}')
577
+ changes[action_name] = f'added ({", ".join(parts)})'
578
+ else:
579
+ # Existing action - update fields if changed
580
+ action_data = config_data['actions'][action_name]
581
+ if not isinstance(action_data, dict):
582
+ action_data = {'entrypoint': action_data}
583
+ config_data['actions'][action_name] = action_data
584
+
585
+ updates = []
586
+
587
+ # Update entrypoint if changed
588
+ if action_data.get('entrypoint') != entrypoint:
589
+ action_data['entrypoint'] = entrypoint
590
+ updates.append(f'entrypoint={entrypoint}')
591
+
592
+ # Update types if changed
593
+ if input_type and action_data.get('input_type') != input_type:
594
+ action_data['input_type'] = input_type
595
+ updates.append(f'input_type={input_type}')
596
+ if output_type and action_data.get('output_type') != output_type:
597
+ action_data['output_type'] = output_type
598
+ updates.append(f'output_type={output_type}')
599
+
600
+ if updates:
601
+ changes[action_name] = ', '.join(updates)
602
+
603
+ # Write back to file
604
+ with config_path.open('w') as f:
605
+ yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
606
+
607
+ return changes
608
+
609
+ @staticmethod
610
+ def discover_actions(plugin_dir: Path | str) -> dict[str, dict[str, Any]]:
611
+ """Discover BaseAction subclasses from plugin source files.
612
+
613
+ Scans Python files in the plugin directory for classes that inherit
614
+ from BaseAction and extracts their metadata.
615
+
616
+ Args:
617
+ plugin_dir: Path to the plugin directory
618
+
619
+ Returns:
620
+ Dict mapping action_name -> {entrypoint, input_type, output_type}
621
+
622
+ Example:
623
+ >>> actions = PluginDiscovery.discover_actions('/path/to/plugin')
624
+ >>> actions
625
+ {'train': {'entrypoint': 'plugin.train.TrainAction', 'input_type': 'yolo_dataset', ...}}
626
+ """
627
+ import ast
628
+ import sys
629
+
630
+ from synapse_sdk.plugins.action import BaseAction
631
+
632
+ plugin_dir = Path(plugin_dir)
633
+ if not plugin_dir.is_dir():
634
+ raise ValueError(f'Plugin directory not found: {plugin_dir}')
635
+
636
+ discovered: dict[str, dict[str, Any]] = {}
637
+
638
+ # Add plugin directory to sys.path for imports
639
+ plugin_dir_str = str(plugin_dir)
640
+ path_added = False
641
+ if plugin_dir_str not in sys.path:
642
+ sys.path.insert(0, plugin_dir_str)
643
+ path_added = True
644
+
645
+ try:
646
+ # Find all Python files (excluding __pycache__, tests, etc.)
647
+ py_files = list(plugin_dir.rglob('*.py'))
648
+ py_files = [
649
+ f
650
+ for f in py_files
651
+ if '__pycache__' not in str(f) and not f.name.startswith('test_') and f.name != 'conftest.py'
652
+ ]
653
+
654
+ for py_file in py_files:
655
+ # Get module path relative to plugin directory
656
+ rel_path = py_file.relative_to(plugin_dir)
657
+ module_parts = list(rel_path.with_suffix('').parts)
658
+
659
+ # Skip __init__.py files for module path
660
+ if module_parts[-1] == '__init__':
661
+ module_parts = module_parts[:-1]
662
+ if not module_parts:
663
+ continue
664
+
665
+ module_name = '.'.join(module_parts)
666
+
667
+ # First, use AST to find potential BaseAction subclasses
668
+ # This avoids importing modules that would fail
669
+ try:
670
+ with py_file.open() as f:
671
+ tree = ast.parse(f.read(), filename=str(py_file))
672
+ except SyntaxError:
673
+ continue
674
+
675
+ # Look for class definitions that might be actions
676
+ potential_classes = []
677
+ for node in ast.walk(tree):
678
+ if isinstance(node, ast.ClassDef):
679
+ # Check if it has BaseAction or similar in bases
680
+ for base in node.bases:
681
+ base_name = ''
682
+ if isinstance(base, ast.Name):
683
+ base_name = base.id
684
+ elif isinstance(base, ast.Attribute):
685
+ base_name = base.attr
686
+ elif isinstance(base, ast.Subscript):
687
+ # Handle Generic[T] style: BaseAction[Params]
688
+ if isinstance(base.value, ast.Name):
689
+ base_name = base.value.id
690
+ elif isinstance(base.value, ast.Attribute):
691
+ base_name = base.value.attr
692
+
693
+ if 'Action' in base_name or 'Base' in base_name:
694
+ potential_classes.append(node.name)
695
+ break
696
+
697
+ if not potential_classes:
698
+ continue
699
+
700
+ # Now import the module and check actual inheritance
701
+ try:
702
+ module = importlib.import_module(module_name)
703
+ except Exception:
704
+ continue
705
+
706
+ for class_name in potential_classes:
707
+ try:
708
+ cls = getattr(module, class_name, None)
709
+ if cls is None:
710
+ continue
711
+
712
+ # Check if it's a proper BaseAction subclass
713
+ if not (
714
+ inspect.isclass(cls)
715
+ and issubclass(cls, BaseAction)
716
+ and cls is not BaseAction
717
+ and not inspect.isabstract(cls)
718
+ ):
719
+ continue
720
+
721
+ # Extract action name
722
+ # Priority: action_name attr > derived from class name
723
+ action_name = getattr(cls, 'action_name', None)
724
+ if not action_name:
725
+ # Derive from class name: TrainAction -> train
726
+ action_name = class_name
727
+ if action_name.endswith('Action'):
728
+ action_name = action_name[:-6]
729
+ action_name = action_name.lower()
730
+
731
+ # Build entrypoint
732
+ entrypoint = f'{module_name}.{class_name}'
733
+
734
+ # Extract types
735
+ input_type = None
736
+ output_type = None
737
+ type_cls = getattr(cls, 'input_type', None)
738
+ if type_cls is not None and hasattr(type_cls, 'name'):
739
+ input_type = type_cls.name
740
+ type_cls = getattr(cls, 'output_type', None)
741
+ if type_cls is not None and hasattr(type_cls, 'name'):
742
+ output_type = type_cls.name
743
+
744
+ discovered[action_name] = {
745
+ 'entrypoint': entrypoint,
746
+ 'input_type': input_type,
747
+ 'output_type': output_type,
748
+ }
749
+
750
+ except Exception:
751
+ continue
752
+
753
+ finally:
754
+ # Clean up sys.path
755
+ if path_added and plugin_dir_str in sys.path:
756
+ sys.path.remove(plugin_dir_str)
757
+
758
+ return discovered
759
+
760
+
761
+ def _load_entrypoint(entrypoint: str) -> type[BaseAction] | Callable:
762
+ """Load class/function from entrypoint string.
763
+
764
+ Supports both colon and dot notation:
765
+ - Colon notation: 'module.path:ClassName' (preferred)
766
+ - Dot notation: 'module.path.ClassName' (common in config.yaml)
767
+
768
+ Args:
769
+ entrypoint: Entrypoint string like 'module.path:ClassName' or 'module.path.ClassName'
770
+
771
+ Returns:
772
+ Loaded class or function
773
+
774
+ Raises:
775
+ ValueError: If entrypoint format is invalid
776
+ ModuleNotFoundError: If module doesn't exist
777
+ AttributeError: If attribute doesn't exist in module
778
+ """
779
+ if ':' in entrypoint:
780
+ # Colon notation: 'module.path:ClassName'
781
+ module_path, attr_name = entrypoint.rsplit(':', 1)
782
+ else:
783
+ # Dot notation: 'module.path.ClassName' -> module='module.path', attr='ClassName'
784
+ module_path, attr_name = entrypoint.rsplit('.', 1)
785
+
786
+ module = importlib.import_module(module_path)
787
+ return getattr(module, attr_name)
788
+
789
+
790
+ __all__ = ['PluginDiscovery']