aiteamutils 0.2.64__py3-none-any.whl → 0.2.66__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- aiteamutils/base_repository.py +1 -17
- aiteamutils/base_service.py +19 -18
- aiteamutils/database.py +200 -4
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.64.dist-info → aiteamutils-0.2.66.dist-info}/METADATA +1 -1
- {aiteamutils-0.2.64.dist-info → aiteamutils-0.2.66.dist-info}/RECORD +7 -7
- {aiteamutils-0.2.64.dist-info → aiteamutils-0.2.66.dist-info}/WHEEL +0 -0
aiteamutils/base_repository.py
CHANGED
@@ -13,24 +13,8 @@ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
|
13
13
|
|
14
14
|
class BaseRepository(Generic[ModelType]):
|
15
15
|
def __init__(self, session: AsyncSession, model: Type[ModelType]):
|
16
|
-
if session is None:
|
17
|
-
raise CustomException(
|
18
|
-
ErrorCode.DB_CONNECTION_ERROR,
|
19
|
-
detail="Database session is not set",
|
20
|
-
source_function=f"{self.__class__.__name__}.session"
|
21
|
-
)
|
22
|
-
if model is None:
|
23
|
-
raise CustomException(
|
24
|
-
ErrorCode.DB_CONNECTION_ERROR,
|
25
|
-
detail="Model is not set",
|
26
|
-
source_function=f"{self.__class__.__name__}.model"
|
27
|
-
)
|
28
|
-
print(f"BaseRepository.__init__() called with session: {session} and model: {model}")
|
29
|
-
|
30
16
|
self._session = session
|
31
17
|
self.model = model
|
32
|
-
|
33
|
-
print(f"BaseRepository initialized with session: {session} and model: {model}")
|
34
18
|
|
35
19
|
@property
|
36
20
|
def session(self) -> AsyncSession:
|
@@ -51,7 +35,7 @@ class BaseRepository(Generic[ModelType]):
|
|
51
35
|
skip: int = 0,
|
52
36
|
limit: int = 100,
|
53
37
|
filters: Optional[Dict[str, Any]] = None,
|
54
|
-
joins: Optional[List[Any]] = None
|
38
|
+
joins: Optional[List[Any]] = None,
|
55
39
|
) -> List[ModelType]:
|
56
40
|
"""
|
57
41
|
엔티티 목록 조회.
|
aiteamutils/base_service.py
CHANGED
@@ -4,9 +4,14 @@ from typing import TypeVar, Generic, Type, Dict, Any, Union, List
|
|
4
4
|
from sqlalchemy.orm import DeclarativeBase
|
5
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
6
6
|
from datetime import datetime
|
7
|
+
|
7
8
|
#패키지 라이브러리
|
8
9
|
from .exceptions import ErrorCode, CustomException
|
9
10
|
from .base_repository import BaseRepository
|
11
|
+
from .database import (
|
12
|
+
process_response,
|
13
|
+
build_search_filters
|
14
|
+
)
|
10
15
|
|
11
16
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
12
17
|
|
@@ -18,28 +23,13 @@ class BaseService(Generic[ModelType]):
|
|
18
23
|
self,
|
19
24
|
model: Type[ModelType],
|
20
25
|
repository: BaseRepository[ModelType],
|
21
|
-
db_session: AsyncSession
|
26
|
+
db_session: AsyncSession,
|
27
|
+
additional_models: Dict[str, Type[DeclarativeBase]] = None,
|
22
28
|
):
|
23
|
-
if not repository:
|
24
|
-
raise CustomException(
|
25
|
-
ErrorCode.INVALID_REQUEST,
|
26
|
-
detail="Repository is not set",
|
27
|
-
source_function=f"{self.__class__.__name__}.__init__"
|
28
|
-
)
|
29
|
-
if not db_session:
|
30
|
-
raise CustomException(
|
31
|
-
ErrorCode.INVALID_REQUEST,
|
32
|
-
detail="Database session is not set",
|
33
|
-
source_function=f"{self.__class__.__name__}.__init__"
|
34
|
-
)
|
35
|
-
print(f"BaseService.__init__() called with repository: {repository} and db_session: {db_session}")
|
36
|
-
|
37
|
-
|
38
29
|
self.model = model
|
39
30
|
self.repository = repository
|
40
31
|
self.db_session = db_session
|
41
|
-
|
42
|
-
print(f"BaseService initialized with repository: {repository} and db_session: {db_session}")
|
32
|
+
self.additional_models = additional_models or {},
|
43
33
|
|
44
34
|
async def list(
|
45
35
|
self,
|
@@ -48,8 +38,19 @@ class BaseService(Generic[ModelType]):
|
|
48
38
|
filters: Dict[str, Any] | None = None,
|
49
39
|
search_params: Dict[str, Any] | None = None,
|
50
40
|
model_name: str | None = None,
|
41
|
+
request: Request | None = None,
|
42
|
+
response_model: Any = None
|
51
43
|
) -> List[Dict[str, Any]]:
|
52
44
|
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
|
+
|
53
54
|
# 모델 이름을 통한 동적 처리
|
54
55
|
if model_name:
|
55
56
|
if model_name not in self.additional_models:
|
aiteamutils/database.py
CHANGED
@@ -4,14 +4,184 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
4
4
|
from sqlalchemy import select, and_
|
5
5
|
from sqlalchemy.orm import DeclarativeBase
|
6
6
|
from sqlalchemy.exc import SQLAlchemyError
|
7
|
-
|
8
|
-
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
7
|
+
from datetime import datetime
|
9
8
|
|
10
9
|
#패키지 라이브러리
|
11
10
|
from .exceptions import ErrorCode, CustomException
|
12
11
|
|
12
|
+
|
13
|
+
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
14
|
+
|
15
|
+
##################
|
16
|
+
# 전처리 #
|
17
|
+
##################
|
18
|
+
|
19
|
+
|
20
|
+
|
13
21
|
##################
|
14
|
-
#
|
22
|
+
# 응답 처리 #
|
23
|
+
##################
|
24
|
+
def process_columns(
|
25
|
+
entity: ModelType,
|
26
|
+
exclude_extra_data: bool = True
|
27
|
+
) -> Dict[str, Any]:
|
28
|
+
"""엔티티의 컬럼들을 처리합니다.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
entity (ModelType): 처리할 엔티티
|
32
|
+
exclude_extra_data (bool, optional): extra_data 컬럼 제외 여부. Defaults to True.
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
Dict[str, Any]: 처리된 컬럼 데이터
|
36
|
+
"""
|
37
|
+
result = {}
|
38
|
+
for column in entity.__table__.columns:
|
39
|
+
if exclude_extra_data and column.name == 'extra_data':
|
40
|
+
continue
|
41
|
+
|
42
|
+
# 필드 값 처리
|
43
|
+
if hasattr(entity, column.name):
|
44
|
+
value = getattr(entity, column.name)
|
45
|
+
if isinstance(value, datetime):
|
46
|
+
value = value.isoformat()
|
47
|
+
result[column.name] = value
|
48
|
+
elif hasattr(entity, 'extra_data') and isinstance(entity.extra_data, dict):
|
49
|
+
result[column.name] = entity.extra_data.get(column.name)
|
50
|
+
else:
|
51
|
+
result[column.name] = None
|
52
|
+
|
53
|
+
# extra_data의 내용을 최상위 레벨로 업데이트
|
54
|
+
if hasattr(entity, 'extra_data') and isinstance(entity.extra_data, dict):
|
55
|
+
result.update(entity.extra_data or {})
|
56
|
+
|
57
|
+
return result
|
58
|
+
|
59
|
+
def process_response(
|
60
|
+
entity: ModelType,
|
61
|
+
response_model: Any = None
|
62
|
+
) -> Dict[str, Any]:
|
63
|
+
"""응답 데이터를 처리합니다.
|
64
|
+
extra_data의 내용을 최상위 레벨로 변환하고, 라우터에서 선언한 응답 스키마에 맞게 데이터를 변환합니다.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
entity (ModelType): 처리할 엔티티
|
68
|
+
response_model (Any, optional): 응답 스키마. Defaults to None.
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
Dict[str, Any]: 처리된 엔티티 데이터
|
72
|
+
"""
|
73
|
+
if not entity:
|
74
|
+
return None
|
75
|
+
|
76
|
+
# 모든 필드 처리
|
77
|
+
result = process_columns(entity)
|
78
|
+
|
79
|
+
# Relationship 처리 (이미 로드된 관계만 처리)
|
80
|
+
for relationship in entity.__mapper__.relationships:
|
81
|
+
if not relationship.key in entity.__dict__:
|
82
|
+
continue
|
83
|
+
|
84
|
+
try:
|
85
|
+
value = getattr(entity, relationship.key)
|
86
|
+
# response_model이 있는 경우 해당 필드의 annotation type을 가져옴
|
87
|
+
nested_response_model = None
|
88
|
+
if response_model and relationship.key in response_model.model_fields:
|
89
|
+
field_info = response_model.model_fields[relationship.key]
|
90
|
+
nested_response_model = field_info.annotation
|
91
|
+
|
92
|
+
if value is not None:
|
93
|
+
if isinstance(value, list):
|
94
|
+
result[relationship.key] = [
|
95
|
+
process_response(item, nested_response_model)
|
96
|
+
for item in value
|
97
|
+
]
|
98
|
+
else:
|
99
|
+
result[relationship.key] = process_response(value, nested_response_model)
|
100
|
+
else:
|
101
|
+
result[relationship.key] = None
|
102
|
+
except Exception:
|
103
|
+
result[relationship.key] = None
|
104
|
+
|
105
|
+
# response_model이 있는 경우 필터링
|
106
|
+
if response_model:
|
107
|
+
# 현재 키 목록을 저장
|
108
|
+
current_keys = list(result.keys())
|
109
|
+
# response_model에 없는 키 제거
|
110
|
+
for key in current_keys:
|
111
|
+
if key not in response_model.model_fields:
|
112
|
+
result.pop(key)
|
113
|
+
# 모델 검증 및 업데이트
|
114
|
+
result.update(response_model(**result).model_dump())
|
115
|
+
|
116
|
+
return result
|
117
|
+
|
118
|
+
|
119
|
+
##################
|
120
|
+
# 조건 처리 #
|
121
|
+
##################
|
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
|
+
def build_conditions(
|
147
|
+
filters: Dict[str, Any],
|
148
|
+
model: Type[ModelType]
|
149
|
+
) -> List[Any]:
|
150
|
+
"""
|
151
|
+
필터 조건을 기반으로 SQLAlchemy 조건 리스트를 생성합니다.
|
152
|
+
|
153
|
+
Args:
|
154
|
+
filters: 필터 조건 딕셔너리.
|
155
|
+
model: SQLAlchemy 모델 클래스.
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
List[Any]: SQLAlchemy 조건 리스트.
|
159
|
+
"""
|
160
|
+
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
|
+
|
172
|
+
# 조건 생성
|
173
|
+
operator = filter_data["operator"]
|
174
|
+
value = filter_data["value"]
|
175
|
+
|
176
|
+
if operator == "like":
|
177
|
+
conditions.append(field.ilike(f"%{value}%"))
|
178
|
+
elif operator == "eq":
|
179
|
+
conditions.append(field == value)
|
180
|
+
|
181
|
+
return conditions
|
182
|
+
|
183
|
+
##################
|
184
|
+
# 쿼리 실행 #
|
15
185
|
##################
|
16
186
|
async def list_entities(
|
17
187
|
session: AsyncSession,
|
@@ -21,12 +191,38 @@ async def list_entities(
|
|
21
191
|
filters: Optional[Dict[str, Any]] = None,
|
22
192
|
joins: Optional[List[Any]] = None
|
23
193
|
) -> List[Dict[str, Any]]:
|
194
|
+
"""
|
195
|
+
엔터티 리스트를 필터 및 조건에 따라 가져오는 함수.
|
196
|
+
|
197
|
+
Args:
|
198
|
+
session: SQLAlchemy AsyncSession.
|
199
|
+
model: SQLAlchemy 모델.
|
200
|
+
skip: 페이지네이션 시작 위치.
|
201
|
+
limit: 페이지네이션 크기.
|
202
|
+
filters: 필터 조건 딕셔너리.
|
203
|
+
예시:
|
204
|
+
filters = {
|
205
|
+
"search": {"field": "username", "operator": "like", "value": "%admin%"},
|
206
|
+
"name": {"field": "name", "operator": "like", "value": "%John%"},
|
207
|
+
"role_ulid": {"field": "role_ulid", "operator": "eq", "value": "1234"}
|
208
|
+
}
|
209
|
+
|
210
|
+
joins: 조인 옵션.
|
211
|
+
예시:
|
212
|
+
joins = [
|
213
|
+
selectinload(YourModel.related_field), # 관련된 필드를 함께 로드
|
214
|
+
joinedload(YourModel.another_related_field) # 다른 관계된 필드를 조인
|
215
|
+
]
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
List[Dict[str, Any]]: 쿼리 결과 리스트.
|
219
|
+
"""
|
24
220
|
try:
|
25
221
|
query = select(model)
|
26
222
|
|
27
223
|
# 필터 조건 적용
|
28
224
|
if filters:
|
29
|
-
conditions =
|
225
|
+
conditions = build_conditions(filters, model)
|
30
226
|
query = query.where(and_(*conditions))
|
31
227
|
|
32
228
|
# 조인 로딩 적용
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.66"
|
@@ -1,15 +1,15 @@
|
|
1
1
|
aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
|
2
2
|
aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
|
3
|
-
aiteamutils/base_repository.py,sha256=
|
4
|
-
aiteamutils/base_service.py,sha256=
|
3
|
+
aiteamutils/base_repository.py,sha256=Vbz2_BGT6gSf8fV2Ph4JWalrsNCDZSjLbOdRwCnz3I4,1540
|
4
|
+
aiteamutils/base_service.py,sha256=CfNBAgFREqOC1791ex2plKLZiSZ1U5el4GvvkVsN4qU,2901
|
5
5
|
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
6
|
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
|
-
aiteamutils/database.py,sha256=
|
7
|
+
aiteamutils/database.py,sha256=CbX7eNFwqz9O4ywVQMLlLrb6hDUJPtsgGg_Hgef-p2I,8126
|
8
8
|
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
9
9
|
aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
|
10
10
|
aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
|
11
11
|
aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
|
12
|
-
aiteamutils/version.py,sha256=
|
13
|
-
aiteamutils-0.2.
|
14
|
-
aiteamutils-0.2.
|
15
|
-
aiteamutils-0.2.
|
12
|
+
aiteamutils/version.py,sha256=sLTec9HL2qseXAi6mzbn7T-ZwoQOncK22kiRkfVSwH8,42
|
13
|
+
aiteamutils-0.2.66.dist-info/METADATA,sha256=SkG0OhfTLO7TiWa1LtvRjPnBNYPqULCb8sb6PllJ21s,1718
|
14
|
+
aiteamutils-0.2.66.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
+
aiteamutils-0.2.66.dist-info/RECORD,,
|
File without changes
|