synapse-sdk 2025.10.1__py3-none-any.whl → 2025.10.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.
Potentially problematic release.
This version of synapse-sdk might be problematic. Click here for more details.
- synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
- synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-overview.md +560 -0
- synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
- synapse_sdk/devtools/docs/docs/plugins/plugins.md +12 -5
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-overview.md +560 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current.json +16 -4
- synapse_sdk/devtools/docs/sidebars.ts +13 -1
- synapse_sdk/plugins/README.md +487 -80
- synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/action.py +10 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +145 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +97 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +250 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +284 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +87 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +127 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
- synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +2 -1
- synapse_sdk/plugins/categories/upload/actions/upload/models.py +134 -94
- synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +2 -2
- synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +106 -14
- synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +113 -36
- synapse_sdk/plugins/categories/upload/templates/README.md +365 -0
- {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/METADATA +1 -1
- {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/RECORD +40 -20
- synapse_sdk/devtools/docs/docs/plugins/developing-upload-template.md +0 -1463
- synapse_sdk/devtools/docs/docs/plugins/upload-plugins.md +0 -1964
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/developing-upload-template.md +0 -1463
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/upload-plugins.md +0 -2077
- {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/WHEEL +0 -0
- {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/entry_points.txt +0 -0
- {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/licenses/LICENSE +0 -0
- {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/top_level.txt +0 -0
synapse_sdk/plugins/README.md
CHANGED
|
@@ -46,7 +46,7 @@ The `Action` class (`synapse_sdk/plugins/categories/base.py`) provides the unifi
|
|
|
46
46
|
```python
|
|
47
47
|
class Action:
|
|
48
48
|
"""Base class for all plugin actions.
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
Class Variables:
|
|
51
51
|
name (str): Action identifier
|
|
52
52
|
category (PluginCategory): Plugin category
|
|
@@ -55,7 +55,7 @@ class Action:
|
|
|
55
55
|
params_model (BaseModel): Parameter validation model
|
|
56
56
|
progress_categories (Dict): Progress tracking categories
|
|
57
57
|
metrics_categories (Dict): Metrics collection categories
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
Instance Variables:
|
|
60
60
|
params (Dict): Validated action parameters
|
|
61
61
|
plugin_config (Dict): Plugin configuration
|
|
@@ -63,7 +63,7 @@ class Action:
|
|
|
63
63
|
client: Backend API client
|
|
64
64
|
run (Run): Execution instance
|
|
65
65
|
"""
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
# Class configuration
|
|
68
68
|
name = None
|
|
69
69
|
category = None
|
|
@@ -72,7 +72,7 @@ class Action:
|
|
|
72
72
|
params_model = None
|
|
73
73
|
progress_categories = None
|
|
74
74
|
metrics_categories = None
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
def start(self):
|
|
77
77
|
"""Main action logic - implement in subclasses."""
|
|
78
78
|
raise NotImplementedError
|
|
@@ -111,20 +111,20 @@ The `Run` class (`models.py`) manages action execution:
|
|
|
111
111
|
```python
|
|
112
112
|
class Run(BaseModel):
|
|
113
113
|
"""Manages plugin execution lifecycle.
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
Key Methods:
|
|
116
116
|
log_message(message, context): Log execution messages
|
|
117
117
|
set_progress(current, total, category): Update progress
|
|
118
118
|
set_metrics(metrics, category): Record metrics
|
|
119
119
|
log(log_type, data): Structured logging
|
|
120
120
|
"""
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
def log_message(self, message: str, context: str = 'INFO'):
|
|
123
123
|
"""Log execution messages with context."""
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
def set_progress(self, current: int, total: int, category: str = None):
|
|
126
126
|
"""Update progress tracking."""
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
def set_metrics(self, metrics: dict, category: str):
|
|
129
129
|
"""Record execution metrics."""
|
|
130
130
|
```
|
|
@@ -165,18 +165,18 @@ class MyActionParams(BaseModel):
|
|
|
165
165
|
@register_action
|
|
166
166
|
class MyAction(Action):
|
|
167
167
|
"""Base implementation for my_category actions."""
|
|
168
|
-
|
|
168
|
+
|
|
169
169
|
name = 'my_action'
|
|
170
170
|
category = PluginCategory.MY_CATEGORY
|
|
171
171
|
method = RunMethod.JOB
|
|
172
172
|
params_model = MyActionParams
|
|
173
|
-
|
|
173
|
+
|
|
174
174
|
progress_categories = {
|
|
175
175
|
'preprocessing': {'proportion': 20},
|
|
176
176
|
'processing': {'proportion': 60},
|
|
177
177
|
'postprocessing': {'proportion': 20}
|
|
178
178
|
}
|
|
179
|
-
|
|
179
|
+
|
|
180
180
|
metrics_categories = {
|
|
181
181
|
'performance': {
|
|
182
182
|
'throughput': 0,
|
|
@@ -184,30 +184,30 @@ class MyAction(Action):
|
|
|
184
184
|
'accuracy': 0
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
-
|
|
187
|
+
|
|
188
188
|
def start(self):
|
|
189
189
|
"""Main execution logic."""
|
|
190
190
|
self.run.log_message("Starting my action...")
|
|
191
|
-
|
|
191
|
+
|
|
192
192
|
# Access validated parameters
|
|
193
193
|
input_path = self.params['input_path']
|
|
194
194
|
output_path = self.params['output_path']
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
# Update progress
|
|
197
197
|
self.run.set_progress(0, 100, 'preprocessing')
|
|
198
|
-
|
|
198
|
+
|
|
199
199
|
# Your implementation here
|
|
200
200
|
result = self.process_data(input_path, output_path)
|
|
201
|
-
|
|
201
|
+
|
|
202
202
|
# Record metrics
|
|
203
203
|
self.run.set_metrics({
|
|
204
204
|
'throughput': result['throughput'],
|
|
205
205
|
'items_processed': result['count']
|
|
206
206
|
}, 'performance')
|
|
207
|
-
|
|
207
|
+
|
|
208
208
|
self.run.log_message("Action completed successfully")
|
|
209
209
|
return result
|
|
210
|
-
|
|
210
|
+
|
|
211
211
|
def process_data(self, input_path, output_path):
|
|
212
212
|
"""Implement category-specific logic."""
|
|
213
213
|
raise NotImplementedError("Subclasses must implement process_data")
|
|
@@ -221,7 +221,7 @@ from synapse_sdk.plugins.categories.my_category import MyAction as BaseMyAction
|
|
|
221
221
|
|
|
222
222
|
class MyAction(BaseMyAction):
|
|
223
223
|
"""Custom implementation of my_action."""
|
|
224
|
-
|
|
224
|
+
|
|
225
225
|
def process_data(self, input_path, output_path):
|
|
226
226
|
"""Custom data processing logic."""
|
|
227
227
|
# Plugin developer implements this
|
|
@@ -266,32 +266,29 @@ class UploadAction(Action):
|
|
|
266
266
|
category = PluginCategory.UPLOAD
|
|
267
267
|
method = RunMethod.JOB
|
|
268
268
|
run_class = UploadRun
|
|
269
|
-
|
|
269
|
+
|
|
270
270
|
def start(self):
|
|
271
271
|
# Comprehensive upload workflow
|
|
272
272
|
storage_id = self.params.get('storage')
|
|
273
273
|
path = self.params.get('path')
|
|
274
|
-
|
|
274
|
+
|
|
275
275
|
# Setup and validation
|
|
276
276
|
storage = self.client.get_storage(storage_id)
|
|
277
277
|
pathlib_cwd = get_pathlib(storage, path)
|
|
278
|
-
|
|
278
|
+
|
|
279
279
|
# Excel metadata processing
|
|
280
280
|
excel_metadata = self._read_excel_metadata(pathlib_cwd)
|
|
281
|
-
|
|
281
|
+
|
|
282
282
|
# File organization and upload
|
|
283
283
|
file_specification = self._analyze_collection()
|
|
284
284
|
organized_files = self._organize_files(pathlib_cwd, file_specification, excel_metadata)
|
|
285
|
-
|
|
286
|
-
#
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
else:
|
|
290
|
-
uploaded_files = self._upload_files(organized_files)
|
|
291
|
-
|
|
285
|
+
|
|
286
|
+
# Upload files
|
|
287
|
+
uploaded_files = self._upload_files(organized_files)
|
|
288
|
+
|
|
292
289
|
# Data unit generation
|
|
293
290
|
generated_data_units = self._generate_data_units(uploaded_files, batch_size)
|
|
294
|
-
|
|
291
|
+
|
|
295
292
|
return {
|
|
296
293
|
'uploaded_files_count': len(uploaded_files),
|
|
297
294
|
'generated_data_units_count': len(generated_data_units)
|
|
@@ -302,6 +299,414 @@ class UploadAction(Action):
|
|
|
302
299
|
|
|
303
300
|
For complex actions that require multiple components, follow the modular structure pattern established by the refactored upload action. This approach improves maintainability, testability, and code organization.
|
|
304
301
|
|
|
302
|
+
## Complex Action Refactoring Patterns
|
|
303
|
+
|
|
304
|
+
### Overview
|
|
305
|
+
|
|
306
|
+
As plugin actions evolve and grow in complexity, they often become monolithic files with 900+ lines containing multiple responsibilities. The SYN-5398 UploadAction refactoring demonstrates how to break down complex actions using **Strategy** and **Facade** design patterns, transforming a 1,600+ line monolithic implementation into a maintainable, testable architecture.
|
|
307
|
+
|
|
308
|
+
### When to Apply Complex Refactoring
|
|
309
|
+
|
|
310
|
+
Consider refactoring when your action exhibits:
|
|
311
|
+
|
|
312
|
+
- **Size**: 900+ lines in a single method or file
|
|
313
|
+
- **Multiple Responsibilities**: Handling validation, file processing, uploads, metadata extraction in one method
|
|
314
|
+
- **Conditional Complexity**: Multiple if/else branches for different processing strategies
|
|
315
|
+
- **Testing Difficulty**: Hard to unit test individual components
|
|
316
|
+
- **Maintenance Issues**: Changes require touching multiple unrelated sections
|
|
317
|
+
|
|
318
|
+
### Strategy Pattern for Pluggable Behaviors
|
|
319
|
+
|
|
320
|
+
The Strategy pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. This is ideal for actions that need different implementations of the same behavior.
|
|
321
|
+
|
|
322
|
+
#### Core Strategy Types
|
|
323
|
+
|
|
324
|
+
Based on the UploadAction refactoring, identify these key strategy categories:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
# 1. ValidationStrategy - Parameter and environment validation
|
|
328
|
+
class ValidationStrategy(ABC):
|
|
329
|
+
@abstractmethod
|
|
330
|
+
def validate(self, context: ActionContext) -> ValidationResult:
|
|
331
|
+
"""Validate parameters and environment."""
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
class UploadValidationStrategy(ValidationStrategy):
|
|
335
|
+
def validate(self, context: ActionContext) -> ValidationResult:
|
|
336
|
+
# Validate storage, collection, file paths
|
|
337
|
+
return ValidationResult(is_valid=True, errors=[])
|
|
338
|
+
|
|
339
|
+
# 2. FileDiscoveryStrategy - Different file discovery methods
|
|
340
|
+
class FileDiscoveryStrategy(ABC):
|
|
341
|
+
@abstractmethod
|
|
342
|
+
def discover_files(self, context: ActionContext) -> List[Path]:
|
|
343
|
+
"""Discover files to process."""
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
class RecursiveFileDiscoveryStrategy(FileDiscoveryStrategy):
|
|
347
|
+
def discover_files(self, context: ActionContext) -> List[Path]:
|
|
348
|
+
# Recursive directory traversal
|
|
349
|
+
return list(context.base_path.rglob("*"))
|
|
350
|
+
|
|
351
|
+
# 3. MetadataStrategy - Various metadata extraction approaches
|
|
352
|
+
class MetadataStrategy(ABC):
|
|
353
|
+
@abstractmethod
|
|
354
|
+
def extract_metadata(self, files: List[Path], context: ActionContext) -> Dict:
|
|
355
|
+
"""Extract metadata from files."""
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
class ExcelMetadataStrategy(MetadataStrategy):
|
|
359
|
+
def extract_metadata(self, files: List[Path], context: ActionContext) -> Dict:
|
|
360
|
+
# Excel-specific metadata extraction
|
|
361
|
+
return {"excel_sheets": [], "total_rows": 0}
|
|
362
|
+
|
|
363
|
+
# 4. UploadStrategy - Different upload implementations
|
|
364
|
+
class UploadStrategy(ABC):
|
|
365
|
+
@abstractmethod
|
|
366
|
+
def upload_files(self, files: List[Path], context: ActionContext) -> List[UploadResult]:
|
|
367
|
+
"""Upload files using specific strategy."""
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
class StandardUploadStrategy(UploadStrategy):
|
|
371
|
+
def upload_files(self, files: List[Path], context: ActionContext) -> List[UploadResult]:
|
|
372
|
+
# Standard upload implementation
|
|
373
|
+
return [self._upload_single(f) for f in files]
|
|
374
|
+
|
|
375
|
+
# 5. DataUnitStrategy - Different data unit generation methods
|
|
376
|
+
class DataUnitStrategy(ABC):
|
|
377
|
+
@abstractmethod
|
|
378
|
+
def generate_data_units(self, uploaded_files: List[UploadResult], context: ActionContext) -> List[DataUnit]:
|
|
379
|
+
"""Generate data units from uploaded files."""
|
|
380
|
+
pass
|
|
381
|
+
|
|
382
|
+
class StandardDataUnitStrategy(DataUnitStrategy):
|
|
383
|
+
def generate_data_units(self, uploaded_files: List[UploadResult], context: ActionContext) -> List[DataUnit]:
|
|
384
|
+
# Standard data unit creation
|
|
385
|
+
return [DataUnit(file=file) for file in uploaded_files]
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### Strategy Factory Pattern
|
|
389
|
+
|
|
390
|
+
Use a factory to create appropriate strategies based on configuration:
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
class StrategyFactory:
|
|
394
|
+
"""Factory for creating action strategies based on context."""
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def create_validation_strategy(context: ActionContext) -> ValidationStrategy:
|
|
398
|
+
if context.params.get('strict_validation', False):
|
|
399
|
+
return StrictValidationStrategy()
|
|
400
|
+
return StandardValidationStrategy()
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def create_upload_strategy(context: ActionContext) -> UploadStrategy:
|
|
404
|
+
return StandardUploadStrategy()
|
|
405
|
+
|
|
406
|
+
@staticmethod
|
|
407
|
+
def create_file_discovery_strategy(context: ActionContext) -> FileDiscoveryStrategy:
|
|
408
|
+
if context.params.get('is_recursive', False):
|
|
409
|
+
return RecursiveFileDiscoveryStrategy()
|
|
410
|
+
return FlatFileDiscoveryStrategy()
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Facade Pattern with Orchestrator
|
|
414
|
+
|
|
415
|
+
The Facade pattern provides a simplified interface to a complex subsystem. The **Orchestrator** coordinates all strategies through a step-based workflow.
|
|
416
|
+
|
|
417
|
+
#### Orchestrator Implementation
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
class UploadOrchestrator:
|
|
421
|
+
"""Facade that orchestrates the complete upload workflow."""
|
|
422
|
+
|
|
423
|
+
def __init__(self, context: ActionContext):
|
|
424
|
+
self.context = context
|
|
425
|
+
self.factory = StrategyFactory()
|
|
426
|
+
self.steps_completed = []
|
|
427
|
+
|
|
428
|
+
# Initialize strategies
|
|
429
|
+
self.validation_strategy = self.factory.create_validation_strategy(context)
|
|
430
|
+
self.file_discovery_strategy = self.factory.create_file_discovery_strategy(context)
|
|
431
|
+
self.metadata_strategy = self.factory.create_metadata_strategy(context)
|
|
432
|
+
self.upload_strategy = self.factory.create_upload_strategy(context)
|
|
433
|
+
self.data_unit_strategy = self.factory.create_data_unit_strategy(context)
|
|
434
|
+
|
|
435
|
+
def execute_workflow(self) -> UploadResult:
|
|
436
|
+
"""Execute the complete upload workflow with rollback support."""
|
|
437
|
+
try:
|
|
438
|
+
# Step 1: Setup and validation
|
|
439
|
+
self._execute_step("setup_validation", self._setup_and_validate)
|
|
440
|
+
|
|
441
|
+
# Step 2: File discovery
|
|
442
|
+
files = self._execute_step("file_discovery", self._discover_files)
|
|
443
|
+
|
|
444
|
+
# Step 3: Excel metadata extraction
|
|
445
|
+
metadata = self._execute_step("metadata_extraction",
|
|
446
|
+
lambda: self._extract_metadata(files))
|
|
447
|
+
|
|
448
|
+
# Step 4: File organization
|
|
449
|
+
organized_files = self._execute_step("file_organization",
|
|
450
|
+
lambda: self._organize_files(files, metadata))
|
|
451
|
+
|
|
452
|
+
# Step 5: File upload
|
|
453
|
+
uploaded_files = self._execute_step("file_upload",
|
|
454
|
+
lambda: self._upload_files(organized_files))
|
|
455
|
+
|
|
456
|
+
# Step 6: Data unit generation
|
|
457
|
+
data_units = self._execute_step("data_unit_generation",
|
|
458
|
+
lambda: self._generate_data_units(uploaded_files))
|
|
459
|
+
|
|
460
|
+
# Step 7: Cleanup
|
|
461
|
+
self._execute_step("cleanup", self._cleanup_temp_files)
|
|
462
|
+
|
|
463
|
+
# Step 8: Result aggregation
|
|
464
|
+
return self._execute_step("result_aggregation",
|
|
465
|
+
lambda: self._aggregate_results(uploaded_files, data_units))
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
self._rollback_completed_steps()
|
|
469
|
+
raise UploadOrchestrationError(f"Workflow failed at step {len(self.steps_completed)}: {e}")
|
|
470
|
+
|
|
471
|
+
def _execute_step(self, step_name: str, step_func: callable):
|
|
472
|
+
"""Execute a workflow step with error handling and progress tracking."""
|
|
473
|
+
self.context.logger.log_message_with_code(LogCode.STEP_STARTED, step_name)
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
result = step_func()
|
|
477
|
+
self.steps_completed.append(step_name)
|
|
478
|
+
self.context.logger.log_message_with_code(LogCode.STEP_COMPLETED, step_name)
|
|
479
|
+
return result
|
|
480
|
+
except Exception as e:
|
|
481
|
+
self.context.logger.log_message_with_code(LogCode.STEP_FAILED, step_name, str(e))
|
|
482
|
+
raise
|
|
483
|
+
|
|
484
|
+
def _setup_and_validate(self):
|
|
485
|
+
"""Step 1: Setup and validation using strategy."""
|
|
486
|
+
validation_result = self.validation_strategy.validate(self.context)
|
|
487
|
+
if not validation_result.is_valid:
|
|
488
|
+
raise ValidationError(f"Validation failed: {validation_result.errors}")
|
|
489
|
+
|
|
490
|
+
def _discover_files(self) -> List[Path]:
|
|
491
|
+
"""Step 2: File discovery using strategy."""
|
|
492
|
+
return self.file_discovery_strategy.discover_files(self.context)
|
|
493
|
+
|
|
494
|
+
def _extract_metadata(self, files: List[Path]) -> Dict:
|
|
495
|
+
"""Step 3: Metadata extraction using strategy."""
|
|
496
|
+
return self.metadata_strategy.extract_metadata(files, self.context)
|
|
497
|
+
|
|
498
|
+
def _upload_files(self, files: List[Path]) -> List[UploadResult]:
|
|
499
|
+
"""Step 5: File upload using strategy."""
|
|
500
|
+
return self.upload_strategy.upload_files(files, self.context)
|
|
501
|
+
|
|
502
|
+
def _generate_data_units(self, uploaded_files: List[UploadResult]) -> List[DataUnit]:
|
|
503
|
+
"""Step 6: Data unit generation using strategy."""
|
|
504
|
+
return self.data_unit_strategy.generate_data_units(uploaded_files, self.context)
|
|
505
|
+
|
|
506
|
+
def _rollback_completed_steps(self):
|
|
507
|
+
"""Rollback completed steps in reverse order."""
|
|
508
|
+
for step in reversed(self.steps_completed):
|
|
509
|
+
try:
|
|
510
|
+
rollback_method = getattr(self, f"_rollback_{step}", None)
|
|
511
|
+
if rollback_method:
|
|
512
|
+
rollback_method()
|
|
513
|
+
except Exception as e:
|
|
514
|
+
self.context.logger.log_message_with_code(LogCode.ROLLBACK_FAILED, step, str(e))
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
#### Transformed Action Implementation
|
|
518
|
+
|
|
519
|
+
The main action becomes dramatically simplified:
|
|
520
|
+
|
|
521
|
+
```python
|
|
522
|
+
# Before: 900+ line monolithic start() method
|
|
523
|
+
class UploadAction(Action):
|
|
524
|
+
def start(self):
|
|
525
|
+
# 900+ lines of mixed responsibilities...
|
|
526
|
+
|
|
527
|
+
# After: Clean, orchestrated implementation (196 lines total)
|
|
528
|
+
class UploadAction(Action):
|
|
529
|
+
"""Upload action using Strategy and Facade patterns."""
|
|
530
|
+
|
|
531
|
+
def __init__(self, *args, **kwargs):
|
|
532
|
+
super().__init__(*args, **kwargs)
|
|
533
|
+
self.context = ActionContext(
|
|
534
|
+
params=self.params,
|
|
535
|
+
client=self.client,
|
|
536
|
+
logger=self.run,
|
|
537
|
+
workspace=self.get_workspace_path()
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def start(self) -> UploadResult:
|
|
541
|
+
"""Main action execution using orchestrator facade."""
|
|
542
|
+
self.run.log_message_with_code(LogCode.UPLOAD_STARTED)
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
# Create and execute orchestrator
|
|
546
|
+
orchestrator = UploadOrchestrator(self.context)
|
|
547
|
+
result = orchestrator.execute_workflow()
|
|
548
|
+
|
|
549
|
+
self.run.log_message_with_code(LogCode.UPLOAD_COMPLETED,
|
|
550
|
+
result.uploaded_files_count, result.data_units_count)
|
|
551
|
+
return result
|
|
552
|
+
|
|
553
|
+
except Exception as e:
|
|
554
|
+
self.run.log_message_with_code(LogCode.UPLOAD_FAILED, str(e))
|
|
555
|
+
raise ActionError(f"Upload action failed: {e}")
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Context Management
|
|
559
|
+
|
|
560
|
+
Use a shared context object to pass state between strategies and orchestrator:
|
|
561
|
+
|
|
562
|
+
```python
|
|
563
|
+
@dataclass
|
|
564
|
+
class ActionContext:
|
|
565
|
+
"""Shared context for action execution."""
|
|
566
|
+
params: Dict[str, Any]
|
|
567
|
+
client: Any
|
|
568
|
+
logger: Any
|
|
569
|
+
workspace: Path
|
|
570
|
+
temp_files: List[Path] = field(default_factory=list)
|
|
571
|
+
metrics: Dict[str, Any] = field(default_factory=dict)
|
|
572
|
+
|
|
573
|
+
def add_temp_file(self, file_path: Path):
|
|
574
|
+
"""Track temporary files for cleanup."""
|
|
575
|
+
self.temp_files.append(file_path)
|
|
576
|
+
|
|
577
|
+
def update_metrics(self, category: str, metrics: Dict[str, Any]):
|
|
578
|
+
"""Update execution metrics."""
|
|
579
|
+
if category not in self.metrics:
|
|
580
|
+
self.metrics[category] = {}
|
|
581
|
+
self.metrics[category].update(metrics)
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Migration Guide for Complex Actions
|
|
585
|
+
|
|
586
|
+
#### Step 1: Identify Strategy Boundaries
|
|
587
|
+
|
|
588
|
+
Analyze your monolithic action to identify:
|
|
589
|
+
|
|
590
|
+
1. **Different algorithms** for the same operation (validation methods, file processing approaches)
|
|
591
|
+
2. **Configurable behaviors** that change based on parameters
|
|
592
|
+
3. **Testable units** that can be isolated
|
|
593
|
+
|
|
594
|
+
#### Step 2: Extract Strategies
|
|
595
|
+
|
|
596
|
+
```python
|
|
597
|
+
# Before: Mixed responsibilities in one method
|
|
598
|
+
def start(self):
|
|
599
|
+
# validation logic (100 lines)
|
|
600
|
+
if self.params.get('strict_mode'):
|
|
601
|
+
# strict validation
|
|
602
|
+
else:
|
|
603
|
+
# standard validation
|
|
604
|
+
|
|
605
|
+
# file discovery logic (150 lines)
|
|
606
|
+
if self.params.get('recursive'):
|
|
607
|
+
# recursive discovery
|
|
608
|
+
else:
|
|
609
|
+
# flat discovery
|
|
610
|
+
|
|
611
|
+
# upload logic (200 lines)
|
|
612
|
+
# Direct upload implementation
|
|
613
|
+
uploaded_files = self._upload_files(organized_files)
|
|
614
|
+
|
|
615
|
+
# After: Separated into strategies
|
|
616
|
+
class StrictValidationStrategy(ValidationStrategy): ...
|
|
617
|
+
class StandardValidationStrategy(ValidationStrategy): ...
|
|
618
|
+
class RecursiveFileDiscoveryStrategy(FileDiscoveryStrategy): ...
|
|
619
|
+
class StandardUploadStrategy(UploadStrategy): ...
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
#### Step 3: Create Orchestrator
|
|
623
|
+
|
|
624
|
+
Design your workflow steps:
|
|
625
|
+
|
|
626
|
+
1. Define clear step boundaries
|
|
627
|
+
2. Implement rollback for each step
|
|
628
|
+
3. Add progress tracking and logging
|
|
629
|
+
4. Handle errors gracefully
|
|
630
|
+
|
|
631
|
+
#### Step 4: Update Action Class
|
|
632
|
+
|
|
633
|
+
Transform the main action to use the orchestrator:
|
|
634
|
+
|
|
635
|
+
1. Create context object
|
|
636
|
+
2. Initialize orchestrator
|
|
637
|
+
3. Execute workflow
|
|
638
|
+
4. Handle results and errors
|
|
639
|
+
|
|
640
|
+
### Benefits and Metrics
|
|
641
|
+
|
|
642
|
+
The SYN-5398 refactoring achieved:
|
|
643
|
+
|
|
644
|
+
#### **Code Reduction**
|
|
645
|
+
|
|
646
|
+
- Main action: 900+ lines → 196 lines (-78% reduction)
|
|
647
|
+
- Total codebase: 1,600+ lines → 1,400+ lines (better organized)
|
|
648
|
+
- Cyclomatic complexity: High → Low (single responsibility per class)
|
|
649
|
+
|
|
650
|
+
#### **Improved Testability**
|
|
651
|
+
|
|
652
|
+
- **89 passing tests** with individual strategy testing
|
|
653
|
+
- **Isolated unit tests** for each strategy component
|
|
654
|
+
- **Integration tests** for orchestrator workflow
|
|
655
|
+
- **Rollback testing** for error scenarios
|
|
656
|
+
|
|
657
|
+
#### **Better Maintainability**
|
|
658
|
+
|
|
659
|
+
- **Single responsibility** per strategy class
|
|
660
|
+
- **Clear separation** of concerns
|
|
661
|
+
- **Reusable strategies** across different actions
|
|
662
|
+
- **Easy to extend** with new strategy implementations
|
|
663
|
+
|
|
664
|
+
#### **Enhanced Flexibility**
|
|
665
|
+
|
|
666
|
+
- **Runtime strategy selection** based on parameters
|
|
667
|
+
- **Pluggable algorithms** without changing core logic
|
|
668
|
+
- **Configuration-driven** behavior changes
|
|
669
|
+
- **A/B testing support** through strategy switching
|
|
670
|
+
|
|
671
|
+
### Example: Converting a Complex Action
|
|
672
|
+
|
|
673
|
+
```python
|
|
674
|
+
# Before: Monolithic action (simplified example)
|
|
675
|
+
class ComplexAction(Action):
|
|
676
|
+
def start(self):
|
|
677
|
+
# 50 lines of validation
|
|
678
|
+
if self.params.get('validation_type') == 'strict':
|
|
679
|
+
# strict validation logic
|
|
680
|
+
else:
|
|
681
|
+
# standard validation logic
|
|
682
|
+
|
|
683
|
+
# 100 lines of data processing
|
|
684
|
+
if self.params.get('processing_method') == 'batch':
|
|
685
|
+
# batch processing logic
|
|
686
|
+
else:
|
|
687
|
+
# stream processing logic
|
|
688
|
+
|
|
689
|
+
# 80 lines of output generation
|
|
690
|
+
if self.params.get('output_format') == 'json':
|
|
691
|
+
# JSON output logic
|
|
692
|
+
else:
|
|
693
|
+
# CSV output logic
|
|
694
|
+
|
|
695
|
+
# After: Strategy-based action
|
|
696
|
+
class ComplexAction(Action):
|
|
697
|
+
def start(self):
|
|
698
|
+
context = ActionContext(params=self.params, logger=self.run)
|
|
699
|
+
orchestrator = ComplexActionOrchestrator(context)
|
|
700
|
+
return orchestrator.execute_workflow()
|
|
701
|
+
|
|
702
|
+
# Individual strategies (testable, reusable)
|
|
703
|
+
class StrictValidationStrategy(ValidationStrategy): ...
|
|
704
|
+
class BatchProcessingStrategy(ProcessingStrategy): ...
|
|
705
|
+
class JSONOutputStrategy(OutputStrategy): ...
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
This pattern transformation makes complex actions maintainable, testable, and extensible while preserving all original functionality.
|
|
709
|
+
|
|
305
710
|
### Recommended File Structure
|
|
306
711
|
|
|
307
712
|
```
|
|
@@ -333,7 +738,7 @@ from .utils import ExcelSecurityConfig, PathAwareJSONEncoder
|
|
|
333
738
|
|
|
334
739
|
__all__ = [
|
|
335
740
|
'UploadAction',
|
|
336
|
-
'UploadRun',
|
|
741
|
+
'UploadRun',
|
|
337
742
|
'UploadParams',
|
|
338
743
|
'UploadStatus',
|
|
339
744
|
'LogCode',
|
|
@@ -359,27 +764,27 @@ from .run import UploadRun
|
|
|
359
764
|
|
|
360
765
|
class UploadAction(Action):
|
|
361
766
|
"""Main upload action implementation."""
|
|
362
|
-
|
|
767
|
+
|
|
363
768
|
name = 'upload'
|
|
364
769
|
category = PluginCategory.UPLOAD
|
|
365
770
|
method = RunMethod.JOB
|
|
366
771
|
run_class = UploadRun
|
|
367
772
|
params_model = UploadParams
|
|
368
|
-
|
|
773
|
+
|
|
369
774
|
def start(self):
|
|
370
775
|
"""Main action logic."""
|
|
371
776
|
# Validate parameters
|
|
372
777
|
self.validate_params()
|
|
373
|
-
|
|
778
|
+
|
|
374
779
|
# Log start
|
|
375
780
|
self.run.log_message_with_code(LogCode.UPLOAD_STARTED)
|
|
376
|
-
|
|
781
|
+
|
|
377
782
|
# Execute main logic
|
|
378
783
|
result = self._process_upload()
|
|
379
|
-
|
|
784
|
+
|
|
380
785
|
# Log completion
|
|
381
786
|
self.run.log_message_with_code(LogCode.UPLOAD_COMPLETED)
|
|
382
|
-
|
|
787
|
+
|
|
383
788
|
return result
|
|
384
789
|
```
|
|
385
790
|
|
|
@@ -394,7 +799,7 @@ from synapse_sdk.utils.pydantic.validators import non_blank
|
|
|
394
799
|
|
|
395
800
|
class UploadParams(BaseModel):
|
|
396
801
|
"""Upload action parameters with validation."""
|
|
397
|
-
|
|
802
|
+
|
|
398
803
|
name: Annotated[str, AfterValidator(non_blank)]
|
|
399
804
|
description: str | None = None
|
|
400
805
|
path: str
|
|
@@ -403,8 +808,7 @@ class UploadParams(BaseModel):
|
|
|
403
808
|
project: int | None = None
|
|
404
809
|
is_recursive: bool = False
|
|
405
810
|
max_file_size_mb: int = 50
|
|
406
|
-
|
|
407
|
-
|
|
811
|
+
|
|
408
812
|
@field_validator('storage', mode='before')
|
|
409
813
|
@classmethod
|
|
410
814
|
def check_storage_exists(cls, value: str, info) -> str:
|
|
@@ -467,19 +871,19 @@ from .enums import LogCode, LOG_MESSAGES
|
|
|
467
871
|
|
|
468
872
|
class UploadRun(Run):
|
|
469
873
|
"""Specialized run management for upload actions."""
|
|
470
|
-
|
|
874
|
+
|
|
471
875
|
def log_message_with_code(self, code: LogCode, *args, level: Optional[Context] = None):
|
|
472
876
|
"""Type-safe logging with predefined messages."""
|
|
473
877
|
if code not in LOG_MESSAGES:
|
|
474
878
|
self.log_message(f'Unknown log code: {code}')
|
|
475
879
|
return
|
|
476
|
-
|
|
880
|
+
|
|
477
881
|
log_config = LOG_MESSAGES[code]
|
|
478
882
|
message = log_config['message'].format(*args) if args else log_config['message']
|
|
479
883
|
log_level = level or log_config['level']
|
|
480
|
-
|
|
884
|
+
|
|
481
885
|
self.log_message(message, context=log_level.value)
|
|
482
|
-
|
|
886
|
+
|
|
483
887
|
def log_upload_event(self, code: LogCode, *args, level: Optional[Context] = None):
|
|
484
888
|
"""Log upload-specific events with metrics."""
|
|
485
889
|
self.log_message_with_code(code, *args, level)
|
|
@@ -515,7 +919,7 @@ from pathlib import Path
|
|
|
515
919
|
|
|
516
920
|
class PathAwareJSONEncoder(json.JSONEncoder):
|
|
517
921
|
"""JSON encoder that handles Path objects."""
|
|
518
|
-
|
|
922
|
+
|
|
519
923
|
def default(self, obj):
|
|
520
924
|
if hasattr(obj, '__fspath__') or hasattr(obj, 'as_posix'):
|
|
521
925
|
return str(obj)
|
|
@@ -525,7 +929,7 @@ class PathAwareJSONEncoder(json.JSONEncoder):
|
|
|
525
929
|
|
|
526
930
|
class ExcelSecurityConfig:
|
|
527
931
|
"""Configuration for Excel file security limits."""
|
|
528
|
-
|
|
932
|
+
|
|
529
933
|
def __init__(self):
|
|
530
934
|
self.MAX_FILE_SIZE_MB = int(os.getenv('EXCEL_MAX_FILE_SIZE_MB', '10'))
|
|
531
935
|
self.MAX_ROWS = int(os.getenv('EXCEL_MAX_ROWS', '10000'))
|
|
@@ -548,7 +952,7 @@ class ExcelSecurityConfig:
|
|
|
548
952
|
# Before: Single upload.py file (1362 lines)
|
|
549
953
|
class UploadAction(Action):
|
|
550
954
|
# All code in one file...
|
|
551
|
-
|
|
955
|
+
|
|
552
956
|
# After: Modular structure
|
|
553
957
|
# action.py - Main logic (546 lines)
|
|
554
958
|
# models.py - Parameter validation (98 lines)
|
|
@@ -591,11 +995,11 @@ class UploadRun(Run):
|
|
|
591
995
|
if code not in LOG_MESSAGES:
|
|
592
996
|
self.log_message(f'Unknown log code: {code}')
|
|
593
997
|
return
|
|
594
|
-
|
|
998
|
+
|
|
595
999
|
log_config = LOG_MESSAGES[code]
|
|
596
1000
|
message = log_config['message'].format(*args) if args else log_config['message']
|
|
597
1001
|
log_level = level or log_config['level'] or Context.INFO
|
|
598
|
-
|
|
1002
|
+
|
|
599
1003
|
if log_level == Context.INFO.value:
|
|
600
1004
|
self.log_message(message, context=log_level.value)
|
|
601
1005
|
else:
|
|
@@ -678,7 +1082,7 @@ class MyAction(Action):
|
|
|
678
1082
|
'description': 'Evaluating results'
|
|
679
1083
|
}
|
|
680
1084
|
}
|
|
681
|
-
|
|
1085
|
+
|
|
682
1086
|
def start(self):
|
|
683
1087
|
# Update specific progress categories
|
|
684
1088
|
self.run.set_progress(50, 100, 'data_loading')
|
|
@@ -691,26 +1095,26 @@ class MyAction(Action):
|
|
|
691
1095
|
def get_runtime_env(self):
|
|
692
1096
|
"""Customize execution environment."""
|
|
693
1097
|
env = super().get_runtime_env()
|
|
694
|
-
|
|
1098
|
+
|
|
695
1099
|
# Add custom packages
|
|
696
1100
|
env['pip']['packages'].extend([
|
|
697
1101
|
'custom-ml-library==2.0.0',
|
|
698
1102
|
'specialized-tool>=1.5.0'
|
|
699
1103
|
])
|
|
700
|
-
|
|
1104
|
+
|
|
701
1105
|
# Set environment variables
|
|
702
1106
|
env['env_vars'].update({
|
|
703
1107
|
'CUDA_VISIBLE_DEVICES': '0,1',
|
|
704
1108
|
'OMP_NUM_THREADS': '8',
|
|
705
1109
|
'CUSTOM_CONFIG_PATH': '/app/config'
|
|
706
1110
|
})
|
|
707
|
-
|
|
1111
|
+
|
|
708
1112
|
# Add working directory files
|
|
709
1113
|
env['working_dir_files'] = {
|
|
710
1114
|
'config.yaml': 'path/to/local/config.yaml',
|
|
711
1115
|
'model_weights.pth': 'path/to/weights.pth'
|
|
712
1116
|
}
|
|
713
|
-
|
|
1117
|
+
|
|
714
1118
|
return env
|
|
715
1119
|
```
|
|
716
1120
|
|
|
@@ -722,27 +1126,27 @@ from typing import Literal, Optional, List
|
|
|
722
1126
|
|
|
723
1127
|
class AdvancedParams(BaseModel):
|
|
724
1128
|
"""Advanced parameter validation."""
|
|
725
|
-
|
|
1129
|
+
|
|
726
1130
|
# Enum-like validation
|
|
727
1131
|
model_type: Literal["cnn", "transformer", "resnet"]
|
|
728
|
-
|
|
1132
|
+
|
|
729
1133
|
# Range validation
|
|
730
1134
|
learning_rate: float = Field(gt=0, le=1, default=0.001)
|
|
731
1135
|
batch_size: int = Field(ge=1, le=1024, default=32)
|
|
732
|
-
|
|
1136
|
+
|
|
733
1137
|
# File path validation
|
|
734
1138
|
data_path: str
|
|
735
1139
|
output_path: Optional[str] = None
|
|
736
|
-
|
|
1140
|
+
|
|
737
1141
|
# Complex validation
|
|
738
1142
|
layers: List[int] = Field(min_items=1, max_items=10)
|
|
739
|
-
|
|
1143
|
+
|
|
740
1144
|
@validator('data_path')
|
|
741
1145
|
def validate_data_path(cls, v):
|
|
742
1146
|
if not os.path.exists(v):
|
|
743
1147
|
raise ValueError(f'Data path does not exist: {v}')
|
|
744
1148
|
return v
|
|
745
|
-
|
|
1149
|
+
|
|
746
1150
|
@validator('output_path')
|
|
747
1151
|
def validate_output_path(cls, v, values):
|
|
748
1152
|
if v is None:
|
|
@@ -750,7 +1154,7 @@ class AdvancedParams(BaseModel):
|
|
|
750
1154
|
data_path = values.get('data_path', '')
|
|
751
1155
|
return f"{data_path}_output"
|
|
752
1156
|
return v
|
|
753
|
-
|
|
1157
|
+
|
|
754
1158
|
@validator('layers')
|
|
755
1159
|
def validate_layers(cls, v):
|
|
756
1160
|
if len(v) < 2:
|
|
@@ -779,11 +1183,11 @@ class UploadAction(Action):
|
|
|
779
1183
|
files = self._discover_files()
|
|
780
1184
|
processed_files = self._process_files(files)
|
|
781
1185
|
return self._generate_output(processed_files)
|
|
782
|
-
|
|
1186
|
+
|
|
783
1187
|
def _validate_inputs(self):
|
|
784
1188
|
"""Separate validation logic."""
|
|
785
1189
|
pass
|
|
786
|
-
|
|
1190
|
+
|
|
787
1191
|
def _discover_files(self):
|
|
788
1192
|
"""Separate file discovery logic."""
|
|
789
1193
|
pass
|
|
@@ -796,11 +1200,11 @@ class LogCode(str, Enum):
|
|
|
796
1200
|
# Good: Type hints and documentation
|
|
797
1201
|
def process_batch(self, items: List[Dict[str, Any]], batch_size: int = 100) -> List[Dict[str, Any]]:
|
|
798
1202
|
"""Process items in batches for memory efficiency.
|
|
799
|
-
|
|
1203
|
+
|
|
800
1204
|
Args:
|
|
801
1205
|
items: List of items to process
|
|
802
1206
|
batch_size: Number of items per batch
|
|
803
|
-
|
|
1207
|
+
|
|
804
1208
|
Returns:
|
|
805
1209
|
List of processed items
|
|
806
1210
|
"""
|
|
@@ -809,16 +1213,13 @@ def process_batch(self, items: List[Dict[str, Any]], batch_size: int = 100) -> L
|
|
|
809
1213
|
### 3. Performance Optimization
|
|
810
1214
|
|
|
811
1215
|
```python
|
|
812
|
-
# Use
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
tasks = [upload_single_file(f) for f in files]
|
|
821
|
-
return await asyncio.gather(*tasks, return_exceptions=True)
|
|
1216
|
+
# Use simple sequential processing for file uploads
|
|
1217
|
+
def _upload_files(self, files: List[Path]) -> List[UploadResult]:
|
|
1218
|
+
results = []
|
|
1219
|
+
for file_path in files:
|
|
1220
|
+
result = self._upload_file(file_path)
|
|
1221
|
+
results.append(result)
|
|
1222
|
+
return results
|
|
822
1223
|
|
|
823
1224
|
# Use generators for memory efficiency
|
|
824
1225
|
def _process_large_dataset(self, data_source):
|
|
@@ -826,7 +1227,7 @@ def _process_large_dataset(self, data_source):
|
|
|
826
1227
|
for chunk in self._chunk_data(data_source, chunk_size=1000):
|
|
827
1228
|
processed_chunk = self._process_chunk(chunk)
|
|
828
1229
|
yield processed_chunk
|
|
829
|
-
|
|
1230
|
+
|
|
830
1231
|
# Update progress
|
|
831
1232
|
self.run.set_progress(self.processed_count, self.total_count, 'processing')
|
|
832
1233
|
```
|
|
@@ -858,11 +1259,11 @@ class MyAction(Action):
|
|
|
858
1259
|
def _validate_file_path(self, file_path: str) -> Path:
|
|
859
1260
|
"""Validate and sanitize file paths."""
|
|
860
1261
|
path = Path(file_path).resolve()
|
|
861
|
-
|
|
1262
|
+
|
|
862
1263
|
# Prevent directory traversal
|
|
863
1264
|
if not str(path).startswith(str(self.workspace_root)):
|
|
864
1265
|
raise ActionError(f"File path outside workspace: {path}")
|
|
865
|
-
|
|
1266
|
+
|
|
866
1267
|
return path
|
|
867
1268
|
|
|
868
1269
|
# Good: Sanitize user inputs
|
|
@@ -886,27 +1287,33 @@ def _validate_data_size(self, data: bytes) -> None:
|
|
|
886
1287
|
### Core Classes
|
|
887
1288
|
|
|
888
1289
|
#### Action
|
|
1290
|
+
|
|
889
1291
|
Base class for all plugin actions.
|
|
890
1292
|
|
|
891
1293
|
**Methods:**
|
|
1294
|
+
|
|
892
1295
|
- `start()`: Main execution method (abstract)
|
|
893
1296
|
- `run_action()`: Execute action with error handling
|
|
894
1297
|
- `get_runtime_env()`: Get execution environment configuration
|
|
895
1298
|
- `validate_params()`: Validate action parameters
|
|
896
1299
|
|
|
897
1300
|
#### Run
|
|
1301
|
+
|
|
898
1302
|
Manages action execution lifecycle.
|
|
899
1303
|
|
|
900
1304
|
**Methods:**
|
|
1305
|
+
|
|
901
1306
|
- `log_message(message, context)`: Log execution messages
|
|
902
1307
|
- `set_progress(current, total, category)`: Update progress
|
|
903
1308
|
- `set_metrics(metrics, category)`: Record metrics
|
|
904
1309
|
- `log(log_type, data)`: Structured logging
|
|
905
1310
|
|
|
906
1311
|
#### PluginRelease
|
|
1312
|
+
|
|
907
1313
|
Manages plugin metadata and configuration.
|
|
908
1314
|
|
|
909
1315
|
**Attributes:**
|
|
1316
|
+
|
|
910
1317
|
- `code`: Plugin identifier
|
|
911
1318
|
- `name`: Human-readable name
|
|
912
1319
|
- `version`: Semantic version
|
|
@@ -930,4 +1337,4 @@ config = load_plugin_config("/path/to/plugin")
|
|
|
930
1337
|
is_valid = validate_plugin("/path/to/plugin")
|
|
931
1338
|
```
|
|
932
1339
|
|
|
933
|
-
This README provides the foundation for developing and extending the Synapse SDK plugin system. For specific implementation examples, refer to the existing plugin categories and their respective documentation.
|
|
1340
|
+
This README provides the foundation for developing and extending the Synapse SDK plugin system. For specific implementation examples, refer to the existing plugin categories and their respective documentation.
|