aiteamutils 0.2.75__py3-none-any.whl → 0.2.77__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|