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
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: upload-plugin-template
|
|
3
|
+
title: 업로드 플러그인 템플릿 개발
|
|
4
|
+
sidebar_position: 3
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# BaseUploader를 사용한 업로드 플러그인 템플릿 개발
|
|
8
|
+
|
|
9
|
+
이 가이드는 BaseUploader 템플릿을 사용하여 사용자 정의 업로드 플러그인을 만들려는 플러그인 개발자를 위한 것입니다. BaseUploader는 업로드 플러그인 내에서 파일 처리 및 구성을 위한 워크플로우 기반의 기초를 제공합니다.
|
|
10
|
+
|
|
11
|
+
## 개요
|
|
12
|
+
|
|
13
|
+
BaseUploader 템플릿 (`synapse_sdk.plugins.categories.upload.templates.plugin`)은 업로드 플러그인 구축을 위한 구조화된 접근 방식을 제공합니다. 메서드 재정의를 통해 사용자 정의를 허용하면서 일반적인 업로드 워크플로우를 처리합니다.
|
|
14
|
+
|
|
15
|
+
### BaseUploader 워크플로우
|
|
16
|
+
|
|
17
|
+
BaseUploader는 6단계 워크플로우 파이프라인을 구현합니다:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
1. setup_directories() # 사용자 정의 디렉토리 구조 생성
|
|
21
|
+
2. organize_files() # 파일 구성 및 구조화
|
|
22
|
+
3. before_process() # 전처리 후크
|
|
23
|
+
4. process_files() # 주요 처리 로직 (필수)
|
|
24
|
+
5. after_process() # 후처리 후크
|
|
25
|
+
6. validate_files() # 최종 검증
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 시작하기
|
|
29
|
+
|
|
30
|
+
### 템플릿 구조
|
|
31
|
+
|
|
32
|
+
업로드 플러그인을 생성하면 다음과 같은 구조를 갖게 됩니다:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
synapse-{plugin-code}-plugin/
|
|
36
|
+
├── config.yaml # 플러그인 메타데이터 및 구성
|
|
37
|
+
├── plugin/ # 소스 코드 디렉토리
|
|
38
|
+
│ ├── __init__.py
|
|
39
|
+
│ └── upload.py # BaseUploader를 사용한 주요 업로드 구현
|
|
40
|
+
├── requirements.txt # 파이썬 의존성
|
|
41
|
+
├── pyproject.toml # 패키지 구성
|
|
42
|
+
└── README.md # 플러그인 문서
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 기본 플러그인 구현
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# plugin/__init__.py
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from typing import Any, Dict, List
|
|
51
|
+
|
|
52
|
+
class BaseUploader:
|
|
53
|
+
"""일반적인 업로드 기능을 가진 기본 클래스."""
|
|
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
|
+
# 재정의 가능한 핵심 워크플로우 메서드
|
|
64
|
+
def setup_directories(self) -> None:
|
|
65
|
+
"""사용자 정의 디렉토리 설정 - 필요에 따라 재정의."""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def organize_files(self, files: List) -> List:
|
|
69
|
+
"""파일 구성 - 사용자 정의 로직을 위해 재정의."""
|
|
70
|
+
return files
|
|
71
|
+
|
|
72
|
+
def before_process(self, organized_files: List) -> List:
|
|
73
|
+
"""전처리 후크 - 필요에 따라 재정의."""
|
|
74
|
+
return organized_files
|
|
75
|
+
|
|
76
|
+
def process_files(self, organized_files: List) -> List:
|
|
77
|
+
"""주요 처리 - 반드시 재정의해야 함."""
|
|
78
|
+
return organized_files
|
|
79
|
+
|
|
80
|
+
def after_process(self, processed_files: List) -> List:
|
|
81
|
+
"""후처리 후크 - 필요에 따라 재정의."""
|
|
82
|
+
return processed_files
|
|
83
|
+
|
|
84
|
+
def validate_files(self, files: List) -> List:
|
|
85
|
+
"""검증 - 사용자 정의 검증을 위해 재정의."""
|
|
86
|
+
return self._filter_valid_files(files)
|
|
87
|
+
|
|
88
|
+
def handle_upload_files(self) -> List:
|
|
89
|
+
"""주요 진입점 - 워크플로우를 실행합니다."""
|
|
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
|
+
"""사용자 정의 업로드 플러그인 구현."""
|
|
104
|
+
|
|
105
|
+
def process_files(self, organized_files: List) -> List:
|
|
106
|
+
"""필수: 파일 처리 로직을 구현하십시오."""
|
|
107
|
+
# 여기에 사용자 정의 처리 로직
|
|
108
|
+
return organized_files
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 핵심 메서드 참조
|
|
112
|
+
|
|
113
|
+
### 필수 메서드
|
|
114
|
+
|
|
115
|
+
#### `process_files(organized_files: List) -> List`
|
|
116
|
+
|
|
117
|
+
**목적**: 모든 플러그인에서 구현해야 하는 주요 처리 메서드.
|
|
118
|
+
|
|
119
|
+
**사용 시기**: 항상 - 플러그인의 핵심 로직이 여기에 들어갑니다.
|
|
120
|
+
|
|
121
|
+
**예시**:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
def process_files(self, organized_files: List) -> List:
|
|
125
|
+
"""TIFF 이미지를 JPEG 형식으로 변환합니다."""
|
|
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
|
+
# TIFF를 JPEG로 변환
|
|
135
|
+
jpeg_path = self.convert_tiff_to_jpeg(file_path)
|
|
136
|
+
converted_files[spec_name] = jpeg_path
|
|
137
|
+
self.run.log_message(f"{file_path}를 {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
|
+
### 선택적 후크 메서드
|
|
148
|
+
|
|
149
|
+
#### `setup_directories() -> None`
|
|
150
|
+
|
|
151
|
+
**목적**: 처리가 시작되기 전에 사용자 정의 디렉토리 구조를 생성합니다.
|
|
152
|
+
|
|
153
|
+
**사용 시기**: 플러그인이 처리, 임시 파일 또는 출력을 위해 특정 디렉토리가 필요할 때.
|
|
154
|
+
|
|
155
|
+
**예시**:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
def setup_directories(self):
|
|
159
|
+
"""처리 디렉토리를 생성합니다."""
|
|
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("처리 디렉토리를 생성했습니다.")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `organize_files(files: List) -> List`
|
|
167
|
+
|
|
168
|
+
**목적**: 주요 처리 전에 파일을 재구성하고 구조화합니다.
|
|
169
|
+
|
|
170
|
+
**사용 시기**: 파일을 다르게 그룹화하거나, 기준으로 필터링하거나, 데이터를 재구성해야 할 때.
|
|
171
|
+
|
|
172
|
+
**예시**:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
def organize_files(self, files: List) -> List:
|
|
176
|
+
"""최적화된 처리를 위해 크기별로 파일을 그룹화합니다."""
|
|
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
|
+
# 큰 파일 먼저 처리
|
|
188
|
+
return large_files + small_files
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### `before_process(organized_files: List) -> List`
|
|
192
|
+
|
|
193
|
+
**목적**: 주요 처리 전 설정 작업을 위한 전처리 후크.
|
|
194
|
+
|
|
195
|
+
**사용 시기**: 검증, 준비 또는 초기화 작업에 사용합니다.
|
|
196
|
+
|
|
197
|
+
**예시**:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
def before_process(self, organized_files: List) -> List:
|
|
201
|
+
"""처리를 위해 파일을 검증하고 준비합니다."""
|
|
202
|
+
self.run.log_message(f"{len(organized_files)}개 파일 그룹의 처리를 시작합니다.")
|
|
203
|
+
|
|
204
|
+
# 사용 가능한 디스크 공간 확인
|
|
205
|
+
if not self.check_disk_space(organized_files):
|
|
206
|
+
raise Exception("처리에 필요한 디스크 공간이 부족합니다.")
|
|
207
|
+
|
|
208
|
+
return organized_files
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### `after_process(processed_files: List) -> List`
|
|
212
|
+
|
|
213
|
+
**목적**: 정리 및 최종화를 위한 후처리 후크.
|
|
214
|
+
|
|
215
|
+
**사용 시기**: 정리, 최종 변환 또는 리소스 할당 해제에 사용합니다.
|
|
216
|
+
|
|
217
|
+
**예시**:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
def after_process(self, processed_files: List) -> List:
|
|
221
|
+
"""임시 파일을 정리하고 요약을 생성합니다."""
|
|
222
|
+
# 임시 파일 제거
|
|
223
|
+
temp_dir = self.path / 'temp'
|
|
224
|
+
if temp_dir.exists():
|
|
225
|
+
shutil.rmtree(temp_dir)
|
|
226
|
+
|
|
227
|
+
# 처리 요약 생성
|
|
228
|
+
summary = {
|
|
229
|
+
'total_processed': len(processed_files),
|
|
230
|
+
'processing_time': time.time() - self.start_time
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
self.run.log_message(f"처리가 완료되었습니다: {summary}")
|
|
234
|
+
return processed_files
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### `validate_files(files: List) -> List`
|
|
238
|
+
|
|
239
|
+
**목적**: 유형 검사를 넘어서는 사용자 정의 검증 로직.
|
|
240
|
+
|
|
241
|
+
**사용 시기**: 내장 파일 유형 검증 외에 추가적인 검증 규칙이 필요할 때.
|
|
242
|
+
|
|
243
|
+
**예시**:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
def validate_files(self, files: List) -> List:
|
|
247
|
+
"""크기 및 형식 검사를 포함한 사용자 정의 검증."""
|
|
248
|
+
# 먼저 내장 검증 적용
|
|
249
|
+
validated_files = super().validate_files(files)
|
|
250
|
+
|
|
251
|
+
# 그런 다음 사용자 정의 검증 적용
|
|
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}")
|
|
258
|
+
|
|
259
|
+
return final_files
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### `filter_files(organized_file: Dict[str, Any]) -> bool`
|
|
263
|
+
|
|
264
|
+
**목적**: 사용자 정의 기준에 따라 개별 파일을 필터링합니다.
|
|
265
|
+
|
|
266
|
+
**사용 시기**: 처리에서 특정 파일을 제외해야 할 때.
|
|
267
|
+
|
|
268
|
+
**예시**:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
def filter_files(self, organized_file: Dict[str, Any]) -> bool:
|
|
272
|
+
"""작은 파일을 필터링합니다."""
|
|
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: # 1KB보다 작은 파일 건너뛰기
|
|
277
|
+
self.run.log_message(f"작은 파일 그룹 건너뛰기: {total_size} 바이트")
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
return True
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## 파일 유형 검증 시스템
|
|
284
|
+
|
|
285
|
+
BaseUploader에는 사용자 정의할 수 있는 내장 검증 시스템이 포함되어 있습니다:
|
|
286
|
+
|
|
287
|
+
### 기본 파일 확장자
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
def get_file_extensions_config(self) -> Dict[str, List[str]]:
|
|
291
|
+
"""허용되는 파일 확장자를 사용자 정의하려면 재정의하십시오."""
|
|
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
|
+
### 사용자 정의 확장자 구성
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
class CustomUploader(BaseUploader):
|
|
306
|
+
def get_file_extensions_config(self) -> Dict[str, List[str]]:
|
|
307
|
+
"""추가 형식에 대한 지원을 추가합니다."""
|
|
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
|
+
### 변환 경고
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
def get_conversion_warnings_config(self) -> Dict[str, str]:
|
|
321
|
+
"""변환 경고를 사용자 정의하려면 재정의하십시오."""
|
|
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
|
+
## 실제 예제
|
|
333
|
+
|
|
334
|
+
### 예제 1: 이미지 처리 플러그인
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from pathlib import Path
|
|
338
|
+
from typing import List
|
|
339
|
+
from plugin import BaseUploader
|
|
340
|
+
|
|
341
|
+
class ImageProcessingUploader(BaseUploader):
|
|
342
|
+
"""TIFF 이미지를 JPEG로 변환하고 썸네일을 생성합니다."""
|
|
343
|
+
|
|
344
|
+
def setup_directories(self):
|
|
345
|
+
"""처리된 이미지를 위한 디렉토리를 생성합니다."""
|
|
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
|
+
"""이미지를 변환하고 썸네일을 생성합니다."""
|
|
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
|
+
# JPEG로 변환
|
|
360
|
+
jpeg_path = self.convert_to_jpeg(file_path)
|
|
361
|
+
converted_files[spec_name] = jpeg_path
|
|
362
|
+
|
|
363
|
+
# 썸네일 생성
|
|
364
|
+
thumbnail_path = self.generate_thumbnail(jpeg_path)
|
|
365
|
+
converted_files[f"{spec_name}_thumbnail"] = thumbnail_path
|
|
366
|
+
|
|
367
|
+
self.run.log_message(f"{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
|
+
"""PIL을 사용하여 TIFF를 JPEG로 변환합니다."""
|
|
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
|
+
"""썸네일을 생성합니다."""
|
|
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
|
+
### 예제 2: 데이터 검증 플러그인
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
class DataValidationUploader(BaseUploader):
|
|
406
|
+
"""데이터 파일을 검증하고 품질 보고서를 생성합니다."""
|
|
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
|
+
# 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
|
+
"""검증 엔진을 초기화합니다."""
|
|
418
|
+
self.validation_results = []
|
|
419
|
+
self.run.log_message(f"{len(organized_files)}개 파일 그룹의 검증을 시작합니다.")
|
|
420
|
+
return organized_files
|
|
421
|
+
|
|
422
|
+
def process_files(self, organized_files: List) -> List:
|
|
423
|
+
"""파일을 검증하고 품질 보고서를 생성합니다."""
|
|
424
|
+
processed_files = []
|
|
425
|
+
|
|
426
|
+
for file_group in organized_files:
|
|
427
|
+
validation_result = self.validate_file_group(file_group)
|
|
428
|
+
|
|
429
|
+
# 검증 메타데이터 추가
|
|
430
|
+
file_group['validation'] = validation_result
|
|
431
|
+
file_group['quality_score'] = validation_result['score']
|
|
432
|
+
|
|
433
|
+
# 검증 결과에 따라 파일 그룹 포함
|
|
434
|
+
if self.should_include_file_group(validation_result):
|
|
435
|
+
processed_files.append(file_group)
|
|
436
|
+
self.run.log_message(f"파일 그룹 통과: 점수 {validation_result['score']}")
|
|
437
|
+
else:
|
|
438
|
+
self.run.log_message(f"파일 그룹 실패: {validation_result['errors']}")
|
|
439
|
+
|
|
440
|
+
return processed_files
|
|
441
|
+
|
|
442
|
+
def validate_file_group(self, file_group: Dict) -> Dict:
|
|
443
|
+
"""파일 그룹의 포괄적인 검증."""
|
|
444
|
+
files_dict = file_group.get('files', {})
|
|
445
|
+
errors = []
|
|
446
|
+
score = 100
|
|
447
|
+
|
|
448
|
+
for spec_name, file_path in files_dict.items():
|
|
449
|
+
# 파일 존재 여부
|
|
450
|
+
if not file_path.exists():
|
|
451
|
+
errors.append(f"파일을 찾을 수 없음: {file_path}")
|
|
452
|
+
score -= 50
|
|
453
|
+
continue
|
|
454
|
+
|
|
455
|
+
# 파일 크기 검증
|
|
456
|
+
file_size = file_path.stat().st_size
|
|
457
|
+
if file_size == 0:
|
|
458
|
+
errors.append(f"빈 파일: {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
|
+
"""파일 그룹을 포함해야 하는지 결정합니다."""
|
|
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
|
+
### 예제 3: 배치 처리 플러그인
|
|
479
|
+
|
|
480
|
+
```python
|
|
481
|
+
class BatchProcessingUploader(BaseUploader):
|
|
482
|
+
"""구성 가능한 배치로 파일을 처리합니다."""
|
|
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
|
+
"""파일을 처리 배치로 구성합니다."""
|
|
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
|
+
# 남은 파일 추가
|
|
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"{len(batches)}개의 배치로 구성되었습니다.")
|
|
517
|
+
return batches
|
|
518
|
+
|
|
519
|
+
def process_files(self, organized_files: List) -> List:
|
|
520
|
+
"""배치로 파일을 처리합니다."""
|
|
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
|
+
"""배치를 순차적으로 처리합니다."""
|
|
532
|
+
all_files = []
|
|
533
|
+
|
|
534
|
+
for i, batch in enumerate(batches, 1):
|
|
535
|
+
self.run.log_message(f"{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
|
+
"""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_id']} 완료")
|
|
559
|
+
except Exception as e:
|
|
560
|
+
self.run.log_message(f"배치 {batch['batch_id']} 실패: {e}")
|
|
561
|
+
|
|
562
|
+
return all_files
|
|
563
|
+
|
|
564
|
+
def process_single_batch(self, batch: Dict) -> List:
|
|
565
|
+
"""단일 파일 배치를 처리합니다."""
|
|
566
|
+
batch_files = batch['files']
|
|
567
|
+
processed_files = []
|
|
568
|
+
|
|
569
|
+
for file_group in batch_files:
|
|
570
|
+
# 배치 메타데이터 추가
|
|
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
|
+
## 모범 사례
|
|
579
|
+
|
|
580
|
+
### 1. 코드 구성
|
|
581
|
+
|
|
582
|
+
- `process_files()`를 핵심 로직에 집중
|
|
583
|
+
- 설정, 정리 및 검증을 위해 후크 메서드 사용
|
|
584
|
+
- 헬퍼 메서드를 사용하여 관심사 분리
|
|
585
|
+
|
|
586
|
+
### 2. 오류 처리
|
|
587
|
+
|
|
588
|
+
- 포괄적인 오류 처리 구현
|
|
589
|
+
- 컨텍스트 정보와 함께 오류 기록
|
|
590
|
+
- 가능하면 정상적으로 실패
|
|
591
|
+
|
|
592
|
+
### 3. 성능
|
|
593
|
+
|
|
594
|
+
- 처리 로직 프로파일링
|
|
595
|
+
- 적절한 데이터 구조 사용
|
|
596
|
+
- 큰 파일에 대한 메모리 사용량 고려
|
|
597
|
+
- I/O 집약적인 작업에 대한 비동기 처리 구현
|
|
598
|
+
|
|
599
|
+
### 4. 테스트
|
|
600
|
+
|
|
601
|
+
- 모든 메서드에 대한 단위 테스트 작성
|
|
602
|
+
- 실제 파일을 사용한 통합 테스트 포함
|
|
603
|
+
- 오류 조건 및 엣지 케이스 테스트
|
|
604
|
+
|
|
605
|
+
### 5. 로깅
|
|
606
|
+
|
|
607
|
+
- 중요한 작업 및 마일스톤 기록
|
|
608
|
+
- 타이밍 정보 포함
|
|
609
|
+
- 분석을 위한 구조화된 로깅 사용
|
|
610
|
+
|
|
611
|
+
### 6. 구성
|
|
612
|
+
|
|
613
|
+
- 플러그인 구성에 `extra_params` 사용
|
|
614
|
+
- 합리적인 기본값 제공
|
|
615
|
+
- 구성 매개변수 검증
|
|
616
|
+
|
|
617
|
+
## 업로드 액션과의 통합
|
|
618
|
+
|
|
619
|
+
BaseUploader 플러그인은 업로드 액션 워크플로우와 통합됩니다:
|
|
620
|
+
|
|
621
|
+
1. **파일 검색**: 업로드 액션이 파일을 검색하고 구성합니다.
|
|
622
|
+
2. **플러그인 호출**: 구성된 파일과 함께 `handle_upload_files()`가 호출됩니다.
|
|
623
|
+
3. **워크플로우 실행**: BaseUploader가 6단계 워크플로우를 실행합니다.
|
|
624
|
+
4. **결과 반환**: 처리된 파일이 업로드 액션으로 반환됩니다.
|
|
625
|
+
5. **업로드 및 데이터 단위 생성**: 업로드 액션이 업로드를 완료합니다.
|
|
626
|
+
|
|
627
|
+
### 데이터 흐름
|
|
628
|
+
|
|
629
|
+
```
|
|
630
|
+
업로드 액션 (OrganizeFilesStep)
|
|
631
|
+
↓ organized_files
|
|
632
|
+
BaseUploader.handle_upload_files()
|
|
633
|
+
↓ setup_directories()
|
|
634
|
+
↓ organize_files()
|
|
635
|
+
↓ before_process()
|
|
636
|
+
↓ process_files() ← 사용자 정의 로직
|
|
637
|
+
↓ after_process()
|
|
638
|
+
↓ validate_files()
|
|
639
|
+
↓ processed_files
|
|
640
|
+
업로드 액션 (UploadFilesStep, GenerateDataUnitsStep)
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
## 구성
|
|
644
|
+
|
|
645
|
+
### 플러그인 구성 (config.yaml)
|
|
646
|
+
|
|
647
|
+
```yaml
|
|
648
|
+
code: "my-upload-plugin"
|
|
649
|
+
name: "내 업로드 플러그인"
|
|
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
|
+
### 의존성 (requirements.txt)
|
|
662
|
+
|
|
663
|
+
```txt
|
|
664
|
+
synapse-sdk>=1.0.0
|
|
665
|
+
pillow>=10.0.0 # 이미지 처리용
|
|
666
|
+
pandas>=2.0.0 # 데이터 처리용
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
## 플러그인 테스트
|
|
670
|
+
|
|
671
|
+
### 단위 테스트
|
|
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
|
+
"""파일 처리 테스트."""
|
|
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
|
+
### 통합 테스트
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
# 샘플 데이터로 테스트
|
|
703
|
+
synapse plugin run upload '{
|
|
704
|
+
"name": "테스트 업로드",
|
|
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
|
+
## 참조
|
|
713
|
+
|
|
714
|
+
- [업로드 플러그인 개요](./upload-plugin-overview.md) - 사용자 가이드 및 구성 참조
|
|
715
|
+
- [업로드 액션 개발](./upload-plugin-action.md) - 액션 아키텍처 및 내부에 대한 SDK 개발자 가이드
|