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
|
@@ -1,1463 +0,0 @@
|
|
|
1
|
-
# BaseUploader로 업로드 템플릿 개발하기
|
|
2
|
-
|
|
3
|
-
이 가이드는 BaseUploader 템플릿 클래스를 사용하여 커스텀 업로드 플러그인을 만들고자 하는 플러그인 개발자를 위한 포괄적인 문서를 제공합니다. BaseUploader는 템플릿 메소드 패턴을 따라 파일 처리 워크플로우를 위한 구조화되고 확장 가능한 기반을 제공합니다.
|
|
4
|
-
|
|
5
|
-
## 빠른 시작
|
|
6
|
-
|
|
7
|
-
### 기본 플러그인 구조
|
|
8
|
-
|
|
9
|
-
BaseUploader를 상속하여 업로드 플러그인을 생성하세요:
|
|
10
|
-
|
|
11
|
-
```python
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import List, Dict, Any
|
|
14
|
-
from . import BaseUploader
|
|
15
|
-
|
|
16
|
-
class MyUploader(BaseUploader):
|
|
17
|
-
def __init__(self, run, path: Path, file_specification: List = None,
|
|
18
|
-
organized_files: List = None, extra_params: Dict = None):
|
|
19
|
-
super().__init__(run, path, file_specification, organized_files, extra_params)
|
|
20
|
-
|
|
21
|
-
def process_files(self, organized_files: List) -> List:
|
|
22
|
-
"""여기에 커스텀 파일 처리 로직을 구현하세요."""
|
|
23
|
-
# 처리 로직이 여기에 들어갑니다
|
|
24
|
-
return organized_files
|
|
25
|
-
|
|
26
|
-
def handle_upload_files(self) -> List[Dict[str, Any]]:
|
|
27
|
-
"""업로드 액션에서 호출되는 주요 진입점."""
|
|
28
|
-
return super().handle_upload_files()
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### 최소 동작 예제
|
|
32
|
-
|
|
33
|
-
```python
|
|
34
|
-
class SimpleUploader(BaseUploader):
|
|
35
|
-
def process_files(self, organized_files: List) -> List:
|
|
36
|
-
"""각 파일 그룹에 메타데이터 추가."""
|
|
37
|
-
for file_group in organized_files:
|
|
38
|
-
file_group['processed_by'] = 'SimpleUploader'
|
|
39
|
-
file_group['processing_timestamp'] = datetime.now().isoformat()
|
|
40
|
-
return organized_files
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## 아키텍처 심층 분석
|
|
44
|
-
|
|
45
|
-
### 워크플로우 파이프라인
|
|
46
|
-
|
|
47
|
-
BaseUploader는 포괄적인 6단계 워크플로우 파이프라인을 구현합니다:
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
1. setup_directories() # 디렉토리 구조 초기화
|
|
51
|
-
2. organize_files() # 파일 그룹화 및 구조화
|
|
52
|
-
3. before_process() # 전처리 훅
|
|
53
|
-
4. process_files() # 주요 처리 로직 (필수)
|
|
54
|
-
5. after_process() # 후처리 훅
|
|
55
|
-
6. validate_files() # 최종 검증 및 필터링
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### 템플릿 메소드 패턴
|
|
59
|
-
|
|
60
|
-
BaseUploader는 다음과 같은 템플릿 메소드 패턴을 사용합니다:
|
|
61
|
-
- **구체적 메소드**: 대부분의 경우에 작동하는 기본 동작 제공
|
|
62
|
-
- **훅 메소드**: 특정 지점에서 커스터마이제이션 허용
|
|
63
|
-
- **추상 메소드**: 서브클래스에서 반드시 구현해야 함
|
|
64
|
-
|
|
65
|
-
## 핵심 메소드 참조
|
|
66
|
-
|
|
67
|
-
### 필수 메소드
|
|
68
|
-
|
|
69
|
-
#### `process_files(organized_files: List) -> List`
|
|
70
|
-
|
|
71
|
-
**목적**: 플러그인의 로직에 따라 파일을 변환하는 주요 처리 메소드.
|
|
72
|
-
|
|
73
|
-
**사용 시기**: 항상 - 모든 플러그인이 반드시 구현해야 하는 핵심 메소드입니다.
|
|
74
|
-
|
|
75
|
-
**매개변수**:
|
|
76
|
-
- `organized_files`: 구성된 파일 데이터를 포함하는 파일 그룹 딕셔너리 목록
|
|
77
|
-
|
|
78
|
-
**반환값**: 업로드 준비가 완료된 처리된 파일 그룹 목록
|
|
79
|
-
|
|
80
|
-
**예제**:
|
|
81
|
-
```python
|
|
82
|
-
def process_files(self, organized_files: List) -> List:
|
|
83
|
-
"""TIFF 이미지를 JPEG 형식으로 변환."""
|
|
84
|
-
processed_files = []
|
|
85
|
-
|
|
86
|
-
for file_group in organized_files:
|
|
87
|
-
files_dict = file_group.get('files', {})
|
|
88
|
-
converted_files = {}
|
|
89
|
-
|
|
90
|
-
for spec_name, file_path in files_dict.items():
|
|
91
|
-
if file_path.suffix.lower() in ['.tif', '.tiff']:
|
|
92
|
-
# TIFF를 JPEG로 변환
|
|
93
|
-
jpeg_path = self.convert_tiff_to_jpeg(file_path)
|
|
94
|
-
converted_files[spec_name] = jpeg_path
|
|
95
|
-
self.run.log_message(f"{file_path}를 {jpeg_path}로 변환했습니다")
|
|
96
|
-
else:
|
|
97
|
-
converted_files[spec_name] = file_path
|
|
98
|
-
|
|
99
|
-
file_group['files'] = converted_files
|
|
100
|
-
processed_files.append(file_group)
|
|
101
|
-
|
|
102
|
-
return processed_files
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### 선택적 훅 메소드
|
|
106
|
-
|
|
107
|
-
#### `setup_directories() -> None`
|
|
108
|
-
|
|
109
|
-
**목적**: 처리 시작 전에 커스텀 디렉토리 구조를 생성.
|
|
110
|
-
|
|
111
|
-
**사용 시기**: 플러그인이 처리, 임시 파일 또는 출력을 위한 특정 디렉토리가 필요할 때.
|
|
112
|
-
|
|
113
|
-
**예제**:
|
|
114
|
-
```python
|
|
115
|
-
def setup_directories(self):
|
|
116
|
-
"""처리 디렉토리 생성."""
|
|
117
|
-
(self.path / 'temp').mkdir(exist_ok=True)
|
|
118
|
-
(self.path / 'processed').mkdir(exist_ok=True)
|
|
119
|
-
(self.path / 'thumbnails').mkdir(exist_ok=True)
|
|
120
|
-
self.run.log_message("처리 디렉토리를 생성했습니다")
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
#### `organize_files(files: List) -> List`
|
|
124
|
-
|
|
125
|
-
**목적**: 주요 처리 전에 파일을 재구성하고 구조화.
|
|
126
|
-
|
|
127
|
-
**사용 시기**: 파일을 다르게 그룹화하거나, 기준에 따라 필터링하거나, 데이터를 재구조화해야 할 때.
|
|
128
|
-
|
|
129
|
-
**예제**:
|
|
130
|
-
```python
|
|
131
|
-
def organize_files(self, files: List) -> List:
|
|
132
|
-
"""타입과 크기별로 파일 그룹화."""
|
|
133
|
-
large_files = []
|
|
134
|
-
small_files = []
|
|
135
|
-
|
|
136
|
-
for file_group in files:
|
|
137
|
-
total_size = sum(f.stat().st_size for f in file_group.get('files', {}).values())
|
|
138
|
-
if total_size > 100 * 1024 * 1024: # 100MB
|
|
139
|
-
large_files.append(file_group)
|
|
140
|
-
else:
|
|
141
|
-
small_files.append(file_group)
|
|
142
|
-
|
|
143
|
-
# 큰 파일을 먼저 처리
|
|
144
|
-
return large_files + small_files
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
#### `before_process(organized_files: List) -> List`
|
|
148
|
-
|
|
149
|
-
**목적**: 주요 처리 전 설정 작업을 위한 전처리 훅.
|
|
150
|
-
|
|
151
|
-
**사용 시기**: 검증, 준비 또는 초기화 작업에 사용.
|
|
152
|
-
|
|
153
|
-
**예제**:
|
|
154
|
-
```python
|
|
155
|
-
def before_process(self, organized_files: List) -> List:
|
|
156
|
-
"""처리를 위한 파일 검증 및 준비."""
|
|
157
|
-
self.run.log_message(f"{len(organized_files)}개 파일 그룹 처리 시작")
|
|
158
|
-
|
|
159
|
-
# 사용 가능한 디스크 공간 확인
|
|
160
|
-
if not self.check_disk_space(organized_files):
|
|
161
|
-
raise Exception("처리를 위한 디스크 공간이 부족합니다")
|
|
162
|
-
|
|
163
|
-
# 처리 리소스 초기화
|
|
164
|
-
self.processing_queue = Queue()
|
|
165
|
-
return organized_files
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
#### `after_process(processed_files: List) -> List`
|
|
169
|
-
|
|
170
|
-
**목적**: 정리 및 완료를 위한 후처리 훅.
|
|
171
|
-
|
|
172
|
-
**사용 시기**: 정리, 최종 변환 또는 리소스 해제에 사용.
|
|
173
|
-
|
|
174
|
-
**예제**:
|
|
175
|
-
```python
|
|
176
|
-
def after_process(self, processed_files: List) -> List:
|
|
177
|
-
"""임시 파일 정리 및 요약 생성."""
|
|
178
|
-
# 임시 파일 제거
|
|
179
|
-
temp_dir = self.path / 'temp'
|
|
180
|
-
if temp_dir.exists():
|
|
181
|
-
shutil.rmtree(temp_dir)
|
|
182
|
-
|
|
183
|
-
# 처리 요약 생성
|
|
184
|
-
summary = {
|
|
185
|
-
'total_processed': len(processed_files),
|
|
186
|
-
'processing_time': time.time() - self.start_time,
|
|
187
|
-
'plugin_version': '1.0.0'
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
self.run.log_message(f"처리 완료: {summary}")
|
|
191
|
-
return processed_files
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
#### `validate_files(files: List) -> List`
|
|
195
|
-
|
|
196
|
-
**목적**: 타입 검사를 넘어선 커스텀 검증 로직.
|
|
197
|
-
|
|
198
|
-
**사용 시기**: 내장된 파일 타입 검증 외에 추가적인 검증 규칙이 필요할 때.
|
|
199
|
-
|
|
200
|
-
**예제**:
|
|
201
|
-
```python
|
|
202
|
-
def validate_files(self, files: List) -> List:
|
|
203
|
-
"""크기 및 형식 검사를 포함한 커스텀 검증."""
|
|
204
|
-
# 먼저 내장 타입 검증 적용
|
|
205
|
-
validated_files = self.validate_file_types(files)
|
|
206
|
-
|
|
207
|
-
# 그 다음 커스텀 검증 적용
|
|
208
|
-
final_files = []
|
|
209
|
-
for file_group in validated_files:
|
|
210
|
-
if self.validate_file_group(file_group):
|
|
211
|
-
final_files.append(file_group)
|
|
212
|
-
else:
|
|
213
|
-
self.run.log_message(f"커스텀 검증 실패한 파일 그룹: {file_group}")
|
|
214
|
-
|
|
215
|
-
return final_files
|
|
216
|
-
|
|
217
|
-
def validate_file_group(self, file_group: Dict) -> bool:
|
|
218
|
-
"""개별 파일 그룹에 대한 커스텀 검증."""
|
|
219
|
-
files_dict = file_group.get('files', {})
|
|
220
|
-
|
|
221
|
-
for spec_name, file_path in files_dict.items():
|
|
222
|
-
# 파일 크기 제한 확인
|
|
223
|
-
if file_path.stat().st_size > 500 * 1024 * 1024: # 500MB 제한
|
|
224
|
-
return False
|
|
225
|
-
|
|
226
|
-
# 파일 접근 가능성 확인
|
|
227
|
-
if not os.access(file_path, os.R_OK):
|
|
228
|
-
return False
|
|
229
|
-
|
|
230
|
-
return True
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
## 고급 기능
|
|
234
|
-
|
|
235
|
-
### 파일 타입 검증 시스템
|
|
236
|
-
|
|
237
|
-
BaseUploader는 커스터마이즈할 수 있는 정교한 검증 시스템을 포함합니다:
|
|
238
|
-
|
|
239
|
-
#### 기본 파일 확장자
|
|
240
|
-
|
|
241
|
-
```python
|
|
242
|
-
def get_file_extensions_config(self) -> Dict[str, List[str]]:
|
|
243
|
-
"""허용된 파일 확장자를 커스터마이즈하려면 오버라이드하세요."""
|
|
244
|
-
return {
|
|
245
|
-
'pcd': ['.pcd'],
|
|
246
|
-
'text': ['.txt', '.html'],
|
|
247
|
-
'audio': ['.wav', '.mp3'],
|
|
248
|
-
'data': ['.bin', '.json', '.fbx'],
|
|
249
|
-
'image': ['.jpg', '.jpeg', '.png'],
|
|
250
|
-
'video': ['.mp4'],
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
#### 커스텀 확장자 구성
|
|
255
|
-
|
|
256
|
-
```python
|
|
257
|
-
class CustomUploader(BaseUploader):
|
|
258
|
-
def get_file_extensions_config(self) -> Dict[str, List[str]]:
|
|
259
|
-
"""추가 형식 지원 추가."""
|
|
260
|
-
config = super().get_file_extensions_config()
|
|
261
|
-
config.update({
|
|
262
|
-
'cad': ['.dwg', '.dxf', '.step'],
|
|
263
|
-
'archive': ['.zip', '.rar', '.7z'],
|
|
264
|
-
'document': ['.pdf', '.docx', '.xlsx']
|
|
265
|
-
})
|
|
266
|
-
return config
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
#### 변환 경고
|
|
270
|
-
|
|
271
|
-
```python
|
|
272
|
-
def get_conversion_warnings_config(self) -> Dict[str, str]:
|
|
273
|
-
"""변환 경고를 커스터마이즈하려면 오버라이드하세요."""
|
|
274
|
-
return {
|
|
275
|
-
'.tif': ' .jpg, .png',
|
|
276
|
-
'.tiff': ' .jpg, .png',
|
|
277
|
-
'.avi': ' .mp4',
|
|
278
|
-
'.mov': ' .mp4',
|
|
279
|
-
'.raw': ' .jpg, .png',
|
|
280
|
-
'.bmp': ' .jpg, .png',
|
|
281
|
-
}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### 커스텀 필터링
|
|
285
|
-
|
|
286
|
-
세밀한 제어를 위해 `filter_files` 메소드를 구현하세요:
|
|
287
|
-
|
|
288
|
-
```python
|
|
289
|
-
def filter_files(self, organized_file: Dict[str, Any]) -> bool:
|
|
290
|
-
"""커스텀 필터링 로직."""
|
|
291
|
-
# 파일 크기별 필터링
|
|
292
|
-
files_dict = organized_file.get('files', {})
|
|
293
|
-
total_size = sum(f.stat().st_size for f in files_dict.values())
|
|
294
|
-
|
|
295
|
-
if total_size < 1024: # 1KB보다 작은 파일 건너뛰기
|
|
296
|
-
self.run.log_message(f"작은 파일 그룹 건너뛰기: {total_size} bytes")
|
|
297
|
-
return False
|
|
298
|
-
|
|
299
|
-
# 파일 나이별 필터링
|
|
300
|
-
oldest_file = min(files_dict.values(), key=lambda f: f.stat().st_mtime)
|
|
301
|
-
age_days = (time.time() - oldest_file.stat().st_mtime) / 86400
|
|
302
|
-
|
|
303
|
-
if age_days > 365: # 1년보다 오래된 파일 건너뛰기
|
|
304
|
-
self.run.log_message(f"오래된 파일 그룹 건너뛰기: {age_days}일 전")
|
|
305
|
-
return False
|
|
306
|
-
|
|
307
|
-
return True
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
## 실제 사용 예제
|
|
311
|
-
|
|
312
|
-
### 예제 1: 이미지 처리 플러그인
|
|
313
|
-
|
|
314
|
-
```python
|
|
315
|
-
class ImageProcessingUploader(BaseUploader):
|
|
316
|
-
"""TIFF 이미지를 JPEG로 변환하고 썸네일을 생성."""
|
|
317
|
-
|
|
318
|
-
def setup_directories(self):
|
|
319
|
-
"""처리된 이미지와 썸네일을 위한 디렉토리 생성."""
|
|
320
|
-
(self.path / 'processed').mkdir(exist_ok=True)
|
|
321
|
-
(self.path / 'thumbnails').mkdir(exist_ok=True)
|
|
322
|
-
|
|
323
|
-
def organize_files(self, files: List) -> List:
|
|
324
|
-
"""원본과 처리된 이미지 분리."""
|
|
325
|
-
raw_images = []
|
|
326
|
-
processed_images = []
|
|
327
|
-
|
|
328
|
-
for file_group in files:
|
|
329
|
-
has_raw = any(
|
|
330
|
-
f.suffix.lower() in ['.tif', '.tiff', '.raw']
|
|
331
|
-
for f in file_group.get('files', {}).values()
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
if has_raw:
|
|
335
|
-
raw_images.append(file_group)
|
|
336
|
-
else:
|
|
337
|
-
processed_images.append(file_group)
|
|
338
|
-
|
|
339
|
-
# 원본 이미지를 먼저 처리
|
|
340
|
-
return raw_images + processed_images
|
|
341
|
-
|
|
342
|
-
def process_files(self, organized_files: List) -> List:
|
|
343
|
-
"""이미지 변환 및 썸네일 생성."""
|
|
344
|
-
processed_files = []
|
|
345
|
-
|
|
346
|
-
for file_group in organized_files:
|
|
347
|
-
files_dict = file_group.get('files', {})
|
|
348
|
-
converted_files = {}
|
|
349
|
-
|
|
350
|
-
for spec_name, file_path in files_dict.items():
|
|
351
|
-
if file_path.suffix.lower() in ['.tif', '.tiff']:
|
|
352
|
-
# JPEG로 변환
|
|
353
|
-
jpeg_path = self.convert_to_jpeg(file_path)
|
|
354
|
-
converted_files[spec_name] = jpeg_path
|
|
355
|
-
|
|
356
|
-
# 썸네일 생성
|
|
357
|
-
thumbnail_path = self.generate_thumbnail(jpeg_path)
|
|
358
|
-
converted_files[f"{spec_name}_thumbnail"] = thumbnail_path
|
|
359
|
-
|
|
360
|
-
self.run.log_message(f"{file_path.name} -> {jpeg_path.name}로 처리했습니다")
|
|
361
|
-
else:
|
|
362
|
-
converted_files[spec_name] = file_path
|
|
363
|
-
|
|
364
|
-
file_group['files'] = converted_files
|
|
365
|
-
processed_files.append(file_group)
|
|
366
|
-
|
|
367
|
-
return processed_files
|
|
368
|
-
|
|
369
|
-
def convert_to_jpeg(self, tiff_path: Path) -> Path:
|
|
370
|
-
"""PIL을 사용하여 TIFF를 JPEG로 변환."""
|
|
371
|
-
from PIL import Image
|
|
372
|
-
|
|
373
|
-
output_path = self.path / 'processed' / f"{tiff_path.stem}.jpg"
|
|
374
|
-
|
|
375
|
-
with Image.open(tiff_path) as img:
|
|
376
|
-
# 필요시 RGB로 변환
|
|
377
|
-
if img.mode in ('RGBA', 'LA', 'P'):
|
|
378
|
-
img = img.convert('RGB')
|
|
379
|
-
|
|
380
|
-
img.save(output_path, 'JPEG', quality=95)
|
|
381
|
-
|
|
382
|
-
return output_path
|
|
383
|
-
|
|
384
|
-
def generate_thumbnail(self, image_path: Path) -> Path:
|
|
385
|
-
"""처리된 이미지의 썸네일 생성."""
|
|
386
|
-
from PIL import Image
|
|
387
|
-
|
|
388
|
-
thumbnail_path = self.path / 'thumbnails' / f"{image_path.stem}_thumb.jpg"
|
|
389
|
-
|
|
390
|
-
with Image.open(image_path) as img:
|
|
391
|
-
img.thumbnail((200, 200), Image.Resampling.LANCZOS)
|
|
392
|
-
img.save(thumbnail_path, 'JPEG', quality=85)
|
|
393
|
-
|
|
394
|
-
return thumbnail_path
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
### 예제 2: 데이터 검증 플러그인
|
|
398
|
-
|
|
399
|
-
```python
|
|
400
|
-
class DataValidationUploader(BaseUploader):
|
|
401
|
-
"""데이터 파일을 검증하고 품질 보고서를 생성."""
|
|
402
|
-
|
|
403
|
-
def __init__(self, run, path: Path, file_specification: List = None,
|
|
404
|
-
organized_files: List = None, extra_params: Dict = None):
|
|
405
|
-
super().__init__(run, path, file_specification, organized_files, extra_params)
|
|
406
|
-
|
|
407
|
-
# extra_params에서 검증 구성 초기화
|
|
408
|
-
self.validation_config = extra_params.get('validation_config', {})
|
|
409
|
-
self.strict_mode = extra_params.get('strict_validation', False)
|
|
410
|
-
|
|
411
|
-
def before_process(self, organized_files: List) -> List:
|
|
412
|
-
"""검증 엔진 초기화."""
|
|
413
|
-
self.validation_results = []
|
|
414
|
-
self.run.log_message(f"{len(organized_files)}개 파일 그룹 검증 시작")
|
|
415
|
-
return organized_files
|
|
416
|
-
|
|
417
|
-
def process_files(self, organized_files: List) -> List:
|
|
418
|
-
"""파일 검증 및 품질 보고서 생성."""
|
|
419
|
-
processed_files = []
|
|
420
|
-
|
|
421
|
-
for file_group in organized_files:
|
|
422
|
-
validation_result = self.validate_file_group(file_group)
|
|
423
|
-
|
|
424
|
-
# 검증 메타데이터 추가
|
|
425
|
-
file_group['validation'] = validation_result
|
|
426
|
-
file_group['quality_score'] = validation_result['score']
|
|
427
|
-
|
|
428
|
-
# 검증 결과에 따라 파일 그룹 포함
|
|
429
|
-
if self.should_include_file_group(validation_result):
|
|
430
|
-
processed_files.append(file_group)
|
|
431
|
-
self.run.log_message(f"파일 그룹 검증 통과: {validation_result['score']}")
|
|
432
|
-
else:
|
|
433
|
-
self.run.log_message(f"파일 그룹 검증 실패: {validation_result['errors']}")
|
|
434
|
-
|
|
435
|
-
return processed_files
|
|
436
|
-
|
|
437
|
-
def validate_file_group(self, file_group: Dict) -> Dict:
|
|
438
|
-
"""파일 그룹의 포괄적인 검증."""
|
|
439
|
-
files_dict = file_group.get('files', {})
|
|
440
|
-
errors = []
|
|
441
|
-
warnings = []
|
|
442
|
-
score = 100
|
|
443
|
-
|
|
444
|
-
for spec_name, file_path in files_dict.items():
|
|
445
|
-
# 파일 존재 및 접근 가능성
|
|
446
|
-
if not file_path.exists():
|
|
447
|
-
errors.append(f"파일을 찾을 수 없음: {file_path}")
|
|
448
|
-
score -= 50
|
|
449
|
-
continue
|
|
450
|
-
|
|
451
|
-
if not os.access(file_path, os.R_OK):
|
|
452
|
-
errors.append(f"파일을 읽을 수 없음: {file_path}")
|
|
453
|
-
score -= 30
|
|
454
|
-
continue
|
|
455
|
-
|
|
456
|
-
# 파일 크기 검증
|
|
457
|
-
file_size = file_path.stat().st_size
|
|
458
|
-
if file_size == 0:
|
|
459
|
-
errors.append(f"빈 파일: {file_path}")
|
|
460
|
-
score -= 40
|
|
461
|
-
elif file_size > 1024 * 1024 * 1024: # 1GB
|
|
462
|
-
warnings.append(f"큰 파일: {file_path} ({file_size} bytes)")
|
|
463
|
-
score -= 10
|
|
464
|
-
|
|
465
|
-
# 확장자에 따른 내용 검증
|
|
466
|
-
try:
|
|
467
|
-
if file_path.suffix.lower() == '.json':
|
|
468
|
-
self.validate_json_file(file_path)
|
|
469
|
-
elif file_path.suffix.lower() in ['.jpg', '.png']:
|
|
470
|
-
self.validate_image_file(file_path)
|
|
471
|
-
# 필요에 따라 더 많은 내용 검증 추가
|
|
472
|
-
except Exception as e:
|
|
473
|
-
errors.append(f"{file_path}의 내용 검증 실패: {str(e)}")
|
|
474
|
-
score -= 25
|
|
475
|
-
|
|
476
|
-
return {
|
|
477
|
-
'score': max(0, score),
|
|
478
|
-
'errors': errors,
|
|
479
|
-
'warnings': warnings,
|
|
480
|
-
'validated_at': datetime.now().isoformat()
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
def should_include_file_group(self, validation_result: Dict) -> bool:
|
|
484
|
-
"""검증 결과를 기반으로 파일 그룹 포함 여부 결정."""
|
|
485
|
-
if validation_result['errors'] and self.strict_mode:
|
|
486
|
-
return False
|
|
487
|
-
|
|
488
|
-
min_score = self.validation_config.get('min_score', 50)
|
|
489
|
-
return validation_result['score'] >= min_score
|
|
490
|
-
|
|
491
|
-
def validate_json_file(self, file_path: Path):
|
|
492
|
-
"""JSON 파일 구조 검증."""
|
|
493
|
-
import json
|
|
494
|
-
with open(file_path, 'r') as f:
|
|
495
|
-
json.load(f) # 유효하지 않은 JSON이면 예외 발생
|
|
496
|
-
|
|
497
|
-
def validate_image_file(self, file_path: Path):
|
|
498
|
-
"""이미지 파일 무결성 검증."""
|
|
499
|
-
from PIL import Image
|
|
500
|
-
with Image.open(file_path) as img:
|
|
501
|
-
img.verify() # 손상된 경우 예외 발생
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### 예제 3: 배치 처리 플러그인
|
|
505
|
-
|
|
506
|
-
```python
|
|
507
|
-
class BatchProcessingUploader(BaseUploader):
|
|
508
|
-
"""진행 상황 추적과 함께 구성 가능한 배치로 파일을 처리."""
|
|
509
|
-
|
|
510
|
-
def __init__(self, run, path: Path, file_specification: List = None,
|
|
511
|
-
organized_files: List = None, extra_params: Dict = None):
|
|
512
|
-
super().__init__(run, path, file_specification, organized_files, extra_params)
|
|
513
|
-
|
|
514
|
-
self.batch_size = extra_params.get('batch_size', 10)
|
|
515
|
-
self.parallel_processing = extra_params.get('use_parallel', True)
|
|
516
|
-
self.max_workers = extra_params.get('max_workers', 4)
|
|
517
|
-
|
|
518
|
-
def organize_files(self, files: List) -> List:
|
|
519
|
-
"""파일을 처리 배치로 구성."""
|
|
520
|
-
batches = []
|
|
521
|
-
current_batch = []
|
|
522
|
-
|
|
523
|
-
for file_group in files:
|
|
524
|
-
current_batch.append(file_group)
|
|
525
|
-
|
|
526
|
-
if len(current_batch) >= self.batch_size:
|
|
527
|
-
batches.append({
|
|
528
|
-
'batch_id': len(batches) + 1,
|
|
529
|
-
'files': current_batch,
|
|
530
|
-
'batch_size': len(current_batch)
|
|
531
|
-
})
|
|
532
|
-
current_batch = []
|
|
533
|
-
|
|
534
|
-
# 남은 파일을 최종 배치로 추가
|
|
535
|
-
if current_batch:
|
|
536
|
-
batches.append({
|
|
537
|
-
'batch_id': len(batches) + 1,
|
|
538
|
-
'files': current_batch,
|
|
539
|
-
'batch_size': len(current_batch)
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
self.run.log_message(f"{len(files)}개 파일을 {len(batches)}개 배치로 구성했습니다")
|
|
543
|
-
return batches
|
|
544
|
-
|
|
545
|
-
def process_files(self, organized_files: List) -> List:
|
|
546
|
-
"""진행 상황 추적과 함께 배치로 파일 처리."""
|
|
547
|
-
all_processed_files = []
|
|
548
|
-
total_batches = len(organized_files)
|
|
549
|
-
|
|
550
|
-
if self.parallel_processing:
|
|
551
|
-
all_processed_files = self.process_batches_parallel(organized_files)
|
|
552
|
-
else:
|
|
553
|
-
all_processed_files = self.process_batches_sequential(organized_files)
|
|
554
|
-
|
|
555
|
-
self.run.log_message(f"{total_batches}개 배치 처리 완료")
|
|
556
|
-
return all_processed_files
|
|
557
|
-
|
|
558
|
-
def process_batches_sequential(self, batches: List) -> List:
|
|
559
|
-
"""배치를 순차적으로 처리."""
|
|
560
|
-
all_files = []
|
|
561
|
-
|
|
562
|
-
for i, batch in enumerate(batches, 1):
|
|
563
|
-
self.run.log_message(f"배치 {i}/{len(batches)} 처리 중")
|
|
564
|
-
|
|
565
|
-
processed_batch = self.process_single_batch(batch)
|
|
566
|
-
all_files.extend(processed_batch)
|
|
567
|
-
|
|
568
|
-
# 진행 상황 업데이트
|
|
569
|
-
progress = (i / len(batches)) * 100
|
|
570
|
-
self.run.log_message(f"진행 상황: {progress:.1f}% 완료")
|
|
571
|
-
|
|
572
|
-
return all_files
|
|
573
|
-
|
|
574
|
-
def process_batches_parallel(self, batches: List) -> List:
|
|
575
|
-
"""ThreadPoolExecutor를 사용하여 배치를 병렬로 처리."""
|
|
576
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
577
|
-
|
|
578
|
-
all_files = []
|
|
579
|
-
completed_batches = 0
|
|
580
|
-
|
|
581
|
-
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
582
|
-
# 모든 배치 제출
|
|
583
|
-
future_to_batch = {
|
|
584
|
-
executor.submit(self.process_single_batch, batch): batch
|
|
585
|
-
for batch in batches
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
# 완료된 배치 처리
|
|
589
|
-
for future in as_completed(future_to_batch):
|
|
590
|
-
batch = future_to_batch[future]
|
|
591
|
-
try:
|
|
592
|
-
processed_files = future.result()
|
|
593
|
-
all_files.extend(processed_files)
|
|
594
|
-
completed_batches += 1
|
|
595
|
-
|
|
596
|
-
progress = (completed_batches / len(batches)) * 100
|
|
597
|
-
self.run.log_message(f"배치 {batch['batch_id']} 완료. 진행 상황: {progress:.1f}%")
|
|
598
|
-
|
|
599
|
-
except Exception as e:
|
|
600
|
-
self.run.log_message(f"배치 {batch['batch_id']} 실패: {str(e)}")
|
|
601
|
-
|
|
602
|
-
return all_files
|
|
603
|
-
|
|
604
|
-
def process_single_batch(self, batch: Dict) -> List:
|
|
605
|
-
"""단일 파일 배치 처리."""
|
|
606
|
-
batch_files = batch['files']
|
|
607
|
-
processed_files = []
|
|
608
|
-
|
|
609
|
-
for file_group in batch_files:
|
|
610
|
-
# 여기에 특정 처리 로직 적용
|
|
611
|
-
processed_file = self.process_file_group(file_group)
|
|
612
|
-
processed_files.append(processed_file)
|
|
613
|
-
|
|
614
|
-
return processed_files
|
|
615
|
-
|
|
616
|
-
def process_file_group(self, file_group: Dict) -> Dict:
|
|
617
|
-
"""개별 파일 그룹 처리 - 여기에 로직을 구현하세요."""
|
|
618
|
-
# 예제: 배치 처리 메타데이터 추가
|
|
619
|
-
file_group['batch_processed'] = True
|
|
620
|
-
file_group['processed_timestamp'] = datetime.now().isoformat()
|
|
621
|
-
return file_group
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
## 오류 처리 및 로깅
|
|
625
|
-
|
|
626
|
-
### 포괄적인 오류 처리
|
|
627
|
-
|
|
628
|
-
```python
|
|
629
|
-
class RobustUploader(BaseUploader):
|
|
630
|
-
def process_files(self, organized_files: List) -> List:
|
|
631
|
-
"""포괄적인 오류 처리로 파일 처리."""
|
|
632
|
-
processed_files = []
|
|
633
|
-
failed_files = []
|
|
634
|
-
|
|
635
|
-
for i, file_group in enumerate(organized_files):
|
|
636
|
-
try:
|
|
637
|
-
self.run.log_message(f"파일 그룹 {i+1}/{len(organized_files)} 처리 중")
|
|
638
|
-
|
|
639
|
-
# 검증과 함께 처리
|
|
640
|
-
processed_file = self.process_file_group_safely(file_group)
|
|
641
|
-
processed_files.append(processed_file)
|
|
642
|
-
|
|
643
|
-
except Exception as e:
|
|
644
|
-
error_info = {
|
|
645
|
-
'file_group': file_group,
|
|
646
|
-
'error': str(e),
|
|
647
|
-
'error_type': type(e).__name__,
|
|
648
|
-
'timestamp': datetime.now().isoformat()
|
|
649
|
-
}
|
|
650
|
-
failed_files.append(error_info)
|
|
651
|
-
|
|
652
|
-
self.run.log_message(f"파일 그룹 처리 실패: {str(e)}")
|
|
653
|
-
|
|
654
|
-
# 다른 파일 처리 계속
|
|
655
|
-
continue
|
|
656
|
-
|
|
657
|
-
# 요약 로그
|
|
658
|
-
self.run.log_message(
|
|
659
|
-
f"처리 완료: {len(processed_files)}개 성공, {len(failed_files)}개 실패"
|
|
660
|
-
)
|
|
661
|
-
|
|
662
|
-
if failed_files:
|
|
663
|
-
# 오류 보고서 저장
|
|
664
|
-
self.save_error_report(failed_files)
|
|
665
|
-
|
|
666
|
-
return processed_files
|
|
667
|
-
|
|
668
|
-
def process_file_group_safely(self, file_group: Dict) -> Dict:
|
|
669
|
-
"""검증 및 오류 검사와 함께 파일 그룹 처리."""
|
|
670
|
-
# 파일 그룹 구조 검증
|
|
671
|
-
if 'files' not in file_group:
|
|
672
|
-
raise ValueError("파일 그룹에 'files' 키가 없습니다")
|
|
673
|
-
|
|
674
|
-
files_dict = file_group['files']
|
|
675
|
-
if not files_dict:
|
|
676
|
-
raise ValueError("파일 그룹에 파일이 없습니다")
|
|
677
|
-
|
|
678
|
-
# 파일 접근 가능성 검증
|
|
679
|
-
for spec_name, file_path in files_dict.items():
|
|
680
|
-
if not file_path.exists():
|
|
681
|
-
raise FileNotFoundError(f"파일을 찾을 수 없음: {file_path}")
|
|
682
|
-
|
|
683
|
-
if not os.access(file_path, os.R_OK):
|
|
684
|
-
raise PermissionError(f"파일을 읽을 수 없음: {file_path}")
|
|
685
|
-
|
|
686
|
-
# 실제 처리 수행
|
|
687
|
-
return self.apply_processing_logic(file_group)
|
|
688
|
-
|
|
689
|
-
def save_error_report(self, failed_files: List):
|
|
690
|
-
"""디버깅을 위한 상세 오류 보고서 저장."""
|
|
691
|
-
error_report_path = self.path / 'error_report.json'
|
|
692
|
-
|
|
693
|
-
report = {
|
|
694
|
-
'timestamp': datetime.now().isoformat(),
|
|
695
|
-
'plugin_name': self.__class__.__name__,
|
|
696
|
-
'total_errors': len(failed_files),
|
|
697
|
-
'errors': failed_files
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
with open(error_report_path, 'w') as f:
|
|
701
|
-
json.dump(report, f, indent=2, default=str)
|
|
702
|
-
|
|
703
|
-
self.run.log_message(f"오류 보고서 저장됨: {error_report_path}")
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
### 구조화된 로깅
|
|
707
|
-
|
|
708
|
-
```python
|
|
709
|
-
class LoggingUploader(BaseUploader):
|
|
710
|
-
def setup_directories(self):
|
|
711
|
-
"""로깅 디렉토리 설정."""
|
|
712
|
-
log_dir = self.path / 'logs'
|
|
713
|
-
log_dir.mkdir(exist_ok=True)
|
|
714
|
-
|
|
715
|
-
# 구조화된 로깅 초기화
|
|
716
|
-
self.setup_structured_logging(log_dir)
|
|
717
|
-
|
|
718
|
-
def setup_structured_logging(self, log_dir: Path):
|
|
719
|
-
"""다양한 레벨의 구조화된 로깅 설정."""
|
|
720
|
-
import logging
|
|
721
|
-
import json
|
|
722
|
-
|
|
723
|
-
# 구조화된 로그를 위한 커스텀 포매터 생성
|
|
724
|
-
class StructuredFormatter(logging.Formatter):
|
|
725
|
-
def format(self, record):
|
|
726
|
-
log_entry = {
|
|
727
|
-
'timestamp': datetime.now().isoformat(),
|
|
728
|
-
'level': record.levelname,
|
|
729
|
-
'message': record.getMessage(),
|
|
730
|
-
'plugin': 'LoggingUploader'
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
# 추가 필드가 있으면 추가
|
|
734
|
-
if hasattr(record, 'file_path'):
|
|
735
|
-
log_entry['file_path'] = str(record.file_path)
|
|
736
|
-
if hasattr(record, 'operation'):
|
|
737
|
-
log_entry['operation'] = record.operation
|
|
738
|
-
if hasattr(record, 'duration'):
|
|
739
|
-
log_entry['duration'] = record.duration
|
|
740
|
-
|
|
741
|
-
return json.dumps(log_entry)
|
|
742
|
-
|
|
743
|
-
# 로거 설정
|
|
744
|
-
self.logger = logging.getLogger('upload_plugin')
|
|
745
|
-
self.logger.setLevel(logging.INFO)
|
|
746
|
-
|
|
747
|
-
# 파일 핸들러
|
|
748
|
-
handler = logging.FileHandler(log_dir / 'plugin.log')
|
|
749
|
-
handler.setFormatter(StructuredFormatter())
|
|
750
|
-
self.logger.addHandler(handler)
|
|
751
|
-
|
|
752
|
-
def process_files(self, organized_files: List) -> List:
|
|
753
|
-
"""상세 로깅과 함께 파일 처리."""
|
|
754
|
-
start_time = time.time()
|
|
755
|
-
|
|
756
|
-
self.logger.info(
|
|
757
|
-
f"파일 처리 시작",
|
|
758
|
-
extra={'operation': 'process_files', 'file_count': len(organized_files)}
|
|
759
|
-
)
|
|
760
|
-
|
|
761
|
-
processed_files = []
|
|
762
|
-
|
|
763
|
-
for i, file_group in enumerate(organized_files):
|
|
764
|
-
file_start_time = time.time()
|
|
765
|
-
|
|
766
|
-
try:
|
|
767
|
-
# 파일 그룹 처리
|
|
768
|
-
processed_file = self.process_file_group(file_group)
|
|
769
|
-
processed_files.append(processed_file)
|
|
770
|
-
|
|
771
|
-
# 성공 로그
|
|
772
|
-
duration = time.time() - file_start_time
|
|
773
|
-
self.logger.info(
|
|
774
|
-
f"파일 그룹 {i+1} 처리 성공",
|
|
775
|
-
extra={
|
|
776
|
-
'operation': 'process_file_group',
|
|
777
|
-
'file_group_index': i,
|
|
778
|
-
'duration': duration
|
|
779
|
-
}
|
|
780
|
-
)
|
|
781
|
-
|
|
782
|
-
except Exception as e:
|
|
783
|
-
# 오류 로그
|
|
784
|
-
duration = time.time() - file_start_time
|
|
785
|
-
self.logger.error(
|
|
786
|
-
f"파일 그룹 {i+1} 처리 실패: {str(e)}",
|
|
787
|
-
extra={
|
|
788
|
-
'operation': 'process_file_group',
|
|
789
|
-
'file_group_index': i,
|
|
790
|
-
'duration': duration,
|
|
791
|
-
'error': str(e)
|
|
792
|
-
}
|
|
793
|
-
)
|
|
794
|
-
raise
|
|
795
|
-
|
|
796
|
-
# 전체 완료 로그
|
|
797
|
-
total_duration = time.time() - start_time
|
|
798
|
-
self.logger.info(
|
|
799
|
-
f"파일 처리 완료",
|
|
800
|
-
extra={
|
|
801
|
-
'operation': 'process_files',
|
|
802
|
-
'total_duration': total_duration,
|
|
803
|
-
'processed_count': len(processed_files)
|
|
804
|
-
}
|
|
805
|
-
)
|
|
806
|
-
|
|
807
|
-
return processed_files
|
|
808
|
-
```
|
|
809
|
-
|
|
810
|
-
## 성능 최적화
|
|
811
|
-
|
|
812
|
-
### 메모리 관리
|
|
813
|
-
|
|
814
|
-
```python
|
|
815
|
-
class MemoryEfficientUploader(BaseUploader):
|
|
816
|
-
"""대용량 파일 처리에 최적화된 업로더."""
|
|
817
|
-
|
|
818
|
-
def __init__(self, run, path: Path, file_specification: List = None,
|
|
819
|
-
organized_files: List = None, extra_params: Dict = None):
|
|
820
|
-
super().__init__(run, path, file_specification, organized_files, extra_params)
|
|
821
|
-
|
|
822
|
-
self.chunk_size = extra_params.get('chunk_size', 8192) # 8KB 청크
|
|
823
|
-
self.memory_limit = extra_params.get('memory_limit_mb', 100) * 1024 * 1024
|
|
824
|
-
|
|
825
|
-
def process_files(self, organized_files: List) -> List:
|
|
826
|
-
"""메모리 관리와 함께 파일 처리."""
|
|
827
|
-
import psutil
|
|
828
|
-
import gc
|
|
829
|
-
|
|
830
|
-
processed_files = []
|
|
831
|
-
|
|
832
|
-
for file_group in organized_files:
|
|
833
|
-
# 처리 전 메모리 사용량 확인
|
|
834
|
-
memory_usage = psutil.Process().memory_info().rss
|
|
835
|
-
|
|
836
|
-
if memory_usage > self.memory_limit:
|
|
837
|
-
self.run.log_message(f"높은 메모리 사용량: {memory_usage / 1024 / 1024:.1f}MB")
|
|
838
|
-
|
|
839
|
-
# 가비지 컬렉션 강제 실행
|
|
840
|
-
gc.collect()
|
|
841
|
-
|
|
842
|
-
# 정리 후 다시 확인
|
|
843
|
-
memory_usage = psutil.Process().memory_info().rss
|
|
844
|
-
if memory_usage > self.memory_limit:
|
|
845
|
-
self.run.log_message("메모리 한계 초과, 더 작은 청크로 처리합니다")
|
|
846
|
-
processed_file = self.process_file_group_chunked(file_group)
|
|
847
|
-
else:
|
|
848
|
-
processed_file = self.process_file_group_normal(file_group)
|
|
849
|
-
else:
|
|
850
|
-
processed_file = self.process_file_group_normal(file_group)
|
|
851
|
-
|
|
852
|
-
processed_files.append(processed_file)
|
|
853
|
-
|
|
854
|
-
return processed_files
|
|
855
|
-
|
|
856
|
-
def process_file_group_chunked(self, file_group: Dict) -> Dict:
|
|
857
|
-
"""메모리 관리를 위해 큰 파일을 청크로 처리."""
|
|
858
|
-
files_dict = file_group.get('files', {})
|
|
859
|
-
processed_files = {}
|
|
860
|
-
|
|
861
|
-
for spec_name, file_path in files_dict.items():
|
|
862
|
-
if file_path.stat().st_size > 50 * 1024 * 1024: # 50MB
|
|
863
|
-
# 큰 파일을 청크로 처리
|
|
864
|
-
processed_path = self.process_large_file_chunked(file_path)
|
|
865
|
-
processed_files[spec_name] = processed_path
|
|
866
|
-
else:
|
|
867
|
-
# 작은 파일은 일반적으로 처리
|
|
868
|
-
processed_files[spec_name] = file_path
|
|
869
|
-
|
|
870
|
-
file_group['files'] = processed_files
|
|
871
|
-
return file_group
|
|
872
|
-
|
|
873
|
-
def process_large_file_chunked(self, file_path: Path) -> Path:
|
|
874
|
-
"""큰 파일을 청크로 처리."""
|
|
875
|
-
output_path = self.path / 'processed' / file_path.name
|
|
876
|
-
|
|
877
|
-
with open(file_path, 'rb') as infile, open(output_path, 'wb') as outfile:
|
|
878
|
-
while True:
|
|
879
|
-
chunk = infile.read(self.chunk_size)
|
|
880
|
-
if not chunk:
|
|
881
|
-
break
|
|
882
|
-
|
|
883
|
-
# 청크에 처리 적용
|
|
884
|
-
processed_chunk = self.process_chunk(chunk)
|
|
885
|
-
outfile.write(processed_chunk)
|
|
886
|
-
|
|
887
|
-
return output_path
|
|
888
|
-
|
|
889
|
-
def process_chunk(self, chunk: bytes) -> bytes:
|
|
890
|
-
"""개별 청크 처리 - 로직으로 오버라이드하세요."""
|
|
891
|
-
# 예제: 단순 통과
|
|
892
|
-
return chunk
|
|
893
|
-
```
|
|
894
|
-
|
|
895
|
-
### 비동기 처리
|
|
896
|
-
|
|
897
|
-
```python
|
|
898
|
-
import asyncio
|
|
899
|
-
from concurrent.futures import ProcessPoolExecutor
|
|
900
|
-
|
|
901
|
-
class AsyncUploader(BaseUploader):
|
|
902
|
-
"""비동기 처리 기능을 갖춘 업로더."""
|
|
903
|
-
|
|
904
|
-
def __init__(self, run, path: Path, file_specification: List = None,
|
|
905
|
-
organized_files: List = None, extra_params: Dict = None):
|
|
906
|
-
super().__init__(run, path, file_specification, organized_files, extra_params)
|
|
907
|
-
|
|
908
|
-
self.max_concurrent = extra_params.get('max_concurrent', 5)
|
|
909
|
-
self.use_process_pool = extra_params.get('use_process_pool', False)
|
|
910
|
-
|
|
911
|
-
def process_files(self, organized_files: List) -> List:
|
|
912
|
-
"""비동기적으로 파일 처리."""
|
|
913
|
-
# 동기 컨텍스트에서 비동기 처리 실행
|
|
914
|
-
return asyncio.run(self._process_files_async(organized_files))
|
|
915
|
-
|
|
916
|
-
async def _process_files_async(self, organized_files: List) -> List:
|
|
917
|
-
"""주요 비동기 처리 메소드."""
|
|
918
|
-
if self.use_process_pool:
|
|
919
|
-
return await self._process_with_process_pool(organized_files)
|
|
920
|
-
else:
|
|
921
|
-
return await self._process_with_async_tasks(organized_files)
|
|
922
|
-
|
|
923
|
-
async def _process_with_async_tasks(self, organized_files: List) -> List:
|
|
924
|
-
"""동시성 제한과 함께 비동기 작업을 사용하여 처리."""
|
|
925
|
-
semaphore = asyncio.Semaphore(self.max_concurrent)
|
|
926
|
-
|
|
927
|
-
async def process_with_semaphore(file_group):
|
|
928
|
-
async with semaphore:
|
|
929
|
-
return await self._process_file_group_async(file_group)
|
|
930
|
-
|
|
931
|
-
# 모든 파일 그룹에 대한 작업 생성
|
|
932
|
-
tasks = [
|
|
933
|
-
process_with_semaphore(file_group)
|
|
934
|
-
for file_group in organized_files
|
|
935
|
-
]
|
|
936
|
-
|
|
937
|
-
# 모든 작업 완료 대기
|
|
938
|
-
processed_files = await asyncio.gather(*tasks, return_exceptions=True)
|
|
939
|
-
|
|
940
|
-
# 예외 필터링 및 오류 로그
|
|
941
|
-
valid_files = []
|
|
942
|
-
for i, result in enumerate(processed_files):
|
|
943
|
-
if isinstance(result, Exception):
|
|
944
|
-
self.run.log_message(f"파일 그룹 {i} 처리 오류: {str(result)}")
|
|
945
|
-
else:
|
|
946
|
-
valid_files.append(result)
|
|
947
|
-
|
|
948
|
-
return valid_files
|
|
949
|
-
|
|
950
|
-
async def _process_with_process_pool(self, organized_files: List) -> List:
|
|
951
|
-
"""CPU 집약적 작업을 위해 프로세스 풀 사용."""
|
|
952
|
-
loop = asyncio.get_event_loop()
|
|
953
|
-
|
|
954
|
-
with ProcessPoolExecutor(max_workers=self.max_concurrent) as executor:
|
|
955
|
-
# 모든 작업을 프로세스 풀에 제출
|
|
956
|
-
futures = [
|
|
957
|
-
loop.run_in_executor(executor, self._process_file_group_sync, file_group)
|
|
958
|
-
for file_group in organized_files
|
|
959
|
-
]
|
|
960
|
-
|
|
961
|
-
# 완료 대기
|
|
962
|
-
processed_files = await asyncio.gather(*futures, return_exceptions=True)
|
|
963
|
-
|
|
964
|
-
# 예외 필터링
|
|
965
|
-
valid_files = []
|
|
966
|
-
for i, result in enumerate(processed_files):
|
|
967
|
-
if isinstance(result, Exception):
|
|
968
|
-
self.run.log_message(f"파일 그룹 {i}의 프로세스 풀 오류: {str(result)}")
|
|
969
|
-
else:
|
|
970
|
-
valid_files.append(result)
|
|
971
|
-
|
|
972
|
-
return valid_files
|
|
973
|
-
|
|
974
|
-
async def _process_file_group_async(self, file_group: Dict) -> Dict:
|
|
975
|
-
"""개별 파일 그룹의 비동기 처리."""
|
|
976
|
-
# 비동기 I/O 작업 시뮬레이션
|
|
977
|
-
await asyncio.sleep(0.1)
|
|
978
|
-
|
|
979
|
-
# 여기에 처리 로직 적용
|
|
980
|
-
file_group['async_processed'] = True
|
|
981
|
-
file_group['processed_timestamp'] = datetime.now().isoformat()
|
|
982
|
-
|
|
983
|
-
return file_group
|
|
984
|
-
|
|
985
|
-
def _process_file_group_sync(self, file_group: Dict) -> Dict:
|
|
986
|
-
"""프로세스 풀을 위한 동기 처리."""
|
|
987
|
-
# 별도 프로세스에서 실행됨
|
|
988
|
-
import time
|
|
989
|
-
time.sleep(0.1) # CPU 작업 시뮬레이션
|
|
990
|
-
|
|
991
|
-
file_group['process_pool_processed'] = True
|
|
992
|
-
file_group['processed_timestamp'] = datetime.now().isoformat()
|
|
993
|
-
|
|
994
|
-
return file_group
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
## 테스트 및 디버깅
|
|
998
|
-
|
|
999
|
-
### 단위 테스트 프레임워크
|
|
1000
|
-
|
|
1001
|
-
```python
|
|
1002
|
-
import unittest
|
|
1003
|
-
from unittest.mock import Mock, patch, MagicMock
|
|
1004
|
-
from pathlib import Path
|
|
1005
|
-
import tempfile
|
|
1006
|
-
import shutil
|
|
1007
|
-
|
|
1008
|
-
class TestMyUploader(unittest.TestCase):
|
|
1009
|
-
"""커스텀 업로더를 위한 테스트 스위트."""
|
|
1010
|
-
|
|
1011
|
-
def setUp(self):
|
|
1012
|
-
"""테스트 환경 설정."""
|
|
1013
|
-
# 임시 디렉토리 생성
|
|
1014
|
-
self.temp_dir = Path(tempfile.mkdtemp())
|
|
1015
|
-
|
|
1016
|
-
# run 객체 모킹
|
|
1017
|
-
self.mock_run = Mock()
|
|
1018
|
-
self.mock_run.log_message = Mock()
|
|
1019
|
-
|
|
1020
|
-
# 샘플 파일 사양
|
|
1021
|
-
self.file_specification = [
|
|
1022
|
-
{'name': 'image_data', 'file_type': 'image'},
|
|
1023
|
-
{'name': 'text_data', 'file_type': 'text'}
|
|
1024
|
-
]
|
|
1025
|
-
|
|
1026
|
-
# 테스트 파일 생성
|
|
1027
|
-
self.test_files = self.create_test_files()
|
|
1028
|
-
|
|
1029
|
-
# 샘플 조직화된 파일
|
|
1030
|
-
self.organized_files = [
|
|
1031
|
-
{
|
|
1032
|
-
'files': {
|
|
1033
|
-
'image_data': self.test_files['image'],
|
|
1034
|
-
'text_data': self.test_files['text']
|
|
1035
|
-
},
|
|
1036
|
-
'metadata': {'group_id': 1}
|
|
1037
|
-
}
|
|
1038
|
-
]
|
|
1039
|
-
|
|
1040
|
-
def tearDown(self):
|
|
1041
|
-
"""테스트 환경 정리."""
|
|
1042
|
-
shutil.rmtree(self.temp_dir)
|
|
1043
|
-
|
|
1044
|
-
def create_test_files(self) -> Dict[str, Path]:
|
|
1045
|
-
"""테스트를 위한 테스트 파일 생성."""
|
|
1046
|
-
files = {}
|
|
1047
|
-
|
|
1048
|
-
# 테스트 이미지 파일 생성
|
|
1049
|
-
image_file = self.temp_dir / 'test_image.jpg'
|
|
1050
|
-
with open(image_file, 'wb') as f:
|
|
1051
|
-
f.write(b'fake_image_data')
|
|
1052
|
-
files['image'] = image_file
|
|
1053
|
-
|
|
1054
|
-
# 테스트 텍스트 파일 생성
|
|
1055
|
-
text_file = self.temp_dir / 'test_text.txt'
|
|
1056
|
-
with open(text_file, 'w') as f:
|
|
1057
|
-
f.write('test content')
|
|
1058
|
-
files['text'] = text_file
|
|
1059
|
-
|
|
1060
|
-
return files
|
|
1061
|
-
|
|
1062
|
-
def test_initialization(self):
|
|
1063
|
-
"""업로더 초기화 테스트."""
|
|
1064
|
-
uploader = MyUploader(
|
|
1065
|
-
run=self.mock_run,
|
|
1066
|
-
path=self.temp_dir,
|
|
1067
|
-
file_specification=self.file_specification,
|
|
1068
|
-
organized_files=self.organized_files
|
|
1069
|
-
)
|
|
1070
|
-
|
|
1071
|
-
self.assertEqual(uploader.path, self.temp_dir)
|
|
1072
|
-
self.assertEqual(uploader.file_specification, self.file_specification)
|
|
1073
|
-
self.assertEqual(uploader.organized_files, self.organized_files)
|
|
1074
|
-
|
|
1075
|
-
def test_process_files(self):
|
|
1076
|
-
"""process_files 메소드 테스트."""
|
|
1077
|
-
uploader = MyUploader(
|
|
1078
|
-
run=self.mock_run,
|
|
1079
|
-
path=self.temp_dir,
|
|
1080
|
-
file_specification=self.file_specification,
|
|
1081
|
-
organized_files=self.organized_files
|
|
1082
|
-
)
|
|
1083
|
-
|
|
1084
|
-
result = uploader.process_files(self.organized_files)
|
|
1085
|
-
|
|
1086
|
-
# 결과 구조 검증
|
|
1087
|
-
self.assertIsInstance(result, list)
|
|
1088
|
-
self.assertEqual(len(result), 1)
|
|
1089
|
-
|
|
1090
|
-
# 처리가 발생했는지 검증
|
|
1091
|
-
processed_file = result[0]
|
|
1092
|
-
self.assertIn('processed_by', processed_file)
|
|
1093
|
-
self.assertEqual(processed_file['processed_by'], 'MyUploader')
|
|
1094
|
-
|
|
1095
|
-
def test_handle_upload_files_workflow(self):
|
|
1096
|
-
"""전체 워크플로우 테스트."""
|
|
1097
|
-
uploader = MyUploader(
|
|
1098
|
-
run=self.mock_run,
|
|
1099
|
-
path=self.temp_dir,
|
|
1100
|
-
file_specification=self.file_specification,
|
|
1101
|
-
organized_files=self.organized_files
|
|
1102
|
-
)
|
|
1103
|
-
|
|
1104
|
-
# 워크플로우 메소드 모킹
|
|
1105
|
-
with patch.object(uploader, 'setup_directories') as mock_setup, \
|
|
1106
|
-
patch.object(uploader, 'organize_files', return_value=self.organized_files) as mock_organize, \
|
|
1107
|
-
patch.object(uploader, 'before_process', return_value=self.organized_files) as mock_before, \
|
|
1108
|
-
patch.object(uploader, 'process_files', return_value=self.organized_files) as mock_process, \
|
|
1109
|
-
patch.object(uploader, 'after_process', return_value=self.organized_files) as mock_after, \
|
|
1110
|
-
patch.object(uploader, 'validate_files', return_value=self.organized_files) as mock_validate:
|
|
1111
|
-
|
|
1112
|
-
result = uploader.handle_upload_files()
|
|
1113
|
-
|
|
1114
|
-
# 모든 메소드가 올바른 순서로 호출되었는지 검증
|
|
1115
|
-
mock_setup.assert_called_once()
|
|
1116
|
-
mock_organize.assert_called_once()
|
|
1117
|
-
mock_before.assert_called_once()
|
|
1118
|
-
mock_process.assert_called_once()
|
|
1119
|
-
mock_after.assert_called_once()
|
|
1120
|
-
mock_validate.assert_called_once()
|
|
1121
|
-
|
|
1122
|
-
self.assertEqual(result, self.organized_files)
|
|
1123
|
-
|
|
1124
|
-
def test_error_handling(self):
|
|
1125
|
-
"""process_files의 오류 처리 테스트."""
|
|
1126
|
-
uploader = MyUploader(
|
|
1127
|
-
run=self.mock_run,
|
|
1128
|
-
path=self.temp_dir,
|
|
1129
|
-
file_specification=self.file_specification,
|
|
1130
|
-
organized_files=self.organized_files
|
|
1131
|
-
)
|
|
1132
|
-
|
|
1133
|
-
# 유효하지 않은 파일 그룹으로 테스트
|
|
1134
|
-
invalid_files = [{'invalid': 'structure'}]
|
|
1135
|
-
|
|
1136
|
-
with self.assertRaises(Exception):
|
|
1137
|
-
uploader.process_files(invalid_files)
|
|
1138
|
-
|
|
1139
|
-
@patch('your_module.some_external_dependency')
|
|
1140
|
-
def test_external_dependencies(self, mock_dependency):
|
|
1141
|
-
"""외부 의존성과의 통합 테스트."""
|
|
1142
|
-
mock_dependency.return_value = 'mocked_result'
|
|
1143
|
-
|
|
1144
|
-
uploader = MyUploader(
|
|
1145
|
-
run=self.mock_run,
|
|
1146
|
-
path=self.temp_dir,
|
|
1147
|
-
file_specification=self.file_specification,
|
|
1148
|
-
organized_files=self.organized_files
|
|
1149
|
-
)
|
|
1150
|
-
|
|
1151
|
-
# 외부 의존성을 사용하는 메소드 테스트
|
|
1152
|
-
result = uploader.some_method_using_dependency()
|
|
1153
|
-
|
|
1154
|
-
mock_dependency.assert_called_once()
|
|
1155
|
-
self.assertEqual(result, 'expected_result_based_on_mock')
|
|
1156
|
-
|
|
1157
|
-
if __name__ == '__main__':
|
|
1158
|
-
# 특정 테스트 실행
|
|
1159
|
-
unittest.main()
|
|
1160
|
-
```
|
|
1161
|
-
|
|
1162
|
-
### 통합 테스트
|
|
1163
|
-
|
|
1164
|
-
```python
|
|
1165
|
-
class TestUploaderIntegration(unittest.TestCase):
|
|
1166
|
-
"""실제 파일 작업을 포함한 업로더 통합 테스트."""
|
|
1167
|
-
|
|
1168
|
-
def setUp(self):
|
|
1169
|
-
"""통합 테스트 환경 설정."""
|
|
1170
|
-
self.temp_dir = Path(tempfile.mkdtemp())
|
|
1171
|
-
self.mock_run = Mock()
|
|
1172
|
-
|
|
1173
|
-
# 현실적인 테스트 파일 생성
|
|
1174
|
-
self.create_realistic_test_files()
|
|
1175
|
-
|
|
1176
|
-
def create_realistic_test_files(self):
|
|
1177
|
-
"""통합 테스트를 위한 현실적인 테스트 파일 생성."""
|
|
1178
|
-
# 다양한 파일 타입 생성
|
|
1179
|
-
(self.temp_dir / 'images').mkdir()
|
|
1180
|
-
(self.temp_dir / 'data').mkdir()
|
|
1181
|
-
|
|
1182
|
-
# 실제로 처리할 수 있는 TIFF 이미지
|
|
1183
|
-
tiff_path = self.temp_dir / 'images' / 'test.tif'
|
|
1184
|
-
# 최소한의 유효한 TIFF 파일 생성
|
|
1185
|
-
self.create_minimal_tiff(tiff_path)
|
|
1186
|
-
|
|
1187
|
-
# JSON 데이터 파일
|
|
1188
|
-
json_path = self.temp_dir / 'data' / 'test.json'
|
|
1189
|
-
with open(json_path, 'w') as f:
|
|
1190
|
-
json.dump({'test': 'data', 'values': [1, 2, 3]}, f)
|
|
1191
|
-
|
|
1192
|
-
self.test_files = {
|
|
1193
|
-
'image_file': tiff_path,
|
|
1194
|
-
'data_file': json_path
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
def create_minimal_tiff(self, path: Path):
|
|
1198
|
-
"""테스트를 위한 최소한의 유효한 TIFF 파일 생성."""
|
|
1199
|
-
try:
|
|
1200
|
-
from PIL import Image
|
|
1201
|
-
import numpy as np
|
|
1202
|
-
|
|
1203
|
-
# 작은 테스트 이미지 생성
|
|
1204
|
-
array = np.zeros((50, 50, 3), dtype=np.uint8)
|
|
1205
|
-
array[10:40, 10:40] = [255, 0, 0] # 빨간 사각형
|
|
1206
|
-
|
|
1207
|
-
image = Image.fromarray(array)
|
|
1208
|
-
image.save(path, 'TIFF')
|
|
1209
|
-
except ImportError:
|
|
1210
|
-
# 대안: PIL을 사용할 수 없으면 빈 파일 생성
|
|
1211
|
-
path.touch()
|
|
1212
|
-
|
|
1213
|
-
def test_full_workflow_with_real_files(self):
|
|
1214
|
-
"""실제 파일 작업과 함께 전체 워크플로우 테스트."""
|
|
1215
|
-
file_specification = [
|
|
1216
|
-
{'name': 'test_image', 'file_type': 'image'},
|
|
1217
|
-
{'name': 'test_data', 'file_type': 'data'}
|
|
1218
|
-
]
|
|
1219
|
-
|
|
1220
|
-
organized_files = [
|
|
1221
|
-
{
|
|
1222
|
-
'files': {
|
|
1223
|
-
'test_image': self.test_files['image_file'],
|
|
1224
|
-
'test_data': self.test_files['data_file']
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
]
|
|
1228
|
-
|
|
1229
|
-
uploader = ImageProcessingUploader(
|
|
1230
|
-
run=self.mock_run,
|
|
1231
|
-
path=self.temp_dir,
|
|
1232
|
-
file_specification=file_specification,
|
|
1233
|
-
organized_files=organized_files
|
|
1234
|
-
)
|
|
1235
|
-
|
|
1236
|
-
# 전체 워크플로우 실행
|
|
1237
|
-
result = uploader.handle_upload_files()
|
|
1238
|
-
|
|
1239
|
-
# 결과 검증
|
|
1240
|
-
self.assertIsInstance(result, list)
|
|
1241
|
-
self.assertTrue(len(result) > 0)
|
|
1242
|
-
|
|
1243
|
-
# 처리 디렉토리가 생성되었는지 확인
|
|
1244
|
-
self.assertTrue((self.temp_dir / 'processed').exists())
|
|
1245
|
-
self.assertTrue((self.temp_dir / 'thumbnails').exists())
|
|
1246
|
-
|
|
1247
|
-
# 로깅 호출 검증
|
|
1248
|
-
self.assertTrue(self.mock_run.log_message.called)
|
|
1249
|
-
```
|
|
1250
|
-
|
|
1251
|
-
### 디버깅 유틸리티
|
|
1252
|
-
|
|
1253
|
-
```python
|
|
1254
|
-
class DebuggingUploader(BaseUploader):
|
|
1255
|
-
"""향상된 디버깅 기능을 갖춘 업로더."""
|
|
1256
|
-
|
|
1257
|
-
def __init__(self, run, path: Path, file_specification: List = None,
|
|
1258
|
-
organized_files: List = None, extra_params: Dict = None):
|
|
1259
|
-
super().__init__(run, path, file_specification, organized_files, extra_params)
|
|
1260
|
-
|
|
1261
|
-
self.debug_mode = extra_params.get('debug_mode', False)
|
|
1262
|
-
self.debug_dir = self.path / 'debug'
|
|
1263
|
-
|
|
1264
|
-
if self.debug_mode:
|
|
1265
|
-
self.debug_dir.mkdir(exist_ok=True)
|
|
1266
|
-
self.setup_debugging()
|
|
1267
|
-
|
|
1268
|
-
def setup_debugging(self):
|
|
1269
|
-
"""디버깅 인프라 초기화."""
|
|
1270
|
-
import json
|
|
1271
|
-
|
|
1272
|
-
# 초기화 상태 저장
|
|
1273
|
-
init_state = {
|
|
1274
|
-
'path': str(self.path),
|
|
1275
|
-
'file_specification': self.file_specification,
|
|
1276
|
-
'organized_files_count': len(self.organized_files),
|
|
1277
|
-
'extra_params': self.extra_params,
|
|
1278
|
-
'timestamp': datetime.now().isoformat()
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
with open(self.debug_dir / 'init_state.json', 'w') as f:
|
|
1282
|
-
json.dump(init_state, f, indent=2, default=str)
|
|
1283
|
-
|
|
1284
|
-
def debug_log(self, message: str, data: Any = None):
|
|
1285
|
-
"""향상된 디버그 로깅."""
|
|
1286
|
-
if not self.debug_mode:
|
|
1287
|
-
return
|
|
1288
|
-
|
|
1289
|
-
debug_entry = {
|
|
1290
|
-
'timestamp': datetime.now().isoformat(),
|
|
1291
|
-
'message': message,
|
|
1292
|
-
'data': data
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
# 디버그 로그에 작성
|
|
1296
|
-
debug_log_path = self.debug_dir / 'debug.log'
|
|
1297
|
-
with open(debug_log_path, 'a') as f:
|
|
1298
|
-
f.write(json.dumps(debug_entry, default=str) + '\n')
|
|
1299
|
-
|
|
1300
|
-
# 메인 run에도 로그
|
|
1301
|
-
self.run.log_message(f"DEBUG: {message}")
|
|
1302
|
-
|
|
1303
|
-
def setup_directories(self):
|
|
1304
|
-
"""디버깅과 함께 디렉토리 설정."""
|
|
1305
|
-
self.debug_log("디렉토리 설정 중")
|
|
1306
|
-
super().setup_directories()
|
|
1307
|
-
|
|
1308
|
-
if self.debug_mode:
|
|
1309
|
-
# 디렉토리 상태 저장
|
|
1310
|
-
dirs_state = {
|
|
1311
|
-
'existing_dirs': [str(p) for p in self.path.iterdir() if p.is_dir()],
|
|
1312
|
-
'path_exists': self.path.exists(),
|
|
1313
|
-
'path_writable': os.access(self.path, os.W_OK)
|
|
1314
|
-
}
|
|
1315
|
-
self.debug_log("디렉토리 설정 완료", dirs_state)
|
|
1316
|
-
|
|
1317
|
-
def process_files(self, organized_files: List) -> List:
|
|
1318
|
-
"""디버깅 계측과 함께 파일 처리."""
|
|
1319
|
-
self.debug_log(f"{len(organized_files)}개 파일 그룹으로 process_files 시작")
|
|
1320
|
-
|
|
1321
|
-
# 입력 상태 저장
|
|
1322
|
-
if self.debug_mode:
|
|
1323
|
-
with open(self.debug_dir / 'input_files.json', 'w') as f:
|
|
1324
|
-
json.dump(organized_files, f, indent=2, default=str)
|
|
1325
|
-
|
|
1326
|
-
processed_files = []
|
|
1327
|
-
|
|
1328
|
-
for i, file_group in enumerate(organized_files):
|
|
1329
|
-
self.debug_log(f"파일 그룹 {i+1} 처리 중")
|
|
1330
|
-
|
|
1331
|
-
try:
|
|
1332
|
-
# 타이밍과 함께 처리
|
|
1333
|
-
start_time = time.time()
|
|
1334
|
-
processed_file = self.process_file_group_with_debug(file_group, i)
|
|
1335
|
-
duration = time.time() - start_time
|
|
1336
|
-
|
|
1337
|
-
processed_files.append(processed_file)
|
|
1338
|
-
self.debug_log(f"파일 그룹 {i+1} 처리 성공", {'duration': duration})
|
|
1339
|
-
|
|
1340
|
-
except Exception as e:
|
|
1341
|
-
error_data = {
|
|
1342
|
-
'file_group_index': i,
|
|
1343
|
-
'error': str(e),
|
|
1344
|
-
'error_type': type(e).__name__,
|
|
1345
|
-
'file_group': file_group
|
|
1346
|
-
}
|
|
1347
|
-
self.debug_log(f"파일 그룹 {i+1} 처리 오류", error_data)
|
|
1348
|
-
|
|
1349
|
-
# 오류 상태 저장
|
|
1350
|
-
if self.debug_mode:
|
|
1351
|
-
with open(self.debug_dir / f'error_group_{i}.json', 'w') as f:
|
|
1352
|
-
json.dump(error_data, f, indent=2, default=str)
|
|
1353
|
-
|
|
1354
|
-
raise
|
|
1355
|
-
|
|
1356
|
-
# 출력 상태 저장
|
|
1357
|
-
if self.debug_mode:
|
|
1358
|
-
with open(self.debug_dir / 'output_files.json', 'w') as f:
|
|
1359
|
-
json.dump(processed_files, f, indent=2, default=str)
|
|
1360
|
-
|
|
1361
|
-
self.debug_log(f"{len(processed_files)}개 처리된 파일로 process_files 완료")
|
|
1362
|
-
return processed_files
|
|
1363
|
-
|
|
1364
|
-
def process_file_group_with_debug(self, file_group: Dict, index: int) -> Dict:
|
|
1365
|
-
"""디버깅과 함께 개별 파일 그룹 처리."""
|
|
1366
|
-
if self.debug_mode:
|
|
1367
|
-
# 중간 상태 저장
|
|
1368
|
-
with open(self.debug_dir / f'group_{index}_input.json', 'w') as f:
|
|
1369
|
-
json.dump(file_group, f, indent=2, default=str)
|
|
1370
|
-
|
|
1371
|
-
# 처리 로직 적용
|
|
1372
|
-
processed_group = self.apply_custom_processing(file_group)
|
|
1373
|
-
|
|
1374
|
-
if self.debug_mode:
|
|
1375
|
-
# 결과 상태 저장
|
|
1376
|
-
with open(self.debug_dir / f'group_{index}_output.json', 'w') as f:
|
|
1377
|
-
json.dump(processed_group, f, indent=2, default=str)
|
|
1378
|
-
|
|
1379
|
-
return processed_group
|
|
1380
|
-
|
|
1381
|
-
def apply_custom_processing(self, file_group: Dict) -> Dict:
|
|
1382
|
-
"""커스텀 처리 로직 - 필요에 따라 구현하세요."""
|
|
1383
|
-
# 예제 구현
|
|
1384
|
-
file_group['debug_processed'] = True
|
|
1385
|
-
file_group['processing_timestamp'] = datetime.now().isoformat()
|
|
1386
|
-
return file_group
|
|
1387
|
-
|
|
1388
|
-
def generate_debug_report(self):
|
|
1389
|
-
"""포괄적인 디버그 보고서 생성."""
|
|
1390
|
-
if not self.debug_mode:
|
|
1391
|
-
return
|
|
1392
|
-
|
|
1393
|
-
report = {
|
|
1394
|
-
'plugin_name': self.__class__.__name__,
|
|
1395
|
-
'debug_session': datetime.now().isoformat(),
|
|
1396
|
-
'files_processed': 0,
|
|
1397
|
-
'errors': [],
|
|
1398
|
-
'performance': {}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
# 디버그 파일 분석
|
|
1402
|
-
for debug_file in self.debug_dir.glob('*.json'):
|
|
1403
|
-
if debug_file.name.startswith('error_'):
|
|
1404
|
-
with open(debug_file) as f:
|
|
1405
|
-
error_data = json.load(f)
|
|
1406
|
-
report['errors'].append(error_data)
|
|
1407
|
-
elif debug_file.name == 'output_files.json':
|
|
1408
|
-
with open(debug_file) as f:
|
|
1409
|
-
output_data = json.load(f)
|
|
1410
|
-
report['files_processed'] = len(output_data)
|
|
1411
|
-
|
|
1412
|
-
# 최종 보고서 저장
|
|
1413
|
-
with open(self.debug_dir / 'debug_report.json', 'w') as f:
|
|
1414
|
-
json.dump(report, f, indent=2, default=str)
|
|
1415
|
-
|
|
1416
|
-
self.run.log_message(f"디버그 보고서 생성됨: {self.debug_dir / 'debug_report.json'}")
|
|
1417
|
-
```
|
|
1418
|
-
|
|
1419
|
-
## 모범 사례 요약
|
|
1420
|
-
|
|
1421
|
-
### 1. 코드 구성
|
|
1422
|
-
- `process_files()`를 핵심 로직에 집중시키기
|
|
1423
|
-
- 설정, 정리, 검증을 위한 훅 메소드 사용
|
|
1424
|
-
- 헬퍼 메소드를 사용하여 관심사 분리
|
|
1425
|
-
- 단일 책임 원칙 따르기
|
|
1426
|
-
|
|
1427
|
-
### 2. 오류 처리
|
|
1428
|
-
- 포괄적인 오류 처리 구현
|
|
1429
|
-
- 컨텍스트 정보와 함께 오류 로깅
|
|
1430
|
-
- 가능할 때 우아하게 실패
|
|
1431
|
-
- 의미 있는 오류 메시지 제공
|
|
1432
|
-
|
|
1433
|
-
### 3. 성능
|
|
1434
|
-
- 처리 로직 프로파일링
|
|
1435
|
-
- 적절한 데이터 구조 사용
|
|
1436
|
-
- 대용량 파일의 메모리 사용량 고려
|
|
1437
|
-
- I/O 집약적 작업에 비동기 처리 구현
|
|
1438
|
-
|
|
1439
|
-
### 4. 테스트
|
|
1440
|
-
- 모든 메소드에 대한 단위 테스트 작성
|
|
1441
|
-
- 실제 파일과 함께 통합 테스트 포함
|
|
1442
|
-
- 오류 조건 및 엣지 케이스 테스트
|
|
1443
|
-
- 외부 의존성에 모킹 사용
|
|
1444
|
-
|
|
1445
|
-
### 5. 로깅
|
|
1446
|
-
- 중요한 작업 및 이정표 로깅
|
|
1447
|
-
- 성능 분석을 위한 타이밍 정보 포함
|
|
1448
|
-
- 더 나은 분석을 위한 구조화된 로깅 사용
|
|
1449
|
-
- 다양한 로그 레벨 제공 (info, warning, error)
|
|
1450
|
-
|
|
1451
|
-
### 6. 구성
|
|
1452
|
-
- 플러그인 구성에 `extra_params` 사용
|
|
1453
|
-
- 합리적인 기본값 제공
|
|
1454
|
-
- 구성 매개변수 검증
|
|
1455
|
-
- 모든 구성 옵션 문서화
|
|
1456
|
-
|
|
1457
|
-
### 7. 문서화
|
|
1458
|
-
- 명확한 독스트링으로 모든 메소드 문서화
|
|
1459
|
-
- 사용 예제 제공
|
|
1460
|
-
- 구성 옵션 문서화
|
|
1461
|
-
- 문제 해결 정보 포함
|
|
1462
|
-
|
|
1463
|
-
이 포괄적인 가이드는 BaseUploader 템플릿을 사용하여 견고하고 효율적이며 유지보수 가능한 업로드 플러그인을 개발하는 데 도움이 될 것입니다. 예제를 특정 사용 사례와 요구사항에 맞게 조정하는 것을 기억하세요.
|