aiteamutils 0.2.0__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/__init__.py +60 -0
- aiteamutils/base_model.py +81 -0
- aiteamutils/base_repository.py +503 -0
- aiteamutils/base_service.py +668 -0
- aiteamutils/cache.py +48 -0
- aiteamutils/config.py +26 -0
- aiteamutils/database.py +823 -0
- aiteamutils/dependencies.py +158 -0
- aiteamutils/enums.py +23 -0
- aiteamutils/exceptions.py +333 -0
- aiteamutils/security.py +396 -0
- aiteamutils/validators.py +188 -0
- aiteamutils-0.2.0.dist-info/METADATA +72 -0
- aiteamutils-0.2.0.dist-info/RECORD +15 -0
- aiteamutils-0.2.0.dist-info/WHEEL +4 -0
aiteamutils/security.py
ADDED
@@ -0,0 +1,396 @@
|
|
1
|
+
"""보안 관련 유틸리티."""
|
2
|
+
from datetime import datetime, timedelta, UTC
|
3
|
+
from typing import Dict, Any, Optional, Literal, Callable
|
4
|
+
from fastapi import Request, HTTPException, status
|
5
|
+
from functools import wraps
|
6
|
+
from jose import jwt, JWTError
|
7
|
+
from passlib.context import CryptContext
|
8
|
+
|
9
|
+
from .exceptions import CustomException, ErrorCode
|
10
|
+
from .database import DatabaseService
|
11
|
+
from .enums import ActivityType
|
12
|
+
from .config import settings
|
13
|
+
|
14
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
15
|
+
|
16
|
+
class RateLimitExceeded(CustomException):
|
17
|
+
"""Rate limit 초과 예외."""
|
18
|
+
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
detail: str,
|
22
|
+
source_function: str,
|
23
|
+
remaining_seconds: float,
|
24
|
+
max_requests: int,
|
25
|
+
window_seconds: int
|
26
|
+
):
|
27
|
+
"""Rate limit 초과 예외를 초기화합니다.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
detail: 상세 메시지
|
31
|
+
source_function: 예외가 발생한 함수명
|
32
|
+
remaining_seconds: 다음 요청까지 남은 시간 (초)
|
33
|
+
max_requests: 허용된 최대 요청 수
|
34
|
+
window_seconds: 시간 윈도우 (초)
|
35
|
+
"""
|
36
|
+
super().__init__(
|
37
|
+
ErrorCode.RATE_LIMIT_EXCEEDED,
|
38
|
+
detail=detail,
|
39
|
+
source_function=source_function,
|
40
|
+
metadata={
|
41
|
+
"remaining_seconds": remaining_seconds,
|
42
|
+
"max_requests": max_requests,
|
43
|
+
"window_seconds": window_seconds
|
44
|
+
}
|
45
|
+
)
|
46
|
+
self.remaining_seconds = remaining_seconds
|
47
|
+
self.max_requests = max_requests
|
48
|
+
self.window_seconds = window_seconds
|
49
|
+
|
50
|
+
class SecurityError(CustomException):
|
51
|
+
"""보안 관련 기본 예외."""
|
52
|
+
|
53
|
+
def __init__(
|
54
|
+
self,
|
55
|
+
error_code: ErrorCode,
|
56
|
+
detail: str,
|
57
|
+
source_function: str,
|
58
|
+
original_error: Optional[Exception] = None
|
59
|
+
):
|
60
|
+
"""보안 관련 예외를 초기화합니다.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
error_code: 에러 코드
|
64
|
+
detail: 상세 메시지
|
65
|
+
source_function: 예외가 발생한 함수명
|
66
|
+
original_error: 원본 예외
|
67
|
+
"""
|
68
|
+
super().__init__(
|
69
|
+
error_code,
|
70
|
+
detail=detail,
|
71
|
+
source_function=f"security.{source_function}",
|
72
|
+
original_error=original_error
|
73
|
+
)
|
74
|
+
|
75
|
+
class TokenError(SecurityError):
|
76
|
+
"""토큰 관련 예외."""
|
77
|
+
|
78
|
+
def __init__(
|
79
|
+
self,
|
80
|
+
detail: str,
|
81
|
+
source_function: str,
|
82
|
+
original_error: Optional[Exception] = None
|
83
|
+
):
|
84
|
+
"""토큰 관련 예외를 초기화합니다."""
|
85
|
+
super().__init__(
|
86
|
+
ErrorCode.INVALID_TOKEN,
|
87
|
+
detail=detail,
|
88
|
+
source_function=source_function,
|
89
|
+
original_error=original_error
|
90
|
+
)
|
91
|
+
|
92
|
+
class RateLimiter:
|
93
|
+
"""Rate limit 관리 클래스."""
|
94
|
+
|
95
|
+
def __init__(self, max_requests: int, window_seconds: int):
|
96
|
+
"""Rate limiter를 초기화합니다.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
max_requests: 허용된 최대 요청 수
|
100
|
+
window_seconds: 시간 윈도우 (초)
|
101
|
+
"""
|
102
|
+
self.max_requests = max_requests
|
103
|
+
self.window_seconds = window_seconds
|
104
|
+
self._cache: Dict[str, Dict[str, Any]] = {}
|
105
|
+
|
106
|
+
def is_allowed(self, key: str) -> bool:
|
107
|
+
"""현재 요청이 허용되는지 확인합니다.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
key: Rate limit 키
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
bool: 요청 허용 여부
|
114
|
+
"""
|
115
|
+
now = datetime.now(UTC)
|
116
|
+
rate_info = self._cache.get(key)
|
117
|
+
|
118
|
+
if rate_info is None or (now - rate_info["start_time"]).total_seconds() >= self.window_seconds:
|
119
|
+
self._cache[key] = {
|
120
|
+
"count": 1,
|
121
|
+
"start_time": now
|
122
|
+
}
|
123
|
+
return True
|
124
|
+
|
125
|
+
if rate_info["count"] >= self.max_requests:
|
126
|
+
return False
|
127
|
+
|
128
|
+
rate_info["count"] += 1
|
129
|
+
return True
|
130
|
+
|
131
|
+
def get_remaining_time(self, key: str) -> float:
|
132
|
+
"""남은 시간을 반환합니다.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
key: Rate limit 키
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
float: 남은 시간 (초)
|
139
|
+
"""
|
140
|
+
rate_info = self._cache.get(key)
|
141
|
+
if not rate_info:
|
142
|
+
return 0
|
143
|
+
|
144
|
+
now = datetime.now(UTC)
|
145
|
+
return max(
|
146
|
+
0,
|
147
|
+
self.window_seconds - (now - rate_info["start_time"]).total_seconds()
|
148
|
+
)
|
149
|
+
|
150
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
151
|
+
"""비밀번호를 검증합니다.
|
152
|
+
|
153
|
+
Args:
|
154
|
+
plain_password: 평문 비밀번호
|
155
|
+
hashed_password: 해시된 비밀번호
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
bool: 비밀번호 일치 여부
|
159
|
+
"""
|
160
|
+
return pwd_context.verify(plain_password, hashed_password)
|
161
|
+
|
162
|
+
def hash_password(password: str) -> str:
|
163
|
+
"""비밀번호를 해시화합니다.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
password: 평문 비밀번호
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
str: 해시된 비밀번호
|
170
|
+
"""
|
171
|
+
return pwd_context.hash(password)
|
172
|
+
|
173
|
+
def rate_limit(
|
174
|
+
max_requests: int,
|
175
|
+
window_seconds: int,
|
176
|
+
key_func: Optional[Callable] = None
|
177
|
+
):
|
178
|
+
"""Rate limiting 데코레이터."""
|
179
|
+
limiter = RateLimiter(max_requests, window_seconds)
|
180
|
+
|
181
|
+
def decorator(func: Callable) -> Callable:
|
182
|
+
@wraps(func)
|
183
|
+
async def wrapper(*args, **kwargs):
|
184
|
+
# Request 객체 찾기
|
185
|
+
request = None
|
186
|
+
for arg in args:
|
187
|
+
if isinstance(arg, Request):
|
188
|
+
request = arg
|
189
|
+
break
|
190
|
+
if not request:
|
191
|
+
for arg in kwargs.values():
|
192
|
+
if isinstance(arg, Request):
|
193
|
+
request = arg
|
194
|
+
break
|
195
|
+
if not request:
|
196
|
+
raise SecurityError(
|
197
|
+
ErrorCode.INTERNAL_ERROR,
|
198
|
+
detail="Request object not found",
|
199
|
+
source_function="rate_limit"
|
200
|
+
)
|
201
|
+
|
202
|
+
# Rate limit 키 생성
|
203
|
+
if key_func:
|
204
|
+
rate_limit_key = f"rate_limit:{key_func(request)}"
|
205
|
+
else:
|
206
|
+
client_ip = request.client.host
|
207
|
+
rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
|
208
|
+
|
209
|
+
try:
|
210
|
+
if not limiter.is_allowed(rate_limit_key):
|
211
|
+
remaining_time = limiter.get_remaining_time(rate_limit_key)
|
212
|
+
raise RateLimitExceeded(
|
213
|
+
detail=f"Rate limit exceeded. Try again in {int(remaining_time)} seconds",
|
214
|
+
source_function=func.__name__,
|
215
|
+
remaining_seconds=remaining_time,
|
216
|
+
max_requests=max_requests,
|
217
|
+
window_seconds=window_seconds
|
218
|
+
)
|
219
|
+
|
220
|
+
return await func(*args, **kwargs)
|
221
|
+
|
222
|
+
except (RateLimitExceeded, SecurityError) as e:
|
223
|
+
raise e
|
224
|
+
except Exception as e:
|
225
|
+
raise SecurityError(
|
226
|
+
ErrorCode.INTERNAL_ERROR,
|
227
|
+
detail=str(e),
|
228
|
+
source_function="rate_limit",
|
229
|
+
original_error=e
|
230
|
+
)
|
231
|
+
|
232
|
+
return wrapper
|
233
|
+
return decorator
|
234
|
+
|
235
|
+
class TokenCreationError(SecurityError):
|
236
|
+
"""토큰 생성 관련 예외."""
|
237
|
+
|
238
|
+
def __init__(
|
239
|
+
self,
|
240
|
+
detail: str,
|
241
|
+
source_function: str,
|
242
|
+
token_type: str,
|
243
|
+
original_error: Optional[Exception] = None
|
244
|
+
):
|
245
|
+
"""토큰 생성 예외를 초기화합니다.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
detail: 상세 메시지
|
249
|
+
source_function: 예외가 발생한 함수명
|
250
|
+
token_type: 토큰 타입
|
251
|
+
original_error: 원본 예외
|
252
|
+
"""
|
253
|
+
super().__init__(
|
254
|
+
ErrorCode.INTERNAL_ERROR,
|
255
|
+
detail=detail,
|
256
|
+
source_function=source_function,
|
257
|
+
original_error=original_error
|
258
|
+
)
|
259
|
+
self.token_type = token_type
|
260
|
+
|
261
|
+
async def create_jwt_token(
|
262
|
+
user_data: Dict[str, Any],
|
263
|
+
token_type: Literal["access", "refresh"],
|
264
|
+
db_service: DatabaseService,
|
265
|
+
log_model: Any,
|
266
|
+
request: Optional[Request] = None
|
267
|
+
) -> str:
|
268
|
+
"""JWT 토큰을 생성하고 로그를 기록합니다.
|
269
|
+
|
270
|
+
Args:
|
271
|
+
user_data: 사용자 데이터 (username, ulid 등 필수)
|
272
|
+
token_type: 토큰 타입 ("access" 또는 "refresh")
|
273
|
+
db_service: 데이터베이스 서비스
|
274
|
+
log_model: 로그를 저장할 모델 클래스
|
275
|
+
request: FastAPI 요청 객체
|
276
|
+
|
277
|
+
Returns:
|
278
|
+
str: 생성된 JWT 토큰
|
279
|
+
|
280
|
+
Raises:
|
281
|
+
TokenCreationError: 토큰 생성 실패 시
|
282
|
+
SecurityError: 기타 보안 관련 오류 발생 시
|
283
|
+
"""
|
284
|
+
try:
|
285
|
+
# 필수 필드 검증
|
286
|
+
required_fields = {"username", "ulid"}
|
287
|
+
missing_fields = required_fields - user_data.keys()
|
288
|
+
if missing_fields:
|
289
|
+
raise TokenCreationError(
|
290
|
+
detail=f"Missing required fields: {', '.join(missing_fields)}",
|
291
|
+
source_function="create_jwt_token",
|
292
|
+
token_type=token_type
|
293
|
+
)
|
294
|
+
|
295
|
+
# 토큰 데이터 생성
|
296
|
+
if token_type == "access":
|
297
|
+
expires_at = datetime.now(UTC) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
298
|
+
token_data = {
|
299
|
+
"iss": settings.TOKEN_ISSUER,
|
300
|
+
"sub": user_data["username"],
|
301
|
+
"aud": settings.TOKEN_AUDIENCE,
|
302
|
+
"exp": expires_at,
|
303
|
+
"token_type": token_type,
|
304
|
+
**user_data
|
305
|
+
}
|
306
|
+
else: # refresh token
|
307
|
+
expires_at = datetime.now(UTC) + timedelta(days=14)
|
308
|
+
token_data = {
|
309
|
+
"iss": settings.TOKEN_ISSUER,
|
310
|
+
"sub": user_data["username"],
|
311
|
+
"exp": expires_at,
|
312
|
+
"token_type": token_type,
|
313
|
+
"user_ulid": user_data["ulid"]
|
314
|
+
}
|
315
|
+
|
316
|
+
try:
|
317
|
+
token = jwt.encode(
|
318
|
+
token_data,
|
319
|
+
settings.JWT_SECRET,
|
320
|
+
algorithm=settings.JWT_ALGORITHM
|
321
|
+
)
|
322
|
+
except Exception as e:
|
323
|
+
raise TokenCreationError(
|
324
|
+
detail="Failed to encode JWT token",
|
325
|
+
source_function="create_jwt_token",
|
326
|
+
token_type=token_type,
|
327
|
+
original_error=e
|
328
|
+
)
|
329
|
+
|
330
|
+
# 로그 생성
|
331
|
+
try:
|
332
|
+
activity_type = ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED
|
333
|
+
await db_service.create_log(
|
334
|
+
model=log_model,
|
335
|
+
log_data={
|
336
|
+
"type": activity_type,
|
337
|
+
"user_ulid": user_data["ulid"],
|
338
|
+
"token": token
|
339
|
+
},
|
340
|
+
request=request
|
341
|
+
)
|
342
|
+
except Exception as e:
|
343
|
+
# 로그 생성 실패는 토큰 생성에 영향을 주지 않음
|
344
|
+
pass
|
345
|
+
|
346
|
+
return token
|
347
|
+
|
348
|
+
except (TokenCreationError, SecurityError) as e:
|
349
|
+
raise e
|
350
|
+
except Exception as e:
|
351
|
+
raise SecurityError(
|
352
|
+
ErrorCode.INTERNAL_ERROR,
|
353
|
+
detail=str(e),
|
354
|
+
source_function="create_jwt_token",
|
355
|
+
original_error=e
|
356
|
+
)
|
357
|
+
|
358
|
+
async def verify_jwt_token(
|
359
|
+
token: str,
|
360
|
+
expected_type: Optional[Literal["access", "refresh"]] = None
|
361
|
+
) -> Dict[str, Any]:
|
362
|
+
"""JWT 토큰을 검증합니다."""
|
363
|
+
try:
|
364
|
+
# 토큰 디코딩
|
365
|
+
payload = jwt.decode(
|
366
|
+
token,
|
367
|
+
settings.JWT_SECRET,
|
368
|
+
algorithms=[settings.JWT_ALGORITHM],
|
369
|
+
audience=settings.TOKEN_AUDIENCE,
|
370
|
+
issuer=settings.TOKEN_ISSUER
|
371
|
+
)
|
372
|
+
|
373
|
+
# 토큰 타입 검증 (expected_type이 주어진 경우에만)
|
374
|
+
if expected_type and payload.get("token_type") != expected_type:
|
375
|
+
raise TokenError(
|
376
|
+
detail=f"Expected {expected_type} token but got {payload.get('token_type')}",
|
377
|
+
source_function="verify_jwt_token"
|
378
|
+
)
|
379
|
+
|
380
|
+
return payload
|
381
|
+
|
382
|
+
except JWTError as e:
|
383
|
+
raise TokenError(
|
384
|
+
detail=str(e),
|
385
|
+
source_function="verify_jwt_token",
|
386
|
+
original_error=e
|
387
|
+
)
|
388
|
+
except (TokenError, SecurityError) as e:
|
389
|
+
raise e
|
390
|
+
except Exception as e:
|
391
|
+
raise SecurityError(
|
392
|
+
ErrorCode.INTERNAL_ERROR,
|
393
|
+
detail=str(e),
|
394
|
+
source_function="verify_jwt_token",
|
395
|
+
original_error=e
|
396
|
+
)
|
@@ -0,0 +1,188 @@
|
|
1
|
+
"""유효성 검사 관련 유틸리티 함수들을 모아둔 모듈입니다."""
|
2
|
+
|
3
|
+
from typing import Any, Optional, Dict
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
5
|
+
from sqlalchemy import select
|
6
|
+
from pydantic import field_validator
|
7
|
+
import re
|
8
|
+
|
9
|
+
from .exceptions import ErrorCode, CustomException
|
10
|
+
from .database import DatabaseService
|
11
|
+
from .base_model import Base
|
12
|
+
|
13
|
+
def validate_with(validator_func, unique_check=None, skip_if_none=False):
|
14
|
+
"""필드 유효성 검사 데코레이터
|
15
|
+
Args:
|
16
|
+
validator_func: 형식 검증 함수
|
17
|
+
unique_check: (table_name, field) 튜플. 지정되면 해당 테이블의 필드에 대해 중복 검사 수행
|
18
|
+
skip_if_none: None 값 허용 여부
|
19
|
+
"""
|
20
|
+
def decorator(field_name: str):
|
21
|
+
def validator(cls, value, info: Any):
|
22
|
+
if skip_if_none and value is None:
|
23
|
+
return value
|
24
|
+
|
25
|
+
# 형식 검증 (필드명도 함께 전달)
|
26
|
+
validator_func(value, field_name)
|
27
|
+
|
28
|
+
# 중복 검사는 별도의 validator로 분리
|
29
|
+
if unique_check:
|
30
|
+
async def check_unique():
|
31
|
+
if not info or not hasattr(info, 'context'):
|
32
|
+
raise CustomException(
|
33
|
+
ErrorCode.VALIDATION_ERROR,
|
34
|
+
detail=f"{field_name}|{value}",
|
35
|
+
source_function=f"Validator.validate_{field_name}"
|
36
|
+
)
|
37
|
+
|
38
|
+
db_service = info.context.get("db_service")
|
39
|
+
if not db_service:
|
40
|
+
raise CustomException(
|
41
|
+
ErrorCode.VALIDATION_ERROR,
|
42
|
+
detail=f"{field_name}|{value}",
|
43
|
+
source_function=f"Validator.validate_{field_name}"
|
44
|
+
)
|
45
|
+
|
46
|
+
table_name, field = unique_check
|
47
|
+
table = Base.metadata.tables.get(table_name)
|
48
|
+
if not table:
|
49
|
+
raise CustomException(
|
50
|
+
ErrorCode.VALIDATION_ERROR,
|
51
|
+
detail=f"{field_name}|{value}",
|
52
|
+
source_function=f"Validator.validate_{field_name}"
|
53
|
+
)
|
54
|
+
|
55
|
+
await db_service.validate_unique_fields(
|
56
|
+
table,
|
57
|
+
{field: value},
|
58
|
+
source_function=f"Validator.validate_{field_name}"
|
59
|
+
)
|
60
|
+
|
61
|
+
# 중복 검사를 위한 별도의 validator 등록
|
62
|
+
field_validator(field_name, mode='after')(check_unique)
|
63
|
+
|
64
|
+
return value
|
65
|
+
return field_validator(field_name, mode='before')(validator)
|
66
|
+
return decorator
|
67
|
+
|
68
|
+
class Validator:
|
69
|
+
@staticmethod
|
70
|
+
def validate_email(email: str, field_name: str = "email") -> None:
|
71
|
+
"""이메일 형식 검증을 수행하는 메서드."""
|
72
|
+
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
73
|
+
if not re.match(pattern, email):
|
74
|
+
raise CustomException(
|
75
|
+
ErrorCode.FIELD_INVALID_EMAIL,
|
76
|
+
detail=f"{field_name}|{email}",
|
77
|
+
source_function="Validator.validate_email"
|
78
|
+
)
|
79
|
+
|
80
|
+
@staticmethod
|
81
|
+
def validate_password(password: str, field_name: str = "password") -> None:
|
82
|
+
"""비밀번호 규칙 검증을 수행하는 메서드."""
|
83
|
+
try:
|
84
|
+
if len(password) < 8:
|
85
|
+
raise CustomException(
|
86
|
+
ErrorCode.FIELD_INVALID_PASSWORD_LENGTH,
|
87
|
+
detail=f"{field_name}|{password}",
|
88
|
+
source_function="Validator.validate_password"
|
89
|
+
)
|
90
|
+
|
91
|
+
if not re.search(r'[A-Z]', password):
|
92
|
+
raise CustomException(
|
93
|
+
ErrorCode.FIELD_INVALID_PASSWORD_UPPER,
|
94
|
+
detail=f"{field_name}|{password}",
|
95
|
+
source_function="Validator.validate_password"
|
96
|
+
)
|
97
|
+
|
98
|
+
if not re.search(r'[0-9]', password):
|
99
|
+
raise CustomException(
|
100
|
+
ErrorCode.FIELD_INVALID_PASSWORD_NUMBER,
|
101
|
+
detail=f"{field_name}|{password}",
|
102
|
+
source_function="Validator.validate_password"
|
103
|
+
)
|
104
|
+
|
105
|
+
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
106
|
+
raise CustomException(
|
107
|
+
ErrorCode.FIELD_INVALID_PASSWORD_SPECIAL,
|
108
|
+
detail=f"{field_name}|{password}",
|
109
|
+
source_function="Validator.validate_password"
|
110
|
+
)
|
111
|
+
|
112
|
+
except CustomException as ce:
|
113
|
+
raise ce
|
114
|
+
except Exception as e:
|
115
|
+
raise CustomException(
|
116
|
+
ErrorCode.VALIDATION_ERROR,
|
117
|
+
detail=f"{field_name}|{password}",
|
118
|
+
source_function="Validator.validate_password"
|
119
|
+
)
|
120
|
+
|
121
|
+
@staticmethod
|
122
|
+
def validate_id(value: str, field_name: str) -> None:
|
123
|
+
"""ID 형식 검증을 수행하는 메서드."""
|
124
|
+
if len(value) < 3:
|
125
|
+
raise CustomException(
|
126
|
+
ErrorCode.FIELD_INVALID_ID_LENGTH,
|
127
|
+
detail=f"{field_name}|{value}",
|
128
|
+
source_function="Validator.validate_id"
|
129
|
+
)
|
130
|
+
if not re.match(r'^[a-z0-9_]+$', value):
|
131
|
+
raise CustomException(
|
132
|
+
ErrorCode.FIELD_INVALID_ID_CHARS,
|
133
|
+
detail=f"{field_name}|{value}",
|
134
|
+
source_function="Validator.validate_id"
|
135
|
+
)
|
136
|
+
|
137
|
+
@staticmethod
|
138
|
+
def validate_mobile(mobile: str, field_name: str = "mobile") -> None:
|
139
|
+
"""휴대전화 번호 형식 검증을 수행하는 메서드."""
|
140
|
+
if not mobile: # Optional 필드이므로 빈 값 허용
|
141
|
+
return
|
142
|
+
|
143
|
+
pattern = r'^010-?[0-9]{4}-?[0-9]{4}$'
|
144
|
+
if not re.match(pattern, mobile):
|
145
|
+
raise CustomException(
|
146
|
+
ErrorCode.INVALID_MOBILE,
|
147
|
+
detail=f"{field_name}|{mobile}",
|
148
|
+
source_function="Validator.validate_mobile"
|
149
|
+
)
|
150
|
+
|
151
|
+
@staticmethod
|
152
|
+
def validate_phone(phone: str, field_name: str = "phone") -> None:
|
153
|
+
"""일반 전화번호 형식 검증을 수행하는 메서드."""
|
154
|
+
if not phone: # Optional 필드이므로 빈 값 허용
|
155
|
+
return
|
156
|
+
|
157
|
+
pattern = r'^(0[2-6][1-5]?)-?([0-9]{3,4})-?([0-9]{4})$'
|
158
|
+
if not re.match(pattern, phone):
|
159
|
+
raise CustomException(
|
160
|
+
ErrorCode.FIELD_INVALID_PHONE,
|
161
|
+
detail=f"{field_name}|{phone}",
|
162
|
+
source_function="Validator.validate_phone"
|
163
|
+
)
|
164
|
+
|
165
|
+
@staticmethod
|
166
|
+
def validate_name(name: str, field_name: str = "name") -> None:
|
167
|
+
"""이름 형식 검증을 수행하는 메서드."""
|
168
|
+
if not name:
|
169
|
+
raise CustomException(
|
170
|
+
ErrorCode.VALIDATION_ERROR,
|
171
|
+
detail=f"{field_name}|{name}",
|
172
|
+
source_function="Validator.validate_name"
|
173
|
+
)
|
174
|
+
|
175
|
+
if len(name) < 2 or len(name) > 100:
|
176
|
+
raise CustomException(
|
177
|
+
ErrorCode.VALIDATION_ERROR,
|
178
|
+
detail=f"{field_name}|{name}",
|
179
|
+
source_function="Validator.validate_name"
|
180
|
+
)
|
181
|
+
|
182
|
+
# 한글, 영문, 공백만 허용
|
183
|
+
if not re.match(r'^[가-힣a-zA-Z\s]+$', name):
|
184
|
+
raise CustomException(
|
185
|
+
ErrorCode.VALIDATION_ERROR,
|
186
|
+
detail=f"{field_name}|{name}",
|
187
|
+
source_function="Validator.validate_name"
|
188
|
+
)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: aiteamutils
|
3
|
+
Version: 0.2.0
|
4
|
+
Summary: AI Team Utilities
|
5
|
+
Project-URL: Homepage, https://github.com/yourusername/aiteamutils
|
6
|
+
Project-URL: Issues, https://github.com/yourusername/aiteamutils/issues
|
7
|
+
Author: AI Team
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
9
|
+
Classifier: Operating System :: OS Independent
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Requires-Python: >=3.8
|
12
|
+
Requires-Dist: fastapi
|
13
|
+
Requires-Dist: python-jose
|
14
|
+
Requires-Dist: sqlalchemy
|
15
|
+
Description-Content-Type: text/markdown
|
16
|
+
|
17
|
+
# AI Team Core Utils
|
18
|
+
|
19
|
+
AI Team Platform의 공통 유틸리티 패키지입니다.
|
20
|
+
|
21
|
+
## 설치 방법
|
22
|
+
|
23
|
+
```bash
|
24
|
+
pip install ai-team-core-utils
|
25
|
+
```
|
26
|
+
|
27
|
+
## 사용 예시
|
28
|
+
|
29
|
+
```python
|
30
|
+
from ai_team_core_utils.database import DatabaseManager
|
31
|
+
from ai_team_core_utils.base_model import Base
|
32
|
+
|
33
|
+
# DB 매니저 초기화
|
34
|
+
db = DatabaseManager("postgresql+asyncpg://user:pass@localhost/db")
|
35
|
+
|
36
|
+
# DB 세션 사용
|
37
|
+
async with db.get_session() as session:
|
38
|
+
# DB 작업 수행
|
39
|
+
pass
|
40
|
+
|
41
|
+
# 예외 처리
|
42
|
+
from ai_team_core_utils.exceptions import CustomException, ErrorCode
|
43
|
+
|
44
|
+
try:
|
45
|
+
# 작업 수행
|
46
|
+
pass
|
47
|
+
except CustomException as e:
|
48
|
+
# 에러 처리
|
49
|
+
print(e.to_dict())
|
50
|
+
```
|
51
|
+
|
52
|
+
## 주요 기능
|
53
|
+
|
54
|
+
- 데이터베이스 유틸리티
|
55
|
+
- 세션 관리
|
56
|
+
- 트랜잭션 관리
|
57
|
+
- 기본 CRUD 작업
|
58
|
+
|
59
|
+
- 인증/인가 유틸리티
|
60
|
+
- JWT 토큰 관리
|
61
|
+
- 비밀번호 해싱
|
62
|
+
- Rate Limiting
|
63
|
+
|
64
|
+
- 예외 처리
|
65
|
+
- 표준화된 에러 코드
|
66
|
+
- 에러 체인 추적
|
67
|
+
- 로깅 통합
|
68
|
+
|
69
|
+
- 공통 모델
|
70
|
+
- 기본 모델 클래스
|
71
|
+
- 타입 검증
|
72
|
+
- 유효성 검사
|
@@ -0,0 +1,15 @@
|
|
1
|
+
aiteamutils/__init__.py,sha256=zmfBXBwNWdbJKCt1rmHk_czHJLmVF-oqRuqq8tf0t0U,1229
|
2
|
+
aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
|
3
|
+
aiteamutils/base_repository.py,sha256=0772JYHpF82vZzR8l21rDcZk8uVj6r52rqJdp150qiE,18927
|
4
|
+
aiteamutils/base_service.py,sha256=nW9sC0SHDIve3WJVUB3rAS_9XGTkIYJRaDxfqA0V3js,24727
|
5
|
+
aiteamutils/cache.py,sha256=tr0Yn8VPYA9QHiKCUzciVlQ2J1RAwNo2K9lGMH4rY3s,1334
|
6
|
+
aiteamutils/config.py,sha256=vC6k6E2-Y4mD0E0kw6WVgSatCl9K_BtTwrVFhLrhCzs,665
|
7
|
+
aiteamutils/database.py,sha256=U71cexPsSmMTKgp098I574PupqnuPltujI4QKHBG2Cc,29952
|
8
|
+
aiteamutils/dependencies.py,sha256=EJeVtq_lACuoheVhkX23N9xiak9bGD-t3-2JtlgBki0,4850
|
9
|
+
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
10
|
+
aiteamutils/exceptions.py,sha256=YV-ISya4wQlHk4twvGo16I5r8h22-tXpn9wa-b3WwDM,15231
|
11
|
+
aiteamutils/security.py,sha256=AZszaTxVEGi1jU1sX3QXHGgshp1lVvd0xXvZejXvs_w,12643
|
12
|
+
aiteamutils/validators.py,sha256=BQA61f5raVAX0BGcTIS3Ht6CyCAdHgqDUmr76bZWgYE,7630
|
13
|
+
aiteamutils-0.2.0.dist-info/METADATA,sha256=ssjX9Y1HQqAY9vNB0MvN9wdILEtxPnIiXXWw3z4-An8,1569
|
14
|
+
aiteamutils-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
+
aiteamutils-0.2.0.dist-info/RECORD,,
|