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,58 @@
1
+ """Plugin CLI commands."""
2
+
3
+ from synapse_sdk.cli.plugin.create import (
4
+ CreateResult,
5
+ PluginSpec,
6
+ create_plugin,
7
+ create_plugin_interactive,
8
+ )
9
+ from synapse_sdk.cli.plugin.job import (
10
+ display_job,
11
+ get_job,
12
+ get_job_logs,
13
+ tail_job_logs,
14
+ )
15
+ from synapse_sdk.cli.plugin.publish import (
16
+ PublishResult,
17
+ create_plugin_archive,
18
+ display_files_preview,
19
+ find_config_file,
20
+ load_synapseignore,
21
+ publish_plugin,
22
+ )
23
+ from synapse_sdk.cli.plugin.run import (
24
+ RunResult,
25
+ resolve_plugin_code,
26
+ run_plugin,
27
+ )
28
+ from synapse_sdk.cli.plugin.test import (
29
+ TestResult,
30
+ test_plugin,
31
+ )
32
+
33
+ __all__ = [
34
+ # Create
35
+ 'CreateResult',
36
+ 'PluginSpec',
37
+ 'create_plugin',
38
+ 'create_plugin_interactive',
39
+ # Job
40
+ 'display_job',
41
+ 'get_job',
42
+ 'get_job_logs',
43
+ 'tail_job_logs',
44
+ # Publish
45
+ 'PublishResult',
46
+ 'create_plugin_archive',
47
+ 'display_files_preview',
48
+ 'find_config_file',
49
+ 'load_synapseignore',
50
+ 'publish_plugin',
51
+ # Run
52
+ 'RunResult',
53
+ 'resolve_plugin_code',
54
+ 'run_plugin',
55
+ # Test
56
+ 'TestResult',
57
+ 'test_plugin',
58
+ ]
@@ -0,0 +1,566 @@
1
+ """Plugin creation command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shutil
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import questionary
12
+ import yaml
13
+ from jinja2 import Environment, FileSystemLoader
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.table import Table
17
+ from rich.tree import Tree
18
+
19
+ from synapse_sdk.plugins.enums import (
20
+ AnnotationCategory,
21
+ AnnotationType,
22
+ DataType,
23
+ PluginCategory,
24
+ SmartToolType,
25
+ )
26
+ from synapse_sdk.plugins.templates import TEMPLATES_DIR
27
+
28
+ # Category descriptions for interactive prompts
29
+ CATEGORY_DESCRIPTIONS = {
30
+ PluginCategory.NEURAL_NET: 'Train and deploy ML models',
31
+ PluginCategory.SMART_TOOL: 'Interactive annotation helpers',
32
+ PluginCategory.EXPORT: 'Data format conversion',
33
+ PluginCategory.UPLOAD: 'External data import',
34
+ PluginCategory.DATA_VALIDATION: 'Pre-annotation data checks',
35
+ PluginCategory.PRE_ANNOTATION: 'Auto-generate initial annotations',
36
+ PluginCategory.POST_ANNOTATION: 'Process annotations after labeling',
37
+ PluginCategory.CUSTOM: 'Custom plugin type',
38
+ }
39
+
40
+ # Tasks by data type
41
+ TASKS_BY_DATA_TYPE = {
42
+ DataType.IMAGE: [
43
+ 'object_detection',
44
+ 'classification',
45
+ 'segmentation',
46
+ 'keypoint',
47
+ ],
48
+ DataType.TEXT: [
49
+ 'classification',
50
+ 'ner',
51
+ 'qa',
52
+ ],
53
+ DataType.VIDEO: [
54
+ 'object_tracking',
55
+ 'action_recognition',
56
+ 'segmentation',
57
+ ],
58
+ DataType.AUDIO: [
59
+ 'classification',
60
+ 'transcription',
61
+ ],
62
+ DataType.PCD: [
63
+ 'object_detection',
64
+ 'segmentation',
65
+ ],
66
+ }
67
+
68
+
69
+ @dataclass
70
+ class PluginSpec:
71
+ """Specification for a plugin to create."""
72
+
73
+ name: str
74
+ code: str
75
+ version: str
76
+ category: PluginCategory
77
+ description: str
78
+
79
+ # Category-specific fields
80
+ data_type: DataType | None = None
81
+ tasks: list[str] = field(default_factory=list)
82
+ annotation_category: AnnotationCategory | None = None
83
+ annotation_type: AnnotationType | None = None
84
+ smart_tool_type: SmartToolType | None = None
85
+ supported_data_types: list[DataType] = field(default_factory=list)
86
+
87
+ @property
88
+ def directory_name(self) -> str:
89
+ """Get the directory name for the plugin."""
90
+ return f'synapse-{self.code}-plugin'
91
+
92
+ def to_template_context(self) -> dict[str, Any]:
93
+ """Convert to template context dictionary."""
94
+ return {
95
+ 'name': self.name,
96
+ 'code': self.code,
97
+ 'version': self.version,
98
+ 'category': self.category.value,
99
+ 'description': self.description,
100
+ 'data_type': self.data_type.value if self.data_type else None,
101
+ 'tasks': [f'{self.data_type.value}.{t}' for t in self.tasks] if self.data_type and self.tasks else [],
102
+ 'annotation_category': self.annotation_category.value if self.annotation_category else None,
103
+ 'annotation_type': self.annotation_type.value if self.annotation_type else None,
104
+ 'smart_tool_type': self.smart_tool_type.value if self.smart_tool_type else None,
105
+ 'supported_data_types': [dt.value for dt in self.supported_data_types],
106
+ }
107
+
108
+
109
+ def slugify(text: str) -> str:
110
+ """Convert text to slug format.
111
+
112
+ Args:
113
+ text: Text to slugify.
114
+
115
+ Returns:
116
+ Slugified text (lowercase, hyphens instead of spaces).
117
+ """
118
+ # Convert to lowercase and replace spaces/underscores with hyphens
119
+ slug = text.lower().strip()
120
+ slug = re.sub(r'[\s_]+', '-', slug)
121
+ # Remove non-alphanumeric characters except hyphens
122
+ slug = re.sub(r'[^a-z0-9-]', '', slug)
123
+ # Remove consecutive hyphens
124
+ slug = re.sub(r'-+', '-', slug)
125
+ # Remove leading/trailing hyphens
126
+ slug = slug.strip('-')
127
+ return slug
128
+
129
+
130
+ def validate_code(code: str) -> bool | str:
131
+ """Validate plugin code format.
132
+
133
+ Args:
134
+ code: Plugin code to validate.
135
+
136
+ Returns:
137
+ True if valid, error message otherwise.
138
+ """
139
+ if not code:
140
+ return 'Code cannot be empty'
141
+ if not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$', code):
142
+ return 'Code must be lowercase letters, numbers, and hyphens (no leading/trailing hyphens)'
143
+ return True
144
+
145
+
146
+ def prompt_category() -> PluginCategory:
147
+ """Prompt user to select a plugin category.
148
+
149
+ Returns:
150
+ Selected plugin category.
151
+ """
152
+ choices = [
153
+ questionary.Choice(
154
+ title=f'{cat.value.replace("_", " ").title():20} {CATEGORY_DESCRIPTIONS[cat]}',
155
+ value=cat,
156
+ )
157
+ for cat in PluginCategory
158
+ ]
159
+
160
+ return questionary.select(
161
+ 'Select plugin category:',
162
+ choices=choices,
163
+ ).ask()
164
+
165
+
166
+ def prompt_basic_info(category: PluginCategory) -> tuple[str, str, str, str]:
167
+ """Prompt user for basic plugin information.
168
+
169
+ Args:
170
+ category: Selected plugin category.
171
+
172
+ Returns:
173
+ Tuple of (name, code, version, description).
174
+ """
175
+ name = questionary.text(
176
+ 'Plugin name:',
177
+ validate=lambda x: len(x) > 0 or 'Name cannot be empty',
178
+ ).ask()
179
+
180
+ default_code = slugify(name)
181
+ code = questionary.text(
182
+ 'Plugin code:',
183
+ default=default_code,
184
+ validate=validate_code,
185
+ ).ask()
186
+
187
+ version = questionary.text(
188
+ 'Version:',
189
+ default='0.1.0',
190
+ ).ask()
191
+
192
+ default_description = f'{name} plugin'
193
+ description = questionary.text(
194
+ 'Description:',
195
+ default=default_description,
196
+ ).ask()
197
+
198
+ return name, code, version, description
199
+
200
+
201
+ def prompt_neural_net_options() -> tuple[DataType | None, list[str]]:
202
+ """Prompt user for neural net specific options.
203
+
204
+ Returns:
205
+ Tuple of (data_type, tasks).
206
+ """
207
+ data_type_choices = [questionary.Choice(title=dt.value, value=dt) for dt in DataType]
208
+ data_type = questionary.select(
209
+ 'Data type:',
210
+ choices=data_type_choices,
211
+ ).ask()
212
+
213
+ available_tasks = TASKS_BY_DATA_TYPE.get(data_type, [])
214
+ if available_tasks:
215
+ tasks = questionary.checkbox(
216
+ 'Tasks (select with space, confirm with enter):',
217
+ choices=available_tasks,
218
+ ).ask()
219
+ else:
220
+ tasks = []
221
+
222
+ return data_type, tasks
223
+
224
+
225
+ def prompt_smart_tool_options() -> tuple[AnnotationCategory, AnnotationType, SmartToolType]:
226
+ """Prompt user for smart tool specific options.
227
+
228
+ Returns:
229
+ Tuple of (annotation_category, annotation_type, smart_tool_type).
230
+ """
231
+ annotation_category = questionary.select(
232
+ 'Annotation category:',
233
+ choices=[ac.value for ac in AnnotationCategory],
234
+ ).ask()
235
+
236
+ annotation_type = questionary.select(
237
+ 'Annotation type:',
238
+ choices=[at.value for at in AnnotationType],
239
+ ).ask()
240
+
241
+ smart_tool_type = questionary.select(
242
+ 'Smart tool type:',
243
+ choices=[
244
+ questionary.Choice(title='Interactive - User triggers predictions', value=SmartToolType.INTERACTIVE),
245
+ questionary.Choice(title='Automatic - Runs on all data', value=SmartToolType.AUTOMATIC),
246
+ questionary.Choice(title='Semi-automatic - Suggests, user confirms', value=SmartToolType.SEMI_AUTOMATIC),
247
+ ],
248
+ ).ask()
249
+
250
+ return (
251
+ AnnotationCategory(annotation_category),
252
+ AnnotationType(annotation_type),
253
+ smart_tool_type,
254
+ )
255
+
256
+
257
+ def prompt_upload_options() -> list[DataType]:
258
+ """Prompt user for upload specific options.
259
+
260
+ Returns:
261
+ List of supported data types.
262
+ """
263
+ choices = [questionary.Choice(title=dt.value, value=dt) for dt in DataType]
264
+ return questionary.checkbox(
265
+ 'Supported data types (select with space):',
266
+ choices=choices,
267
+ ).ask()
268
+
269
+
270
+ def collect_plugin_spec(
271
+ *,
272
+ name: str | None = None,
273
+ code: str | None = None,
274
+ category: str | None = None,
275
+ interactive: bool = True,
276
+ ) -> PluginSpec | None:
277
+ """Collect plugin specification from user.
278
+
279
+ Args:
280
+ name: Plugin name (skip prompt if provided).
281
+ code: Plugin code (skip prompt if provided).
282
+ category: Plugin category (skip prompt if provided).
283
+ interactive: Whether to prompt for input.
284
+
285
+ Returns:
286
+ PluginSpec if successful, None if user cancelled.
287
+ """
288
+ # Get category
289
+ if category:
290
+ selected_category = PluginCategory(category)
291
+ elif interactive:
292
+ selected_category = prompt_category()
293
+ if not selected_category:
294
+ return None
295
+ else:
296
+ selected_category = PluginCategory.CUSTOM
297
+
298
+ # Get basic info
299
+ if name and code:
300
+ plugin_name = name
301
+ plugin_code = code
302
+ version = '0.1.0'
303
+ description = f'{name} plugin'
304
+ elif interactive:
305
+ result = prompt_basic_info(selected_category)
306
+ if not all(result):
307
+ return None
308
+ plugin_name, plugin_code, version, description = result
309
+ else:
310
+ raise ValueError('name and code are required in non-interactive mode')
311
+
312
+ # Create base spec
313
+ spec = PluginSpec(
314
+ name=plugin_name,
315
+ code=plugin_code,
316
+ version=version,
317
+ category=selected_category,
318
+ description=description,
319
+ )
320
+
321
+ # Category-specific prompts
322
+ if interactive:
323
+ if selected_category == PluginCategory.NEURAL_NET:
324
+ data_type, tasks = prompt_neural_net_options()
325
+ if data_type:
326
+ spec.data_type = data_type
327
+ spec.tasks = tasks or []
328
+
329
+ elif selected_category == PluginCategory.SMART_TOOL:
330
+ result = prompt_smart_tool_options()
331
+ if all(result):
332
+ spec.annotation_category, spec.annotation_type, spec.smart_tool_type = result
333
+
334
+ elif selected_category == PluginCategory.UPLOAD:
335
+ supported = prompt_upload_options()
336
+ if supported:
337
+ spec.supported_data_types = supported
338
+
339
+ return spec
340
+
341
+
342
+ def display_preview(spec: PluginSpec, console: Console) -> None:
343
+ """Display a preview of the plugin to be created.
344
+
345
+ Args:
346
+ spec: Plugin specification.
347
+ console: Rich console for output.
348
+ """
349
+ # Create tree view of files
350
+ tree = Tree(f'[bold]{spec.directory_name}/[/bold]')
351
+ tree.add('[dim]config.yaml[/dim] Plugin configuration')
352
+ tree.add('[dim]README.md[/dim] Documentation')
353
+ tree.add('[dim]requirements.txt[/dim] Dependencies')
354
+ tree.add('[dim]pyproject.toml[/dim] Project metadata')
355
+ tree.add('[dim].gitignore[/dim] Git ignore rules')
356
+ tree.add('[dim].synapseignore[/dim] Publish ignore rules')
357
+
358
+ plugin_branch = tree.add('[dim]plugin/[/dim] Action implementations')
359
+
360
+ # Add category-specific files
361
+ category = spec.category.value
362
+ if category == 'neural_net':
363
+ plugin_branch.add('[dim]train.py[/dim]')
364
+ plugin_branch.add('[dim]inference.py[/dim]')
365
+ elif category == 'smart_tool':
366
+ plugin_branch.add('[dim]auto_label.py[/dim]')
367
+ elif category == 'export':
368
+ plugin_branch.add('[dim]export.py[/dim]')
369
+ elif category == 'upload':
370
+ plugin_branch.add('[dim]upload.py[/dim]')
371
+ elif category == 'data_validation':
372
+ plugin_branch.add('[dim]validate.py[/dim]')
373
+ elif category == 'pre_annotation':
374
+ plugin_branch.add('[dim]pre_annotate.py[/dim]')
375
+ elif category == 'post_annotation':
376
+ plugin_branch.add('[dim]post_annotate.py[/dim]')
377
+ else:
378
+ plugin_branch.add('[dim]main.py[/dim]')
379
+
380
+ # Create info table
381
+ table = Table(show_header=False, box=None, padding=(0, 2))
382
+ table.add_column('Key', style='dim')
383
+ table.add_column('Value')
384
+ table.add_row('Name', spec.name)
385
+ table.add_row('Code', spec.code)
386
+ table.add_row('Version', spec.version)
387
+ table.add_row('Category', spec.category.value.replace('_', ' ').title())
388
+
389
+ if spec.data_type:
390
+ table.add_row('Data Type', spec.data_type.value)
391
+ if spec.tasks:
392
+ table.add_row('Tasks', ', '.join(spec.tasks))
393
+ if spec.annotation_type:
394
+ table.add_row('Annotation Type', spec.annotation_type.value)
395
+ if spec.smart_tool_type:
396
+ table.add_row('Tool Type', spec.smart_tool_type.value)
397
+ if spec.supported_data_types:
398
+ table.add_row('Supported Types', ', '.join(dt.value for dt in spec.supported_data_types))
399
+
400
+ console.print()
401
+ console.print(Panel(table, title='Plugin Info', border_style='blue'))
402
+ console.print()
403
+ console.print(Panel(tree, title='Files to Create', border_style='green'))
404
+ console.print()
405
+
406
+
407
+ def render_template(template_path: Path, context: dict[str, Any]) -> str:
408
+ """Render a Jinja2 template.
409
+
410
+ Args:
411
+ template_path: Path to template file.
412
+ context: Template context.
413
+
414
+ Returns:
415
+ Rendered template content.
416
+ """
417
+ env = Environment(
418
+ loader=FileSystemLoader(template_path.parent),
419
+ keep_trailing_newline=True,
420
+ )
421
+ template = env.get_template(template_path.name)
422
+ return template.render(**context)
423
+
424
+
425
+ def create_plugin(spec: PluginSpec, output_dir: Path, console: Console) -> Path:
426
+ """Create a plugin from specification.
427
+
428
+ Args:
429
+ spec: Plugin specification.
430
+ output_dir: Directory to create plugin in.
431
+ console: Rich console for output.
432
+
433
+ Returns:
434
+ Path to created plugin directory.
435
+ """
436
+ plugin_dir = output_dir / spec.directory_name
437
+ context = spec.to_template_context()
438
+ category = spec.category.value
439
+
440
+ # Create plugin directory
441
+ plugin_dir.mkdir(parents=True, exist_ok=True)
442
+ (plugin_dir / 'plugin').mkdir(exist_ok=True)
443
+
444
+ # Render base templates
445
+ base_dir = TEMPLATES_DIR / 'base'
446
+ for template_file in base_dir.glob('*.j2'):
447
+ output_name = template_file.stem # Remove .j2
448
+ content = render_template(template_file, context)
449
+ (plugin_dir / output_name).write_text(content)
450
+
451
+ # Render base plugin/__init__.py
452
+ init_template = base_dir / 'plugin' / '__init__.py.j2'
453
+ if init_template.exists():
454
+ content = render_template(init_template, context)
455
+ (plugin_dir / 'plugin' / '__init__.py').write_text(content)
456
+
457
+ # Render category-specific templates
458
+ category_dir = TEMPLATES_DIR / category
459
+ if category_dir.exists():
460
+ # Merge category config with base config
461
+ category_config_template = category_dir / 'config.yaml.j2'
462
+ if category_config_template.exists():
463
+ base_config_path = plugin_dir / 'config.yaml'
464
+ base_config = yaml.safe_load(base_config_path.read_text())
465
+ category_config_content = render_template(category_config_template, context)
466
+ category_config = yaml.safe_load(category_config_content)
467
+ base_config.update(category_config)
468
+ base_config_path.write_text(yaml.dump(base_config, sort_keys=False, default_flow_style=False))
469
+
470
+ # Copy category-specific plugin files
471
+ category_plugin_dir = category_dir / 'plugin'
472
+ if category_plugin_dir.exists():
473
+ for template_file in category_plugin_dir.glob('*.j2'):
474
+ output_name = template_file.stem
475
+ content = render_template(template_file, context)
476
+ (plugin_dir / 'plugin' / output_name).write_text(content)
477
+
478
+ console.print(f'[green]Created plugin at[/green] {plugin_dir}')
479
+ return plugin_dir
480
+
481
+
482
+ @dataclass
483
+ class CreateResult:
484
+ """Result of plugin creation."""
485
+
486
+ plugin_dir: Path
487
+ spec: PluginSpec
488
+
489
+
490
+ def create_plugin_interactive(
491
+ *,
492
+ output_dir: Path | None = None,
493
+ name: str | None = None,
494
+ code: str | None = None,
495
+ category: str | None = None,
496
+ console: Console,
497
+ yes: bool = False,
498
+ ) -> CreateResult | None:
499
+ """Create a plugin interactively.
500
+
501
+ Args:
502
+ output_dir: Directory to create plugin in. Defaults to current directory.
503
+ name: Plugin name (skip prompt if provided).
504
+ code: Plugin code (skip prompt if provided).
505
+ category: Plugin category (skip prompt if provided).
506
+ console: Rich console for output.
507
+ yes: Skip confirmation prompt.
508
+
509
+ Returns:
510
+ CreateResult if successful, None if cancelled.
511
+ """
512
+ output_dir = output_dir or Path.cwd()
513
+
514
+ # Collect spec
515
+ spec = collect_plugin_spec(
516
+ name=name,
517
+ code=code,
518
+ category=category,
519
+ interactive=True,
520
+ )
521
+
522
+ if not spec:
523
+ return None
524
+
525
+ # Check if directory already exists
526
+ plugin_dir = output_dir / spec.directory_name
527
+ if plugin_dir.exists():
528
+ console.print(f'[red]Directory already exists:[/red] {plugin_dir}')
529
+ if not yes:
530
+ overwrite = questionary.confirm(
531
+ 'Overwrite existing directory?',
532
+ default=False,
533
+ ).ask()
534
+ if not overwrite:
535
+ return None
536
+ shutil.rmtree(plugin_dir)
537
+
538
+ # Show preview
539
+ display_preview(spec, console)
540
+
541
+ # Confirm
542
+ if not yes:
543
+ confirmed = questionary.confirm(
544
+ 'Create plugin?',
545
+ default=True,
546
+ ).ask()
547
+ if not confirmed:
548
+ console.print('[yellow]Cancelled[/yellow]')
549
+ return None
550
+
551
+ # Create plugin
552
+ plugin_dir = create_plugin(spec, output_dir, console)
553
+
554
+ return CreateResult(plugin_dir=plugin_dir, spec=spec)
555
+
556
+
557
+ __all__ = [
558
+ 'PluginSpec',
559
+ 'CreateResult',
560
+ 'create_plugin',
561
+ 'create_plugin_interactive',
562
+ 'collect_plugin_spec',
563
+ 'display_preview',
564
+ 'slugify',
565
+ 'validate_code',
566
+ ]