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.
@@ -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,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.32
3
+ Version: 0.2.34
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,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
- RuntimeError: 설정이 초기화되지 않은 경우
78
+ CustomException: 설정이 초기화되지 않은 경우
78
79
  """
79
80
  if _settings is None:
80
- raise RuntimeError("Settings not initialized. Call init_settings first.")
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
- RuntimeError: DatabaseService가 초기화되지 않은 경우
29
+ CustomException: DatabaseService가 초기화되지 않은 경우
30
30
  """
31
31
  if _database_service is None:
32
- raise RuntimeError("DatabaseService not initialized. Call init_settings with db_url first.")
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 ValueError("Either db_url or session must be provided")
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 RuntimeError("Session factory not initialized")
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
- ValueError: 이미 등록된 서비스인 경우
29
+ CustomException: 이미 등록된 서비스인 경우
30
30
  """
31
31
  if name in self._services:
32
- raise ValueError(f"Service '{name}' is already registered.")
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
- ValueError: 등록되지 않은 서비스인 경우
49
+ CustomException: 등록되지 않은 서비스인 경우
46
50
  """
47
51
  if name not in self._services:
48
- raise ValueError(f"Service '{name}' is not registered.")
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
- return pwd_context.verify(plain_password, hashed_password)
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
- return pwd_context.hash(password)
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
- limiter = RateLimiter(max_requests, window_seconds)
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 SecurityError(
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
- # Rate limit 키 생성
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
- try:
211
- if not limiter.is_allowed(rate_limit_key):
212
- remaining_time = limiter.get_remaining_time(rate_limit_key)
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=f"Rate limit exceeded. Try again in {int(remaining_time)} seconds",
249
+ detail=rate_limit_key,
215
250
  source_function=func.__name__,
216
- remaining_seconds=remaining_time,
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
- except (RateLimitExceeded, SecurityError) as e:
260
+ except CustomException as e:
261
+ # CustomException은 그대로 전파
224
262
  raise e
225
263
  except Exception as e:
226
- raise SecurityError(
264
+ # 다른 예외는 INTERNAL_ERROR로 래핑
265
+ raise CustomException(
227
266
  ErrorCode.INTERNAL_ERROR,
228
267
  detail=str(e),
229
- source_function="rate_limit",
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 TokenCreationError(
292
- detail=f"Missing required fields: {', '.join(missing_fields)}",
293
- source_function="create_jwt_token",
294
- token_type=token_type
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 TokenCreationError(
341
- detail="Failed to encode JWT token",
342
- source_function="create_jwt_token",
343
- token_type=token_type,
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 (TokenCreationError, SecurityError) as e:
404
+ except CustomException as e:
366
405
  raise e
367
406
  except Exception as e:
368
- raise SecurityError(
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 TokenError(
394
- detail=f"Expected {expected_type} token but got {payload.get('token_type')}",
395
- source_function="verify_jwt_token"
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 TokenError(
402
- detail=str(e),
403
- source_function="verify_jwt_token",
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 (TokenError, SecurityError) as e:
458
+ except CustomException as e:
407
459
  raise e
408
460
  except Exception as e:
409
- raise SecurityError(
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
  )
@@ -0,0 +1,2 @@
1
+ """버전 정보"""
2
+ __version__ = "0.2.34"
@@ -1,2 +0,0 @@
1
- """버전 정보"""
2
- __version__ = "0.2.32"
File without changes
File without changes
File without changes