synapse-sdk 1.0.0a23__py3-none-any.whl → 2025.12.3__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.
Files changed (228) hide show
  1. synapse_sdk/__init__.py +24 -0
  2. synapse_sdk/cli/__init__.py +310 -5
  3. synapse_sdk/cli/alias/__init__.py +22 -0
  4. synapse_sdk/cli/alias/create.py +36 -0
  5. synapse_sdk/cli/alias/dataclass.py +31 -0
  6. synapse_sdk/cli/alias/default.py +16 -0
  7. synapse_sdk/cli/alias/delete.py +15 -0
  8. synapse_sdk/cli/alias/list.py +19 -0
  9. synapse_sdk/cli/alias/read.py +15 -0
  10. synapse_sdk/cli/alias/update.py +17 -0
  11. synapse_sdk/cli/alias/utils.py +61 -0
  12. synapse_sdk/cli/code_server.py +687 -0
  13. synapse_sdk/cli/config.py +440 -0
  14. synapse_sdk/cli/devtools.py +90 -0
  15. synapse_sdk/cli/plugin/__init__.py +33 -0
  16. synapse_sdk/cli/{create_plugin.py → plugin/create.py} +2 -2
  17. synapse_sdk/{plugins/cli → cli/plugin}/publish.py +23 -15
  18. synapse_sdk/clients/agent/__init__.py +9 -3
  19. synapse_sdk/clients/agent/container.py +143 -0
  20. synapse_sdk/clients/agent/core.py +19 -0
  21. synapse_sdk/clients/agent/ray.py +298 -9
  22. synapse_sdk/clients/backend/__init__.py +30 -12
  23. synapse_sdk/clients/backend/annotation.py +13 -5
  24. synapse_sdk/clients/backend/core.py +31 -4
  25. synapse_sdk/clients/backend/data_collection.py +186 -0
  26. synapse_sdk/clients/backend/hitl.py +17 -0
  27. synapse_sdk/clients/backend/integration.py +16 -1
  28. synapse_sdk/clients/backend/ml.py +5 -1
  29. synapse_sdk/clients/backend/models.py +78 -0
  30. synapse_sdk/clients/base.py +384 -41
  31. synapse_sdk/clients/ray/serve.py +2 -0
  32. synapse_sdk/clients/validators/collections.py +31 -0
  33. synapse_sdk/devtools/config.py +94 -0
  34. synapse_sdk/devtools/server.py +41 -0
  35. synapse_sdk/devtools/streamlit_app/__init__.py +5 -0
  36. synapse_sdk/devtools/streamlit_app/app.py +128 -0
  37. synapse_sdk/devtools/streamlit_app/services/__init__.py +11 -0
  38. synapse_sdk/devtools/streamlit_app/services/job_service.py +233 -0
  39. synapse_sdk/devtools/streamlit_app/services/plugin_service.py +236 -0
  40. synapse_sdk/devtools/streamlit_app/services/serve_service.py +95 -0
  41. synapse_sdk/devtools/streamlit_app/ui/__init__.py +15 -0
  42. synapse_sdk/devtools/streamlit_app/ui/config_tab.py +76 -0
  43. synapse_sdk/devtools/streamlit_app/ui/deployment_tab.py +66 -0
  44. synapse_sdk/devtools/streamlit_app/ui/http_tab.py +125 -0
  45. synapse_sdk/devtools/streamlit_app/ui/jobs_tab.py +573 -0
  46. synapse_sdk/devtools/streamlit_app/ui/serve_tab.py +346 -0
  47. synapse_sdk/devtools/streamlit_app/ui/status_bar.py +118 -0
  48. synapse_sdk/devtools/streamlit_app/utils/__init__.py +40 -0
  49. synapse_sdk/devtools/streamlit_app/utils/json_viewer.py +197 -0
  50. synapse_sdk/devtools/streamlit_app/utils/log_formatter.py +38 -0
  51. synapse_sdk/devtools/streamlit_app/utils/styles.py +241 -0
  52. synapse_sdk/devtools/streamlit_app/utils/ui_components.py +289 -0
  53. synapse_sdk/devtools/streamlit_app.py +10 -0
  54. synapse_sdk/loggers.py +120 -9
  55. synapse_sdk/plugins/README.md +1340 -0
  56. synapse_sdk/plugins/__init__.py +0 -13
  57. synapse_sdk/plugins/categories/base.py +117 -11
  58. synapse_sdk/plugins/categories/data_validation/actions/validation.py +72 -0
  59. synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +33 -5
  60. synapse_sdk/plugins/categories/export/actions/__init__.py +3 -0
  61. synapse_sdk/plugins/categories/export/actions/export/__init__.py +28 -0
  62. synapse_sdk/plugins/categories/export/actions/export/action.py +165 -0
  63. synapse_sdk/plugins/categories/export/actions/export/enums.py +113 -0
  64. synapse_sdk/plugins/categories/export/actions/export/exceptions.py +53 -0
  65. synapse_sdk/plugins/categories/export/actions/export/models.py +74 -0
  66. synapse_sdk/plugins/categories/export/actions/export/run.py +195 -0
  67. synapse_sdk/plugins/categories/export/actions/export/utils.py +187 -0
  68. synapse_sdk/plugins/categories/export/templates/config.yaml +21 -0
  69. synapse_sdk/plugins/categories/export/templates/plugin/__init__.py +390 -0
  70. synapse_sdk/plugins/categories/export/templates/plugin/export.py +160 -0
  71. synapse_sdk/plugins/categories/neural_net/actions/deployment.py +13 -12
  72. synapse_sdk/plugins/categories/neural_net/actions/train.py +1134 -31
  73. synapse_sdk/plugins/categories/neural_net/actions/tune.py +534 -0
  74. synapse_sdk/plugins/categories/neural_net/base/inference.py +1 -1
  75. synapse_sdk/plugins/categories/neural_net/templates/config.yaml +32 -4
  76. synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +26 -10
  77. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  78. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  79. synapse_sdk/plugins/categories/{export/actions/export.py → pre_annotation/actions/pre_annotation/action.py} +4 -4
  80. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  81. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +148 -0
  82. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  83. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  84. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  85. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +100 -0
  86. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +248 -0
  87. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  88. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  89. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +265 -0
  90. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  91. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  92. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +92 -0
  93. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +243 -0
  94. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  95. synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +19 -0
  96. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/to_task.py +40 -0
  97. synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +2 -0
  98. synapse_sdk/plugins/categories/upload/__init__.py +0 -0
  99. synapse_sdk/plugins/categories/upload/actions/__init__.py +0 -0
  100. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +19 -0
  101. synapse_sdk/plugins/categories/upload/actions/upload/action.py +236 -0
  102. synapse_sdk/plugins/categories/upload/actions/upload/context.py +185 -0
  103. synapse_sdk/plugins/categories/upload/actions/upload/enums.py +493 -0
  104. synapse_sdk/plugins/categories/upload/actions/upload/exceptions.py +36 -0
  105. synapse_sdk/plugins/categories/upload/actions/upload/factory.py +138 -0
  106. synapse_sdk/plugins/categories/upload/actions/upload/models.py +214 -0
  107. synapse_sdk/plugins/categories/upload/actions/upload/orchestrator.py +183 -0
  108. synapse_sdk/plugins/categories/upload/actions/upload/registry.py +113 -0
  109. synapse_sdk/plugins/categories/upload/actions/upload/run.py +179 -0
  110. synapse_sdk/plugins/categories/upload/actions/upload/steps/__init__.py +1 -0
  111. synapse_sdk/plugins/categories/upload/actions/upload/steps/base.py +107 -0
  112. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +62 -0
  113. synapse_sdk/plugins/categories/upload/actions/upload/steps/collection.py +63 -0
  114. synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +91 -0
  115. synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +82 -0
  116. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +235 -0
  117. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +201 -0
  118. synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +104 -0
  119. synapse_sdk/plugins/categories/upload/actions/upload/steps/validate.py +71 -0
  120. synapse_sdk/plugins/categories/upload/actions/upload/strategies/__init__.py +1 -0
  121. synapse_sdk/plugins/categories/upload/actions/upload/strategies/base.py +82 -0
  122. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/__init__.py +1 -0
  123. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/batch.py +39 -0
  124. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/single.py +29 -0
  125. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/__init__.py +1 -0
  126. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +300 -0
  127. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +287 -0
  128. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/__init__.py +1 -0
  129. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/excel.py +174 -0
  130. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/none.py +16 -0
  131. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/__init__.py +1 -0
  132. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/sync.py +84 -0
  133. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/__init__.py +1 -0
  134. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +60 -0
  135. synapse_sdk/plugins/categories/upload/actions/upload/utils.py +250 -0
  136. synapse_sdk/plugins/categories/upload/templates/README.md +470 -0
  137. synapse_sdk/plugins/categories/upload/templates/config.yaml +33 -0
  138. synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py +310 -0
  139. synapse_sdk/plugins/categories/upload/templates/plugin/upload.py +102 -0
  140. synapse_sdk/plugins/enums.py +3 -1
  141. synapse_sdk/plugins/models.py +148 -11
  142. synapse_sdk/plugins/templates/plugin-config-schema.json +406 -0
  143. synapse_sdk/plugins/templates/schema.json +491 -0
  144. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +1 -0
  145. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +1 -1
  146. synapse_sdk/plugins/utils/__init__.py +46 -0
  147. synapse_sdk/plugins/utils/actions.py +119 -0
  148. synapse_sdk/plugins/utils/config.py +203 -0
  149. synapse_sdk/plugins/{utils.py → utils/legacy.py} +26 -46
  150. synapse_sdk/plugins/utils/ray_gcs.py +66 -0
  151. synapse_sdk/plugins/utils/registry.py +58 -0
  152. synapse_sdk/shared/__init__.py +25 -0
  153. synapse_sdk/shared/enums.py +93 -0
  154. synapse_sdk/types.py +19 -0
  155. synapse_sdk/utils/converters/__init__.py +240 -0
  156. synapse_sdk/utils/converters/coco/__init__.py +0 -0
  157. synapse_sdk/utils/converters/coco/from_dm.py +322 -0
  158. synapse_sdk/utils/converters/coco/to_dm.py +215 -0
  159. synapse_sdk/utils/converters/dm/__init__.py +57 -0
  160. synapse_sdk/utils/converters/dm/base.py +137 -0
  161. synapse_sdk/utils/converters/dm/from_v1.py +273 -0
  162. synapse_sdk/utils/converters/dm/to_v1.py +321 -0
  163. synapse_sdk/utils/converters/dm/tools/__init__.py +214 -0
  164. synapse_sdk/utils/converters/dm/tools/answer.py +95 -0
  165. synapse_sdk/utils/converters/dm/tools/bounding_box.py +132 -0
  166. synapse_sdk/utils/converters/dm/tools/bounding_box_3d.py +121 -0
  167. synapse_sdk/utils/converters/dm/tools/classification.py +75 -0
  168. synapse_sdk/utils/converters/dm/tools/keypoint.py +117 -0
  169. synapse_sdk/utils/converters/dm/tools/named_entity.py +111 -0
  170. synapse_sdk/utils/converters/dm/tools/polygon.py +122 -0
  171. synapse_sdk/utils/converters/dm/tools/polyline.py +124 -0
  172. synapse_sdk/utils/converters/dm/tools/prompt.py +94 -0
  173. synapse_sdk/utils/converters/dm/tools/relation.py +86 -0
  174. synapse_sdk/utils/converters/dm/tools/segmentation.py +141 -0
  175. synapse_sdk/utils/converters/dm/tools/segmentation_3d.py +83 -0
  176. synapse_sdk/utils/converters/dm/types.py +168 -0
  177. synapse_sdk/utils/converters/dm/utils.py +162 -0
  178. synapse_sdk/utils/converters/dm_legacy/__init__.py +56 -0
  179. synapse_sdk/utils/converters/dm_legacy/from_v1.py +627 -0
  180. synapse_sdk/utils/converters/dm_legacy/to_v1.py +367 -0
  181. synapse_sdk/utils/converters/pascal/__init__.py +0 -0
  182. synapse_sdk/utils/converters/pascal/from_dm.py +244 -0
  183. synapse_sdk/utils/converters/pascal/to_dm.py +214 -0
  184. synapse_sdk/utils/converters/yolo/__init__.py +0 -0
  185. synapse_sdk/utils/converters/yolo/from_dm.py +384 -0
  186. synapse_sdk/utils/converters/yolo/to_dm.py +267 -0
  187. synapse_sdk/utils/dataset.py +46 -0
  188. synapse_sdk/utils/encryption.py +158 -0
  189. synapse_sdk/utils/file/__init__.py +58 -0
  190. synapse_sdk/utils/file/archive.py +32 -0
  191. synapse_sdk/utils/file/checksum.py +56 -0
  192. synapse_sdk/utils/file/chunking.py +31 -0
  193. synapse_sdk/utils/file/download.py +385 -0
  194. synapse_sdk/utils/file/encoding.py +40 -0
  195. synapse_sdk/utils/file/io.py +22 -0
  196. synapse_sdk/utils/file/upload.py +165 -0
  197. synapse_sdk/utils/file/video/__init__.py +29 -0
  198. synapse_sdk/utils/file/video/transcode.py +307 -0
  199. synapse_sdk/utils/file.py.backup +301 -0
  200. synapse_sdk/utils/http.py +138 -0
  201. synapse_sdk/utils/network.py +309 -0
  202. synapse_sdk/utils/storage/__init__.py +72 -0
  203. synapse_sdk/utils/storage/providers/__init__.py +183 -0
  204. synapse_sdk/utils/storage/providers/file_system.py +134 -0
  205. synapse_sdk/utils/storage/providers/gcp.py +13 -0
  206. synapse_sdk/utils/storage/providers/http.py +190 -0
  207. synapse_sdk/utils/storage/providers/s3.py +91 -0
  208. synapse_sdk/utils/storage/providers/sftp.py +47 -0
  209. synapse_sdk/utils/storage/registry.py +17 -0
  210. synapse_sdk-2025.12.3.dist-info/METADATA +123 -0
  211. synapse_sdk-2025.12.3.dist-info/RECORD +279 -0
  212. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/WHEEL +1 -1
  213. synapse_sdk/clients/backend/dataset.py +0 -51
  214. synapse_sdk/plugins/categories/import/actions/import.py +0 -10
  215. synapse_sdk/plugins/cli/__init__.py +0 -21
  216. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
  217. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
  218. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
  219. synapse_sdk/utils/file.py +0 -168
  220. synapse_sdk/utils/storage.py +0 -91
  221. synapse_sdk-1.0.0a23.dist-info/METADATA +0 -44
  222. synapse_sdk-1.0.0a23.dist-info/RECORD +0 -114
  223. /synapse_sdk/{plugins/cli → cli/plugin}/run.py +0 -0
  224. /synapse_sdk/{plugins/categories/import → clients/validators}/__init__.py +0 -0
  225. /synapse_sdk/{plugins/categories/import/actions → devtools}/__init__.py +0 -0
  226. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/entry_points.txt +0 -0
  227. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info/licenses}/LICENSE +0 -0
  228. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,128 @@
1
+ """Main Streamlit DevTools Application."""
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ import streamlit as st
7
+
8
+ from synapse_sdk.clients.backend import BackendClient
9
+ from synapse_sdk.devtools.config import get_backend_config, load_devtools_config
10
+
11
+ from .services import JobService, PluginService, ServeService
12
+ from .ui import ConfigTab, DeploymentTab, HttpTab, JobsTab, ServeTab
13
+ from .ui.status_bar import StatusBar
14
+ from .utils import CUSTOM_CSS
15
+
16
+
17
+ class DevToolsApp:
18
+ """Main DevTools application class."""
19
+
20
+ def __init__(self):
21
+ self.plugin_directory = Path.cwd()
22
+
23
+ # Initialize backend client and agent info
24
+ self.backend_client = self._init_backend_client()
25
+ self.agent_id = self._get_agent_id()
26
+ self.agent_info = self._get_agent_info()
27
+
28
+ # Initialize services
29
+ self.plugin_service = PluginService(self.plugin_directory, self.backend_client)
30
+ self.job_service = JobService(self.backend_client)
31
+ self.serve_service = ServeService(self.backend_client)
32
+
33
+ # Initialize UI components
34
+ self.status_bar = StatusBar(
35
+ backend_client=self.backend_client, agent_id=self.agent_id, agent_info=self.agent_info
36
+ )
37
+ self.config_tab = ConfigTab(self.plugin_service)
38
+ self.http_tab = HttpTab(self.plugin_service, self.agent_id)
39
+ self.deployment_tab = DeploymentTab(self.plugin_service)
40
+ self.jobs_tab = JobsTab(self.job_service, self.agent_id, self.agent_info)
41
+ self.serve_tab = ServeTab(self.serve_service, self.agent_id, self.agent_info)
42
+
43
+ def _init_backend_client(self) -> Optional[BackendClient]:
44
+ """Initialize backend client from configuration."""
45
+ config = get_backend_config()
46
+ if config:
47
+ return BackendClient(config['host'], access_token=config['token'])
48
+ return None
49
+
50
+ def _get_agent_id(self) -> Optional[int]:
51
+ """Get agent ID from devtools configuration."""
52
+ devtools_config = load_devtools_config()
53
+ agent_config = devtools_config.get('agent', {})
54
+ if agent_config and 'id' in agent_config:
55
+ return agent_config['id']
56
+ return None
57
+
58
+ def _get_agent_info(self) -> Optional[Dict]:
59
+ """Get complete agent information from configuration or backend."""
60
+ devtools_config = load_devtools_config()
61
+ agent_config = devtools_config.get('agent', {})
62
+
63
+ # Return the full agent config which should include name, ip, etc.
64
+ if agent_config:
65
+ return agent_config
66
+
67
+ # If we have an agent_id but no full config, try to fetch from backend
68
+ if self.agent_id and self.backend_client:
69
+ try:
70
+ # Try to get agent details from backend
71
+ # This would require an API endpoint - for now return basic info
72
+ return {
73
+ 'id': self.agent_id,
74
+ 'name': agent_config.get('name', f'Agent-{self.agent_id}'),
75
+ 'url': agent_config.get('url', agent_config.get('ip', 'localhost')),
76
+ }
77
+ except Exception:
78
+ pass
79
+
80
+ return None
81
+
82
+ def run(self):
83
+ """Main application entry point."""
84
+ # Page configuration
85
+ st.set_page_config(
86
+ page_title='Synapse DevTools',
87
+ page_icon=None,
88
+ layout='wide',
89
+ initial_sidebar_state='collapsed',
90
+ )
91
+
92
+ # Apply custom CSS
93
+ st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
94
+
95
+ # Display status bar
96
+ self.status_bar.render()
97
+
98
+ # Initialize session state
99
+ if 'config' not in st.session_state:
100
+ st.session_state['config'] = self.plugin_service.load_config()
101
+
102
+ # Create tabs
103
+ tabs = st.tabs(['Configuration', 'HTTP Request', 'Deployment', 'Jobs', 'Serve Apps'])
104
+
105
+ with tabs[0]:
106
+ self.config_tab.render()
107
+
108
+ with tabs[1]:
109
+ self.http_tab.render()
110
+
111
+ with tabs[2]:
112
+ self.deployment_tab.render()
113
+
114
+ with tabs[3]:
115
+ self.jobs_tab.render()
116
+
117
+ with tabs[4]:
118
+ self.serve_tab.render()
119
+
120
+
121
+ def main():
122
+ """Main entry point for Streamlit app."""
123
+ app = DevToolsApp()
124
+ app.run()
125
+
126
+
127
+ if __name__ == '__main__':
128
+ main()
@@ -0,0 +1,11 @@
1
+ """Services for business logic."""
2
+
3
+ from .job_service import JobService
4
+ from .plugin_service import PluginService
5
+ from .serve_service import ServeService
6
+
7
+ __all__ = [
8
+ 'JobService',
9
+ 'PluginService',
10
+ 'ServeService',
11
+ ]
@@ -0,0 +1,233 @@
1
+ """Job service for managing job operations."""
2
+
3
+ from datetime import datetime
4
+ from typing import Dict, List, Optional
5
+
6
+ from synapse_sdk.clients.backend import BackendClient
7
+ from synapse_sdk.clients.exceptions import ClientError
8
+
9
+
10
+ class JobService:
11
+ """Service for job-related operations."""
12
+
13
+ def __init__(self, backend_client: Optional[BackendClient] = None):
14
+ self.backend_client = backend_client
15
+
16
+ def list_jobs(self, agent_id: Optional[int] = None, agent_info: Optional[Dict] = None) -> List[Dict]:
17
+ """List jobs from the backend."""
18
+ if not self.backend_client:
19
+ return []
20
+
21
+ try:
22
+ params = {}
23
+ if agent_id:
24
+ params['agent'] = agent_id
25
+
26
+ jobs_response = self.backend_client.list_jobs(params=params)
27
+
28
+ # Handle paginated response - extract results
29
+ if jobs_response is None:
30
+ return []
31
+ elif isinstance(jobs_response, dict) and 'results' in jobs_response:
32
+ jobs = jobs_response['results']
33
+ else:
34
+ jobs = jobs_response if isinstance(jobs_response, list) else []
35
+
36
+ # Remove None jobs and sort by created time (most recent first)
37
+ valid_jobs = [job for job in jobs if job is not None]
38
+ valid_jobs.sort(key=lambda job: job.get('created', ''), reverse=True)
39
+
40
+ # Try to enrich jobs with plugin names
41
+ enriched_jobs = []
42
+ for job in valid_jobs:
43
+ enriched_job = job.copy()
44
+
45
+ # Try to get plugin info
46
+ if 'plugin_release' in job:
47
+ try:
48
+ # Try to fetch plugin release details
49
+ plugin_release_response = self.backend_client.get(f'/plugin_releases/{job["plugin_release"]}/')
50
+ if plugin_release_response and isinstance(plugin_release_response, dict):
51
+ # Get version from plugin release
52
+ enriched_job['plugin_version'] = plugin_release_response.get('version')
53
+
54
+ # Try to get plugin details from the plugin ID
55
+ plugin_id = plugin_release_response.get('plugin')
56
+ if plugin_id:
57
+ try:
58
+ plugin_response = self.backend_client.get(f'/plugins/{plugin_id}/')
59
+ if plugin_response and isinstance(plugin_response, dict):
60
+ enriched_job['plugin_name'] = plugin_response.get('name')
61
+ enriched_job['plugin_code'] = plugin_response.get('code')
62
+ except Exception:
63
+ # Fallback to config if plugin fetch fails
64
+ config = plugin_release_response.get('config', {})
65
+ enriched_job['plugin_name'] = config.get('name') or config.get('code')
66
+ enriched_job['plugin_code'] = config.get('code')
67
+ else:
68
+ # Fallback to config if no plugin ID
69
+ config = plugin_release_response.get('config', {})
70
+ enriched_job['plugin_name'] = config.get('name') or config.get('code')
71
+ enriched_job['plugin_code'] = config.get('code')
72
+ except Exception:
73
+ pass
74
+
75
+ # Try to get agent info
76
+ if 'agent' in job:
77
+ # First check if we have local agent info
78
+ if agent_info and job.get('agent') == agent_id:
79
+ enriched_job['agent_name'] = agent_info.get('name')
80
+ enriched_job['agent_url'] = agent_info.get('url')
81
+ else:
82
+ # Try to fetch agent details from API
83
+ try:
84
+ agent_response = self.backend_client.get(f'/agents/{job["agent"]}/')
85
+ if agent_response and isinstance(agent_response, dict):
86
+ enriched_job['agent_name'] = agent_response.get('name')
87
+ enriched_job['agent_url'] = agent_response.get('url')
88
+ except Exception:
89
+ pass
90
+
91
+ enriched_jobs.append(enriched_job)
92
+
93
+ return enriched_jobs
94
+ except ClientError:
95
+ raise
96
+ except Exception as e:
97
+ raise Exception(f'Failed to list jobs: {e}')
98
+
99
+ def get_job_logs(self, job_id: str):
100
+ """Get all logs for a job at once."""
101
+ if not self.backend_client:
102
+ raise Exception('Backend client not configured')
103
+
104
+ try:
105
+ # Get logs from console_logs endpoint (returns list of strings)
106
+ logs_response = self.backend_client.list_job_console_logs(job_id)
107
+
108
+ # API returns a list of log strings
109
+ if isinstance(logs_response, list):
110
+ if not logs_response:
111
+ # Empty list means no logs yet
112
+ yield 'No logs available for this job yet.\n'
113
+ else:
114
+ for log_entry in logs_response:
115
+ # Each entry is already a formatted string with timestamp
116
+ yield str(log_entry) + '\n' if not str(log_entry).endswith('\n') else str(log_entry)
117
+ elif logs_response:
118
+ # Fallback for unexpected format
119
+ yield str(logs_response) + '\n'
120
+ else:
121
+ yield 'No logs available for this job yet.\n'
122
+
123
+ except Exception as e:
124
+ from synapse_sdk.clients.exceptions import ClientError
125
+
126
+ # Check if it's a ClientError with specific status
127
+ if isinstance(e, ClientError):
128
+ if e.status == 404:
129
+ yield 'Job not found or logs not available.\n'
130
+ elif e.status == 401:
131
+ yield 'Unauthorized to access job logs.\n'
132
+ else:
133
+ # Check for Korean error message in the reason
134
+ error_str = str(e.reason) if hasattr(e, 'reason') else str(e)
135
+ if '찾을 수 없습니다' in error_str:
136
+ yield 'Job logs not found. The job may not have generated any logs yet.\n'
137
+ else:
138
+ yield f'Failed to fetch logs: {error_str}\n'
139
+ else:
140
+ error_str = str(e)
141
+ if '찾을 수 없습니다' in error_str or '404' in error_str or 'Not found' in error_str:
142
+ yield 'Job logs not found. The job may not have generated any logs yet.\n'
143
+ elif '401' in error_str or 'Unauthorized' in error_str:
144
+ yield 'Unauthorized to access job logs.\n'
145
+ else:
146
+ yield f'Failed to fetch logs: {error_str}\n'
147
+
148
+ def stream_job_logs(self, job_id: str):
149
+ """Stream logs for a job (simulated streaming since tail endpoint doesn't exist)."""
150
+ if not self.backend_client:
151
+ raise Exception('Backend client not configured')
152
+
153
+ # Since tail_console_logs endpoint returns 404, we'll simulate streaming
154
+ # by fetching all logs and yielding them one by one
155
+ try:
156
+ from synapse_sdk.clients.exceptions import ClientError
157
+
158
+ # Get logs from console_logs endpoint (returns list of strings)
159
+ logs_response = self.backend_client.list_job_console_logs(job_id)
160
+
161
+ # API returns a list of log strings
162
+ if isinstance(logs_response, list):
163
+ if not logs_response:
164
+ # Empty list means no logs yet
165
+ yield 'No logs available for this job yet.\n'
166
+ else:
167
+ # Yield logs one by one to simulate streaming
168
+ for log_entry in logs_response:
169
+ # Each entry is already a formatted string with timestamp
170
+ yield str(log_entry) + '\n' if not str(log_entry).endswith('\n') else str(log_entry)
171
+ elif logs_response:
172
+ # Fallback for unexpected format
173
+ yield str(logs_response) + '\n'
174
+ else:
175
+ yield 'No logs available for this job yet.\n'
176
+
177
+ except Exception as e:
178
+ # Check if it's a ClientError with specific status
179
+ if isinstance(e, ClientError):
180
+ if e.status == 404:
181
+ yield 'Job not found or logs not available.\n'
182
+ elif e.status == 401:
183
+ yield 'Unauthorized to access job logs.\n'
184
+ else:
185
+ # Check for Korean error message in the reason
186
+ error_str = str(e.reason) if hasattr(e, 'reason') else str(e)
187
+ if '찾을 수 없습니다' in error_str:
188
+ yield 'Job logs not found. The job may not have generated any logs yet.\n'
189
+ else:
190
+ yield f'Failed to stream logs: {error_str}\n'
191
+ else:
192
+ error_str = str(e)
193
+ if '찾을 수 없습니다' in error_str or '404' in error_str or 'Not found' in error_str:
194
+ yield 'Job logs not found. The job may not have generated any logs yet.\n'
195
+ elif '401' in error_str or 'Unauthorized' in error_str:
196
+ yield 'Unauthorized to access job logs.\n'
197
+ else:
198
+ yield f'Failed to stream logs: {error_str}\n'
199
+
200
+ @staticmethod
201
+ def format_duration(created: str, completed: Optional[str] = None) -> str:
202
+ """Format duration between created and completed timestamps."""
203
+ if not created:
204
+ return '-'
205
+
206
+ try:
207
+ created_dt = datetime.fromisoformat(created.replace('+09:00', '+09:00'))
208
+ if completed:
209
+ completed_dt = datetime.fromisoformat(completed.replace('+09:00', '+09:00'))
210
+ duration = completed_dt - created_dt
211
+ total_seconds = int(duration.total_seconds())
212
+ if total_seconds < 60:
213
+ return f'{total_seconds}s'
214
+ else:
215
+ minutes = total_seconds // 60
216
+ seconds = total_seconds % 60
217
+ return f'{minutes}m {seconds}s'
218
+ else:
219
+ return '...'
220
+ except Exception:
221
+ return '-'
222
+
223
+ @staticmethod
224
+ def format_timestamp(timestamp: str) -> str:
225
+ """Format timestamp for display."""
226
+ if not timestamp:
227
+ return '-'
228
+
229
+ try:
230
+ dt = datetime.fromisoformat(timestamp.replace('+09:00', '+09:00'))
231
+ return dt.strftime('%m/%d %H:%M')
232
+ except Exception:
233
+ return timestamp[:16] if len(timestamp) > 16 else timestamp
@@ -0,0 +1,236 @@
1
+ """Plugin service for managing plugin operations."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Dict, Optional
8
+
9
+ import yaml
10
+
11
+ from synapse_sdk.cli.plugin.publish import _publish
12
+ from synapse_sdk.clients.backend import BackendClient
13
+
14
+
15
+ class PluginService:
16
+ """Service for plugin-related operations."""
17
+
18
+ def __init__(self, plugin_directory: Path, backend_client: Optional[BackendClient] = None):
19
+ self.plugin_directory = plugin_directory
20
+ self.config_path = plugin_directory / 'config.yaml'
21
+ self.test_http_path = plugin_directory / 'test.http'
22
+ self.backend_client = backend_client
23
+
24
+ def load_config(self) -> Dict:
25
+ """Load plugin configuration from config.yaml."""
26
+ if not self.config_path.exists():
27
+ return {}
28
+
29
+ try:
30
+ with open(self.config_path, 'r') as f:
31
+ return yaml.safe_load(f)
32
+ except Exception:
33
+ return {}
34
+
35
+ def save_config(self, config: Dict) -> bool:
36
+ """Save plugin configuration to config.yaml."""
37
+ try:
38
+ # Backup existing config
39
+ if self.config_path.exists():
40
+ backup_path = self.config_path.with_suffix('.yaml.bak')
41
+ self.config_path.rename(backup_path)
42
+
43
+ # Write new config
44
+ with open(self.config_path, 'w') as f:
45
+ yaml.dump(config, f, default_flow_style=False, allow_unicode=True, indent=2)
46
+
47
+ return True
48
+ except Exception:
49
+ # Restore backup if write failed
50
+ backup_path = self.config_path.with_suffix('.yaml.bak')
51
+ if backup_path.exists():
52
+ backup_path.rename(self.config_path)
53
+ return False
54
+
55
+ def parse_test_http(self) -> Dict[str, Dict]:
56
+ """Parse test.http file and extract action parameters."""
57
+ if not self.test_http_path.exists():
58
+ return {}
59
+
60
+ requests = {}
61
+ current_request_name = None
62
+ current_body_lines = []
63
+ in_body = False
64
+
65
+ try:
66
+ with open(self.test_http_path, 'r') as f:
67
+ for line in f:
68
+ line = line.strip()
69
+ if line.startswith('###'):
70
+ # Process previous request
71
+ if current_request_name and current_body_lines:
72
+ try:
73
+ full_request = json.loads(''.join(current_body_lines))
74
+ if 'params' in full_request:
75
+ requests[current_request_name] = full_request['params']
76
+ except json.JSONDecodeError:
77
+ pass
78
+
79
+ # Start new request
80
+ current_request_name = line.replace('###', '').strip()
81
+ current_body_lines = []
82
+ in_body = False
83
+ elif current_request_name and line.startswith('{'):
84
+ in_body = True
85
+ current_body_lines.append(line)
86
+ elif in_body and current_request_name:
87
+ current_body_lines.append(line)
88
+
89
+ # Process last request
90
+ if current_request_name and current_body_lines:
91
+ try:
92
+ full_request = json.loads(''.join(current_body_lines))
93
+ if 'params' in full_request:
94
+ requests[current_request_name] = full_request['params']
95
+ except json.JSONDecodeError:
96
+ pass
97
+
98
+ return requests
99
+ except Exception:
100
+ return {}
101
+
102
+ def update_test_http_params(self, action: str, new_params: Dict) -> bool:
103
+ """Update parameters for a specific action in test.http."""
104
+ if not self.test_http_path.exists():
105
+ return False
106
+
107
+ try:
108
+ with open(self.test_http_path, 'r') as f:
109
+ lines = f.readlines()
110
+
111
+ current_action = None
112
+ in_json = False
113
+ json_start_line = -1
114
+ json_lines = []
115
+ updated = False
116
+
117
+ for i, line in enumerate(lines):
118
+ if line.strip().startswith('###'):
119
+ current_action = line.replace('###', '').strip()
120
+ in_json = False
121
+ json_lines = []
122
+ elif current_action and line.strip().startswith('{'):
123
+ in_json = True
124
+ json_start_line = i
125
+ json_lines = [line]
126
+ elif in_json:
127
+ json_lines.append(line)
128
+ if line.strip() == '}':
129
+ if current_action == action:
130
+ try:
131
+ full_json = json.loads(''.join(json_lines))
132
+ full_json['params'] = new_params
133
+ new_json = json.dumps(full_json, indent=2)
134
+ lines[json_start_line : i + 1] = [new_json + '\n']
135
+ updated = True
136
+ break
137
+ except json.JSONDecodeError:
138
+ pass
139
+ in_json = False
140
+ json_lines = []
141
+
142
+ if updated:
143
+ with open(self.test_http_path, 'w') as f:
144
+ f.writelines(lines)
145
+ return True
146
+
147
+ return False
148
+ except Exception:
149
+ return False
150
+
151
+ def execute_plugin_action(
152
+ self, action: str, params: Dict, plugin_code: str, agent_id: Optional[int] = None, debug: bool = True
153
+ ) -> Dict:
154
+ """Execute a plugin action via HTTP request."""
155
+ if not self.backend_client:
156
+ return {'success': False, 'error': 'Backend client not configured'}
157
+
158
+ if not plugin_code:
159
+ return {'success': False, 'error': 'Plugin code not found in configuration'}
160
+
161
+ plugin_url = f'{self.backend_client.base_url}/plugins/{plugin_code}/run/'
162
+
163
+ headers = {
164
+ 'Accept': 'application/json; indent=4',
165
+ 'Content-Type': 'application/json',
166
+ }
167
+
168
+ # Get auth headers from backend client
169
+ auth_headers = self.backend_client._get_headers()
170
+ headers.update(auth_headers)
171
+
172
+ payload = {
173
+ 'agent': agent_id or 2,
174
+ 'action': action,
175
+ 'params': params,
176
+ 'debug': debug,
177
+ }
178
+
179
+ start_time = time.time()
180
+
181
+ try:
182
+ import requests
183
+
184
+ response = requests.post(plugin_url, json=payload, headers=headers, timeout=30)
185
+ execution_time = int((time.time() - start_time) * 1000)
186
+
187
+ try:
188
+ response_data = response.json()
189
+ except Exception:
190
+ response_data = response.text
191
+
192
+ return {
193
+ 'success': response.status_code < 400,
194
+ 'status_code': response.status_code,
195
+ 'response_data': response_data,
196
+ 'execution_time': execution_time,
197
+ 'url': plugin_url,
198
+ 'method': 'POST',
199
+ 'headers': headers,
200
+ 'payload': payload,
201
+ }
202
+ except Exception as e:
203
+ execution_time = int((time.time() - start_time) * 1000)
204
+ return {
205
+ 'success': False,
206
+ 'status_code': 500,
207
+ 'error': str(e),
208
+ 'execution_time': execution_time,
209
+ 'url': plugin_url,
210
+ }
211
+
212
+ def publish_plugin(self, host: str, access_token: str, debug: bool = True) -> Dict:
213
+ """Publish plugin to Synapse platform."""
214
+ original_cwd = os.getcwd()
215
+ try:
216
+ os.chdir(str(self.plugin_directory))
217
+
218
+ debug_modules = os.getenv('SYNAPSE_DEBUG_MODULES', '')
219
+ plugin_release = _publish(host, access_token, debug, debug_modules)
220
+
221
+ return {
222
+ 'success': True,
223
+ 'message': (
224
+ f'Successfully published "{plugin_release.name}" ({plugin_release.code}) to synapse backend!'
225
+ ),
226
+ 'plugin_code': plugin_release.code,
227
+ 'version': plugin_release.version,
228
+ 'name': plugin_release.name,
229
+ }
230
+ except Exception as e:
231
+ return {
232
+ 'success': False,
233
+ 'error': str(e),
234
+ }
235
+ finally:
236
+ os.chdir(original_cwd)