aiteamutils 0.2.65__tar.gz → 0.2.67__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/PKG-INFO +1 -1
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/aiteamutils/base_repository.py +22 -10
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/aiteamutils/base_service.py +14 -0
- aiteamutils-0.2.67/aiteamutils/database.py +244 -0
- aiteamutils-0.2.67/aiteamutils/version.py +2 -0
- aiteamutils-0.2.65/aiteamutils/database.py +0 -48
- aiteamutils-0.2.65/aiteamutils/version.py +0 -2
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/.cursorrules +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/.gitignore +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/README.md +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/aiteamutils/__init__.py +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/aiteamutils/base_model.py +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/aiteamutils/cache.py +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/aiteamutils/config.py +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/aiteamutils/enums.py +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/aiteamutils/exceptions.py +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/aiteamutils/security.py +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/aiteamutils/validators.py +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/pyproject.toml +0 -0
- {aiteamutils-0.2.65 → aiteamutils-0.2.67}/setup.py +0 -0
@@ -35,17 +35,29 @@ class BaseRepository(Generic[ModelType]):
|
|
35
35
|
skip: int = 0,
|
36
36
|
limit: int = 100,
|
37
37
|
filters: Optional[Dict[str, Any]] = None,
|
38
|
-
joins: Optional[List[Any]] = None
|
38
|
+
joins: Optional[List[Any]] = None,
|
39
39
|
) -> List[ModelType]:
|
40
40
|
"""
|
41
41
|
엔티티 목록 조회.
|
42
42
|
"""
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
43
|
+
try:
|
44
|
+
# 기본 CRUD 작업 호출
|
45
|
+
return await list_entities(
|
46
|
+
session=self.session,
|
47
|
+
model=self.model,
|
48
|
+
skip=skip,
|
49
|
+
limit=limit,
|
50
|
+
filters=filters,
|
51
|
+
joins=joins,
|
52
|
+
)
|
53
|
+
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
|
+
raise e
|
57
|
+
except Exception as e:
|
58
|
+
raise CustomException(
|
59
|
+
ErrorCode.INTERNAL_ERROR,
|
60
|
+
detail=str(e),
|
61
|
+
source_function=f"{self.__class__.__name__}.list",
|
62
|
+
original_error=e
|
63
|
+
)
|
@@ -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
|
|
@@ -37,6 +42,15 @@ class BaseService(Generic[ModelType]):
|
|
37
42
|
response_model: Any = None
|
38
43
|
) -> List[Dict[str, Any]]:
|
39
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
|
+
|
40
54
|
# 모델 이름을 통한 동적 처리
|
41
55
|
if model_name:
|
42
56
|
if model_name not in self.additional_models:
|
@@ -0,0 +1,244 @@
|
|
1
|
+
#기본 라이브러리
|
2
|
+
from typing import TypeVar, Generic, Type, Any, Dict, List, Optional
|
3
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
4
|
+
from sqlalchemy import select, and_
|
5
|
+
from sqlalchemy.orm import DeclarativeBase
|
6
|
+
from sqlalchemy.exc import SQLAlchemyError
|
7
|
+
from datetime import datetime
|
8
|
+
|
9
|
+
#패키지 라이브러리
|
10
|
+
from .exceptions import ErrorCode, CustomException
|
11
|
+
|
12
|
+
|
13
|
+
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
14
|
+
|
15
|
+
##################
|
16
|
+
# 전처리 #
|
17
|
+
##################
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
##################
|
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
|
+
# 쿼리 실행 #
|
185
|
+
##################
|
186
|
+
async def list_entities(
|
187
|
+
session: AsyncSession,
|
188
|
+
model: Type[ModelType],
|
189
|
+
skip: int = 0,
|
190
|
+
limit: int = 100,
|
191
|
+
filters: Optional[Dict[str, Any]] = None,
|
192
|
+
joins: Optional[List[Any]] = None
|
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
|
+
"""
|
220
|
+
try:
|
221
|
+
query = select(model)
|
222
|
+
|
223
|
+
# 필터 조건 적용
|
224
|
+
if filters:
|
225
|
+
conditions = build_conditions(filters, model)
|
226
|
+
query = query.where(and_(*conditions))
|
227
|
+
|
228
|
+
# 조인 로딩 적용
|
229
|
+
if joins:
|
230
|
+
for join_option in joins:
|
231
|
+
query = query.options(join_option)
|
232
|
+
|
233
|
+
# 페이지네이션 적용
|
234
|
+
query = query.limit(limit).offset(skip)
|
235
|
+
|
236
|
+
result = await session.execute(query)
|
237
|
+
return result.scalars().unique().all()
|
238
|
+
except SQLAlchemyError as e:
|
239
|
+
raise CustomException(
|
240
|
+
ErrorCode.DB_READ_ERROR,
|
241
|
+
detail=f"{model.__name__}|{str(e)}",
|
242
|
+
source_function="database.list_entities",
|
243
|
+
original_error=e
|
244
|
+
)
|
@@ -1,48 +0,0 @@
|
|
1
|
-
#기본 라이브러리
|
2
|
-
from typing import TypeVar, Generic, Type, Any, Dict, List, Optional
|
3
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
4
|
-
from sqlalchemy import select, and_
|
5
|
-
from sqlalchemy.orm import DeclarativeBase
|
6
|
-
from sqlalchemy.exc import SQLAlchemyError
|
7
|
-
|
8
|
-
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
9
|
-
|
10
|
-
#패키지 라이브러리
|
11
|
-
from .exceptions import ErrorCode, CustomException
|
12
|
-
|
13
|
-
##################
|
14
|
-
# 1. 쿼리 실행 #
|
15
|
-
##################
|
16
|
-
async def list_entities(
|
17
|
-
session: AsyncSession,
|
18
|
-
model: Type[ModelType],
|
19
|
-
skip: int = 0,
|
20
|
-
limit: int = 100,
|
21
|
-
filters: Optional[Dict[str, Any]] = None,
|
22
|
-
joins: Optional[List[Any]] = None
|
23
|
-
) -> List[Dict[str, Any]]:
|
24
|
-
try:
|
25
|
-
query = select(model)
|
26
|
-
|
27
|
-
# 필터 조건 적용
|
28
|
-
if filters:
|
29
|
-
conditions = [getattr(model, key) == value for key, value in filters.items()]
|
30
|
-
query = query.where(and_(*conditions))
|
31
|
-
|
32
|
-
# 조인 로딩 적용
|
33
|
-
if joins:
|
34
|
-
for join_option in joins:
|
35
|
-
query = query.options(join_option)
|
36
|
-
|
37
|
-
# 페이지네이션 적용
|
38
|
-
query = query.limit(limit).offset(skip)
|
39
|
-
|
40
|
-
result = await session.execute(query)
|
41
|
-
return result.scalars().unique().all()
|
42
|
-
except SQLAlchemyError as e:
|
43
|
-
raise CustomException(
|
44
|
-
ErrorCode.DB_READ_ERROR,
|
45
|
-
detail=f"{model.__name__}|{str(e)}",
|
46
|
-
source_function="database.list_entities",
|
47
|
-
original_error=e
|
48
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|