aiteamutils 0.2.32__py3-none-any.whl → 0.2.34__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/config.py +7 -2
- aiteamutils/database.py +16 -4
- aiteamutils/dependencies.py +12 -4
- aiteamutils/security.py +88 -36
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.32.dist-info → aiteamutils-0.2.34.dist-info}/METADATA +1 -1
- {aiteamutils-0.2.32.dist-info → aiteamutils-0.2.34.dist-info}/RECORD +8 -8
- {aiteamutils-0.2.32.dist-info → aiteamutils-0.2.34.dist-info}/WHEEL +0 -0
aiteamutils/config.py
CHANGED
@@ -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
|
aiteamutils/database.py
CHANGED
@@ -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:
|
aiteamutils/dependencies.py
CHANGED
@@ -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 초기화
|
aiteamutils/security.py
CHANGED
@@ -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
|
)
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.34"
|
@@ -3,14 +3,14 @@ aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,270
|
|
3
3
|
aiteamutils/base_repository.py,sha256=qdwQ7Sj2fUqxpDg6cWM48n_QbwPK_VUlG9zTSem8iCk,18968
|
4
4
|
aiteamutils/base_service.py,sha256=E4dHGE0DvhmRyFplh46SwKJOSF_nUL7OAsCkf_ZJF_8,24733
|
5
5
|
aiteamutils/cache.py,sha256=tr0Yn8VPYA9QHiKCUzciVlQ2J1RAwNo2K9lGMH4rY3s,1334
|
6
|
-
aiteamutils/config.py,sha256=
|
7
|
-
aiteamutils/database.py,sha256=
|
8
|
-
aiteamutils/dependencies.py,sha256=
|
6
|
+
aiteamutils/config.py,sha256=kFKMeIx1KcuEwwx4VjZdCgoTOHCkG3ySYVJ0G6cvMoA,2849
|
7
|
+
aiteamutils/database.py,sha256=CaH73g8PPNcmLCz4Xr0DBtNSrLcpRlyt0F1zO5Tigfo,33811
|
8
|
+
aiteamutils/dependencies.py,sha256=GgN8sFM7qGNmOfpLaFrhuHOgPlBVXWrEgxv29jxjEgI,4226
|
9
9
|
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
10
10
|
aiteamutils/exceptions.py,sha256=YV-ISya4wQlHk4twvGo16I5r8h22-tXpn9wa-b3WwDM,15231
|
11
|
-
aiteamutils/security.py,sha256=
|
11
|
+
aiteamutils/security.py,sha256=NHiX0rIznYqMVqSIDFj1u2qjsJb6YFgC0GMjzVjnbWk,15135
|
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=t69ncPrInx6hcKQn1rb-2H6AE0Aas3A-6K--9iJOFlg,42
|
14
|
+
aiteamutils-0.2.34.dist-info/METADATA,sha256=Nq_9Q5465EIAShF1fdxThWkhL5WpELqkea3ffyic28g,1718
|
15
|
+
aiteamutils-0.2.34.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.34.dist-info/RECORD,,
|
File without changes
|