aiteamutils 0.2.140__py3-none-any.whl → 0.2.141__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,144 @@ 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
+ # 파일 저장 실패 시 저장된 파일들 삭제
155
+ if file_infos:
156
+ from .files import FileHandler
157
+ for file_list in file_infos.values():
158
+ for file_info in file_list:
159
+ await FileHandler.delete_files(file_info["storage_path"])
160
+ raise CustomException(
161
+ e.error_code,
162
+ detail=e.detail,
163
+ source_function=f"{self.__class__.__name__}._process_files",
164
+ original_error=e
165
+ )
166
+ except Exception as e:
167
+ # 파일 저장 실패 시 저장된 파일들 삭제
168
+ if file_infos:
169
+ from .files import FileHandler
170
+ for file_list in file_infos.values():
171
+ for file_info in file_list:
172
+ await FileHandler.delete_files(file_info["storage_path"])
173
+ raise CustomException(
174
+ ErrorCode.FILE_SYSTEM_ERROR,
175
+ detail=str(e),
176
+ source_function=f"{self.__class__.__name__}._process_files",
177
+ original_error=e
178
+ )
179
+
180
+ async def create(
181
+ self,
182
+ request: Request,
183
+ entity_data: Dict[str, Any],
184
+ response_model: Any = None,
185
+ exclude_entities: List[str] | None = None,
186
+ unique_check: List[Dict[str, Any]] | None = None,
187
+ fk_check: List[Dict[str, Any]] | None = None,
188
+ org_ulid_position: str = "organization_ulid",
189
+ role_permission: str | None = None,
190
+ token_settings: Dict[str, Any] | None = None,
191
+ storage_dir: str | None = None
192
+ ) -> ModelType:
193
+ try:
194
+ if role_permission:
195
+ permission_result = await verify_role_permission(
196
+ request=request,
197
+ role_permission=role_permission,
198
+ token_settings=token_settings,
199
+ org_ulid_position=org_ulid_position
200
+ )
201
+
202
+ if not permission_result:
203
+ raise CustomException(
204
+ ErrorCode.FORBIDDEN,
205
+ detail=f"{role_permission}",
206
+ source_function=f"base_service.{self.__class__.__name__}.create.permission_result"
207
+ )
208
+
93
209
  async with self.db_session.begin():
94
210
  # 고유 검사 수행
95
211
  if unique_check:
@@ -98,69 +214,30 @@ class BaseService(Generic[ModelType]):
98
214
  if fk_check:
99
215
  await validate_unique_fields(self.db_session, fk_check, find_value=False)
100
216
 
101
- # 엔티티 생성
217
+ # 파일 데이터 분리를 위한 임시 엔티티 생성
218
+ temp_entity = type('TempEntity', (), {'ulid': str(ULID())})()
219
+
220
+ # 파일 데이터 분리
221
+ entity_data_copy, _ = await self._process_files(
222
+ entity_data=entity_data,
223
+ entity_result=temp_entity,
224
+ storage_dir=storage_dir,
225
+ operation="create"
226
+ )
227
+
228
+ # 엔티티 생성 (파일 데이터가 제거된 상태)
102
229
  result = await self.repository.create(
103
230
  entity_data=entity_data_copy,
104
231
  exclude_entities=exclude_entities
105
232
  )
106
233
 
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
- )
234
+ # 실제 파일 처리
235
+ _, file_infos = await self._process_files(
236
+ entity_data=entity_data,
237
+ entity_result=result,
238
+ storage_dir=storage_dir,
239
+ operation="create"
240
+ )
164
241
 
165
242
  # 결과 반환
166
243
  if response_model:
@@ -173,22 +250,15 @@ class BaseService(Generic[ModelType]):
173
250
  return result
174
251
 
175
252
  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
253
  raise e
183
254
  except Exception as e:
184
- # 다른 예외 처리
185
255
  raise CustomException(
186
256
  ErrorCode.INTERNAL_ERROR,
187
257
  detail=str(e),
188
258
  source_function=f"base_service.{self.__class__.__name__}.create",
189
259
  original_error=e
190
260
  )
191
-
261
+
192
262
  async def update(
193
263
  self,
194
264
  request: Request,
@@ -200,7 +270,8 @@ class BaseService(Generic[ModelType]):
200
270
  response_model: Any = None,
201
271
  org_ulid_position: str = "organization_ulid",
202
272
  role_permission: str = "update",
203
- token_settings: Dict[str, Any] | None = None
273
+ token_settings: Dict[str, Any] | None = None,
274
+ storage_dir: str | None = None
204
275
  ) -> ModelType:
205
276
  try:
206
277
  async with self.db_session.begin():
@@ -226,16 +297,47 @@ class BaseService(Generic[ModelType]):
226
297
 
227
298
  conditions = {"ulid": ulid}
228
299
 
229
- result = await self.repository.update(
300
+ # 기존 엔티티 조회
301
+ existing_entity = await self.repository.get(conditions=conditions)
302
+ if not existing_entity:
303
+ raise CustomException(
304
+ ErrorCode.NOT_FOUND,
305
+ detail=str(ulid or conditions),
306
+ source_function=f"base_service.{self.__class__.__name__}.update.get_entity"
307
+ )
308
+
309
+ # 파일 데이터 분리
310
+ entity_data_copy, _ = await self._process_files(
230
311
  entity_data=entity_data,
312
+ entity_result=existing_entity,
313
+ storage_dir=storage_dir,
314
+ operation="update"
315
+ )
316
+
317
+ # 엔티티 수정 (파일 데이터가 제거된 상태)
318
+ result = await self.repository.update(
319
+ entity_data=entity_data_copy,
231
320
  conditions=conditions,
232
321
  exclude_entities=exclude_entities
233
322
  )
234
323
 
324
+ # 실제 파일 처리
325
+ _, file_infos = await self._process_files(
326
+ entity_data=entity_data,
327
+ entity_result=result,
328
+ storage_dir=storage_dir,
329
+ operation="update"
330
+ )
331
+
235
332
  if response_model:
236
- return process_response(result, response_model)
333
+ processed_result = process_response(result, response_model)
334
+ # 파일 정보 추가
335
+ for key, value in file_infos.items():
336
+ processed_result[key] = value
337
+ return processed_result
237
338
  else:
238
339
  return result
340
+
239
341
  except CustomException as e:
240
342
  raise e
241
343
  except Exception as e:
@@ -253,7 +355,8 @@ class BaseService(Generic[ModelType]):
253
355
  conditions: Dict[str, Any] | None = None,
254
356
  org_ulid_position: str = "organization_ulid",
255
357
  role_permission: str = "delete",
256
- token_settings: Dict[str, Any] | None = None
358
+ token_settings: Dict[str, Any] | None = None,
359
+ storage_dir: str | None = None
257
360
  ) -> bool:
258
361
  try:
259
362
  if not ULID.from_str(ulid):
@@ -276,6 +379,19 @@ class BaseService(Generic[ModelType]):
276
379
 
277
380
  conditions["is_deleted"] = False
278
381
 
382
+ # 엔티티 조회 (파일 삭제를 위해)
383
+ entity = await self.repository.get(conditions=conditions)
384
+ if not entity:
385
+ return False
386
+
387
+ # 파일 처리 (삭제)
388
+ _, _ = await self._process_files(
389
+ entity_data={},
390
+ entity_result=entity,
391
+ storage_dir=storage_dir,
392
+ operation="delete"
393
+ )
394
+
279
395
  return await self.repository.delete(
280
396
  conditions=conditions
281
397
  )
aiteamutils/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  """버전 정보"""
2
- __version__ = "0.2.140"
2
+ __version__ = "0.2.141"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.140
3
+ Version: 0.2.141
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,7 +1,7 @@
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=3M2Mnt5Woz9upNethadxrRTd8kpmxgE_uPIgYYjqTWA,20223
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
@@ -10,7 +10,7 @@ aiteamutils/exceptions.py,sha256=pgf3ersezObyl17wAO3I2fb8m9t2OzWDX1mSjwAWm2Y,160
10
10
  aiteamutils/files.py,sha256=Dfi1rYMBZwV-3GMqZ77_I4BNMpnyL-OBaj9WwZVhCbs,8999
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=0QCUDt0xcylZJRMZSBiqnFqBsyUsdARPHNxrKiKuxJs,43
14
+ aiteamutils-0.2.141.dist-info/METADATA,sha256=p5daz8rrb2jslha3DxpLf38auLU9H7tIv8va5uxj_Ak,1743
15
+ aiteamutils-0.2.141.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ aiteamutils-0.2.141.dist-info/RECORD,,