aiteamutils 0.2.52__tar.gz → 0.2.54__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.52
3
+ Version: 0.2.54
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
@@ -0,0 +1,207 @@
1
+ """데이터베이스 유틸리티 모듈."""
2
+ from typing import Any, Dict, Optional, Type, List, Union
3
+ from sqlalchemy import select, and_
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+ from sqlalchemy.exc import IntegrityError, SQLAlchemyError
6
+
7
+ from .exceptions import ErrorCode, CustomException
8
+ from .base_model import Base
9
+
10
+ class DatabaseService:
11
+ def __init__(self, session: AsyncSession):
12
+ """DatabaseService 초기화
13
+
14
+ Args:
15
+ session (AsyncSession): 외부에서 주입받은 데이터베이스 세션
16
+ """
17
+ self._session = session
18
+
19
+ @property
20
+ def session(self) -> AsyncSession:
21
+ """현재 세션을 반환합니다."""
22
+ if self._session is None:
23
+ raise CustomException(
24
+ ErrorCode.DB_CONNECTION_ERROR,
25
+ detail="session",
26
+ source_function="DatabaseService.session"
27
+ )
28
+ return self._session
29
+
30
+ async def create_entity(
31
+ self,
32
+ model: Type[Base],
33
+ entity_data: Dict[str, Any]
34
+ ) -> Any:
35
+ """엔티티를 생성합니다.
36
+
37
+ Args:
38
+ model: 모델 클래스
39
+ entity_data: 생성할 엔티티 데이터
40
+
41
+ Returns:
42
+ 생성된 엔티티
43
+
44
+ Raises:
45
+ CustomException: 엔티티 생성 실패 시
46
+ """
47
+ try:
48
+ entity = model(**entity_data)
49
+ self.session.add(entity)
50
+ await self.session.flush()
51
+ await self.session.refresh(entity)
52
+ return entity
53
+ except IntegrityError as e:
54
+ await self.session.rollback()
55
+ raise CustomException(
56
+ ErrorCode.DB_INTEGRITY_ERROR,
57
+ detail=str(e),
58
+ source_function="DatabaseService.create_entity",
59
+ original_error=e
60
+ )
61
+ except Exception as e:
62
+ await self.session.rollback()
63
+ raise CustomException(
64
+ ErrorCode.DB_CREATE_ERROR,
65
+ detail=str(e),
66
+ source_function="DatabaseService.create_entity",
67
+ original_error=e
68
+ )
69
+
70
+ async def get_entity(
71
+ self,
72
+ model: Type[Base],
73
+ filters: Dict[str, Any]
74
+ ) -> Optional[Any]:
75
+ """필터 조건으로 엔티티를 조회합니다.
76
+
77
+ Args:
78
+ model: 모델 클래스
79
+ filters: 필터 조건
80
+
81
+ Returns:
82
+ 조회된 엔티티 또는 None
83
+
84
+ Raises:
85
+ CustomException: 조회 실패 시
86
+ """
87
+ try:
88
+ stmt = select(model).filter_by(**filters)
89
+ result = await self.session.execute(stmt)
90
+ return result.scalars().first()
91
+ except Exception as e:
92
+ raise CustomException(
93
+ ErrorCode.DB_QUERY_ERROR,
94
+ detail=str(e),
95
+ source_function="DatabaseService.get_entity",
96
+ original_error=e
97
+ )
98
+
99
+ async def list_entities(
100
+ self,
101
+ model: Type[Base],
102
+ filters: Optional[Dict[str, Any]] = None,
103
+ skip: int = 0,
104
+ limit: int = 100
105
+ ) -> List[Any]:
106
+ """엔티티 목록을 조회합니다.
107
+
108
+ Args:
109
+ model: 모델 클래스
110
+ filters: 필터 조건
111
+ skip: 건너뛸 레코드 수
112
+ limit: 조회할 최대 레코드 수
113
+
114
+ Returns:
115
+ 엔티티 목록
116
+
117
+ Raises:
118
+ CustomException: 조회 실패 시
119
+ """
120
+ try:
121
+ stmt = select(model)
122
+ if filters:
123
+ stmt = stmt.filter_by(**filters)
124
+ stmt = stmt.offset(skip).limit(limit)
125
+ result = await self.session.execute(stmt)
126
+ return result.scalars().all()
127
+ except Exception as e:
128
+ raise CustomException(
129
+ ErrorCode.DB_QUERY_ERROR,
130
+ detail=str(e),
131
+ source_function="DatabaseService.list_entities",
132
+ original_error=e
133
+ )
134
+
135
+ async def update_entity(
136
+ self,
137
+ entity: Base,
138
+ update_data: Dict[str, Any]
139
+ ) -> Any:
140
+ """엔티티를 수정합니다.
141
+
142
+ Args:
143
+ entity: 수정할 엔티티
144
+ update_data: 수정할 데이터
145
+
146
+ Returns:
147
+ 수정된 엔티티
148
+
149
+ Raises:
150
+ CustomException: 수정 실패 시
151
+ """
152
+ try:
153
+ for key, value in update_data.items():
154
+ setattr(entity, key, value)
155
+ await self.session.flush()
156
+ await self.session.refresh(entity)
157
+ return entity
158
+ except IntegrityError as e:
159
+ await self.session.rollback()
160
+ raise CustomException(
161
+ ErrorCode.DB_INTEGRITY_ERROR,
162
+ detail=str(e),
163
+ source_function="DatabaseService.update_entity",
164
+ original_error=e
165
+ )
166
+ except Exception as e:
167
+ await self.session.rollback()
168
+ raise CustomException(
169
+ ErrorCode.DB_UPDATE_ERROR,
170
+ detail=str(e),
171
+ source_function="DatabaseService.update_entity",
172
+ original_error=e
173
+ )
174
+
175
+ async def delete_entity(
176
+ self,
177
+ entity: Base,
178
+ soft_delete: bool = True
179
+ ) -> bool:
180
+ """엔티티를 삭제합니다.
181
+
182
+ Args:
183
+ entity: 삭제할 엔티티
184
+ soft_delete: 소프트 삭제 여부
185
+
186
+ Returns:
187
+ 삭제 성공 여부
188
+
189
+ Raises:
190
+ CustomException: 삭제 실패 시
191
+ """
192
+ try:
193
+ if soft_delete:
194
+ entity.is_deleted = True
195
+ await self.session.flush()
196
+ else:
197
+ await self.session.delete(entity)
198
+ await self.session.flush()
199
+ return True
200
+ except Exception as e:
201
+ await self.session.rollback()
202
+ raise CustomException(
203
+ ErrorCode.DB_DELETE_ERROR,
204
+ detail=str(e),
205
+ source_function="DatabaseService.delete_entity",
206
+ original_error=e
207
+ )
@@ -9,33 +9,13 @@ from .exceptions import CustomException, ErrorCode
9
9
  from .config import get_settings
10
10
  from .base_service import BaseService
11
11
  from .base_repository import BaseRepository
12
- from .database import db_manager
12
+ from .database import DatabaseService
13
13
 
14
14
  T = TypeVar("T", bound=BaseService)
15
15
  R = TypeVar("R", bound=BaseRepository)
16
16
 
17
17
  _service_registry: Dict[str, Dict[str, Any]] = {}
18
18
 
19
- async def get_db() -> AsyncGenerator[AsyncSession, None]:
20
- """데이터베이스 세션을 반환합니다.
21
-
22
- Yields:
23
- AsyncSession: 데이터베이스 세션
24
-
25
- Raises:
26
- CustomException: 세션 생성 실패 시
27
- """
28
- try:
29
- async with db_manager.get_session() as session:
30
- yield session
31
- except Exception as e:
32
- raise CustomException(
33
- ErrorCode.DATABASE_ERROR,
34
- detail=str(e),
35
- source_function="dependencies.get_db",
36
- original_error=e
37
- )
38
-
39
19
  def register_service(
40
20
  service_class: Type[T],
41
21
  repository_class: Optional[Type[R]] = None,
@@ -86,6 +66,9 @@ async def _get_service(
86
66
  repository_class = service_info["repository_class"]
87
67
  dependencies = service_info["dependencies"]
88
68
 
69
+ # 데이터베이스 서비스 생성
70
+ db_service = DatabaseService(session=session)
71
+
89
72
  # 저장소 인스턴스 생성
90
73
  repository = None
91
74
  if repository_class:
@@ -93,8 +76,8 @@ async def _get_service(
93
76
 
94
77
  # 서비스 인스턴스 생성
95
78
  service = service_class(
79
+ db=db_service,
96
80
  repository=repository,
97
- session=session,
98
81
  request=request,
99
82
  **dependencies
100
83
  )
@@ -122,14 +105,14 @@ def get_service(service_name: str) -> Callable:
122
105
  """
123
106
  async def _get_service_dependency(
124
107
  request: Request,
125
- session: AsyncSession = Depends(get_db)
108
+ session: AsyncSession
126
109
  ) -> BaseService:
127
110
  return await _get_service(service_name, session, request)
128
111
  return _get_service_dependency
129
112
 
130
113
  async def get_current_user(
131
114
  request: Request,
132
- session: AsyncSession = Depends(get_db),
115
+ session: AsyncSession,
133
116
  auth_service: BaseService = Depends(get_service("AuthService"))
134
117
  ) -> Dict[str, Any]:
135
118
  """현재 사용자 정보를 반환합니다.
@@ -1,12 +1,10 @@
1
1
  """보안 관련 유틸리티."""
2
2
  from datetime import datetime, timedelta, UTC
3
- from typing import Dict, Any, Optional, Literal, Callable, Type
4
- from sqlalchemy.orm import DeclarativeBase as Base
3
+ from typing import Dict, Any, Optional, Literal, Callable, TYPE_CHECKING
5
4
  from fastapi import Request, HTTPException, status
6
5
  from functools import wraps
7
6
  from jose import jwt, JWTError
8
7
  from passlib.context import CryptContext
9
- from sqlalchemy.ext.asyncio import AsyncSession
10
8
  import logging
11
9
 
12
10
  from .exceptions import CustomException, ErrorCode
@@ -20,29 +18,12 @@ _rate_limits: Dict[str, Dict[str, Any]] = {}
20
18
 
21
19
  class RateLimitExceeded(CustomException):
22
20
  """Rate limit 초과 예외."""
23
-
24
- def __init__(
25
- self,
26
- detail: str,
27
- source_function: str,
28
- remaining_seconds: float,
29
- max_requests: int,
30
- window_seconds: int
31
- ):
32
- """Rate limit 초과 예외를 초기화합니다."""
21
+ def __init__(self, detail: str, source_function: str):
33
22
  super().__init__(
34
23
  ErrorCode.RATE_LIMIT_EXCEEDED,
35
24
  detail=detail,
36
- source_function=source_function,
37
- metadata={
38
- "remaining_seconds": remaining_seconds,
39
- "max_requests": max_requests,
40
- "window_seconds": window_seconds
41
- }
25
+ source_function=source_function
42
26
  )
43
- self.remaining_seconds = remaining_seconds
44
- self.max_requests = max_requests
45
- self.window_seconds = window_seconds
46
27
 
47
28
  def verify_password(plain_password: str, hashed_password: str) -> bool:
48
29
  """비밀번호를 검증합니다."""
@@ -68,28 +49,97 @@ def hash_password(password: str) -> str:
68
49
  original_error=e
69
50
  )
70
51
 
52
+ def rate_limit(
53
+ max_requests: int,
54
+ window_seconds: int,
55
+ key_func: Optional[Callable] = None
56
+ ):
57
+ """Rate limiting 데코레이터."""
58
+ def decorator(func: Callable) -> Callable:
59
+ @wraps(func)
60
+ async def wrapper(*args, **kwargs):
61
+ # Request 객체 찾기
62
+ request = None
63
+ for arg in args:
64
+ if isinstance(arg, Request):
65
+ request = arg
66
+ break
67
+ if not request:
68
+ for arg in kwargs.values():
69
+ if isinstance(arg, Request):
70
+ request = arg
71
+ break
72
+ if not request:
73
+ raise CustomException(
74
+ ErrorCode.INTERNAL_ERROR,
75
+ detail="Request object not found",
76
+ source_function="rate_limit"
77
+ )
78
+
79
+ # 레이트 리밋 키 생성
80
+ if key_func:
81
+ rate_limit_key = f"rate_limit:{key_func(request)}"
82
+ else:
83
+ client_ip = request.client.host
84
+ rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
85
+
86
+ try:
87
+ now = datetime.now(UTC)
88
+
89
+ # 현재 rate limit 정보 가져오기
90
+ rate_info = _rate_limits.get(rate_limit_key)
91
+
92
+ if rate_info is None or (now - rate_info["start_time"]).total_seconds() >= window_seconds:
93
+ # 새로운 rate limit 설정
94
+ _rate_limits[rate_limit_key] = {
95
+ "count": 1,
96
+ "start_time": now
97
+ }
98
+ else:
99
+ # 기존 rate limit 업데이트
100
+ if rate_info["count"] >= max_requests:
101
+ # rate limit 초과
102
+ remaining_seconds = window_seconds - (now - rate_info["start_time"]).total_seconds()
103
+ raise CustomException(
104
+ ErrorCode.RATE_LIMIT_EXCEEDED,
105
+ detail=f"{int(remaining_seconds)}",
106
+ source_function=func.__name__
107
+ )
108
+ rate_info["count"] += 1
109
+
110
+ try:
111
+ # 원래 함수 실행
112
+ return await func(*args, **kwargs)
113
+ except CustomException as e:
114
+ raise e
115
+ except Exception as e:
116
+ raise CustomException(
117
+ ErrorCode.INTERNAL_ERROR,
118
+ detail=str(e),
119
+ source_function=func.__name__,
120
+ original_error=e
121
+ )
122
+
123
+ except CustomException as e:
124
+ raise e
125
+ except Exception as e:
126
+ raise CustomException(
127
+ ErrorCode.INTERNAL_ERROR,
128
+ detail=str(e),
129
+ source_function="rate_limit",
130
+ original_error=e
131
+ )
132
+
133
+ return wrapper
134
+ return decorator
135
+
71
136
  async def create_jwt_token(
72
137
  user_data: Dict[str, Any],
73
138
  token_type: Literal["access", "refresh"],
74
- session: AsyncSession,
75
- log_model: Type[Base],
139
+ db_service: Any,
76
140
  request: Optional[Request] = None
77
141
  ) -> str:
78
- """JWT 토큰을 생성합니다.
79
-
80
- Args:
81
- user_data (Dict[str, Any]): 사용자 데이터
82
- token_type (Literal["access", "refresh"]): 토큰 타입
83
- session (AsyncSession): 데이터베이스 세션
84
- log_model (Type[Base]): 로그 모델
85
- request (Optional[Request], optional): FastAPI 요청 객체. Defaults to None.
86
-
87
- Returns:
88
- str: 생성된 JWT 토큰
89
-
90
- Raises:
91
- CustomException: 토큰 생성 실패 시
92
- """
142
+ """JWT 토큰을 생성하고 로그를 기록합니다."""
93
143
  try:
94
144
  settings = get_settings()
95
145
 
@@ -98,7 +148,7 @@ async def create_jwt_token(
98
148
  token_data = {
99
149
  # 등록 클레임
100
150
  "iss": settings.token_issuer,
101
- "sub": user_data["ulid"],
151
+ "sub": user_data["username"],
102
152
  "aud": settings.token_audience,
103
153
  "exp": expires_at,
104
154
 
@@ -123,7 +173,7 @@ async def create_jwt_token(
123
173
  expires_at = datetime.now(UTC) + timedelta(days=14)
124
174
  token_data = {
125
175
  "iss": settings.token_issuer,
126
- "sub": user_data["ulid"],
176
+ "sub": user_data["username"],
127
177
  "exp": expires_at,
128
178
  "token_type": token_type,
129
179
  "user_ulid": user_data["ulid"]
@@ -146,13 +196,14 @@ async def create_jwt_token(
146
196
  # 로그 생성
147
197
  try:
148
198
  activity_type = ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED
149
- log_entry = log_model(
150
- type=activity_type,
151
- user_ulid=user_data["ulid"],
152
- token=token
199
+ await db_service.create_log(
200
+ {
201
+ "type": activity_type,
202
+ "user_ulid": user_data["ulid"],
203
+ "token": token
204
+ },
205
+ request
153
206
  )
154
- session.add(log_entry)
155
- await session.flush()
156
207
  except Exception as e:
157
208
  # 로그 생성 실패는 토큰 생성에 영향을 주지 않음
158
209
  logging.error(f"Failed to create token log: {str(e)}")
@@ -173,18 +224,7 @@ async def verify_jwt_token(
173
224
  token: str,
174
225
  expected_type: Optional[Literal["access", "refresh"]] = None
175
226
  ) -> Dict[str, Any]:
176
- """JWT 토큰을 검증합니다.
177
-
178
- Args:
179
- token: 검증할 JWT 토큰
180
- expected_type: 예상되는 토큰 타입
181
-
182
- Returns:
183
- Dict[str, Any]: 토큰 페이로드
184
-
185
- Raises:
186
- CustomException: 토큰 검증 실패 시
187
- """
227
+ """JWT 토큰을 검증합니다."""
188
228
  try:
189
229
  settings = get_settings()
190
230
 
@@ -202,23 +242,14 @@ async def verify_jwt_token(
202
242
  if not token_type:
203
243
  raise CustomException(
204
244
  ErrorCode.INVALID_TOKEN,
205
- detail="Token type is missing",
245
+ detail=token,
206
246
  source_function="security.verify_jwt_token"
207
247
  )
208
248
 
209
249
  if expected_type and token_type != expected_type:
210
250
  raise CustomException(
211
251
  ErrorCode.INVALID_TOKEN,
212
- detail=f"Expected {expected_type} token but got {token_type}",
213
- source_function="security.verify_jwt_token"
214
- )
215
-
216
- # 사용자 식별자 검증
217
- user_ulid = payload.get("user_ulid")
218
- if not user_ulid:
219
- raise CustomException(
220
- ErrorCode.INVALID_TOKEN,
221
- detail="User identifier is missing",
252
+ detail=token,
222
253
  source_function="security.verify_jwt_token"
223
254
  )
224
255
 
@@ -227,7 +258,7 @@ async def verify_jwt_token(
227
258
  except JWTError as e:
228
259
  raise CustomException(
229
260
  ErrorCode.INVALID_TOKEN,
230
- detail=str(e),
261
+ detail=token,
231
262
  source_function="security.verify_jwt_token",
232
263
  original_error=e
233
264
  )
@@ -239,4 +270,22 @@ async def verify_jwt_token(
239
270
  detail=str(e),
240
271
  source_function="security.verify_jwt_token",
241
272
  original_error=e
273
+ )
274
+
275
+ def validate_token(token: str) -> Dict[str, Any]:
276
+ """JWT 토큰을 검증하고 페이로드를 반환합니다."""
277
+ try:
278
+ settings = get_settings()
279
+ payload = jwt.decode(
280
+ token,
281
+ settings.jwt_secret,
282
+ algorithms=[settings.jwt_algorithm]
283
+ )
284
+ return payload
285
+ except JWTError as e:
286
+ raise CustomException(
287
+ ErrorCode.INVALID_TOKEN,
288
+ detail=token,
289
+ source_function="security.validate_token",
290
+ original_error=e
242
291
  )
@@ -0,0 +1,2 @@
1
+ """버전 정보"""
2
+ __version__ = "0.2.54"