aiteamutils 0.2.58__tar.gz → 0.2.59__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.58
3
+ Version: 0.2.59
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
@@ -0,0 +1,260 @@
1
+ """기본 서비스 모듈."""
2
+ from datetime import datetime
3
+ from typing import TypeVar, Generic, Dict, Any, List, Optional, Type
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+ from sqlalchemy.orm import DeclarativeBase
6
+ from fastapi import Request
7
+
8
+ from .database import DatabaseService
9
+ from .exceptions import CustomException, ErrorCode
10
+ from .base_repository import BaseRepository
11
+
12
+ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
13
+
14
+ class BaseService(Generic[ModelType]):
15
+ def __init__(
16
+ self,
17
+ db: DatabaseService,
18
+ repository: Optional[BaseRepository] = None,
19
+ request: Optional[Request] = None
20
+ ):
21
+ """BaseService 초기화
22
+
23
+ Args:
24
+ db: 데이터베이스 서비스
25
+ repository: 레포지토리 인스턴스 (선택)
26
+ request: FastAPI 요청 객체 (선택)
27
+ """
28
+ self.db = db
29
+ self.repository = repository
30
+ self.request = request
31
+ self.model = repository.model if repository else None
32
+
33
+ def _process_response(self, entity: ModelType, response_model: Any = None) -> Dict[str, Any]:
34
+ """응답 데이터를 처리합니다.
35
+
36
+ Args:
37
+ entity: 처리할 엔티티
38
+ response_model: 응답 모델 클래스
39
+
40
+ Returns:
41
+ 처리된 데이터
42
+ """
43
+ if not entity:
44
+ return None
45
+
46
+ # 기본 데이터 변환
47
+ result = {}
48
+
49
+ # 테이블 컬럼 처리
50
+ for column in entity.__table__.columns:
51
+ value = getattr(entity, column.name)
52
+ if isinstance(value, datetime):
53
+ value = value.isoformat()
54
+ result[column.name] = value
55
+
56
+ # Relationship 처리 (이미 로드된 관계만)
57
+ for relationship in entity.__mapper__.relationships:
58
+ if relationship.key not in entity.__dict__:
59
+ continue
60
+
61
+ try:
62
+ value = getattr(entity, relationship.key)
63
+ if value is not None:
64
+ if isinstance(value, list):
65
+ result[relationship.key] = [
66
+ self._process_response(item)
67
+ for item in value
68
+ ]
69
+ else:
70
+ result[relationship.key] = self._process_response(value)
71
+ else:
72
+ result[relationship.key] = None
73
+ except Exception:
74
+ result[relationship.key] = None
75
+
76
+ # response_model이 있는 경우 필터링
77
+ if response_model:
78
+ # response_model에 없는 필드 제거
79
+ keys_to_remove = [key for key in result if key not in response_model.model_fields]
80
+ for key in keys_to_remove:
81
+ result.pop(key)
82
+ # 모델 검증
83
+ return response_model(**result).model_dump()
84
+
85
+ return result
86
+
87
+ async def create(
88
+ self,
89
+ data: Dict[str, Any],
90
+ response_model: Any = None
91
+ ) -> Dict[str, Any]:
92
+ """엔티티를 생성합니다.
93
+
94
+ Args:
95
+ data: 생성할 데이터
96
+ response_model: 응답 모델 클래스
97
+
98
+ Returns:
99
+ 생성된 엔티티
100
+
101
+ Raises:
102
+ CustomException: 생성 실패 시
103
+ """
104
+ try:
105
+ entity = await self.db.create_entity(self.model, data)
106
+ return self._process_response(entity, response_model)
107
+ except CustomException as e:
108
+ raise e
109
+ except Exception as e:
110
+ raise CustomException(
111
+ ErrorCode.DB_CREATE_ERROR,
112
+ detail=str(e),
113
+ source_function=f"{self.__class__.__name__}.create",
114
+ original_error=e
115
+ )
116
+
117
+ async def get(
118
+ self,
119
+ ulid: str,
120
+ response_model: Any = None
121
+ ) -> Dict[str, Any]:
122
+ """엔티티를 조회합니다.
123
+
124
+ Args:
125
+ ulid: 조회할 엔티티의 ULID
126
+ response_model: 응답 모델 클래스
127
+
128
+ Returns:
129
+ 조회된 엔티티
130
+
131
+ Raises:
132
+ CustomException: 조회 실패 시
133
+ """
134
+ try:
135
+ entity = await self.db.get_entity(self.model, {"ulid": ulid, "is_deleted": False})
136
+ if not entity:
137
+ raise CustomException(
138
+ ErrorCode.NOT_FOUND,
139
+ detail=f"{self.model.__tablename__}|ulid|{ulid}",
140
+ source_function=f"{self.__class__.__name__}.get"
141
+ )
142
+ return self._process_response(entity, response_model)
143
+ except CustomException as e:
144
+ raise e
145
+ except Exception as e:
146
+ raise CustomException(
147
+ ErrorCode.DB_QUERY_ERROR,
148
+ detail=str(e),
149
+ source_function=f"{self.__class__.__name__}.get",
150
+ original_error=e
151
+ )
152
+
153
+ async def list(
154
+ self,
155
+ skip: int = 0,
156
+ limit: int = 100,
157
+ filters: Optional[Dict[str, Any]] = None,
158
+ response_model: Any = None
159
+ ) -> List[Dict[str, Any]]:
160
+ """엔티티 목록을 조회합니다.
161
+
162
+ Args:
163
+ skip: 건너뛸 레코드 수
164
+ limit: 조회할 최대 레코드 수
165
+ filters: 필터 조건
166
+ response_model: 응답 모델 클래스
167
+
168
+ Returns:
169
+ 엔티티 목록
170
+
171
+ Raises:
172
+ CustomException: 조회 실패 시
173
+ """
174
+ try:
175
+ entities = await self.db.list_entities(
176
+ self.model,
177
+ filters=filters,
178
+ skip=skip,
179
+ limit=limit
180
+ )
181
+ return [self._process_response(entity, response_model) for entity in entities]
182
+ except CustomException as e:
183
+ raise e
184
+ except Exception as e:
185
+ raise CustomException(
186
+ ErrorCode.DB_QUERY_ERROR,
187
+ detail=str(e),
188
+ source_function=f"{self.__class__.__name__}.list",
189
+ original_error=e
190
+ )
191
+
192
+ async def update(
193
+ self,
194
+ ulid: str,
195
+ data: Dict[str, Any],
196
+ response_model: Any = None
197
+ ) -> Dict[str, Any]:
198
+ """엔티티를 수정합니다.
199
+
200
+ Args:
201
+ ulid: 수정할 엔티티의 ULID
202
+ data: 수정할 데이터
203
+ response_model: 응답 모델 클래스
204
+
205
+ Returns:
206
+ 수정된 엔티티
207
+
208
+ Raises:
209
+ CustomException: 수정 실패 시
210
+ """
211
+ try:
212
+ entity = await self.db.get_entity(self.model, {"ulid": ulid, "is_deleted": False})
213
+ if not entity:
214
+ raise CustomException(
215
+ ErrorCode.NOT_FOUND,
216
+ detail=f"{self.model.__tablename__}|ulid|{ulid}",
217
+ source_function=f"{self.__class__.__name__}.update"
218
+ )
219
+ updated = await self.db.update_entity(entity, data)
220
+ return self._process_response(updated, response_model)
221
+ except CustomException as e:
222
+ raise e
223
+ except Exception as e:
224
+ raise CustomException(
225
+ ErrorCode.DB_UPDATE_ERROR,
226
+ detail=str(e),
227
+ source_function=f"{self.__class__.__name__}.update",
228
+ original_error=e
229
+ )
230
+
231
+ async def delete(self, ulid: str) -> bool:
232
+ """엔티티를 삭제합니다.
233
+
234
+ Args:
235
+ ulid: 삭제할 엔티티의 ULID
236
+
237
+ Returns:
238
+ 삭제 성공 여부
239
+
240
+ Raises:
241
+ CustomException: 삭제 실패 시
242
+ """
243
+ try:
244
+ entity = await self.db.get_entity(self.model, {"ulid": ulid, "is_deleted": False})
245
+ if not entity:
246
+ raise CustomException(
247
+ ErrorCode.NOT_FOUND,
248
+ detail=f"{self.model.__tablename__}|ulid|{ulid}",
249
+ source_function=f"{self.__class__.__name__}.delete"
250
+ )
251
+ return await self.db.delete_entity(entity)
252
+ except CustomException as e:
253
+ raise e
254
+ except Exception as e:
255
+ raise CustomException(
256
+ ErrorCode.DB_DELETE_ERROR,
257
+ detail=str(e),
258
+ source_function=f"{self.__class__.__name__}.delete",
259
+ original_error=e
260
+ )
@@ -1,12 +1,9 @@
1
1
  """의존성 관리 모듈."""
2
2
  from typing import Type, TypeVar, Dict, Any, Optional, Callable, List, AsyncGenerator
3
- from fastapi import Request, Depends, HTTPException, status
3
+ from fastapi import Request, Depends
4
4
  from sqlalchemy.ext.asyncio import AsyncSession
5
- from jose import jwt, JWTError
6
- import logging
7
5
 
8
6
  from .exceptions import CustomException, ErrorCode
9
- from .config import get_settings
10
7
  from .base_service import BaseService
11
8
  from .base_repository import BaseRepository
12
9
  from .database import DatabaseService
@@ -20,8 +17,7 @@ _session_provider = None
20
17
  __all__ = [
21
18
  "setup_dependencies",
22
19
  "register_service",
23
- "get_service",
24
- "get_current_user"
20
+ "get_service"
25
21
  ]
26
22
 
27
23
  def setup_dependencies(session_provider: Callable[[], AsyncGenerator[AsyncSession, None]]) -> None:
@@ -139,89 +135,4 @@ def get_service(service_name: str) -> Callable:
139
135
  session: AsyncSession = Depends(_session_provider)
140
136
  ) -> BaseService:
141
137
  return await _get_service(service_name, session, request)
142
- return _get_service_dependency
143
-
144
- async def get_current_user(
145
- request: Request,
146
- session: AsyncSession,
147
- auth_service: BaseService = Depends(get_service("AuthService"))
148
- ) -> Dict[str, Any]:
149
- """현재 사용자 정보를 반환합니다.
150
-
151
- Args:
152
- request: FastAPI 요청 객체
153
- session: 데이터베이스 세션
154
- auth_service: 인증 서비스
155
-
156
- Returns:
157
- Dict[str, Any]: 사용자 정보
158
-
159
- Raises:
160
- HTTPException: 인증 실패 시
161
- """
162
- settings = get_settings()
163
-
164
- # Authorization 헤더 검증
165
- authorization = request.headers.get("Authorization")
166
- if not authorization or not authorization.startswith("Bearer "):
167
- raise HTTPException(
168
- status_code=status.HTTP_401_UNAUTHORIZED,
169
- detail="Not authenticated",
170
- headers={"WWW-Authenticate": "Bearer"}
171
- )
172
-
173
- token = authorization.split(" ")[1]
174
-
175
- try:
176
- # JWT 토큰 디코딩
177
- payload = jwt.decode(
178
- token,
179
- settings.jwt_secret,
180
- algorithms=[settings.jwt_algorithm],
181
- issuer=settings.token_issuer,
182
- audience=settings.token_audience
183
- )
184
-
185
- # 토큰 타입 검증
186
- token_type = payload.get("token_type")
187
- if token_type != "access":
188
- raise HTTPException(
189
- status_code=status.HTTP_401_UNAUTHORIZED,
190
- detail="Invalid token type",
191
- headers={"WWW-Authenticate": "Bearer"}
192
- )
193
-
194
- # 사용자 조회
195
- user_ulid = payload.get("user_ulid")
196
- if not user_ulid:
197
- raise HTTPException(
198
- status_code=status.HTTP_401_UNAUTHORIZED,
199
- detail="Invalid token payload",
200
- headers={"WWW-Authenticate": "Bearer"}
201
- )
202
-
203
- user = await auth_service.get_by_ulid(user_ulid)
204
- if not user:
205
- raise HTTPException(
206
- status_code=status.HTTP_401_UNAUTHORIZED,
207
- detail="User not found",
208
- headers={"WWW-Authenticate": "Bearer"}
209
- )
210
-
211
- return user
212
-
213
- except JWTError as e:
214
- raise HTTPException(
215
- status_code=status.HTTP_401_UNAUTHORIZED,
216
- detail=str(e),
217
- headers={"WWW-Authenticate": "Bearer"}
218
- )
219
- except HTTPException as e:
220
- raise e
221
- except Exception as e:
222
- logging.error(f"Error in get_current_user: {str(e)}")
223
- raise HTTPException(
224
- status_code=status.HTTP_401_UNAUTHORIZED,
225
- detail="Authentication failed",
226
- headers={"WWW-Authenticate": "Bearer"}
227
- )
138
+ return _get_service_dependency
@@ -0,0 +1,2 @@
1
+ """버전 정보"""
2
+ __version__ = "0.2.59"
@@ -1,671 +0,0 @@
1
- """기본 서비스 모듈."""
2
- from datetime import datetime
3
- from typing import TypeVar, Generic, Dict, Any, List, Optional, Type, Union
4
- from sqlalchemy.orm import DeclarativeBase, Load
5
- from sqlalchemy.exc import IntegrityError, SQLAlchemyError
6
- from .database import DatabaseService
7
- from .exceptions import CustomException, ErrorCode
8
- from .base_repository import BaseRepository
9
- from .security import hash_password
10
- from fastapi import Request
11
- from ulid import ULID
12
- from sqlalchemy import select
13
-
14
- ModelType = TypeVar("ModelType", bound=DeclarativeBase)
15
-
16
- class BaseService(Generic[ModelType]):
17
-
18
- ##################
19
- # 1. 초기화 영역 #
20
- ##################
21
- def __init__(
22
- self,
23
- repository: BaseRepository[ModelType],
24
- additional_models: Dict[str, Type[DeclarativeBase]] = None
25
- ):
26
- """
27
- Args:
28
- repository (BaseRepository[ModelType]): 레포지토리 인스턴스
29
- additional_models (Dict[str, Type[DeclarativeBase]], optional): 추가 모델 매핑. Defaults to None.
30
- """
31
- self.repository = repository
32
- self.model = repository.model
33
- self.additional_models = additional_models or {}
34
- self._session = None
35
- self.searchable_fields = {
36
- "name": {"type": "text", "description": "이름"},
37
- "organization_ulid": {"type": "exact", "description": "조직 ID"}
38
- }
39
-
40
- @property
41
- def session(self):
42
- """현재 세션을 반환합니다."""
43
- if self._session is None:
44
- raise CustomException(
45
- ErrorCode.DB_CONNECTION_ERROR,
46
- detail="Database session is not set",
47
- source_function=f"{self.__class__.__name__}.session"
48
- )
49
- return self._session
50
-
51
- @session.setter
52
- def session(self, value):
53
- """세션을 설정합니다."""
54
- self._session = value
55
- if hasattr(self.repository, 'session'):
56
- self.repository.session = value
57
-
58
- #########################
59
- # 2. 이벤트 처리 메서드 #
60
- #########################
61
- async def pre_save(self, data: Dict[str, Any]) -> Dict[str, Any]:
62
- """저장 전 처리를 수행합니다.
63
-
64
- Args:
65
- data (Dict[str, Any]): 저장할 데이터
66
-
67
- Returns:
68
- Dict[str, Any]: 처리된 데이터
69
- """
70
- return data
71
-
72
- async def post_save(self, entity: ModelType) -> None:
73
- """저장 후 처리를 수행합니다.
74
-
75
- Args:
76
- entity (ModelType): 저장된 엔티티
77
- """
78
- pass
79
-
80
- async def pre_delete(self, ulid: str) -> None:
81
- """삭제 전 처리를 수행합니다.
82
-
83
- Args:
84
- ulid (str): 삭제할 엔티티의 ULID
85
- """
86
- pass
87
-
88
- async def post_delete(self, ulid: str) -> None:
89
- """삭제 후 처리를 수행합니다.
90
-
91
- Args:
92
- ulid (str): 삭제된 엔티티의 ULID
93
- """
94
- pass
95
-
96
- ######################
97
- # 3. 캐시 관리 메서드 #
98
- ######################
99
- async def get_from_cache(self, key: str) -> Optional[Any]:
100
- """캐시에서 데이터를 조회합니다.
101
-
102
- Args:
103
- key (str): 캐시 키
104
-
105
- Returns:
106
- Optional[Any]: 캐시된 데이터 또는 None
107
- """
108
- return None
109
-
110
- async def set_to_cache(self, key: str, value: Any, ttl: int = 3600) -> None:
111
- """데이터를 캐시에 저장합니다.
112
-
113
- Args:
114
- key (str): 캐시 키
115
- value (Any): 저장할 값
116
- ttl (int, optional): 캐시 유효 시간(초). Defaults to 3600.
117
- """
118
- pass
119
-
120
- async def invalidate_cache(self, key: str) -> None:
121
- """캐시를 무효화합니다.
122
-
123
- Args:
124
- key (str): 캐시 키
125
- """
126
- pass
127
-
128
- ##########################
129
- # 4. 비즈니스 검증 메서드 #
130
- ##########################
131
- def _validate_business_rules(self, data: Dict[str, Any]) -> None:
132
- """비즈니스 규칙을 검증합니다.
133
-
134
- Args:
135
- data (Dict[str, Any]): 검증할 데이터
136
-
137
- Raises:
138
- CustomException: 비즈니스 규칙 위반 시
139
- """
140
- pass
141
-
142
- def _validate_permissions(self, request: Request, action: str) -> None:
143
- """권한을 검증합니다.
144
-
145
- Args:
146
- request (Request): FastAPI 요청 객체
147
- action (str): 수행할 작업
148
-
149
- Raises:
150
- CustomException: 권한이 없는 경우
151
- """
152
- pass
153
-
154
- ########################
155
- # 5. 응답 처리 메서드 #
156
- ########################
157
- def _handle_response_model(self, entity: ModelType, response_model: Any) -> Dict[str, Any]:
158
- """응답 모델에 맞게 데이터를 처리합니다.
159
-
160
- Args:
161
- entity (ModelType): 처리할 엔티티
162
- response_model (Any): 응답 모델
163
-
164
- Returns:
165
- Dict[str, Any]: 처리된 데이터
166
- """
167
- if not response_model:
168
- return self._process_response(entity)
169
-
170
- result = self._process_response(entity)
171
-
172
- # response_model에 없는 필드 제거
173
- keys_to_remove = [key for key in result if key not in response_model.model_fields]
174
- for key in keys_to_remove:
175
- result.pop(key)
176
-
177
- # 모델 검증
178
- return response_model(**result).model_dump()
179
-
180
- def _handle_exclude_fields(self, data: Dict[str, Any], exclude_fields: List[str]) -> Dict[str, Any]:
181
- """제외할 필드를 처리합니다.
182
-
183
- Args:
184
- data (Dict[str, Any]): 처리할 데이터
185
- exclude_fields (List[str]): 제외할 필드 목록
186
-
187
- Returns:
188
- Dict[str, Any]: 처리된 데이터
189
- """
190
- if not exclude_fields:
191
- return data
192
-
193
- return {k: v for k, v in data.items() if k not in exclude_fields}
194
-
195
- def _validate_ulid(self, ulid: str) -> bool:
196
- """ULID 형식을 검증합니다.
197
-
198
- Args:
199
- ulid (str): 검증할 ULID
200
-
201
- Returns:
202
- bool: 유효한 ULID 여부
203
- """
204
- try:
205
- ULID.from_str(ulid)
206
- return True
207
- except (ValueError, AttributeError):
208
- return False
209
-
210
- def _process_columns(self, entity: ModelType, exclude_extra_data: bool = True) -> Dict[str, Any]:
211
- """엔티티의 컬럼들을 처리합니다.
212
-
213
- Args:
214
- entity (ModelType): 처리할 엔티티
215
- exclude_extra_data (bool, optional): extra_data 컬럼 제외 여부. Defaults to True.
216
-
217
- Returns:
218
- Dict[str, Any]: 처리된 컬럼 데이터
219
- """
220
- result = {}
221
- for column in entity.__table__.columns:
222
- if exclude_extra_data and column.name == 'extra_data':
223
- continue
224
-
225
- # 필드 값 처리
226
- if hasattr(entity, column.name):
227
- value = getattr(entity, column.name)
228
- if isinstance(value, datetime):
229
- value = value.isoformat()
230
- result[column.name] = value
231
- elif hasattr(entity, 'extra_data') and isinstance(entity.extra_data, dict):
232
- result[column.name] = entity.extra_data.get(column.name)
233
- else:
234
- result[column.name] = None
235
-
236
- # extra_data의 내용을 최상위 레벨로 업데이트
237
- if hasattr(entity, 'extra_data') and isinstance(entity.extra_data, dict):
238
- result.update(entity.extra_data or {})
239
-
240
- return result
241
-
242
- def _process_response(self, entity: ModelType, response_model: Any = None) -> Dict[str, Any]:
243
- """응답 데이터를 처리합니다.
244
- extra_data의 내용을 최상위 레벨로 변환하고, 라우터에서 선언한 응답 스키마에 맞게 데이터를 변환합니다.
245
-
246
- Args:
247
- entity (ModelType): 처리할 엔티티
248
- response_model (Any, optional): 응답 스키마. Defaults to None.
249
-
250
- Returns:
251
- Dict[str, Any]: 처리된 엔티티 데이터
252
- """
253
- if not entity:
254
- return None
255
-
256
- # 모든 필드 처리
257
- result = self._process_columns(entity)
258
-
259
- # Relationship 처리 (이미 로드된 관계만 처리)
260
- for relationship in entity.__mapper__.relationships:
261
- if not relationship.key in entity.__dict__:
262
- continue
263
-
264
- try:
265
- value = getattr(entity, relationship.key)
266
- # response_model이 있는 경우 해당 필드의 annotation type을 가져옴
267
- nested_response_model = None
268
- if response_model and relationship.key in response_model.model_fields:
269
- field_info = response_model.model_fields[relationship.key]
270
- nested_response_model = field_info.annotation
271
-
272
- if value is not None:
273
- if isinstance(value, list):
274
- result[relationship.key] = [
275
- self._process_response(item, nested_response_model)
276
- for item in value
277
- ]
278
- else:
279
- result[relationship.key] = self._process_response(value, nested_response_model)
280
- else:
281
- result[relationship.key] = None
282
- except Exception:
283
- result[relationship.key] = None
284
-
285
- # response_model이 있는 경우 필터링
286
- if response_model:
287
- # 현재 키 목록을 저장
288
- current_keys = list(result.keys())
289
- # response_model에 없는 키 제거
290
- for key in current_keys:
291
- if key not in response_model.model_fields:
292
- result.pop(key)
293
- # 모델 검증 및 업데이트
294
- result.update(response_model(**result).model_dump())
295
-
296
- return result
297
-
298
- def _process_basic_fields(self, entity: ModelType) -> Dict[str, Any]:
299
- """엔티티의 기본 필드만 처리합니다.
300
-
301
- Args:
302
- entity (ModelType): 처리할 엔티티
303
-
304
- Returns:
305
- Dict[str, Any]: 기본 필드만 포함된 딕셔너리
306
- """
307
- if not entity:
308
- return None
309
-
310
- return self._process_columns(entity)
311
-
312
- async def _create_for_model(self, model_name: str, data: Dict[str, Any], exclude_fields: List[str] = None) -> DeclarativeBase:
313
- """지정된 모델에 대해 새로운 엔티티를 생성합니다.
314
-
315
- Args:
316
- model_name (str): 생성할 모델 이름
317
- data (Dict[str, Any]): 생성할 엔티티 데이터
318
- exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
319
-
320
- Returns:
321
- DeclarativeBase: 생성된 엔티티
322
-
323
- Raises:
324
- CustomException: 데이터베이스 작업 중 오류 발생 시
325
- """
326
- if model_name not in self.additional_models:
327
- raise CustomException(
328
- ErrorCode.INVALID_REQUEST,
329
- detail=f"Model {model_name} not registered",
330
- source_function=f"{self.__class__.__name__}._create_for_model"
331
- )
332
-
333
- try:
334
- # 제외할 필드 처리
335
- if exclude_fields:
336
- data = {k: v for k, v in data.items() if k not in exclude_fields}
337
-
338
- return await self.db_service.create_entity(self.additional_models[model_name], data)
339
- except CustomException as e:
340
- raise e
341
- except Exception as e:
342
- raise CustomException(
343
- ErrorCode.DB_CREATE_ERROR,
344
- detail=str(e),
345
- source_function=f"{self.__class__.__name__}._create_for_model",
346
- original_error=e
347
- )
348
-
349
- def _process_password(self, data: Dict[str, Any]) -> Dict[str, Any]:
350
- """비밀번호 필드가 있는 경우 해시화합니다.
351
-
352
- Args:
353
- data (Dict[str, Any]): 처리할 데이터
354
-
355
- Returns:
356
- Dict[str, Any]: 처리된 데이터
357
- """
358
- if "password" in data:
359
- data["password"] = hash_password(data["password"])
360
- return data
361
-
362
- #######################
363
- # 6. CRUD 작업 메서드 #
364
- #######################
365
- async def create(self, data: Dict[str, Any], exclude_fields: List[str] = None, model_name: str = None) -> Union[ModelType, DeclarativeBase]:
366
- """새로운 엔티티를 생성합니다.
367
-
368
- Args:
369
- data (Dict[str, Any]): 생성할 엔티티 데이터
370
- exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
371
- model_name (str, optional): 생성할 모델 이름. Defaults to None.
372
-
373
- Returns:
374
- Union[ModelType, DeclarativeBase]: 생성된 엔티티
375
-
376
- Raises:
377
- CustomException: 데이터베이스 작업 중 오류 발생 시
378
- """
379
- try:
380
- # 비밀번호 해시화
381
- data = self._process_password(data)
382
-
383
- # 제외할 필드 처리
384
- if exclude_fields:
385
- data = {k: v for k, v in data.items() if k not in exclude_fields}
386
-
387
- if model_name:
388
- return await self._create_for_model(model_name, data)
389
-
390
- return await self.repository.create(data)
391
- except CustomException as e:
392
- raise e
393
- except Exception as e:
394
- raise CustomException(
395
- ErrorCode.DB_CREATE_ERROR,
396
- detail=str(e),
397
- source_function=f"{self.__class__.__name__}.create",
398
- original_error=e
399
- )
400
-
401
- async def update(
402
- self,
403
- ulid: str,
404
- data: Dict[str, Any],
405
- exclude_fields: List[str] = None,
406
- model_name: str = None
407
- ) -> Optional[ModelType]:
408
- """기존 엔티티를 수정합니다.
409
-
410
- Args:
411
- ulid (str): 수정할 엔티티의 ULID
412
- data (Dict[str, Any]): 수정할 데이터
413
- exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
414
- model_name (str, optional): 수정할 모델 이름. Defaults to None.
415
-
416
- Returns:
417
- Optional[ModelType]: 수정된 엔티티, 없으면 None
418
-
419
- Raises:
420
- CustomException: 데이터베이스 작업 중 오류 발생 시
421
- """
422
- try:
423
- # 비밀번호 해시화
424
- data = self._process_password(data)
425
-
426
- # 제외할 필드 처리
427
- if exclude_fields:
428
- data = {k: v for k, v in data.items() if k not in exclude_fields}
429
-
430
- async with self.db_service.transaction():
431
- if model_name:
432
- if model_name not in self.additional_models:
433
- raise CustomException(
434
- ErrorCode.INVALID_REQUEST,
435
- detail=f"Model {model_name} not registered",
436
- source_function=f"{self.__class__.__name__}.update"
437
- )
438
- entity = await self.db_service.update_entity(
439
- self.additional_models[model_name],
440
- {"ulid": ulid},
441
- data
442
- )
443
- if not entity:
444
- raise CustomException(
445
- ErrorCode.NOT_FOUND,
446
- detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
447
- source_function=f"{self.__class__.__name__}.update"
448
- )
449
- return entity
450
-
451
- entity = await self.repository.update(ulid, data)
452
- if not entity:
453
- raise CustomException(
454
- ErrorCode.NOT_FOUND,
455
- detail=f"{self.model.__tablename__}|ulid|{ulid}",
456
- source_function=f"{self.__class__.__name__}.update"
457
- )
458
- return entity
459
- except CustomException as e:
460
- raise e
461
- except Exception as e:
462
- raise CustomException(
463
- ErrorCode.DB_UPDATE_ERROR,
464
- detail=str(e),
465
- source_function=f"{self.__class__.__name__}.update",
466
- original_error=e
467
- )
468
-
469
- async def delete(self, ulid: str, model_name: str = None) -> bool:
470
- """엔티티를 소프트 삭제합니다 (is_deleted = True)."""
471
- try:
472
- if model_name:
473
- if model_name not in self.additional_models:
474
- raise CustomException(
475
- ErrorCode.INVALID_REQUEST,
476
- detail=f"Model {model_name} not registered",
477
- source_function=f"{self.__class__.__name__}.delete"
478
- )
479
-
480
- stmt = select(self.additional_models[model_name]).filter_by(ulid=ulid, is_deleted=False)
481
- result = await self.session.execute(stmt)
482
- entity = result.scalars().first()
483
-
484
- if not entity:
485
- raise CustomException(
486
- ErrorCode.NOT_FOUND,
487
- detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
488
- source_function=f"{self.__class__.__name__}.delete"
489
- )
490
-
491
- entity.is_deleted = True
492
- await self.session.flush()
493
- return True
494
-
495
- return await self.repository.delete(ulid)
496
-
497
- except CustomException as e:
498
- raise e
499
- except Exception as e:
500
- raise CustomException(
501
- ErrorCode.DB_DELETE_ERROR,
502
- detail=str(e),
503
- source_function=f"{self.__class__.__name__}.delete",
504
- original_error=e
505
- )
506
-
507
- async def real_row_delete(self, ulid: str, model_name: str = None) -> bool:
508
- """엔티티를 실제로 삭제합니다.
509
-
510
- Args:
511
- ulid (str): 삭제할 엔티티의 ULID
512
- model_name (str, optional): 삭제할 모델 이름. Defaults to None.
513
-
514
- Returns:
515
- bool: 삭제 성공 여부
516
-
517
- Raises:
518
- CustomException: 데이터베이스 작업 중 오류 발생 시
519
- """
520
- try:
521
- if model_name:
522
- if model_name not in self.additional_models:
523
- raise CustomException(
524
- ErrorCode.INVALID_REQUEST,
525
- detail=f"Model {model_name} not registered",
526
- source_function=f"{self.__class__.__name__}.real_row_delete"
527
- )
528
- entity = await self.db_service.retrieve_entity(
529
- self.additional_models[model_name],
530
- {"ulid": ulid}
531
- )
532
- if entity:
533
- await self.db_service.delete_entity(entity)
534
- return True
535
- return False
536
-
537
- return await self.repository.real_row_delete(ulid)
538
- except CustomException as e:
539
- raise e
540
- except Exception as e:
541
- raise CustomException(
542
- ErrorCode.DB_DELETE_ERROR,
543
- detail=str(e),
544
- source_function=f"{self.__class__.__name__}.real_row_delete",
545
- original_error=e
546
- )
547
-
548
- #########################
549
- # 7. 조회 및 검색 메서드 #
550
- #########################
551
- async def list(
552
- self,
553
- skip: int = 0,
554
- limit: int = 100,
555
- filters: Dict[str, Any] | None = None,
556
- search_params: Dict[str, Any] | None = None,
557
- model_name: str | None = None,
558
- request: Request | None = None,
559
- response_model: Any = None
560
- ) -> List[Dict[str, Any]]:
561
- """엔티티 목록을 조회합니다."""
562
- try:
563
- if model_name:
564
- if model_name not in self.additional_models:
565
- raise CustomException(
566
- ErrorCode.INVALID_REQUEST,
567
- detail=f"Model {model_name} not registered",
568
- source_function=f"{self.__class__.__name__}.list"
569
- )
570
-
571
- stmt = select(self.additional_models[model_name]).where(
572
- self.additional_models[model_name].is_deleted == False
573
- )
574
-
575
- if filters:
576
- for key, value in filters.items():
577
- if value is not None:
578
- stmt = stmt.where(getattr(self.additional_models[model_name], key) == value)
579
-
580
- stmt = stmt.offset(skip).limit(limit)
581
- result = await self.session.execute(stmt)
582
- entities = result.scalars().all()
583
-
584
- return [self._process_response(entity, response_model) for entity in entities]
585
-
586
- return await self.repository.list(
587
- skip=skip,
588
- limit=limit,
589
- filters=filters,
590
- search_params=search_params
591
- )
592
-
593
- except CustomException as e:
594
- e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
595
- e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
596
- raise e
597
- except Exception as e:
598
- raise CustomException(
599
- ErrorCode.INTERNAL_ERROR,
600
- detail=str(e),
601
- source_function=f"{self.__class__.__name__}.list",
602
- original_error=e
603
- )
604
-
605
- async def get(
606
- self,
607
- ulid: str,
608
- model_name: str | None = None,
609
- request: Request | None = None,
610
- response_model: Any = None
611
- ) -> Optional[Dict[str, Any]]:
612
- """특정 엔티티를 조회합니다.
613
-
614
- Args:
615
- ulid (str): 조회할 엔티티의 ULID
616
- model_name (str | None, optional): 조회할 모델 이름. Defaults to None.
617
- request (Request | None, optional): 요청 객체. Defaults to None.
618
- response_model (Any, optional): 응답 스키마. Defaults to None.
619
-
620
- Returns:
621
- Optional[Dict[str, Any]]: 조회된 엔티티, 없으면 None
622
-
623
- Raises:
624
- CustomException: 데이터베이스 작업 중 오류 발생 시
625
- """
626
- try:
627
- # ULID 검증
628
- if not self._validate_ulid(ulid):
629
- raise CustomException(
630
- ErrorCode.VALIDATION_ERROR,
631
- detail=f"Invalid ULID format: {ulid}",
632
- source_function=f"{self.__class__.__name__}.get"
633
- )
634
-
635
- if model_name:
636
- if model_name not in self.additional_models:
637
- raise CustomException(
638
- ErrorCode.INVALID_REQUEST,
639
- detail=f"Model {model_name} not registered",
640
- source_function=f"{self.__class__.__name__}.get"
641
- )
642
- entity = await self.db_service.retrieve_entity(
643
- self.additional_models[model_name],
644
- {"ulid": ulid, "is_deleted": False}
645
- )
646
- if not entity:
647
- raise CustomException(
648
- ErrorCode.NOT_FOUND,
649
- detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
650
- source_function=f"{self.__class__.__name__}.get"
651
- )
652
- return self._process_response(entity, response_model)
653
-
654
- entity = await self.repository.get(ulid)
655
- if not entity:
656
- raise CustomException(
657
- ErrorCode.NOT_FOUND,
658
- detail=f"{self.model.__tablename__}|ulid|{ulid}",
659
- source_function=f"{self.__class__.__name__}.get"
660
- )
661
-
662
- return self._process_response(entity, response_model)
663
- except CustomException as e:
664
- raise e
665
- except Exception as e:
666
- raise CustomException(
667
- ErrorCode.INTERNAL_ERROR,
668
- detail=str(e),
669
- source_function=f"{self.__class__.__name__}.get",
670
- original_error=e
671
- )
@@ -1,2 +0,0 @@
1
- """버전 정보"""
2
- __version__ = "0.2.58"
File without changes
File without changes
File without changes
File without changes