aiteamutils 0.2.135__py3-none-any.whl → 0.2.136__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 +38 -2
- aiteamutils/files.py +255 -0
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.135.dist-info → aiteamutils-0.2.136.dist-info}/METADATA +1 -1
- {aiteamutils-0.2.135.dist-info → aiteamutils-0.2.136.dist-info}/RECORD +6 -5
- {aiteamutils-0.2.135.dist-info → aiteamutils-0.2.136.dist-info}/WHEEL +0 -0
aiteamutils/base_service.py
CHANGED
@@ -45,7 +45,8 @@ class BaseService(Generic[ModelType]):
|
|
45
45
|
fk_check: List[Dict[str, Any]] | None = None,
|
46
46
|
org_ulid_position: str = "organization_ulid",
|
47
47
|
role_permission: str | None = None,
|
48
|
-
token_settings: Dict[str, Any] | None = None
|
48
|
+
token_settings: Dict[str, Any] | None = None,
|
49
|
+
storage_dir: str | None = None
|
49
50
|
) -> ModelType:
|
50
51
|
|
51
52
|
if role_permission:
|
@@ -72,18 +73,53 @@ class BaseService(Generic[ModelType]):
|
|
72
73
|
if fk_check:
|
73
74
|
await validate_unique_fields(self.db_session, fk_check, find_value=False)
|
74
75
|
|
76
|
+
# _files로 끝나는 키 처리
|
77
|
+
file_keys = [key for key in entity_data.keys() if key.endswith('_files')]
|
78
|
+
file_infos = {}
|
79
|
+
|
80
|
+
if file_keys and not storage_dir:
|
81
|
+
raise CustomException(
|
82
|
+
ErrorCode.INVALID_INPUT,
|
83
|
+
detail="storage_dir is required for file upload",
|
84
|
+
source_function=f"{self.__class__.__name__}.create"
|
85
|
+
)
|
86
|
+
|
87
|
+
# 먼저 엔티티를 생성하여 ULID를 얻음
|
75
88
|
result = await self.repository.create(
|
76
89
|
entity_data=entity_data,
|
77
90
|
exclude_entities=exclude_entities
|
78
91
|
)
|
92
|
+
|
93
|
+
# 파일 처리
|
94
|
+
for file_key in file_keys:
|
95
|
+
files_data = entity_data.pop(file_key) # 파일 데이터 추출 및 제거
|
96
|
+
if files_data:
|
97
|
+
from .files import FileHandler
|
98
|
+
file_infos[file_key] = await FileHandler.save_files(
|
99
|
+
files=files_data,
|
100
|
+
storage_dir=storage_dir,
|
101
|
+
entity_name=self.model.__tablename__,
|
102
|
+
entity_ulid=result.ulid,
|
103
|
+
db_session=self.db_session
|
104
|
+
)
|
79
105
|
|
80
106
|
# 결과 반환
|
81
107
|
if response_model:
|
82
|
-
|
108
|
+
processed_result = process_response(result, response_model)
|
109
|
+
# 파일 정보 추가
|
110
|
+
for key, value in file_infos.items():
|
111
|
+
processed_result[key] = value
|
112
|
+
return processed_result
|
83
113
|
else:
|
84
114
|
return result
|
85
115
|
|
86
116
|
except CustomException as e:
|
117
|
+
# 파일 저장 실패 시 저장된 파일들 삭제
|
118
|
+
if 'file_infos' in locals():
|
119
|
+
for file_list in file_infos.values():
|
120
|
+
for file_info in file_list:
|
121
|
+
from .files import FileHandler
|
122
|
+
await FileHandler.delete_files(file_info["storage_path"])
|
87
123
|
raise e
|
88
124
|
except Exception as e:
|
89
125
|
# 다른 예외 처리
|
aiteamutils/files.py
ADDED
@@ -0,0 +1,255 @@
|
|
1
|
+
import os
|
2
|
+
import hashlib
|
3
|
+
import aiofiles
|
4
|
+
from datetime import datetime, timezone
|
5
|
+
from typing import BinaryIO, Dict, Any, List, Tuple, Union
|
6
|
+
from pathlib import Path
|
7
|
+
from mimetypes import guess_type
|
8
|
+
from sqlalchemy import text
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
10
|
+
from ulid import ULID
|
11
|
+
|
12
|
+
from .exceptions import ErrorCode, CustomException
|
13
|
+
|
14
|
+
class FileHandler:
|
15
|
+
"""파일 처리를 위한 핵심 기능 제공 클래스"""
|
16
|
+
|
17
|
+
@staticmethod
|
18
|
+
def _create_directory(directory: str) -> None:
|
19
|
+
"""디렉토리가 없는 경우 생성
|
20
|
+
|
21
|
+
Args:
|
22
|
+
directory (str): 생성할 디렉토리 경로
|
23
|
+
"""
|
24
|
+
try:
|
25
|
+
os.makedirs(directory, exist_ok=True)
|
26
|
+
except Exception as e:
|
27
|
+
raise CustomException(
|
28
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
29
|
+
detail=str(directory),
|
30
|
+
source_function="FileHandler._create_directory",
|
31
|
+
original_error=e
|
32
|
+
)
|
33
|
+
|
34
|
+
@staticmethod
|
35
|
+
async def _calculate_checksum(file: BinaryIO) -> str:
|
36
|
+
"""파일의 SHA-256 체크섬 계산
|
37
|
+
|
38
|
+
Args:
|
39
|
+
file (BinaryIO): 체크섬을 계산할 파일 객체
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
str: 계산된 체크섬 값
|
43
|
+
"""
|
44
|
+
try:
|
45
|
+
sha256_hash = hashlib.sha256()
|
46
|
+
current_position = file.tell()
|
47
|
+
file.seek(0)
|
48
|
+
|
49
|
+
for chunk in iter(lambda: file.read(4096), b""):
|
50
|
+
sha256_hash.update(chunk)
|
51
|
+
|
52
|
+
file.seek(current_position)
|
53
|
+
return sha256_hash.hexdigest()
|
54
|
+
except Exception as e:
|
55
|
+
raise CustomException(
|
56
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
57
|
+
detail="checksum calculation failed",
|
58
|
+
source_function="FileHandler._calculate_checksum",
|
59
|
+
original_error=e
|
60
|
+
)
|
61
|
+
|
62
|
+
@staticmethod
|
63
|
+
def _get_mime_type(filename: str) -> str:
|
64
|
+
"""파일의 MIME 타입 추측
|
65
|
+
|
66
|
+
Args:
|
67
|
+
filename (str): MIME 타입을 추측할 파일명
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
str: 추측된 MIME 타입
|
71
|
+
"""
|
72
|
+
mime_type, _ = guess_type(filename)
|
73
|
+
return mime_type or "application/octet-stream"
|
74
|
+
|
75
|
+
@staticmethod
|
76
|
+
async def _save_file(
|
77
|
+
file: BinaryIO,
|
78
|
+
original_name: str,
|
79
|
+
storage_dir: str,
|
80
|
+
entity_name: str,
|
81
|
+
entity_ulid: str
|
82
|
+
) -> 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
|
+
"""
|
95
|
+
try:
|
96
|
+
# 저장 디렉토리 생성
|
97
|
+
FileHandler._create_directory(storage_dir)
|
98
|
+
|
99
|
+
# 파일 메타데이터 준비
|
100
|
+
file_ext = os.path.splitext(original_name)[1]
|
101
|
+
storage_filename = f"{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}_{entity_ulid}{file_ext}"
|
102
|
+
storage_path = os.path.join(storage_dir, storage_filename)
|
103
|
+
|
104
|
+
# 파일 저장
|
105
|
+
current_position = file.tell()
|
106
|
+
file.seek(0)
|
107
|
+
|
108
|
+
async with aiofiles.open(storage_path, 'wb') as f:
|
109
|
+
while chunk := file.read(8192):
|
110
|
+
await f.write(chunk)
|
111
|
+
|
112
|
+
# 체크섬 계산
|
113
|
+
file.seek(0)
|
114
|
+
checksum = await FileHandler._calculate_checksum(file)
|
115
|
+
|
116
|
+
# 파일 포인터 복구
|
117
|
+
file.seek(current_position)
|
118
|
+
|
119
|
+
return {
|
120
|
+
"original_name": original_name,
|
121
|
+
"storage_path": storage_path,
|
122
|
+
"mime_type": FileHandler._get_mime_type(original_name),
|
123
|
+
"size": os.path.getsize(storage_path),
|
124
|
+
"checksum": checksum,
|
125
|
+
"entity_name": entity_name,
|
126
|
+
"entity_ulid": entity_ulid
|
127
|
+
}
|
128
|
+
except CustomException as e:
|
129
|
+
raise e
|
130
|
+
except Exception as e:
|
131
|
+
raise CustomException(
|
132
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
133
|
+
detail=str(storage_path),
|
134
|
+
source_function="FileHandler._save_file",
|
135
|
+
original_error=e
|
136
|
+
)
|
137
|
+
|
138
|
+
@staticmethod
|
139
|
+
async def save_files(
|
140
|
+
files: Union[Tuple[BinaryIO, str], List[Tuple[BinaryIO, str]]],
|
141
|
+
storage_dir: str,
|
142
|
+
entity_name: str,
|
143
|
+
entity_ulid: str,
|
144
|
+
db_session: AsyncSession
|
145
|
+
) -> List[Dict[str, Any]]:
|
146
|
+
"""파일(들)을 저장하고 메타데이터 반환
|
147
|
+
|
148
|
+
Args:
|
149
|
+
files: 단일 파일 튜플 (file, original_name) 또는 파일 튜플 리스트
|
150
|
+
storage_dir (str): 저장 디렉토리 경로
|
151
|
+
entity_name (str): 엔티티 이름
|
152
|
+
entity_ulid (str): 엔티티 ULID
|
153
|
+
db_session (AsyncSession): DB 세션
|
154
|
+
|
155
|
+
Returns:
|
156
|
+
List[Dict[str, Any]]: 저장된 파일들의 메타데이터 리스트
|
157
|
+
"""
|
158
|
+
file_infos = []
|
159
|
+
# 단일 파일인 경우 리스트로 변환
|
160
|
+
files_list = [files] if isinstance(files, tuple) else files
|
161
|
+
|
162
|
+
for file, original_name in files_list:
|
163
|
+
# 파일 저장 및 메타데이터 생성
|
164
|
+
file_info = await FileHandler._save_file(
|
165
|
+
file=file,
|
166
|
+
original_name=original_name,
|
167
|
+
storage_dir=storage_dir,
|
168
|
+
entity_name=entity_name,
|
169
|
+
entity_ulid=entity_ulid
|
170
|
+
)
|
171
|
+
|
172
|
+
# DB에 파일 정보 저장 (트랜잭션은 BaseService에서 관리)
|
173
|
+
result = await db_session.execute(
|
174
|
+
text("""
|
175
|
+
INSERT INTO files (
|
176
|
+
entity_name, entity_ulid, original_name, storage_path,
|
177
|
+
mime_type, size, checksum
|
178
|
+
) VALUES (
|
179
|
+
:entity_name, :entity_ulid, :original_name, :storage_path,
|
180
|
+
:mime_type, :size, :checksum
|
181
|
+
) RETURNING *
|
182
|
+
"""),
|
183
|
+
{
|
184
|
+
"entity_name": entity_name,
|
185
|
+
"entity_ulid": entity_ulid,
|
186
|
+
"original_name": file_info["original_name"],
|
187
|
+
"storage_path": file_info["storage_path"],
|
188
|
+
"mime_type": file_info["mime_type"],
|
189
|
+
"size": file_info["size"],
|
190
|
+
"checksum": file_info["checksum"]
|
191
|
+
}
|
192
|
+
)
|
193
|
+
|
194
|
+
file_info = dict(result.fetchone())
|
195
|
+
file_infos.append(file_info)
|
196
|
+
|
197
|
+
return file_infos
|
198
|
+
|
199
|
+
@staticmethod
|
200
|
+
async def delete_files(storage_paths: Union[str, List[str]]) -> None:
|
201
|
+
"""파일(들) 삭제
|
202
|
+
|
203
|
+
Args:
|
204
|
+
storage_paths: 단일 파일 경로 또는 파일 경로 리스트
|
205
|
+
"""
|
206
|
+
paths_list = [storage_paths] if isinstance(storage_paths, str) else storage_paths
|
207
|
+
for storage_path in paths_list:
|
208
|
+
await FileHandler._delete_file(storage_path)
|
209
|
+
|
210
|
+
@staticmethod
|
211
|
+
async def _delete_file(storage_path: str) -> None:
|
212
|
+
"""파일 삭제 (내부 사용)
|
213
|
+
|
214
|
+
Args:
|
215
|
+
storage_path (str): 삭제할 파일 경로
|
216
|
+
"""
|
217
|
+
try:
|
218
|
+
if os.path.exists(storage_path):
|
219
|
+
os.remove(storage_path)
|
220
|
+
except Exception as e:
|
221
|
+
raise CustomException(
|
222
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
223
|
+
detail=str(storage_path),
|
224
|
+
source_function="FileHandler._delete_file",
|
225
|
+
original_error=e
|
226
|
+
)
|
227
|
+
|
228
|
+
@staticmethod
|
229
|
+
async def read_file(storage_path: str) -> BinaryIO:
|
230
|
+
"""파일 읽기
|
231
|
+
|
232
|
+
Args:
|
233
|
+
storage_path (str): 읽을 파일 경로
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
BinaryIO: 파일 객체
|
237
|
+
"""
|
238
|
+
try:
|
239
|
+
if not os.path.exists(storage_path):
|
240
|
+
raise CustomException(
|
241
|
+
ErrorCode.FILE_NOT_FOUND,
|
242
|
+
detail=str(storage_path),
|
243
|
+
source_function="FileHandler.read_file"
|
244
|
+
)
|
245
|
+
|
246
|
+
return open(storage_path, 'rb')
|
247
|
+
except CustomException as e:
|
248
|
+
raise e
|
249
|
+
except Exception as e:
|
250
|
+
raise CustomException(
|
251
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
252
|
+
detail=str(storage_path),
|
253
|
+
source_function="FileHandler.read_file",
|
254
|
+
original_error=e
|
255
|
+
)
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.136"
|
@@ -1,15 +1,16 @@
|
|
1
1
|
aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
|
2
2
|
aiteamutils/base_model.py,sha256=0rs4cjnF2ea3Q2vBTj6F64BGk7ZglJsChsS7ne_R_tg,4056
|
3
3
|
aiteamutils/base_repository.py,sha256=vzBw3g3jCJetTDblZvZenEGXk89Qu_65_02C7QTcf8Q,4615
|
4
|
-
aiteamutils/base_service.py,sha256=
|
4
|
+
aiteamutils/base_service.py,sha256=jWN0QTIIe5SkNitA_EP2GF0t1tyrCm1VftQtR3hJMT8,12507
|
5
5
|
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
6
|
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
7
|
aiteamutils/database.py,sha256=_aiS_akyJHHFSWZCmPcO82_O32xdUnXlNrPW5KhzxaA,20396
|
8
8
|
aiteamutils/enums.py,sha256=7WLqlcJqQWtETAga2WAxNp3dJTQIAd2TW-4WzkoHHa8,2498
|
9
9
|
aiteamutils/exceptions.py,sha256=pgf3ersezObyl17wAO3I2fb8m9t2OzWDX1mSjwAWm2Y,16035
|
10
|
+
aiteamutils/files.py,sha256=tdvivl3XLNv7Al7H1gGFczmrHM8XlQpiZsEc2xQ_UTU,8829
|
10
11
|
aiteamutils/security.py,sha256=McUl3t5Z5SyUDVUHymHdDkYyF4YSeg4g9fFMML4W6Kw,11630
|
11
12
|
aiteamutils/validators.py,sha256=msOrha36xWsapm4VAh63YmFq1GVyC9tzZcjXYFCEZ_g,11949
|
12
|
-
aiteamutils/version.py,sha256=
|
13
|
-
aiteamutils-0.2.
|
14
|
-
aiteamutils-0.2.
|
15
|
-
aiteamutils-0.2.
|
13
|
+
aiteamutils/version.py,sha256=GEzMhIGk9CTUp2zEaaKPPRMurlXlp9MeMYAAzWYudjo,43
|
14
|
+
aiteamutils-0.2.136.dist-info/METADATA,sha256=qXiXhvObJ6S8Xlm7NrCwLUMQn6cTLZXnJa-IEDgZ7PY,1719
|
15
|
+
aiteamutils-0.2.136.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.136.dist-info/RECORD,,
|
File without changes
|