aiteamutils 0.2.75__py3-none-any.whl → 0.2.77__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_model.py +5 -4
- aiteamutils/base_repository.py +7 -2
- aiteamutils/base_service.py +12 -14
- aiteamutils/database.py +27 -25
- aiteamutils/security.py +36 -55
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.75.dist-info → aiteamutils-0.2.77.dist-info}/METADATA +1 -1
- aiteamutils-0.2.77.dist-info/RECORD +15 -0
- aiteamutils-0.2.75.dist-info/RECORD +0 -15
- {aiteamutils-0.2.75.dist-info → aiteamutils-0.2.77.dist-info}/WHEEL +0 -0
aiteamutils/base_model.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from datetime import datetime, timezone
|
1
|
+
from datetime import datetime, timedelta, timezone
|
2
2
|
from typing import Any, Dict, TypeVar, Generic, Optional
|
3
3
|
from ulid import ULID
|
4
4
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
@@ -24,16 +24,17 @@ class BaseColumn(Base):
|
|
24
24
|
nullable=False
|
25
25
|
)
|
26
26
|
created_at: Mapped[datetime] = mapped_column(
|
27
|
-
default=datetime.
|
27
|
+
default=datetime.now(timezone.utc),
|
28
28
|
index=True
|
29
29
|
)
|
30
30
|
updated_at: Mapped[datetime] = mapped_column(
|
31
|
-
default=datetime.
|
32
|
-
onupdate=datetime.
|
31
|
+
default=datetime.now(timezone.utc),
|
32
|
+
onupdate=datetime.now(timezone.utc),
|
33
33
|
index=True
|
34
34
|
)
|
35
35
|
deleted_at: Mapped[datetime] = mapped_column(
|
36
36
|
default=None,
|
37
|
+
nullable=True
|
37
38
|
)
|
38
39
|
is_deleted: Mapped[bool] = mapped_column(
|
39
40
|
default=False,
|
aiteamutils/base_repository.py
CHANGED
@@ -39,12 +39,17 @@ class BaseRepository(Generic[ModelType]):
|
|
39
39
|
#######################
|
40
40
|
# 입력 및 수정, 삭제 #
|
41
41
|
#######################
|
42
|
-
async def create(
|
42
|
+
async def create(
|
43
|
+
self,
|
44
|
+
entity_data: Dict[str, Any],
|
45
|
+
exclude_entities: List[str] | None = None
|
46
|
+
) -> ModelType:
|
43
47
|
try:
|
44
48
|
return await create_entity(
|
45
49
|
session=self.session,
|
46
50
|
model=self.model,
|
47
|
-
entity_data=entity_data
|
51
|
+
entity_data=entity_data,
|
52
|
+
exclude_entities=exclude_entities
|
48
53
|
)
|
49
54
|
except CustomException as e:
|
50
55
|
raise e
|
aiteamutils/base_service.py
CHANGED
@@ -46,21 +46,19 @@ class BaseService(Generic[ModelType]):
|
|
46
46
|
) -> ModelType:
|
47
47
|
|
48
48
|
try:
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
if "password" in entity_data:
|
57
|
-
entity_data["password"] = hash_password(entity_data["password"])
|
58
|
-
# 제외할 엔티티가 있으면 제외
|
59
|
-
if exclude_entities:
|
60
|
-
entity_data = {k: v for k, v in entity_data.items() if k not in exclude_entities}
|
49
|
+
async with self.db_session.begin():
|
50
|
+
# 고유 검사 수행
|
51
|
+
if unique_check:
|
52
|
+
await validate_unique_fields(self.db_session, unique_check, find_value=True)
|
53
|
+
# 외래 키 검사 수행
|
54
|
+
if fk_check:
|
55
|
+
await validate_unique_fields(self.db_session, fk_check, find_value=False)
|
61
56
|
|
62
|
-
|
63
|
-
|
57
|
+
# repository의 create 메서드를 트랜잭션 내에서 실행
|
58
|
+
return await self.repository.create(
|
59
|
+
entity_data=entity_data,
|
60
|
+
exclude_entities=exclude_entities
|
61
|
+
)
|
64
62
|
except CustomException as e:
|
65
63
|
raise e
|
66
64
|
except Exception as e:
|
aiteamutils/database.py
CHANGED
@@ -23,7 +23,6 @@ from sqlalchemy import MetaData, Table, insert
|
|
23
23
|
#패키지 라이브러리
|
24
24
|
from .exceptions import ErrorCode, CustomException
|
25
25
|
|
26
|
-
|
27
26
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
28
27
|
|
29
28
|
##################
|
@@ -32,8 +31,11 @@ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
|
32
31
|
def process_entity_data(
|
33
32
|
model: Type[ModelType],
|
34
33
|
entity_data: Dict[str, Any],
|
35
|
-
existing_data: Dict[str, Any] = None
|
34
|
+
existing_data: Dict[str, Any] = None,
|
35
|
+
exclude_entities: List[str] | None = None
|
36
36
|
) -> Dict[str, Any]:
|
37
|
+
from .security import hash_password
|
38
|
+
|
37
39
|
"""
|
38
40
|
엔티티 데이터를 전처리하고 모델 속성과 extra_data를 분리합니다.
|
39
41
|
|
@@ -64,6 +66,9 @@ def process_entity_data(
|
|
64
66
|
if existing_data and "extra_data" in existing_data:
|
65
67
|
extra_data = existing_data["extra_data"].copy()
|
66
68
|
|
69
|
+
# 제외할 엔티티가 있으면 제거
|
70
|
+
if exclude_entities:
|
71
|
+
entity_data = {k: v for k, v in entity_data.items() if k not in exclude_entities}
|
67
72
|
|
68
73
|
# Swagger 자동 생성 속성 패턴
|
69
74
|
swagger_patterns = {"additionalProp1", "additionalProp2", "additionalProp3"}
|
@@ -73,9 +78,14 @@ def process_entity_data(
|
|
73
78
|
if key in swagger_patterns:
|
74
79
|
continue
|
75
80
|
|
81
|
+
# 패스워드 필드 처리
|
82
|
+
if key == "password":
|
83
|
+
value = hash_password(value) # 비밀번호 암호화
|
84
|
+
|
76
85
|
# 모델 속성에 있는 경우 model_data에 추가
|
77
86
|
if key in model_attr:
|
78
87
|
model_data[key] = value
|
88
|
+
|
79
89
|
# 모델 속성에 없는 경우 extra_data에 추가
|
80
90
|
else:
|
81
91
|
extra_data[key] = value
|
@@ -237,7 +247,8 @@ def build_conditions(
|
|
237
247
|
async def create_entity(
|
238
248
|
session: AsyncSession,
|
239
249
|
model: Type[ModelType],
|
240
|
-
entity_data: Dict[str, Any]
|
250
|
+
entity_data: Dict[str, Any],
|
251
|
+
exclude_entities: List[str] | None = None
|
241
252
|
) -> ModelType:
|
242
253
|
"""
|
243
254
|
새로운 엔티티를 데이터베이스에 생성합니다.
|
@@ -255,9 +266,11 @@ async def create_entity(
|
|
255
266
|
"""
|
256
267
|
try:
|
257
268
|
# 엔티티 데이터 전처리
|
258
|
-
processed_data = process_entity_data(
|
259
|
-
|
260
|
-
|
269
|
+
processed_data = process_entity_data(
|
270
|
+
model=model,
|
271
|
+
entity_data=entity_data,
|
272
|
+
exclude_entities=exclude_entities
|
273
|
+
)
|
261
274
|
|
262
275
|
# 새로운 엔티티 생성
|
263
276
|
entity = model(**processed_data)
|
@@ -267,7 +280,6 @@ async def create_entity(
|
|
267
280
|
|
268
281
|
# 데이터베이스에 커밋
|
269
282
|
await session.flush()
|
270
|
-
await session.commit()
|
271
283
|
await session.refresh(entity)
|
272
284
|
|
273
285
|
# 생성된 엔티티 반환
|
@@ -336,7 +348,6 @@ async def update_entity(
|
|
336
348
|
|
337
349
|
# 변경 사항 커밋
|
338
350
|
await session.flush()
|
339
|
-
await session.commit()
|
340
351
|
await session.refresh(entity)
|
341
352
|
|
342
353
|
return entity
|
@@ -373,7 +384,6 @@ async def delete_entity(
|
|
373
384
|
entity.deleted_at = datetime.now()
|
374
385
|
|
375
386
|
await session.flush()
|
376
|
-
await session.commit()
|
377
387
|
await session.refresh(entity)
|
378
388
|
|
379
389
|
except SQLAlchemyError as e:
|
@@ -498,33 +508,25 @@ async def get_entity(
|
|
498
508
|
##################
|
499
509
|
async def log_create(
|
500
510
|
session: AsyncSession,
|
501
|
-
|
511
|
+
model: str,
|
502
512
|
log_data: Dict[str, Any],
|
503
|
-
request: Request = None
|
513
|
+
request: Optional[Request] = None
|
504
514
|
) -> None:
|
505
515
|
try:
|
506
|
-
# ULID 생성
|
507
|
-
if "ulid" not in log_data:
|
508
|
-
log_data["ulid"] = ULID()
|
509
|
-
|
510
516
|
# 사용자 에이전트 및 IP 주소 추가
|
511
517
|
if request:
|
512
518
|
log_data["user_agent"] = request.headers.get("user-agent")
|
513
519
|
log_data["ip_address"] = request.headers.get("x-forwarded-for") or request.client.host
|
514
520
|
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
insert_stmt = insert(table).values(log_data)
|
521
|
-
await session.execute(insert_stmt)
|
522
|
-
await session.commit()
|
523
|
-
|
521
|
+
await create_entity(
|
522
|
+
session=session,
|
523
|
+
model=model,
|
524
|
+
entity_data=log_data
|
525
|
+
)
|
524
526
|
except Exception as e:
|
525
527
|
raise CustomException(
|
526
528
|
ErrorCode.INTERNAL_ERROR,
|
527
|
-
detail=f"{model
|
529
|
+
detail=f"{model}|{str(e)}",
|
528
530
|
source_function="database.log_create",
|
529
531
|
original_error=e
|
530
532
|
)
|
aiteamutils/security.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
"""보안 관련 유틸리티."""
|
2
|
-
from datetime import datetime, timedelta,
|
2
|
+
from datetime import datetime, timedelta, timezone
|
3
3
|
from typing import Dict, Any, Optional, Literal, Callable, TYPE_CHECKING
|
4
4
|
from fastapi import Request, HTTPException, status
|
5
5
|
from functools import wraps
|
@@ -85,7 +85,7 @@ def rate_limit(
|
|
85
85
|
rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
|
86
86
|
|
87
87
|
try:
|
88
|
-
now = datetime.now(
|
88
|
+
now = datetime.now(timezone.utc)
|
89
89
|
|
90
90
|
# 현재 rate limit 정보 가져오기
|
91
91
|
rate_info = _rate_limits.get(rate_limit_key)
|
@@ -136,54 +136,54 @@ async def create_jwt_token(
|
|
136
136
|
user_data: Dict[str, Any],
|
137
137
|
token_type: Literal["access", "refresh"],
|
138
138
|
db_session: AsyncSession,
|
139
|
-
token_settings: Dict[str,
|
140
|
-
request: Optional[Request] = None,
|
139
|
+
token_settings: Dict[str, Any],
|
141
140
|
) -> str:
|
142
141
|
"""JWT 토큰을 생성하고 로그를 기록합니다."""
|
143
142
|
try:
|
144
143
|
# 토큰 데이터 구성
|
145
144
|
if token_type == "access":
|
146
|
-
expires_at = datetime.now(
|
145
|
+
expires_at = datetime.now(timezone.utc) + timedelta(minutes=token_settings["ACCESS_TOKEN_EXPIRE_MINUTES"])
|
146
|
+
|
147
147
|
token_data = {
|
148
148
|
# 등록 클레임
|
149
|
-
"iss": token_settings
|
150
|
-
"sub": user_data
|
151
|
-
"aud": token_settings
|
149
|
+
"iss": token_settings["TOKEN_ISSUER"],
|
150
|
+
"sub": user_data.username,
|
151
|
+
"aud": token_settings["TOKEN_AUDIENCE"],
|
152
152
|
"exp": expires_at,
|
153
153
|
|
154
154
|
# 공개 클레임
|
155
|
-
"username": user_data
|
156
|
-
"name": user_data.
|
155
|
+
"username": user_data.username,
|
156
|
+
"name": user_data.name,
|
157
157
|
|
158
158
|
# 비공개 클레임
|
159
|
-
"user_ulid": user_data
|
160
|
-
"role_ulid": user_data.
|
161
|
-
"status": user_data.
|
162
|
-
"last_login": datetime.now(
|
159
|
+
"user_ulid": user_data.ulid,
|
160
|
+
"role_ulid": user_data.role_ulid,
|
161
|
+
"status": user_data.status,
|
162
|
+
"last_login": datetime.now(timezone.utc).isoformat(),
|
163
163
|
"token_type": token_type,
|
164
164
|
|
165
165
|
# 조직 관련 클레임
|
166
|
-
"organization_ulid": user_data.
|
167
|
-
"organization_id": user_data.
|
168
|
-
"organization_name": user_data.
|
169
|
-
"company_name": user_data.
|
166
|
+
"organization_ulid": user_data.role.organization.ulid,
|
167
|
+
"organization_id": user_data.role.organization.id,
|
168
|
+
"organization_name": user_data.role.organization.name,
|
169
|
+
"company_name": user_data.role.organization.company.name
|
170
170
|
}
|
171
171
|
else: # refresh token
|
172
|
-
expires_at = datetime.now(
|
172
|
+
expires_at = datetime.now(timezone.utc) + timedelta(days=14)
|
173
173
|
token_data = {
|
174
|
-
"iss": token_settings
|
175
|
-
"sub": user_data
|
174
|
+
"iss": token_settings["TOKEN_ISSUER"],
|
175
|
+
"sub": user_data.username,
|
176
176
|
"exp": expires_at,
|
177
177
|
"token_type": token_type,
|
178
|
-
"user_ulid": user_data
|
178
|
+
"user_ulid": user_data.ulid
|
179
179
|
}
|
180
180
|
|
181
181
|
# JWT 토큰 생성
|
182
182
|
try:
|
183
183
|
token = jwt.encode(
|
184
184
|
token_data,
|
185
|
-
token_settings
|
186
|
-
algorithm=token_settings
|
185
|
+
token_settings["JWT_SECRET"],
|
186
|
+
algorithm=token_settings["JWT_ALGORITHM"]
|
187
187
|
)
|
188
188
|
except Exception as e:
|
189
189
|
raise CustomException(
|
@@ -192,26 +192,6 @@ async def create_jwt_token(
|
|
192
192
|
source_function="security.create_jwt_token",
|
193
193
|
original_error=e
|
194
194
|
)
|
195
|
-
|
196
|
-
# 로그 생성
|
197
|
-
try:
|
198
|
-
log_data = {
|
199
|
-
"type": ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED,
|
200
|
-
"user_ulid": user_data["ulid"],
|
201
|
-
"token": token
|
202
|
-
}
|
203
|
-
|
204
|
-
# log_create 함수 호출
|
205
|
-
await log_create(
|
206
|
-
session=db_session,
|
207
|
-
table_name="user_logs",
|
208
|
-
log_data=log_data,
|
209
|
-
request=request
|
210
|
-
)
|
211
|
-
except Exception as e:
|
212
|
-
# 로그 생성 실패는 토큰 생성에 영향을 주지 않음
|
213
|
-
logging.error(f"Failed to create token log: {str(e)}")
|
214
|
-
|
215
195
|
return token
|
216
196
|
|
217
197
|
except CustomException as e:
|
@@ -227,19 +207,18 @@ async def create_jwt_token(
|
|
227
207
|
|
228
208
|
async def verify_jwt_token(
|
229
209
|
token: str,
|
230
|
-
expected_type: Optional[Literal["access", "refresh"]] = None
|
210
|
+
expected_type: Optional[Literal["access", "refresh"]] = None,
|
211
|
+
token_settings: Dict[str, Any] = None
|
231
212
|
) -> Dict[str, Any]:
|
232
213
|
"""JWT 토큰을 검증합니다."""
|
233
214
|
try:
|
234
|
-
settings = get_settings()
|
235
|
-
|
236
215
|
# 토큰 디코딩
|
237
216
|
payload = jwt.decode(
|
238
217
|
token,
|
239
|
-
|
240
|
-
algorithms=[
|
241
|
-
audience=
|
242
|
-
issuer=
|
218
|
+
token_settings["JWT_SECRET"],
|
219
|
+
algorithms=[token_settings["JWT_ALGORITHM"]],
|
220
|
+
audience=token_settings["TOKEN_AUDIENCE"],
|
221
|
+
issuer=token_settings["TOKEN_ISSUER"]
|
243
222
|
)
|
244
223
|
|
245
224
|
# 토큰 타입 검증
|
@@ -277,14 +256,16 @@ async def verify_jwt_token(
|
|
277
256
|
original_error=e
|
278
257
|
)
|
279
258
|
|
280
|
-
def validate_token(
|
259
|
+
def validate_token(
|
260
|
+
token: str,
|
261
|
+
token_settings: Dict[str, Any] = None
|
262
|
+
) -> Dict[str, Any]:
|
281
263
|
"""JWT 토큰을 검증하고 페이로드를 반환합니다."""
|
282
264
|
try:
|
283
|
-
settings = get_settings()
|
284
265
|
payload = jwt.decode(
|
285
266
|
token,
|
286
|
-
|
287
|
-
algorithms=[
|
267
|
+
token_settings["JWT_SECRET"],
|
268
|
+
algorithms=[token_settings["JWT_ALGORITHM"]]
|
288
269
|
)
|
289
270
|
return payload
|
290
271
|
except JWTError as e:
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.77"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
|
2
|
+
aiteamutils/base_model.py,sha256=SWifZPzlUGyKAjmV7Uoe7PUhedm215efD7zOay8DhNc,2906
|
3
|
+
aiteamutils/base_repository.py,sha256=VLCLLVkNnYLmZ6EMcr3Tu0RkMqDyMhtCO3M_2j6q-R8,4409
|
4
|
+
aiteamutils/base_service.py,sha256=GmO_fqrSEbIs_Jc5BoRBTLfaJaS6CIxYPAs4B4TdtaU,9123
|
5
|
+
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
|
+
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
|
+
aiteamutils/database.py,sha256=zf5Ab5ZFYQBrtvxW9te9RoVsWlMkOpTdmJ8rDwfvINs,19192
|
8
|
+
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
9
|
+
aiteamutils/exceptions.py,sha256=3FUCIqXgYmMqonnMgUlh-J2xtApiiCgg4WM-2UV4vmQ,15823
|
10
|
+
aiteamutils/security.py,sha256=I8X_ABREYHObUZt1xi70v0dTCGUgGCmI6NxieZCN1qo,9648
|
11
|
+
aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
|
12
|
+
aiteamutils/version.py,sha256=73Vlr-_6fNkFw1Zd12TlIw0hBU7AOnfd-bV9rDxyz3Q,42
|
13
|
+
aiteamutils-0.2.77.dist-info/METADATA,sha256=64hwUhsBPM-DwlehLu8fXmUwJOnkRQSKWr05BxUFNxI,1718
|
14
|
+
aiteamutils-0.2.77.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
+
aiteamutils-0.2.77.dist-info/RECORD,,
|
@@ -1,15 +0,0 @@
|
|
1
|
-
aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
|
2
|
-
aiteamutils/base_model.py,sha256=TJiiA7bFhePObPJEzF1Qz4ekPfFoIxKGpe848BBdHLc,2840
|
3
|
-
aiteamutils/base_repository.py,sha256=oTZP3mrkqZwAGv8cNqi0tp9wellvTuO6EXN1P1o7zRs,4285
|
4
|
-
aiteamutils/base_service.py,sha256=tkGGVN8wrx9dmCVVnRSsoYGPmVD2H7NmEz-PoifIvW0,9300
|
5
|
-
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
|
-
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
|
-
aiteamutils/database.py,sha256=8T8c7RpcPEcBXO-AU_-1UhnTI4RwkPLxWF0eWQYwnME,19080
|
8
|
-
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
9
|
-
aiteamutils/exceptions.py,sha256=3FUCIqXgYmMqonnMgUlh-J2xtApiiCgg4WM-2UV4vmQ,15823
|
10
|
-
aiteamutils/security.py,sha256=7cKY6XhiSEkQJe-XYE2tyYc2u1JLYeXUEjg7AUzEnQY,10277
|
11
|
-
aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
|
12
|
-
aiteamutils/version.py,sha256=FgfB6O4LkLSpldQbG5FG5Fai_x5fo9cg9AQ85w_3RF8,42
|
13
|
-
aiteamutils-0.2.75.dist-info/METADATA,sha256=QtxRHW3XcGt5NndEB95Fue80VRGgM54zWbLtssOtkcI,1718
|
14
|
-
aiteamutils-0.2.75.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
-
aiteamutils-0.2.75.dist-info/RECORD,,
|
File without changes
|