aiteamutils 0.2.73__tar.gz → 0.2.76__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.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
+