aiteamutils 0.2.51__py3-none-any.whl → 0.2.53__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 +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
|