aiteamutils 0.2.72__py3-none-any.whl → 0.2.75__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 +9 -2
- aiteamutils/base_repository.py +83 -5
- aiteamutils/base_service.py +201 -21
- aiteamutils/database.py +393 -54
- aiteamutils/exceptions.py +6 -2
- aiteamutils/security.py +28 -23
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.72.dist-info → aiteamutils-0.2.75.dist-info}/METADATA +1 -1
- aiteamutils-0.2.75.dist-info/RECORD +15 -0
- aiteamutils-0.2.72.dist-info/RECORD +0 -15
- {aiteamutils-0.2.72.dist-info → aiteamutils-0.2.75.dist-info}/WHEEL +0 -0
aiteamutils/base_model.py
CHANGED
@@ -23,10 +23,17 @@ class BaseColumn(Base):
|
|
23
23
|
doc="ULID",
|
24
24
|
nullable=False
|
25
25
|
)
|
26
|
-
created_at: Mapped[datetime] = mapped_column(
|
26
|
+
created_at: Mapped[datetime] = mapped_column(
|
27
|
+
default=datetime.utcnow,
|
28
|
+
index=True
|
29
|
+
)
|
27
30
|
updated_at: Mapped[datetime] = mapped_column(
|
28
31
|
default=datetime.utcnow,
|
29
|
-
onupdate=datetime.utcnow
|
32
|
+
onupdate=datetime.utcnow,
|
33
|
+
index=True
|
34
|
+
)
|
35
|
+
deleted_at: Mapped[datetime] = mapped_column(
|
36
|
+
default=None,
|
30
37
|
)
|
31
38
|
is_deleted: Mapped[bool] = mapped_column(
|
32
39
|
default=False,
|
aiteamutils/base_repository.py
CHANGED
@@ -7,7 +7,13 @@ from sqlalchemy import select
|
|
7
7
|
|
8
8
|
#패키지 라이브러리
|
9
9
|
from .exceptions import ErrorCode, CustomException
|
10
|
-
from .database import
|
10
|
+
from .database import (
|
11
|
+
list_entities,
|
12
|
+
get_entity,
|
13
|
+
create_entity,
|
14
|
+
update_entity,
|
15
|
+
delete_entity
|
16
|
+
)
|
11
17
|
|
12
18
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
13
19
|
|
@@ -29,13 +35,62 @@ class BaseRepository(Generic[ModelType]):
|
|
29
35
|
source_function=f"{self.__class__.__name__}.session"
|
30
36
|
)
|
31
37
|
self._session = value
|
38
|
+
|
39
|
+
#######################
|
40
|
+
# 입력 및 수정, 삭제 #
|
41
|
+
#######################
|
42
|
+
async def create(self, entity_data: Dict[str, Any]) -> ModelType:
|
43
|
+
try:
|
44
|
+
return await create_entity(
|
45
|
+
session=self.session,
|
46
|
+
model=self.model,
|
47
|
+
entity_data=entity_data
|
48
|
+
)
|
49
|
+
except CustomException as e:
|
50
|
+
raise e
|
51
|
+
except Exception as e:
|
52
|
+
raise CustomException(
|
53
|
+
ErrorCode.INTERNAL_ERROR,
|
54
|
+
detail=str(e),
|
55
|
+
source_function=f"{self.__class__.__name__}.create",
|
56
|
+
original_error=e
|
57
|
+
)
|
58
|
+
|
59
|
+
async def update(
|
60
|
+
self,
|
61
|
+
update_data: Dict[str, Any],
|
62
|
+
conditions: Dict[str, Any]
|
63
|
+
) -> ModelType:
|
64
|
+
try:
|
65
|
+
return await update_entity(
|
66
|
+
session=self.session,
|
67
|
+
model=self.model,
|
68
|
+
update_data=update_data,
|
69
|
+
conditions=conditions
|
70
|
+
)
|
71
|
+
except CustomException as e:
|
72
|
+
raise e
|
73
|
+
|
74
|
+
async def delete(
|
75
|
+
self,
|
76
|
+
conditions: Dict[str, Any]
|
77
|
+
) -> None:
|
78
|
+
await delete_entity(
|
79
|
+
session=self.session,
|
80
|
+
model=self.model,
|
81
|
+
conditions=conditions
|
82
|
+
)
|
32
83
|
|
84
|
+
#########################
|
85
|
+
# 조회 및 검색 메서드 #
|
86
|
+
#########################
|
33
87
|
async def list(
|
34
88
|
self,
|
35
89
|
skip: int = 0,
|
36
90
|
limit: int = 100,
|
37
91
|
filters: Optional[Dict[str, Any]] = None,
|
38
|
-
|
92
|
+
explicit_joins: Optional[List[Any]] = None,
|
93
|
+
loading_joins: Optional[List[Any]] = None
|
39
94
|
) -> List[ModelType]:
|
40
95
|
"""
|
41
96
|
엔티티 목록 조회.
|
@@ -48,11 +103,10 @@ class BaseRepository(Generic[ModelType]):
|
|
48
103
|
skip=skip,
|
49
104
|
limit=limit,
|
50
105
|
filters=filters,
|
51
|
-
|
106
|
+
explicit_joins=explicit_joins,
|
107
|
+
loading_joins=loading_joins
|
52
108
|
)
|
53
109
|
except CustomException as e:
|
54
|
-
e.detail = f"Repository list error for {self.model.__tablename__}: {e.detail}"
|
55
|
-
e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
|
56
110
|
raise e
|
57
111
|
except Exception as e:
|
58
112
|
raise CustomException(
|
@@ -61,3 +115,27 @@ class BaseRepository(Generic[ModelType]):
|
|
61
115
|
source_function=f"{self.__class__.__name__}.list",
|
62
116
|
original_error=e
|
63
117
|
)
|
118
|
+
|
119
|
+
async def get(
|
120
|
+
self,
|
121
|
+
conditions: Dict[str, Any] | None = None,
|
122
|
+
explicit_joins: Optional[List[Any]] = None,
|
123
|
+
loading_joins: Optional[List[Any]] = None
|
124
|
+
) -> ModelType:
|
125
|
+
try:
|
126
|
+
return await get_entity(
|
127
|
+
session=self.session,
|
128
|
+
model=self.model,
|
129
|
+
conditions=conditions,
|
130
|
+
explicit_joins=explicit_joins,
|
131
|
+
loading_joins=loading_joins
|
132
|
+
)
|
133
|
+
except CustomException as e:
|
134
|
+
raise e
|
135
|
+
except Exception as e:
|
136
|
+
raise CustomException(
|
137
|
+
ErrorCode.INTERNAL_ERROR,
|
138
|
+
detail=str(e),
|
139
|
+
source_function=f"{self.__class__.__name__}.get",
|
140
|
+
original_error=e
|
141
|
+
)
|
aiteamutils/base_service.py
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
#기본 라이브러리
|
2
2
|
from fastapi import Request
|
3
|
-
from typing import TypeVar, Generic, Type, Dict, Any, Union, List
|
3
|
+
from typing import TypeVar, Generic, Type, Dict, Any, Union, List, Optional
|
4
4
|
from sqlalchemy.orm import DeclarativeBase
|
5
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
6
6
|
from datetime import datetime
|
7
|
+
from ulid import ULID
|
7
8
|
|
8
9
|
#패키지 라이브러리
|
9
10
|
from .exceptions import ErrorCode, CustomException
|
10
11
|
from .base_repository import BaseRepository
|
11
12
|
from .database import (
|
12
13
|
process_response,
|
13
|
-
|
14
|
+
validate_unique_fields
|
14
15
|
)
|
15
|
-
|
16
|
+
from .security import hash_password
|
16
17
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
17
18
|
|
18
19
|
class BaseService(Generic[ModelType]):
|
@@ -30,27 +31,151 @@ class BaseService(Generic[ModelType]):
|
|
30
31
|
self.repository = repository
|
31
32
|
self.db_session = db_session
|
32
33
|
self.additional_models = additional_models or {},
|
34
|
+
|
35
|
+
#######################
|
36
|
+
# 입력 및 수정, 삭제 #
|
37
|
+
#######################
|
38
|
+
async def create(
|
39
|
+
self,
|
40
|
+
entity_data: Dict[str, Any],
|
41
|
+
model_name: str | None = None,
|
42
|
+
response_model: Any = None,
|
43
|
+
exclude_entities: List[str] | None = None,
|
44
|
+
unique_check: List[Dict[str, Any]] | None = None,
|
45
|
+
fk_check: List[Dict[str, Any]] | None = None,
|
46
|
+
) -> ModelType:
|
47
|
+
|
48
|
+
try:
|
49
|
+
# 고유 검사 수행
|
50
|
+
if unique_check:
|
51
|
+
await validate_unique_fields(self.db_session, unique_check, find_value=True)
|
52
|
+
# 외래 키 검사 수행
|
53
|
+
if fk_check:
|
54
|
+
await validate_unique_fields(self.db_session, fk_check, find_value=False)
|
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}
|
61
|
+
|
62
|
+
# repository의 create 메서드를 트랜잭션 내에서 실행
|
63
|
+
return await self.repository.create(entity_data)
|
64
|
+
except CustomException as e:
|
65
|
+
raise e
|
66
|
+
except Exception as e:
|
67
|
+
# 다른 예외 처리
|
68
|
+
raise CustomException(
|
69
|
+
ErrorCode.INTERNAL_ERROR,
|
70
|
+
detail=str(e),
|
71
|
+
source_function=f"{self.__class__.__name__}.create",
|
72
|
+
original_error=e
|
73
|
+
)
|
74
|
+
|
75
|
+
async def update(
|
76
|
+
self,
|
77
|
+
ulid: str | None = None,
|
78
|
+
update_data: Dict[str, Any] | None = None,
|
79
|
+
conditions: Dict[str, Any] | None = None,
|
80
|
+
unique_check: List[Dict[str, Any]] | None = None,
|
81
|
+
exclude_entities: List[str] | None = None
|
82
|
+
) -> ModelType:
|
83
|
+
try:
|
84
|
+
# 고유 검사 수행
|
85
|
+
if unique_check:
|
86
|
+
await validate_unique_fields(self.db_session, unique_check, find_value=True)
|
87
|
+
# 비밀번호가 있으면 해시 처리
|
88
|
+
if "password" in update_data:
|
89
|
+
update_data["password"] = hash_password(update_data["password"])
|
90
|
+
# 제외할 엔티티가 있으면 제외
|
91
|
+
if exclude_entities:
|
92
|
+
update_data = {k: v for k, v in update_data.items() if k not in exclude_entities}
|
93
|
+
|
94
|
+
if not ulid and not conditions:
|
95
|
+
raise CustomException(
|
96
|
+
ErrorCode.INVALID_INPUT,
|
97
|
+
detail="Either 'ulid' or 'conditions' must be provided.",
|
98
|
+
source_function="database.update_entity"
|
99
|
+
)
|
100
|
+
|
101
|
+
# ulid로 조건 생성
|
102
|
+
if ulid:
|
103
|
+
if not ULID.from_str(ulid):
|
104
|
+
raise CustomException(
|
105
|
+
ErrorCode.VALIDATION_ERROR,
|
106
|
+
detail=ulid,
|
107
|
+
source_function=f"{self.__class__.__name__}.update"
|
108
|
+
)
|
109
|
+
|
110
|
+
conditions = {"ulid": ulid}
|
111
|
+
|
112
|
+
return await self.repository.update(
|
113
|
+
update_data=update_data,
|
114
|
+
conditions=conditions
|
115
|
+
)
|
116
|
+
except CustomException as e:
|
117
|
+
raise e
|
118
|
+
except Exception as e:
|
119
|
+
raise CustomException(
|
120
|
+
ErrorCode.INTERNAL_ERROR,
|
121
|
+
detail=str(e),
|
122
|
+
source_function=f"{self.__class__.__name__}.update",
|
123
|
+
original_error=e
|
124
|
+
)
|
125
|
+
|
126
|
+
async def delete(
|
127
|
+
self,
|
128
|
+
ulid: str | None = None,
|
129
|
+
conditions: Dict[str, Any] | None = None
|
130
|
+
) -> None:
|
131
|
+
try:
|
132
|
+
if not ULID.from_str(ulid):
|
133
|
+
raise CustomException(
|
134
|
+
ErrorCode.VALIDATION_ERROR,
|
135
|
+
detail=ulid,
|
136
|
+
source_function=f"{self.__class__.__name__}.delete"
|
137
|
+
)
|
138
|
+
|
139
|
+
if not ulid and not conditions:
|
140
|
+
raise CustomException(
|
141
|
+
ErrorCode.INVALID_INPUT,
|
142
|
+
detail="Either 'ulid' or 'conditions' must be provided.",
|
143
|
+
source_function="database.update_entity"
|
144
|
+
)
|
33
145
|
|
146
|
+
# ulid로 조건 생성
|
147
|
+
if ulid:
|
148
|
+
conditions = {"ulid": ulid}
|
149
|
+
|
150
|
+
conditions["is_deleted"] = False
|
151
|
+
|
152
|
+
return await self.repository.delete(
|
153
|
+
conditions=conditions
|
154
|
+
)
|
155
|
+
except CustomException as e:
|
156
|
+
raise e
|
157
|
+
except Exception as e:
|
158
|
+
raise CustomException(
|
159
|
+
ErrorCode.INTERNAL_ERROR,
|
160
|
+
detail=str(e),
|
161
|
+
source_function=f"{self.__class__.__name__}.delete",
|
162
|
+
original_error=e
|
163
|
+
)
|
164
|
+
|
165
|
+
#########################
|
166
|
+
# 조회 및 검색 메서드 #
|
167
|
+
#########################
|
34
168
|
async def list(
|
35
169
|
self,
|
36
170
|
skip: int = 0,
|
37
171
|
limit: int = 100,
|
38
|
-
filters: Dict[str, Any] | None = None,
|
39
|
-
search_params: Dict[str, Any] | None = None,
|
172
|
+
filters: List[Dict[str, Any]] | None = None,
|
40
173
|
model_name: str | None = None,
|
41
|
-
|
42
|
-
|
174
|
+
response_model: Any = None,
|
175
|
+
explicit_joins: Optional[List[Any]] = None,
|
176
|
+
loading_joins: Optional[List[Any]] = None
|
43
177
|
) -> List[Dict[str, Any]]:
|
44
178
|
try:
|
45
|
-
# 검색 조건 처리 및 필터 병합
|
46
|
-
if search_params:
|
47
|
-
search_filters = build_search_filters(request, search_params)
|
48
|
-
if filters:
|
49
|
-
# 기존 filters와 search_filters 병합 (search_filters가 우선 적용)
|
50
|
-
filters.update(search_filters)
|
51
|
-
else:
|
52
|
-
filters = search_filters
|
53
|
-
|
54
179
|
# 모델 이름을 통한 동적 처리
|
55
180
|
if model_name:
|
56
181
|
if model_name not in self.additional_models:
|
@@ -60,19 +185,74 @@ class BaseService(Generic[ModelType]):
|
|
60
185
|
source_function=f"{self.__class__.__name__}.list"
|
61
186
|
)
|
62
187
|
model = self.additional_models[model_name]
|
63
|
-
|
188
|
+
entities = await self.repository.list(
|
189
|
+
skip=skip,
|
190
|
+
limit=limit,
|
191
|
+
filters=filters,
|
192
|
+
model=model
|
193
|
+
)
|
194
|
+
return [process_response(entity, response_model) for entity in entities]
|
64
195
|
|
65
|
-
|
196
|
+
entities = await self.repository.list(
|
197
|
+
skip=skip,
|
198
|
+
limit=limit,
|
199
|
+
filters=filters,
|
200
|
+
explicit_joins=explicit_joins,
|
201
|
+
loading_joins=loading_joins
|
202
|
+
)
|
203
|
+
return [process_response(entity, response_model) for entity in entities]
|
204
|
+
|
66
205
|
except CustomException as e:
|
67
|
-
e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
|
68
|
-
e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
|
69
206
|
raise e
|
70
207
|
except Exception as e:
|
71
208
|
raise CustomException(
|
72
209
|
ErrorCode.INTERNAL_ERROR,
|
73
|
-
detail=str(e),
|
74
210
|
source_function=f"{self.__class__.__name__}.list",
|
75
211
|
original_error=e
|
76
212
|
)
|
213
|
+
|
214
|
+
async def get(
|
215
|
+
self,
|
216
|
+
ulid: str,
|
217
|
+
model_name: str | None = None,
|
218
|
+
response_model: Any = None,
|
219
|
+
conditions: Dict[str, Any] | None = None,
|
220
|
+
explicit_joins: Optional[List[Any]] = None,
|
221
|
+
loading_joins: Optional[List[Any]] = None
|
222
|
+
):
|
223
|
+
try:
|
224
|
+
if not ulid and not conditions:
|
225
|
+
raise CustomException(
|
226
|
+
ErrorCode.INVALID_INPUT,
|
227
|
+
detail="Either 'ulid' or 'conditions' must be provided.",
|
228
|
+
source_function="database.update_entity"
|
229
|
+
)
|
230
|
+
|
231
|
+
# ulid로 조건 생성
|
232
|
+
if ulid:
|
233
|
+
if not ULID.from_str(ulid):
|
234
|
+
raise CustomException(
|
235
|
+
ErrorCode.VALIDATION_ERROR,
|
236
|
+
detail=ulid,
|
237
|
+
source_function=f"{self.__class__.__name__}.update"
|
238
|
+
)
|
239
|
+
|
240
|
+
conditions = {"ulid": ulid}
|
241
|
+
|
242
|
+
entity = await self.repository.get(
|
243
|
+
conditions=conditions,
|
244
|
+
explicit_joins=explicit_joins,
|
245
|
+
loading_joins=loading_joins
|
246
|
+
)
|
247
|
+
return process_response(entity, response_model)
|
77
248
|
|
249
|
+
except CustomException as e:
|
250
|
+
raise e
|
251
|
+
except Exception as e:
|
252
|
+
raise CustomException(
|
253
|
+
ErrorCode.INTERNAL_ERROR,
|
254
|
+
detail=str(e),
|
255
|
+
source_function=f"{self.__class__.__name__}.get",
|
256
|
+
original_error=e
|
257
|
+
)
|
78
258
|
|
aiteamutils/database.py
CHANGED
@@ -1,10 +1,24 @@
|
|
1
1
|
#기본 라이브러리
|
2
|
-
from typing import
|
2
|
+
from typing import (
|
3
|
+
TypeVar,
|
4
|
+
Generic,
|
5
|
+
Type,
|
6
|
+
Any,
|
7
|
+
Dict,
|
8
|
+
List,
|
9
|
+
Optional,
|
10
|
+
AsyncGenerator
|
11
|
+
)
|
3
12
|
from sqlalchemy.ext.asyncio import AsyncSession
|
4
|
-
from sqlalchemy import select, and_
|
5
|
-
from sqlalchemy.orm import DeclarativeBase
|
13
|
+
from sqlalchemy import select, and_, or_
|
14
|
+
from sqlalchemy.orm import DeclarativeBase, joinedload, selectinload
|
6
15
|
from sqlalchemy.exc import SQLAlchemyError
|
7
16
|
from datetime import datetime
|
17
|
+
from contextlib import asynccontextmanager
|
18
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
19
|
+
from fastapi import Request
|
20
|
+
from ulid import ULID
|
21
|
+
from sqlalchemy import MetaData, Table, insert
|
8
22
|
|
9
23
|
#패키지 라이브러리
|
10
24
|
from .exceptions import ErrorCode, CustomException
|
@@ -15,8 +29,62 @@ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
|
15
29
|
##################
|
16
30
|
# 전처리 #
|
17
31
|
##################
|
32
|
+
def process_entity_data(
|
33
|
+
model: Type[ModelType],
|
34
|
+
entity_data: Dict[str, Any],
|
35
|
+
existing_data: Dict[str, Any] = None
|
36
|
+
) -> Dict[str, Any]:
|
37
|
+
"""
|
38
|
+
엔티티 데이터를 전처리하고 모델 속성과 extra_data를 분리합니다.
|
39
|
+
|
40
|
+
이 함수는 다음과 같은 작업을 수행합니다:
|
41
|
+
1. 모델의 기본 속성을 식별합니다.
|
42
|
+
2. Swagger 자동 생성 속성을 제외합니다.
|
43
|
+
3. 모델 속성에 해당하는 데이터는 model_data에 저장
|
44
|
+
4. 모델 속성에 없는 데이터는 extra_data에 저장
|
45
|
+
5. 기존 엔티티 데이터의 extra_data를 유지할 수 있습니다.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
model (Type[ModelType]): 데이터 모델 클래스
|
49
|
+
entity_data (Dict[str, Any]): 처리할 엔티티 데이터
|
50
|
+
existing_entity_data (Dict[str, Any], optional): 기존 엔티티 데이터. Defaults to None.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
Dict[str, Any]: 전처리된 모델 데이터 (extra_data 포함)
|
54
|
+
"""
|
55
|
+
# 모델의 속성을 가져와서 전처리합니다.
|
56
|
+
model_attr = {
|
57
|
+
attr for attr in dir(model)
|
58
|
+
if not attr.startswith('_') and not callable(getattr(model, attr))
|
59
|
+
}
|
60
|
+
model_data = {}
|
61
|
+
extra_data = {}
|
62
|
+
|
63
|
+
# 기존 엔티티 데이터가 있으면 추가
|
64
|
+
if existing_data and "extra_data" in existing_data:
|
65
|
+
extra_data = existing_data["extra_data"].copy()
|
66
|
+
|
67
|
+
|
68
|
+
# Swagger 자동 생성 속성 패턴
|
69
|
+
swagger_patterns = {"additionalProp1", "additionalProp2", "additionalProp3"}
|
70
|
+
|
71
|
+
for key, value in entity_data.items():
|
72
|
+
# Swagger 자동 생성 속성 무시
|
73
|
+
if key in swagger_patterns:
|
74
|
+
continue
|
75
|
+
|
76
|
+
# 모델 속성에 있는 경우 model_data에 추가
|
77
|
+
if key in model_attr:
|
78
|
+
model_data[key] = value
|
79
|
+
# 모델 속성에 없는 경우 extra_data에 추가
|
80
|
+
else:
|
81
|
+
extra_data[key] = value
|
18
82
|
|
83
|
+
# extra_data가 있고 모델에 extra_data 속성이 있는 경우 추가
|
84
|
+
if extra_data and "extra_data" in model_attr:
|
85
|
+
model_data["extra_data"] = extra_data
|
19
86
|
|
87
|
+
return model_data
|
20
88
|
|
21
89
|
##################
|
22
90
|
# 응답 처리 #
|
@@ -115,81 +183,224 @@ def process_response(
|
|
115
183
|
|
116
184
|
return result
|
117
185
|
|
118
|
-
|
119
186
|
##################
|
120
187
|
# 조건 처리 #
|
121
188
|
##################
|
122
|
-
def build_search_filters(
|
123
|
-
request: Dict[str, Any],
|
124
|
-
search_params: Dict[str, Dict[str, Any]]
|
125
|
-
) -> Dict[str, Any]:
|
126
|
-
"""
|
127
|
-
요청 데이터와 검색 파라미터를 기반으로 필터 조건을 생성합니다.
|
128
|
-
|
129
|
-
Args:
|
130
|
-
request: 요청 데이터 (key-value 형태).
|
131
|
-
search_params: 검색 조건 설정을 위한 파라미터.
|
132
|
-
|
133
|
-
Returns:
|
134
|
-
filters: 필터 조건 딕셔너리.
|
135
|
-
"""
|
136
|
-
filters = {}
|
137
|
-
for key, param in search_params.items():
|
138
|
-
value = request.get(key)
|
139
|
-
if value is not None:
|
140
|
-
if param["like"]:
|
141
|
-
filters[key] = {"field": param["fields"][0], "operator": "like", "value": f"%{value}%"}
|
142
|
-
else:
|
143
|
-
filters[key] = {"field": param["fields"][0], "operator": "eq", "value": value}
|
144
|
-
return filters
|
145
|
-
|
146
189
|
def build_conditions(
|
147
|
-
|
148
|
-
|
190
|
+
filters: List[Dict[str, Any]],
|
191
|
+
model: Type[ModelType]
|
149
192
|
) -> List[Any]:
|
150
193
|
"""
|
151
194
|
필터 조건을 기반으로 SQLAlchemy 조건 리스트를 생성합니다.
|
152
195
|
|
153
196
|
Args:
|
154
|
-
filters: 필터 조건
|
197
|
+
filters: 필터 조건 리스트.
|
155
198
|
model: SQLAlchemy 모델 클래스.
|
156
199
|
|
157
200
|
Returns:
|
158
201
|
List[Any]: SQLAlchemy 조건 리스트.
|
159
202
|
"""
|
160
203
|
conditions = []
|
161
|
-
for filter_data in filters.values():
|
162
|
-
if "." in filter_data["field"]:
|
163
|
-
# 관계를 따라 암묵적으로 연결된 모델의 필드 가져오기
|
164
|
-
related_model_name, field_name = filter_data["field"].split(".")
|
165
|
-
relationship_property = getattr(model, related_model_name)
|
166
|
-
related_model = relationship_property.property.mapper.class_
|
167
|
-
field = getattr(related_model, field_name)
|
168
|
-
else:
|
169
|
-
# 현재 모델의 필드 가져오기
|
170
|
-
field = getattr(model, filter_data["field"])
|
171
204
|
|
172
|
-
|
173
|
-
|
174
|
-
value
|
205
|
+
for filter_item in filters:
|
206
|
+
value = filter_item.get("value")
|
207
|
+
if not value: # 값이 없으면 건너뜀
|
208
|
+
continue
|
209
|
+
|
210
|
+
operator = filter_item.get("operator", "eq")
|
211
|
+
or_conditions = []
|
175
212
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
213
|
+
for field_path in filter_item.get("fields", []):
|
214
|
+
current_model = model
|
215
|
+
|
216
|
+
# 관계를 따라 필드 가져오기
|
217
|
+
for part in field_path.split(".")[:-1]:
|
218
|
+
relationship_property = getattr(current_model, part)
|
219
|
+
current_model = relationship_property.property.mapper.class_
|
220
|
+
|
221
|
+
field = getattr(current_model, field_path.split(".")[-1])
|
222
|
+
|
223
|
+
# 조건 생성
|
224
|
+
if operator == "like":
|
225
|
+
or_conditions.append(field.ilike(f"%{value}%"))
|
226
|
+
elif operator == "eq":
|
227
|
+
or_conditions.append(field == value)
|
228
|
+
|
229
|
+
if or_conditions: # OR 조건이 있을 때만 추가
|
230
|
+
conditions.append(or_(*or_conditions))
|
180
231
|
|
181
232
|
return conditions
|
182
233
|
|
183
234
|
##################
|
184
235
|
# 쿼리 실행 #
|
185
236
|
##################
|
237
|
+
async def create_entity(
|
238
|
+
session: AsyncSession,
|
239
|
+
model: Type[ModelType],
|
240
|
+
entity_data: Dict[str, Any]
|
241
|
+
) -> ModelType:
|
242
|
+
"""
|
243
|
+
새로운 엔티티를 데이터베이스에 생성합니다.
|
244
|
+
|
245
|
+
Args:
|
246
|
+
session (AsyncSession): 데이터베이스 세션
|
247
|
+
model (Type[ModelType]): 생성할 모델 클래스
|
248
|
+
entity_data (Dict[str, Any]): 엔티티 생성에 필요한 데이터
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
ModelType: 생성된 엔티티
|
252
|
+
|
253
|
+
Raises:
|
254
|
+
CustomException: 엔티티 생성 중 발생하는 데이터베이스 오류
|
255
|
+
"""
|
256
|
+
try:
|
257
|
+
# 엔티티 데이터 전처리
|
258
|
+
processed_data = process_entity_data(model, entity_data)
|
259
|
+
|
260
|
+
# 외래 키 필드 검증
|
261
|
+
|
262
|
+
# 새로운 엔티티 생성
|
263
|
+
entity = model(**processed_data)
|
264
|
+
|
265
|
+
# 세션에 엔티티 추가
|
266
|
+
session.add(entity)
|
267
|
+
|
268
|
+
# 데이터베이스에 커밋
|
269
|
+
await session.flush()
|
270
|
+
await session.commit()
|
271
|
+
await session.refresh(entity)
|
272
|
+
|
273
|
+
# 생성된 엔티티 반환
|
274
|
+
return entity
|
275
|
+
|
276
|
+
except SQLAlchemyError as e:
|
277
|
+
# 데이터베이스 오류 발생 시 CustomException으로 변환
|
278
|
+
raise CustomException(
|
279
|
+
ErrorCode.DB_CREATE_ERROR,
|
280
|
+
detail=f"{model.__name__}|{str(e)}",
|
281
|
+
source_function="database.create_entity",
|
282
|
+
original_error=e
|
283
|
+
)
|
284
|
+
|
285
|
+
async def update_entity(
|
286
|
+
session: AsyncSession,
|
287
|
+
model: Type[ModelType],
|
288
|
+
conditions: Dict[str, Any],
|
289
|
+
update_data: Dict[str, Any]
|
290
|
+
) -> ModelType:
|
291
|
+
"""
|
292
|
+
조건을 기반으로 엔티티를 조회하고 업데이트합니다.
|
293
|
+
|
294
|
+
Args:
|
295
|
+
session (AsyncSession): 데이터베이스 세션
|
296
|
+
model (Type[ModelType]): 업데이트할 모델 클래스
|
297
|
+
conditions (Dict[str, Any]): 엔티티 조회 조건
|
298
|
+
conditions = {"user_id": 1, "status": "active"}
|
299
|
+
update_data (Dict[str, Any]): 업데이트할 데이터
|
300
|
+
update_data = {"status": "inactive"}
|
301
|
+
Returns:
|
302
|
+
ModelType: 업데이트된 엔티티
|
303
|
+
|
304
|
+
Raises:
|
305
|
+
CustomException: 엔티티 조회 또는 업데이트 중 발생하는 데이터베이스 오류
|
306
|
+
"""
|
307
|
+
try:
|
308
|
+
# 조건 기반 엔티티 조회
|
309
|
+
stmt = select(model)
|
310
|
+
for key, value in conditions.items():
|
311
|
+
stmt = stmt.where(getattr(model, key) == value)
|
312
|
+
|
313
|
+
result = await session.execute(stmt)
|
314
|
+
entity = result.scalar_one_or_none()
|
315
|
+
|
316
|
+
if not entity:
|
317
|
+
raise CustomException(
|
318
|
+
ErrorCode.NOT_FOUND,
|
319
|
+
detail=f"{model.__name__}|{conditions}.",
|
320
|
+
source_function="database.update_entity"
|
321
|
+
)
|
322
|
+
|
323
|
+
# 기존 데이터를 딕셔너리로 변환
|
324
|
+
existing_data = {
|
325
|
+
column.name: getattr(entity, column.name)
|
326
|
+
for column in entity.__table__.columns
|
327
|
+
}
|
328
|
+
|
329
|
+
# 데이터 병합 및 전처리
|
330
|
+
processed_data = process_entity_data(model, update_data, existing_data)
|
331
|
+
|
332
|
+
# 엔티티 데이터 업데이트
|
333
|
+
for key, value in processed_data.items():
|
334
|
+
if hasattr(entity, key):
|
335
|
+
setattr(entity, key, value)
|
336
|
+
|
337
|
+
# 변경 사항 커밋
|
338
|
+
await session.flush()
|
339
|
+
await session.commit()
|
340
|
+
await session.refresh(entity)
|
341
|
+
|
342
|
+
return entity
|
343
|
+
|
344
|
+
except SQLAlchemyError as e:
|
345
|
+
raise CustomException(
|
346
|
+
ErrorCode.DB_UPDATE_ERROR,
|
347
|
+
detail=f"{model.__name__}|{conditions}",
|
348
|
+
source_function="database.update_entity",
|
349
|
+
original_error=e
|
350
|
+
)
|
351
|
+
|
352
|
+
async def delete_entity(
|
353
|
+
session: AsyncSession,
|
354
|
+
model: Type[ModelType],
|
355
|
+
conditions: Dict[str, Any]
|
356
|
+
) -> None:
|
357
|
+
try:
|
358
|
+
stmt = select(model)
|
359
|
+
for key, value in conditions.items():
|
360
|
+
stmt = stmt.where(getattr(model, key) == value)
|
361
|
+
|
362
|
+
result = await session.execute(stmt)
|
363
|
+
entity = result.scalar_one_or_none()
|
364
|
+
|
365
|
+
if not entity:
|
366
|
+
raise CustomException(
|
367
|
+
ErrorCode.NOT_FOUND,
|
368
|
+
detail=f"{model.__name__}|{conditions}.",
|
369
|
+
source_function="database.delete_entity"
|
370
|
+
)
|
371
|
+
|
372
|
+
entity.is_deleted = True
|
373
|
+
entity.deleted_at = datetime.now()
|
374
|
+
|
375
|
+
await session.flush()
|
376
|
+
await session.commit()
|
377
|
+
await session.refresh(entity)
|
378
|
+
|
379
|
+
except SQLAlchemyError as e:
|
380
|
+
raise CustomException(
|
381
|
+
ErrorCode.DB_DELETE_ERROR,
|
382
|
+
detail=f"{model.__name__}|{conditions}",
|
383
|
+
source_function="database.delete_entity",
|
384
|
+
original_error=e
|
385
|
+
)
|
386
|
+
|
387
|
+
async def purge_entity(
|
388
|
+
session: AsyncSession,
|
389
|
+
model: Type[ModelType],
|
390
|
+
entity: ModelType
|
391
|
+
) -> None:
|
392
|
+
# 엔티티를 영구 삭제합니다.
|
393
|
+
await session.delete(entity)
|
394
|
+
await session.commit()
|
395
|
+
|
186
396
|
async def list_entities(
|
187
397
|
session: AsyncSession,
|
188
398
|
model: Type[ModelType],
|
189
399
|
skip: int = 0,
|
190
400
|
limit: int = 100,
|
191
401
|
filters: Optional[Dict[str, Any]] = None,
|
192
|
-
|
402
|
+
explicit_joins: Optional[List[Any]] = None,
|
403
|
+
loading_joins: Optional[List[Any]] = None
|
193
404
|
) -> List[Dict[str, Any]]:
|
194
405
|
"""
|
195
406
|
엔터티 리스트를 필터 및 조건에 따라 가져오는 함수.
|
@@ -220,20 +431,26 @@ async def list_entities(
|
|
220
431
|
try:
|
221
432
|
query = select(model)
|
222
433
|
|
434
|
+
# 명시적 조인 적용
|
435
|
+
if explicit_joins:
|
436
|
+
for join_target in explicit_joins:
|
437
|
+
query = query.join(join_target) # 명시적으로 정의된 조인 추가
|
438
|
+
|
439
|
+
# 조인 로딩 적용
|
440
|
+
if loading_joins:
|
441
|
+
for join_option in loading_joins:
|
442
|
+
query = query.options(join_option)
|
443
|
+
|
223
444
|
# 필터 조건 적용
|
224
445
|
if filters:
|
225
446
|
conditions = build_conditions(filters, model)
|
226
447
|
query = query.where(and_(*conditions))
|
227
448
|
|
228
|
-
# 조인 로딩 적용
|
229
|
-
if joins:
|
230
|
-
for join_option in joins:
|
231
|
-
query = query.options(join_option)
|
232
|
-
|
233
449
|
# 페이지네이션 적용
|
234
450
|
query = query.limit(limit).offset(skip)
|
235
451
|
|
236
452
|
result = await session.execute(query)
|
453
|
+
|
237
454
|
return result.scalars().unique().all()
|
238
455
|
except SQLAlchemyError as e:
|
239
456
|
raise CustomException(
|
@@ -242,3 +459,125 @@ async def list_entities(
|
|
242
459
|
source_function="database.list_entities",
|
243
460
|
original_error=e
|
244
461
|
)
|
462
|
+
|
463
|
+
async def get_entity(
|
464
|
+
session: AsyncSession,
|
465
|
+
model: Type[ModelType],
|
466
|
+
conditions: Dict[str, Any],
|
467
|
+
explicit_joins: Optional[List[Any]] = None,
|
468
|
+
loading_joins: Optional[List[Any]] = None
|
469
|
+
) -> ModelType:
|
470
|
+
try:
|
471
|
+
query = select(model)
|
472
|
+
|
473
|
+
if explicit_joins:
|
474
|
+
for join_target in explicit_joins:
|
475
|
+
query = query.join(join_target)
|
476
|
+
|
477
|
+
if loading_joins:
|
478
|
+
for join_option in loading_joins:
|
479
|
+
query = query.options(join_option)
|
480
|
+
|
481
|
+
if conditions:
|
482
|
+
for key, value in conditions.items():
|
483
|
+
query = query.where(getattr(model, key) == value)
|
484
|
+
|
485
|
+
result = await session.execute(query)
|
486
|
+
return result.scalars().unique().one_or_none()
|
487
|
+
|
488
|
+
except SQLAlchemyError as e:
|
489
|
+
raise CustomException(
|
490
|
+
ErrorCode.DB_READ_ERROR,
|
491
|
+
detail=str(e),
|
492
|
+
source_function="database.get_entity",
|
493
|
+
original_error=str(e)
|
494
|
+
)
|
495
|
+
|
496
|
+
##################
|
497
|
+
# 로그 등 #
|
498
|
+
##################
|
499
|
+
async def log_create(
|
500
|
+
session: AsyncSession,
|
501
|
+
table_name: str,
|
502
|
+
log_data: Dict[str, Any],
|
503
|
+
request: Request = None
|
504
|
+
) -> None:
|
505
|
+
try:
|
506
|
+
# ULID 생성
|
507
|
+
if "ulid" not in log_data:
|
508
|
+
log_data["ulid"] = ULID()
|
509
|
+
|
510
|
+
# 사용자 에이전트 및 IP 주소 추가
|
511
|
+
if request:
|
512
|
+
log_data["user_agent"] = request.headers.get("user-agent")
|
513
|
+
log_data["ip_address"] = request.headers.get("x-forwarded-for") or request.client.host
|
514
|
+
|
515
|
+
# 동적으로 테이블 로드
|
516
|
+
metadata = MetaData(bind=session.bind)
|
517
|
+
table = Table(table_name, metadata, autoload_with=session.bind)
|
518
|
+
|
519
|
+
# 데이터 삽입
|
520
|
+
insert_stmt = insert(table).values(log_data)
|
521
|
+
await session.execute(insert_stmt)
|
522
|
+
await session.commit()
|
523
|
+
|
524
|
+
except Exception as e:
|
525
|
+
raise CustomException(
|
526
|
+
ErrorCode.INTERNAL_ERROR,
|
527
|
+
detail=f"{model.__name__}|{str(e)}",
|
528
|
+
source_function="database.log_create",
|
529
|
+
original_error=e
|
530
|
+
)
|
531
|
+
|
532
|
+
######################
|
533
|
+
# 검증 #
|
534
|
+
######################
|
535
|
+
async def validate_unique_fields(
|
536
|
+
session: AsyncSession,
|
537
|
+
unique_check: List[Dict[str, Any]] | None = None,
|
538
|
+
find_value: bool = True # True: 값이 있는지, False: 값이 없는지 확인
|
539
|
+
) -> None:
|
540
|
+
try:
|
541
|
+
for check in unique_check:
|
542
|
+
value = check["value"]
|
543
|
+
model = check["model"]
|
544
|
+
fields = check["fields"]
|
545
|
+
|
546
|
+
# 여러 개의 컬럼이 있을 경우 모든 조건을 만족해야 한다
|
547
|
+
conditions = [getattr(model, column) == value for column in fields]
|
548
|
+
|
549
|
+
# 쿼리 실행
|
550
|
+
query = select(model).where(or_(*conditions))
|
551
|
+
result = await session.execute(query)
|
552
|
+
existing = result.scalar_one_or_none()
|
553
|
+
|
554
|
+
# 값이 있는지 확인 (find_value=True) 또는 값이 없는지 확인 (find_value=False)
|
555
|
+
if find_value and existing: # 값이 존재하는 경우
|
556
|
+
raise CustomException(
|
557
|
+
ErrorCode.FIELD_INVALID_UNIQUE,
|
558
|
+
detail=f"{model.name}|{value}",
|
559
|
+
source_function="database.validate_unique_fields.existing"
|
560
|
+
)
|
561
|
+
elif not find_value and not existing: # 값이 존재하지 않는 경우
|
562
|
+
raise CustomException(
|
563
|
+
ErrorCode.FIELD_INVALID_NOT_EXIST,
|
564
|
+
detail=f"{model.name}|{value}",
|
565
|
+
source_function="database.validate_unique_fields.not_existing"
|
566
|
+
)
|
567
|
+
|
568
|
+
except CustomException as e:
|
569
|
+
# 특정 CustomException 처리
|
570
|
+
raise CustomException(
|
571
|
+
e.error_code,
|
572
|
+
detail=str(e),
|
573
|
+
source_function="database.validate_unique_fields.Exception",
|
574
|
+
original_error=e
|
575
|
+
)
|
576
|
+
except Exception as e:
|
577
|
+
# 알 수 없는 예외 처리
|
578
|
+
raise CustomException(
|
579
|
+
ErrorCode.INTERNAL_ERROR,
|
580
|
+
detail=f"Unexpected error: {str(e)}",
|
581
|
+
source_function="database.validate_unique_fields.Exception",
|
582
|
+
original_error=e
|
583
|
+
)
|
aiteamutils/exceptions.py
CHANGED
@@ -64,12 +64,16 @@ class ErrorCode(Enum):
|
|
64
64
|
VALIDATION_ERROR = ErrorResponse(4001, "VALIDATION_ERROR", 422, "유효성 검사 오류")
|
65
65
|
FIELD_INVALID_FORMAT = ErrorResponse(4002, "FIELD_INVALID_FORMAT", 400, "잘못된 형식입니다")
|
66
66
|
REQUIRED_FIELD_MISSING = ErrorResponse(4003, "VALIDATION_REQUIRED_FIELD_MISSING", 400, "필수 필드가 누락되었습니다")
|
67
|
+
FIELD_INVALID_UNIQUE = ErrorResponse(4004, "VALIDATION_FIELD_INVALID_UNIQUE", 400, "중복된 값이 존재합니다")
|
68
|
+
FIELD_INVALID_NOT_EXIST = ErrorResponse(4005, "VALIDATION_FIELD_INVALID_NOT_EXIST", 400, "존재하지 않는 값입니다")
|
67
69
|
|
68
70
|
# General 에러: 5000번대
|
69
71
|
NOT_FOUND = ErrorResponse(5001, "GENERAL_NOT_FOUND", 404, "리소스를 찾을 수 없습니다")
|
70
72
|
INTERNAL_ERROR = ErrorResponse(5002, "GENERAL_INTERNAL_ERROR", 500, "내부 서버 오류")
|
71
73
|
SERVICE_UNAVAILABLE = ErrorResponse(5003, "GENERAL_SERVICE_UNAVAILABLE", 503, "서비스를 사용할 수 없습니다")
|
72
74
|
SERVICE_NOT_REGISTERED = ErrorResponse(5003, "GENERAL_SERVICE_UNAVAILABLE", 503, "서비스를 사용할 수 없습니다")
|
75
|
+
LOGIN_ERROR = ErrorResponse(5004, "LOGIN_ERROR", 401, "로그인 오류")
|
76
|
+
TOKEN_ERROR = ErrorResponse(5005, "TOKEN_ERROR", 401, "토큰 오류")
|
73
77
|
|
74
78
|
class CustomException(Exception):
|
75
79
|
"""사용자 정의 예외 클래스"""
|
@@ -98,7 +102,7 @@ class CustomException(Exception):
|
|
98
102
|
|
99
103
|
self.error_chain.append({
|
100
104
|
"error_code": str(self.error_code),
|
101
|
-
"detail": str(self.detail),
|
105
|
+
"detail": str(self.detail) if self.detail else None,
|
102
106
|
"source_function": self.source_function,
|
103
107
|
"original_error": str(self.original_error) if self.original_error else None
|
104
108
|
})
|
@@ -115,7 +119,7 @@ class CustomException(Exception):
|
|
115
119
|
"""에러 정보를 딕셔너리로 반환합니다."""
|
116
120
|
return {
|
117
121
|
"error_code": self.error_code.name,
|
118
|
-
"detail": self.detail,
|
122
|
+
"detail": self.detail if self.detail else None,
|
119
123
|
"source_function": self.source_function,
|
120
124
|
"error_chain": self.error_chain,
|
121
125
|
"original_error": str(self.original_error) if self.original_error else None
|
aiteamutils/security.py
CHANGED
@@ -6,10 +6,11 @@ from functools import wraps
|
|
6
6
|
from jose import jwt, JWTError
|
7
7
|
from passlib.context import CryptContext
|
8
8
|
import logging
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
9
10
|
|
10
11
|
from .exceptions import CustomException, ErrorCode
|
11
12
|
from .enums import ActivityType
|
12
|
-
from .
|
13
|
+
from .database import log_create
|
13
14
|
|
14
15
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
15
16
|
|
@@ -115,7 +116,6 @@ def rate_limit(
|
|
115
116
|
except Exception as e:
|
116
117
|
raise CustomException(
|
117
118
|
ErrorCode.INTERNAL_ERROR,
|
118
|
-
detail=str(e),
|
119
119
|
source_function=func.__name__,
|
120
120
|
original_error=e
|
121
121
|
)
|
@@ -125,7 +125,6 @@ def rate_limit(
|
|
125
125
|
except Exception as e:
|
126
126
|
raise CustomException(
|
127
127
|
ErrorCode.INTERNAL_ERROR,
|
128
|
-
detail=str(e),
|
129
128
|
source_function="rate_limit",
|
130
129
|
original_error=e
|
131
130
|
)
|
@@ -136,20 +135,20 @@ def rate_limit(
|
|
136
135
|
async def create_jwt_token(
|
137
136
|
user_data: Dict[str, Any],
|
138
137
|
token_type: Literal["access", "refresh"],
|
139
|
-
|
140
|
-
|
138
|
+
db_session: AsyncSession,
|
139
|
+
token_settings: Dict[str, str],
|
140
|
+
request: Optional[Request] = None,
|
141
141
|
) -> str:
|
142
142
|
"""JWT 토큰을 생성하고 로그를 기록합니다."""
|
143
143
|
try:
|
144
|
-
|
145
|
-
|
144
|
+
# 토큰 데이터 구성
|
146
145
|
if token_type == "access":
|
147
|
-
expires_at = datetime.now(UTC) + timedelta(minutes=
|
146
|
+
expires_at = datetime.now(UTC) + timedelta(minutes=token_settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
148
147
|
token_data = {
|
149
148
|
# 등록 클레임
|
150
|
-
"iss":
|
149
|
+
"iss": token_settings.TOKEN_ISSUER,
|
151
150
|
"sub": user_data["username"],
|
152
|
-
"aud":
|
151
|
+
"aud": token_settings.TOKEN_AUDIENCE,
|
153
152
|
"exp": expires_at,
|
154
153
|
|
155
154
|
# 공개 클레임
|
@@ -172,22 +171,23 @@ async def create_jwt_token(
|
|
172
171
|
else: # refresh token
|
173
172
|
expires_at = datetime.now(UTC) + timedelta(days=14)
|
174
173
|
token_data = {
|
175
|
-
"iss":
|
174
|
+
"iss": token_settings.TOKEN_ISSUER,
|
176
175
|
"sub": user_data["username"],
|
177
176
|
"exp": expires_at,
|
178
177
|
"token_type": token_type,
|
179
178
|
"user_ulid": user_data["ulid"]
|
180
179
|
}
|
181
180
|
|
181
|
+
# JWT 토큰 생성
|
182
182
|
try:
|
183
183
|
token = jwt.encode(
|
184
184
|
token_data,
|
185
|
-
|
186
|
-
algorithm=
|
185
|
+
token_settings.JWT_SECRET,
|
186
|
+
algorithm=token_settings.JWT_ALGORITHM
|
187
187
|
)
|
188
188
|
except Exception as e:
|
189
189
|
raise CustomException(
|
190
|
-
ErrorCode.
|
190
|
+
ErrorCode.TOKEN_ERROR,
|
191
191
|
detail=f"token|{token_type}",
|
192
192
|
source_function="security.create_jwt_token",
|
193
193
|
original_error=e
|
@@ -195,14 +195,18 @@ async def create_jwt_token(
|
|
195
195
|
|
196
196
|
# 로그 생성
|
197
197
|
try:
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
206
210
|
)
|
207
211
|
except Exception as e:
|
208
212
|
# 로그 생성 실패는 토큰 생성에 영향을 주지 않음
|
@@ -217,9 +221,10 @@ async def create_jwt_token(
|
|
217
221
|
ErrorCode.INTERNAL_ERROR,
|
218
222
|
detail=str(e),
|
219
223
|
source_function="security.create_jwt_token",
|
220
|
-
original_error=e
|
224
|
+
original_error=str(e)
|
221
225
|
)
|
222
226
|
|
227
|
+
|
223
228
|
async def verify_jwt_token(
|
224
229
|
token: str,
|
225
230
|
expected_type: Optional[Literal["access", "refresh"]] = None
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.75"
|
@@ -0,0 +1,15 @@
|
|
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,,
|
@@ -1,15 +0,0 @@
|
|
1
|
-
aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
|
2
|
-
aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
|
3
|
-
aiteamutils/base_repository.py,sha256=tG_xz4hHYAN3-wkrLvEPxyTucV4pzT6dihoKVJp2JIc,2079
|
4
|
-
aiteamutils/base_service.py,sha256=CfNBAgFREqOC1791ex2plKLZiSZ1U5el4GvvkVsN4qU,2901
|
5
|
-
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
|
-
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
|
-
aiteamutils/database.py,sha256=CbX7eNFwqz9O4ywVQMLlLrb6hDUJPtsgGg_Hgef-p2I,8126
|
8
|
-
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
9
|
-
aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
|
10
|
-
aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
|
11
|
-
aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
|
12
|
-
aiteamutils/version.py,sha256=9RIZRehNxp6dA61_1-bTUirXc1q9QDosp-QZ_woFu0g,42
|
13
|
-
aiteamutils-0.2.72.dist-info/METADATA,sha256=WsFA3r2H1XUIsDXmhenz97uj3eT5jyZpN4CbNbps8rA,1718
|
14
|
-
aiteamutils-0.2.72.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
-
aiteamutils-0.2.72.dist-info/RECORD,,
|
File without changes
|