aiteamutils 0.2.135__py3-none-any.whl → 0.2.137__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.
@@ -32,7 +32,7 @@ class BaseRepository(Generic[ModelType]):
32
32
  raise CustomException(
33
33
  ErrorCode.DB_CONNECTION_ERROR,
34
34
  detail="Session cannot be None",
35
- source_function=f"{self.__class__.__name__}.session"
35
+ source_function=f"base_repository.{self.__class__.__name__}.session"
36
36
  )
37
37
  self._session = value
38
38
 
@@ -57,7 +57,7 @@ class BaseRepository(Generic[ModelType]):
57
57
  raise CustomException(
58
58
  ErrorCode.INTERNAL_ERROR,
59
59
  detail=str(e),
60
- source_function=f"{self.__class__.__name__}.create",
60
+ source_function=f"base_repository.{self.__class__.__name__}.create",
61
61
  original_error=e
62
62
  )
63
63
 
@@ -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:
@@ -60,7 +61,7 @@ class BaseService(Generic[ModelType]):
60
61
  raise CustomException(
61
62
  ErrorCode.FORBIDDEN,
62
63
  detail=f"{role_permission}",
63
- source_function=f"{self.__class__.__name__}.create"
64
+ source_function=f"base_service.{self.__class__.__name__}.create.permission_result"
64
65
  )
65
66
 
66
67
  try:
@@ -72,25 +73,60 @@ 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"base_service.{self.__class__.__name__}.create.file_keys"
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
- return process_response(result, response_model)
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
  # 다른 예외 처리
90
126
  raise CustomException(
91
127
  ErrorCode.INTERNAL_ERROR,
92
128
  detail=str(e),
93
- source_function=f"{self.__class__.__name__}.create",
129
+ source_function=f"base_service.{self.__class__.__name__}.create",
94
130
  original_error=e
95
131
  )
96
132
 
@@ -117,7 +153,7 @@ class BaseService(Generic[ModelType]):
117
153
  raise CustomException(
118
154
  ErrorCode.INVALID_INPUT,
119
155
  detail="Either 'ulid' or 'conditions' must be provided.",
120
- source_function="database.update_entity"
156
+ source_function=f"base_service.{self.__class__.__name__}.update.ulid_or_conditions"
121
157
  )
122
158
 
123
159
  # ulid로 조건 생성
@@ -126,7 +162,7 @@ class BaseService(Generic[ModelType]):
126
162
  raise CustomException(
127
163
  ErrorCode.VALIDATION_ERROR,
128
164
  detail=ulid,
129
- source_function=f"{self.__class__.__name__}.update"
165
+ source_function=f"base_service.{self.__class__.__name__}.update.ulid_or_conditions"
130
166
  )
131
167
 
132
168
  conditions = {"ulid": ulid}
@@ -147,7 +183,7 @@ class BaseService(Generic[ModelType]):
147
183
  raise CustomException(
148
184
  ErrorCode.INTERNAL_ERROR,
149
185
  detail=str(e),
150
- source_function=f"{self.__class__.__name__}.update",
186
+ source_function=f"base_service.{self.__class__.__name__}.update",
151
187
  original_error=e
152
188
  )
153
189
 
@@ -165,14 +201,14 @@ class BaseService(Generic[ModelType]):
165
201
  raise CustomException(
166
202
  ErrorCode.VALIDATION_ERROR,
167
203
  detail=ulid,
168
- source_function=f"{self.__class__.__name__}.delete"
204
+ source_function=f"base_service.{self.__class__.__name__}.delete.ulid_validation"
169
205
  )
170
206
 
171
207
  if not ulid and not conditions:
172
208
  raise CustomException(
173
209
  ErrorCode.INVALID_INPUT,
174
210
  detail="Either 'ulid' or 'conditions' must be provided.",
175
- source_function="database.update_entity"
211
+ source_function=f"base_service.{self.__class__.__name__}.delete.ulid_or_conditions"
176
212
  )
177
213
 
178
214
  # ulid로 조건 생성
@@ -190,7 +226,7 @@ class BaseService(Generic[ModelType]):
190
226
  raise CustomException(
191
227
  ErrorCode.INTERNAL_ERROR,
192
228
  detail=str(e),
193
- source_function=f"{self.__class__.__name__}.delete",
229
+ source_function=f"base_service.{self.__class__.__name__}.delete",
194
230
  original_error=e
195
231
  )
196
232
 
@@ -246,7 +282,7 @@ class BaseService(Generic[ModelType]):
246
282
  except Exception as e:
247
283
  raise CustomException(
248
284
  ErrorCode.INTERNAL_ERROR,
249
- source_function=f"{self.__class__.__name__}.list",
285
+ source_function=f"base_service.{self.__class__.__name__}.list",
250
286
  original_error=e
251
287
  )
252
288
 
@@ -268,7 +304,7 @@ class BaseService(Generic[ModelType]):
268
304
  raise CustomException(
269
305
  ErrorCode.INVALID_INPUT,
270
306
  detail="Either 'ulid' or 'conditions' must be provided.",
271
- source_function="database.update_entity"
307
+ source_function=f"base_service.{self.__class__.__name__}.get.ulid_or_conditions"
272
308
  )
273
309
 
274
310
  # ulid로 조건 생성
@@ -277,7 +313,7 @@ class BaseService(Generic[ModelType]):
277
313
  raise CustomException(
278
314
  ErrorCode.VALIDATION_ERROR,
279
315
  detail=ulid,
280
- source_function=f"{self.__class__.__name__}.update"
316
+ source_function=f"base_service.{self.__class__.__name__}.get.ulid_validation"
281
317
  )
282
318
 
283
319
  conditions = {"ulid": ulid}
@@ -295,7 +331,7 @@ class BaseService(Generic[ModelType]):
295
331
  raise CustomException(
296
332
  ErrorCode.INTERNAL_ERROR,
297
333
  detail=str(e),
298
- source_function=f"{self.__class__.__name__}.get",
334
+ source_function=f"base_service.{self.__class__.__name__}.get",
299
335
  original_error=e
300
336
  )
301
337
 
aiteamutils/database.py CHANGED
@@ -341,7 +341,7 @@ async def update_entity(
341
341
  raise CustomException(
342
342
  ErrorCode.NOT_FOUND,
343
343
  detail=f"{model.__name__}|{conditions}.",
344
- source_function="database.update_entity"
344
+ source_function="database.update_entity.not_entity"
345
345
  )
346
346
 
347
347
  # 기존 데이터를 딕셔너리로 변환
@@ -394,7 +394,7 @@ async def delete_entity(
394
394
  raise CustomException(
395
395
  ErrorCode.NOT_FOUND,
396
396
  detail=f"{model.__name__}|{conditions}.",
397
- source_function="database.delete_entity"
397
+ source_function="database.delete_entity.not_entity"
398
398
  )
399
399
 
400
400
  entity.is_deleted = True
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.135"
2
+ __version__ = "0.2.137"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.135
3
+ Version: 0.2.137
4
4
  Summary: AI Team Utilities
5
5
  Project-URL: Homepage, https://github.com/yourusername/aiteamutils
6
6
  Project-URL: Issues, https://github.com/yourusername/aiteamutils/issues
@@ -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
- aiteamutils/base_repository.py,sha256=vzBw3g3jCJetTDblZvZenEGXk89Qu_65_02C7QTcf8Q,4615
4
- aiteamutils/base_service.py,sha256=nHikjwGp29QrQPr2W8Ye9sKxmVS_8prRG3Nu42TU1Ms,10670
3
+ aiteamutils/base_repository.py,sha256=Oy2zE1i5qx60Xf1tnsaKLyFWapiPqt5JH8NejwNrPWg,4647
4
+ aiteamutils/base_service.py,sha256=LrmntqvNx8SxgNrumkkPMv0vaKMMPVJrHzoXmDdx83M,12839
5
5
  aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
6
6
  aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
7
- aiteamutils/database.py,sha256=_aiS_akyJHHFSWZCmPcO82_O32xdUnXlNrPW5KhzxaA,20396
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=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=fdwI_s2lFV68CAZnF0FL52Cw9MKy1Mhcfci5r2bWY4o,43
13
- aiteamutils-0.2.135.dist-info/METADATA,sha256=2erJHJYmx8Vw12QGwfyXEvQC3-z77qtFUxYve85aSvo,1719
14
- aiteamutils-0.2.135.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- aiteamutils-0.2.135.dist-info/RECORD,,
13
+ aiteamutils/version.py,sha256=paOXlTEx5pKbXz3iDW_A_JPzLhKBZg6PmXu5SqD_82I,43
14
+ aiteamutils-0.2.137.dist-info/METADATA,sha256=4dfPUZv6C9Xoq8E0HbsY8ToWEx_lsCLpM8qawukEPz0,1719
15
+ aiteamutils-0.2.137.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ aiteamutils-0.2.137.dist-info/RECORD,,