aiteamutils 0.2.134__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_model.py CHANGED
@@ -93,4 +93,43 @@ class BaseSchema(BaseModel):
93
93
 
94
94
  def to_dict(self) -> Dict[str, Any]:
95
95
  """모델을 딕셔너리로 변환"""
96
- return self.model_dump()
96
+ return self.model_dump()
97
+
98
+ class BaseFileModel(BaseColumn):
99
+ """파일 관련 기본 모델"""
100
+ __abstract__ = True
101
+
102
+ entity_name: Mapped[str] = mapped_column(
103
+ String,
104
+ nullable=False,
105
+ doc="엔티티 이름"
106
+ )
107
+ entity_ulid: Mapped[str] = mapped_column(
108
+ String,
109
+ nullable=False,
110
+ doc="엔티티 ULID"
111
+ )
112
+ original_name: Mapped[str] = mapped_column(
113
+ String,
114
+ nullable=False,
115
+ doc="원본 파일명"
116
+ )
117
+ storage_path: Mapped[str] = mapped_column(
118
+ String,
119
+ nullable=False,
120
+ doc="저장 경로"
121
+ )
122
+ mime_type: Mapped[str] = mapped_column(
123
+ String,
124
+ nullable=False,
125
+ doc="MIME 타입"
126
+ )
127
+ size: Mapped[int] = mapped_column(
128
+ nullable=False,
129
+ doc="파일 크기(bytes)"
130
+ )
131
+ checksum: Mapped[str] = mapped_column(
132
+ String,
133
+ nullable=False,
134
+ doc="파일 체크섬"
135
+ )
@@ -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
- 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
  # 다른 예외 처리
aiteamutils/database.py CHANGED
@@ -157,7 +157,6 @@ def process_response(
157
157
 
158
158
  # Relationship 처리 (이미 로드된 관계만 처리)
159
159
  for relationship in entity.__mapper__.relationships:
160
- print(f"\n[DEBUG] Processing relationship: {relationship.key}")
161
160
  if not relationship.key in entity.__dict__:
162
161
  continue
163
162
 
@@ -195,7 +194,6 @@ def process_response(
195
194
  # response_model에 없는 키 제거
196
195
  for key in current_keys:
197
196
  if key not in response_model.model_fields:
198
- print(f"[DEBUG] Removing key not in response model: {key}")
199
197
  result.pop(key)
200
198
  # 모델 검증 및 업데이트
201
199
  try:
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.134"
2
+ __version__ = "0.2.136"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.134
3
+ Version: 0.2.136
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
- aiteamutils/base_model.py,sha256=Y__fWJaB8kCyMQLnj88iQVanTdwwJcog13eOdGFlsN8,3129
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=nHikjwGp29QrQPr2W8Ye9sKxmVS_8prRG3Nu42TU1Ms,10670
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
- aiteamutils/database.py,sha256=2hrPy_V-n4kKC0I8MkGiS_5-7ZqKypbiYr0nekMVa4Y,20544
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=egCk8YV5GyRjVbkuh35PwKcfmp7pVNPyW7e2yjO10Gg,43
13
- aiteamutils-0.2.134.dist-info/METADATA,sha256=-DYgH2SVlCJWygI5qWqZDCt78OPLj_c5YiFOmLg-ziw,1719
14
- aiteamutils-0.2.134.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- aiteamutils-0.2.134.dist-info/RECORD,,
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,,