aiteamutils 0.2.51__tar.gz → 0.2.52__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.51
3
+ Version: 0.2.52
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,208 @@
1
+ """기본 레포지토리 모듈."""
2
+ from typing import TypeVar, Generic, Dict, Any, List, Optional, Type, Union
3
+ from sqlalchemy.orm import DeclarativeBase, Load
4
+ from sqlalchemy.exc import IntegrityError, SQLAlchemyError
5
+ from sqlalchemy import select, or_, and_
6
+ from .exceptions import CustomException, ErrorCode
7
+ from sqlalchemy.orm import joinedload
8
+ from sqlalchemy.sql import Select
9
+ from fastapi import Request
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
13
+
14
+ class BaseRepository(Generic[ModelType]):
15
+ ##################
16
+ # 1. 초기화 영역 #
17
+ ##################
18
+ def __init__(self, session: AsyncSession, model: Type[ModelType]):
19
+ """
20
+ Args:
21
+ session (AsyncSession): 데이터베이스 세션
22
+ model (Type[ModelType]): 모델 클래스
23
+ """
24
+ self.session = session
25
+ self.model = model
26
+
27
+ @property
28
+ def session(self):
29
+ """현재 세션을 반환합니다."""
30
+ if self._session is None:
31
+ raise CustomException(
32
+ ErrorCode.DB_CONNECTION_ERROR,
33
+ detail="Database session is not set",
34
+ source_function=f"{self.__class__.__name__}.session"
35
+ )
36
+ return self._session
37
+
38
+ @session.setter
39
+ def session(self, value):
40
+ """세션을 설정합니다."""
41
+ self._session = value
42
+
43
+ #######################
44
+ # 2. CRUD 작업 #
45
+ #######################
46
+ async def get(
47
+ self,
48
+ ulid: str
49
+ ) -> Optional[Dict[str, Any]]:
50
+ """ULID로 엔티티를 조회합니다."""
51
+ try:
52
+ stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
53
+ result = await self.session.execute(stmt)
54
+ entity = result.scalars().unique().first()
55
+
56
+ if not entity:
57
+ raise CustomException(
58
+ ErrorCode.DB_NO_RESULT,
59
+ detail=f"{self.model.__tablename__}|ulid|{ulid}",
60
+ source_function=f"{self.__class__.__name__}.get"
61
+ )
62
+ return entity
63
+ except CustomException as e:
64
+ e.detail = f"Repository error for {self.model.__tablename__}: {e.detail}"
65
+ e.source_function = f"{self.__class__.__name__}.get -> {e.source_function}"
66
+ raise e
67
+ except SQLAlchemyError as e:
68
+ raise CustomException(
69
+ ErrorCode.DB_QUERY_ERROR,
70
+ detail=f"Database error in {self.model.__tablename__}: {str(e)}",
71
+ source_function=f"{self.__class__.__name__}.get",
72
+ original_error=e
73
+ )
74
+
75
+ async def list(
76
+ self,
77
+ skip: int = 0,
78
+ limit: int = 100,
79
+ filters: Dict[str, Any] | None = None,
80
+ search_params: Dict[str, Any] | None = None
81
+ ) -> List[Any]:
82
+ """엔티티 목록을 조회합니다."""
83
+ try:
84
+ stmt = select(self.model).where(self.model.is_deleted == False)
85
+
86
+ # 필터 적용
87
+ if filters:
88
+ stmt = self._apply_filters(stmt, filters)
89
+
90
+ # 검색 적용
91
+ if search_params:
92
+ stmt = self._apply_search_params(stmt, search_params)
93
+
94
+ # 페이지네이션 적용
95
+ stmt = stmt.limit(limit).offset(skip)
96
+
97
+ result = await self.session.execute(stmt)
98
+ return result.scalars().unique().all()
99
+
100
+ except SQLAlchemyError as e:
101
+ raise CustomException(
102
+ ErrorCode.DB_QUERY_ERROR,
103
+ detail=f"Unexpected repository list error in {self.model.__tablename__}: {str(e)}",
104
+ source_function=f"{self.__class__.__name__}.list",
105
+ original_error=e,
106
+ )
107
+
108
+ async def create(self, data: Dict[str, Any]) -> ModelType:
109
+ """새로운 엔티티를 생성합니다."""
110
+ try:
111
+ entity = self.model(**data)
112
+ self.session.add(entity)
113
+ await self.session.flush()
114
+ await self.session.refresh(entity)
115
+ return entity
116
+ except IntegrityError as e:
117
+ await self.session.rollback()
118
+ self._handle_integrity_error(e, "create", data)
119
+ except SQLAlchemyError as e:
120
+ await self.session.rollback()
121
+ raise CustomException(
122
+ ErrorCode.DB_CREATE_ERROR,
123
+ detail=f"Database create error in {self.model.__tablename__}: {str(e)}",
124
+ source_function=f"{self.__class__.__name__}.create",
125
+ original_error=e
126
+ )
127
+
128
+ async def update(self, ulid: str, data: Dict[str, Any]) -> Optional[ModelType]:
129
+ """기존 엔티티를 수정합니다."""
130
+ try:
131
+ stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
132
+ result = await self.session.execute(stmt)
133
+ entity = result.scalars().first()
134
+
135
+ if not entity:
136
+ raise CustomException(
137
+ ErrorCode.DB_NO_RESULT,
138
+ detail=f"{self.model.__tablename__}|ulid|{ulid}",
139
+ source_function=f"{self.__class__.__name__}.update"
140
+ )
141
+
142
+ for key, value in data.items():
143
+ setattr(entity, key, value)
144
+
145
+ await self.session.flush()
146
+ await self.session.refresh(entity)
147
+ return entity
148
+
149
+ except IntegrityError as e:
150
+ await self.session.rollback()
151
+ self._handle_integrity_error(e, "update", data)
152
+ except SQLAlchemyError as e:
153
+ await self.session.rollback()
154
+ raise CustomException(
155
+ ErrorCode.DB_UPDATE_ERROR,
156
+ detail=f"Database update error in {self.model.__tablename__}: {str(e)}",
157
+ source_function=f"{self.__class__.__name__}.update",
158
+ original_error=e
159
+ )
160
+
161
+ async def delete(self, ulid: str) -> bool:
162
+ """엔티티를 소프트 삭제합니다."""
163
+ try:
164
+ stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
165
+ result = await self.session.execute(stmt)
166
+ entity = result.scalars().first()
167
+
168
+ if not entity:
169
+ raise CustomException(
170
+ ErrorCode.DB_NO_RESULT,
171
+ detail=f"{self.model.__tablename__}|ulid|{ulid}",
172
+ source_function=f"{self.__class__.__name__}.delete"
173
+ )
174
+
175
+ entity.is_deleted = True
176
+ await self.session.flush()
177
+ return True
178
+
179
+ except SQLAlchemyError as e:
180
+ await self.session.rollback()
181
+ raise CustomException(
182
+ ErrorCode.DB_DELETE_ERROR,
183
+ detail=f"Database delete error in {self.model.__tablename__}: {str(e)}",
184
+ source_function=f"{self.__class__.__name__}.delete",
185
+ original_error=e
186
+ )
187
+
188
+ async def real_delete(self, ulid: str) -> bool:
189
+ """엔티티를 실제로 삭제합니다."""
190
+ try:
191
+ stmt = select(self.model).filter_by(ulid=ulid)
192
+ result = await self.session.execute(stmt)
193
+ entity = result.scalars().first()
194
+
195
+ if entity:
196
+ await self.session.delete(entity)
197
+ await self.session.flush()
198
+ return True
199
+ return False
200
+
201
+ except SQLAlchemyError as e:
202
+ await self.session.rollback()
203
+ raise CustomException(
204
+ ErrorCode.DB_DELETE_ERROR,
205
+ detail=f"Database real delete error in {self.model.__tablename__}: {str(e)}",
206
+ source_function=f"{self.__class__.__name__}.real_delete",
207
+ original_error=e
208
+ )
@@ -9,6 +9,7 @@ from .base_repository import BaseRepository
9
9
  from .security import hash_password
10
10
  from fastapi import Request
11
11
  from ulid import ULID
12
+ from sqlalchemy import select
12
13
 
13
14
  ModelType = TypeVar("ModelType", bound=DeclarativeBase)
14
15
 
@@ -30,11 +31,29 @@ class BaseService(Generic[ModelType]):
30
31
  self.repository = repository
31
32
  self.model = repository.model
32
33
  self.additional_models = additional_models or {}
33
- self.db_service = repository.db_service
34
+ self._session = None
34
35
  self.searchable_fields = {
35
36
  "name": {"type": "text", "description": "이름"},
36
37
  "organization_ulid": {"type": "exact", "description": "조직 ID"}
37
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
38
57
 
39
58
  #########################
40
59
  # 2. 이벤트 처리 메서드 #
@@ -448,18 +467,7 @@ class BaseService(Generic[ModelType]):
448
467
  )
449
468
 
450
469
  async def delete(self, ulid: str, model_name: str = None) -> bool:
451
- """엔티티를 소프트 삭제합니다 (is_deleted = True).
452
-
453
- Args:
454
- ulid (str): 삭제할 엔티티의 ULID
455
- model_name (str, optional): 삭제할 모델 이름. Defaults to None.
456
-
457
- Returns:
458
- bool: 삭제 성공 여부
459
-
460
- Raises:
461
- CustomException: 데이터베이스 작업 중 오류 발생 시
462
- """
470
+ """엔티티를 소프트 삭제합니다 (is_deleted = True)."""
463
471
  try:
464
472
  if model_name:
465
473
  if model_name not in self.additional_models:
@@ -468,23 +476,24 @@ class BaseService(Generic[ModelType]):
468
476
  detail=f"Model {model_name} not registered",
469
477
  source_function=f"{self.__class__.__name__}.delete"
470
478
  )
471
- entity = await self.db_service.soft_delete_entity(self.additional_models[model_name], ulid)
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
+
472
484
  if not entity:
473
485
  raise CustomException(
474
486
  ErrorCode.NOT_FOUND,
475
487
  detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
476
488
  source_function=f"{self.__class__.__name__}.delete"
477
489
  )
490
+
491
+ entity.is_deleted = True
492
+ await self.session.flush()
478
493
  return True
479
494
 
480
- entity = await self.repository.delete(ulid)
481
- if not entity:
482
- raise CustomException(
483
- ErrorCode.NOT_FOUND,
484
- detail=f"{self.model.__tablename__}|ulid|{ulid}",
485
- source_function=f"{self.__class__.__name__}.delete"
486
- )
487
- return True
495
+ return await self.repository.delete(ulid)
496
+
488
497
  except CustomException as e:
489
498
  raise e
490
499
  except Exception as e:
@@ -549,20 +558,7 @@ class BaseService(Generic[ModelType]):
549
558
  request: Request | None = None,
550
559
  response_model: Any = None
551
560
  ) -> List[Dict[str, Any]]:
552
- """엔티티 목록을 조회합니다.
553
-
554
- Args:
555
- skip (int, optional): 건너뛸 레코드 수. Defaults to 0.
556
- limit (int, optional): 조회할 최대 레코드 수. Defaults to 100.
557
- filters (Dict[str, Any] | None, optional): 필터링 조건. Defaults to None.
558
- search_params (Dict[str, Any] | None, optional): 검색 파라미터. Defaults to None.
559
- model_name (str | None, optional): 조회할 모델 이름. Defaults to None.
560
- request (Request | None, optional): 요청 객체. Defaults to None.
561
- response_model (Any, optional): 응답 스키마. Defaults to None.
562
-
563
- Returns:
564
- List[Dict[str, Any]]: 엔티티 목록
565
- """
561
+ """엔티티 목록을 조회합니다."""
566
562
  try:
567
563
  if model_name:
568
564
  if model_name not in self.additional_models:
@@ -571,21 +567,28 @@ class BaseService(Generic[ModelType]):
571
567
  detail=f"Model {model_name} not registered",
572
568
  source_function=f"{self.__class__.__name__}.list"
573
569
  )
574
- entities = await self.db_service.list_entities(
575
- self.additional_models[model_name],
576
- skip=skip,
577
- limit=limit,
578
- filters=filters
570
+
571
+ stmt = select(self.additional_models[model_name]).where(
572
+ self.additional_models[model_name].is_deleted == False
579
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
+
580
584
  return [self._process_response(entity, response_model) for entity in entities]
581
585
 
582
- entities = await self.repository.list(
586
+ return await self.repository.list(
583
587
  skip=skip,
584
588
  limit=limit,
585
589
  filters=filters,
586
590
  search_params=search_params
587
591
  )
588
- return [self._process_response(entity, response_model) for entity in entities]
589
592
 
590
593
  except CustomException as e:
591
594
  e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
@@ -764,11 +764,12 @@ class DatabaseService:
764
764
  processed_data = self.preprocess_data(model, log_data)
765
765
  entity = model(**processed_data)
766
766
 
767
- # 로그 엔티티 저장
768
- self.db.add(entity)
769
- await self.db.flush()
770
-
771
- return entity
767
+ async with self.get_session() as session:
768
+ # 로그 엔티티 저장
769
+ session.add(entity)
770
+ await session.flush()
771
+ await session.commit()
772
+ return entity
772
773
 
773
774
  except Exception as e:
774
775
  logging.error(f"Failed to create log: {str(e)}")
@@ -996,6 +997,24 @@ class DatabaseService:
996
997
  original_error=e
997
998
  )
998
999
 
1000
+ async def create_session(self) -> AsyncSession:
1001
+ """새로운 데이터베이스 세션을 생성합니다.
1002
+
1003
+ Returns:
1004
+ AsyncSession: 생성된 세션
1005
+
1006
+ Raises:
1007
+ CustomException: 세션 생성 실패 시
1008
+ """
1009
+ if self.session_factory is None:
1010
+ raise CustomException(
1011
+ ErrorCode.DB_CONNECTION_ERROR,
1012
+ detail="session_factory is not initialized",
1013
+ source_function="DatabaseService.create_session"
1014
+ )
1015
+
1016
+ return self.session_factory()
1017
+
999
1018
  async def get_db() -> AsyncGenerator[AsyncSession, None]:
1000
1019
  """데이터베이스 세션 의존성
1001
1020
 
@@ -0,0 +1,213 @@
1
+ """의존성 관리 모듈."""
2
+ from typing import Type, TypeVar, Dict, Any, Optional, Callable, List, AsyncGenerator
3
+ from fastapi import Request, Depends, HTTPException, status
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+ from jose import jwt, JWTError
6
+ import logging
7
+
8
+ from .exceptions import CustomException, ErrorCode
9
+ from .config import get_settings
10
+ from .base_service import BaseService
11
+ from .base_repository import BaseRepository
12
+ from .database import db_manager
13
+
14
+ T = TypeVar("T", bound=BaseService)
15
+ R = TypeVar("R", bound=BaseRepository)
16
+
17
+ _service_registry: Dict[str, Dict[str, Any]] = {}
18
+
19
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
20
+ """데이터베이스 세션을 반환합니다.
21
+
22
+ Yields:
23
+ AsyncSession: 데이터베이스 세션
24
+
25
+ Raises:
26
+ CustomException: 세션 생성 실패 시
27
+ """
28
+ try:
29
+ async with db_manager.get_session() as session:
30
+ yield session
31
+ except Exception as e:
32
+ raise CustomException(
33
+ ErrorCode.DATABASE_ERROR,
34
+ detail=str(e),
35
+ source_function="dependencies.get_db",
36
+ original_error=e
37
+ )
38
+
39
+ def register_service(
40
+ service_class: Type[T],
41
+ repository_class: Optional[Type[R]] = None,
42
+ **kwargs
43
+ ) -> None:
44
+ """서비스를 등록합니다.
45
+
46
+ Args:
47
+ service_class: 서비스 클래스
48
+ repository_class: 저장소 클래스 (선택)
49
+ **kwargs: 추가 의존성
50
+ """
51
+ service_name = service_class.__name__
52
+ _service_registry[service_name] = {
53
+ "service_class": service_class,
54
+ "repository_class": repository_class,
55
+ "dependencies": kwargs
56
+ }
57
+
58
+ async def _get_service(
59
+ service_name: str,
60
+ session: AsyncSession,
61
+ request: Request
62
+ ) -> BaseService:
63
+ """서비스 인스턴스를 생성합니다.
64
+
65
+ Args:
66
+ service_name: 서비스 이름
67
+ session: 데이터베이스 세션
68
+ request: FastAPI 요청 객체
69
+
70
+ Returns:
71
+ BaseService: 서비스 인스턴스
72
+
73
+ Raises:
74
+ CustomException: 서비스 생성 실패 시
75
+ """
76
+ try:
77
+ service_info = _service_registry.get(service_name)
78
+ if not service_info:
79
+ raise CustomException(
80
+ ErrorCode.SERVICE_NOT_FOUND,
81
+ detail=service_name,
82
+ source_function="dependencies._get_service"
83
+ )
84
+
85
+ service_class = service_info["service_class"]
86
+ repository_class = service_info["repository_class"]
87
+ dependencies = service_info["dependencies"]
88
+
89
+ # 저장소 인스턴스 생성
90
+ repository = None
91
+ if repository_class:
92
+ repository = repository_class(session=session)
93
+
94
+ # 서비스 인스턴스 생성
95
+ service = service_class(
96
+ repository=repository,
97
+ session=session,
98
+ request=request,
99
+ **dependencies
100
+ )
101
+
102
+ return service
103
+
104
+ except CustomException as e:
105
+ raise e
106
+ except Exception as e:
107
+ raise CustomException(
108
+ ErrorCode.INTERNAL_ERROR,
109
+ detail=str(e),
110
+ source_function="dependencies._get_service",
111
+ original_error=e
112
+ )
113
+
114
+ def get_service(service_name: str) -> Callable:
115
+ """서비스 의존성을 반환합니다.
116
+
117
+ Args:
118
+ service_name: 서비스 이름
119
+
120
+ Returns:
121
+ Callable: 서비스 의존성 함수
122
+ """
123
+ async def _get_service_dependency(
124
+ request: Request,
125
+ session: AsyncSession = Depends(get_db)
126
+ ) -> BaseService:
127
+ return await _get_service(service_name, session, request)
128
+ return _get_service_dependency
129
+
130
+ async def get_current_user(
131
+ request: Request,
132
+ session: AsyncSession = Depends(get_db),
133
+ auth_service: BaseService = Depends(get_service("AuthService"))
134
+ ) -> Dict[str, Any]:
135
+ """현재 사용자 정보를 반환합니다.
136
+
137
+ Args:
138
+ request: FastAPI 요청 객체
139
+ session: 데이터베이스 세션
140
+ auth_service: 인증 서비스
141
+
142
+ Returns:
143
+ Dict[str, Any]: 사용자 정보
144
+
145
+ Raises:
146
+ HTTPException: 인증 실패 시
147
+ """
148
+ settings = get_settings()
149
+
150
+ # Authorization 헤더 검증
151
+ authorization = request.headers.get("Authorization")
152
+ if not authorization or not authorization.startswith("Bearer "):
153
+ raise HTTPException(
154
+ status_code=status.HTTP_401_UNAUTHORIZED,
155
+ detail="Not authenticated",
156
+ headers={"WWW-Authenticate": "Bearer"}
157
+ )
158
+
159
+ token = authorization.split(" ")[1]
160
+
161
+ try:
162
+ # JWT 토큰 디코딩
163
+ payload = jwt.decode(
164
+ token,
165
+ settings.jwt_secret,
166
+ algorithms=[settings.jwt_algorithm],
167
+ issuer=settings.token_issuer,
168
+ audience=settings.token_audience
169
+ )
170
+
171
+ # 토큰 타입 검증
172
+ token_type = payload.get("token_type")
173
+ if token_type != "access":
174
+ raise HTTPException(
175
+ status_code=status.HTTP_401_UNAUTHORIZED,
176
+ detail="Invalid token type",
177
+ headers={"WWW-Authenticate": "Bearer"}
178
+ )
179
+
180
+ # 사용자 조회
181
+ user_ulid = payload.get("user_ulid")
182
+ if not user_ulid:
183
+ raise HTTPException(
184
+ status_code=status.HTTP_401_UNAUTHORIZED,
185
+ detail="Invalid token payload",
186
+ headers={"WWW-Authenticate": "Bearer"}
187
+ )
188
+
189
+ user = await auth_service.get_by_ulid(user_ulid)
190
+ if not user:
191
+ raise HTTPException(
192
+ status_code=status.HTTP_401_UNAUTHORIZED,
193
+ detail="User not found",
194
+ headers={"WWW-Authenticate": "Bearer"}
195
+ )
196
+
197
+ return user
198
+
199
+ except JWTError as e:
200
+ raise HTTPException(
201
+ status_code=status.HTTP_401_UNAUTHORIZED,
202
+ detail=str(e),
203
+ headers={"WWW-Authenticate": "Bearer"}
204
+ )
205
+ except HTTPException as e:
206
+ raise e
207
+ except Exception as e:
208
+ logging.error(f"Error in get_current_user: {str(e)}")
209
+ raise HTTPException(
210
+ status_code=status.HTTP_401_UNAUTHORIZED,
211
+ detail="Authentication failed",
212
+ headers={"WWW-Authenticate": "Bearer"}
213
+ )