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.

Files changed (78) hide show
  1. synapse_sdk/clients/base.py +129 -9
  2. synapse_sdk/devtools/docs/docs/api/clients/base.md +230 -8
  3. synapse_sdk/devtools/docs/docs/api/plugins/models.md +58 -3
  4. synapse_sdk/devtools/docs/docs/plugins/categories/neural-net-plugins/train-action-overview.md +663 -0
  5. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
  6. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
  7. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
  8. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
  9. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
  10. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-overview.md +585 -0
  11. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
  12. synapse_sdk/devtools/docs/docs/plugins/export-plugins.md +39 -0
  13. synapse_sdk/devtools/docs/docs/plugins/plugins.md +12 -5
  14. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/base.md +230 -8
  15. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/plugins/models.md +114 -0
  16. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/neural-net-plugins/train-action-overview.md +621 -0
  17. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
  18. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
  19. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
  20. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
  21. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
  22. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-overview.md +585 -0
  23. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
  24. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/export-plugins.md +39 -0
  25. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current.json +16 -4
  26. synapse_sdk/devtools/docs/sidebars.ts +45 -1
  27. synapse_sdk/plugins/README.md +487 -80
  28. synapse_sdk/plugins/categories/base.py +1 -0
  29. synapse_sdk/plugins/categories/export/actions/export/action.py +8 -3
  30. synapse_sdk/plugins/categories/export/actions/export/utils.py +108 -8
  31. synapse_sdk/plugins/categories/export/templates/config.yaml +18 -0
  32. synapse_sdk/plugins/categories/export/templates/plugin/export.py +97 -0
  33. synapse_sdk/plugins/categories/neural_net/actions/train.py +592 -22
  34. synapse_sdk/plugins/categories/neural_net/actions/tune.py +150 -3
  35. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  36. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  37. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/action.py +10 -0
  38. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  39. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +145 -0
  40. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  41. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  42. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  43. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +97 -0
  44. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +250 -0
  45. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  46. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  47. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +284 -0
  48. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  49. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  50. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +87 -0
  51. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +127 -0
  52. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  53. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +2 -1
  54. synapse_sdk/plugins/categories/upload/actions/upload/action.py +8 -1
  55. synapse_sdk/plugins/categories/upload/actions/upload/context.py +0 -1
  56. synapse_sdk/plugins/categories/upload/actions/upload/models.py +134 -94
  57. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +2 -2
  58. synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +6 -2
  59. synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +24 -9
  60. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +130 -18
  61. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +147 -37
  62. synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +10 -5
  63. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +31 -6
  64. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +65 -37
  65. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +17 -2
  66. synapse_sdk/plugins/categories/upload/templates/README.md +394 -0
  67. synapse_sdk/plugins/models.py +62 -0
  68. synapse_sdk/utils/file/download.py +261 -0
  69. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/METADATA +15 -2
  70. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/RECORD +74 -43
  71. synapse_sdk/devtools/docs/docs/plugins/developing-upload-template.md +0 -1463
  72. synapse_sdk/devtools/docs/docs/plugins/upload-plugins.md +0 -1964
  73. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/developing-upload-template.md +0 -1463
  74. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/upload-plugins.md +0 -2077
  75. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/WHEEL +0 -0
  76. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/entry_points.txt +0 -0
  77. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/licenses/LICENSE +0 -0
  78. {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 템플릿을 사용하여 견고하고 효율적이며 유지보수 가능한 업로드 플러그인을 개발하는 데 도움이 될 것입니다. 예제를 특정 사용 사례와 요구사항에 맞게 조정하는 것을 기억하세요.