aiteamutils 0.2.144__py3-none-any.whl → 0.2.146__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.
@@ -6,6 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
6
6
  from datetime import datetime
7
7
  from ulid import ULID
8
8
  from sqlalchemy import text
9
+ import logging
9
10
 
10
11
  #패키지 라이브러리
11
12
  from .exceptions import ErrorCode, CustomException
@@ -17,6 +18,8 @@ from .database import (
17
18
  from .security import hash_password, verify_jwt_token, verify_role_permission
18
19
  ModelType = TypeVar("ModelType", bound=DeclarativeBase)
19
20
 
21
+ logger = logging.getLogger(__name__)
22
+
20
23
  class BaseService(Generic[ModelType]):
21
24
  ##################
22
25
  # 초기화 영역 #
@@ -55,16 +58,23 @@ class BaseService(Generic[ModelType]):
55
58
  Tuple[Dict[str, Any], Dict[str, Any]]: (처리된 엔티티 데이터, 파일 정보)
56
59
  """
57
60
  try:
61
+ logger.info(f"[파일 처리 시작] operation: {operation}, storage_dir: {storage_dir}")
62
+ logger.info(f"[엔티티 데이터] {entity_data}")
63
+
58
64
  entity_data_copy = entity_data.copy()
59
65
  file_infos = {}
60
66
 
61
67
  # 파일 데이터 분리
62
68
  separated_files = {}
63
69
  if operation != "delete" and 'extra_data' in entity_data_copy and isinstance(entity_data_copy['extra_data'], dict):
70
+ logger.info(f"[extra_data 확인] {entity_data_copy['extra_data']}")
64
71
  extra_data = entity_data_copy['extra_data'].copy()
65
72
  file_fields = {k: v for k, v in extra_data.items() if k.endswith('_files')}
73
+ logger.info(f"[파일 필드 확인] {file_fields}")
74
+ logger.info(f"[파일 필드 타입] {[(k, type(v), type(v[0]) if isinstance(v, list) and v else None) for k, v in file_fields.items()]}")
66
75
 
67
76
  if file_fields and not storage_dir:
77
+ logger.error("[에러] storage_dir이 필요하지만 제공되지 않음")
68
78
  raise CustomException(
69
79
  ErrorCode.INVALID_INPUT,
70
80
  detail="storage_dir is required for file upload",
@@ -74,6 +84,7 @@ class BaseService(Generic[ModelType]):
74
84
  # 파일 필드 분리 및 제거
75
85
  for field_name, files in file_fields.items():
76
86
  if files:
87
+ logger.info(f"[파일 처리] 필드: {field_name}, 파일 수: {len(files)}")
77
88
  separated_files[field_name] = files
78
89
  del extra_data[field_name]
79
90
 
@@ -81,6 +92,7 @@ class BaseService(Generic[ModelType]):
81
92
 
82
93
  # 기존 파일 삭제 (update 또는 delete 작업 시)
83
94
  if operation in ["update", "delete"]:
95
+ logger.info("[기존 파일 삭제 시작]")
84
96
  from .files import FileHandler
85
97
  # files 테이블에서 기존 파일 정보 조회
86
98
  existing_files = await self.db_session.execute(
@@ -96,9 +108,11 @@ class BaseService(Generic[ModelType]):
96
108
  }
97
109
  )
98
110
  existing_files = existing_files.fetchall()
111
+ logger.info(f"[기존 파일 조회 결과] {existing_files}")
99
112
 
100
113
  # 기존 파일 삭제
101
114
  for file_info in existing_files:
115
+ logger.info(f"[파일 삭제] {file_info[0]}")
102
116
  await FileHandler.delete_files(file_info[0])
103
117
 
104
118
  # files 테이블에서 레코드 삭제
@@ -113,11 +127,14 @@ class BaseService(Generic[ModelType]):
113
127
  "entity_ulid": entity_result.ulid
114
128
  }
115
129
  )
130
+ logger.info("[기존 파일 DB 레코드 삭제 완료]")
116
131
 
117
132
  # 새 파일 저장 (create 또는 update 작업 시)
118
133
  if operation != "delete" and separated_files:
134
+ logger.info("[새 파일 저장 시작]")
119
135
  from .files import FileHandler
120
136
  for field_name, files in separated_files.items():
137
+ logger.info(f"[필드 처리] {field_name}, 파일 수: {len(files)}")
121
138
  saved_files = await FileHandler.save_files(
122
139
  files=files,
123
140
  storage_dir=storage_dir,
@@ -127,6 +144,7 @@ class BaseService(Generic[ModelType]):
127
144
  column_name=field_name.replace('_files', '')
128
145
  )
129
146
  file_infos[field_name] = saved_files
147
+ logger.info(f"[파일 저장 완료] {field_name}: {len(saved_files)}개")
130
148
 
131
149
  # extra_data 업데이트 - 파일 정보 캐싱
132
150
  if not hasattr(entity_result, 'extra_data'):
@@ -144,17 +162,22 @@ class BaseService(Generic[ModelType]):
144
162
  'column_name': f['column_name']
145
163
  } for f in saved_files
146
164
  ]
165
+ logger.info(f"[extra_data 업데이트 완료] {field_name}")
147
166
 
148
167
  # extra_data 업데이트된 엔티티 저장
149
168
  await self.db_session.flush()
169
+ logger.info("[DB flush 완료]")
150
170
 
171
+ logger.info("[파일 처리 완료]")
151
172
  return entity_data_copy, file_infos
152
173
 
153
174
  except CustomException as e:
175
+ logger.error(f"[CustomException 발생] {e.error_code}: {e.detail}")
154
176
  if file_infos:
155
177
  from .files import FileHandler
156
178
  for file_list in file_infos.values():
157
179
  for file_info in file_list:
180
+ logger.info(f"[에러 복구] 파일 삭제: {file_info['storage_path']}")
158
181
  await FileHandler.delete_files(file_info["storage_path"])
159
182
  raise CustomException(
160
183
  e.error_code,
@@ -163,10 +186,12 @@ class BaseService(Generic[ModelType]):
163
186
  original_error=e
164
187
  )
165
188
  except Exception as e:
189
+ logger.error(f"[예외 발생] {str(e)}")
166
190
  if file_infos:
167
191
  from .files import FileHandler
168
192
  for file_list in file_infos.values():
169
193
  for file_info in file_list:
194
+ logger.info(f"[에러 복구] 파일 삭제: {file_info['storage_path']}")
170
195
  await FileHandler.delete_files(file_info["storage_path"])
171
196
  raise CustomException(
172
197
  ErrorCode.FILE_SYSTEM_ERROR,
aiteamutils/files.py CHANGED
@@ -8,22 +8,39 @@ from mimetypes import guess_type
8
8
  from sqlalchemy import text
9
9
  from sqlalchemy.ext.asyncio import AsyncSession
10
10
  from ulid import ULID
11
+ import logging
11
12
 
12
13
  from .exceptions import ErrorCode, CustomException
14
+ from .base_model import BaseFileModel
15
+
16
+ logger = logging.getLogger(__name__)
13
17
 
14
18
  class FileHandler:
15
19
  """파일 처리를 위한 핵심 기능 제공 클래스"""
16
20
 
17
21
  @staticmethod
18
22
  def _create_directory(directory: str) -> None:
19
- """디렉토리가 없는 경우 생성
20
-
21
- Args:
22
- directory (str): 생성할 디렉토리 경로
23
- """
23
+ """디렉토리가 없는 경우 생성"""
24
24
  try:
25
+ logger.info(f"[디렉토리 생성 시도] {directory}")
26
+ if os.path.exists(directory):
27
+ logger.info(f"[디렉토리 이미 존재] {directory}")
28
+ return
29
+
25
30
  os.makedirs(directory, exist_ok=True)
31
+ logger.info(f"[디렉토리 생성 완료] {directory}")
32
+
33
+ # 권한 확인
34
+ if not os.access(directory, os.W_OK):
35
+ logger.error(f"[권한 에러] 디렉토리에 쓰기 권한 없음: {directory}")
36
+ raise CustomException(
37
+ ErrorCode.FILE_SYSTEM_ERROR,
38
+ detail=f"{directory}|No write permission",
39
+ source_function="FileHandler._create_directory"
40
+ )
41
+
26
42
  except Exception as e:
43
+ logger.error(f"[디렉토리 생성 실패] {directory}: {str(e)}")
27
44
  raise CustomException(
28
45
  ErrorCode.FILE_SYSTEM_ERROR,
29
46
  detail=f"{directory}|{str(e)}",
@@ -80,19 +97,10 @@ class FileHandler:
80
97
  entity_name: str,
81
98
  entity_ulid: str
82
99
  ) -> 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
- """
100
+ """파일을 저장하고 메타데이터 반환"""
95
101
  try:
102
+ logger.info(f"[파일 저장 시작] 원본 파일명: {original_name}, 저장 경로: {storage_dir}")
103
+
96
104
  # 저장 디렉토리 생성
97
105
  FileHandler._create_directory(storage_dir)
98
106
 
@@ -100,23 +108,30 @@ class FileHandler:
100
108
  file_ext = os.path.splitext(original_name)[1]
101
109
  storage_filename = f"{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}_{entity_ulid}{file_ext}"
102
110
  storage_path = os.path.join(storage_dir, storage_filename)
111
+ logger.info(f"[저장 파일명] {storage_filename}")
103
112
 
104
113
  # 파일 저장
105
114
  current_position = file.tell()
106
115
  file.seek(0)
107
116
 
108
- async with aiofiles.open(storage_path, 'wb') as f:
109
- while chunk := file.read(8192):
110
- await f.write(chunk)
117
+ try:
118
+ async with aiofiles.open(storage_path, 'wb') as f:
119
+ while chunk := file.read(8192):
120
+ await f.write(chunk)
121
+ logger.info(f"[파일 쓰기 완료] {storage_path}")
122
+ except Exception as e:
123
+ logger.error(f"[파일 쓰기 실패] {storage_path}: {str(e)}")
124
+ raise
111
125
 
112
126
  # 체크섬 계산
113
127
  file.seek(0)
114
128
  checksum = await FileHandler._calculate_checksum(file)
129
+ logger.info(f"[체크섬 계산 완료] {checksum}")
115
130
 
116
131
  # 파일 포인터 복구
117
132
  file.seek(current_position)
118
133
 
119
- return {
134
+ file_info = {
120
135
  "original_name": original_name,
121
136
  "storage_path": storage_path,
122
137
  "mime_type": FileHandler._get_mime_type(original_name),
@@ -125,9 +140,14 @@ class FileHandler:
125
140
  "entity_name": entity_name,
126
141
  "entity_ulid": entity_ulid
127
142
  }
143
+ logger.info(f"[파일 정보] {file_info}")
144
+ return file_info
145
+
128
146
  except CustomException as e:
147
+ logger.error(f"[CustomException 발생] {e.error_code}: {e.detail}")
129
148
  raise e
130
149
  except Exception as e:
150
+ logger.error(f"[파일 저장 실패] {str(e)}")
131
151
  raise CustomException(
132
152
  ErrorCode.FILE_SYSTEM_ERROR,
133
153
  detail=f"{storage_path}|{str(e)}",
@@ -144,59 +164,84 @@ class FileHandler:
144
164
  db_session: AsyncSession,
145
165
  column_name: str = None
146
166
  ) -> List[Dict[str, Any]]:
147
- """파일(들)을 저장하고 메타데이터 반환
167
+ """파일(들)을 저장하고 메타데이터 반환"""
168
+ logger.info(f"[다중 파일 저장 시작] storage_dir: {storage_dir}, entity_name: {entity_name}")
169
+ logger.info(f"[파일 데이터 타입] files type: {type(files)}")
170
+ if isinstance(files, list):
171
+ logger.info(f"[파일 리스트 내용] {[(type(f[0]), f[1]) for f in files]}")
172
+ else:
173
+ logger.info(f"[단일 파일 내용] {(type(files[0]), files[1])}")
148
174
 
149
- Args:
150
- files: 단일 파일 튜플 (file, original_name) 또는 파일 튜플 리스트
151
- storage_dir (str): 저장 디렉토리 경로
152
- entity_name (str): 엔티티 이름
153
- entity_ulid (str): 엔티티 ULID
154
- db_session (AsyncSession): DB 세션
155
- column_name (str): 파일이 속한 컬럼 이름
156
-
157
- Returns:
158
- List[Dict[str, Any]]: 저장된 파일들의 메타데이터 리스트
159
- """
160
175
  file_infos = []
161
176
  # 단일 파일인 경우 리스트로 변환
162
177
  files_list = [files] if isinstance(files, tuple) else files
178
+ logger.info(f"[처리할 파일 수] {len(files_list)}")
163
179
 
164
180
  for file, original_name in files_list:
165
- # 파일 저장 및 메타데이터 생성
166
- file_info = await FileHandler._save_file(
167
- file=file,
168
- original_name=original_name,
169
- storage_dir=storage_dir,
170
- entity_name=entity_name,
171
- entity_ulid=entity_ulid
172
- )
173
-
174
- # DB에 파일 정보 저장 (트랜잭션은 BaseService에서 관리)
175
- result = await db_session.execute(
176
- text("""
177
- INSERT INTO files (
178
- entity_name, entity_ulid, original_name, storage_path,
179
- mime_type, size, checksum, column_name
180
- ) VALUES (
181
- :entity_name, :entity_ulid, :original_name, :storage_path,
182
- :mime_type, :size, :checksum, :column_name
183
- ) RETURNING *
184
- """),
185
- {
186
- "entity_name": entity_name,
187
- "entity_ulid": entity_ulid,
188
- "original_name": file_info["original_name"],
189
- "storage_path": file_info["storage_path"],
190
- "mime_type": file_info["mime_type"],
191
- "size": file_info["size"],
192
- "checksum": file_info["checksum"],
193
- "column_name": column_name
194
- }
195
- )
196
-
197
- file_info = dict(result.fetchone())
198
- file_infos.append(file_info)
199
-
181
+ try:
182
+ logger.info(f"[개별 파일 처리 시작] {original_name}")
183
+ logger.info(f"[파일 객체 정보] type: {type(file)}, seekable: {file.seekable()}, readable: {file.readable()}")
184
+
185
+ # 파일 저장 및 메타데이터 생성
186
+ file_info = await FileHandler._save_file(
187
+ file=file,
188
+ original_name=original_name,
189
+ storage_dir=storage_dir,
190
+ entity_name=entity_name,
191
+ entity_ulid=entity_ulid
192
+ )
193
+
194
+ # DB에 파일 정보 저장
195
+ try:
196
+ logger.info(f"[DB 저장 시작] {original_name}")
197
+
198
+ # FileModel 인스턴스 생성
199
+ file_record = BaseFileModel(
200
+ entity_name=entity_name,
201
+ entity_ulid=entity_ulid,
202
+ original_name=file_info["original_name"],
203
+ storage_path=file_info["storage_path"],
204
+ mime_type=file_info["mime_type"],
205
+ size=file_info["size"],
206
+ checksum=file_info["checksum"],
207
+ column_name=column_name
208
+ )
209
+
210
+ # DB에 저장
211
+ db_session.add(file_record)
212
+ await db_session.flush()
213
+
214
+ # 결과를 딕셔너리로 변환
215
+ file_info = {
216
+ "ulid": file_record.ulid,
217
+ "entity_name": file_record.entity_name,
218
+ "entity_ulid": file_record.entity_ulid,
219
+ "original_name": file_record.original_name,
220
+ "storage_path": file_record.storage_path,
221
+ "mime_type": file_record.mime_type,
222
+ "size": file_record.size,
223
+ "checksum": file_record.checksum,
224
+ "column_name": file_record.column_name
225
+ }
226
+
227
+ file_infos.append(file_info)
228
+ logger.info(f"[DB 저장 완료] {original_name}, ulid: {file_record.ulid}")
229
+
230
+ except Exception as e:
231
+ logger.error(f"[DB 저장 실패] {original_name}: {str(e)}")
232
+ # 파일 삭제 시도
233
+ try:
234
+ if "storage_path" in file_info:
235
+ await FileHandler.delete_files(file_info["storage_path"])
236
+ logger.info(f"[저장 실패로 인한 파일 삭제] {file_info['storage_path']}")
237
+ except Exception as del_e:
238
+ logger.error(f"[파일 삭제 실패] {str(del_e)}")
239
+ raise
240
+ except Exception as e:
241
+ logger.error(f"[파일 처리 실패] {original_name}: {str(e)}")
242
+ raise
243
+
244
+ logger.info(f"[다중 파일 저장 완료] 성공: {len(file_infos)}개")
200
245
  return file_infos
201
246
 
202
247
  @staticmethod
aiteamutils/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  """버전 정보"""
2
- __version__ = "0.2.144"
2
+ __version__ = "0.2.146"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.144
3
+ Version: 0.2.146
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=Q80ojzdOEBT2EwIvh83w-34bmoJrQhQEZSLgHZ5VZhs,20725
4
+ aiteamutils/base_service.py,sha256=r7sAETymMMvKUjYFo8To3EnnIIOPFQ7PGI_IzuNx5-o,22504
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=1XuLAvsQbnURHSZ2IyZIXrZH6OWgnC5Tl1vcTDyi1GY,9017
10
+ aiteamutils/files.py,sha256=Kvm3nIUZ6NJjjTvHX-sXeLTxgH-k1jJq_Twqvur14Hw,11823
11
11
  aiteamutils/security.py,sha256=McUl3t5Z5SyUDVUHymHdDkYyF4YSeg4g9fFMML4W6Kw,11630
12
12
  aiteamutils/validators.py,sha256=_WHN6jqJQzKM5uPTg-Da8U2qqevS84XeKMkCCF4C_lY,9591
13
- aiteamutils/version.py,sha256=ABHmb_QXO44fuZdnlGt_GXa9dIXPlE7GFyR_F1eCXBY,43
14
- aiteamutils-0.2.144.dist-info/METADATA,sha256=AlNST7gAdkpc_zugXaX2rr0O-4h9LFIwf2plP5Fm4hQ,1743
15
- aiteamutils-0.2.144.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- aiteamutils-0.2.144.dist-info/RECORD,,
13
+ aiteamutils/version.py,sha256=ClTCT30rXE2oKBHZRmAcdozSNQ3LRVgasEyLlocl1QA,43
14
+ aiteamutils-0.2.146.dist-info/METADATA,sha256=vHpFbI0TxUminJyv0a0Hs5TPhslR8nh9LqnojZ-Ujgc,1743
15
+ aiteamutils-0.2.146.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ aiteamutils-0.2.146.dist-info/RECORD,,