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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.65
3
+ Version: 0.2.67
4
4
  Summary: AI Team Utilities
5
5
  Project-URL: Homepage, https://github.com/yourusername/aiteamutils
6
6
  Project-URL: Issues, https://github.com/yourusername/aiteamutils/issues
@@ -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
- # 기본 CRUD 작업 호출
44
- return await list_entities(
45
- session=self.session,
46
- model=self.model,
47
- skip=skip,
48
- limit=limit,
49
- filters=filters,
50
- joins=joins,
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
+ )
@@ -0,0 +1,2 @@
1
+ """버전 정보"""
2
+ __version__ = "0.2.67"
@@ -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
- )
@@ -1,2 +0,0 @@
1
- """버전 정보"""
2
- __version__ = "0.2.65"
File without changes
File without changes
File without changes
File without changes