aiteamutils 0.2.58__py3-none-any.whl → 0.2.59__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_service.py +142 -553
- aiteamutils/dependencies.py +3 -92
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.58.dist-info → aiteamutils-0.2.59.dist-info}/METADATA +1 -1
- {aiteamutils-0.2.58.dist-info → aiteamutils-0.2.59.dist-info}/RECORD +6 -6
- {aiteamutils-0.2.58.dist-info → aiteamutils-0.2.59.dist-info}/WHEEL +0 -0
aiteamutils/base_service.py
CHANGED
@@ -1,393 +1,109 @@
|
|
1
1
|
"""기본 서비스 모듈."""
|
2
2
|
from datetime import datetime
|
3
|
-
from typing import TypeVar, Generic, Dict, Any, List, Optional, Type
|
4
|
-
from sqlalchemy.
|
5
|
-
from sqlalchemy.
|
3
|
+
from typing import TypeVar, Generic, Dict, Any, List, Optional, Type
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
5
|
+
from sqlalchemy.orm import DeclarativeBase
|
6
|
+
from fastapi import Request
|
7
|
+
|
6
8
|
from .database import DatabaseService
|
7
9
|
from .exceptions import CustomException, ErrorCode
|
8
10
|
from .base_repository import BaseRepository
|
9
|
-
from .security import hash_password
|
10
|
-
from fastapi import Request
|
11
|
-
from ulid import ULID
|
12
|
-
from sqlalchemy import select
|
13
11
|
|
14
12
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
15
13
|
|
16
14
|
class BaseService(Generic[ModelType]):
|
17
|
-
|
18
|
-
##################
|
19
|
-
# 1. 초기화 영역 #
|
20
|
-
##################
|
21
15
|
def __init__(
|
22
16
|
self,
|
23
|
-
|
24
|
-
|
17
|
+
db: DatabaseService,
|
18
|
+
repository: Optional[BaseRepository] = None,
|
19
|
+
request: Optional[Request] = None
|
25
20
|
):
|
26
|
-
"""
|
27
|
-
Args:
|
28
|
-
repository (BaseRepository[ModelType]): 레포지토리 인스턴스
|
29
|
-
additional_models (Dict[str, Type[DeclarativeBase]], optional): 추가 모델 매핑. Defaults to None.
|
30
|
-
"""
|
31
|
-
self.repository = repository
|
32
|
-
self.model = repository.model
|
33
|
-
self.additional_models = additional_models or {}
|
34
|
-
self._session = None
|
35
|
-
self.searchable_fields = {
|
36
|
-
"name": {"type": "text", "description": "이름"},
|
37
|
-
"organization_ulid": {"type": "exact", "description": "조직 ID"}
|
38
|
-
}
|
39
|
-
|
40
|
-
@property
|
41
|
-
def session(self):
|
42
|
-
"""현재 세션을 반환합니다."""
|
43
|
-
if self._session is None:
|
44
|
-
raise CustomException(
|
45
|
-
ErrorCode.DB_CONNECTION_ERROR,
|
46
|
-
detail="Database session is not set",
|
47
|
-
source_function=f"{self.__class__.__name__}.session"
|
48
|
-
)
|
49
|
-
return self._session
|
50
|
-
|
51
|
-
@session.setter
|
52
|
-
def session(self, value):
|
53
|
-
"""세션을 설정합니다."""
|
54
|
-
self._session = value
|
55
|
-
if hasattr(self.repository, 'session'):
|
56
|
-
self.repository.session = value
|
57
|
-
|
58
|
-
#########################
|
59
|
-
# 2. 이벤트 처리 메서드 #
|
60
|
-
#########################
|
61
|
-
async def pre_save(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
62
|
-
"""저장 전 처리를 수행합니다.
|
63
|
-
|
64
|
-
Args:
|
65
|
-
data (Dict[str, Any]): 저장할 데이터
|
66
|
-
|
67
|
-
Returns:
|
68
|
-
Dict[str, Any]: 처리된 데이터
|
69
|
-
"""
|
70
|
-
return data
|
71
|
-
|
72
|
-
async def post_save(self, entity: ModelType) -> None:
|
73
|
-
"""저장 후 처리를 수행합니다.
|
74
|
-
|
75
|
-
Args:
|
76
|
-
entity (ModelType): 저장된 엔티티
|
77
|
-
"""
|
78
|
-
pass
|
79
|
-
|
80
|
-
async def pre_delete(self, ulid: str) -> None:
|
81
|
-
"""삭제 전 처리를 수행합니다.
|
82
|
-
|
83
|
-
Args:
|
84
|
-
ulid (str): 삭제할 엔티티의 ULID
|
85
|
-
"""
|
86
|
-
pass
|
87
|
-
|
88
|
-
async def post_delete(self, ulid: str) -> None:
|
89
|
-
"""삭제 후 처리를 수행합니다.
|
90
|
-
|
91
|
-
Args:
|
92
|
-
ulid (str): 삭제된 엔티티의 ULID
|
93
|
-
"""
|
94
|
-
pass
|
95
|
-
|
96
|
-
######################
|
97
|
-
# 3. 캐시 관리 메서드 #
|
98
|
-
######################
|
99
|
-
async def get_from_cache(self, key: str) -> Optional[Any]:
|
100
|
-
"""캐시에서 데이터를 조회합니다.
|
101
|
-
|
102
|
-
Args:
|
103
|
-
key (str): 캐시 키
|
104
|
-
|
105
|
-
Returns:
|
106
|
-
Optional[Any]: 캐시된 데이터 또는 None
|
107
|
-
"""
|
108
|
-
return None
|
109
|
-
|
110
|
-
async def set_to_cache(self, key: str, value: Any, ttl: int = 3600) -> None:
|
111
|
-
"""데이터를 캐시에 저장합니다.
|
112
|
-
|
113
|
-
Args:
|
114
|
-
key (str): 캐시 키
|
115
|
-
value (Any): 저장할 값
|
116
|
-
ttl (int, optional): 캐시 유효 시간(초). Defaults to 3600.
|
117
|
-
"""
|
118
|
-
pass
|
119
|
-
|
120
|
-
async def invalidate_cache(self, key: str) -> None:
|
121
|
-
"""캐시를 무효화합니다.
|
122
|
-
|
123
|
-
Args:
|
124
|
-
key (str): 캐시 키
|
125
|
-
"""
|
126
|
-
pass
|
127
|
-
|
128
|
-
##########################
|
129
|
-
# 4. 비즈니스 검증 메서드 #
|
130
|
-
##########################
|
131
|
-
def _validate_business_rules(self, data: Dict[str, Any]) -> None:
|
132
|
-
"""비즈니스 규칙을 검증합니다.
|
133
|
-
|
134
|
-
Args:
|
135
|
-
data (Dict[str, Any]): 검증할 데이터
|
136
|
-
|
137
|
-
Raises:
|
138
|
-
CustomException: 비즈니스 규칙 위반 시
|
139
|
-
"""
|
140
|
-
pass
|
141
|
-
|
142
|
-
def _validate_permissions(self, request: Request, action: str) -> None:
|
143
|
-
"""권한을 검증합니다.
|
144
|
-
|
145
|
-
Args:
|
146
|
-
request (Request): FastAPI 요청 객체
|
147
|
-
action (str): 수행할 작업
|
148
|
-
|
149
|
-
Raises:
|
150
|
-
CustomException: 권한이 없는 경우
|
151
|
-
"""
|
152
|
-
pass
|
153
|
-
|
154
|
-
########################
|
155
|
-
# 5. 응답 처리 메서드 #
|
156
|
-
########################
|
157
|
-
def _handle_response_model(self, entity: ModelType, response_model: Any) -> Dict[str, Any]:
|
158
|
-
"""응답 모델에 맞게 데이터를 처리합니다.
|
159
|
-
|
160
|
-
Args:
|
161
|
-
entity (ModelType): 처리할 엔티티
|
162
|
-
response_model (Any): 응답 모델
|
163
|
-
|
164
|
-
Returns:
|
165
|
-
Dict[str, Any]: 처리된 데이터
|
166
|
-
"""
|
167
|
-
if not response_model:
|
168
|
-
return self._process_response(entity)
|
169
|
-
|
170
|
-
result = self._process_response(entity)
|
21
|
+
"""BaseService 초기화
|
171
22
|
|
172
|
-
# response_model에 없는 필드 제거
|
173
|
-
keys_to_remove = [key for key in result if key not in response_model.model_fields]
|
174
|
-
for key in keys_to_remove:
|
175
|
-
result.pop(key)
|
176
|
-
|
177
|
-
# 모델 검증
|
178
|
-
return response_model(**result).model_dump()
|
179
|
-
|
180
|
-
def _handle_exclude_fields(self, data: Dict[str, Any], exclude_fields: List[str]) -> Dict[str, Any]:
|
181
|
-
"""제외할 필드를 처리합니다.
|
182
|
-
|
183
23
|
Args:
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
Returns:
|
188
|
-
Dict[str, Any]: 처리된 데이터
|
189
|
-
"""
|
190
|
-
if not exclude_fields:
|
191
|
-
return data
|
192
|
-
|
193
|
-
return {k: v for k, v in data.items() if k not in exclude_fields}
|
194
|
-
|
195
|
-
def _validate_ulid(self, ulid: str) -> bool:
|
196
|
-
"""ULID 형식을 검증합니다.
|
197
|
-
|
198
|
-
Args:
|
199
|
-
ulid (str): 검증할 ULID
|
200
|
-
|
201
|
-
Returns:
|
202
|
-
bool: 유효한 ULID 여부
|
24
|
+
db: 데이터베이스 서비스
|
25
|
+
repository: 레포지토리 인스턴스 (선택)
|
26
|
+
request: FastAPI 요청 객체 (선택)
|
203
27
|
"""
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
return False
|
209
|
-
|
210
|
-
def _process_columns(self, entity: ModelType, exclude_extra_data: bool = True) -> Dict[str, Any]:
|
211
|
-
"""엔티티의 컬럼들을 처리합니다.
|
212
|
-
|
213
|
-
Args:
|
214
|
-
entity (ModelType): 처리할 엔티티
|
215
|
-
exclude_extra_data (bool, optional): extra_data 컬럼 제외 여부. Defaults to True.
|
216
|
-
|
217
|
-
Returns:
|
218
|
-
Dict[str, Any]: 처리된 컬럼 데이터
|
219
|
-
"""
|
220
|
-
result = {}
|
221
|
-
for column in entity.__table__.columns:
|
222
|
-
if exclude_extra_data and column.name == 'extra_data':
|
223
|
-
continue
|
224
|
-
|
225
|
-
# 필드 값 처리
|
226
|
-
if hasattr(entity, column.name):
|
227
|
-
value = getattr(entity, column.name)
|
228
|
-
if isinstance(value, datetime):
|
229
|
-
value = value.isoformat()
|
230
|
-
result[column.name] = value
|
231
|
-
elif hasattr(entity, 'extra_data') and isinstance(entity.extra_data, dict):
|
232
|
-
result[column.name] = entity.extra_data.get(column.name)
|
233
|
-
else:
|
234
|
-
result[column.name] = None
|
235
|
-
|
236
|
-
# extra_data의 내용을 최상위 레벨로 업데이트
|
237
|
-
if hasattr(entity, 'extra_data') and isinstance(entity.extra_data, dict):
|
238
|
-
result.update(entity.extra_data or {})
|
239
|
-
|
240
|
-
return result
|
28
|
+
self.db = db
|
29
|
+
self.repository = repository
|
30
|
+
self.request = request
|
31
|
+
self.model = repository.model if repository else None
|
241
32
|
|
242
33
|
def _process_response(self, entity: ModelType, response_model: Any = None) -> Dict[str, Any]:
|
243
34
|
"""응답 데이터를 처리합니다.
|
244
|
-
|
245
|
-
|
35
|
+
|
246
36
|
Args:
|
247
|
-
entity
|
248
|
-
response_model
|
249
|
-
|
37
|
+
entity: 처리할 엔티티
|
38
|
+
response_model: 응답 모델 클래스
|
39
|
+
|
250
40
|
Returns:
|
251
|
-
|
41
|
+
처리된 데이터
|
252
42
|
"""
|
253
43
|
if not entity:
|
254
44
|
return None
|
255
|
-
|
256
|
-
#
|
257
|
-
result =
|
45
|
+
|
46
|
+
# 기본 데이터 변환
|
47
|
+
result = {}
|
48
|
+
|
49
|
+
# 테이블 컬럼 처리
|
50
|
+
for column in entity.__table__.columns:
|
51
|
+
value = getattr(entity, column.name)
|
52
|
+
if isinstance(value, datetime):
|
53
|
+
value = value.isoformat()
|
54
|
+
result[column.name] = value
|
258
55
|
|
259
|
-
# Relationship 처리 (이미 로드된 관계만
|
56
|
+
# Relationship 처리 (이미 로드된 관계만)
|
260
57
|
for relationship in entity.__mapper__.relationships:
|
261
|
-
if
|
58
|
+
if relationship.key not in entity.__dict__:
|
262
59
|
continue
|
263
60
|
|
264
61
|
try:
|
265
62
|
value = getattr(entity, relationship.key)
|
266
|
-
# response_model이 있는 경우 해당 필드의 annotation type을 가져옴
|
267
|
-
nested_response_model = None
|
268
|
-
if response_model and relationship.key in response_model.model_fields:
|
269
|
-
field_info = response_model.model_fields[relationship.key]
|
270
|
-
nested_response_model = field_info.annotation
|
271
|
-
|
272
63
|
if value is not None:
|
273
64
|
if isinstance(value, list):
|
274
65
|
result[relationship.key] = [
|
275
|
-
self._process_response(item
|
66
|
+
self._process_response(item)
|
276
67
|
for item in value
|
277
68
|
]
|
278
69
|
else:
|
279
|
-
result[relationship.key] = self._process_response(value
|
70
|
+
result[relationship.key] = self._process_response(value)
|
280
71
|
else:
|
281
72
|
result[relationship.key] = None
|
282
73
|
except Exception:
|
283
74
|
result[relationship.key] = None
|
284
|
-
|
75
|
+
|
285
76
|
# response_model이 있는 경우 필터링
|
286
77
|
if response_model:
|
287
|
-
#
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
result.update(response_model(**result).model_dump())
|
295
|
-
|
78
|
+
# response_model에 없는 필드 제거
|
79
|
+
keys_to_remove = [key for key in result if key not in response_model.model_fields]
|
80
|
+
for key in keys_to_remove:
|
81
|
+
result.pop(key)
|
82
|
+
# 모델 검증
|
83
|
+
return response_model(**result).model_dump()
|
84
|
+
|
296
85
|
return result
|
297
86
|
|
298
|
-
def
|
299
|
-
|
300
|
-
|
87
|
+
async def create(
|
88
|
+
self,
|
89
|
+
data: Dict[str, Any],
|
90
|
+
response_model: Any = None
|
91
|
+
) -> Dict[str, Any]:
|
92
|
+
"""엔티티를 생성합니다.
|
93
|
+
|
301
94
|
Args:
|
302
|
-
|
303
|
-
|
304
|
-
Returns:
|
305
|
-
Dict[str, Any]: 기본 필드만 포함된 딕셔너리
|
306
|
-
"""
|
307
|
-
if not entity:
|
308
|
-
return None
|
95
|
+
data: 생성할 데이터
|
96
|
+
response_model: 응답 모델 클래스
|
309
97
|
|
310
|
-
return self._process_columns(entity)
|
311
|
-
|
312
|
-
async def _create_for_model(self, model_name: str, data: Dict[str, Any], exclude_fields: List[str] = None) -> DeclarativeBase:
|
313
|
-
"""지정된 모델에 대해 새로운 엔티티를 생성합니다.
|
314
|
-
|
315
|
-
Args:
|
316
|
-
model_name (str): 생성할 모델 이름
|
317
|
-
data (Dict[str, Any]): 생성할 엔티티 데이터
|
318
|
-
exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
|
319
|
-
|
320
|
-
Returns:
|
321
|
-
DeclarativeBase: 생성된 엔티티
|
322
|
-
|
323
|
-
Raises:
|
324
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
325
|
-
"""
|
326
|
-
if model_name not in self.additional_models:
|
327
|
-
raise CustomException(
|
328
|
-
ErrorCode.INVALID_REQUEST,
|
329
|
-
detail=f"Model {model_name} not registered",
|
330
|
-
source_function=f"{self.__class__.__name__}._create_for_model"
|
331
|
-
)
|
332
|
-
|
333
|
-
try:
|
334
|
-
# 제외할 필드 처리
|
335
|
-
if exclude_fields:
|
336
|
-
data = {k: v for k, v in data.items() if k not in exclude_fields}
|
337
|
-
|
338
|
-
return await self.db_service.create_entity(self.additional_models[model_name], data)
|
339
|
-
except CustomException as e:
|
340
|
-
raise e
|
341
|
-
except Exception as e:
|
342
|
-
raise CustomException(
|
343
|
-
ErrorCode.DB_CREATE_ERROR,
|
344
|
-
detail=str(e),
|
345
|
-
source_function=f"{self.__class__.__name__}._create_for_model",
|
346
|
-
original_error=e
|
347
|
-
)
|
348
|
-
|
349
|
-
def _process_password(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
350
|
-
"""비밀번호 필드가 있는 경우 해시화합니다.
|
351
|
-
|
352
|
-
Args:
|
353
|
-
data (Dict[str, Any]): 처리할 데이터
|
354
|
-
|
355
|
-
Returns:
|
356
|
-
Dict[str, Any]: 처리된 데이터
|
357
|
-
"""
|
358
|
-
if "password" in data:
|
359
|
-
data["password"] = hash_password(data["password"])
|
360
|
-
return data
|
361
|
-
|
362
|
-
#######################
|
363
|
-
# 6. CRUD 작업 메서드 #
|
364
|
-
#######################
|
365
|
-
async def create(self, data: Dict[str, Any], exclude_fields: List[str] = None, model_name: str = None) -> Union[ModelType, DeclarativeBase]:
|
366
|
-
"""새로운 엔티티를 생성합니다.
|
367
|
-
|
368
|
-
Args:
|
369
|
-
data (Dict[str, Any]): 생성할 엔티티 데이터
|
370
|
-
exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
|
371
|
-
model_name (str, optional): 생성할 모델 이름. Defaults to None.
|
372
|
-
|
373
98
|
Returns:
|
374
|
-
|
375
|
-
|
99
|
+
생성된 엔티티
|
100
|
+
|
376
101
|
Raises:
|
377
|
-
CustomException:
|
102
|
+
CustomException: 생성 실패 시
|
378
103
|
"""
|
379
104
|
try:
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
# 제외할 필드 처리
|
384
|
-
if exclude_fields:
|
385
|
-
data = {k: v for k, v in data.items() if k not in exclude_fields}
|
386
|
-
|
387
|
-
if model_name:
|
388
|
-
return await self._create_for_model(model_name, data)
|
389
|
-
|
390
|
-
return await self.repository.create(data)
|
105
|
+
entity = await self.db.create_entity(self.model, data)
|
106
|
+
return self._process_response(entity, response_model)
|
391
107
|
except CustomException as e:
|
392
108
|
raise e
|
393
109
|
except Exception as e:
|
@@ -398,274 +114,147 @@ class BaseService(Generic[ModelType]):
|
|
398
114
|
original_error=e
|
399
115
|
)
|
400
116
|
|
401
|
-
async def
|
117
|
+
async def get(
|
402
118
|
self,
|
403
119
|
ulid: str,
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
"""기존 엔티티를 수정합니다.
|
409
|
-
|
120
|
+
response_model: Any = None
|
121
|
+
) -> Dict[str, Any]:
|
122
|
+
"""엔티티를 조회합니다.
|
123
|
+
|
410
124
|
Args:
|
411
|
-
ulid
|
412
|
-
|
413
|
-
exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
|
414
|
-
model_name (str, optional): 수정할 모델 이름. Defaults to None.
|
415
|
-
|
416
|
-
Returns:
|
417
|
-
Optional[ModelType]: 수정된 엔티티, 없으면 None
|
418
|
-
|
419
|
-
Raises:
|
420
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
421
|
-
"""
|
422
|
-
try:
|
423
|
-
# 비밀번호 해시화
|
424
|
-
data = self._process_password(data)
|
425
|
-
|
426
|
-
# 제외할 필드 처리
|
427
|
-
if exclude_fields:
|
428
|
-
data = {k: v for k, v in data.items() if k not in exclude_fields}
|
429
|
-
|
430
|
-
async with self.db_service.transaction():
|
431
|
-
if model_name:
|
432
|
-
if model_name not in self.additional_models:
|
433
|
-
raise CustomException(
|
434
|
-
ErrorCode.INVALID_REQUEST,
|
435
|
-
detail=f"Model {model_name} not registered",
|
436
|
-
source_function=f"{self.__class__.__name__}.update"
|
437
|
-
)
|
438
|
-
entity = await self.db_service.update_entity(
|
439
|
-
self.additional_models[model_name],
|
440
|
-
{"ulid": ulid},
|
441
|
-
data
|
442
|
-
)
|
443
|
-
if not entity:
|
444
|
-
raise CustomException(
|
445
|
-
ErrorCode.NOT_FOUND,
|
446
|
-
detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
|
447
|
-
source_function=f"{self.__class__.__name__}.update"
|
448
|
-
)
|
449
|
-
return entity
|
450
|
-
|
451
|
-
entity = await self.repository.update(ulid, data)
|
452
|
-
if not entity:
|
453
|
-
raise CustomException(
|
454
|
-
ErrorCode.NOT_FOUND,
|
455
|
-
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
456
|
-
source_function=f"{self.__class__.__name__}.update"
|
457
|
-
)
|
458
|
-
return entity
|
459
|
-
except CustomException as e:
|
460
|
-
raise e
|
461
|
-
except Exception as e:
|
462
|
-
raise CustomException(
|
463
|
-
ErrorCode.DB_UPDATE_ERROR,
|
464
|
-
detail=str(e),
|
465
|
-
source_function=f"{self.__class__.__name__}.update",
|
466
|
-
original_error=e
|
467
|
-
)
|
468
|
-
|
469
|
-
async def delete(self, ulid: str, model_name: str = None) -> bool:
|
470
|
-
"""엔티티를 소프트 삭제합니다 (is_deleted = True)."""
|
471
|
-
try:
|
472
|
-
if model_name:
|
473
|
-
if model_name not in self.additional_models:
|
474
|
-
raise CustomException(
|
475
|
-
ErrorCode.INVALID_REQUEST,
|
476
|
-
detail=f"Model {model_name} not registered",
|
477
|
-
source_function=f"{self.__class__.__name__}.delete"
|
478
|
-
)
|
479
|
-
|
480
|
-
stmt = select(self.additional_models[model_name]).filter_by(ulid=ulid, is_deleted=False)
|
481
|
-
result = await self.session.execute(stmt)
|
482
|
-
entity = result.scalars().first()
|
483
|
-
|
484
|
-
if not entity:
|
485
|
-
raise CustomException(
|
486
|
-
ErrorCode.NOT_FOUND,
|
487
|
-
detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
|
488
|
-
source_function=f"{self.__class__.__name__}.delete"
|
489
|
-
)
|
490
|
-
|
491
|
-
entity.is_deleted = True
|
492
|
-
await self.session.flush()
|
493
|
-
return True
|
494
|
-
|
495
|
-
return await self.repository.delete(ulid)
|
125
|
+
ulid: 조회할 엔티티의 ULID
|
126
|
+
response_model: 응답 모델 클래스
|
496
127
|
|
497
|
-
except CustomException as e:
|
498
|
-
raise e
|
499
|
-
except Exception as e:
|
500
|
-
raise CustomException(
|
501
|
-
ErrorCode.DB_DELETE_ERROR,
|
502
|
-
detail=str(e),
|
503
|
-
source_function=f"{self.__class__.__name__}.delete",
|
504
|
-
original_error=e
|
505
|
-
)
|
506
|
-
|
507
|
-
async def real_row_delete(self, ulid: str, model_name: str = None) -> bool:
|
508
|
-
"""엔티티를 실제로 삭제합니다.
|
509
|
-
|
510
|
-
Args:
|
511
|
-
ulid (str): 삭제할 엔티티의 ULID
|
512
|
-
model_name (str, optional): 삭제할 모델 이름. Defaults to None.
|
513
|
-
|
514
128
|
Returns:
|
515
|
-
|
516
|
-
|
129
|
+
조회된 엔티티
|
130
|
+
|
517
131
|
Raises:
|
518
|
-
CustomException:
|
132
|
+
CustomException: 조회 실패 시
|
519
133
|
"""
|
520
134
|
try:
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
)
|
528
|
-
entity = await self.db_service.retrieve_entity(
|
529
|
-
self.additional_models[model_name],
|
530
|
-
{"ulid": ulid}
|
135
|
+
entity = await self.db.get_entity(self.model, {"ulid": ulid, "is_deleted": False})
|
136
|
+
if not entity:
|
137
|
+
raise CustomException(
|
138
|
+
ErrorCode.NOT_FOUND,
|
139
|
+
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
140
|
+
source_function=f"{self.__class__.__name__}.get"
|
531
141
|
)
|
532
|
-
|
533
|
-
await self.db_service.delete_entity(entity)
|
534
|
-
return True
|
535
|
-
return False
|
536
|
-
|
537
|
-
return await self.repository.real_row_delete(ulid)
|
142
|
+
return self._process_response(entity, response_model)
|
538
143
|
except CustomException as e:
|
539
144
|
raise e
|
540
145
|
except Exception as e:
|
541
146
|
raise CustomException(
|
542
|
-
ErrorCode.
|
147
|
+
ErrorCode.DB_QUERY_ERROR,
|
543
148
|
detail=str(e),
|
544
|
-
source_function=f"{self.__class__.__name__}.
|
149
|
+
source_function=f"{self.__class__.__name__}.get",
|
545
150
|
original_error=e
|
546
151
|
)
|
547
152
|
|
548
|
-
#########################
|
549
|
-
# 7. 조회 및 검색 메서드 #
|
550
|
-
#########################
|
551
153
|
async def list(
|
552
154
|
self,
|
553
155
|
skip: int = 0,
|
554
156
|
limit: int = 100,
|
555
|
-
filters: Dict[str, Any]
|
556
|
-
search_params: Dict[str, Any] | None = None,
|
557
|
-
model_name: str | None = None,
|
558
|
-
request: Request | None = None,
|
157
|
+
filters: Optional[Dict[str, Any]] = None,
|
559
158
|
response_model: Any = None
|
560
159
|
) -> List[Dict[str, Any]]:
|
561
|
-
"""엔티티 목록을 조회합니다.
|
160
|
+
"""엔티티 목록을 조회합니다.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
skip: 건너뛸 레코드 수
|
164
|
+
limit: 조회할 최대 레코드 수
|
165
|
+
filters: 필터 조건
|
166
|
+
response_model: 응답 모델 클래스
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
엔티티 목록
|
170
|
+
|
171
|
+
Raises:
|
172
|
+
CustomException: 조회 실패 시
|
173
|
+
"""
|
562
174
|
try:
|
563
|
-
|
564
|
-
|
565
|
-
raise CustomException(
|
566
|
-
ErrorCode.INVALID_REQUEST,
|
567
|
-
detail=f"Model {model_name} not registered",
|
568
|
-
source_function=f"{self.__class__.__name__}.list"
|
569
|
-
)
|
570
|
-
|
571
|
-
stmt = select(self.additional_models[model_name]).where(
|
572
|
-
self.additional_models[model_name].is_deleted == False
|
573
|
-
)
|
574
|
-
|
575
|
-
if filters:
|
576
|
-
for key, value in filters.items():
|
577
|
-
if value is not None:
|
578
|
-
stmt = stmt.where(getattr(self.additional_models[model_name], key) == value)
|
579
|
-
|
580
|
-
stmt = stmt.offset(skip).limit(limit)
|
581
|
-
result = await self.session.execute(stmt)
|
582
|
-
entities = result.scalars().all()
|
583
|
-
|
584
|
-
return [self._process_response(entity, response_model) for entity in entities]
|
585
|
-
|
586
|
-
return await self.repository.list(
|
587
|
-
skip=skip,
|
588
|
-
limit=limit,
|
175
|
+
entities = await self.db.list_entities(
|
176
|
+
self.model,
|
589
177
|
filters=filters,
|
590
|
-
|
178
|
+
skip=skip,
|
179
|
+
limit=limit
|
591
180
|
)
|
592
|
-
|
181
|
+
return [self._process_response(entity, response_model) for entity in entities]
|
593
182
|
except CustomException as e:
|
594
|
-
e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
|
595
|
-
e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
|
596
183
|
raise e
|
597
184
|
except Exception as e:
|
598
185
|
raise CustomException(
|
599
|
-
ErrorCode.
|
186
|
+
ErrorCode.DB_QUERY_ERROR,
|
600
187
|
detail=str(e),
|
601
188
|
source_function=f"{self.__class__.__name__}.list",
|
602
189
|
original_error=e
|
603
190
|
)
|
604
191
|
|
605
|
-
async def
|
192
|
+
async def update(
|
606
193
|
self,
|
607
194
|
ulid: str,
|
608
|
-
|
609
|
-
request: Request | None = None,
|
195
|
+
data: Dict[str, Any],
|
610
196
|
response_model: Any = None
|
611
|
-
) ->
|
612
|
-
"""
|
613
|
-
|
197
|
+
) -> Dict[str, Any]:
|
198
|
+
"""엔티티를 수정합니다.
|
199
|
+
|
614
200
|
Args:
|
615
|
-
ulid
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
201
|
+
ulid: 수정할 엔티티의 ULID
|
202
|
+
data: 수정할 데이터
|
203
|
+
response_model: 응답 모델 클래스
|
204
|
+
|
620
205
|
Returns:
|
621
|
-
|
206
|
+
수정된 엔티티
|
622
207
|
|
623
208
|
Raises:
|
624
|
-
CustomException:
|
209
|
+
CustomException: 수정 실패 시
|
625
210
|
"""
|
626
211
|
try:
|
627
|
-
|
628
|
-
if not
|
212
|
+
entity = await self.db.get_entity(self.model, {"ulid": ulid, "is_deleted": False})
|
213
|
+
if not entity:
|
629
214
|
raise CustomException(
|
630
|
-
ErrorCode.
|
631
|
-
detail=f"
|
632
|
-
source_function=f"{self.__class__.__name__}.
|
633
|
-
)
|
634
|
-
|
635
|
-
if model_name:
|
636
|
-
if model_name not in self.additional_models:
|
637
|
-
raise CustomException(
|
638
|
-
ErrorCode.INVALID_REQUEST,
|
639
|
-
detail=f"Model {model_name} not registered",
|
640
|
-
source_function=f"{self.__class__.__name__}.get"
|
641
|
-
)
|
642
|
-
entity = await self.db_service.retrieve_entity(
|
643
|
-
self.additional_models[model_name],
|
644
|
-
{"ulid": ulid, "is_deleted": False}
|
215
|
+
ErrorCode.NOT_FOUND,
|
216
|
+
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
217
|
+
source_function=f"{self.__class__.__name__}.update"
|
645
218
|
)
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
219
|
+
updated = await self.db.update_entity(entity, data)
|
220
|
+
return self._process_response(updated, response_model)
|
221
|
+
except CustomException as e:
|
222
|
+
raise e
|
223
|
+
except Exception as e:
|
224
|
+
raise CustomException(
|
225
|
+
ErrorCode.DB_UPDATE_ERROR,
|
226
|
+
detail=str(e),
|
227
|
+
source_function=f"{self.__class__.__name__}.update",
|
228
|
+
original_error=e
|
229
|
+
)
|
653
230
|
|
654
|
-
|
231
|
+
async def delete(self, ulid: str) -> bool:
|
232
|
+
"""엔티티를 삭제합니다.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
ulid: 삭제할 엔티티의 ULID
|
236
|
+
|
237
|
+
Returns:
|
238
|
+
삭제 성공 여부
|
239
|
+
|
240
|
+
Raises:
|
241
|
+
CustomException: 삭제 실패 시
|
242
|
+
"""
|
243
|
+
try:
|
244
|
+
entity = await self.db.get_entity(self.model, {"ulid": ulid, "is_deleted": False})
|
655
245
|
if not entity:
|
656
246
|
raise CustomException(
|
657
247
|
ErrorCode.NOT_FOUND,
|
658
248
|
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
659
|
-
source_function=f"{self.__class__.__name__}.
|
249
|
+
source_function=f"{self.__class__.__name__}.delete"
|
660
250
|
)
|
661
|
-
|
662
|
-
return self._process_response(entity, response_model)
|
251
|
+
return await self.db.delete_entity(entity)
|
663
252
|
except CustomException as e:
|
664
253
|
raise e
|
665
254
|
except Exception as e:
|
666
255
|
raise CustomException(
|
667
|
-
ErrorCode.
|
256
|
+
ErrorCode.DB_DELETE_ERROR,
|
668
257
|
detail=str(e),
|
669
|
-
source_function=f"{self.__class__.__name__}.
|
258
|
+
source_function=f"{self.__class__.__name__}.delete",
|
670
259
|
original_error=e
|
671
260
|
)
|
aiteamutils/dependencies.py
CHANGED
@@ -1,12 +1,9 @@
|
|
1
1
|
"""의존성 관리 모듈."""
|
2
2
|
from typing import Type, TypeVar, Dict, Any, Optional, Callable, List, AsyncGenerator
|
3
|
-
from fastapi import Request, Depends
|
3
|
+
from fastapi import Request, Depends
|
4
4
|
from sqlalchemy.ext.asyncio import AsyncSession
|
5
|
-
from jose import jwt, JWTError
|
6
|
-
import logging
|
7
5
|
|
8
6
|
from .exceptions import CustomException, ErrorCode
|
9
|
-
from .config import get_settings
|
10
7
|
from .base_service import BaseService
|
11
8
|
from .base_repository import BaseRepository
|
12
9
|
from .database import DatabaseService
|
@@ -20,8 +17,7 @@ _session_provider = None
|
|
20
17
|
__all__ = [
|
21
18
|
"setup_dependencies",
|
22
19
|
"register_service",
|
23
|
-
"get_service"
|
24
|
-
"get_current_user"
|
20
|
+
"get_service"
|
25
21
|
]
|
26
22
|
|
27
23
|
def setup_dependencies(session_provider: Callable[[], AsyncGenerator[AsyncSession, None]]) -> None:
|
@@ -139,89 +135,4 @@ def get_service(service_name: str) -> Callable:
|
|
139
135
|
session: AsyncSession = Depends(_session_provider)
|
140
136
|
) -> BaseService:
|
141
137
|
return await _get_service(service_name, session, request)
|
142
|
-
return _get_service_dependency
|
143
|
-
|
144
|
-
async def get_current_user(
|
145
|
-
request: Request,
|
146
|
-
session: AsyncSession,
|
147
|
-
auth_service: BaseService = Depends(get_service("AuthService"))
|
148
|
-
) -> Dict[str, Any]:
|
149
|
-
"""현재 사용자 정보를 반환합니다.
|
150
|
-
|
151
|
-
Args:
|
152
|
-
request: FastAPI 요청 객체
|
153
|
-
session: 데이터베이스 세션
|
154
|
-
auth_service: 인증 서비스
|
155
|
-
|
156
|
-
Returns:
|
157
|
-
Dict[str, Any]: 사용자 정보
|
158
|
-
|
159
|
-
Raises:
|
160
|
-
HTTPException: 인증 실패 시
|
161
|
-
"""
|
162
|
-
settings = get_settings()
|
163
|
-
|
164
|
-
# Authorization 헤더 검증
|
165
|
-
authorization = request.headers.get("Authorization")
|
166
|
-
if not authorization or not authorization.startswith("Bearer "):
|
167
|
-
raise HTTPException(
|
168
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
169
|
-
detail="Not authenticated",
|
170
|
-
headers={"WWW-Authenticate": "Bearer"}
|
171
|
-
)
|
172
|
-
|
173
|
-
token = authorization.split(" ")[1]
|
174
|
-
|
175
|
-
try:
|
176
|
-
# JWT 토큰 디코딩
|
177
|
-
payload = jwt.decode(
|
178
|
-
token,
|
179
|
-
settings.jwt_secret,
|
180
|
-
algorithms=[settings.jwt_algorithm],
|
181
|
-
issuer=settings.token_issuer,
|
182
|
-
audience=settings.token_audience
|
183
|
-
)
|
184
|
-
|
185
|
-
# 토큰 타입 검증
|
186
|
-
token_type = payload.get("token_type")
|
187
|
-
if token_type != "access":
|
188
|
-
raise HTTPException(
|
189
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
190
|
-
detail="Invalid token type",
|
191
|
-
headers={"WWW-Authenticate": "Bearer"}
|
192
|
-
)
|
193
|
-
|
194
|
-
# 사용자 조회
|
195
|
-
user_ulid = payload.get("user_ulid")
|
196
|
-
if not user_ulid:
|
197
|
-
raise HTTPException(
|
198
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
199
|
-
detail="Invalid token payload",
|
200
|
-
headers={"WWW-Authenticate": "Bearer"}
|
201
|
-
)
|
202
|
-
|
203
|
-
user = await auth_service.get_by_ulid(user_ulid)
|
204
|
-
if not user:
|
205
|
-
raise HTTPException(
|
206
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
207
|
-
detail="User not found",
|
208
|
-
headers={"WWW-Authenticate": "Bearer"}
|
209
|
-
)
|
210
|
-
|
211
|
-
return user
|
212
|
-
|
213
|
-
except JWTError as e:
|
214
|
-
raise HTTPException(
|
215
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
216
|
-
detail=str(e),
|
217
|
-
headers={"WWW-Authenticate": "Bearer"}
|
218
|
-
)
|
219
|
-
except HTTPException as e:
|
220
|
-
raise e
|
221
|
-
except Exception as e:
|
222
|
-
logging.error(f"Error in get_current_user: {str(e)}")
|
223
|
-
raise HTTPException(
|
224
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
225
|
-
detail="Authentication failed",
|
226
|
-
headers={"WWW-Authenticate": "Bearer"}
|
227
|
-
)
|
138
|
+
return _get_service_dependency
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.59"
|
@@ -1,16 +1,16 @@
|
|
1
1
|
aiteamutils/__init__.py,sha256=U9UEyU0AqpLZzbPJiBvwDEh-s2405y1IeWZ0zCqFSl0,1307
|
2
2
|
aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
|
3
3
|
aiteamutils/base_repository.py,sha256=vqsundoN0h7FVvgqTBEnnJNMcFpvMK0s_nxBWdIYg-U,7846
|
4
|
-
aiteamutils/base_service.py,sha256=
|
4
|
+
aiteamutils/base_service.py,sha256=o4oeX__RmUFp5kV-fb4VwBhF8nTAZNMjoWJvK7G1Wmk,8615
|
5
5
|
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
6
|
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
7
|
aiteamutils/database.py,sha256=x0x5gnSyGfwo_klL9O65RnGOQID6c9tH2miwFveVyoE,6326
|
8
|
-
aiteamutils/dependencies.py,sha256=
|
8
|
+
aiteamutils/dependencies.py,sha256=Qoy_M7r65Fv3Y8RMVBi81QLdfjfWIk-pm7O5_Przsa4,4084
|
9
9
|
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
10
10
|
aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
|
11
11
|
aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
|
12
12
|
aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
|
13
|
-
aiteamutils/version.py,sha256=
|
14
|
-
aiteamutils-0.2.
|
15
|
-
aiteamutils-0.2.
|
16
|
-
aiteamutils-0.2.
|
13
|
+
aiteamutils/version.py,sha256=_ql6JzqtHA14js-9_8pUBJDK_7BZm5uHqA1j4jfryCs,42
|
14
|
+
aiteamutils-0.2.59.dist-info/METADATA,sha256=OOT0-1lTWlUQv7fL2MtELHAUPSj18nqiaiXRyFc8QoQ,1718
|
15
|
+
aiteamutils-0.2.59.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.59.dist-info/RECORD,,
|
File without changes
|