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.
@@ -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 create(
39
+ async def _process_files(
39
40
  self,
40
- request: Request,
41
41
  entity_data: Dict[str, Any],
42
- response_model: Any = None,
43
- exclude_entities: List[str] | None = None,
44
- unique_check: List[Dict[str, Any]] | None = None,
45
- fk_check: List[Dict[str, Any]] | None = None,
46
- org_ulid_position: str = "organization_ulid",
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
- separated_files = {}
59
+ file_infos = {}
71
60
 
72
- # extra_data 내의 파일 필드 분리
73
- if 'extra_data' in entity_data_copy and isinstance(entity_data_copy['extra_data'], dict):
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"base_service.{self.__class__.__name__}.create.file_fields"
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
- if separated_files:
110
- try:
111
- from .files import FileHandler
112
- for field_name, files in separated_files.items():
113
- saved_files = await FileHandler.save_files(
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
- result = await self.repository.update(
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
- return process_response(result, response_model)
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(directory),
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(storage_path),
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.140"
2
+ __version__ = "0.2.142"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.140
3
+ Version: 0.2.142
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,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=qzg7_rtnXFglg36RDsMScHkILrUbdsm67lmqicnFnlk,16051
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=Dfi1rYMBZwV-3GMqZ77_I4BNMpnyL-OBaj9WwZVhCbs,8999
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=lUYyf6EhBRHxkbymtXrWeCpibvn_CBwk1pD92zTG0TU,43
14
- aiteamutils-0.2.140.dist-info/METADATA,sha256=Y48xQbnxkdGCnlCjL7QuEfUyq4Ou0h65b6bUy3KkXck,1743
15
- aiteamutils-0.2.140.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- aiteamutils-0.2.140.dist-info/RECORD,,
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,,