aiteamutils 0.2.157__py3-none-any.whl → 0.2.159__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.
aiteamutils/base_service.py
CHANGED
@@ -39,181 +39,191 @@ class BaseService(Generic[ModelType]):
|
|
39
39
|
#######################
|
40
40
|
# 입력 및 수정, 삭제 #
|
41
41
|
#######################
|
42
|
-
async def
|
42
|
+
async def _separate_file_data(
|
43
43
|
self,
|
44
|
-
entity_data: Dict[str, Any]
|
45
|
-
entity_result: Any,
|
46
|
-
storage_dir: str,
|
47
|
-
operation: Literal["create", "update", "delete"] = "create"
|
44
|
+
entity_data: Dict[str, Any]
|
48
45
|
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
49
|
-
"""파일
|
46
|
+
"""파일 데이터를 엔티티 데이터에서 분리하는 함수
|
50
47
|
|
51
48
|
Args:
|
52
49
|
entity_data (Dict[str, Any]): 엔티티 데이터
|
53
|
-
entity_result (Any): 생성/수정된 엔티티 결과
|
54
|
-
storage_dir (str): 저장 디렉토리 경로
|
55
|
-
operation (str): 수행할 작업 유형 ("create", "update", "delete")
|
56
50
|
|
57
51
|
Returns:
|
58
|
-
Tuple[Dict[str, Any], Dict[str, Any]]: (
|
52
|
+
Tuple[Dict[str, Any], Dict[str, Any]]: (파일 제외된 엔티티 데이터, 분리된 파일 데이터)
|
59
53
|
"""
|
60
54
|
try:
|
61
|
-
logger.info(
|
62
|
-
logger.info(f"[엔티티 데이터] {entity_data}")
|
63
|
-
|
55
|
+
logger.info("[파일 데이터 분리 시작]")
|
64
56
|
entity_data_copy = entity_data.copy()
|
65
|
-
file_infos = {}
|
66
|
-
|
67
|
-
# 파일 데이터 분리
|
68
57
|
separated_files = {}
|
69
|
-
|
70
|
-
|
58
|
+
|
59
|
+
if 'extra_data' in entity_data_copy and isinstance(entity_data_copy['extra_data'], dict):
|
71
60
|
extra_data = entity_data_copy['extra_data'].copy()
|
72
|
-
|
73
|
-
file_fields = {}
|
74
|
-
for k, v in extra_data.items():
|
75
|
-
logger.info(f"[필드 검사] 필드명: {k}, 타입: {type(v)}, 값: {v}")
|
76
|
-
if v and isinstance(v, (list, tuple)):
|
77
|
-
logger.info(f"[리스트/튜플 검사] 필드명: {k}, 첫 번째 항목 타입: {type(v[0]) if v else 'None'}")
|
78
|
-
# 파일 객체를 포함하는 튜플/리스트인 경우만 처리
|
79
|
-
if any(isinstance(item, (tuple, list)) and len(item) == 2 and hasattr(item[0], 'read') for item in v):
|
80
|
-
logger.info(f"[파일 객체 포함 필드 발견] {k}")
|
81
|
-
file_fields[k] = v
|
82
|
-
|
83
|
-
logger.info(f"[파일 필드 확인] {file_fields}")
|
84
|
-
logger.info(f"[파일 필드 타입] {[(k, type(v), type(v[0]) if isinstance(v, list) and v else None) for k, v in file_fields.items()]}")
|
61
|
+
fields_to_remove = []
|
85
62
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
63
|
+
# 파일 필드 체크 및 분리 (딕셔너리 수정 없이)
|
64
|
+
for field_name, files in extra_data.items():
|
65
|
+
logger.info(f"[필드 검사] 필드명: {field_name}, 타입: {type(files)}")
|
66
|
+
if files and isinstance(files, (list, tuple)):
|
67
|
+
# 파일 객체를 포함하는 튜플/리스트인 경우만 처리
|
68
|
+
if any(isinstance(item, (tuple, list)) and len(item) == 2 and hasattr(item[0], 'read') for item in files):
|
69
|
+
logger.info(f"[파일 필드 발견] {field_name}")
|
70
|
+
separated_files[field_name] = files
|
71
|
+
fields_to_remove.append(field_name)
|
93
72
|
|
94
|
-
# 파일 필드
|
95
|
-
for field_name
|
96
|
-
logger.info(f"[파일 처리] 필드: {field_name}, 파일 수: {len(files)}")
|
97
|
-
logger.info(f"[파일 상세] 필드: {field_name}, 파일 내용: {files}")
|
98
|
-
separated_files[field_name] = files
|
73
|
+
# 파일 필드 제거 (별도 단계로 분리)
|
74
|
+
for field_name in fields_to_remove:
|
99
75
|
del extra_data[field_name]
|
100
76
|
|
101
77
|
entity_data_copy['extra_data'] = extra_data
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
78
|
+
|
79
|
+
logger.info("[파일 데이터 분리 완료]")
|
80
|
+
return entity_data_copy, separated_files
|
81
|
+
|
82
|
+
except Exception as e:
|
83
|
+
logger.error(f"[파일 분리 중 에러 발생] {str(e)}")
|
84
|
+
raise CustomException(
|
85
|
+
ErrorCode.INTERNAL_ERROR,
|
86
|
+
detail=str(e),
|
87
|
+
source_function=f"{self.__class__.__name__}._separate_file_data",
|
88
|
+
original_error=e
|
89
|
+
)
|
90
|
+
|
91
|
+
async def _save_files(
|
92
|
+
self,
|
93
|
+
separated_files: Dict[str, Any],
|
94
|
+
entity_result: Any,
|
95
|
+
storage_dir: str
|
96
|
+
) -> Dict[str, Any]:
|
97
|
+
"""분리된 파일 데이터를 저장하는 함수
|
98
|
+
|
99
|
+
Args:
|
100
|
+
separated_files (Dict[str, Any]): 분리된 파일 데이터
|
101
|
+
entity_result (Any): 생성/수정된 엔티티
|
102
|
+
storage_dir (str): 저장 디렉토리
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
Dict[str, Any]: 저장된 파일 정보
|
106
|
+
"""
|
107
|
+
try:
|
108
|
+
if not storage_dir:
|
109
|
+
raise CustomException(
|
110
|
+
ErrorCode.INVALID_INPUT,
|
111
|
+
detail="storage_dir is required for file upload",
|
112
|
+
source_function=f"{self.__class__.__name__}._save_files"
|
120
113
|
)
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
"""),
|
136
|
-
{
|
137
|
-
"entity_name": self.model.__tablename__,
|
138
|
-
"entity_ulid": entity_result.ulid
|
139
|
-
}
|
114
|
+
|
115
|
+
logger.info("[파일 저장 시작]")
|
116
|
+
file_infos = {}
|
117
|
+
from .files import FileHandler
|
118
|
+
|
119
|
+
for field_name, files in separated_files.items():
|
120
|
+
logger.info(f"[필드 처리] {field_name}, 파일 수: {len(files)}")
|
121
|
+
saved_files = await FileHandler.save_files(
|
122
|
+
files=files,
|
123
|
+
storage_dir=storage_dir,
|
124
|
+
entity_name=self.model.__tablename__,
|
125
|
+
entity_ulid=entity_result.ulid,
|
126
|
+
db_session=self.db_session,
|
127
|
+
column_name=field_name
|
140
128
|
)
|
141
|
-
|
142
|
-
|
143
|
-
# 새 파일 저장 (create 또는 update 작업 시)
|
144
|
-
if operation != "delete" and separated_files:
|
145
|
-
logger.info("[새 파일 저장 시작]")
|
146
|
-
from .files import FileHandler
|
129
|
+
file_infos[field_name] = saved_files
|
147
130
|
|
148
|
-
#
|
149
|
-
if
|
150
|
-
|
151
|
-
return entity_data_copy, {}
|
131
|
+
# extra_data 업데이트
|
132
|
+
if not hasattr(entity_result, 'extra_data') or not entity_result.extra_data:
|
133
|
+
entity_result.extra_data = {}
|
152
134
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
entity_result.extra_data = {}
|
169
|
-
if not entity_result.extra_data:
|
170
|
-
entity_result.extra_data = {}
|
171
|
-
|
172
|
-
entity_result.extra_data[field_name] = [
|
173
|
-
{
|
174
|
-
'original_name': f['original_name'],
|
175
|
-
'storage_path': f['storage_path'],
|
176
|
-
'mime_type': f['mime_type'],
|
177
|
-
'size': f['size'],
|
178
|
-
'checksum': f['checksum'],
|
179
|
-
'column_name': f['column_name']
|
180
|
-
} for f in saved_files
|
181
|
-
]
|
182
|
-
logger.info(f"[extra_data 업데이트 완료] {field_name}")
|
183
|
-
|
184
|
-
# extra_data 업데이트된 엔티티 저장
|
185
|
-
await self.db_session.flush()
|
186
|
-
logger.info("[DB flush 완료]")
|
187
|
-
|
188
|
-
logger.info("[파일 처리 완료]")
|
189
|
-
return entity_data_copy, file_infos
|
190
|
-
|
135
|
+
entity_result.extra_data[field_name] = [
|
136
|
+
{
|
137
|
+
'original_name': f['original_name'],
|
138
|
+
'storage_path': f['storage_path'],
|
139
|
+
'mime_type': f['mime_type'],
|
140
|
+
'size': f['size'],
|
141
|
+
'checksum': f['checksum'],
|
142
|
+
'column_name': f['column_name']
|
143
|
+
} for f in saved_files
|
144
|
+
]
|
145
|
+
|
146
|
+
await self.db_session.flush()
|
147
|
+
logger.info("[파일 저장 완료]")
|
148
|
+
return file_infos
|
149
|
+
|
191
150
|
except CustomException as e:
|
192
|
-
logger.error(f"[CustomException 발생] {e.error_code}: {e.detail}")
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
await FileHandler.delete_files(file_info["storage_path"])
|
151
|
+
logger.error(f"[파일 저장 중 CustomException 발생] {e.error_code}: {e.detail}")
|
152
|
+
await self._cleanup_saved_files(file_infos)
|
153
|
+
raise e
|
154
|
+
except Exception as e:
|
155
|
+
logger.error(f"[파일 저장 중 에러 발생] {str(e)}")
|
156
|
+
await self._cleanup_saved_files(file_infos)
|
199
157
|
raise CustomException(
|
200
|
-
|
201
|
-
detail=e
|
202
|
-
source_function=f"{self.__class__.__name__}.
|
158
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
159
|
+
detail=str(e),
|
160
|
+
source_function=f"{self.__class__.__name__}._save_files",
|
203
161
|
original_error=e
|
204
162
|
)
|
163
|
+
|
164
|
+
async def _cleanup_saved_files(self, file_infos: Dict[str, Any]):
|
165
|
+
"""저장된 파일들을 정리(삭제)하는 함수"""
|
166
|
+
if file_infos:
|
167
|
+
from .files import FileHandler
|
168
|
+
for file_list in file_infos.values():
|
169
|
+
for file_info in file_list:
|
170
|
+
logger.info(f"[에러 복구] 파일 삭제: {file_info['storage_path']}")
|
171
|
+
await FileHandler.delete_files(file_info["storage_path"])
|
172
|
+
|
173
|
+
async def _delete_files(
|
174
|
+
self,
|
175
|
+
entity_result: Any
|
176
|
+
) -> None:
|
177
|
+
"""엔티티와 관련된 파일들을 삭제하는 함수
|
178
|
+
|
179
|
+
Args:
|
180
|
+
entity_result (Any): 삭제할 파일이 있는 엔티티
|
181
|
+
"""
|
182
|
+
try:
|
183
|
+
logger.info("[파일 삭제 시작]")
|
184
|
+
from .files import FileHandler
|
185
|
+
|
186
|
+
# files 테이블에서 기존 파일 정보 조회
|
187
|
+
existing_files = await self.db_session.execute(
|
188
|
+
text("""
|
189
|
+
SELECT storage_path
|
190
|
+
FROM files
|
191
|
+
WHERE entity_name = :entity_name
|
192
|
+
AND entity_ulid = :entity_ulid
|
193
|
+
"""),
|
194
|
+
{
|
195
|
+
"entity_name": self.model.__tablename__,
|
196
|
+
"entity_ulid": entity_result.ulid
|
197
|
+
}
|
198
|
+
)
|
199
|
+
existing_files = existing_files.fetchall()
|
200
|
+
logger.info(f"[기존 파일 조회 결과] {existing_files}")
|
201
|
+
|
202
|
+
# 기존 파일 삭제
|
203
|
+
for file_info in existing_files:
|
204
|
+
logger.info(f"[파일 삭제] {file_info[0]}")
|
205
|
+
await FileHandler.delete_files(file_info[0])
|
206
|
+
|
207
|
+
# files 테이블에서 레코드 삭제
|
208
|
+
await self.db_session.execute(
|
209
|
+
text("""
|
210
|
+
DELETE FROM files
|
211
|
+
WHERE entity_name = :entity_name
|
212
|
+
AND entity_ulid = :entity_ulid
|
213
|
+
"""),
|
214
|
+
{
|
215
|
+
"entity_name": self.model.__tablename__,
|
216
|
+
"entity_ulid": entity_result.ulid
|
217
|
+
}
|
218
|
+
)
|
219
|
+
logger.info("[파일 DB 레코드 삭제 완료]")
|
220
|
+
|
205
221
|
except Exception as e:
|
206
|
-
logger.error(f"[
|
207
|
-
if file_infos:
|
208
|
-
from .files import FileHandler
|
209
|
-
for file_list in file_infos.values():
|
210
|
-
for file_info in file_list:
|
211
|
-
logger.info(f"[에러 복구] 파일 삭제: {file_info['storage_path']}")
|
212
|
-
await FileHandler.delete_files(file_info["storage_path"])
|
222
|
+
logger.error(f"[파일 삭제 중 에러 발생] {str(e)}")
|
213
223
|
raise CustomException(
|
214
224
|
ErrorCode.FILE_SYSTEM_ERROR,
|
215
|
-
detail=
|
216
|
-
source_function=f"{self.__class__.__name__}.
|
225
|
+
detail=str(e),
|
226
|
+
source_function=f"{self.__class__.__name__}._delete_files",
|
217
227
|
original_error=e
|
218
228
|
)
|
219
229
|
|
@@ -254,16 +264,8 @@ class BaseService(Generic[ModelType]):
|
|
254
264
|
if fk_check:
|
255
265
|
await validate_unique_fields(self.db_session, fk_check, find_value=False)
|
256
266
|
|
257
|
-
# 파일 데이터 분리를 위한 임시 엔티티 생성
|
258
|
-
temp_entity = type('TempEntity', (), {'ulid': str(ULID())})()
|
259
|
-
|
260
267
|
# 파일 데이터 분리
|
261
|
-
entity_data_copy,
|
262
|
-
entity_data=entity_data,
|
263
|
-
entity_result=temp_entity,
|
264
|
-
storage_dir=storage_dir,
|
265
|
-
operation="create"
|
266
|
-
)
|
268
|
+
entity_data_copy, separated_files = await self._separate_file_data(entity_data)
|
267
269
|
|
268
270
|
# 엔티티 생성 (파일 데이터가 제거된 상태)
|
269
271
|
result = await self.repository.create(
|
@@ -271,20 +273,19 @@ class BaseService(Generic[ModelType]):
|
|
271
273
|
exclude_entities=exclude_entities
|
272
274
|
)
|
273
275
|
|
274
|
-
#
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
276
|
+
# 파일 저장
|
277
|
+
file_infos = {}
|
278
|
+
if storage_dir and separated_files:
|
279
|
+
file_infos = await self._save_files(
|
280
|
+
separated_files=separated_files,
|
281
|
+
entity_result=result,
|
282
|
+
storage_dir=storage_dir
|
281
283
|
)
|
282
284
|
|
283
285
|
# 결과 반환
|
284
286
|
if response_model:
|
285
287
|
processed_result = process_response(result, response_model)
|
286
|
-
|
287
|
-
if storage_dir and 'file_infos' in locals():
|
288
|
+
if file_infos:
|
288
289
|
for key, value in file_infos.items():
|
289
290
|
processed_result[key] = value
|
290
291
|
return processed_result
|
@@ -362,12 +363,7 @@ class BaseService(Generic[ModelType]):
|
|
362
363
|
)
|
363
364
|
|
364
365
|
# 파일 데이터 분리
|
365
|
-
entity_data_copy,
|
366
|
-
entity_data=entity_data,
|
367
|
-
entity_result=existing_entity,
|
368
|
-
storage_dir=storage_dir,
|
369
|
-
operation="update"
|
370
|
-
)
|
366
|
+
entity_data_copy, separated_files = await self._separate_file_data(entity_data)
|
371
367
|
|
372
368
|
# 엔티티 수정 (파일 데이터가 제거된 상태)
|
373
369
|
result = await self.repository.update(
|
@@ -376,19 +372,21 @@ class BaseService(Generic[ModelType]):
|
|
376
372
|
exclude_entities=exclude_entities
|
377
373
|
)
|
378
374
|
|
379
|
-
#
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
375
|
+
# 기존 파일 삭제 후 새 파일 저장
|
376
|
+
file_infos = {}
|
377
|
+
if storage_dir and separated_files:
|
378
|
+
await self._delete_files(result)
|
379
|
+
file_infos = await self._save_files(
|
380
|
+
separated_files=separated_files,
|
381
|
+
entity_result=result,
|
382
|
+
storage_dir=storage_dir
|
383
|
+
)
|
386
384
|
|
387
385
|
if response_model:
|
388
386
|
processed_result = process_response(result, response_model)
|
389
|
-
|
390
|
-
|
391
|
-
|
387
|
+
if file_infos:
|
388
|
+
for key, value in file_infos.items():
|
389
|
+
processed_result[key] = value
|
392
390
|
return processed_result
|
393
391
|
else:
|
394
392
|
return result
|
@@ -439,13 +437,9 @@ class BaseService(Generic[ModelType]):
|
|
439
437
|
if not entity:
|
440
438
|
return False
|
441
439
|
|
442
|
-
# 파일
|
443
|
-
|
444
|
-
|
445
|
-
entity_result=entity,
|
446
|
-
storage_dir=storage_dir,
|
447
|
-
operation="delete"
|
448
|
-
)
|
440
|
+
# 파일 삭제
|
441
|
+
if storage_dir:
|
442
|
+
await self._delete_files(entity)
|
449
443
|
|
450
444
|
return await self.repository.delete(
|
451
445
|
conditions=conditions
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.159"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
|
2
2
|
aiteamutils/base_model.py,sha256=yBZqzTDF9PA4wCAvmYfG12FdVwLtxOEUCcA3z2i6fXU,4176
|
3
3
|
aiteamutils/base_repository.py,sha256=Oy2zE1i5qx60Xf1tnsaKLyFWapiPqt5JH8NejwNrPWg,4647
|
4
|
-
aiteamutils/base_service.py,sha256=
|
4
|
+
aiteamutils/base_service.py,sha256=uYEym6Cp2prK_ZbzAtrQaYLZRqn-AolTwoso_ASEqCk,22043
|
5
5
|
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
6
|
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
7
|
aiteamutils/database.py,sha256=msvBKtxWeQVOo0v2Q9i2azuTNtnUItuNNar52gdRZTo,20418
|
@@ -11,7 +11,7 @@ aiteamutils/files.py,sha256=Qq2w3VAEOzvsDirYtxRTN48LnIzf4TPUH2LftmyYtQk,12831
|
|
11
11
|
aiteamutils/models.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
|
12
12
|
aiteamutils/security.py,sha256=McUl3t5Z5SyUDVUHymHdDkYyF4YSeg4g9fFMML4W6Kw,11630
|
13
13
|
aiteamutils/validators.py,sha256=_WHN6jqJQzKM5uPTg-Da8U2qqevS84XeKMkCCF4C_lY,9591
|
14
|
-
aiteamutils/version.py,sha256=
|
15
|
-
aiteamutils-0.2.
|
16
|
-
aiteamutils-0.2.
|
17
|
-
aiteamutils-0.2.
|
14
|
+
aiteamutils/version.py,sha256=xr1O2ZYSzlPjLr1SLG4IBRNdFzNlMV4GLn1gHFE6Kd8,43
|
15
|
+
aiteamutils-0.2.159.dist-info/METADATA,sha256=gmkHCBPBGe1kRWjRwOBNlhg8lywLG2N0EG4IXLNP-D0,1743
|
16
|
+
aiteamutils-0.2.159.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
17
|
+
aiteamutils-0.2.159.dist-info/RECORD,,
|
File without changes
|