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.

Files changed (44) hide show
  1. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
  2. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-overview.md +560 -0
  3. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
  4. synapse_sdk/devtools/docs/docs/plugins/plugins.md +12 -5
  5. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
  6. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-overview.md +560 -0
  7. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
  8. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current.json +16 -4
  9. synapse_sdk/devtools/docs/sidebars.ts +13 -1
  10. synapse_sdk/plugins/README.md +487 -80
  11. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  12. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  13. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/action.py +10 -0
  14. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  15. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +145 -0
  16. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  17. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  18. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  19. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +97 -0
  20. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +250 -0
  21. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  22. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  23. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +284 -0
  24. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  25. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  26. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +87 -0
  27. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +127 -0
  28. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  29. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +2 -1
  30. synapse_sdk/plugins/categories/upload/actions/upload/models.py +134 -94
  31. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +2 -2
  32. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +106 -14
  33. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +113 -36
  34. synapse_sdk/plugins/categories/upload/templates/README.md +365 -0
  35. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/METADATA +1 -1
  36. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/RECORD +40 -20
  37. synapse_sdk/devtools/docs/docs/plugins/developing-upload-template.md +0 -1463
  38. synapse_sdk/devtools/docs/docs/plugins/upload-plugins.md +0 -1964
  39. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/developing-upload-template.md +0 -1463
  40. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/upload-plugins.md +0 -2077
  41. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/WHEEL +0 -0
  42. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/entry_points.txt +0 -0
  43. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/licenses/LICENSE +0 -0
  44. {synapse_sdk-2025.10.1.dist-info → synapse_sdk-2025.10.3.dist-info}/top_level.txt +0 -0
@@ -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
- # Async or sync upload based on configuration
287
- if self.params.get('use_async_upload', False):
288
- uploaded_files = self.run_async(self._upload_files_async(organized_files, 10))
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
- use_async_upload: bool = True
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 async for I/O-bound operations
813
- async def _upload_files_async(self, files: List[Path], max_concurrent: int = 10):
814
- semaphore = asyncio.Semaphore(max_concurrent)
815
-
816
- async def upload_single_file(file_path):
817
- async with semaphore:
818
- return await self._upload_file(file_path)
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.