aiteamutils 0.2.139__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.
aiteamutils/base_model.py CHANGED
@@ -109,6 +109,11 @@ class BaseFileModel(BaseColumn):
109
109
  nullable=False,
110
110
  doc="엔티티 ULID"
111
111
  )
112
+ column_name: Mapped[str] = mapped_column(
113
+ String,
114
+ nullable=False,
115
+ doc="컬럼 이름"
116
+ )
112
117
  original_name: Mapped[str] = mapped_column(
113
118
  String,
114
119
  nullable=False,
@@ -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,44 +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
- from .files import FileHandler
111
- for field_name, files in separated_files.items():
112
- saved_files = await FileHandler.save_files(
113
- files=files,
114
- storage_dir=storage_dir,
115
- entity_name=self.model.__tablename__,
116
- entity_ulid=result.ulid,
117
- db_session=self.db_session
118
- )
119
- file_infos[field_name] = saved_files
120
-
121
- # extra_data 업데이트
122
- if not hasattr(result, 'extra_data'):
123
- result.extra_data = {}
124
- if not result.extra_data:
125
- result.extra_data = {}
126
-
127
- result.extra_data[field_name] = [
128
- {
129
- 'original_name': f['original_name'],
130
- 'storage_path': f['storage_path'],
131
- 'mime_type': f['mime_type'],
132
- 'size': f['size'],
133
- 'checksum': f['checksum']
134
- } for f in saved_files
135
- ]
136
-
137
- # extra_data 업데이트된 엔티티 저장
138
- await self.db_session.flush()
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
+ )
139
241
 
140
242
  # 결과 반환
141
243
  if response_model:
@@ -148,22 +250,15 @@ class BaseService(Generic[ModelType]):
148
250
  return result
149
251
 
150
252
  except CustomException as e:
151
- # 파일 저장 실패 시 저장된 파일들 삭제
152
- if 'file_infos' in locals():
153
- for file_list in file_infos.values():
154
- for file_info in file_list:
155
- from .files import FileHandler
156
- await FileHandler.delete_files(file_info["storage_path"])
157
253
  raise e
158
254
  except Exception as e:
159
- # 다른 예외 처리
160
255
  raise CustomException(
161
256
  ErrorCode.INTERNAL_ERROR,
162
257
  detail=str(e),
163
258
  source_function=f"base_service.{self.__class__.__name__}.create",
164
259
  original_error=e
165
260
  )
166
-
261
+
167
262
  async def update(
168
263
  self,
169
264
  request: Request,
@@ -175,7 +270,8 @@ class BaseService(Generic[ModelType]):
175
270
  response_model: Any = None,
176
271
  org_ulid_position: str = "organization_ulid",
177
272
  role_permission: str = "update",
178
- token_settings: Dict[str, Any] | None = None
273
+ token_settings: Dict[str, Any] | None = None,
274
+ storage_dir: str | None = None
179
275
  ) -> ModelType:
180
276
  try:
181
277
  async with self.db_session.begin():
@@ -201,16 +297,47 @@ class BaseService(Generic[ModelType]):
201
297
 
202
298
  conditions = {"ulid": ulid}
203
299
 
204
- 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(
205
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,
206
320
  conditions=conditions,
207
321
  exclude_entities=exclude_entities
208
322
  )
209
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
+
210
332
  if response_model:
211
- 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
212
338
  else:
213
339
  return result
340
+
214
341
  except CustomException as e:
215
342
  raise e
216
343
  except Exception as e:
@@ -228,7 +355,8 @@ class BaseService(Generic[ModelType]):
228
355
  conditions: Dict[str, Any] | None = None,
229
356
  org_ulid_position: str = "organization_ulid",
230
357
  role_permission: str = "delete",
231
- token_settings: Dict[str, Any] | None = None
358
+ token_settings: Dict[str, Any] | None = None,
359
+ storage_dir: str | None = None
232
360
  ) -> bool:
233
361
  try:
234
362
  if not ULID.from_str(ulid):
@@ -251,6 +379,19 @@ class BaseService(Generic[ModelType]):
251
379
 
252
380
  conditions["is_deleted"] = False
253
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
+
254
395
  return await self.repository.delete(
255
396
  conditions=conditions
256
397
  )
aiteamutils/files.py CHANGED
@@ -141,7 +141,8 @@ class FileHandler:
141
141
  storage_dir: str,
142
142
  entity_name: str,
143
143
  entity_ulid: str,
144
- db_session: AsyncSession
144
+ db_session: AsyncSession,
145
+ column_name: str = None
145
146
  ) -> List[Dict[str, Any]]:
146
147
  """파일(들)을 저장하고 메타데이터 반환
147
148
 
@@ -151,6 +152,7 @@ class FileHandler:
151
152
  entity_name (str): 엔티티 이름
152
153
  entity_ulid (str): 엔티티 ULID
153
154
  db_session (AsyncSession): DB 세션
155
+ column_name (str): 파일이 속한 컬럼 이름
154
156
 
155
157
  Returns:
156
158
  List[Dict[str, Any]]: 저장된 파일들의 메타데이터 리스트
@@ -174,10 +176,10 @@ class FileHandler:
174
176
  text("""
175
177
  INSERT INTO files (
176
178
  entity_name, entity_ulid, original_name, storage_path,
177
- mime_type, size, checksum
179
+ mime_type, size, checksum, column_name
178
180
  ) VALUES (
179
181
  :entity_name, :entity_ulid, :original_name, :storage_path,
180
- :mime_type, :size, :checksum
182
+ :mime_type, :size, :checksum, :column_name
181
183
  ) RETURNING *
182
184
  """),
183
185
  {
@@ -187,7 +189,8 @@ class FileHandler:
187
189
  "storage_path": file_info["storage_path"],
188
190
  "mime_type": file_info["mime_type"],
189
191
  "size": file_info["size"],
190
- "checksum": file_info["checksum"]
192
+ "checksum": file_info["checksum"],
193
+ "column_name": column_name
191
194
  }
192
195
  )
193
196
 
aiteamutils/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  """버전 정보"""
2
- __version__ = "0.2.139"
2
+ __version__ = "0.2.141"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.139
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,16 +1,16 @@
1
1
  aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
2
- aiteamutils/base_model.py,sha256=0rs4cjnF2ea3Q2vBTj6F64BGk7ZglJsChsS7ne_R_tg,4056
2
+ aiteamutils/base_model.py,sha256=yBZqzTDF9PA4wCAvmYfG12FdVwLtxOEUCcA3z2i6fXU,4176
3
3
  aiteamutils/base_repository.py,sha256=Oy2zE1i5qx60Xf1tnsaKLyFWapiPqt5JH8NejwNrPWg,4647
4
- aiteamutils/base_service.py,sha256=QSUzdIcV88EdmjEF7vMyrN5CjKhS6HTbsoXSp8P9Gag,14432
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
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
+ 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=zSd70aa9K_B4_aJg7_D1oTM_3HSncLzFwXbLhIGYVA4,43
14
- aiteamutils-0.2.139.dist-info/METADATA,sha256=OmryJvAMZSQ69Szubzpbj94WBvAOv71nbu6eJdHyYuk,1743
15
- aiteamutils-0.2.139.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- aiteamutils-0.2.139.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,,