aiteamutils 0.2.50__py3-none-any.whl → 0.2.52__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/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
|