aiteamutils 0.2.48__tar.gz → 0.2.101__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.
Files changed (25) hide show
  1. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/.gitignore +1 -6
  2. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/PKG-INFO +1 -1
  3. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/aiteamutils/__init__.py +0 -14
  4. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/aiteamutils/base_model.py +15 -4
  5. aiteamutils-0.2.101/aiteamutils/base_repository.py +150 -0
  6. aiteamutils-0.2.101/aiteamutils/base_service.py +301 -0
  7. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/aiteamutils/config.py +23 -13
  8. aiteamutils-0.2.101/aiteamutils/database.py +599 -0
  9. aiteamutils-0.2.101/aiteamutils/enums.py +84 -0
  10. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/aiteamutils/exceptions.py +9 -2
  11. aiteamutils-0.2.101/aiteamutils/security.py +328 -0
  12. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/aiteamutils/validators.py +0 -1
  13. aiteamutils-0.2.101/aiteamutils/version.py +2 -0
  14. aiteamutils-0.2.48/aiteamutils/base_repository.py +0 -504
  15. aiteamutils-0.2.48/aiteamutils/base_service.py +0 -668
  16. aiteamutils-0.2.48/aiteamutils/database.py +0 -1096
  17. aiteamutils-0.2.48/aiteamutils/dependencies.py +0 -135
  18. aiteamutils-0.2.48/aiteamutils/enums.py +0 -23
  19. aiteamutils-0.2.48/aiteamutils/security.py +0 -479
  20. aiteamutils-0.2.48/aiteamutils/version.py +0 -2
  21. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/.cursorrules +0 -0
  22. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/README.md +0 -0
  23. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/aiteamutils/cache.py +0 -0
  24. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/pyproject.toml +0 -0
  25. {aiteamutils-0.2.48 → aiteamutils-0.2.101}/setup.py +0 -0
@@ -13,15 +13,10 @@ build/
13
13
  .vscode/
14
14
  .idea/
15
15
 
16
- # Client specific
17
- clients/
18
- !clients/.gitkeep
19
- !frontend/src/clients
20
-
21
16
  # Logs
22
17
  *.log
23
18
  npm-debug.log*
24
19
 
25
20
  # System
26
21
  .DS_Store
27
- Thumbs.db
22
+ Thumbs.db
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.48
3
+ Version: 0.2.101
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,11 +1,4 @@
1
1
  from .base_model import Base
2
- from .database import (
3
- DatabaseService,
4
- DatabaseServiceManager,
5
- get_db,
6
- get_database_service,
7
- lifespan
8
- )
9
2
  from .exceptions import (
10
3
  CustomException,
11
4
  ErrorCode,
@@ -34,13 +27,6 @@ __all__ = [
34
27
  "BaseService",
35
28
  "BaseRepository",
36
29
 
37
- # Database
38
- "DatabaseService",
39
- "DatabaseServiceManager",
40
- "get_db",
41
- "get_database_service",
42
- "lifespan",
43
-
44
30
  # Exceptions
45
31
  "CustomException",
46
32
  "ErrorCode",
@@ -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,21 @@ 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
+ TIMESTAMP(timezone=True),
28
+ default=datetime.now(timezone.utc),
29
+ index=True
30
+ )
27
31
  updated_at: Mapped[datetime] = mapped_column(
28
- default=datetime.utcnow,
29
- onupdate=datetime.utcnow
32
+ TIMESTAMP(timezone=True),
33
+ default=datetime.now(timezone.utc),
34
+ onupdate=datetime.now(timezone.utc),
35
+ index=True
36
+ )
37
+ deleted_at: Mapped[datetime] = mapped_column(
38
+ TIMESTAMP(timezone=True),
39
+ default=None,
40
+ nullable=True
30
41
  )
31
42
  is_deleted: Mapped[bool] = mapped_column(
32
43
  default=False,
@@ -0,0 +1,150 @@
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
+ exclude_entities: List[str] | None = None
69
+ ) -> ModelType:
70
+ try:
71
+ return await update_entity(
72
+ session=self.session,
73
+ model=self.model,
74
+ update_data=update_data,
75
+ conditions=conditions,
76
+ exclude_entities=exclude_entities
77
+ )
78
+ except CustomException as e:
79
+ raise e
80
+
81
+ async def delete(
82
+ self,
83
+ conditions: Dict[str, Any]
84
+ ) -> bool:
85
+ await delete_entity(
86
+ session=self.session,
87
+ model=self.model,
88
+ conditions=conditions
89
+ )
90
+ return True
91
+ #########################
92
+ # 조회 및 검색 메서드 #
93
+ #########################
94
+ async def list(
95
+ self,
96
+ skip: int = 0,
97
+ limit: int = 100,
98
+ filters: Optional[List[Dict[str, Any]]] = None,
99
+ explicit_joins: Optional[List[Any]] = None,
100
+ loading_joins: Optional[List[Any]] = None,
101
+ order: Optional[List[Dict[str, str]]] = None
102
+ ) -> List[ModelType]:
103
+ """
104
+ 엔티티 목록 조회.
105
+ """
106
+ try:
107
+ # 기본 CRUD 작업 호출
108
+ return await list_entities(
109
+ session=self.session,
110
+ model=self.model,
111
+ skip=skip,
112
+ limit=limit,
113
+ filters=filters,
114
+ explicit_joins=explicit_joins,
115
+ loading_joins=loading_joins,
116
+ order=order
117
+ )
118
+ except CustomException as e:
119
+ raise e
120
+ except Exception as e:
121
+ raise CustomException(
122
+ ErrorCode.INTERNAL_ERROR,
123
+ detail=str(e),
124
+ source_function=f"{self.__class__.__name__}.list",
125
+ original_error=e
126
+ )
127
+
128
+ async def get(
129
+ self,
130
+ conditions: Dict[str, Any] | None = None,
131
+ explicit_joins: Optional[List[Any]] = None,
132
+ loading_joins: Optional[List[Any]] = None
133
+ ) -> ModelType:
134
+ try:
135
+ return await get_entity(
136
+ session=self.session,
137
+ model=self.model,
138
+ conditions=conditions,
139
+ explicit_joins=explicit_joins,
140
+ loading_joins=loading_joins
141
+ )
142
+ except CustomException as e:
143
+ raise e
144
+ except Exception as e:
145
+ raise CustomException(
146
+ ErrorCode.INTERNAL_ERROR,
147
+ detail=str(e),
148
+ source_function=f"{self.__class__.__name__}.get",
149
+ original_error=e
150
+ )
@@ -0,0 +1,301 @@
1
+ #기본 라이브러리
2
+ from fastapi import Request
3
+ from typing import TypeVar, Generic, Type, Dict, Any, Union, List, Optional, Literal
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, verify_jwt_token, verify_role_permission
17
+ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
18
+
19
+ class BaseService(Generic[ModelType]):
20
+ ##################
21
+ # 초기화 영역 #
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
+ request: Request,
41
+ entity_data: Dict[str, Any],
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
+ org_ulid_position: str = "organization_ulid",
47
+ role_permission: str | None = None,
48
+ token_settings: Dict[str, Any] | None = None
49
+ ) -> ModelType:
50
+
51
+ if role_permission:
52
+ permission_result = await verify_role_permission(
53
+ request=request,
54
+ role_permission=role_permission,
55
+ token_settings=token_settings,
56
+ org_ulid_position=org_ulid_position
57
+ )
58
+
59
+ if not permission_result:
60
+ raise CustomException(
61
+ ErrorCode.FORBIDDEN,
62
+ detail=f"{role_permission}",
63
+ source_function=f"{self.__class__.__name__}.create"
64
+ )
65
+
66
+ try:
67
+ async with self.db_session.begin():
68
+ # 고유 검사 수행
69
+ if unique_check:
70
+ await validate_unique_fields(self.db_session, unique_check, find_value=True)
71
+ # 외래 키 검사 수행
72
+ if fk_check:
73
+ await validate_unique_fields(self.db_session, fk_check, find_value=False)
74
+
75
+ result = await self.repository.create(
76
+ entity_data=entity_data,
77
+ exclude_entities=exclude_entities
78
+ )
79
+
80
+ # 결과 반환
81
+ if response_model:
82
+ return process_response(result, response_model)
83
+ else:
84
+ return result
85
+
86
+ except CustomException as e:
87
+ raise e
88
+ except Exception as e:
89
+ # 다른 예외 처리
90
+ raise CustomException(
91
+ ErrorCode.INTERNAL_ERROR,
92
+ detail=str(e),
93
+ source_function=f"{self.__class__.__name__}.create",
94
+ original_error=e
95
+ )
96
+
97
+ async def update(
98
+ self,
99
+ request: Request,
100
+ ulid: str | None = None,
101
+ update_data: Dict[str, Any] | None = None,
102
+ conditions: Dict[str, Any] | None = None,
103
+ unique_check: List[Dict[str, Any]] | None = None,
104
+ exclude_entities: List[str] | None = None,
105
+ response_model: Any = None,
106
+ org_ulid_position: str = "organization_ulid",
107
+ role_permission: str = "update",
108
+ token_settings: Dict[str, Any] | None = None
109
+ ) -> ModelType:
110
+ try:
111
+ async with self.db_session.begin():
112
+ # 고유 검사 수행
113
+ if unique_check:
114
+ await validate_unique_fields(self.db_session, unique_check, find_value=True)
115
+
116
+ if not ulid and not conditions:
117
+ raise CustomException(
118
+ ErrorCode.INVALID_INPUT,
119
+ detail="Either 'ulid' or 'conditions' must be provided.",
120
+ source_function="database.update_entity"
121
+ )
122
+
123
+ # ulid로 조건 생성
124
+ if ulid:
125
+ if not ULID.from_str(ulid):
126
+ raise CustomException(
127
+ ErrorCode.VALIDATION_ERROR,
128
+ detail=ulid,
129
+ source_function=f"{self.__class__.__name__}.update"
130
+ )
131
+
132
+ conditions = {"ulid": ulid}
133
+
134
+ result = await self.repository.update(
135
+ update_data=update_data,
136
+ conditions=conditions,
137
+ exclude_entities=exclude_entities
138
+ )
139
+
140
+ if response_model:
141
+ return process_response(result, response_model)
142
+ else:
143
+ return result
144
+ except CustomException as e:
145
+ raise e
146
+ except Exception as e:
147
+ raise CustomException(
148
+ ErrorCode.INTERNAL_ERROR,
149
+ detail=str(e),
150
+ source_function=f"{self.__class__.__name__}.update",
151
+ original_error=e
152
+ )
153
+
154
+ async def delete(
155
+ self,
156
+ request: Request,
157
+ ulid: str | None = None,
158
+ conditions: Dict[str, Any] | None = None,
159
+ org_ulid_position: str = "organization_ulid",
160
+ role_permission: str = "delete",
161
+ token_settings: Dict[str, Any] | None = None
162
+ ) -> bool:
163
+ try:
164
+ if not ULID.from_str(ulid):
165
+ raise CustomException(
166
+ ErrorCode.VALIDATION_ERROR,
167
+ detail=ulid,
168
+ source_function=f"{self.__class__.__name__}.delete"
169
+ )
170
+
171
+ if not ulid and not conditions:
172
+ raise CustomException(
173
+ ErrorCode.INVALID_INPUT,
174
+ detail="Either 'ulid' or 'conditions' must be provided.",
175
+ source_function="database.update_entity"
176
+ )
177
+
178
+ # ulid로 조건 생성
179
+ if ulid:
180
+ conditions = {"ulid": ulid}
181
+
182
+ conditions["is_deleted"] = False
183
+
184
+ return await self.repository.delete(
185
+ conditions=conditions
186
+ )
187
+ except CustomException as e:
188
+ raise e
189
+ except Exception as e:
190
+ raise CustomException(
191
+ ErrorCode.INTERNAL_ERROR,
192
+ detail=str(e),
193
+ source_function=f"{self.__class__.__name__}.delete",
194
+ original_error=e
195
+ )
196
+
197
+ #########################
198
+ # 조회 및 검색 메서드 #
199
+ #########################
200
+ async def list(
201
+ self,
202
+ request: Request,
203
+ skip: int = 0,
204
+ limit: int = 100,
205
+ filters: List[Dict[str, Any]] | None = None,
206
+ org_ulid_position: str = "organization_ulid",
207
+ role_permission: str | None = None,
208
+ response_model: Any = None,
209
+ explicit_joins: Optional[List[Any]] = None,
210
+ loading_joins: Optional[List[Any]] = None,
211
+ order: Optional[str] = None,
212
+ token_settings: Dict[str, Any] | None = None
213
+ ) -> List[Dict[str, Any]]:
214
+ filters = list(filters) if filters is not None else []
215
+
216
+ # if role_permission:
217
+ # permission_result = await verify_role_permission(
218
+ # request=request,
219
+ # role_permission=role_permission,
220
+ # token_settings=token_settings,
221
+ # org_ulid_position=org_ulid_position
222
+ # )
223
+
224
+ # if permission_result and isinstance(permission_result, dict):
225
+ # filters.append(permission_result)
226
+
227
+ try:
228
+ if order is None:
229
+ order = "created_at|desc"
230
+
231
+ order_by = order.split("|")
232
+ order = [{"field": order_by[0], "direction": order_by[1]}]
233
+
234
+ entities = await self.repository.list(
235
+ skip=skip,
236
+ limit=limit,
237
+ filters=filters,
238
+ explicit_joins=explicit_joins,
239
+ loading_joins=loading_joins,
240
+ order=order
241
+ )
242
+ return [process_response(entity, response_model) for entity in entities]
243
+
244
+ except CustomException as e:
245
+ raise e
246
+ except Exception as e:
247
+ raise CustomException(
248
+ ErrorCode.INTERNAL_ERROR,
249
+ source_function=f"{self.__class__.__name__}.list",
250
+ original_error=e
251
+ )
252
+
253
+ async def get(
254
+ self,
255
+ request: Request,
256
+ ulid: str,
257
+ model_name: str | None = None,
258
+ response_model: Any = None,
259
+ conditions: Dict[str, Any] | None = None,
260
+ explicit_joins: Optional[List[Any]] = None,
261
+ loading_joins: Optional[List[Any]] = None,
262
+ org_ulid_position: str = "organization_ulid",
263
+ role_permission: str = "get",
264
+ token_settings: Dict[str, Any] | None = None
265
+ ):
266
+ try:
267
+ if not ulid and not conditions:
268
+ raise CustomException(
269
+ ErrorCode.INVALID_INPUT,
270
+ detail="Either 'ulid' or 'conditions' must be provided.",
271
+ source_function="database.update_entity"
272
+ )
273
+
274
+ # ulid로 조건 생성
275
+ if ulid:
276
+ if not ULID.from_str(ulid):
277
+ raise CustomException(
278
+ ErrorCode.VALIDATION_ERROR,
279
+ detail=ulid,
280
+ source_function=f"{self.__class__.__name__}.update"
281
+ )
282
+
283
+ conditions = {"ulid": ulid}
284
+
285
+ entity = await self.repository.get(
286
+ conditions=conditions,
287
+ explicit_joins=explicit_joins,
288
+ loading_joins=loading_joins
289
+ )
290
+ return process_response(entity, response_model)
291
+
292
+ except CustomException as e:
293
+ raise e
294
+ except Exception as e:
295
+ raise CustomException(
296
+ ErrorCode.INTERNAL_ERROR,
297
+ detail=str(e),
298
+ source_function=f"{self.__class__.__name__}.get",
299
+ original_error=e
300
+ )
301
+
@@ -1,6 +1,5 @@
1
1
  """설정 모듈."""
2
2
  from typing import Union
3
- from .database import DatabaseServiceManager
4
3
  from .exceptions import CustomException, ErrorCode
5
4
 
6
5
  class Settings:
@@ -11,13 +10,28 @@ class Settings:
11
10
  jwt_algorithm: str = "HS256",
12
11
  access_token_expire_minutes: int = 30,
13
12
  token_issuer: str = "ai-team",
14
- token_audience: str = "ai-team"
13
+ token_audience: str = "ai-team",
14
+ db_url: str = None,
15
+ db_echo: bool = False,
16
+ db_pool_size: int = 5,
17
+ db_max_overflow: int = 10,
18
+ db_pool_timeout: int = 30,
19
+ db_pool_recycle: int = 1800
15
20
  ):
21
+ # JWT 설정
16
22
  self.JWT_SECRET = jwt_secret
17
23
  self.JWT_ALGORITHM = jwt_algorithm
18
24
  self.ACCESS_TOKEN_EXPIRE_MINUTES = access_token_expire_minutes
19
25
  self.TOKEN_ISSUER = token_issuer
20
26
  self.TOKEN_AUDIENCE = token_audience
27
+
28
+ # 데이터베이스 설정
29
+ self.DB_URL = db_url
30
+ self.DB_ECHO = db_echo
31
+ self.DB_POOL_SIZE = db_pool_size
32
+ self.DB_MAX_OVERFLOW = db_max_overflow
33
+ self.DB_POOL_TIMEOUT = db_pool_timeout
34
+ self.DB_POOL_RECYCLE = db_pool_recycle
21
35
 
22
36
  _settings: Union[Settings, None] = None
23
37
 
@@ -55,18 +69,14 @@ async def init_settings(
55
69
  jwt_algorithm=jwt_algorithm,
56
70
  access_token_expire_minutes=access_token_expire_minutes,
57
71
  token_issuer=token_issuer,
58
- token_audience=token_audience
72
+ token_audience=token_audience,
73
+ db_url=db_url,
74
+ db_echo=db_echo,
75
+ db_pool_size=db_pool_size,
76
+ db_max_overflow=db_max_overflow,
77
+ db_pool_timeout=db_pool_timeout,
78
+ db_pool_recycle=db_pool_recycle
59
79
  )
60
-
61
- if db_url:
62
- await DatabaseServiceManager.get_instance(
63
- db_url=db_url,
64
- db_echo=db_echo,
65
- db_pool_size=db_pool_size,
66
- db_max_overflow=db_max_overflow,
67
- db_pool_timeout=db_pool_timeout,
68
- db_pool_recycle=db_pool_recycle
69
- )
70
80
 
71
81
  def get_settings() -> Settings:
72
82
  """현재 설정을 반환하는 함수