aiteamutils 0.2.32__tar.gz → 0.2.34__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- aiteamutils-0.2.34/.cursorrules +90 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/PKG-INFO +1 -1
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/config.py +7 -2
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/database.py +16 -4
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/dependencies.py +12 -4
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/security.py +88 -36
- aiteamutils-0.2.34/aiteamutils/version.py +2 -0
- aiteamutils-0.2.32/aiteamutils/version.py +0 -2
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/.gitignore +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/README.md +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/__init__.py +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/base_model.py +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/base_repository.py +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/base_service.py +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/cache.py +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/enums.py +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/exceptions.py +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/aiteamutils/validators.py +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/pyproject.toml +0 -0
- {aiteamutils-0.2.32 → aiteamutils-0.2.34}/setup.py +0 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
- 에러 처리는 `core/app/utils/CustomException`의 `CustomException` 클래스를 사용:
|
2
|
+
- 형식: `CustomException(ErrorCode, detail=value, source_function="function_name")`
|
3
|
+
- **ErrorCode**: `core/app/utils/CustomException`에 정의된 값 사용.
|
4
|
+
- **detail**: 검증/처리 중인 값 자체만 포함 (예: 전화번호 검증 시 전화번호 값만)
|
5
|
+
- 단일 값: 해당 값만 포함 (예: `detail=phone_number`)
|
6
|
+
- 복합 값: 파이프(|)로 구분 (예: `detail=f"{table}|{column}|{value}"`)
|
7
|
+
- 추가 텍스트나 설명 없이 값만 포함
|
8
|
+
- **source_function**: 에러 발생 함수명을 문자열로 입력.
|
9
|
+
|
10
|
+
# 에러 처리 규칙
|
11
|
+
1. 에러 응답 형식:
|
12
|
+
```json
|
13
|
+
{
|
14
|
+
"error_code": 3003, // 숫자 형식
|
15
|
+
"error_type": "DUPLICATE_ERROR",
|
16
|
+
"status_code": 409,
|
17
|
+
"path": "/companies",
|
18
|
+
"method": "POST",
|
19
|
+
"client_ip": "172.20.0.2",
|
20
|
+
"user_agent": "Mozilla/5.0..."
|
21
|
+
}
|
22
|
+
```
|
23
|
+
|
24
|
+
2. 에러 로그 형식:
|
25
|
+
```json
|
26
|
+
{
|
27
|
+
"error_code": 3003,
|
28
|
+
"error_type": "DUPLICATE_ERROR",
|
29
|
+
"status_code": 409,
|
30
|
+
"path": "/companies",
|
31
|
+
"method": "POST",
|
32
|
+
"client_ip": "172.20.0.2",
|
33
|
+
"user_agent": "Mozilla/5.0...",
|
34
|
+
"detail": "companies|id|test-company",
|
35
|
+
"source_function": "DatabaseService.validate_unique_fields",
|
36
|
+
"error_chain": [
|
37
|
+
{
|
38
|
+
"error_code": "ErrorCode.DUPLICATE_ERROR",
|
39
|
+
"detail": "companies|id|test-company",
|
40
|
+
"source_function": "DatabaseService.validate_unique_fields",
|
41
|
+
"original_error": null
|
42
|
+
},
|
43
|
+
{
|
44
|
+
"error_code": "ErrorCode.DUPLICATE_ERROR",
|
45
|
+
"detail": "companies|id|test-company",
|
46
|
+
"source_function": "OrganizationRepository.create_company",
|
47
|
+
"original_error": null
|
48
|
+
}
|
49
|
+
],
|
50
|
+
"original_error": null
|
51
|
+
}
|
52
|
+
```
|
53
|
+
|
54
|
+
3. detail 형식:
|
55
|
+
- 단일 필드 중복: `{테이블명}|{필드명}|{값}`
|
56
|
+
- 복수 필드 중복: `{테이블명}|{필드1}:{값1}|{필드2}:{값2}`
|
57
|
+
- 외래키 위반: `{참조테이블명}|{필드명}|{값}`
|
58
|
+
- 유효성 검사: `{검증값}`
|
59
|
+
|
60
|
+
4. 에러 체인 구성:
|
61
|
+
- 에러는 최초 발생 지점부터 시작하여 호출 스택을 따라 전파
|
62
|
+
- 각 레이어는 자신의 컨텍스트 정보를 추가하여 에러를 상위로 전달
|
63
|
+
- `parent_source_function`을 통해 호출 경로 추적
|
64
|
+
- 원본 에러 정보는 `original_error`로 보존
|
65
|
+
|
66
|
+
5. 레이어별 에러 처리:
|
67
|
+
- Database 레이어:
|
68
|
+
- 중복 검사: `DUPLICATE_ERROR`
|
69
|
+
- 외래키 위반: `FOREIGN_KEY_VIOLATION`
|
70
|
+
- 쿼리 실패: `DB_QUERY_ERROR`
|
71
|
+
- Repository 레이어:
|
72
|
+
- 원본 에러 보존 및 컨텍스트 추가
|
73
|
+
- Service 레이어:
|
74
|
+
- 비즈니스 로직 실패: `INTERNAL_ERROR`
|
75
|
+
- 트랜잭션 실패: `TRANSACTION_ERROR`
|
76
|
+
- API 레이어:
|
77
|
+
- 입력값 검증: `VALIDATION_ERROR`
|
78
|
+
- 권한 검증: `UNAUTHORIZED`, `FORBIDDEN`
|
79
|
+
|
80
|
+
6. 에러 처리 원칙:
|
81
|
+
- 클라이언트 응답에서는 `detail`, `source_function`, `error_chain` 제외
|
82
|
+
- 로그에는 모든 정보를 포함하여 디버깅 용이성 확보
|
83
|
+
- 에러는 발생 지점에서 바로 처리하고 적절한 에러 코드 사용
|
84
|
+
- 중첩된 try-except 블록 대신 early return 패턴 사용
|
85
|
+
- 모든 예외는 `CustomException`으로 변환하여 일관된 처리
|
86
|
+
|
87
|
+
7. 에러 코드 관리:
|
88
|
+
- 모든 에러 코드는 `ErrorCode` enum에 정의
|
89
|
+
- 도메인별로 구분된 에러 코드 범위 사용
|
90
|
+
- 각 에러 코드는 고유한 숫자 값과 HTTP 상태 코드 매핑
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""설정 모듈."""
|
2
2
|
from typing import Union
|
3
3
|
from .database import init_database_service
|
4
|
+
from .exceptions import CustomException, ErrorCode
|
4
5
|
|
5
6
|
class Settings:
|
6
7
|
"""기본 설정 클래스"""
|
@@ -74,8 +75,12 @@ def get_settings() -> Settings:
|
|
74
75
|
Settings: 설정 객체
|
75
76
|
|
76
77
|
Raises:
|
77
|
-
|
78
|
+
CustomException: 설정이 초기화되지 않은 경우
|
78
79
|
"""
|
79
80
|
if _settings is None:
|
80
|
-
raise
|
81
|
+
raise CustomException(
|
82
|
+
ErrorCode.INTERNAL_ERROR,
|
83
|
+
detail="settings",
|
84
|
+
source_function="get_settings"
|
85
|
+
)
|
81
86
|
return _settings
|
@@ -26,10 +26,14 @@ def get_database_service() -> 'DatabaseService':
|
|
26
26
|
DatabaseService: DatabaseService 인스턴스
|
27
27
|
|
28
28
|
Raises:
|
29
|
-
|
29
|
+
CustomException: DatabaseService가 초기화되지 않은 경우
|
30
30
|
"""
|
31
31
|
if _database_service is None:
|
32
|
-
raise
|
32
|
+
raise CustomException(
|
33
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
34
|
+
detail="database_service",
|
35
|
+
source_function="get_database_service"
|
36
|
+
)
|
33
37
|
return _database_service
|
34
38
|
|
35
39
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
@@ -100,13 +104,21 @@ class DatabaseService:
|
|
100
104
|
)
|
101
105
|
self.db = session
|
102
106
|
else:
|
103
|
-
raise
|
107
|
+
raise CustomException(
|
108
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
109
|
+
detail="db_url|session",
|
110
|
+
source_function="DatabaseService.__init__"
|
111
|
+
)
|
104
112
|
|
105
113
|
@asynccontextmanager
|
106
114
|
async def get_session(self) -> AsyncGenerator[AsyncSession, None]:
|
107
115
|
"""데이터베이스 세션을 생성하고 반환하는 비동기 컨텍스트 매니저."""
|
108
116
|
if self.session_factory is None:
|
109
|
-
raise
|
117
|
+
raise CustomException(
|
118
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
119
|
+
detail="session_factory",
|
120
|
+
source_function="DatabaseService.get_session"
|
121
|
+
)
|
110
122
|
|
111
123
|
async with self.session_factory() as session:
|
112
124
|
try:
|
@@ -26,10 +26,14 @@ class ServiceRegistry:
|
|
26
26
|
service_class (Type): Service 클래스
|
27
27
|
|
28
28
|
Raises:
|
29
|
-
|
29
|
+
CustomException: 이미 등록된 서비스인 경우
|
30
30
|
"""
|
31
31
|
if name in self._services:
|
32
|
-
raise
|
32
|
+
raise CustomException(
|
33
|
+
ErrorCode.INTERNAL_ERROR,
|
34
|
+
detail=f"service|{name}",
|
35
|
+
source_function="ServiceRegistry.register"
|
36
|
+
)
|
33
37
|
self._services[name] = (repository_class, service_class)
|
34
38
|
|
35
39
|
def get(self, name: str) -> Tuple[Type, Type]:
|
@@ -42,10 +46,14 @@ class ServiceRegistry:
|
|
42
46
|
Tuple[Type, Type]: (Repository 클래스, Service 클래스) 튜플
|
43
47
|
|
44
48
|
Raises:
|
45
|
-
|
49
|
+
CustomException: 등록되지 않은 서비스인 경우
|
46
50
|
"""
|
47
51
|
if name not in self._services:
|
48
|
-
raise
|
52
|
+
raise CustomException(
|
53
|
+
ErrorCode.NOT_FOUND,
|
54
|
+
detail=f"service|{name}",
|
55
|
+
source_function="ServiceRegistry.get"
|
56
|
+
)
|
49
57
|
return self._services[name]
|
50
58
|
|
51
59
|
# ServiceRegistry 초기화
|
@@ -157,8 +157,19 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
157
157
|
|
158
158
|
Returns:
|
159
159
|
bool: 비밀번호 일치 여부
|
160
|
+
|
161
|
+
Raises:
|
162
|
+
CustomException: 비밀번호 검증 실패 시
|
160
163
|
"""
|
161
|
-
|
164
|
+
try:
|
165
|
+
return pwd_context.verify(plain_password, hashed_password)
|
166
|
+
except Exception as e:
|
167
|
+
raise CustomException(
|
168
|
+
ErrorCode.INVALID_PASSWORD,
|
169
|
+
detail=plain_password,
|
170
|
+
source_function="security.verify_password",
|
171
|
+
original_error=e
|
172
|
+
)
|
162
173
|
|
163
174
|
def hash_password(password: str) -> str:
|
164
175
|
"""비밀번호를 해시화합니다.
|
@@ -168,8 +179,19 @@ def hash_password(password: str) -> str:
|
|
168
179
|
|
169
180
|
Returns:
|
170
181
|
str: 해시된 비밀번호
|
182
|
+
|
183
|
+
Raises:
|
184
|
+
CustomException: 비밀번호 해시화 실패 시
|
171
185
|
"""
|
172
|
-
|
186
|
+
try:
|
187
|
+
return pwd_context.hash(password)
|
188
|
+
except Exception as e:
|
189
|
+
raise CustomException(
|
190
|
+
ErrorCode.INTERNAL_ERROR,
|
191
|
+
detail=password,
|
192
|
+
source_function="security.hash_password",
|
193
|
+
original_error=e
|
194
|
+
)
|
173
195
|
|
174
196
|
def rate_limit(
|
175
197
|
max_requests: int,
|
@@ -177,7 +199,7 @@ def rate_limit(
|
|
177
199
|
key_func: Optional[Callable] = None
|
178
200
|
):
|
179
201
|
"""Rate limiting 데코레이터."""
|
180
|
-
|
202
|
+
rate_limits: Dict[str, Dict[str, Any]] = {}
|
181
203
|
|
182
204
|
def decorator(func: Callable) -> Callable:
|
183
205
|
@wraps(func)
|
@@ -194,39 +216,56 @@ def rate_limit(
|
|
194
216
|
request = arg
|
195
217
|
break
|
196
218
|
if not request:
|
197
|
-
raise
|
219
|
+
raise CustomException(
|
198
220
|
ErrorCode.INTERNAL_ERROR,
|
199
221
|
detail="Request object not found",
|
200
222
|
source_function="rate_limit"
|
201
223
|
)
|
202
224
|
|
203
|
-
#
|
225
|
+
# 레이트 리밋 키 생성
|
204
226
|
if key_func:
|
205
227
|
rate_limit_key = f"rate_limit:{key_func(request)}"
|
206
228
|
else:
|
207
229
|
client_ip = request.client.host
|
208
230
|
rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
|
209
231
|
|
210
|
-
|
211
|
-
|
212
|
-
|
232
|
+
now = datetime.now(UTC)
|
233
|
+
|
234
|
+
# 현재 rate limit 정보 가져오기
|
235
|
+
rate_info = rate_limits.get(rate_limit_key)
|
236
|
+
|
237
|
+
if rate_info is None or (now - rate_info["start_time"]).total_seconds() >= window_seconds:
|
238
|
+
# 새로운 rate limit 설정
|
239
|
+
rate_limits[rate_limit_key] = {
|
240
|
+
"count": 1,
|
241
|
+
"start_time": now
|
242
|
+
}
|
243
|
+
else:
|
244
|
+
# 기존 rate limit 업데이트
|
245
|
+
if rate_info["count"] >= max_requests:
|
246
|
+
# rate limit 초과
|
247
|
+
remaining_seconds = window_seconds - (now - rate_info["start_time"]).total_seconds()
|
213
248
|
raise RateLimitExceeded(
|
214
|
-
detail=
|
249
|
+
detail=rate_limit_key,
|
215
250
|
source_function=func.__name__,
|
216
|
-
remaining_seconds=
|
251
|
+
remaining_seconds=remaining_seconds,
|
217
252
|
max_requests=max_requests,
|
218
253
|
window_seconds=window_seconds
|
219
254
|
)
|
220
|
-
|
255
|
+
rate_info["count"] += 1
|
256
|
+
|
257
|
+
try:
|
258
|
+
# 원래 함수 실행
|
221
259
|
return await func(*args, **kwargs)
|
222
|
-
|
223
|
-
|
260
|
+
except CustomException as e:
|
261
|
+
# CustomException은 그대로 전파
|
224
262
|
raise e
|
225
263
|
except Exception as e:
|
226
|
-
|
264
|
+
# 다른 예외는 INTERNAL_ERROR로 래핑
|
265
|
+
raise CustomException(
|
227
266
|
ErrorCode.INTERNAL_ERROR,
|
228
267
|
detail=str(e),
|
229
|
-
source_function=
|
268
|
+
source_function=func.__name__,
|
230
269
|
original_error=e
|
231
270
|
)
|
232
271
|
|
@@ -288,10 +327,10 @@ async def create_jwt_token(
|
|
288
327
|
required_fields = {"username", "ulid"}
|
289
328
|
missing_fields = required_fields - set(user_data.keys())
|
290
329
|
if missing_fields:
|
291
|
-
raise
|
292
|
-
|
293
|
-
|
294
|
-
|
330
|
+
raise CustomException(
|
331
|
+
ErrorCode.REQUIRED_FIELD_MISSING,
|
332
|
+
detail="|".join(missing_fields),
|
333
|
+
source_function="security.create_jwt_token"
|
295
334
|
)
|
296
335
|
|
297
336
|
if token_type == "access":
|
@@ -337,10 +376,10 @@ async def create_jwt_token(
|
|
337
376
|
algorithm=settings.JWT_ALGORITHM
|
338
377
|
)
|
339
378
|
except Exception as e:
|
340
|
-
raise
|
341
|
-
|
342
|
-
|
343
|
-
|
379
|
+
raise CustomException(
|
380
|
+
ErrorCode.INTERNAL_ERROR,
|
381
|
+
detail=f"token|{token_type}",
|
382
|
+
source_function="security.create_jwt_token",
|
344
383
|
original_error=e
|
345
384
|
)
|
346
385
|
|
@@ -362,13 +401,13 @@ async def create_jwt_token(
|
|
362
401
|
|
363
402
|
return token
|
364
403
|
|
365
|
-
except
|
404
|
+
except CustomException as e:
|
366
405
|
raise e
|
367
406
|
except Exception as e:
|
368
|
-
raise
|
407
|
+
raise CustomException(
|
369
408
|
ErrorCode.INTERNAL_ERROR,
|
370
409
|
detail=str(e),
|
371
|
-
source_function="create_jwt_token",
|
410
|
+
source_function="security.create_jwt_token",
|
372
411
|
original_error=e
|
373
412
|
)
|
374
413
|
|
@@ -376,7 +415,18 @@ async def verify_jwt_token(
|
|
376
415
|
token: str,
|
377
416
|
expected_type: Optional[Literal["access", "refresh"]] = None
|
378
417
|
) -> Dict[str, Any]:
|
379
|
-
"""JWT 토큰을 검증합니다.
|
418
|
+
"""JWT 토큰을 검증합니다.
|
419
|
+
|
420
|
+
Args:
|
421
|
+
token: 검증할 JWT 토큰
|
422
|
+
expected_type: 예상되는 토큰 타입
|
423
|
+
|
424
|
+
Returns:
|
425
|
+
Dict[str, Any]: 토큰 페이로드
|
426
|
+
|
427
|
+
Raises:
|
428
|
+
CustomException: 토큰 검증 실패 시
|
429
|
+
"""
|
380
430
|
try:
|
381
431
|
settings = get_settings()
|
382
432
|
# 토큰 디코딩
|
@@ -390,25 +440,27 @@ async def verify_jwt_token(
|
|
390
440
|
|
391
441
|
# 토큰 타입 검증 (expected_type이 주어진 경우에만)
|
392
442
|
if expected_type and payload.get("token_type") != expected_type:
|
393
|
-
raise
|
394
|
-
|
395
|
-
|
443
|
+
raise CustomException(
|
444
|
+
ErrorCode.INVALID_TOKEN,
|
445
|
+
detail=f"token|{expected_type}|{payload.get('token_type')}",
|
446
|
+
source_function="security.verify_jwt_token"
|
396
447
|
)
|
397
448
|
|
398
449
|
return payload
|
399
450
|
|
400
451
|
except JWTError as e:
|
401
|
-
raise
|
402
|
-
|
403
|
-
|
452
|
+
raise CustomException(
|
453
|
+
ErrorCode.INVALID_TOKEN,
|
454
|
+
detail=token[:10] + "...",
|
455
|
+
source_function="security.verify_jwt_token",
|
404
456
|
original_error=e
|
405
457
|
)
|
406
|
-
except
|
458
|
+
except CustomException as e:
|
407
459
|
raise e
|
408
460
|
except Exception as e:
|
409
|
-
raise
|
461
|
+
raise CustomException(
|
410
462
|
ErrorCode.INTERNAL_ERROR,
|
411
463
|
detail=str(e),
|
412
|
-
source_function="verify_jwt_token",
|
464
|
+
source_function="security.verify_jwt_token",
|
413
465
|
original_error=e
|
414
466
|
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|