aiteamutils 0.2.64__py3-none-any.whl → 0.2.66__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_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
|