aiteamutils 0.2.140__py3-none-any.whl → 0.2.142__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 +217 -103
- aiteamutils/files.py +2 -2
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.140.dist-info → aiteamutils-0.2.142.dist-info}/METADATA +1 -1
- {aiteamutils-0.2.140.dist-info → aiteamutils-0.2.142.dist-info}/RECORD +6 -6
- {aiteamutils-0.2.140.dist-info → aiteamutils-0.2.142.dist-info}/WHEEL +0 -0
aiteamutils/base_service.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
#기본 라이브러리
|
2
2
|
from fastapi import Request
|
3
|
-
from typing import TypeVar, Generic, Type, Dict, Any, Union, List, Optional, Literal
|
3
|
+
from typing import TypeVar, Generic, Type, Dict, Any, Union, List, Optional, Literal, Tuple
|
4
4
|
from sqlalchemy.orm import DeclarativeBase
|
5
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
6
6
|
from datetime import datetime
|
7
7
|
from ulid import ULID
|
8
|
+
from sqlalchemy import text
|
8
9
|
|
9
10
|
#패키지 라이브러리
|
10
11
|
from .exceptions import ErrorCode, CustomException
|
@@ -35,42 +36,31 @@ class BaseService(Generic[ModelType]):
|
|
35
36
|
#######################
|
36
37
|
# 입력 및 수정, 삭제 #
|
37
38
|
#######################
|
38
|
-
async def
|
39
|
+
async def _process_files(
|
39
40
|
self,
|
40
|
-
request: Request,
|
41
41
|
entity_data: Dict[str, Any],
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
role_permission: str | None = None,
|
48
|
-
token_settings: Dict[str, Any] | None = None,
|
49
|
-
storage_dir: str | None = None
|
50
|
-
) -> ModelType:
|
51
|
-
|
52
|
-
if role_permission:
|
53
|
-
permission_result = await verify_role_permission(
|
54
|
-
request=request,
|
55
|
-
role_permission=role_permission,
|
56
|
-
token_settings=token_settings,
|
57
|
-
org_ulid_position=org_ulid_position
|
58
|
-
)
|
59
|
-
|
60
|
-
if not permission_result:
|
61
|
-
raise CustomException(
|
62
|
-
ErrorCode.FORBIDDEN,
|
63
|
-
detail=f"{role_permission}",
|
64
|
-
source_function=f"base_service.{self.__class__.__name__}.create.permission_result"
|
65
|
-
)
|
42
|
+
entity_result: Any,
|
43
|
+
storage_dir: str,
|
44
|
+
operation: Literal["create", "update", "delete"] = "create"
|
45
|
+
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
46
|
+
"""파일 처리를 위한 내부 메서드
|
66
47
|
|
48
|
+
Args:
|
49
|
+
entity_data (Dict[str, Any]): 엔티티 데이터
|
50
|
+
entity_result (Any): 생성/수정된 엔티티 결과
|
51
|
+
storage_dir (str): 저장 디렉토리 경로
|
52
|
+
operation (str): 수행할 작업 유형 ("create", "update", "delete")
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Tuple[Dict[str, Any], Dict[str, Any]]: (처리된 엔티티 데이터, 파일 정보)
|
56
|
+
"""
|
67
57
|
try:
|
68
|
-
# 파일 데이터 분리
|
69
58
|
entity_data_copy = entity_data.copy()
|
70
|
-
|
59
|
+
file_infos = {}
|
71
60
|
|
72
|
-
#
|
73
|
-
|
61
|
+
# 파일 데이터 분리
|
62
|
+
separated_files = {}
|
63
|
+
if operation != "delete" and 'extra_data' in entity_data_copy and isinstance(entity_data_copy['extra_data'], dict):
|
74
64
|
extra_data = entity_data_copy['extra_data'].copy()
|
75
65
|
file_fields = {k: v for k, v in extra_data.items() if k.endswith('_files')}
|
76
66
|
|
@@ -78,18 +68,142 @@ class BaseService(Generic[ModelType]):
|
|
78
68
|
raise CustomException(
|
79
69
|
ErrorCode.INVALID_INPUT,
|
80
70
|
detail="storage_dir is required for file upload",
|
81
|
-
source_function=f"
|
71
|
+
source_function=f"{self.__class__.__name__}._process_files"
|
82
72
|
)
|
83
73
|
|
84
74
|
# 파일 필드 분리 및 제거
|
85
75
|
for field_name, files in file_fields.items():
|
86
76
|
if files:
|
87
77
|
separated_files[field_name] = files
|
88
|
-
# extra_data에서 파일 필드 제거
|
89
78
|
del extra_data[field_name]
|
90
79
|
|
91
80
|
entity_data_copy['extra_data'] = extra_data
|
92
81
|
|
82
|
+
# 기존 파일 삭제 (update 또는 delete 작업 시)
|
83
|
+
if operation in ["update", "delete"]:
|
84
|
+
from .files import FileHandler
|
85
|
+
# files 테이블에서 기존 파일 정보 조회
|
86
|
+
existing_files = await self.db_session.execute(
|
87
|
+
text("""
|
88
|
+
SELECT storage_path
|
89
|
+
FROM files
|
90
|
+
WHERE entity_name = :entity_name
|
91
|
+
AND entity_ulid = :entity_ulid
|
92
|
+
"""),
|
93
|
+
{
|
94
|
+
"entity_name": self.model.__tablename__,
|
95
|
+
"entity_ulid": entity_result.ulid
|
96
|
+
}
|
97
|
+
)
|
98
|
+
existing_files = existing_files.fetchall()
|
99
|
+
|
100
|
+
# 기존 파일 삭제
|
101
|
+
for file_info in existing_files:
|
102
|
+
await FileHandler.delete_files(file_info[0])
|
103
|
+
|
104
|
+
# files 테이블에서 레코드 삭제
|
105
|
+
await self.db_session.execute(
|
106
|
+
text("""
|
107
|
+
DELETE FROM files
|
108
|
+
WHERE entity_name = :entity_name
|
109
|
+
AND entity_ulid = :entity_ulid
|
110
|
+
"""),
|
111
|
+
{
|
112
|
+
"entity_name": self.model.__tablename__,
|
113
|
+
"entity_ulid": entity_result.ulid
|
114
|
+
}
|
115
|
+
)
|
116
|
+
|
117
|
+
# 새 파일 저장 (create 또는 update 작업 시)
|
118
|
+
if operation != "delete" and separated_files:
|
119
|
+
from .files import FileHandler
|
120
|
+
for field_name, files in separated_files.items():
|
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.replace('_files', '')
|
128
|
+
)
|
129
|
+
file_infos[field_name] = saved_files
|
130
|
+
|
131
|
+
# extra_data 업데이트 - 파일 정보 캐싱
|
132
|
+
if not hasattr(entity_result, 'extra_data'):
|
133
|
+
entity_result.extra_data = {}
|
134
|
+
if not entity_result.extra_data:
|
135
|
+
entity_result.extra_data = {}
|
136
|
+
|
137
|
+
entity_result.extra_data[field_name] = [
|
138
|
+
{
|
139
|
+
'original_name': f['original_name'],
|
140
|
+
'storage_path': f['storage_path'],
|
141
|
+
'mime_type': f['mime_type'],
|
142
|
+
'size': f['size'],
|
143
|
+
'checksum': f['checksum'],
|
144
|
+
'column_name': f['column_name']
|
145
|
+
} for f in saved_files
|
146
|
+
]
|
147
|
+
|
148
|
+
# extra_data 업데이트된 엔티티 저장
|
149
|
+
await self.db_session.flush()
|
150
|
+
|
151
|
+
return entity_data_copy, file_infos
|
152
|
+
|
153
|
+
except CustomException as e:
|
154
|
+
if file_infos:
|
155
|
+
from .files import FileHandler
|
156
|
+
for file_list in file_infos.values():
|
157
|
+
for file_info in file_list:
|
158
|
+
await FileHandler.delete_files(file_info["storage_path"])
|
159
|
+
raise CustomException(
|
160
|
+
e.error_code,
|
161
|
+
detail=e.detail,
|
162
|
+
source_function=f"{self.__class__.__name__}._process_files",
|
163
|
+
original_error=e
|
164
|
+
)
|
165
|
+
except Exception as e:
|
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
|
+
await FileHandler.delete_files(file_info["storage_path"])
|
171
|
+
raise CustomException(
|
172
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
173
|
+
detail=str(e),
|
174
|
+
source_function=f"{self.__class__.__name__}._process_files",
|
175
|
+
original_error=e
|
176
|
+
)
|
177
|
+
|
178
|
+
async def create(
|
179
|
+
self,
|
180
|
+
request: Request,
|
181
|
+
entity_data: Dict[str, Any],
|
182
|
+
response_model: Any = None,
|
183
|
+
exclude_entities: List[str] | None = None,
|
184
|
+
unique_check: List[Dict[str, Any]] | None = None,
|
185
|
+
fk_check: List[Dict[str, Any]] | None = None,
|
186
|
+
org_ulid_position: str = "organization_ulid",
|
187
|
+
role_permission: str | None = None,
|
188
|
+
token_settings: Dict[str, Any] | None = None,
|
189
|
+
storage_dir: str | None = None
|
190
|
+
) -> ModelType:
|
191
|
+
try:
|
192
|
+
if role_permission:
|
193
|
+
permission_result = await verify_role_permission(
|
194
|
+
request=request,
|
195
|
+
role_permission=role_permission,
|
196
|
+
token_settings=token_settings,
|
197
|
+
org_ulid_position=org_ulid_position
|
198
|
+
)
|
199
|
+
|
200
|
+
if not permission_result:
|
201
|
+
raise CustomException(
|
202
|
+
ErrorCode.FORBIDDEN,
|
203
|
+
detail=f"{role_permission}",
|
204
|
+
source_function=f"base_service.{self.__class__.__name__}.create.permission_result"
|
205
|
+
)
|
206
|
+
|
93
207
|
async with self.db_session.begin():
|
94
208
|
# 고유 검사 수행
|
95
209
|
if unique_check:
|
@@ -98,69 +212,30 @@ class BaseService(Generic[ModelType]):
|
|
98
212
|
if fk_check:
|
99
213
|
await validate_unique_fields(self.db_session, fk_check, find_value=False)
|
100
214
|
|
101
|
-
# 엔티티 생성
|
215
|
+
# 파일 데이터 분리를 위한 임시 엔티티 생성
|
216
|
+
temp_entity = type('TempEntity', (), {'ulid': str(ULID())})()
|
217
|
+
|
218
|
+
# 파일 데이터 분리
|
219
|
+
entity_data_copy, _ = await self._process_files(
|
220
|
+
entity_data=entity_data,
|
221
|
+
entity_result=temp_entity,
|
222
|
+
storage_dir=storage_dir,
|
223
|
+
operation="create"
|
224
|
+
)
|
225
|
+
|
226
|
+
# 엔티티 생성 (파일 데이터가 제거된 상태)
|
102
227
|
result = await self.repository.create(
|
103
228
|
entity_data=entity_data_copy,
|
104
229
|
exclude_entities=exclude_entities
|
105
230
|
)
|
106
231
|
|
107
|
-
# 파일 처리
|
108
|
-
file_infos =
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
files=files,
|
115
|
-
storage_dir=storage_dir,
|
116
|
-
entity_name=self.model.__tablename__,
|
117
|
-
entity_ulid=result.ulid,
|
118
|
-
db_session=self.db_session,
|
119
|
-
column_name=field_name.replace('_files', '') # _files 접미사 제거
|
120
|
-
)
|
121
|
-
file_infos[field_name] = saved_files
|
122
|
-
|
123
|
-
# extra_data 업데이트 - 파일 정보 캐싱
|
124
|
-
if not hasattr(result, 'extra_data'):
|
125
|
-
result.extra_data = {}
|
126
|
-
if not result.extra_data:
|
127
|
-
result.extra_data = {}
|
128
|
-
|
129
|
-
result.extra_data[field_name] = [
|
130
|
-
{
|
131
|
-
'original_name': f['original_name'],
|
132
|
-
'storage_path': f['storage_path'],
|
133
|
-
'mime_type': f['mime_type'],
|
134
|
-
'size': f['size'],
|
135
|
-
'checksum': f['checksum'],
|
136
|
-
'column_name': f['column_name']
|
137
|
-
} for f in saved_files
|
138
|
-
]
|
139
|
-
|
140
|
-
# extra_data 업데이트된 엔티티 저장
|
141
|
-
await self.db_session.flush()
|
142
|
-
except CustomException as e:
|
143
|
-
# 파일 저장 실패 시 저장된 파일들 삭제
|
144
|
-
for file_list in file_infos.values():
|
145
|
-
for file_info in file_list:
|
146
|
-
await FileHandler.delete_files(file_info["storage_path"])
|
147
|
-
raise CustomException(
|
148
|
-
e.error_code,
|
149
|
-
detail=e.detail,
|
150
|
-
source_function=f"{self.__class__.__name__}.create.file_processing",
|
151
|
-
original_error=e
|
152
|
-
)
|
153
|
-
except Exception as e:
|
154
|
-
# 파일 저장 실패 시 저장된 파일들 삭제
|
155
|
-
for file_list in file_infos.values():
|
156
|
-
for file_info in file_list:
|
157
|
-
await FileHandler.delete_files(file_info["storage_path"])
|
158
|
-
raise CustomException(
|
159
|
-
ErrorCode.FILE_SYSTEM_ERROR,
|
160
|
-
detail=str(e),
|
161
|
-
source_function=f"{self.__class__.__name__}.create.file_processing",
|
162
|
-
original_error=e
|
163
|
-
)
|
232
|
+
# 실제 파일 처리
|
233
|
+
_, file_infos = await self._process_files(
|
234
|
+
entity_data=entity_data,
|
235
|
+
entity_result=result,
|
236
|
+
storage_dir=storage_dir,
|
237
|
+
operation="create"
|
238
|
+
)
|
164
239
|
|
165
240
|
# 결과 반환
|
166
241
|
if response_model:
|
@@ -173,22 +248,15 @@ class BaseService(Generic[ModelType]):
|
|
173
248
|
return result
|
174
249
|
|
175
250
|
except CustomException as e:
|
176
|
-
# 파일 저장 실패 시 저장된 파일들 삭제
|
177
|
-
if 'file_infos' in locals():
|
178
|
-
for file_list in file_infos.values():
|
179
|
-
for file_info in file_list:
|
180
|
-
from .files import FileHandler
|
181
|
-
await FileHandler.delete_files(file_info["storage_path"])
|
182
251
|
raise e
|
183
252
|
except Exception as e:
|
184
|
-
# 다른 예외 처리
|
185
253
|
raise CustomException(
|
186
254
|
ErrorCode.INTERNAL_ERROR,
|
187
255
|
detail=str(e),
|
188
256
|
source_function=f"base_service.{self.__class__.__name__}.create",
|
189
257
|
original_error=e
|
190
258
|
)
|
191
|
-
|
259
|
+
|
192
260
|
async def update(
|
193
261
|
self,
|
194
262
|
request: Request,
|
@@ -200,7 +268,8 @@ class BaseService(Generic[ModelType]):
|
|
200
268
|
response_model: Any = None,
|
201
269
|
org_ulid_position: str = "organization_ulid",
|
202
270
|
role_permission: str = "update",
|
203
|
-
token_settings: Dict[str, Any] | None = None
|
271
|
+
token_settings: Dict[str, Any] | None = None,
|
272
|
+
storage_dir: str | None = None
|
204
273
|
) -> ModelType:
|
205
274
|
try:
|
206
275
|
async with self.db_session.begin():
|
@@ -226,16 +295,47 @@ class BaseService(Generic[ModelType]):
|
|
226
295
|
|
227
296
|
conditions = {"ulid": ulid}
|
228
297
|
|
229
|
-
|
298
|
+
# 기존 엔티티 조회
|
299
|
+
existing_entity = await self.repository.get(conditions=conditions)
|
300
|
+
if not existing_entity:
|
301
|
+
raise CustomException(
|
302
|
+
ErrorCode.NOT_FOUND,
|
303
|
+
detail=str(ulid or conditions),
|
304
|
+
source_function=f"base_service.{self.__class__.__name__}.update.get_entity"
|
305
|
+
)
|
306
|
+
|
307
|
+
# 파일 데이터 분리
|
308
|
+
entity_data_copy, _ = await self._process_files(
|
230
309
|
entity_data=entity_data,
|
310
|
+
entity_result=existing_entity,
|
311
|
+
storage_dir=storage_dir,
|
312
|
+
operation="update"
|
313
|
+
)
|
314
|
+
|
315
|
+
# 엔티티 수정 (파일 데이터가 제거된 상태)
|
316
|
+
result = await self.repository.update(
|
317
|
+
entity_data=entity_data_copy,
|
231
318
|
conditions=conditions,
|
232
319
|
exclude_entities=exclude_entities
|
233
320
|
)
|
234
321
|
|
322
|
+
# 실제 파일 처리
|
323
|
+
_, file_infos = await self._process_files(
|
324
|
+
entity_data=entity_data,
|
325
|
+
entity_result=result,
|
326
|
+
storage_dir=storage_dir,
|
327
|
+
operation="update"
|
328
|
+
)
|
329
|
+
|
235
330
|
if response_model:
|
236
|
-
|
331
|
+
processed_result = process_response(result, response_model)
|
332
|
+
# 파일 정보 추가
|
333
|
+
for key, value in file_infos.items():
|
334
|
+
processed_result[key] = value
|
335
|
+
return processed_result
|
237
336
|
else:
|
238
337
|
return result
|
338
|
+
|
239
339
|
except CustomException as e:
|
240
340
|
raise e
|
241
341
|
except Exception as e:
|
@@ -253,7 +353,8 @@ class BaseService(Generic[ModelType]):
|
|
253
353
|
conditions: Dict[str, Any] | None = None,
|
254
354
|
org_ulid_position: str = "organization_ulid",
|
255
355
|
role_permission: str = "delete",
|
256
|
-
token_settings: Dict[str, Any] | None = None
|
356
|
+
token_settings: Dict[str, Any] | None = None,
|
357
|
+
storage_dir: str | None = None
|
257
358
|
) -> bool:
|
258
359
|
try:
|
259
360
|
if not ULID.from_str(ulid):
|
@@ -276,6 +377,19 @@ class BaseService(Generic[ModelType]):
|
|
276
377
|
|
277
378
|
conditions["is_deleted"] = False
|
278
379
|
|
380
|
+
# 엔티티 조회 (파일 삭제를 위해)
|
381
|
+
entity = await self.repository.get(conditions=conditions)
|
382
|
+
if not entity:
|
383
|
+
return False
|
384
|
+
|
385
|
+
# 파일 처리 (삭제)
|
386
|
+
_, _ = await self._process_files(
|
387
|
+
entity_data={},
|
388
|
+
entity_result=entity,
|
389
|
+
storage_dir=storage_dir,
|
390
|
+
operation="delete"
|
391
|
+
)
|
392
|
+
|
279
393
|
return await self.repository.delete(
|
280
394
|
conditions=conditions
|
281
395
|
)
|
aiteamutils/files.py
CHANGED
@@ -26,7 +26,7 @@ class FileHandler:
|
|
26
26
|
except Exception as e:
|
27
27
|
raise CustomException(
|
28
28
|
ErrorCode.FILE_SYSTEM_ERROR,
|
29
|
-
detail=str(
|
29
|
+
detail=f"{directory}|{str(e)}",
|
30
30
|
source_function="FileHandler._create_directory",
|
31
31
|
original_error=e
|
32
32
|
)
|
@@ -130,7 +130,7 @@ class FileHandler:
|
|
130
130
|
except Exception as e:
|
131
131
|
raise CustomException(
|
132
132
|
ErrorCode.FILE_SYSTEM_ERROR,
|
133
|
-
detail=str(
|
133
|
+
detail=f"{storage_path}|{str(e)}",
|
134
134
|
source_function="FileHandler._save_file",
|
135
135
|
original_error=e
|
136
136
|
)
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.142"
|
@@ -1,16 +1,16 @@
|
|
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=UtPPFxBWlvkWml-kUYUkiHJrSbPf7VQa0B0q_IraGm8,20091
|
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
|
8
8
|
aiteamutils/enums.py,sha256=7WLqlcJqQWtETAga2WAxNp3dJTQIAd2TW-4WzkoHHa8,2498
|
9
9
|
aiteamutils/exceptions.py,sha256=pgf3ersezObyl17wAO3I2fb8m9t2OzWDX1mSjwAWm2Y,16035
|
10
|
-
aiteamutils/files.py,sha256=
|
10
|
+
aiteamutils/files.py,sha256=1XuLAvsQbnURHSZ2IyZIXrZH6OWgnC5Tl1vcTDyi1GY,9017
|
11
11
|
aiteamutils/security.py,sha256=McUl3t5Z5SyUDVUHymHdDkYyF4YSeg4g9fFMML4W6Kw,11630
|
12
12
|
aiteamutils/validators.py,sha256=_WHN6jqJQzKM5uPTg-Da8U2qqevS84XeKMkCCF4C_lY,9591
|
13
|
-
aiteamutils/version.py,sha256=
|
14
|
-
aiteamutils-0.2.
|
15
|
-
aiteamutils-0.2.
|
16
|
-
aiteamutils-0.2.
|
13
|
+
aiteamutils/version.py,sha256=cSpUTZx6ixUNli53iAkCYH4zyowdUwk8cWwr7ZC4O78,43
|
14
|
+
aiteamutils-0.2.142.dist-info/METADATA,sha256=DmfTkUiy1Sm2qxUkeElfhp4gvnkveW6j5-Y8xr-BzVA,1743
|
15
|
+
aiteamutils-0.2.142.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.142.dist-info/RECORD,,
|
File without changes
|