aiteamutils 0.2.51__py3-none-any.whl → 0.2.53__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 +24 -5
- aiteamutils/dependencies.py +183 -105
- aiteamutils/security.py +93 -281
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.51.dist-info → aiteamutils-0.2.53.dist-info}/METADATA +1 -1
- aiteamutils-0.2.53.dist-info/RECORD +16 -0
- aiteamutils-0.2.51.dist-info/RECORD +0 -16
- {aiteamutils-0.2.51.dist-info → aiteamutils-0.2.53.dist-info}/WHEEL +0 -0
aiteamutils/security.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
"""보안 관련 유틸리티."""
|
2
2
|
from datetime import datetime, timedelta, UTC
|
3
|
-
from typing import Dict, Any, Optional, Literal, Callable,
|
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
|
@@ -9,7 +8,6 @@ from passlib.context import CryptContext
|
|
9
8
|
import logging
|
10
9
|
|
11
10
|
from .exceptions import CustomException, ErrorCode
|
12
|
-
from .database import DatabaseService
|
13
11
|
from .enums import ActivityType
|
14
12
|
from .config import get_settings
|
15
13
|
|
@@ -20,151 +18,15 @@ _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 초과 예외를 초기화합니다.
|
33
|
-
|
34
|
-
Args:
|
35
|
-
detail: 상세 메시지
|
36
|
-
source_function: 예외가 발생한 함수명
|
37
|
-
remaining_seconds: 다음 요청까지 남은 시간 (초)
|
38
|
-
max_requests: 허용된 최대 요청 수
|
39
|
-
window_seconds: 시간 윈도우 (초)
|
40
|
-
"""
|
21
|
+
def __init__(self, detail: str, source_function: str):
|
41
22
|
super().__init__(
|
42
23
|
ErrorCode.RATE_LIMIT_EXCEEDED,
|
43
24
|
detail=detail,
|
44
|
-
source_function=source_function
|
45
|
-
metadata={
|
46
|
-
"remaining_seconds": remaining_seconds,
|
47
|
-
"max_requests": max_requests,
|
48
|
-
"window_seconds": window_seconds
|
49
|
-
}
|
50
|
-
)
|
51
|
-
self.remaining_seconds = remaining_seconds
|
52
|
-
self.max_requests = max_requests
|
53
|
-
self.window_seconds = window_seconds
|
54
|
-
|
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()
|
25
|
+
source_function=source_function
|
153
26
|
)
|
154
27
|
|
155
28
|
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
|
-
"""
|
29
|
+
"""비밀번호를 검증합니다."""
|
168
30
|
try:
|
169
31
|
return pwd_context.verify(plain_password, hashed_password)
|
170
32
|
except Exception as e:
|
@@ -176,17 +38,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
176
38
|
)
|
177
39
|
|
178
40
|
def hash_password(password: str) -> str:
|
179
|
-
"""비밀번호를 해시화합니다.
|
180
|
-
|
181
|
-
Args:
|
182
|
-
password: 평문 비밀번호
|
183
|
-
|
184
|
-
Returns:
|
185
|
-
str: 해시된 비밀번호
|
186
|
-
|
187
|
-
Raises:
|
188
|
-
CustomException: 비밀번호 해시화 실패 시
|
189
|
-
"""
|
41
|
+
"""비밀번호를 해시화합니다."""
|
190
42
|
try:
|
191
43
|
return pwd_context.hash(password)
|
192
44
|
except Exception as e:
|
@@ -206,8 +58,6 @@ def rate_limit(
|
|
206
58
|
def decorator(func: Callable) -> Callable:
|
207
59
|
@wraps(func)
|
208
60
|
async def wrapper(*args, **kwargs):
|
209
|
-
logging.info(f"[rate_limit] Starting rate limit check for {func.__name__}")
|
210
|
-
|
211
61
|
# Request 객체 찾기
|
212
62
|
request = None
|
213
63
|
for arg in args:
|
@@ -220,7 +70,6 @@ def rate_limit(
|
|
220
70
|
request = arg
|
221
71
|
break
|
222
72
|
if not request:
|
223
|
-
logging.error("[rate_limit] Request object not found in args or kwargs")
|
224
73
|
raise CustomException(
|
225
74
|
ErrorCode.INTERNAL_ERROR,
|
226
75
|
detail="Request object not found",
|
@@ -234,125 +83,73 @@ def rate_limit(
|
|
234
83
|
client_ip = request.client.host
|
235
84
|
rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
|
236
85
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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),
|
260
119
|
source_function=func.__name__,
|
261
|
-
|
262
|
-
max_requests=max_requests,
|
263
|
-
window_seconds=window_seconds
|
120
|
+
original_error=e
|
264
121
|
)
|
265
|
-
|
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
|
122
|
+
|
273
123
|
except CustomException as e:
|
274
|
-
logging.error(f"[rate_limit] CustomException occurred: {str(e)}")
|
275
124
|
raise e
|
276
125
|
except Exception as e:
|
277
|
-
logging.error(f"[rate_limit] Unexpected error occurred: {str(e)}")
|
278
126
|
raise CustomException(
|
279
127
|
ErrorCode.INTERNAL_ERROR,
|
280
128
|
detail=str(e),
|
281
|
-
source_function=
|
129
|
+
source_function="rate_limit",
|
282
130
|
original_error=e
|
283
131
|
)
|
284
|
-
|
132
|
+
|
285
133
|
return wrapper
|
286
134
|
return decorator
|
287
135
|
|
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
136
|
async def create_jwt_token(
|
315
137
|
user_data: Dict[str, Any],
|
316
138
|
token_type: Literal["access", "refresh"],
|
317
|
-
db_service:
|
318
|
-
log_model: Type[Base],
|
139
|
+
db_service: Any,
|
319
140
|
request: Optional[Request] = None
|
320
141
|
) -> str:
|
321
|
-
"""JWT 토큰을 생성하고 로그를 기록합니다.
|
322
|
-
|
323
|
-
Args:
|
324
|
-
user_data: 사용자 데이터 딕셔너리 (username, ulid, name, role_ulid, status, organization 정보 등)
|
325
|
-
token_type: 토큰 타입 ("access" 또는 "refresh")
|
326
|
-
db_service: 데이터베이스 서비스
|
327
|
-
log_model: 로그 모델 클래스
|
328
|
-
request: FastAPI 요청 객체
|
329
|
-
|
330
|
-
Returns:
|
331
|
-
str: 생성된 JWT 토큰
|
332
|
-
|
333
|
-
Raises:
|
334
|
-
CustomException: 토큰 생성 실패 시
|
335
|
-
"""
|
142
|
+
"""JWT 토큰을 생성하고 로그를 기록합니다."""
|
336
143
|
try:
|
337
144
|
settings = get_settings()
|
338
145
|
|
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
146
|
if token_type == "access":
|
350
|
-
expires_at = datetime.now(UTC) + timedelta(minutes=settings.
|
147
|
+
expires_at = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes)
|
351
148
|
token_data = {
|
352
149
|
# 등록 클레임
|
353
|
-
"iss": settings.
|
354
|
-
"sub": user_data["
|
355
|
-
"aud": settings.
|
150
|
+
"iss": settings.token_issuer,
|
151
|
+
"sub": user_data["username"],
|
152
|
+
"aud": settings.token_audience,
|
356
153
|
"exp": expires_at,
|
357
154
|
|
358
155
|
# 공개 클레임
|
@@ -375,18 +172,18 @@ async def create_jwt_token(
|
|
375
172
|
else: # refresh token
|
376
173
|
expires_at = datetime.now(UTC) + timedelta(days=14)
|
377
174
|
token_data = {
|
378
|
-
"iss": settings.
|
379
|
-
"sub": user_data["
|
175
|
+
"iss": settings.token_issuer,
|
176
|
+
"sub": user_data["username"],
|
380
177
|
"exp": expires_at,
|
381
178
|
"token_type": token_type,
|
382
179
|
"user_ulid": user_data["ulid"]
|
383
180
|
}
|
384
|
-
|
181
|
+
|
385
182
|
try:
|
386
183
|
token = jwt.encode(
|
387
184
|
token_data,
|
388
|
-
settings.
|
389
|
-
algorithm=settings.
|
185
|
+
settings.jwt_secret,
|
186
|
+
algorithm=settings.jwt_algorithm
|
390
187
|
)
|
391
188
|
except Exception as e:
|
392
189
|
raise CustomException(
|
@@ -400,17 +197,16 @@ async def create_jwt_token(
|
|
400
197
|
try:
|
401
198
|
activity_type = ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED
|
402
199
|
await db_service.create_log(
|
403
|
-
|
404
|
-
log_data={
|
200
|
+
{
|
405
201
|
"type": activity_type,
|
406
202
|
"user_ulid": user_data["ulid"],
|
407
203
|
"token": token
|
408
204
|
},
|
409
|
-
request
|
205
|
+
request
|
410
206
|
)
|
411
207
|
except Exception as e:
|
412
208
|
# 로그 생성 실패는 토큰 생성에 영향을 주지 않음
|
413
|
-
|
209
|
+
logging.error(f"Failed to create token log: {str(e)}")
|
414
210
|
|
415
211
|
return token
|
416
212
|
|
@@ -428,43 +224,41 @@ async def verify_jwt_token(
|
|
428
224
|
token: str,
|
429
225
|
expected_type: Optional[Literal["access", "refresh"]] = None
|
430
226
|
) -> Dict[str, Any]:
|
431
|
-
"""JWT 토큰을 검증합니다.
|
432
|
-
|
433
|
-
Args:
|
434
|
-
token: 검증할 JWT 토큰
|
435
|
-
expected_type: 예상되는 토큰 타입
|
436
|
-
|
437
|
-
Returns:
|
438
|
-
Dict[str, Any]: 토큰 페이로드
|
439
|
-
|
440
|
-
Raises:
|
441
|
-
CustomException: 토큰 검증 실패 시
|
442
|
-
"""
|
227
|
+
"""JWT 토큰을 검증합니다."""
|
443
228
|
try:
|
444
229
|
settings = get_settings()
|
230
|
+
|
445
231
|
# 토큰 디코딩
|
446
232
|
payload = jwt.decode(
|
447
233
|
token,
|
448
|
-
settings.
|
449
|
-
algorithms=[settings.
|
450
|
-
audience=settings.
|
451
|
-
issuer=settings.
|
234
|
+
settings.jwt_secret,
|
235
|
+
algorithms=[settings.jwt_algorithm],
|
236
|
+
audience=settings.token_audience,
|
237
|
+
issuer=settings.token_issuer
|
452
238
|
)
|
453
239
|
|
454
|
-
# 토큰 타입 검증
|
455
|
-
|
240
|
+
# 토큰 타입 검증
|
241
|
+
token_type = payload.get("token_type")
|
242
|
+
if not token_type:
|
456
243
|
raise CustomException(
|
457
244
|
ErrorCode.INVALID_TOKEN,
|
458
|
-
detail=
|
245
|
+
detail=token,
|
459
246
|
source_function="security.verify_jwt_token"
|
460
247
|
)
|
461
|
-
|
248
|
+
|
249
|
+
if expected_type and token_type != expected_type:
|
250
|
+
raise CustomException(
|
251
|
+
ErrorCode.INVALID_TOKEN,
|
252
|
+
detail=token,
|
253
|
+
source_function="security.verify_jwt_token"
|
254
|
+
)
|
255
|
+
|
462
256
|
return payload
|
463
257
|
|
464
258
|
except JWTError as e:
|
465
259
|
raise CustomException(
|
466
260
|
ErrorCode.INVALID_TOKEN,
|
467
|
-
detail=token
|
261
|
+
detail=token,
|
468
262
|
source_function="security.verify_jwt_token",
|
469
263
|
original_error=e
|
470
264
|
)
|
@@ -476,4 +270,22 @@ async def verify_jwt_token(
|
|
476
270
|
detail=str(e),
|
477
271
|
source_function="security.verify_jwt_token",
|
478
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
|
479
291
|
)
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.53"
|
@@ -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=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
|
12
|
+
aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
|
13
|
+
aiteamutils/version.py,sha256=l6fF0dCvrM5gxQHard3VFOKO2YQLlkx_RlQ9Azm0-pk,42
|
14
|
+
aiteamutils-0.2.53.dist-info/METADATA,sha256=LKH9v0dzZbm_PRCEZ-NS-ASC1p6pRyU3h8Mk6Nw4IYE,1718
|
15
|
+
aiteamutils-0.2.53.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.53.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=Be0hr833X3zMz2gaJYugvE9E7o05uBmXKPhZiGFJQTM,38984
|
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=8MKWHJHM8zYlzctvZN4uCH_rdHayPQd-Sxyv9JJ2554,42
|
14
|
-
aiteamutils-0.2.51.dist-info/METADATA,sha256=u7kI-u4xQZcrWK20EtNRzzzJNJvZ22GRD3eI8I7-u-M,1718
|
15
|
-
aiteamutils-0.2.51.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
-
aiteamutils-0.2.51.dist-info/RECORD,,
|
File without changes
|