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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any