aiteamutils 0.2.73__tar.gz → 0.2.76__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.73
3
+ Version: 0.2.76
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
@@ -1,4 +1,4 @@
1
- from datetime import datetime, timezone
1
+ from datetime import datetime, timedelta, timezone
2
2
  from typing import Any, Dict, TypeVar, Generic, Optional
3
3
  from ulid import ULID
4
4
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
@@ -23,10 +23,18 @@ class BaseColumn(Base):
23
23
  doc="ULID",
24
24
  nullable=False
25
25
  )
26
- created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
26
+ created_at: Mapped[datetime] = mapped_column(
27
+ default=datetime.now(timezone.utc),
28
+ index=True
29
+ )
27
30
  updated_at: Mapped[datetime] = mapped_column(
28
- default=datetime.utcnow,
29
- onupdate=datetime.utcnow
31
+ default=datetime.now(timezone.utc),
32
+ onupdate=datetime.now(timezone.utc),
33
+ index=True
34
+ )
35
+ deleted_at: Mapped[datetime] = mapped_column(
36
+ default=None,
37
+ nullable=True
30
38
  )
31
39
  is_deleted: Mapped[bool] = mapped_column(
32
40
  default=False,
@@ -0,0 +1,146 @@
1
+ #기본 라이브러리
2
+ from typing import TypeVar, Generic, Type, Any, Dict, List, Optional
3
+ from sqlalchemy.orm import DeclarativeBase
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+ from sqlalchemy.exc import SQLAlchemyError
6
+ from sqlalchemy import select
7
+
8
+ #패키지 라이브러리
9
+ from .exceptions import ErrorCode, CustomException
10
+ from .database import (
11
+ list_entities,
12
+ get_entity,
13
+ create_entity,
14
+ update_entity,
15
+ delete_entity
16
+ )
17
+
18
+ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
19
+
20
+ class BaseRepository(Generic[ModelType]):
21
+ def __init__(self, session: AsyncSession, model: Type[ModelType]):
22
+ self._session = session
23
+ self.model = model
24
+
25
+ @property
26
+ def session(self) -> AsyncSession:
27
+ return self._session
28
+
29
+ @session.setter
30
+ def session(self, value: AsyncSession):
31
+ if value is None:
32
+ raise CustomException(
33
+ ErrorCode.DB_CONNECTION_ERROR,
34
+ detail="Session cannot be None",
35
+ source_function=f"{self.__class__.__name__}.session"
36
+ )
37
+ self._session = value
38
+
39
+ #######################
40
+ # 입력 및 수정, 삭제 #
41
+ #######################
42
+ async def create(
43
+ self,
44
+ entity_data: Dict[str, Any],
45
+ exclude_entities: List[str] | None = None
46
+ ) -> ModelType:
47
+ try:
48
+ return await create_entity(
49
+ session=self.session,
50
+ model=self.model,
51
+ entity_data=entity_data,
52
+ exclude_entities=exclude_entities
53
+ )
54
+ except CustomException as e:
55
+ raise e
56
+ except Exception as e:
57
+ raise CustomException(
58
+ ErrorCode.INTERNAL_ERROR,
59
+ detail=str(e),
60
+ source_function=f"{self.__class__.__name__}.create",
61
+ original_error=e
62
+ )
63
+
64
+ async def update(
65
+ self,
66
+ update_data: Dict[str, Any],
67
+ conditions: Dict[str, Any]
68
+ ) -> ModelType:
69
+ try:
70
+ return await update_entity(
71
+ session=self.session,
72
+ model=self.model,
73
+ update_data=update_data,
74
+ conditions=conditions
75
+ )
76
+ except CustomException as e:
77
+ raise e
78
+
79
+ async def delete(
80
+ self,
81
+ conditions: Dict[str, Any]
82
+ ) -> None:
83
+ await delete_entity(
84
+ session=self.session,
85
+ model=self.model,
86
+ conditions=conditions
87
+ )
88
+
89
+ #########################
90
+ # 조회 및 검색 메서드 #
91
+ #########################
92
+ async def list(
93
+ self,
94
+ skip: int = 0,
95
+ limit: int = 100,
96
+ filters: Optional[Dict[str, Any]] = None,
97
+ explicit_joins: Optional[List[Any]] = None,
98
+ loading_joins: Optional[List[Any]] = None
99
+ ) -> List[ModelType]:
100
+ """
101
+ 엔티티 목록 조회.
102
+ """
103
+ try:
104
+ # 기본 CRUD 작업 호출
105
+ return await list_entities(
106
+ session=self.session,
107
+ model=self.model,
108
+ skip=skip,
109
+ limit=limit,
110
+ filters=filters,
111
+ explicit_joins=explicit_joins,
112
+ loading_joins=loading_joins
113
+ )
114
+ except CustomException as e:
115
+ raise e
116
+ except Exception as e:
117
+ raise CustomException(
118
+ ErrorCode.INTERNAL_ERROR,
119
+ detail=str(e),
120
+ source_function=f"{self.__class__.__name__}.list",
121
+ original_error=e
122
+ )
123
+
124
+ async def get(
125
+ self,
126
+ conditions: Dict[str, Any] | None = None,
127
+ explicit_joins: Optional[List[Any]] = None,
128
+ loading_joins: Optional[List[Any]] = None
129
+ ) -> ModelType:
130
+ try:
131
+ return await get_entity(
132
+ session=self.session,
133
+ model=self.model,
134
+ conditions=conditions,
135
+ explicit_joins=explicit_joins,
136
+ loading_joins=loading_joins
137
+ )
138
+ except CustomException as e:
139
+ raise e
140
+ except Exception as e:
141
+ raise CustomException(
142
+ ErrorCode.INTERNAL_ERROR,
143
+ detail=str(e),
144
+ source_function=f"{self.__class__.__name__}.get",
145
+ original_error=e
146
+ )
@@ -0,0 +1,256 @@
1
+ #기본 라이브러리
2
+ from fastapi import Request
3
+ from typing import TypeVar, Generic, Type, Dict, Any, Union, List, Optional
4
+ from sqlalchemy.orm import DeclarativeBase
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from datetime import datetime
7
+ from ulid import ULID
8
+
9
+ #패키지 라이브러리
10
+ from .exceptions import ErrorCode, CustomException
11
+ from .base_repository import BaseRepository
12
+ from .database import (
13
+ process_response,
14
+ validate_unique_fields
15
+ )
16
+ from .security import hash_password
17
+ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
18
+
19
+ class BaseService(Generic[ModelType]):
20
+ ##################
21
+ # 1. 초기화 영역 #
22
+ ##################
23
+ def __init__(
24
+ self,
25
+ model: Type[ModelType],
26
+ repository: BaseRepository[ModelType],
27
+ db_session: AsyncSession,
28
+ additional_models: Dict[str, Type[DeclarativeBase]] = None,
29
+ ):
30
+ self.model = model
31
+ self.repository = repository
32
+ self.db_session = db_session
33
+ self.additional_models = additional_models or {},
34
+
35
+ #######################
36
+ # 입력 및 수정, 삭제 #
37
+ #######################
38
+ async def create(
39
+ self,
40
+ entity_data: Dict[str, Any],
41
+ model_name: str | None = None,
42
+ response_model: Any = None,
43
+ exclude_entities: List[str] | None = None,
44
+ unique_check: List[Dict[str, Any]] | None = None,
45
+ fk_check: List[Dict[str, Any]] | None = None,
46
+ ) -> ModelType:
47
+
48
+ try:
49
+ async with self.db_session.begin():
50
+ # 고유 검사 수행
51
+ if unique_check:
52
+ await validate_unique_fields(self.db_session, unique_check, find_value=True)
53
+ # 외래 키 검사 수행
54
+ if fk_check:
55
+ await validate_unique_fields(self.db_session, fk_check, find_value=False)
56
+
57
+ # repository의 create 메서드를 트랜잭션 내에서 실행
58
+ return await self.repository.create(
59
+ entity_data=entity_data,
60
+ exclude_entities=exclude_entities
61
+ )
62
+ except CustomException as e:
63
+ raise e
64
+ except Exception as e:
65
+ # 다른 예외 처리
66
+ raise CustomException(
67
+ ErrorCode.INTERNAL_ERROR,
68
+ detail=str(e),
69
+ source_function=f"{self.__class__.__name__}.create",
70
+ original_error=e
71
+ )
72
+
73
+ async def update(
74
+ self,
75
+ ulid: str | None = None,
76
+ update_data: Dict[str, Any] | None = None,
77
+ conditions: Dict[str, Any] | None = None,
78
+ unique_check: List[Dict[str, Any]] | None = None,
79
+ exclude_entities: List[str] | None = None
80
+ ) -> ModelType:
81
+ try:
82
+ # 고유 검사 수행
83
+ if unique_check:
84
+ await validate_unique_fields(self.db_session, unique_check, find_value=True)
85
+ # 비밀번호가 있으면 해시 처리
86
+ if "password" in update_data:
87
+ update_data["password"] = hash_password(update_data["password"])
88
+ # 제외할 엔티티가 있으면 제외
89
+ if exclude_entities:
90
+ update_data = {k: v for k, v in update_data.items() if k not in exclude_entities}
91
+
92
+ if not ulid and not conditions:
93
+ raise CustomException(
94
+ ErrorCode.INVALID_INPUT,
95
+ detail="Either 'ulid' or 'conditions' must be provided.",
96
+ source_function="database.update_entity"
97
+ )
98
+
99
+ # ulid로 조건 생성
100
+ if ulid:
101
+ if not ULID.from_str(ulid):
102
+ raise CustomException(
103
+ ErrorCode.VALIDATION_ERROR,
104
+ detail=ulid,
105
+ source_function=f"{self.__class__.__name__}.update"
106
+ )
107
+
108
+ conditions = {"ulid": ulid}
109
+
110
+ return await self.repository.update(
111
+ update_data=update_data,
112
+ conditions=conditions
113
+ )
114
+ except CustomException as e:
115
+ raise e
116
+ except Exception as e:
117
+ raise CustomException(
118
+ ErrorCode.INTERNAL_ERROR,
119
+ detail=str(e),
120
+ source_function=f"{self.__class__.__name__}.update",
121
+ original_error=e
122
+ )
123
+
124
+ async def delete(
125
+ self,
126
+ ulid: str | None = None,
127
+ conditions: Dict[str, Any] | None = None
128
+ ) -> None:
129
+ try:
130
+ if not ULID.from_str(ulid):
131
+ raise CustomException(
132
+ ErrorCode.VALIDATION_ERROR,
133
+ detail=ulid,
134
+ source_function=f"{self.__class__.__name__}.delete"
135
+ )
136
+
137
+ if not ulid and not conditions:
138
+ raise CustomException(
139
+ ErrorCode.INVALID_INPUT,
140
+ detail="Either 'ulid' or 'conditions' must be provided.",
141
+ source_function="database.update_entity"
142
+ )
143
+
144
+ # ulid로 조건 생성
145
+ if ulid:
146
+ conditions = {"ulid": ulid}
147
+
148
+ conditions["is_deleted"] = False
149
+
150
+ return await self.repository.delete(
151
+ conditions=conditions
152
+ )
153
+ except CustomException as e:
154
+ raise e
155
+ except Exception as e:
156
+ raise CustomException(
157
+ ErrorCode.INTERNAL_ERROR,
158
+ detail=str(e),
159
+ source_function=f"{self.__class__.__name__}.delete",
160
+ original_error=e
161
+ )
162
+
163
+ #########################
164
+ # 조회 및 검색 메서드 #
165
+ #########################
166
+ async def list(
167
+ self,
168
+ skip: int = 0,
169
+ limit: int = 100,
170
+ filters: List[Dict[str, Any]] | None = None,
171
+ model_name: str | None = None,
172
+ response_model: Any = None,
173
+ explicit_joins: Optional[List[Any]] = None,
174
+ loading_joins: Optional[List[Any]] = None
175
+ ) -> List[Dict[str, Any]]:
176
+ try:
177
+ # 모델 이름을 통한 동적 처리
178
+ if model_name:
179
+ if model_name not in self.additional_models:
180
+ raise CustomException(
181
+ ErrorCode.INVALID_REQUEST,
182
+ detail=f"Model {model_name} not registered",
183
+ source_function=f"{self.__class__.__name__}.list"
184
+ )
185
+ model = self.additional_models[model_name]
186
+ entities = await self.repository.list(
187
+ skip=skip,
188
+ limit=limit,
189
+ filters=filters,
190
+ model=model
191
+ )
192
+ return [process_response(entity, response_model) for entity in entities]
193
+
194
+ entities = await self.repository.list(
195
+ skip=skip,
196
+ limit=limit,
197
+ filters=filters,
198
+ explicit_joins=explicit_joins,
199
+ loading_joins=loading_joins
200
+ )
201
+ return [process_response(entity, response_model) for entity in entities]
202
+
203
+ except CustomException as e:
204
+ raise e
205
+ except Exception as e:
206
+ raise CustomException(
207
+ ErrorCode.INTERNAL_ERROR,
208
+ source_function=f"{self.__class__.__name__}.list",
209
+ original_error=e
210
+ )
211
+
212
+ async def get(
213
+ self,
214
+ ulid: str,
215
+ model_name: str | None = None,
216
+ response_model: Any = None,
217
+ conditions: Dict[str, Any] | None = None,
218
+ explicit_joins: Optional[List[Any]] = None,
219
+ loading_joins: Optional[List[Any]] = None
220
+ ):
221
+ try:
222
+ if not ulid and not conditions:
223
+ raise CustomException(
224
+ ErrorCode.INVALID_INPUT,
225
+ detail="Either 'ulid' or 'conditions' must be provided.",
226
+ source_function="database.update_entity"
227
+ )
228
+
229
+ # ulid로 조건 생성
230
+ if ulid:
231
+ if not ULID.from_str(ulid):
232
+ raise CustomException(
233
+ ErrorCode.VALIDATION_ERROR,
234
+ detail=ulid,
235
+ source_function=f"{self.__class__.__name__}.update"
236
+ )
237
+
238
+ conditions = {"ulid": ulid}
239
+
240
+ entity = await self.repository.get(
241
+ conditions=conditions,
242
+ explicit_joins=explicit_joins,
243
+ loading_joins=loading_joins
244
+ )
245
+ return process_response(entity, response_model)
246
+
247
+ except CustomException as e:
248
+ raise e
249
+ except Exception as e:
250
+ raise CustomException(
251
+ ErrorCode.INTERNAL_ERROR,
252
+ detail=str(e),
253
+ source_function=f"{self.__class__.__name__}.get",
254
+ original_error=e
255
+ )
256
+