synapse-sdk 1.0.0b3__py3-none-any.whl → 1.0.0b5__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.
@@ -273,7 +273,7 @@ def cli(ctx, dev_tools):
273
273
  message='Select an option:',
274
274
  choices=[
275
275
  ('🌐 Run Dev Tools', 'devtools'),
276
- ('💻 Code-Server IDE', 'code_server'),
276
+ ('💻 Open Code-Server IDE', 'code_server'),
277
277
  ('⚙️ Configuration', 'config'),
278
278
  ('🔌 Plugin Management', 'plugin'),
279
279
  ('🚪 Exit', 'exit'),
@@ -1,26 +1,37 @@
1
1
  """Code-server integration for remote plugin development."""
2
2
 
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
3
7
  from typing import Optional
8
+ from urllib.parse import quote
4
9
 
5
10
  import click
11
+ import inquirer
12
+ import yaml
6
13
 
7
14
  from synapse_sdk.cli.config import fetch_agents_from_backend, get_agent_config
8
15
  from synapse_sdk.devtools.config import get_backend_config
16
+ from synapse_sdk.utils.encryption import encrypt_plugin, get_plugin_info, is_plugin_directory
9
17
 
10
18
 
11
- @click.command()
12
- @click.option('--agent', help='Agent name or ID')
13
- @click.option('--open-browser/--no-open-browser', default=True, help='Open in browser')
14
- def code_server(agent: Optional[str], open_browser: bool):
15
- """Connect to web-based code-server on an agent for plugin development."""
19
+ def get_agent_client(agent: Optional[str] = None):
20
+ """Helper function to get an agent client.
16
21
 
22
+ Args:
23
+ agent: Optional agent ID. If not provided, uses current agent or prompts user.
24
+
25
+ Returns:
26
+ tuple: (AgentClient instance, agent_id) or (None, None) if failed
27
+ """
17
28
  # Get current agent configuration
18
29
  agent_config = get_agent_config()
19
30
  backend_config = get_backend_config()
20
31
 
21
32
  if not backend_config:
22
33
  click.echo("❌ No backend configured. Run 'synapse config' first.")
23
- return
34
+ return None, None
24
35
 
25
36
  # If no agent specified, use current agent or let user choose
26
37
  if not agent:
@@ -32,7 +43,7 @@ def code_server(agent: Optional[str], open_browser: bool):
32
43
  agents, error = fetch_agents_from_backend()
33
44
  if not agents:
34
45
  click.echo('❌ No agents available. Check your backend configuration.')
35
- return
46
+ return None, None
36
47
 
37
48
  if len(agents) == 1:
38
49
  # If only one agent, use it
@@ -52,14 +63,13 @@ def code_server(agent: Optional[str], open_browser: bool):
52
63
  agent = agents[choice - 1]['id']
53
64
  else:
54
65
  click.echo('❌ Invalid selection')
55
- return
66
+ return None, None
56
67
  except (ValueError, EOFError, KeyboardInterrupt):
57
68
  click.echo('\n❌ Cancelled')
58
- return
69
+ return None, None
59
70
 
60
- # Connect to agent and get code-server info
71
+ # Get agent details from backend
61
72
  try:
62
- # Get agent details from backend to get the agent URL
63
73
  from synapse_sdk.clients.backend import BackendClient
64
74
 
65
75
  backend_client = BackendClient(backend_config['host'], access_token=backend_config['token'])
@@ -70,100 +80,336 @@ def code_server(agent: Optional[str], open_browser: bool):
70
80
  except Exception as e:
71
81
  click.echo(f'❌ Failed to get agent information for: {agent}')
72
82
  click.echo(f'Error: {e}')
73
- return
83
+ return None, None
74
84
 
75
85
  if not agent_info or not agent_info.get('url'):
76
86
  click.echo(f'❌ Agent {agent} does not have a valid URL')
77
- click.echo(f'Agent info: {agent_info}')
78
- return
87
+ return None, None
79
88
 
80
89
  # Get the agent token from local configuration
81
90
  agent_token = agent_config.get('token')
82
91
  if not agent_token:
83
92
  click.echo('❌ No agent token found in configuration')
84
93
  click.echo("Run 'synapse config' to configure the agent")
85
- return
94
+ return None, None
86
95
 
87
96
  # Create agent client
88
97
  from synapse_sdk.clients.agent import AgentClient
89
98
 
90
99
  client = AgentClient(base_url=agent_info['url'], agent_token=agent_token, user_token=backend_config['token'])
100
+ return client, agent
91
101
 
92
- # Get code-server information
93
- try:
94
- info = client.get_code_server_info()
95
- except AttributeError:
96
- # Fallback to direct API call if method doesn't exist
97
- response = client._get('code-server/info/')
98
- info = response if isinstance(response, dict) else {}
99
- except Exception as e:
100
- # Handle other errors
101
- click.echo(f'❌ Failed to get code-server info: {e}')
102
- click.echo(f'Agent URL: {agent_info.get("url")}')
103
- click.echo('\nNote: The agent might not have code-server endpoint implemented yet.')
104
- return
102
+ except Exception as e:
103
+ click.echo(f'❌ Failed to connect to agent: {e}')
104
+ return None, None
105
105
 
106
- if not info or not info.get('available', False):
107
- message = info.get('message', 'Code-server is not available') if info else 'Failed to get code-server info'
108
- click.echo(f'❌ {message}')
109
- click.echo('\nTo enable code-server, reinstall the agent with code-server support.')
110
- return
111
106
 
112
- # Display connection information
113
- click.echo('\n✅ Code-Server is available!')
107
+ def detect_and_encrypt_plugin(workspace_path: str) -> Optional[dict]:
108
+ """Detect and encrypt plugin code in the workspace.
114
109
 
115
- workspace = info.get('workspace', '/home/coder/workspace')
110
+ Args:
111
+ workspace_path: Path to check for plugin
116
112
 
117
- # Show web browser access
118
- click.echo('\n🌐 Web-based VS Code:')
119
- click.echo(f' URL: {info["url"]}')
120
- password = info.get('password')
121
- if password:
122
- click.echo(f' Password: {password}')
123
- else:
124
- click.echo(' Password: Not required (passwordless mode)')
113
+ Returns:
114
+ dict: Encrypted plugin data or None if no plugin found
115
+ """
116
+ plugin_path = Path(workspace_path)
117
+
118
+ if not is_plugin_directory(plugin_path):
119
+ return None
120
+
121
+ try:
122
+ plugin_info = get_plugin_info(plugin_path)
123
+ click.echo(f'🔍 Detected plugin: {plugin_info["name"]}')
124
+
125
+ if 'version' in plugin_info:
126
+ click.echo(f' Version: {plugin_info["version"]}')
127
+ if 'description' in plugin_info:
128
+ click.echo(f' Description: {plugin_info["description"]}')
129
+
130
+ click.echo('🔐 Encrypting plugin code...')
131
+ encrypted_package, password = encrypt_plugin(plugin_path)
132
+
133
+ # Add password to the package (in real implementation, this would be handled securely)
134
+ encrypted_package['password'] = password
135
+
136
+ click.echo('✅ Plugin code encrypted successfully')
137
+ return encrypted_package
138
+
139
+ except Exception as e:
140
+ click.echo(f'❌ Failed to encrypt plugin: {e}')
141
+ return None
142
+
143
+
144
+ def is_code_server_installed() -> bool:
145
+ """Check if code-server is installed locally.
146
+
147
+ Returns:
148
+ bool: True if code-server is available in PATH
149
+ """
150
+ return shutil.which('code-server') is not None
151
+
152
+
153
+ def get_code_server_port() -> int:
154
+ """Get code-server port from config file.
125
155
 
126
- click.echo(f'\n📁 Workspace: {workspace}')
156
+ Returns:
157
+ int: Port number from config, defaults to 8880 if not found
158
+ """
159
+ config_path = Path.home() / '.config' / 'code-server' / 'config.yaml'
127
160
 
128
- # Optionally open in browser
129
- if open_browser and info.get('url'):
130
- import subprocess
161
+ try:
162
+ if config_path.exists():
163
+ with open(config_path, 'r') as f:
164
+ config = yaml.safe_load(f)
165
+
166
+ # Parse bind-addr which can be in format "127.0.0.1:8880" or just ":8880"
167
+ bind_addr = config.get('bind-addr', '')
168
+ if ':' in bind_addr:
169
+ port_str = bind_addr.split(':')[-1]
170
+ try:
171
+ return int(port_str)
172
+ except ValueError:
173
+ pass
174
+ except Exception:
175
+ # If any error occurs reading config, fall back to default
176
+ pass
131
177
 
132
- click.echo('\nAttempting to open in browser...')
178
+ # Default port if config not found or invalid
179
+ return 8880
133
180
 
134
- # Try to open browser, suppressing stderr to avoid xdg-open noise
181
+
182
+ def launch_local_code_server(workspace_path: str, open_browser: bool = True) -> None:
183
+ """Launch local code-server instance.
184
+
185
+ Args:
186
+ workspace_path: Directory to open in code-server
187
+ open_browser: Whether to open browser automatically
188
+ """
189
+ try:
190
+ # Get port from config
191
+ port = get_code_server_port()
192
+
193
+ # Create URL with folder query parameter
194
+ encoded_path = quote(workspace_path)
195
+ url_with_folder = f'http://localhost:{port}/?folder={encoded_path}'
196
+
197
+ # Basic code-server command - let code-server handle the workspace internally
198
+ cmd = ['code-server', workspace_path]
199
+
200
+ if not open_browser:
201
+ cmd.append('--disable-getting-started-override')
202
+
203
+ click.echo(f'🚀 Starting local code-server for workspace: {workspace_path}')
204
+ click.echo(f' URL: {url_with_folder}')
205
+ click.echo(' Press Ctrl+C to stop the server')
206
+
207
+ # Start code-server in background if we need to open browser
208
+ if open_browser:
209
+ # Start code-server in background
210
+ import threading
211
+ import time
212
+
213
+ def start_server():
214
+ subprocess.run(cmd)
215
+
216
+ server_thread = threading.Thread(target=start_server, daemon=True)
217
+ server_thread.start()
218
+
219
+ # Give server a moment to start
220
+ click.echo(' Waiting for server to start...')
221
+ time.sleep(3)
222
+
223
+ # Open browser with folder parameter
135
224
  try:
136
- # Use subprocess to suppress xdg-open errors
137
- result = subprocess.run(['xdg-open', info['url']], capture_output=True, text=True, timeout=2)
225
+ result = subprocess.run(['xdg-open', url_with_folder], capture_output=True, text=True, timeout=2)
138
226
  if result.returncode != 0:
139
227
  click.echo('⚠️ Could not open browser automatically (no display?)')
140
- click.echo(f'👉 Please manually open: {info["url"]}')
228
+ click.echo(f'👉 Please manually open: {url_with_folder}')
229
+ else:
230
+ click.echo('✅ Browser opened successfully')
141
231
  except (subprocess.TimeoutExpired, FileNotFoundError):
142
- # xdg-open not available or timed out
143
232
  click.echo('⚠️ Could not open browser (headless environment)')
144
- click.echo(f'👉 Please manually open: {info["url"]}')
233
+ click.echo(f'👉 Please manually open: {url_with_folder}')
145
234
  except Exception:
146
- # Fallback for other errors
147
- click.echo(f'👉 Please manually open: {info["url"]}')
235
+ click.echo(f'👉 Please manually open: {url_with_folder}')
236
+
237
+ # Wait for the server thread (blocking)
238
+ try:
239
+ server_thread.join()
240
+ except KeyboardInterrupt:
241
+ click.echo('\n\n✅ Code-server stopped')
242
+ else:
243
+ # Start code-server normally (blocking)
244
+ subprocess.run(cmd)
245
+
246
+ except KeyboardInterrupt:
247
+ click.echo('\n\n✅ Code-server stopped')
248
+ except Exception as e:
249
+ click.echo(f'❌ Failed to start local code-server: {e}')
250
+
251
+
252
+ def show_code_server_installation_help() -> None:
253
+ """Show installation instructions for code-server."""
254
+ click.echo('\n❌ Code-server is not installed locally')
255
+ click.echo('\n📦 To install code-server, choose one of these options:')
256
+ click.echo('\n1. Install script (recommended):')
257
+ click.echo(' curl -fsSL https://code-server.dev/install.sh | sh')
258
+ click.echo('\n2. Using npm:')
259
+ click.echo(' npm install -g code-server')
260
+ click.echo('\n3. Using yarn:')
261
+ click.echo(' yarn global add code-server')
262
+ click.echo('\n4. Download from releases:')
263
+ click.echo(' https://github.com/coder/code-server/releases')
264
+ click.echo('\n📚 For more installation options, visit: https://coder.com/docs/code-server/latest/install')
265
+
148
266
 
149
- # Show additional instructions
150
- click.echo('\n📝 Quick Start:')
151
- click.echo('1. Open the URL in your browser')
152
- click.echo('2. Enter the password if prompted')
153
- click.echo('3. Start coding in the web-based VS Code!')
267
+ def run_agent_code_server(agent: Optional[str], workspace: str, open_browser: bool) -> None:
268
+ """Run code-server through agent (existing functionality).
154
269
 
270
+ Args:
271
+ agent: Agent name or ID
272
+ workspace: Workspace directory path
273
+ open_browser: Whether to open browser automatically
274
+ """
275
+ client, _ = get_agent_client(agent)
276
+ if not client:
277
+ return
278
+
279
+ # Check for plugin and show info if found
280
+ plugin_data = detect_and_encrypt_plugin(workspace)
281
+ if plugin_data:
282
+ click.echo('📦 Plugin detected and encrypted for secure transfer')
283
+
284
+ # Get code-server information
285
+ try:
286
+ info = client.get_code_server_info(workspace_path=workspace)
155
287
  except Exception as e:
156
- import traceback
288
+ # Handle other errors
289
+ click.echo(f'❌ Failed to get code-server info: {e}')
290
+ click.echo('\nNote: The agent might not have code-server endpoint implemented yet.')
291
+ return
157
292
 
158
- click.echo(f'❌ Failed to connect to agent: {e}')
293
+ # Ensure info is a dictionary
294
+ if not isinstance(info, dict):
295
+ click.echo('❌ Invalid response from agent')
296
+ return
297
+
298
+ if not info.get('available', False):
299
+ message = info.get('message', 'Code-server is not available')
300
+ click.echo(f'❌ {message}')
301
+ click.echo('\nTo enable code-server, reinstall the agent with code-server support.')
302
+ return
303
+
304
+ # Display connection information
305
+ click.echo('\n✅ Code-Server is available!')
306
+
307
+ # Get the workspace path from response or use the requested one
308
+ actual_workspace = info.get('workspace', workspace)
309
+
310
+ # Show web browser access
311
+ click.echo('\n🌐 Web-based VS Code:')
312
+ url = info.get('url')
313
+ if not url:
314
+ click.echo('❌ No URL provided by agent')
315
+ return
316
+
317
+ click.echo(f' URL: {url}')
318
+ password = info.get('password')
319
+ if password:
320
+ click.echo(f' Password: {password}')
321
+ else:
322
+ click.echo(' Password: Not required (passwordless mode)')
323
+
324
+ # Show workspace information with better context
325
+ click.echo(f'\n📁 Agent Workspace: {actual_workspace}')
326
+ click.echo(f'📂 Local Project: {workspace}')
327
+
328
+ # Only show warning if the paths are drastically different and it's not the expected container path
329
+ if actual_workspace != workspace and not actual_workspace.startswith('/home/coder'):
330
+ click.echo(' ⚠️ Note: Agent workspace differs from local project path')
331
+
332
+ # Optionally open in browser
333
+ if open_browser and url:
334
+ click.echo('\nAttempting to open in browser...')
159
335
 
160
- # Show more detailed error in debug mode
161
- if '--debug' in click.get_current_context().params:
162
- click.echo('\nDebug trace:')
163
- click.echo(traceback.format_exc())
336
+ # Try to open browser, suppressing stderr to avoid xdg-open noise
337
+ try:
338
+ # Use subprocess to suppress xdg-open errors
339
+ result = subprocess.run(['xdg-open', url], capture_output=True, text=True, timeout=2)
340
+ if result.returncode != 0:
341
+ click.echo('⚠️ Could not open browser automatically (no display?)')
342
+ click.echo(f'👉 Please manually open: {url}')
343
+ except (subprocess.TimeoutExpired, FileNotFoundError):
344
+ # xdg-open not available or timed out
345
+ click.echo('⚠️ Could not open browser (headless environment)')
346
+ click.echo(f'👉 Please manually open: {url}')
347
+ except Exception:
348
+ # Fallback for other errors
349
+ click.echo(f'👉 Please manually open: {url}')
350
+
351
+ # Show additional instructions
352
+ click.echo('\n📝 Quick Start:')
353
+ click.echo('1. Open the URL in your browser')
354
+ click.echo('2. Enter the password if prompted')
355
+ click.echo('3. Start coding in the web-based VS Code!')
356
+
357
+ # Add note about workspace synchronization
358
+ if actual_workspace.startswith('/home/coder'):
359
+ click.echo("\n💡 Note: Your local project files will be available in the agent's workspace.")
360
+ click.echo(' Changes made in code-server will be reflected in your local project.')
361
+
362
+
363
+ @click.command()
364
+ @click.option('--agent', help='Agent name or ID')
365
+ @click.option('--open-browser/--no-open-browser', default=True, help='Open in browser')
366
+ @click.option('--workspace', help='Workspace directory path (defaults to current directory)')
367
+ def code_server(agent: Optional[str], open_browser: bool, workspace: Optional[str]):
368
+ """Open code-server either through agent or locally."""
369
+
370
+ # Get current working directory if workspace not specified
371
+ if not workspace:
372
+ workspace = os.getcwd()
373
+
374
+ click.echo(f'Using workspace: {workspace}')
375
+
376
+ # Check if local code-server is available
377
+ local_available = is_code_server_installed()
378
+
379
+ # Create menu options based on availability
380
+ choices = []
164
381
 
165
- click.echo('\nTroubleshooting:')
166
- click.echo('1. Check if agent is running and accessible')
167
- click.echo('2. Verify the agent has code-server support (may need agent update)')
168
- click.echo('3. Check agent logs for more details')
169
- click.echo('4. Try running with --no-open-browser to see connection details')
382
+ # Always offer agent option
383
+ choices.append(('Open code-server through agent', 'agent'))
384
+
385
+ # Add local option if available
386
+ if local_available:
387
+ choices.append(('Open local code-server', 'local'))
388
+ else:
389
+ choices.append(('Install local code-server (not installed)', 'install'))
390
+
391
+ choices.append(('Cancel', 'cancel'))
392
+
393
+ # Show selection menu
394
+ questions = [inquirer.List('option', message='How would you like to open code-server?', choices=choices)]
395
+
396
+ try:
397
+ answers = inquirer.prompt(questions)
398
+ if not answers or answers['option'] == 'cancel':
399
+ click.echo('Cancelled')
400
+ return
401
+
402
+ if answers['option'] == 'agent':
403
+ click.echo('\n🤖 Opening code-server through agent...')
404
+ run_agent_code_server(agent, workspace, open_browser)
405
+
406
+ elif answers['option'] == 'local':
407
+ click.echo('\n💻 Starting local code-server...')
408
+ launch_local_code_server(workspace, open_browser)
409
+
410
+ elif answers['option'] == 'install':
411
+ show_code_server_installation_help()
412
+
413
+ except (KeyboardInterrupt, EOFError):
414
+ click.echo('\n\nCancelled')
415
+ return
@@ -9,3 +9,18 @@ class CoreClientMixin(BaseClient):
9
9
  def get_metrics(self, panel):
10
10
  path = f'metrics/{panel}/'
11
11
  return self._get(path)
12
+
13
+ def get_code_server_info(self, workspace_path=None):
14
+ """Get code-server connection information from the agent.
15
+
16
+ Args:
17
+ workspace_path: Optional path to set as the workspace directory
18
+
19
+ Returns:
20
+ dict: Code-server connection information
21
+ """
22
+ path = 'code-server/info/'
23
+ params = {}
24
+ if workspace_path:
25
+ params['workspace'] = workspace_path
26
+ return self._get(path, params=params)
@@ -40,7 +40,7 @@ class ExportRun(Run):
40
40
  """Log export file information.
41
41
 
42
42
  Args:
43
- log_type (str): The type of log ('export_data_file' or 'export_original_file').
43
+ log_type (str): The type of log ('export_data_file' or 'export_original_file' or 'etc').
44
44
  target_id (int): The ID of the data file.
45
45
  data_file_info (dict): The JSON info of the data file.
46
46
  status (ExportStatus): The status of the data file.
@@ -88,6 +88,16 @@ class ExportRun(Run):
88
88
  """Log export origin data file."""
89
89
  self.log_file('export_original_file', target_id, data_file_info, status, error)
90
90
 
91
+ def export_log_etc_file(
92
+ self,
93
+ target_id: int,
94
+ data_file_info: dict,
95
+ status: ExportStatus = ExportStatus.STAND_BY,
96
+ error: str | None = None,
97
+ ):
98
+ """Log export etc file."""
99
+ self.log_file('etc', target_id, data_file_info, status, error)
100
+
91
101
 
92
102
  class ExportTargetHandler(ABC):
93
103
  """
@@ -8,6 +8,7 @@ from pydantic import AfterValidator, BaseModel, field_validator
8
8
  from pydantic_core import PydanticCustomError
9
9
 
10
10
  from synapse_sdk.clients.backend import BackendClient
11
+ from synapse_sdk.clients.backend.models import JobStatus
11
12
  from synapse_sdk.clients.exceptions import ClientError
12
13
  from synapse_sdk.plugins.categories.base import Action
13
14
  from synapse_sdk.plugins.categories.decorators import register_action
@@ -35,6 +36,14 @@ class CriticalError(Exception):
35
36
  super().__init__(self.message)
36
37
 
37
38
 
39
+ class PreAnnotationToTaskFailed(Exception):
40
+ """Pre-annotation to task failed."""
41
+
42
+ def __init__(self, message: str = 'Pre-annotation to task failed'):
43
+ self.message = message
44
+ super().__init__(self.message)
45
+
46
+
38
47
  class ToTaskRun(Run):
39
48
  class AnnotateTaskEventLog(BaseModel):
40
49
  """Annotate task event log model."""
@@ -255,6 +264,25 @@ class ToTaskParams(BaseModel):
255
264
  return value
256
265
 
257
266
 
267
+ class ToTaskResult(BaseModel):
268
+ """Result model for ToTaskAction.start method.
269
+
270
+ Args:
271
+ status (JobStatus): The job status from the action execution.
272
+ message (str): A descriptive message about the action result.
273
+ """
274
+
275
+ status: JobStatus
276
+ message: str
277
+
278
+ def model_dump(self, **kwargs):
279
+ """Override model_dump to return status as enum value."""
280
+ data = super().model_dump(**kwargs)
281
+ if 'status' in data and isinstance(data['status'], JobStatus):
282
+ data['status'] = data['status'].value
283
+ return data
284
+
285
+
258
286
  @register_action
259
287
  class ToTaskAction(Action):
260
288
  """ToTask action for pre-annotation data processing.
@@ -306,14 +334,20 @@ class ToTaskAction(Action):
306
334
  }
307
335
  }
308
336
 
309
- def start(self):
337
+ def start(self) -> dict:
310
338
  """Start to_task action.
311
339
 
312
340
  * Generate tasks.
313
341
  * Annotate data to tasks.
342
+
343
+ Returns:
344
+ dict: Validated result with status and message.
314
345
  """
315
346
  if not self.run or not self.params:
316
- raise ValueError('Run instance or parameters not properly initialized')
347
+ result = ToTaskResult(
348
+ status=JobStatus.FAILED, message='Run instance or parameters not properly initialized'
349
+ )
350
+ raise PreAnnotationToTaskFailed(result.message)
317
351
 
318
352
  # Type assertion to help the linter
319
353
  assert isinstance(self.run, ToTaskRun)
@@ -325,20 +359,23 @@ class ToTaskAction(Action):
325
359
  if isinstance(project_response, str):
326
360
  self.run.log_message_with_code('INVALID_PROJECT_RESPONSE')
327
361
  self.run.end_log()
328
- return {}
362
+ result = ToTaskResult(status=JobStatus.FAILED, message='Invalid project response received')
363
+ raise PreAnnotationToTaskFailed(result.message)
329
364
  project: Dict[str, Any] = project_response
330
365
 
331
366
  data_collection_id = project.get('data_collection')
332
367
  if not data_collection_id:
333
368
  self.run.log_message_with_code('NO_DATA_COLLECTION')
334
369
  self.run.end_log()
335
- return {}
370
+ result = ToTaskResult(status=JobStatus.FAILED, message='Project does not have a data collection')
371
+ raise PreAnnotationToTaskFailed(result.message)
336
372
 
337
373
  data_collection_response = client.get_data_collection(data_collection_id)
338
374
  if isinstance(data_collection_response, str):
339
375
  self.run.log_message_with_code('INVALID_DATA_COLLECTION_RESPONSE')
340
376
  self.run.end_log()
341
- return {}
377
+ result = ToTaskResult(status=JobStatus.FAILED, message='Invalid data collection response received')
378
+ raise PreAnnotationToTaskFailed(result.message)
342
379
  data_collection: Dict[str, Any] = data_collection_response
343
380
 
344
381
  # Generate tasks if provided project is empty.
@@ -355,7 +392,8 @@ class ToTaskAction(Action):
355
392
  if not task_ids_count:
356
393
  self.run.log_message_with_code('NO_TASKS_FOUND')
357
394
  self.run.end_log()
358
- return {}
395
+ result = ToTaskResult(status=JobStatus.FAILED, message='No tasks found to annotate')
396
+ raise PreAnnotationToTaskFailed(result.message)
359
397
 
360
398
  # Annotate data to tasks.
361
399
  method = self.params.get('method')
@@ -365,23 +403,39 @@ class ToTaskAction(Action):
365
403
  if not target_specification_name:
366
404
  self.run.log_message_with_code('TARGET_SPEC_REQUIRED')
367
405
  self.run.end_log()
368
- return {}
406
+ result = ToTaskResult(
407
+ status=JobStatus.FAILED, message='Target specification name is required for file annotation method'
408
+ )
409
+ raise PreAnnotationToTaskFailed(result.message)
369
410
 
370
411
  file_specifications = data_collection.get('file_specifications', [])
371
412
  target_spec_exists = any(spec.get('name') == target_specification_name for spec in file_specifications)
372
413
  if not target_spec_exists:
373
414
  self.run.log_message_with_code('TARGET_SPEC_NOT_FOUND', target_specification_name)
374
415
  self.run.end_log()
375
- return {}
416
+ result = ToTaskResult(
417
+ status=JobStatus.FAILED,
418
+ message=f"Target specification name '{target_specification_name}' not found in file specifications",
419
+ )
420
+ raise PreAnnotationToTaskFailed(result.message)
376
421
  self._handle_annotate_data_from_files(task_ids, target_specification_name)
377
422
  elif method == AnnotationMethod.INFERENCE:
378
423
  self._handle_annotate_data_with_inference(task_ids)
379
424
  else:
380
425
  self.run.log_message_with_code('UNSUPPORTED_METHOD', method)
381
426
  self.run.end_log()
382
- return {}
427
+ result = ToTaskResult(status=JobStatus.FAILED, message=f'Unsupported annotation method: {method}')
428
+ raise PreAnnotationToTaskFailed(result.message)
429
+
430
+ current_progress = self.run.logger.get_current_progress()
431
+ if current_progress['overall'] != 100:
432
+ result = ToTaskResult(
433
+ status=JobStatus.FAILED, message='Pre-annotation to task failed. Current progress is not 100%'
434
+ )
435
+ raise PreAnnotationToTaskFailed(result.message)
383
436
 
384
- return {}
437
+ result = ToTaskResult(status=JobStatus.SUCCEEDED, message='Pre-annotation to task completed successfully')
438
+ return result.model_dump()
385
439
 
386
440
  def _handle_annotate_data_from_files(self, task_ids: List[int], target_specification_name: str):
387
441
  """Handle annotate data from files to tasks.
@@ -757,21 +811,21 @@ class ToTaskAction(Action):
757
811
  except Exception as e:
758
812
  return {'success': False, 'error': f'Failed to restart pre-processor: {str(e)}'}
759
813
 
760
- def _extract_primary_file_url(self, task: Dict[str, Any]) -> Tuple[str, str]:
814
+ def _extract_primary_file_url(self, task: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
761
815
  """Extract the primary file URL from task data.
762
816
 
763
817
  Args:
764
818
  task (Dict[str, Any]): The task data.
765
819
 
766
820
  Returns:
767
- Tuple[str, str]: The primary file URL and original name.
821
+ Tuple[Optional[str], Optional[str]]: The primary file URL and original name.
768
822
  """
769
823
  data_unit = task.get('data_unit', {})
770
824
  files = data_unit.get('files', {})
771
825
 
772
826
  for file_info in files.values():
773
827
  if isinstance(file_info, dict) and file_info.get('is_primary') and file_info.get('url'):
774
- return file_info['url'], file_info['file_name_original']
828
+ return file_info['url'], file_info.get('file_name_original')
775
829
 
776
830
  return None, None
777
831
 
@@ -18,6 +18,7 @@ from synapse_sdk.i18n import gettext as _
18
18
  from synapse_sdk.plugins.categories.base import Action
19
19
  from synapse_sdk.plugins.categories.decorators import register_action
20
20
  from synapse_sdk.plugins.enums import PluginCategory, RunMethod
21
+ from synapse_sdk.plugins.exceptions import ActionError
21
22
  from synapse_sdk.plugins.models import Run
22
23
  from synapse_sdk.shared.enums import Context
23
24
  from synapse_sdk.utils.pydantic.validators import non_blank
@@ -684,30 +685,26 @@ class UploadAction(Action):
684
685
  # Validate the organized files
685
686
  if not self._validate_organized_files(organized_files, file_specification_template):
686
687
  self.run.log_message('Validation failed.', context=Context.ERROR.value)
687
- self.run.log_message('Upload is aborted due to validation errors.', context=Context.ERROR.value)
688
- return result
688
+ raise ActionError('Upload is aborted due to validation errors.')
689
689
 
690
690
  # Upload files to synapse-backend.
691
691
  if not organized_files:
692
692
  self.run.log_message('Files not found on the path.', context=Context.WARNING.value)
693
- self.run.log_message('Upload is aborted due to missing files.', context=Context.ERROR.value)
694
- return result
693
+ raise ActionError('Upload is aborted due to missing files.')
695
694
  uploaded_files = self._upload_files(organized_files)
696
695
  result['uploaded_files_count'] = len(uploaded_files)
697
696
 
698
697
  # Generate data units for the uploaded data.
699
698
  if not uploaded_files:
700
699
  self.run.log_message('No files were uploaded.', context=Context.WARNING.value)
701
- self.run.log_message('Upload is aborted due to no uploaded files.', context=Context.ERROR.value)
702
- return result
700
+ raise ActionError('Upload is aborted due to no uploaded files.')
703
701
  generated_data_units = self._generate_data_units(uploaded_files)
704
702
  result['generated_data_units_count'] = len(generated_data_units)
705
703
 
706
704
  # Setup task with uploaded synapse-backend data units.
707
705
  if not generated_data_units:
708
706
  self.run.log_message('No data units were generated.', context=Context.WARNING.value)
709
- self.run.log_message('Upload is aborted due to no generated data units.', context=Context.ERROR.value)
710
- return result
707
+ raise ActionError('Upload is aborted due to no generated data units.')
711
708
 
712
709
  self.run.log_message('Import completed.')
713
710
  return result
@@ -0,0 +1,158 @@
1
+ """Encryption utilities for plugin code security."""
2
+
3
+ import base64
4
+ import os
5
+ import zipfile
6
+ from io import BytesIO
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional, Tuple
9
+
10
+ from cryptography.fernet import Fernet
11
+ from cryptography.hazmat.primitives import hashes
12
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
13
+
14
+
15
+ def generate_key(password: str, salt: bytes) -> bytes:
16
+ """Generate encryption key from password.
17
+
18
+ * Argument iterations should be at least 500,000.
19
+ """
20
+ kdf = PBKDF2HMAC(
21
+ algorithm=hashes.SHA256(),
22
+ length=32,
23
+ salt=salt,
24
+ iterations=700000,
25
+ )
26
+ return base64.urlsafe_b64encode(kdf.derive(password.encode()))
27
+
28
+
29
+ def encrypt_data(data: bytes, password: str) -> Dict:
30
+ """Encrypt data with password."""
31
+ salt = os.urandom(16)
32
+ key = generate_key(password, salt)
33
+ fernet = Fernet(key)
34
+ encrypted_data = fernet.encrypt(data)
35
+
36
+ return {
37
+ 'encrypted_data': base64.b64encode(encrypted_data).decode(),
38
+ 'salt': base64.b64encode(salt).decode(),
39
+ }
40
+
41
+
42
+ def decrypt_data(encrypted_package: Dict, password: str) -> bytes:
43
+ """Decrypt data with password."""
44
+ salt = base64.b64decode(encrypted_package['salt'])
45
+ encrypted_data = base64.b64decode(encrypted_package['encrypted_data'])
46
+
47
+ key = generate_key(password, salt)
48
+ fernet = Fernet(key)
49
+ return fernet.decrypt(encrypted_data)
50
+
51
+
52
+ def is_plugin_directory(path: Path) -> bool:
53
+ """Check if directory contains a Synapse plugin."""
54
+ config_file = path / 'config.yaml'
55
+ plugin_dir = path / 'plugin'
56
+
57
+ return config_file.exists() and plugin_dir.exists() and plugin_dir.is_dir()
58
+
59
+
60
+ def get_plugin_files(plugin_path: Path) -> List[Tuple[Path, str]]:
61
+ """Get all plugin files with their relative paths."""
62
+ plugin_files = []
63
+
64
+ # Essential plugin files
65
+ essential_patterns = [
66
+ 'config.yaml',
67
+ 'requirements.txt',
68
+ 'README.md',
69
+ 'pyproject.toml',
70
+ ]
71
+
72
+ for pattern in essential_patterns:
73
+ file_path = plugin_path / pattern
74
+ if file_path.exists():
75
+ plugin_files.append((file_path, pattern))
76
+
77
+ # Plugin source code
78
+ plugin_dir = plugin_path / 'plugin'
79
+ if plugin_dir.exists():
80
+ for file_path in plugin_dir.rglob('*'):
81
+ if file_path.is_file() and not file_path.name.startswith('.'):
82
+ relative_path = f'plugin/{file_path.relative_to(plugin_dir)}'
83
+ plugin_files.append((file_path, relative_path))
84
+
85
+ # Additional common directories
86
+ for additional_dir in ['tests', 'docs', 'data']:
87
+ dir_path = plugin_path / additional_dir
88
+ if dir_path.exists() and dir_path.is_dir():
89
+ for file_path in dir_path.rglob('*'):
90
+ if file_path.is_file() and not file_path.name.startswith('.'):
91
+ relative_path = f'{additional_dir}/{file_path.relative_to(dir_path)}'
92
+ plugin_files.append((file_path, relative_path))
93
+
94
+ return plugin_files
95
+
96
+
97
+ def create_plugin_archive(plugin_path: Path) -> bytes:
98
+ """Create a zip archive of the plugin."""
99
+ plugin_files = get_plugin_files(plugin_path)
100
+
101
+ archive_buffer = BytesIO()
102
+ with zipfile.ZipFile(archive_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
103
+ for file_path, archive_path in plugin_files:
104
+ zip_file.write(file_path, archive_path)
105
+
106
+ return archive_buffer.getvalue()
107
+
108
+
109
+ def encrypt_plugin(plugin_path: Path, password: Optional[str] = None) -> Dict:
110
+ """Encrypt a plugin directory."""
111
+ if not is_plugin_directory(plugin_path):
112
+ raise ValueError(f'Directory {plugin_path} is not a valid plugin directory')
113
+
114
+ # Generate password if not provided
115
+ if password is None:
116
+ password = base64.urlsafe_b64encode(os.urandom(32)).decode()
117
+
118
+ # Create plugin archive
119
+ archive_data = create_plugin_archive(plugin_path)
120
+
121
+ # Encrypt the archive
122
+ encrypted_package = encrypt_data(archive_data, password)
123
+
124
+ # Add metadata
125
+ encrypted_package.update({
126
+ 'plugin_name': plugin_path.name,
127
+ 'plugin_path': str(plugin_path),
128
+ 'encryption_method': 'fernet',
129
+ 'archive_format': 'zip',
130
+ })
131
+
132
+ return encrypted_package, password
133
+
134
+
135
+ def get_plugin_info(plugin_path: Path) -> Optional[Dict]:
136
+ """Get basic plugin information."""
137
+ if not is_plugin_directory(plugin_path):
138
+ return None
139
+
140
+ info = {'name': plugin_path.name, 'path': str(plugin_path), 'is_plugin': True}
141
+
142
+ # Try to read config.yaml for additional info
143
+ config_file = plugin_path / 'config.yaml'
144
+ if config_file.exists():
145
+ try:
146
+ import yaml
147
+
148
+ with open(config_file, 'r', encoding='utf-8') as f:
149
+ config = yaml.safe_load(f)
150
+ info.update({
151
+ 'version': config.get('version', 'unknown'),
152
+ 'description': config.get('description', ''),
153
+ 'category': config.get('category', 'unknown'),
154
+ })
155
+ except Exception:
156
+ pass # Continue without config details if parsing fails
157
+
158
+ return info
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synapse-sdk
3
- Version: 1.0.0b3
3
+ Version: 1.0.0b5
4
4
  Summary: synapse sdk
5
5
  Author-email: datamaker <developer@datamaker.io>
6
6
  License: MIT
@@ -6,8 +6,8 @@ synapse_sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  synapse_sdk/i18n.py,sha256=VXMR-Zm_1hTAg9iPk3YZNNq-T1Bhx1J2fEtRT6kyYbg,766
7
7
  synapse_sdk/loggers.py,sha256=xK48h3ZaDDZLaF-qsdnv1-6-4vw_cYlgpSCKHYUQw1g,6549
8
8
  synapse_sdk/types.py,sha256=khzn8KpgxFdn1SrpbcuX84m_Md1Mz_HIoUoPq8uok40,698
9
- synapse_sdk/cli/__init__.py,sha256=tdOwJRf6qggHqFrXBTwvhUbkG0KZmJ_xDlDdDv7Wn8M,11663
10
- synapse_sdk/cli/code_server.py,sha256=vp5i0p8BO-bA3XrbqBtb95S0kGhQZwzmPJj42YTX72s,7042
9
+ synapse_sdk/cli/__init__.py,sha256=64Qaxak5rwhEKUj5xLkdg1vtBgyl4Do1Z97eNiqNqEE,11668
10
+ synapse_sdk/cli/code_server.py,sha256=0AnyZSrHxfH7YU02PXPjcvekH-R1BRRrIA0NbA8Ckd8,15421
11
11
  synapse_sdk/cli/config.py,sha256=2WVKeAdcDeiwRb4JnNEOeZoAVlTHrY2jx4NAUiAZO2c,16156
12
12
  synapse_sdk/cli/devtools.py,sha256=hi06utdLllptlUy3HhPfmfRqjc9bdiVGezY0U5s_0pY,2617
13
13
  synapse_sdk/cli/alias/__init__.py,sha256=jDy8N_KupVy7n_jKKWhjQOj76-mR-uoVvMoyzObUkuI,405
@@ -28,7 +28,7 @@ synapse_sdk/clients/base.py,sha256=2-aCbWx0LcFGXXgd9nkWtaaSroRJybjRAHAT4qCjhv0,1
28
28
  synapse_sdk/clients/exceptions.py,sha256=ylv7x10eOp4aA3a48jwonnvqvkiYwzJYXjkVkRTAjwk,220
29
29
  synapse_sdk/clients/utils.py,sha256=8pPJTdzHiRPSbZMoQYHAgR2BAMO6u_R_jMV6a2p34iQ,392
30
30
  synapse_sdk/clients/agent/__init__.py,sha256=FqYbtzMJdzRfuU2SA-Yxdc0JKmVP1wxH6OlUNmB4lH8,2230
31
- synapse_sdk/clients/agent/core.py,sha256=x2jgORTjT7pJY67SLuc-5lMG6CD5OWpy8UgGeTf7IhA,270
31
+ synapse_sdk/clients/agent/core.py,sha256=aeMSzf8BF7LjVcmHaL8zC7ofBZUff8kIeqkW1xUJ6Sk,745
32
32
  synapse_sdk/clients/agent/ray.py,sha256=1EDl-bMN2CvKl07-qMidSWNOGpvIvzcWl7jDBCza65o,3248
33
33
  synapse_sdk/clients/agent/service.py,sha256=s7KuPK_DB1nr2VHrigttV1WyFonaGHNrPvU8loRxHcE,478
34
34
  synapse_sdk/clients/backend/__init__.py,sha256=9FzjQn0ljRhtdaoG3n38Mdgte7GFwIh4OtEmoqVg2_E,2098
@@ -113,7 +113,7 @@ synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py,sh
113
113
  synapse_sdk/plugins/categories/export/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
114
114
  synapse_sdk/plugins/categories/export/enums.py,sha256=gtyngvQ1DKkos9iKGcbecwTVQQ6sDwbrBPSGPNb5Am0,127
115
115
  synapse_sdk/plugins/categories/export/actions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
- synapse_sdk/plugins/categories/export/actions/export.py,sha256=ml4dlJzKc1q8h0ysLU3EF5rGj6FIorAE4sbuLzdG_r8,11704
116
+ synapse_sdk/plugins/categories/export/actions/export.py,sha256=7OCU3kwkYqYnelG6vfZ4ClyyMj_FHT-WQoUmgcfH080,12012
117
117
  synapse_sdk/plugins/categories/export/templates/config.yaml,sha256=N7YmnFROb3s3M35SA9nmabyzoSb5O2t2TRPicwFNN2o,56
118
118
  synapse_sdk/plugins/categories/export/templates/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
119
119
  synapse_sdk/plugins/categories/export/templates/plugin/export.py,sha256=GDb6Ucodsr5aBPMU4alr68-DyFoLR5TyhC_MCaJrkF0,6411
@@ -141,7 +141,7 @@ synapse_sdk/plugins/categories/post_annotation/templates/plugin/post_annotation.
141
141
  synapse_sdk/plugins/categories/pre_annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
142
  synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
143
  synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation.py,sha256=6ib3RmnGrjpsQ0e_G-mRH1lfFunQ3gh2M831vuDn7HU,344
144
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task.py,sha256=M4EUOkCCSchxcdyfkhxW0eQZ80I55poVuW6od8hiuxI,37541
144
+ synapse_sdk/plugins/categories/pre_annotation/actions/to_task.py,sha256=x6KPK89OWS7wsmYZ-wFFPhUl_fXEO74NIVB4bUChm6Q,40323
145
145
  synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml,sha256=VREoCp9wsvZ8T2E1d_MEKlR8TC_herDJGVQtu3ezAYU,589
146
146
  synapse_sdk/plugins/categories/pre_annotation/templates/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
147
147
  synapse_sdk/plugins/categories/pre_annotation/templates/plugin/pre_annotation.py,sha256=HBHxHuv2gMBzDB2alFfrzI_SZ1Ztk6mo7eFbR5GqHKw,106
@@ -154,7 +154,7 @@ synapse_sdk/plugins/categories/smart_tool/templates/plugin/__init__.py,sha256=47
154
154
  synapse_sdk/plugins/categories/smart_tool/templates/plugin/auto_label.py,sha256=eevNg0nOcYFR4z_L_R-sCvVOYoLWSAH1jwDkAf3YCjY,320
155
155
  synapse_sdk/plugins/categories/upload/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
156
156
  synapse_sdk/plugins/categories/upload/actions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
- synapse_sdk/plugins/categories/upload/actions/upload.py,sha256=7oV8kcJ_JTkHtRHHf0e1s3dka_CKf2engMkIioLTdek,38291
157
+ synapse_sdk/plugins/categories/upload/actions/upload.py,sha256=Lhc_ezB_FvkDI7LTe50tadOQ2W9vp2ppTWYNJSWYKfY,38114
158
158
  synapse_sdk/plugins/categories/upload/templates/config.yaml,sha256=6_dRa0_J2aS8NSUfO4MKbPxZcdPS2FpJzzp51edYAZc,281
159
159
  synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
160
  synapse_sdk/plugins/categories/upload/templates/plugin/upload.py,sha256=IZU4sdSMSLKPCtlNqF7DP2howTdYR6hr74HCUZsGdPk,1559
@@ -180,6 +180,7 @@ synapse_sdk/shared/enums.py,sha256=5uy4HGKtGCAvrBMuVSzwloJ6f41sOk0ty__605zF8hg,3
180
180
  synapse_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
181
181
  synapse_sdk/utils/dataset.py,sha256=zWTzFmv589izFr62BDuApi3r5FpTsdm-5AmriC0AEdM,1865
182
182
  synapse_sdk/utils/debug.py,sha256=F7JlUwYjTFZAMRbBqKm6hxOIz-_IXYA8lBInOS4jbS4,100
183
+ synapse_sdk/utils/encryption.py,sha256=KMARrAk5aIHfBLC8CvdXiSIuaGvxljluubjF9PVLf7c,5100
183
184
  synapse_sdk/utils/file.py,sha256=wWBQAx0cB5a-fjfRMeJV-KjBil1ZyKRz-vXno3xBSoo,6834
184
185
  synapse_sdk/utils/http.py,sha256=yRxYfru8tMnBVeBK-7S0Ga13yOf8oRHquG5e8K_FWcI,4759
185
186
  synapse_sdk/utils/module_loading.py,sha256=chHpU-BZjtYaTBD_q0T7LcKWtqKvYBS4L0lPlKkoMQ8,1020
@@ -210,9 +211,9 @@ synapse_sdk/utils/storage/providers/gcp.py,sha256=i2BQCu1Kej1If9SuNr2_lEyTcr5M_n
210
211
  synapse_sdk/utils/storage/providers/http.py,sha256=2DhIulND47JOnS5ZY7MZUex7Su3peAPksGo1Wwg07L4,5828
211
212
  synapse_sdk/utils/storage/providers/s3.py,sha256=ZmqekAvIgcQBdRU-QVJYv1Rlp6VHfXwtbtjTSphua94,2573
212
213
  synapse_sdk/utils/storage/providers/sftp.py,sha256=_8s9hf0JXIO21gvm-JVS00FbLsbtvly4c-ETLRax68A,1426
213
- synapse_sdk-1.0.0b3.dist-info/licenses/LICENSE,sha256=bKzmC5YAg4V1Fhl8OO_tqY8j62hgdncAkN7VrdjmrGk,1101
214
- synapse_sdk-1.0.0b3.dist-info/METADATA,sha256=k_X8gPzH1h6Xqq3b0lZxUFTvQqTEL2wwe6e-dgW7X8Q,3718
215
- synapse_sdk-1.0.0b3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
216
- synapse_sdk-1.0.0b3.dist-info/entry_points.txt,sha256=VNptJoGoNJI8yLXfBmhgUefMsmGI0m3-0YoMvrOgbxo,48
217
- synapse_sdk-1.0.0b3.dist-info/top_level.txt,sha256=ytgJMRK1slVOKUpgcw3LEyHHP7S34J6n_gJzdkcSsw8,12
218
- synapse_sdk-1.0.0b3.dist-info/RECORD,,
214
+ synapse_sdk-1.0.0b5.dist-info/licenses/LICENSE,sha256=bKzmC5YAg4V1Fhl8OO_tqY8j62hgdncAkN7VrdjmrGk,1101
215
+ synapse_sdk-1.0.0b5.dist-info/METADATA,sha256=dE3YqjlFZ96vvRoQ59Htr65rSrvmibKDAvsGwOIB6U4,3718
216
+ synapse_sdk-1.0.0b5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
217
+ synapse_sdk-1.0.0b5.dist-info/entry_points.txt,sha256=VNptJoGoNJI8yLXfBmhgUefMsmGI0m3-0YoMvrOgbxo,48
218
+ synapse_sdk-1.0.0b5.dist-info/top_level.txt,sha256=ytgJMRK1slVOKUpgcw3LEyHHP7S34J6n_gJzdkcSsw8,12
219
+ synapse_sdk-1.0.0b5.dist-info/RECORD,,