aiteamutils 0.2.143__py3-none-any.whl → 0.2.145__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 +43 -6
- aiteamutils/files.py +76 -58
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.143.dist-info → aiteamutils-0.2.145.dist-info}/METADATA +1 -1
- {aiteamutils-0.2.143.dist-info → aiteamutils-0.2.145.dist-info}/RECORD +6 -6
- {aiteamutils-0.2.143.dist-info → aiteamutils-0.2.145.dist-info}/WHEEL +0 -0
aiteamutils/base_service.py
CHANGED
@@ -6,6 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
6
|
from datetime import datetime
|
7
7
|
from ulid import ULID
|
8
8
|
from sqlalchemy import text
|
9
|
+
import logging
|
9
10
|
|
10
11
|
#패키지 라이브러리
|
11
12
|
from .exceptions import ErrorCode, CustomException
|
@@ -17,6 +18,8 @@ from .database import (
|
|
17
18
|
from .security import hash_password, verify_jwt_token, verify_role_permission
|
18
19
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
19
20
|
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
20
23
|
class BaseService(Generic[ModelType]):
|
21
24
|
##################
|
22
25
|
# 초기화 영역 #
|
@@ -55,16 +58,22 @@ class BaseService(Generic[ModelType]):
|
|
55
58
|
Tuple[Dict[str, Any], Dict[str, Any]]: (처리된 엔티티 데이터, 파일 정보)
|
56
59
|
"""
|
57
60
|
try:
|
61
|
+
logger.info(f"[파일 처리 시작] operation: {operation}, storage_dir: {storage_dir}")
|
62
|
+
logger.info(f"[엔티티 데이터] {entity_data}")
|
63
|
+
|
58
64
|
entity_data_copy = entity_data.copy()
|
59
65
|
file_infos = {}
|
60
66
|
|
61
67
|
# 파일 데이터 분리
|
62
68
|
separated_files = {}
|
63
69
|
if operation != "delete" and 'extra_data' in entity_data_copy and isinstance(entity_data_copy['extra_data'], dict):
|
70
|
+
logger.info(f"[extra_data 확인] {entity_data_copy['extra_data']}")
|
64
71
|
extra_data = entity_data_copy['extra_data'].copy()
|
65
72
|
file_fields = {k: v for k, v in extra_data.items() if k.endswith('_files')}
|
73
|
+
logger.info(f"[파일 필드 확인] {file_fields}")
|
66
74
|
|
67
75
|
if file_fields and not storage_dir:
|
76
|
+
logger.error("[에러] storage_dir이 필요하지만 제공되지 않음")
|
68
77
|
raise CustomException(
|
69
78
|
ErrorCode.INVALID_INPUT,
|
70
79
|
detail="storage_dir is required for file upload",
|
@@ -74,6 +83,7 @@ class BaseService(Generic[ModelType]):
|
|
74
83
|
# 파일 필드 분리 및 제거
|
75
84
|
for field_name, files in file_fields.items():
|
76
85
|
if files:
|
86
|
+
logger.info(f"[파일 처리] 필드: {field_name}, 파일 수: {len(files)}")
|
77
87
|
separated_files[field_name] = files
|
78
88
|
del extra_data[field_name]
|
79
89
|
|
@@ -81,6 +91,7 @@ class BaseService(Generic[ModelType]):
|
|
81
91
|
|
82
92
|
# 기존 파일 삭제 (update 또는 delete 작업 시)
|
83
93
|
if operation in ["update", "delete"]:
|
94
|
+
logger.info("[기존 파일 삭제 시작]")
|
84
95
|
from .files import FileHandler
|
85
96
|
# files 테이블에서 기존 파일 정보 조회
|
86
97
|
existing_files = await self.db_session.execute(
|
@@ -96,9 +107,11 @@ class BaseService(Generic[ModelType]):
|
|
96
107
|
}
|
97
108
|
)
|
98
109
|
existing_files = existing_files.fetchall()
|
110
|
+
logger.info(f"[기존 파일 조회 결과] {existing_files}")
|
99
111
|
|
100
112
|
# 기존 파일 삭제
|
101
113
|
for file_info in existing_files:
|
114
|
+
logger.info(f"[파일 삭제] {file_info[0]}")
|
102
115
|
await FileHandler.delete_files(file_info[0])
|
103
116
|
|
104
117
|
# files 테이블에서 레코드 삭제
|
@@ -113,11 +126,14 @@ class BaseService(Generic[ModelType]):
|
|
113
126
|
"entity_ulid": entity_result.ulid
|
114
127
|
}
|
115
128
|
)
|
129
|
+
logger.info("[기존 파일 DB 레코드 삭제 완료]")
|
116
130
|
|
117
131
|
# 새 파일 저장 (create 또는 update 작업 시)
|
118
132
|
if operation != "delete" and separated_files:
|
133
|
+
logger.info("[새 파일 저장 시작]")
|
119
134
|
from .files import FileHandler
|
120
135
|
for field_name, files in separated_files.items():
|
136
|
+
logger.info(f"[필드 처리] {field_name}, 파일 수: {len(files)}")
|
121
137
|
saved_files = await FileHandler.save_files(
|
122
138
|
files=files,
|
123
139
|
storage_dir=storage_dir,
|
@@ -127,6 +143,7 @@ class BaseService(Generic[ModelType]):
|
|
127
143
|
column_name=field_name.replace('_files', '')
|
128
144
|
)
|
129
145
|
file_infos[field_name] = saved_files
|
146
|
+
logger.info(f"[파일 저장 완료] {field_name}: {len(saved_files)}개")
|
130
147
|
|
131
148
|
# extra_data 업데이트 - 파일 정보 캐싱
|
132
149
|
if not hasattr(entity_result, 'extra_data'):
|
@@ -144,17 +161,22 @@ class BaseService(Generic[ModelType]):
|
|
144
161
|
'column_name': f['column_name']
|
145
162
|
} for f in saved_files
|
146
163
|
]
|
164
|
+
logger.info(f"[extra_data 업데이트 완료] {field_name}")
|
147
165
|
|
148
166
|
# extra_data 업데이트된 엔티티 저장
|
149
167
|
await self.db_session.flush()
|
168
|
+
logger.info("[DB flush 완료]")
|
150
169
|
|
170
|
+
logger.info("[파일 처리 완료]")
|
151
171
|
return entity_data_copy, file_infos
|
152
172
|
|
153
173
|
except CustomException as e:
|
174
|
+
logger.error(f"[CustomException 발생] {e.error_code}: {e.detail}")
|
154
175
|
if file_infos:
|
155
176
|
from .files import FileHandler
|
156
177
|
for file_list in file_infos.values():
|
157
178
|
for file_info in file_list:
|
179
|
+
logger.info(f"[에러 복구] 파일 삭제: {file_info['storage_path']}")
|
158
180
|
await FileHandler.delete_files(file_info["storage_path"])
|
159
181
|
raise CustomException(
|
160
182
|
e.error_code,
|
@@ -163,14 +185,16 @@ class BaseService(Generic[ModelType]):
|
|
163
185
|
original_error=e
|
164
186
|
)
|
165
187
|
except Exception as e:
|
188
|
+
logger.error(f"[예외 발생] {str(e)}")
|
166
189
|
if file_infos:
|
167
190
|
from .files import FileHandler
|
168
191
|
for file_list in file_infos.values():
|
169
192
|
for file_info in file_list:
|
193
|
+
logger.info(f"[에러 복구] 파일 삭제: {file_info['storage_path']}")
|
170
194
|
await FileHandler.delete_files(file_info["storage_path"])
|
171
195
|
raise CustomException(
|
172
196
|
ErrorCode.FILE_SYSTEM_ERROR,
|
173
|
-
detail=str(e),
|
197
|
+
detail=f"File processing error: {str(e)}",
|
174
198
|
source_function=f"{self.__class__.__name__}._process_files",
|
175
199
|
original_error=e
|
176
200
|
)
|
@@ -248,14 +272,27 @@ class BaseService(Generic[ModelType]):
|
|
248
272
|
return result
|
249
273
|
|
250
274
|
except CustomException as e:
|
251
|
-
raise e
|
252
|
-
except Exception as e:
|
253
275
|
raise CustomException(
|
254
|
-
|
255
|
-
detail=str(e),
|
256
|
-
source_function=f"base_service.{self.__class__.__name__}.create
|
276
|
+
e.error_code,
|
277
|
+
detail=f"{e.detail}|{str(e)}",
|
278
|
+
source_function=f"base_service.{self.__class__.__name__}.create",
|
257
279
|
original_error=e
|
258
280
|
)
|
281
|
+
except Exception as e:
|
282
|
+
if isinstance(e, str) and "FILE_SYSTEM_ERROR" in str(e):
|
283
|
+
raise CustomException(
|
284
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
285
|
+
detail=f"{str(e)}",
|
286
|
+
source_function=f"base_service.{self.__class__.__name__}.create",
|
287
|
+
original_error=e
|
288
|
+
)
|
289
|
+
else:
|
290
|
+
raise CustomException(
|
291
|
+
ErrorCode.INTERNAL_ERROR,
|
292
|
+
detail=f"{str(e)}",
|
293
|
+
source_function=f"base_service.{self.__class__.__name__}.create",
|
294
|
+
original_error=e
|
295
|
+
)
|
259
296
|
|
260
297
|
async def update(
|
261
298
|
self,
|
aiteamutils/files.py
CHANGED
@@ -8,22 +8,38 @@ from mimetypes import guess_type
|
|
8
8
|
from sqlalchemy import text
|
9
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
10
10
|
from ulid import ULID
|
11
|
+
import logging
|
11
12
|
|
12
13
|
from .exceptions import ErrorCode, CustomException
|
13
14
|
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
14
17
|
class FileHandler:
|
15
18
|
"""파일 처리를 위한 핵심 기능 제공 클래스"""
|
16
19
|
|
17
20
|
@staticmethod
|
18
21
|
def _create_directory(directory: str) -> None:
|
19
|
-
"""디렉토리가 없는 경우 생성
|
20
|
-
|
21
|
-
Args:
|
22
|
-
directory (str): 생성할 디렉토리 경로
|
23
|
-
"""
|
22
|
+
"""디렉토리가 없는 경우 생성"""
|
24
23
|
try:
|
24
|
+
logger.info(f"[디렉토리 생성 시도] {directory}")
|
25
|
+
if os.path.exists(directory):
|
26
|
+
logger.info(f"[디렉토리 이미 존재] {directory}")
|
27
|
+
return
|
28
|
+
|
25
29
|
os.makedirs(directory, exist_ok=True)
|
30
|
+
logger.info(f"[디렉토리 생성 완료] {directory}")
|
31
|
+
|
32
|
+
# 권한 확인
|
33
|
+
if not os.access(directory, os.W_OK):
|
34
|
+
logger.error(f"[권한 에러] 디렉토리에 쓰기 권한 없음: {directory}")
|
35
|
+
raise CustomException(
|
36
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
37
|
+
detail=f"{directory}|No write permission",
|
38
|
+
source_function="FileHandler._create_directory"
|
39
|
+
)
|
40
|
+
|
26
41
|
except Exception as e:
|
42
|
+
logger.error(f"[디렉토리 생성 실패] {directory}: {str(e)}")
|
27
43
|
raise CustomException(
|
28
44
|
ErrorCode.FILE_SYSTEM_ERROR,
|
29
45
|
detail=f"{directory}|{str(e)}",
|
@@ -80,19 +96,10 @@ class FileHandler:
|
|
80
96
|
entity_name: str,
|
81
97
|
entity_ulid: str
|
82
98
|
) -> Dict[str, Any]:
|
83
|
-
"""파일을 저장하고 메타데이터 반환
|
84
|
-
|
85
|
-
Args:
|
86
|
-
file (BinaryIO): 저장할 파일 객체
|
87
|
-
original_name (str): 원본 파일명
|
88
|
-
storage_dir (str): 저장 디렉토리 경로
|
89
|
-
entity_name (str): 엔티티 이름
|
90
|
-
entity_ulid (str): 엔티티 ULID
|
91
|
-
|
92
|
-
Returns:
|
93
|
-
Dict[str, Any]: 저장된 파일의 메타데이터
|
94
|
-
"""
|
99
|
+
"""파일을 저장하고 메타데이터 반환"""
|
95
100
|
try:
|
101
|
+
logger.info(f"[파일 저장 시작] 원본 파일명: {original_name}, 저장 경로: {storage_dir}")
|
102
|
+
|
96
103
|
# 저장 디렉토리 생성
|
97
104
|
FileHandler._create_directory(storage_dir)
|
98
105
|
|
@@ -100,23 +107,30 @@ class FileHandler:
|
|
100
107
|
file_ext = os.path.splitext(original_name)[1]
|
101
108
|
storage_filename = f"{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}_{entity_ulid}{file_ext}"
|
102
109
|
storage_path = os.path.join(storage_dir, storage_filename)
|
110
|
+
logger.info(f"[저장 파일명] {storage_filename}")
|
103
111
|
|
104
112
|
# 파일 저장
|
105
113
|
current_position = file.tell()
|
106
114
|
file.seek(0)
|
107
115
|
|
108
|
-
|
109
|
-
|
110
|
-
|
116
|
+
try:
|
117
|
+
async with aiofiles.open(storage_path, 'wb') as f:
|
118
|
+
while chunk := file.read(8192):
|
119
|
+
await f.write(chunk)
|
120
|
+
logger.info(f"[파일 쓰기 완료] {storage_path}")
|
121
|
+
except Exception as e:
|
122
|
+
logger.error(f"[파일 쓰기 실패] {storage_path}: {str(e)}")
|
123
|
+
raise
|
111
124
|
|
112
125
|
# 체크섬 계산
|
113
126
|
file.seek(0)
|
114
127
|
checksum = await FileHandler._calculate_checksum(file)
|
128
|
+
logger.info(f"[체크섬 계산 완료] {checksum}")
|
115
129
|
|
116
130
|
# 파일 포인터 복구
|
117
131
|
file.seek(current_position)
|
118
132
|
|
119
|
-
|
133
|
+
file_info = {
|
120
134
|
"original_name": original_name,
|
121
135
|
"storage_path": storage_path,
|
122
136
|
"mime_type": FileHandler._get_mime_type(original_name),
|
@@ -125,9 +139,14 @@ class FileHandler:
|
|
125
139
|
"entity_name": entity_name,
|
126
140
|
"entity_ulid": entity_ulid
|
127
141
|
}
|
142
|
+
logger.info(f"[파일 정보] {file_info}")
|
143
|
+
return file_info
|
144
|
+
|
128
145
|
except CustomException as e:
|
146
|
+
logger.error(f"[CustomException 발생] {e.error_code}: {e.detail}")
|
129
147
|
raise e
|
130
148
|
except Exception as e:
|
149
|
+
logger.error(f"[파일 저장 실패] {str(e)}")
|
131
150
|
raise CustomException(
|
132
151
|
ErrorCode.FILE_SYSTEM_ERROR,
|
133
152
|
detail=f"{storage_path}|{str(e)}",
|
@@ -144,24 +163,16 @@ class FileHandler:
|
|
144
163
|
db_session: AsyncSession,
|
145
164
|
column_name: str = None
|
146
165
|
) -> List[Dict[str, Any]]:
|
147
|
-
"""파일(들)을 저장하고 메타데이터 반환
|
166
|
+
"""파일(들)을 저장하고 메타데이터 반환"""
|
167
|
+
logger.info(f"[다중 파일 저장 시작] storage_dir: {storage_dir}, entity_name: {entity_name}")
|
148
168
|
|
149
|
-
Args:
|
150
|
-
files: 단일 파일 튜플 (file, original_name) 또는 파일 튜플 리스트
|
151
|
-
storage_dir (str): 저장 디렉토리 경로
|
152
|
-
entity_name (str): 엔티티 이름
|
153
|
-
entity_ulid (str): 엔티티 ULID
|
154
|
-
db_session (AsyncSession): DB 세션
|
155
|
-
column_name (str): 파일이 속한 컬럼 이름
|
156
|
-
|
157
|
-
Returns:
|
158
|
-
List[Dict[str, Any]]: 저장된 파일들의 메타데이터 리스트
|
159
|
-
"""
|
160
169
|
file_infos = []
|
161
170
|
# 단일 파일인 경우 리스트로 변환
|
162
171
|
files_list = [files] if isinstance(files, tuple) else files
|
172
|
+
logger.info(f"[처리할 파일 수] {len(files_list)}")
|
163
173
|
|
164
174
|
for file, original_name in files_list:
|
175
|
+
logger.info(f"[개별 파일 처리] {original_name}")
|
165
176
|
# 파일 저장 및 메타데이터 생성
|
166
177
|
file_info = await FileHandler._save_file(
|
167
178
|
file=file,
|
@@ -171,32 +182,39 @@ class FileHandler:
|
|
171
182
|
entity_ulid=entity_ulid
|
172
183
|
)
|
173
184
|
|
174
|
-
# DB에 파일 정보 저장
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
"
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
185
|
+
# DB에 파일 정보 저장
|
186
|
+
try:
|
187
|
+
logger.info(f"[DB 저장 시작] {original_name}")
|
188
|
+
result = await db_session.execute(
|
189
|
+
text("""
|
190
|
+
INSERT INTO files (
|
191
|
+
entity_name, entity_ulid, original_name, storage_path,
|
192
|
+
mime_type, size, checksum, column_name
|
193
|
+
) VALUES (
|
194
|
+
:entity_name, :entity_ulid, :original_name, :storage_path,
|
195
|
+
:mime_type, :size, :checksum, :column_name
|
196
|
+
) RETURNING *
|
197
|
+
"""),
|
198
|
+
{
|
199
|
+
"entity_name": entity_name,
|
200
|
+
"entity_ulid": entity_ulid,
|
201
|
+
"original_name": file_info["original_name"],
|
202
|
+
"storage_path": file_info["storage_path"],
|
203
|
+
"mime_type": file_info["mime_type"],
|
204
|
+
"size": file_info["size"],
|
205
|
+
"checksum": file_info["checksum"],
|
206
|
+
"column_name": column_name
|
207
|
+
}
|
208
|
+
)
|
209
|
+
|
210
|
+
file_info = dict(result.fetchone())
|
211
|
+
file_infos.append(file_info)
|
212
|
+
logger.info(f"[DB 저장 완료] {original_name}")
|
213
|
+
except Exception as e:
|
214
|
+
logger.error(f"[DB 저장 실패] {original_name}: {str(e)}")
|
215
|
+
raise
|
199
216
|
|
217
|
+
logger.info(f"[다중 파일 저장 완료] 성공: {len(file_infos)}개")
|
200
218
|
return file_infos
|
201
219
|
|
202
220
|
@staticmethod
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.145"
|
@@ -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=dqs0b5xyCdBibgbgHKG9LCyeyQW1MNwiwuOrmSluJHI,22349
|
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=hBCnXCa8wrBK05aSZAF08tcH_eiYmVQqUaSQqCgib7o,10311
|
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=qTfcWqtaTYCddRCJFGc8bvpJSvvkq6vXGLan238hPT4,43
|
14
|
+
aiteamutils-0.2.145.dist-info/METADATA,sha256=SSsB1r0zBl1--1_PNz-nvFAlMP1VFduSYZIwaSrKxZc,1743
|
15
|
+
aiteamutils-0.2.145.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.145.dist-info/RECORD,,
|
File without changes
|