aiteamutils 0.2.51__tar.gz → 0.2.52__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.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
+ )