aiteamutils 0.2.65__tar.gz → 0.2.67__tar.gz

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.
@@ -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