aiteamutils 0.2.50__py3-none-any.whl → 0.2.52__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- aiteamutils/base_repository.py +68 -364
- aiteamutils/base_service.py +46 -43
- aiteamutils/database.py +35 -48
- aiteamutils/dependencies.py +183 -105
- aiteamutils/security.py +52 -289
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.50.dist-info → aiteamutils-0.2.52.dist-info}/METADATA +1 -1
- aiteamutils-0.2.52.dist-info/RECORD +16 -0
- aiteamutils-0.2.50.dist-info/RECORD +0 -16
- {aiteamutils-0.2.50.dist-info → aiteamutils-0.2.52.dist-info}/WHEEL +0 -0
aiteamutils/security.py
CHANGED
@@ -6,10 +6,10 @@ from fastapi import Request, HTTPException, status
|
|
6
6
|
from functools import wraps
|
7
7
|
from jose import jwt, JWTError
|
8
8
|
from passlib.context import CryptContext
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
9
10
|
import logging
|
10
11
|
|
11
12
|
from .exceptions import CustomException, ErrorCode
|
12
|
-
from .database import DatabaseService
|
13
13
|
from .enums import ActivityType
|
14
14
|
from .config import get_settings
|
15
15
|
|
@@ -29,15 +29,7 @@ class RateLimitExceeded(CustomException):
|
|
29
29
|
max_requests: int,
|
30
30
|
window_seconds: int
|
31
31
|
):
|
32
|
-
"""Rate limit 초과 예외를 초기화합니다.
|
33
|
-
|
34
|
-
Args:
|
35
|
-
detail: 상세 메시지
|
36
|
-
source_function: 예외가 발생한 함수명
|
37
|
-
remaining_seconds: 다음 요청까지 남은 시간 (초)
|
38
|
-
max_requests: 허용된 최대 요청 수
|
39
|
-
window_seconds: 시간 윈도우 (초)
|
40
|
-
"""
|
32
|
+
"""Rate limit 초과 예외를 초기화합니다."""
|
41
33
|
super().__init__(
|
42
34
|
ErrorCode.RATE_LIMIT_EXCEEDED,
|
43
35
|
detail=detail,
|
@@ -52,119 +44,8 @@ class RateLimitExceeded(CustomException):
|
|
52
44
|
self.max_requests = max_requests
|
53
45
|
self.window_seconds = window_seconds
|
54
46
|
|
55
|
-
class SecurityError(CustomException):
|
56
|
-
"""보안 관련 기본 예외."""
|
57
|
-
|
58
|
-
def __init__(
|
59
|
-
self,
|
60
|
-
error_code: ErrorCode,
|
61
|
-
detail: str,
|
62
|
-
source_function: str,
|
63
|
-
original_error: Optional[Exception] = None
|
64
|
-
):
|
65
|
-
"""보안 관련 예외를 초기화합니다.
|
66
|
-
|
67
|
-
Args:
|
68
|
-
error_code: 에러 코드
|
69
|
-
detail: 상세 메시지
|
70
|
-
source_function: 예외가 발생한 함수명
|
71
|
-
original_error: 원본 예외
|
72
|
-
"""
|
73
|
-
super().__init__(
|
74
|
-
error_code,
|
75
|
-
detail=detail,
|
76
|
-
source_function=f"security.{source_function}",
|
77
|
-
original_error=original_error
|
78
|
-
)
|
79
|
-
|
80
|
-
class TokenError(SecurityError):
|
81
|
-
"""토큰 관련 예외."""
|
82
|
-
|
83
|
-
def __init__(
|
84
|
-
self,
|
85
|
-
detail: str,
|
86
|
-
source_function: str,
|
87
|
-
original_error: Optional[Exception] = None
|
88
|
-
):
|
89
|
-
"""토큰 관련 예외를 초기화합니다."""
|
90
|
-
super().__init__(
|
91
|
-
ErrorCode.INVALID_TOKEN,
|
92
|
-
detail=detail,
|
93
|
-
source_function=source_function,
|
94
|
-
original_error=original_error
|
95
|
-
)
|
96
|
-
|
97
|
-
class RateLimiter:
|
98
|
-
"""Rate limit 관리 클래스."""
|
99
|
-
|
100
|
-
def __init__(self, max_requests: int, window_seconds: int):
|
101
|
-
"""Rate limiter를 초기화합니다.
|
102
|
-
|
103
|
-
Args:
|
104
|
-
max_requests: 허용된 최대 요청 수
|
105
|
-
window_seconds: 시간 윈도우 (초)
|
106
|
-
"""
|
107
|
-
self.max_requests = max_requests
|
108
|
-
self.window_seconds = window_seconds
|
109
|
-
self._cache: Dict[str, Dict[str, Any]] = {}
|
110
|
-
|
111
|
-
def is_allowed(self, key: str) -> bool:
|
112
|
-
"""현재 요청이 허용되는지 확인합니다.
|
113
|
-
|
114
|
-
Args:
|
115
|
-
key: Rate limit 키
|
116
|
-
|
117
|
-
Returns:
|
118
|
-
bool: 요청 허용 여부
|
119
|
-
"""
|
120
|
-
now = datetime.now(UTC)
|
121
|
-
rate_info = self._cache.get(key)
|
122
|
-
|
123
|
-
if rate_info is None or (now - rate_info["start_time"]).total_seconds() >= self.window_seconds:
|
124
|
-
self._cache[key] = {
|
125
|
-
"count": 1,
|
126
|
-
"start_time": now
|
127
|
-
}
|
128
|
-
return True
|
129
|
-
|
130
|
-
if rate_info["count"] >= self.max_requests:
|
131
|
-
return False
|
132
|
-
|
133
|
-
rate_info["count"] += 1
|
134
|
-
return True
|
135
|
-
|
136
|
-
def get_remaining_time(self, key: str) -> float:
|
137
|
-
"""남은 시간을 반환합니다.
|
138
|
-
|
139
|
-
Args:
|
140
|
-
key: Rate limit 키
|
141
|
-
|
142
|
-
Returns:
|
143
|
-
float: 남은 시간 (초)
|
144
|
-
"""
|
145
|
-
rate_info = self._cache.get(key)
|
146
|
-
if not rate_info:
|
147
|
-
return 0
|
148
|
-
|
149
|
-
now = datetime.now(UTC)
|
150
|
-
return max(
|
151
|
-
0,
|
152
|
-
self.window_seconds - (now - rate_info["start_time"]).total_seconds()
|
153
|
-
)
|
154
|
-
|
155
47
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
156
|
-
"""비밀번호를 검증합니다.
|
157
|
-
|
158
|
-
Args:
|
159
|
-
plain_password: 평문 비밀번호
|
160
|
-
hashed_password: 해시된 비밀번호
|
161
|
-
|
162
|
-
Returns:
|
163
|
-
bool: 비밀번호 일치 여부
|
164
|
-
|
165
|
-
Raises:
|
166
|
-
CustomException: 비밀번호 검증 실패 시
|
167
|
-
"""
|
48
|
+
"""비밀번호를 검증합니다."""
|
168
49
|
try:
|
169
50
|
return pwd_context.verify(plain_password, hashed_password)
|
170
51
|
except Exception as e:
|
@@ -176,17 +57,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
176
57
|
)
|
177
58
|
|
178
59
|
def hash_password(password: str) -> str:
|
179
|
-
"""비밀번호를 해시화합니다.
|
180
|
-
|
181
|
-
Args:
|
182
|
-
password: 평문 비밀번호
|
183
|
-
|
184
|
-
Returns:
|
185
|
-
str: 해시된 비밀번호
|
186
|
-
|
187
|
-
Raises:
|
188
|
-
CustomException: 비밀번호 해시화 실패 시
|
189
|
-
"""
|
60
|
+
"""비밀번호를 해시화합니다."""
|
190
61
|
try:
|
191
62
|
return pwd_context.hash(password)
|
192
63
|
except Exception as e:
|
@@ -197,135 +68,21 @@ def hash_password(password: str) -> str:
|
|
197
68
|
original_error=e
|
198
69
|
)
|
199
70
|
|
200
|
-
def rate_limit(
|
201
|
-
max_requests: int,
|
202
|
-
window_seconds: int,
|
203
|
-
key_func: Optional[Callable] = None
|
204
|
-
):
|
205
|
-
"""Rate limiting 데코레이터."""
|
206
|
-
def decorator(func: Callable) -> Callable:
|
207
|
-
@wraps(func)
|
208
|
-
async def wrapper(*args, **kwargs):
|
209
|
-
logging.info(f"[rate_limit] Starting rate limit check for {func.__name__}")
|
210
|
-
|
211
|
-
# Request 객체 찾기
|
212
|
-
request = None
|
213
|
-
for arg in args:
|
214
|
-
if isinstance(arg, Request):
|
215
|
-
request = arg
|
216
|
-
break
|
217
|
-
if not request:
|
218
|
-
for arg in kwargs.values():
|
219
|
-
if isinstance(arg, Request):
|
220
|
-
request = arg
|
221
|
-
break
|
222
|
-
if not request:
|
223
|
-
logging.error("[rate_limit] Request object not found in args or kwargs")
|
224
|
-
raise CustomException(
|
225
|
-
ErrorCode.INTERNAL_ERROR,
|
226
|
-
detail="Request object not found",
|
227
|
-
source_function="rate_limit"
|
228
|
-
)
|
229
|
-
|
230
|
-
# 레이트 리밋 키 생성
|
231
|
-
if key_func:
|
232
|
-
rate_limit_key = f"rate_limit:{key_func(request)}"
|
233
|
-
else:
|
234
|
-
client_ip = request.client.host
|
235
|
-
rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
|
236
|
-
|
237
|
-
logging.info(f"[rate_limit] Rate limit key: {rate_limit_key}")
|
238
|
-
|
239
|
-
now = datetime.now(UTC)
|
240
|
-
|
241
|
-
# 현재 rate limit 정보 가져오기
|
242
|
-
rate_info = _rate_limits.get(rate_limit_key)
|
243
|
-
logging.info(f"[rate_limit] Current rate info: {rate_info}")
|
244
|
-
|
245
|
-
if rate_info is None or (now - rate_info["start_time"]).total_seconds() >= window_seconds:
|
246
|
-
# 새로운 rate limit 설정
|
247
|
-
_rate_limits[rate_limit_key] = {
|
248
|
-
"count": 1,
|
249
|
-
"start_time": now
|
250
|
-
}
|
251
|
-
logging.info(f"[rate_limit] Created new rate limit: {_rate_limits[rate_limit_key]}")
|
252
|
-
else:
|
253
|
-
# 기존 rate limit 업데이트
|
254
|
-
if rate_info["count"] >= max_requests:
|
255
|
-
# rate limit 초과
|
256
|
-
remaining_seconds = window_seconds - (now - rate_info["start_time"]).total_seconds()
|
257
|
-
logging.warning(f"[rate_limit] Rate limit exceeded. Remaining seconds: {remaining_seconds}")
|
258
|
-
raise RateLimitExceeded(
|
259
|
-
detail=rate_limit_key,
|
260
|
-
source_function=func.__name__,
|
261
|
-
remaining_seconds=remaining_seconds,
|
262
|
-
max_requests=max_requests,
|
263
|
-
window_seconds=window_seconds
|
264
|
-
)
|
265
|
-
rate_info["count"] += 1
|
266
|
-
logging.info(f"[rate_limit] Updated rate info: {rate_info}")
|
267
|
-
|
268
|
-
try:
|
269
|
-
logging.info(f"[rate_limit] Executing original function: {func.__name__}")
|
270
|
-
result = await func(*args, **kwargs)
|
271
|
-
logging.info("[rate_limit] Function executed successfully")
|
272
|
-
return result
|
273
|
-
except CustomException as e:
|
274
|
-
logging.error(f"[rate_limit] CustomException occurred: {str(e)}")
|
275
|
-
raise e
|
276
|
-
except Exception as e:
|
277
|
-
logging.error(f"[rate_limit] Unexpected error occurred: {str(e)}")
|
278
|
-
raise CustomException(
|
279
|
-
ErrorCode.INTERNAL_ERROR,
|
280
|
-
detail=str(e),
|
281
|
-
source_function=func.__name__,
|
282
|
-
original_error=e
|
283
|
-
)
|
284
|
-
|
285
|
-
return wrapper
|
286
|
-
return decorator
|
287
|
-
|
288
|
-
class TokenCreationError(SecurityError):
|
289
|
-
"""토큰 생성 관련 예외."""
|
290
|
-
|
291
|
-
def __init__(
|
292
|
-
self,
|
293
|
-
detail: str,
|
294
|
-
source_function: str,
|
295
|
-
token_type: str,
|
296
|
-
original_error: Optional[Exception] = None
|
297
|
-
):
|
298
|
-
"""토큰 생성 예외를 초기화합니다.
|
299
|
-
|
300
|
-
Args:
|
301
|
-
detail: 상세 메시지
|
302
|
-
source_function: 예외가 발생한 함수명
|
303
|
-
token_type: 토큰 타입
|
304
|
-
original_error: 원본 예외
|
305
|
-
"""
|
306
|
-
super().__init__(
|
307
|
-
ErrorCode.INTERNAL_ERROR,
|
308
|
-
detail=detail,
|
309
|
-
source_function=source_function,
|
310
|
-
original_error=original_error
|
311
|
-
)
|
312
|
-
self.token_type = token_type
|
313
|
-
|
314
71
|
async def create_jwt_token(
|
315
72
|
user_data: Dict[str, Any],
|
316
73
|
token_type: Literal["access", "refresh"],
|
317
|
-
|
74
|
+
session: AsyncSession,
|
318
75
|
log_model: Type[Base],
|
319
76
|
request: Optional[Request] = None
|
320
77
|
) -> str:
|
321
|
-
"""JWT 토큰을
|
78
|
+
"""JWT 토큰을 생성합니다.
|
322
79
|
|
323
80
|
Args:
|
324
|
-
user_data
|
325
|
-
token_type
|
326
|
-
|
327
|
-
log_model: 로그 모델
|
328
|
-
request: FastAPI 요청
|
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.
|
329
86
|
|
330
87
|
Returns:
|
331
88
|
str: 생성된 JWT 토큰
|
@@ -336,23 +93,13 @@ async def create_jwt_token(
|
|
336
93
|
try:
|
337
94
|
settings = get_settings()
|
338
95
|
|
339
|
-
# 필수 필드 검증
|
340
|
-
required_fields = {"username", "ulid"}
|
341
|
-
missing_fields = required_fields - set(user_data.keys())
|
342
|
-
if missing_fields:
|
343
|
-
raise CustomException(
|
344
|
-
ErrorCode.REQUIRED_FIELD_MISSING,
|
345
|
-
detail="|".join(missing_fields),
|
346
|
-
source_function="security.create_jwt_token"
|
347
|
-
)
|
348
|
-
|
349
96
|
if token_type == "access":
|
350
|
-
expires_at = datetime.now(UTC) + timedelta(minutes=settings.
|
97
|
+
expires_at = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes)
|
351
98
|
token_data = {
|
352
99
|
# 등록 클레임
|
353
|
-
"iss": settings.
|
100
|
+
"iss": settings.token_issuer,
|
354
101
|
"sub": user_data["ulid"],
|
355
|
-
"aud": settings.
|
102
|
+
"aud": settings.token_audience,
|
356
103
|
"exp": expires_at,
|
357
104
|
|
358
105
|
# 공개 클레임
|
@@ -375,18 +122,18 @@ async def create_jwt_token(
|
|
375
122
|
else: # refresh token
|
376
123
|
expires_at = datetime.now(UTC) + timedelta(days=14)
|
377
124
|
token_data = {
|
378
|
-
"iss": settings.
|
125
|
+
"iss": settings.token_issuer,
|
379
126
|
"sub": user_data["ulid"],
|
380
127
|
"exp": expires_at,
|
381
128
|
"token_type": token_type,
|
382
129
|
"user_ulid": user_data["ulid"]
|
383
130
|
}
|
384
|
-
|
131
|
+
|
385
132
|
try:
|
386
133
|
token = jwt.encode(
|
387
134
|
token_data,
|
388
|
-
settings.
|
389
|
-
algorithm=settings.
|
135
|
+
settings.jwt_secret,
|
136
|
+
algorithm=settings.jwt_algorithm
|
390
137
|
)
|
391
138
|
except Exception as e:
|
392
139
|
raise CustomException(
|
@@ -399,18 +146,16 @@ async def create_jwt_token(
|
|
399
146
|
# 로그 생성
|
400
147
|
try:
|
401
148
|
activity_type = ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
"user_ulid": user_data["ulid"],
|
407
|
-
"token": token
|
408
|
-
},
|
409
|
-
request=request
|
149
|
+
log_entry = log_model(
|
150
|
+
type=activity_type,
|
151
|
+
user_ulid=user_data["ulid"],
|
152
|
+
token=token
|
410
153
|
)
|
154
|
+
session.add(log_entry)
|
155
|
+
await session.flush()
|
411
156
|
except Exception as e:
|
412
157
|
# 로그 생성 실패는 토큰 생성에 영향을 주지 않음
|
413
|
-
|
158
|
+
logging.error(f"Failed to create token log: {str(e)}")
|
414
159
|
|
415
160
|
return token
|
416
161
|
|
@@ -442,29 +187,47 @@ async def verify_jwt_token(
|
|
442
187
|
"""
|
443
188
|
try:
|
444
189
|
settings = get_settings()
|
190
|
+
|
445
191
|
# 토큰 디코딩
|
446
192
|
payload = jwt.decode(
|
447
193
|
token,
|
448
|
-
settings.
|
449
|
-
algorithms=[settings.
|
450
|
-
audience=settings.
|
451
|
-
issuer=settings.
|
194
|
+
settings.jwt_secret,
|
195
|
+
algorithms=[settings.jwt_algorithm],
|
196
|
+
audience=settings.token_audience,
|
197
|
+
issuer=settings.token_issuer
|
452
198
|
)
|
453
199
|
|
454
|
-
# 토큰 타입 검증
|
455
|
-
|
200
|
+
# 토큰 타입 검증
|
201
|
+
token_type = payload.get("token_type")
|
202
|
+
if not token_type:
|
456
203
|
raise CustomException(
|
457
204
|
ErrorCode.INVALID_TOKEN,
|
458
|
-
detail=
|
205
|
+
detail="Token type is missing",
|
459
206
|
source_function="security.verify_jwt_token"
|
460
207
|
)
|
461
|
-
|
208
|
+
|
209
|
+
if expected_type and token_type != expected_type:
|
210
|
+
raise CustomException(
|
211
|
+
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",
|
222
|
+
source_function="security.verify_jwt_token"
|
223
|
+
)
|
224
|
+
|
462
225
|
return payload
|
463
226
|
|
464
227
|
except JWTError as e:
|
465
228
|
raise CustomException(
|
466
229
|
ErrorCode.INVALID_TOKEN,
|
467
|
-
detail=
|
230
|
+
detail=str(e),
|
468
231
|
source_function="security.verify_jwt_token",
|
469
232
|
original_error=e
|
470
233
|
)
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.52"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
aiteamutils/__init__.py,sha256=IAvWobxODQeMIgttFf3e1IGMO-DktLyUmnHeKqGDZWg,1346
|
2
|
+
aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
|
3
|
+
aiteamutils/base_repository.py,sha256=vqsundoN0h7FVvgqTBEnnJNMcFpvMK0s_nxBWdIYg-U,7846
|
4
|
+
aiteamutils/base_service.py,sha256=s2AcA-6_ogOQKgt2xf_3AG2s6tqBceU4nJoXO1II7S8,24588
|
5
|
+
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
|
+
aiteamutils/config.py,sha256=OM_b7g8sqZ3zY_DSF9ry-zn5wn4dlXdx5OhjfTGr0TE,2876
|
7
|
+
aiteamutils/database.py,sha256=uSMNWJge5RuQI7zJBJVX24fAHcSPChl-95_2TwK_GOw,39654
|
8
|
+
aiteamutils/dependencies.py,sha256=q-OrEOJh4xEpc7ag6nTyey1pQwK9G0ZEDgXB_iTbaM0,6449
|
9
|
+
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
10
|
+
aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
|
11
|
+
aiteamutils/security.py,sha256=AUcGlNZeURF9AIF87p9SvWYKVao2_CISk8B6WasF-mo,8050
|
12
|
+
aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
|
13
|
+
aiteamutils/version.py,sha256=jkNi_8VOucj0ySHwq10MaZLBXEjXin60tdcDfPSEO5A,42
|
14
|
+
aiteamutils-0.2.52.dist-info/METADATA,sha256=1XX034T6pG0yeYvGKWcPiSbUpmGF1NFlP8wQELSqyRo,1718
|
15
|
+
aiteamutils-0.2.52.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.52.dist-info/RECORD,,
|
@@ -1,16 +0,0 @@
|
|
1
|
-
aiteamutils/__init__.py,sha256=IAvWobxODQeMIgttFf3e1IGMO-DktLyUmnHeKqGDZWg,1346
|
2
|
-
aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
|
3
|
-
aiteamutils/base_repository.py,sha256=qdwQ7Sj2fUqxpDg6cWM48n_QbwPK_VUlG9zTSem8iCk,18968
|
4
|
-
aiteamutils/base_service.py,sha256=E4dHGE0DvhmRyFplh46SwKJOSF_nUL7OAsCkf_ZJF_8,24733
|
5
|
-
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
|
-
aiteamutils/config.py,sha256=OM_b7g8sqZ3zY_DSF9ry-zn5wn4dlXdx5OhjfTGr0TE,2876
|
7
|
-
aiteamutils/database.py,sha256=rEKuZ8_3eBjP1KpTNVgWrVLAtKX_IkZJzul2pa8bDlg,40075
|
8
|
-
aiteamutils/dependencies.py,sha256=0mNmZiPlA8wTj4cgmjGXs6y9kkIQ5RtLZHdEeHxyblo,4490
|
9
|
-
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
10
|
-
aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
|
11
|
-
aiteamutils/security.py,sha256=KSZf3WJAlW0UXVM1GEHwuGy2x27UHwtMr8aq-GuIXZk,16016
|
12
|
-
aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
|
13
|
-
aiteamutils/version.py,sha256=lxXpdmBOudu-YVW902wEuXxWiy_elYifbZv5G4sOhdo,42
|
14
|
-
aiteamutils-0.2.50.dist-info/METADATA,sha256=WjFxSSNzciuhgenfILvBNXtMKSY-HIAd6m88tZXrEiU,1718
|
15
|
-
aiteamutils-0.2.50.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
-
aiteamutils-0.2.50.dist-info/RECORD,,
|
File without changes
|