synapse-sdk 2025.9.5__py3-none-any.whl → 2025.10.6__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/clients/base.py +129 -9
- synapse_sdk/devtools/docs/docs/api/clients/base.md +230 -8
- synapse_sdk/devtools/docs/docs/api/plugins/models.md +58 -3
- synapse_sdk/devtools/docs/docs/plugins/categories/neural-net-plugins/train-action-overview.md +663 -0
- synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
- synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
- synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
- synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
- 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 +585 -0
- synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
- synapse_sdk/devtools/docs/docs/plugins/export-plugins.md +39 -0
- synapse_sdk/devtools/docs/docs/plugins/plugins.md +12 -5
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/base.md +230 -8
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/plugins/models.md +114 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/neural-net-plugins/train-action-overview.md +621 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
- 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 +585 -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/plugins/export-plugins.md +39 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current.json +16 -4
- synapse_sdk/devtools/docs/sidebars.ts +45 -1
- synapse_sdk/plugins/README.md +487 -80
- synapse_sdk/plugins/categories/base.py +1 -0
- synapse_sdk/plugins/categories/export/actions/export/action.py +8 -3
- synapse_sdk/plugins/categories/export/actions/export/utils.py +108 -8
- synapse_sdk/plugins/categories/export/templates/config.yaml +18 -0
- synapse_sdk/plugins/categories/export/templates/plugin/export.py +97 -0
- synapse_sdk/plugins/categories/neural_net/actions/train.py +592 -22
- synapse_sdk/plugins/categories/neural_net/actions/tune.py +150 -3
- 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/action.py +8 -1
- synapse_sdk/plugins/categories/upload/actions/upload/context.py +0 -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/generate.py +6 -2
- synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +24 -9
- synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +130 -18
- synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +147 -37
- synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +10 -5
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +31 -6
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +65 -37
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +17 -2
- synapse_sdk/plugins/categories/upload/templates/README.md +394 -0
- synapse_sdk/plugins/models.py +62 -0
- synapse_sdk/utils/file/download.py +261 -0
- {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/METADATA +15 -2
- {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/RECORD +74 -43
- 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.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/WHEEL +0 -0
- {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/entry_points.txt +0 -0
- {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/licenses/LICENSE +0 -0
- {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: upload-plugin-template
|
|
3
|
+
title: Upload Plugin Template Development
|
|
4
|
+
sidebar_position: 3
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Upload Plugin Template Development with BaseUploader
|
|
8
|
+
|
|
9
|
+
This guide is for plugin developers who want to create custom upload plugins using the BaseUploader template. The BaseUploader provides a workflow-based foundation for file processing and organization within upload plugins.
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
The BaseUploader template (`synapse_sdk.plugins.categories.upload.templates.plugin`) provides a structured approach to building upload plugins. It handles the common upload workflow while allowing customization through method overrides.
|
|
14
|
+
|
|
15
|
+
### BaseUploader Workflow
|
|
16
|
+
|
|
17
|
+
The BaseUploader implements a 6-step workflow pipeline:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
1. setup_directories() # Create custom directory structures
|
|
21
|
+
2. organize_files() # Organize and structure files
|
|
22
|
+
3. before_process() # Pre-processing hooks
|
|
23
|
+
4. process_files() # Main processing logic (REQUIRED)
|
|
24
|
+
5. after_process() # Post-processing hooks
|
|
25
|
+
6. validate_files() # Final validation
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Getting Started
|
|
29
|
+
|
|
30
|
+
### Template Structure
|
|
31
|
+
|
|
32
|
+
When you create an upload plugin, you get this structure:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
synapse-{plugin-code}-plugin/
|
|
36
|
+
├── config.yaml # Plugin metadata and configuration
|
|
37
|
+
├── plugin/ # Source code directory
|
|
38
|
+
│ ├── __init__.py
|
|
39
|
+
│ └── upload.py # Main upload implementation with BaseUploader
|
|
40
|
+
├── requirements.txt # Python dependencies
|
|
41
|
+
├── pyproject.toml # Package configuration
|
|
42
|
+
└── README.md # Plugin documentation
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Basic Plugin Implementation
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# plugin/__init__.py
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from typing import Any, Dict, List
|
|
51
|
+
|
|
52
|
+
class BaseUploader:
|
|
53
|
+
"""Base class with common upload functionality."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, run, path: Path, file_specification: List = None,
|
|
56
|
+
organized_files: List = None, extra_params: Dict = None):
|
|
57
|
+
self.run = run
|
|
58
|
+
self.path = path
|
|
59
|
+
self.file_specification = file_specification or []
|
|
60
|
+
self.organized_files = organized_files or []
|
|
61
|
+
self.extra_params = extra_params or {}
|
|
62
|
+
|
|
63
|
+
# Core workflow methods available for override
|
|
64
|
+
def setup_directories(self) -> None:
|
|
65
|
+
"""Setup custom directories - override as needed."""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def organize_files(self, files: List) -> List:
|
|
69
|
+
"""Organize files - override for custom logic."""
|
|
70
|
+
return files
|
|
71
|
+
|
|
72
|
+
def before_process(self, organized_files: List) -> List:
|
|
73
|
+
"""Pre-process hook - override as needed."""
|
|
74
|
+
return organized_files
|
|
75
|
+
|
|
76
|
+
def process_files(self, organized_files: List) -> List:
|
|
77
|
+
"""Main processing - MUST be overridden."""
|
|
78
|
+
return organized_files
|
|
79
|
+
|
|
80
|
+
def after_process(self, processed_files: List) -> List:
|
|
81
|
+
"""Post-process hook - override as needed."""
|
|
82
|
+
return processed_files
|
|
83
|
+
|
|
84
|
+
def validate_files(self, files: List) -> List:
|
|
85
|
+
"""Validation - override for custom validation."""
|
|
86
|
+
return self._filter_valid_files(files)
|
|
87
|
+
|
|
88
|
+
def handle_upload_files(self) -> List:
|
|
89
|
+
"""Main entry point - executes the workflow."""
|
|
90
|
+
self.setup_directories()
|
|
91
|
+
current_files = self.organized_files
|
|
92
|
+
current_files = self.organize_files(current_files)
|
|
93
|
+
current_files = self.before_process(current_files)
|
|
94
|
+
current_files = self.process_files(current_files)
|
|
95
|
+
current_files = self.after_process(current_files)
|
|
96
|
+
current_files = self.validate_files(current_files)
|
|
97
|
+
return current_files
|
|
98
|
+
|
|
99
|
+
# plugin/upload.py
|
|
100
|
+
from . import BaseUploader
|
|
101
|
+
|
|
102
|
+
class Uploader(BaseUploader):
|
|
103
|
+
"""Custom upload plugin implementation."""
|
|
104
|
+
|
|
105
|
+
def process_files(self, organized_files: List) -> List:
|
|
106
|
+
"""Required: Implement your file processing logic."""
|
|
107
|
+
# Your custom processing logic here
|
|
108
|
+
return organized_files
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Core Methods Reference
|
|
112
|
+
|
|
113
|
+
### Required Method
|
|
114
|
+
|
|
115
|
+
#### `process_files(organized_files: List) -> List`
|
|
116
|
+
|
|
117
|
+
**Purpose**: Main processing method that must be implemented by all plugins.
|
|
118
|
+
|
|
119
|
+
**When to use**: Always - this is where your plugin's core logic goes.
|
|
120
|
+
|
|
121
|
+
**Example**:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
def process_files(self, organized_files: List) -> List:
|
|
125
|
+
"""Convert TIFF images to JPEG format."""
|
|
126
|
+
processed_files = []
|
|
127
|
+
|
|
128
|
+
for file_group in organized_files:
|
|
129
|
+
files_dict = file_group.get('files', {})
|
|
130
|
+
converted_files = {}
|
|
131
|
+
|
|
132
|
+
for spec_name, file_path in files_dict.items():
|
|
133
|
+
if file_path.suffix.lower() in ['.tif', '.tiff']:
|
|
134
|
+
# Convert TIFF to JPEG
|
|
135
|
+
jpeg_path = self.convert_tiff_to_jpeg(file_path)
|
|
136
|
+
converted_files[spec_name] = jpeg_path
|
|
137
|
+
self.run.log_message(f"Converted {file_path} to {jpeg_path}")
|
|
138
|
+
else:
|
|
139
|
+
converted_files[spec_name] = file_path
|
|
140
|
+
|
|
141
|
+
file_group['files'] = converted_files
|
|
142
|
+
processed_files.append(file_group)
|
|
143
|
+
|
|
144
|
+
return processed_files
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Optional Hook Methods
|
|
148
|
+
|
|
149
|
+
#### `setup_directories() -> None`
|
|
150
|
+
|
|
151
|
+
**Purpose**: Create custom directory structures before processing begins.
|
|
152
|
+
|
|
153
|
+
**When to use**: When your plugin needs specific directories for processing, temporary files, or output.
|
|
154
|
+
|
|
155
|
+
**Example**:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
def setup_directories(self):
|
|
159
|
+
"""Create processing directories."""
|
|
160
|
+
(self.path / 'temp').mkdir(exist_ok=True)
|
|
161
|
+
(self.path / 'processed').mkdir(exist_ok=True)
|
|
162
|
+
(self.path / 'thumbnails').mkdir(exist_ok=True)
|
|
163
|
+
self.run.log_message("Created processing directories")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `organize_files(files: List) -> List`
|
|
167
|
+
|
|
168
|
+
**Purpose**: Reorganize and structure files before main processing.
|
|
169
|
+
|
|
170
|
+
**When to use**: When you need to group files differently, filter by criteria, or restructure the data.
|
|
171
|
+
|
|
172
|
+
**Example**:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
def organize_files(self, files: List) -> List:
|
|
176
|
+
"""Group files by size for optimized processing."""
|
|
177
|
+
large_files = []
|
|
178
|
+
small_files = []
|
|
179
|
+
|
|
180
|
+
for file_group in files:
|
|
181
|
+
total_size = sum(f.stat().st_size for f in file_group.get('files', {}).values())
|
|
182
|
+
if total_size > 100 * 1024 * 1024: # 100MB
|
|
183
|
+
large_files.append(file_group)
|
|
184
|
+
else:
|
|
185
|
+
small_files.append(file_group)
|
|
186
|
+
|
|
187
|
+
# Process large files first
|
|
188
|
+
return large_files + small_files
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### `before_process(organized_files: List) -> List`
|
|
192
|
+
|
|
193
|
+
**Purpose**: Pre-processing hook for setup tasks before main processing.
|
|
194
|
+
|
|
195
|
+
**When to use**: For validation, preparation, or initialization tasks.
|
|
196
|
+
|
|
197
|
+
**Example**:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
def before_process(self, organized_files: List) -> List:
|
|
201
|
+
"""Validate and prepare files for processing."""
|
|
202
|
+
self.run.log_message(f"Starting processing of {len(organized_files)} file groups")
|
|
203
|
+
|
|
204
|
+
# Check available disk space
|
|
205
|
+
if not self.check_disk_space(organized_files):
|
|
206
|
+
raise Exception("Insufficient disk space for processing")
|
|
207
|
+
|
|
208
|
+
return organized_files
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### `after_process(processed_files: List) -> List`
|
|
212
|
+
|
|
213
|
+
**Purpose**: Post-processing hook for cleanup and finalization.
|
|
214
|
+
|
|
215
|
+
**When to use**: For cleanup, final transformations, or resource deallocation.
|
|
216
|
+
|
|
217
|
+
**Example**:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
def after_process(self, processed_files: List) -> List:
|
|
221
|
+
"""Clean up temporary files and generate summary."""
|
|
222
|
+
# Remove temporary files
|
|
223
|
+
temp_dir = self.path / 'temp'
|
|
224
|
+
if temp_dir.exists():
|
|
225
|
+
shutil.rmtree(temp_dir)
|
|
226
|
+
|
|
227
|
+
# Generate processing summary
|
|
228
|
+
summary = {
|
|
229
|
+
'total_processed': len(processed_files),
|
|
230
|
+
'processing_time': time.time() - self.start_time
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
self.run.log_message(f"Processing complete: {summary}")
|
|
234
|
+
return processed_files
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### `validate_files(files: List) -> List`
|
|
238
|
+
|
|
239
|
+
**Purpose**: Custom validation logic beyond type checking.
|
|
240
|
+
|
|
241
|
+
**When to use**: When you need additional validation rules beyond built-in file type validation.
|
|
242
|
+
|
|
243
|
+
**Example**:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
def validate_files(self, files: List) -> List:
|
|
247
|
+
"""Custom validation with size and format checks."""
|
|
248
|
+
# First apply built-in validation
|
|
249
|
+
validated_files = super().validate_files(files)
|
|
250
|
+
|
|
251
|
+
# Then apply custom validation
|
|
252
|
+
final_files = []
|
|
253
|
+
for file_group in validated_files:
|
|
254
|
+
if self.validate_file_group(file_group):
|
|
255
|
+
final_files.append(file_group)
|
|
256
|
+
else:
|
|
257
|
+
self.run.log_message(f"File group failed validation: {file_group}")
|
|
258
|
+
|
|
259
|
+
return final_files
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### `filter_files(organized_file: Dict[str, Any]) -> bool`
|
|
263
|
+
|
|
264
|
+
**Purpose**: Filter individual files based on custom criteria.
|
|
265
|
+
|
|
266
|
+
**When to use**: When you need to exclude specific files from processing.
|
|
267
|
+
|
|
268
|
+
**Example**:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
def filter_files(self, organized_file: Dict[str, Any]) -> bool:
|
|
272
|
+
"""Filter out small files."""
|
|
273
|
+
files_dict = organized_file.get('files', {})
|
|
274
|
+
total_size = sum(f.stat().st_size for f in files_dict.values())
|
|
275
|
+
|
|
276
|
+
if total_size < 1024: # Skip files smaller than 1KB
|
|
277
|
+
self.run.log_message(f"Skipping small file group: {total_size} bytes")
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
return True
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## File Type Validation System
|
|
284
|
+
|
|
285
|
+
The BaseUploader includes a built-in validation system that you can customize:
|
|
286
|
+
|
|
287
|
+
### Default File Extensions
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
def get_file_extensions_config(self) -> Dict[str, List[str]]:
|
|
291
|
+
"""Override to customize allowed file extensions."""
|
|
292
|
+
return {
|
|
293
|
+
'pcd': ['.pcd'],
|
|
294
|
+
'text': ['.txt', '.html'],
|
|
295
|
+
'audio': ['.wav', '.mp3'],
|
|
296
|
+
'data': ['.bin', '.json', '.fbx'],
|
|
297
|
+
'image': ['.jpg', '.jpeg', '.png'],
|
|
298
|
+
'video': ['.mp4'],
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Custom Extension Configuration
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
class CustomUploader(BaseUploader):
|
|
306
|
+
def get_file_extensions_config(self) -> Dict[str, List[str]]:
|
|
307
|
+
"""Add support for additional formats."""
|
|
308
|
+
config = super().get_file_extensions_config()
|
|
309
|
+
config.update({
|
|
310
|
+
'cad': ['.dwg', '.dxf', '.step'],
|
|
311
|
+
'archive': ['.zip', '.rar', '.7z'],
|
|
312
|
+
'document': ['.pdf', '.docx', '.xlsx']
|
|
313
|
+
})
|
|
314
|
+
return config
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Conversion Warnings
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
def get_conversion_warnings_config(self) -> Dict[str, str]:
|
|
321
|
+
"""Override to customize conversion warnings."""
|
|
322
|
+
return {
|
|
323
|
+
'.tif': ' .jpg, .png',
|
|
324
|
+
'.tiff': ' .jpg, .png',
|
|
325
|
+
'.avi': ' .mp4',
|
|
326
|
+
'.mov': ' .mp4',
|
|
327
|
+
'.raw': ' .jpg, .png',
|
|
328
|
+
'.bmp': ' .jpg, .png',
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Real-World Examples
|
|
333
|
+
|
|
334
|
+
### Example 1: Image Processing Plugin
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from pathlib import Path
|
|
338
|
+
from typing import List
|
|
339
|
+
from plugin import BaseUploader
|
|
340
|
+
|
|
341
|
+
class ImageProcessingUploader(BaseUploader):
|
|
342
|
+
"""Converts TIFF images to JPEG and generates thumbnails."""
|
|
343
|
+
|
|
344
|
+
def setup_directories(self):
|
|
345
|
+
"""Create directories for processed images."""
|
|
346
|
+
(self.path / 'processed').mkdir(exist_ok=True)
|
|
347
|
+
(self.path / 'thumbnails').mkdir(exist_ok=True)
|
|
348
|
+
|
|
349
|
+
def process_files(self, organized_files: List) -> List:
|
|
350
|
+
"""Convert images and generate thumbnails."""
|
|
351
|
+
processed_files = []
|
|
352
|
+
|
|
353
|
+
for file_group in organized_files:
|
|
354
|
+
files_dict = file_group.get('files', {})
|
|
355
|
+
converted_files = {}
|
|
356
|
+
|
|
357
|
+
for spec_name, file_path in files_dict.items():
|
|
358
|
+
if file_path.suffix.lower() in ['.tif', '.tiff']:
|
|
359
|
+
# Convert to JPEG
|
|
360
|
+
jpeg_path = self.convert_to_jpeg(file_path)
|
|
361
|
+
converted_files[spec_name] = jpeg_path
|
|
362
|
+
|
|
363
|
+
# Generate thumbnail
|
|
364
|
+
thumbnail_path = self.generate_thumbnail(jpeg_path)
|
|
365
|
+
converted_files[f"{spec_name}_thumbnail"] = thumbnail_path
|
|
366
|
+
|
|
367
|
+
self.run.log_message(f"Processed {file_path.name}")
|
|
368
|
+
else:
|
|
369
|
+
converted_files[spec_name] = file_path
|
|
370
|
+
|
|
371
|
+
file_group['files'] = converted_files
|
|
372
|
+
processed_files.append(file_group)
|
|
373
|
+
|
|
374
|
+
return processed_files
|
|
375
|
+
|
|
376
|
+
def convert_to_jpeg(self, tiff_path: Path) -> Path:
|
|
377
|
+
"""Convert TIFF to JPEG using PIL."""
|
|
378
|
+
from PIL import Image
|
|
379
|
+
|
|
380
|
+
output_path = self.path / 'processed' / f"{tiff_path.stem}.jpg"
|
|
381
|
+
|
|
382
|
+
with Image.open(tiff_path) as img:
|
|
383
|
+
if img.mode in ('RGBA', 'LA', 'P'):
|
|
384
|
+
img = img.convert('RGB')
|
|
385
|
+
img.save(output_path, 'JPEG', quality=95)
|
|
386
|
+
|
|
387
|
+
return output_path
|
|
388
|
+
|
|
389
|
+
def generate_thumbnail(self, image_path: Path) -> Path:
|
|
390
|
+
"""Generate thumbnail."""
|
|
391
|
+
from PIL import Image
|
|
392
|
+
|
|
393
|
+
thumbnail_path = self.path / 'thumbnails' / f"{image_path.stem}_thumb.jpg"
|
|
394
|
+
|
|
395
|
+
with Image.open(image_path) as img:
|
|
396
|
+
img.thumbnail((200, 200), Image.Resampling.LANCZOS)
|
|
397
|
+
img.save(thumbnail_path, 'JPEG', quality=85)
|
|
398
|
+
|
|
399
|
+
return thumbnail_path
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Example 2: Data Validation Plugin
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
class DataValidationUploader(BaseUploader):
|
|
406
|
+
"""Validates data files and generates quality reports."""
|
|
407
|
+
|
|
408
|
+
def __init__(self, run, path, file_specification=None,
|
|
409
|
+
organized_files=None, extra_params=None):
|
|
410
|
+
super().__init__(run, path, file_specification, organized_files, extra_params)
|
|
411
|
+
|
|
412
|
+
# Initialize from extra_params
|
|
413
|
+
self.validation_config = extra_params.get('validation_config', {})
|
|
414
|
+
self.strict_mode = extra_params.get('strict_validation', False)
|
|
415
|
+
|
|
416
|
+
def before_process(self, organized_files: List) -> List:
|
|
417
|
+
"""Initialize validation engine."""
|
|
418
|
+
self.validation_results = []
|
|
419
|
+
self.run.log_message(f"Starting validation of {len(organized_files)} file groups")
|
|
420
|
+
return organized_files
|
|
421
|
+
|
|
422
|
+
def process_files(self, organized_files: List) -> List:
|
|
423
|
+
"""Validate files and generate quality reports."""
|
|
424
|
+
processed_files = []
|
|
425
|
+
|
|
426
|
+
for file_group in organized_files:
|
|
427
|
+
validation_result = self.validate_file_group(file_group)
|
|
428
|
+
|
|
429
|
+
# Add validation metadata
|
|
430
|
+
file_group['validation'] = validation_result
|
|
431
|
+
file_group['quality_score'] = validation_result['score']
|
|
432
|
+
|
|
433
|
+
# Include file group based on validation results
|
|
434
|
+
if self.should_include_file_group(validation_result):
|
|
435
|
+
processed_files.append(file_group)
|
|
436
|
+
self.run.log_message(f"File group passed: score {validation_result['score']}")
|
|
437
|
+
else:
|
|
438
|
+
self.run.log_message(f"File group failed: {validation_result['errors']}")
|
|
439
|
+
|
|
440
|
+
return processed_files
|
|
441
|
+
|
|
442
|
+
def validate_file_group(self, file_group: Dict) -> Dict:
|
|
443
|
+
"""Comprehensive validation of file group."""
|
|
444
|
+
files_dict = file_group.get('files', {})
|
|
445
|
+
errors = []
|
|
446
|
+
score = 100
|
|
447
|
+
|
|
448
|
+
for spec_name, file_path in files_dict.items():
|
|
449
|
+
# File existence
|
|
450
|
+
if not file_path.exists():
|
|
451
|
+
errors.append(f"File not found: {file_path}")
|
|
452
|
+
score -= 50
|
|
453
|
+
continue
|
|
454
|
+
|
|
455
|
+
# File size validation
|
|
456
|
+
file_size = file_path.stat().st_size
|
|
457
|
+
if file_size == 0:
|
|
458
|
+
errors.append(f"Empty file: {file_path}")
|
|
459
|
+
score -= 40
|
|
460
|
+
elif file_size > 1024 * 1024 * 1024: # 1GB
|
|
461
|
+
score -= 10
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
'score': max(0, score),
|
|
465
|
+
'errors': errors,
|
|
466
|
+
'validated_at': datetime.now().isoformat()
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
def should_include_file_group(self, validation_result: Dict) -> bool:
|
|
470
|
+
"""Determine if file group should be included."""
|
|
471
|
+
if validation_result['errors'] and self.strict_mode:
|
|
472
|
+
return False
|
|
473
|
+
|
|
474
|
+
min_score = self.validation_config.get('min_score', 50)
|
|
475
|
+
return validation_result['score'] >= min_score
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Example 3: Batch Processing Plugin
|
|
479
|
+
|
|
480
|
+
```python
|
|
481
|
+
class BatchProcessingUploader(BaseUploader):
|
|
482
|
+
"""Processes files in configurable batches."""
|
|
483
|
+
|
|
484
|
+
def __init__(self, run, path, file_specification=None,
|
|
485
|
+
organized_files=None, extra_params=None):
|
|
486
|
+
super().__init__(run, path, file_specification, organized_files, extra_params)
|
|
487
|
+
|
|
488
|
+
self.batch_size = extra_params.get('batch_size', 10)
|
|
489
|
+
self.parallel_processing = extra_params.get('use_parallel', True)
|
|
490
|
+
self.max_workers = extra_params.get('max_workers', 4)
|
|
491
|
+
|
|
492
|
+
def organize_files(self, files: List) -> List:
|
|
493
|
+
"""Organize files into processing batches."""
|
|
494
|
+
batches = []
|
|
495
|
+
current_batch = []
|
|
496
|
+
|
|
497
|
+
for file_group in files:
|
|
498
|
+
current_batch.append(file_group)
|
|
499
|
+
|
|
500
|
+
if len(current_batch) >= self.batch_size:
|
|
501
|
+
batches.append({
|
|
502
|
+
'batch_id': len(batches) + 1,
|
|
503
|
+
'files': current_batch,
|
|
504
|
+
'batch_size': len(current_batch)
|
|
505
|
+
})
|
|
506
|
+
current_batch = []
|
|
507
|
+
|
|
508
|
+
# Add remaining files
|
|
509
|
+
if current_batch:
|
|
510
|
+
batches.append({
|
|
511
|
+
'batch_id': len(batches) + 1,
|
|
512
|
+
'files': current_batch,
|
|
513
|
+
'batch_size': len(current_batch)
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
self.run.log_message(f"Organized into {len(batches)} batches")
|
|
517
|
+
return batches
|
|
518
|
+
|
|
519
|
+
def process_files(self, organized_files: List) -> List:
|
|
520
|
+
"""Process files in batches."""
|
|
521
|
+
all_processed_files = []
|
|
522
|
+
|
|
523
|
+
if self.parallel_processing:
|
|
524
|
+
all_processed_files = self.process_batches_parallel(organized_files)
|
|
525
|
+
else:
|
|
526
|
+
all_processed_files = self.process_batches_sequential(organized_files)
|
|
527
|
+
|
|
528
|
+
return all_processed_files
|
|
529
|
+
|
|
530
|
+
def process_batches_sequential(self, batches: List) -> List:
|
|
531
|
+
"""Process batches sequentially."""
|
|
532
|
+
all_files = []
|
|
533
|
+
|
|
534
|
+
for i, batch in enumerate(batches, 1):
|
|
535
|
+
self.run.log_message(f"Processing batch {i}/{len(batches)}")
|
|
536
|
+
processed_batch = self.process_single_batch(batch)
|
|
537
|
+
all_files.extend(processed_batch)
|
|
538
|
+
|
|
539
|
+
return all_files
|
|
540
|
+
|
|
541
|
+
def process_batches_parallel(self, batches: List) -> List:
|
|
542
|
+
"""Process batches in parallel using ThreadPoolExecutor."""
|
|
543
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
544
|
+
|
|
545
|
+
all_files = []
|
|
546
|
+
|
|
547
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
548
|
+
future_to_batch = {
|
|
549
|
+
executor.submit(self.process_single_batch, batch): batch
|
|
550
|
+
for batch in batches
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
for future in as_completed(future_to_batch):
|
|
554
|
+
batch = future_to_batch[future]
|
|
555
|
+
try:
|
|
556
|
+
processed_files = future.result()
|
|
557
|
+
all_files.extend(processed_files)
|
|
558
|
+
self.run.log_message(f"Batch {batch['batch_id']} complete")
|
|
559
|
+
except Exception as e:
|
|
560
|
+
self.run.log_message(f"Batch {batch['batch_id']} failed: {e}")
|
|
561
|
+
|
|
562
|
+
return all_files
|
|
563
|
+
|
|
564
|
+
def process_single_batch(self, batch: Dict) -> List:
|
|
565
|
+
"""Process a single batch of files."""
|
|
566
|
+
batch_files = batch['files']
|
|
567
|
+
processed_files = []
|
|
568
|
+
|
|
569
|
+
for file_group in batch_files:
|
|
570
|
+
# Add batch metadata
|
|
571
|
+
file_group['batch_processed'] = True
|
|
572
|
+
file_group['batch_id'] = batch['batch_id']
|
|
573
|
+
processed_files.append(file_group)
|
|
574
|
+
|
|
575
|
+
return processed_files
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Best Practices
|
|
579
|
+
|
|
580
|
+
### 1. Code Organization
|
|
581
|
+
|
|
582
|
+
- Keep `process_files()` focused on core logic
|
|
583
|
+
- Use hook methods for setup, cleanup, and validation
|
|
584
|
+
- Separate concerns using helper methods
|
|
585
|
+
|
|
586
|
+
### 2. Error Handling
|
|
587
|
+
|
|
588
|
+
- Implement comprehensive error handling
|
|
589
|
+
- Log errors with context information
|
|
590
|
+
- Fail gracefully when possible
|
|
591
|
+
|
|
592
|
+
### 3. Performance
|
|
593
|
+
|
|
594
|
+
- Profile your processing logic
|
|
595
|
+
- Use appropriate data structures
|
|
596
|
+
- Consider memory usage for large files
|
|
597
|
+
- Implement async processing for I/O-heavy operations
|
|
598
|
+
|
|
599
|
+
### 4. Testing
|
|
600
|
+
|
|
601
|
+
- Write unit tests for all methods
|
|
602
|
+
- Include integration tests with real files
|
|
603
|
+
- Test error conditions and edge cases
|
|
604
|
+
|
|
605
|
+
### 5. Logging
|
|
606
|
+
|
|
607
|
+
- Log important operations and milestones
|
|
608
|
+
- Include timing information
|
|
609
|
+
- Use structured logging for analysis
|
|
610
|
+
|
|
611
|
+
### 6. Configuration
|
|
612
|
+
|
|
613
|
+
- Use `extra_params` for plugin configuration
|
|
614
|
+
- Provide sensible defaults
|
|
615
|
+
- Validate configuration parameters
|
|
616
|
+
|
|
617
|
+
## Integration with Upload Action
|
|
618
|
+
|
|
619
|
+
Your BaseUploader plugin integrates with the upload action workflow:
|
|
620
|
+
|
|
621
|
+
1. **File Discovery**: Upload action discovers and organizes files
|
|
622
|
+
2. **Plugin Invocation**: Your `handle_upload_files()` is called with organized files
|
|
623
|
+
3. **Workflow Execution**: BaseUploader runs its 6-step workflow
|
|
624
|
+
4. **Return Results**: Processed files are returned to upload action
|
|
625
|
+
5. **Upload & Data Unit Creation**: Upload action completes the upload
|
|
626
|
+
|
|
627
|
+
### Data Flow
|
|
628
|
+
|
|
629
|
+
```
|
|
630
|
+
Upload Action (OrganizeFilesStep)
|
|
631
|
+
↓ organized_files
|
|
632
|
+
BaseUploader.handle_upload_files()
|
|
633
|
+
↓ setup_directories()
|
|
634
|
+
↓ organize_files()
|
|
635
|
+
↓ before_process()
|
|
636
|
+
↓ process_files() ← Your custom logic
|
|
637
|
+
↓ after_process()
|
|
638
|
+
↓ validate_files()
|
|
639
|
+
↓ processed_files
|
|
640
|
+
Upload Action (UploadFilesStep, GenerateDataUnitsStep)
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
## Configuration
|
|
644
|
+
|
|
645
|
+
### Plugin Configuration (config.yaml)
|
|
646
|
+
|
|
647
|
+
```yaml
|
|
648
|
+
code: "my-upload-plugin"
|
|
649
|
+
name: "My Upload Plugin"
|
|
650
|
+
version: "1.0.0"
|
|
651
|
+
category: "upload"
|
|
652
|
+
|
|
653
|
+
package_manager: "pip"
|
|
654
|
+
|
|
655
|
+
actions:
|
|
656
|
+
upload:
|
|
657
|
+
entrypoint: "plugin.upload.Uploader"
|
|
658
|
+
method: "job"
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Dependencies (requirements.txt)
|
|
662
|
+
|
|
663
|
+
```txt
|
|
664
|
+
synapse-sdk>=1.0.0
|
|
665
|
+
pillow>=10.0.0 # For image processing
|
|
666
|
+
pandas>=2.0.0 # For data processing
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
## Testing Your Plugin
|
|
670
|
+
|
|
671
|
+
### Unit Testing
|
|
672
|
+
|
|
673
|
+
```python
|
|
674
|
+
import pytest
|
|
675
|
+
from unittest.mock import Mock
|
|
676
|
+
from pathlib import Path
|
|
677
|
+
from plugin.upload import Uploader
|
|
678
|
+
|
|
679
|
+
class TestUploader:
|
|
680
|
+
|
|
681
|
+
def setup_method(self):
|
|
682
|
+
self.mock_run = Mock()
|
|
683
|
+
self.test_path = Path('/tmp/test')
|
|
684
|
+
self.file_spec = [{'name': 'image_1', 'file_type': 'image'}]
|
|
685
|
+
|
|
686
|
+
def test_process_files(self):
|
|
687
|
+
"""Test file processing."""
|
|
688
|
+
uploader = Uploader(
|
|
689
|
+
run=self.mock_run,
|
|
690
|
+
path=self.test_path,
|
|
691
|
+
file_specification=self.file_spec,
|
|
692
|
+
organized_files=[{'files': {}}]
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
result = uploader.process_files([{'files': {}}])
|
|
696
|
+
assert isinstance(result, list)
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Integration Testing
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
# Test with sample data
|
|
703
|
+
synapse plugin run upload '{
|
|
704
|
+
"name": "Test Upload",
|
|
705
|
+
"use_single_path": true,
|
|
706
|
+
"path": "/test/data",
|
|
707
|
+
"storage": 1,
|
|
708
|
+
"data_collection": 5
|
|
709
|
+
}' --plugin my-upload-plugin --debug
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
## See Also
|
|
713
|
+
|
|
714
|
+
- [Upload Plugin Overview](./upload-plugin-overview.md) - User guide and configuration reference
|
|
715
|
+
- [Upload Action Development](./upload-plugin-action.md) - SDK developer guide for action architecture and internals
|