aiteamutils 0.2.41__tar.gz → 0.2.43__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.41
3
+ Version: 0.2.43
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,9 +1,10 @@
1
1
  from .base_model import Base
2
2
  from .database import (
3
3
  DatabaseService,
4
+ DatabaseServiceManager,
4
5
  get_db,
5
6
  get_database_service,
6
- get_database_session
7
+ lifespan
7
8
  )
8
9
  from .exceptions import (
9
10
  CustomException,
@@ -35,9 +36,10 @@ __all__ = [
35
36
 
36
37
  # Database
37
38
  "DatabaseService",
39
+ "DatabaseServiceManager",
38
40
  "get_db",
39
41
  "get_database_service",
40
- "get_database_session",
42
+ "lifespan",
41
43
 
42
44
  # Exceptions
43
45
  "CustomException",
@@ -1,6 +1,6 @@
1
1
  from typing import Any, Optional
2
2
  from redis.asyncio import Redis
3
- from app.config import settings
3
+ from .config import get_settings
4
4
 
5
5
  class Cache:
6
6
  _instance = None
@@ -15,6 +15,7 @@ class Cache:
15
15
  """
16
16
  if not cls._instance:
17
17
  cls._instance = cls()
18
+ settings = get_settings()
18
19
  cls._redis = Redis.from_url(settings.REDIS_URL, encoding="utf-8", decode_responses=True)
19
20
  return cls._instance
20
21
 
@@ -1,6 +1,6 @@
1
1
  """설정 모듈."""
2
2
  from typing import Union
3
- from .database import init_database_service
3
+ from .database import DatabaseServiceManager
4
4
  from .exceptions import CustomException, ErrorCode
5
5
 
6
6
  class Settings:
@@ -21,7 +21,7 @@ class Settings:
21
21
 
22
22
  _settings: Union[Settings, None] = None
23
23
 
24
- def init_settings(
24
+ async def init_settings(
25
25
  jwt_secret: str,
26
26
  jwt_algorithm: str = "HS256",
27
27
  access_token_expire_minutes: int = 30,
@@ -59,7 +59,7 @@ def init_settings(
59
59
  )
60
60
 
61
61
  if db_url:
62
- init_database_service(
62
+ await DatabaseServiceManager.get_instance(
63
63
  db_url=db_url,
64
64
  db_echo=db_echo,
65
65
  db_pool_size=db_pool_size,
@@ -1,3 +1,5 @@
1
+ import asyncio
2
+ import logging
1
3
  from typing import Any, Dict, Optional, Type, AsyncGenerator, TypeVar, List, Union
2
4
  from sqlalchemy import select, update, and_, Table
3
5
  from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, AsyncEngine
@@ -6,10 +8,9 @@ from sqlalchemy.exc import IntegrityError, SQLAlchemyError
6
8
  from sqlalchemy.pool import QueuePool
7
9
  from contextlib import asynccontextmanager
8
10
  from sqlalchemy import or_
9
- from fastapi import Request, Depends
11
+ from fastapi import Request, Depends, FastAPI
10
12
  from ulid import ULID
11
13
  from sqlalchemy.sql import Select
12
- import logging
13
14
 
14
15
  from .exceptions import ErrorCode, CustomException
15
16
  from .base_model import Base, BaseColumn
@@ -17,60 +18,89 @@ from .enums import ActivityType
17
18
 
18
19
  T = TypeVar("T", bound=BaseColumn)
19
20
 
20
- # 전역 데이터베이스 서비스 인스턴스
21
- _database_service: Union['DatabaseService', None] = None
22
-
23
- def get_database_service() -> 'DatabaseService':
24
- """DatabaseService 인스턴스를 반환하는 함수
21
+ class DatabaseServiceManager:
22
+ _instance: Optional['DatabaseService'] = None
23
+ _lock = asyncio.Lock()
25
24
 
26
- Returns:
27
- DatabaseService: DatabaseService 인스턴스
28
-
29
- Raises:
30
- CustomException: DatabaseService가 초기화되지 않은 경우
31
- """
32
- if _database_service is None:
33
- raise CustomException(
34
- ErrorCode.DB_CONNECTION_ERROR,
35
- detail="Database service is not initialized. Call init_database_service() first.",
36
- source_function="get_database_service"
37
- )
38
- return _database_service
25
+ @classmethod
26
+ async def get_instance(
27
+ cls,
28
+ db_url: str = None,
29
+ db_echo: bool = False,
30
+ db_pool_size: int = 5,
31
+ db_max_overflow: int = 10,
32
+ db_pool_timeout: int = 30,
33
+ db_pool_recycle: int = 1800,
34
+ **kwargs
35
+ ) -> 'DatabaseService':
36
+ """데이터베이스 서비스의 싱글톤 인스턴스를 반환합니다.
39
37
 
40
- async def get_db() -> AsyncGenerator[AsyncSession, None]:
41
- """데이터베이스 세션을 생성하고 반환하는 비동기 제너레이터.
42
-
43
- Yields:
44
- AsyncSession: 데이터베이스 세션
45
-
46
- Raises:
47
- CustomException: 세션 생성 실패 시
48
- """
49
- db_service = get_database_service()
50
- try:
51
- async with db_service.get_session() as session:
52
- yield session
53
- except Exception as e:
54
- raise CustomException(
55
- ErrorCode.DB_CONNECTION_ERROR,
56
- detail=f"Failed to get database session: {str(e)}",
57
- source_function="get_db",
58
- original_error=e
59
- )
60
- finally:
61
- if 'session' in locals():
62
- await session.close()
38
+ Args:
39
+ db_url (str, optional): 데이터베이스 URL
40
+ db_echo (bool, optional): SQL 로깅 여부
41
+ db_pool_size (int, optional): DB 커넥션 풀 크기
42
+ db_max_overflow (int, optional): 최대 초과 커넥션 수
43
+ db_pool_timeout (int, optional): 커넥션 풀 타임아웃
44
+ db_pool_recycle (int, optional): 커넥션 재활용 시간
63
45
 
64
- def get_database_session(db: AsyncSession = Depends(get_db)) -> 'DatabaseService':
65
- """DatabaseService 의존성
46
+ Returns:
47
+ DatabaseService: 데이터베이스 서비스 인스턴스
66
48
 
67
- Args:
68
- db (AsyncSession): 데이터베이스 세션
49
+ Raises:
50
+ CustomException: 데이터베이스 초기화 실패 시
51
+ """
52
+ async with cls._lock:
53
+ if not cls._instance:
54
+ if not db_url:
55
+ raise CustomException(
56
+ ErrorCode.DB_CONNECTION_ERROR,
57
+ detail="Database URL is required for initialization",
58
+ source_function="DatabaseServiceManager.get_instance"
59
+ )
60
+ try:
61
+ cls._instance = DatabaseService(
62
+ db_url=db_url,
63
+ db_echo=db_echo,
64
+ db_pool_size=db_pool_size,
65
+ db_max_overflow=db_max_overflow,
66
+ db_pool_timeout=db_pool_timeout,
67
+ db_pool_recycle=db_pool_recycle,
68
+ **kwargs
69
+ )
70
+ logging.info("Database service initialized successfully")
71
+ except Exception as e:
72
+ logging.error(f"Failed to initialize database service: {str(e)}")
73
+ raise CustomException(
74
+ ErrorCode.DB_CONNECTION_ERROR,
75
+ detail=f"Failed to initialize database service: {str(e)}",
76
+ source_function="DatabaseServiceManager.get_instance",
77
+ original_error=e
78
+ )
79
+ return cls._instance
69
80
 
70
- Returns:
71
- DatabaseService: DatabaseService 인스턴스
72
- """
73
- return DatabaseService(session=db)
81
+ @classmethod
82
+ async def cleanup(cls) -> None:
83
+ """데이터베이스 서비스 인스턴스를 정리합니다."""
84
+ async with cls._lock:
85
+ if cls._instance:
86
+ try:
87
+ if cls._instance.engine:
88
+ await cls._instance.engine.dispose()
89
+ cls._instance = None
90
+ logging.info("Database service cleaned up successfully")
91
+ except Exception as e:
92
+ logging.error(f"Error during database service cleanup: {str(e)}")
93
+ raise CustomException(
94
+ ErrorCode.DB_CONNECTION_ERROR,
95
+ detail=f"Failed to cleanup database service: {str(e)}",
96
+ source_function="DatabaseServiceManager.cleanup",
97
+ original_error=e
98
+ )
99
+
100
+ @classmethod
101
+ async def is_initialized(cls) -> bool:
102
+ """데이터베이스 서비스가 초기화되었는지 확인합니다."""
103
+ return cls._instance is not None
74
104
 
75
105
  class DatabaseService:
76
106
  def __init__(
@@ -122,10 +152,42 @@ class DatabaseService:
122
152
  else:
123
153
  raise CustomException(
124
154
  ErrorCode.DB_CONNECTION_ERROR,
125
- detail="db_url|session",
155
+ detail="Either db_url or session must be provided",
126
156
  source_function="DatabaseService.__init__"
127
157
  )
128
158
 
159
+ async def is_connected(self) -> bool:
160
+ """데이터베이스 연결 상태를 확인합니다."""
161
+ try:
162
+ if not self.engine:
163
+ return False
164
+ async with self.engine.connect() as conn:
165
+ await conn.execute(select(1))
166
+ return True
167
+ except Exception:
168
+ return False
169
+
170
+ async def retry_connection(self, retries: int = 3, delay: int = 2) -> bool:
171
+ """데이터베이스 연결을 재시도합니다.
172
+
173
+ Args:
174
+ retries (int): 재시도 횟수
175
+ delay (int): 재시도 간 대기 시간(초)
176
+
177
+ Returns:
178
+ bool: 연결 성공 여부
179
+ """
180
+ for attempt in range(retries):
181
+ try:
182
+ if await self.is_connected():
183
+ return True
184
+ await asyncio.sleep(delay)
185
+ except Exception as e:
186
+ logging.error(f"Connection retry attempt {attempt + 1} failed: {str(e)}")
187
+ if attempt == retries - 1:
188
+ return False
189
+ return False
190
+
129
191
  @asynccontextmanager
130
192
  async def get_session(self) -> AsyncGenerator[AsyncSession, None]:
131
193
  """데이터베이스 세션을 생성하고 반환하는 비동기 컨텍스트 매니저."""
@@ -934,60 +996,99 @@ class DatabaseService:
934
996
  original_error=e
935
997
  )
936
998
 
937
- def init_database_service(
938
- db_url: str,
939
- db_echo: bool = False,
940
- db_pool_size: int = 5,
941
- db_max_overflow: int = 10,
942
- db_pool_timeout: int = 30,
943
- db_pool_recycle: int = 1800
944
- ) -> DatabaseService:
945
- """데이터베이스 서비스를 초기화합니다.
999
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
1000
+ """데이터베이스 세션을 생성하고 반환하는 비동기 제너레이터.
946
1001
 
947
- Args:
948
- db_url (str): 데이터베이스 URL
949
- db_echo (bool, optional): SQL 로깅 여부
950
- db_pool_size (int, optional): DB 커넥션 풀 크기
951
- db_max_overflow (int, optional): 최대 초과 커넥션 수
952
- db_pool_timeout (int, optional): 커넥션 풀 타임아웃
953
- db_pool_recycle (int, optional): 커넥션 재활용 시간
954
-
955
- Returns:
956
- DatabaseService: 초기화된 데이터베이스 서비스 인스턴스
1002
+ Yields:
1003
+ AsyncSession: 데이터베이스 세션
957
1004
 
958
1005
  Raises:
959
- CustomException: 데이터베이스 초기화 실패 시
1006
+ CustomException: 세션 생성 실패 시
960
1007
  """
961
- try:
962
- global _database_service
963
- if _database_service is not None:
964
- logging.info("Database service already initialized")
965
- return _database_service
966
-
967
- logging.info(f"Initializing database service with URL: {db_url}")
968
- _database_service = DatabaseService(
969
- db_url=db_url,
970
- db_echo=db_echo,
971
- db_pool_size=db_pool_size,
972
- db_max_overflow=db_max_overflow,
973
- db_pool_timeout=db_pool_timeout,
974
- db_pool_recycle=db_pool_recycle
1008
+ if not await DatabaseServiceManager.is_initialized():
1009
+ raise CustomException(
1010
+ ErrorCode.DB_CONNECTION_ERROR,
1011
+ detail="Database service is not initialized",
1012
+ source_function="get_db"
975
1013
  )
976
-
977
- if not _database_service.engine:
978
- raise CustomException(
979
- ErrorCode.DB_CONNECTION_ERROR,
980
- detail="Database engine initialization failed",
981
- source_function="init_database_service"
982
- )
1014
+
1015
+ db_service = await DatabaseServiceManager.get_instance()
1016
+ if not db_service or not db_service.engine:
1017
+ raise CustomException(
1018
+ ErrorCode.DB_CONNECTION_ERROR,
1019
+ detail="Database service or engine is not properly initialized",
1020
+ source_function="get_db"
1021
+ )
1022
+
1023
+ try:
1024
+ async with db_service.get_session() as session:
1025
+ if session is None:
1026
+ raise CustomException(
1027
+ ErrorCode.DB_CONNECTION_ERROR,
1028
+ detail="Failed to create database session",
1029
+ source_function="get_db"
1030
+ )
983
1031
 
984
- logging.info("Database service initialized successfully")
985
- return _database_service
1032
+ # 세션이 유효한지 확인
1033
+ try:
1034
+ await session.execute(select(1))
1035
+ except Exception as e:
1036
+ raise CustomException(
1037
+ ErrorCode.DB_CONNECTION_ERROR,
1038
+ detail="Database session is not valid",
1039
+ source_function="get_db",
1040
+ original_error=e
1041
+ )
1042
+
1043
+ yield session
986
1044
  except Exception as e:
987
- logging.error(f"Failed to initialize database service: {str(e)}")
988
1045
  raise CustomException(
989
1046
  ErrorCode.DB_CONNECTION_ERROR,
990
- detail=f"Failed to initialize database service: {str(e)}",
991
- source_function="init_database_service",
1047
+ detail=f"Failed to get database session: {str(e)}",
1048
+ source_function="get_db",
992
1049
  original_error=e
993
- )
1050
+ )
1051
+ finally:
1052
+ if 'session' in locals() and session is not None:
1053
+ await session.close()
1054
+
1055
+ async def get_database_service() -> DatabaseService:
1056
+ """DatabaseService 의존성
1057
+
1058
+ Returns:
1059
+ DatabaseService: DatabaseService 인스턴스
1060
+
1061
+ Raises:
1062
+ CustomException: 데이터베이스 서비스가 초기화되지 않은 경우
1063
+ """
1064
+ if not await DatabaseServiceManager.is_initialized():
1065
+ raise CustomException(
1066
+ ErrorCode.DB_CONNECTION_ERROR,
1067
+ detail="Database service is not initialized",
1068
+ source_function="get_database_service"
1069
+ )
1070
+
1071
+ return await DatabaseServiceManager.get_instance()
1072
+
1073
+ @asynccontextmanager
1074
+ async def lifespan(app: FastAPI):
1075
+ """FastAPI 애플리케이션 라이프사이클 관리자.
1076
+
1077
+ Args:
1078
+ app (FastAPI): FastAPI 애플리케이션 인스턴스
1079
+ """
1080
+ try:
1081
+ # 시작 시 초기화
1082
+ if not await DatabaseServiceManager.is_initialized():
1083
+ await DatabaseServiceManager.get_instance(
1084
+ db_url=app.state.settings.DATABASE_URL,
1085
+ db_echo=app.state.settings.DB_ECHO,
1086
+ db_pool_size=app.state.settings.DB_POOL_SIZE,
1087
+ db_max_overflow=app.state.settings.DB_MAX_OVERFLOW,
1088
+ db_pool_timeout=app.state.settings.DB_POOL_TIMEOUT,
1089
+ db_pool_recycle=app.state.settings.DB_POOL_RECYCLE
1090
+ )
1091
+ yield
1092
+ finally:
1093
+ # 종료 시 정리
1094
+ await DatabaseServiceManager.cleanup()
@@ -5,87 +5,49 @@ from jose import JWTError, jwt
5
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
6
  import logging
7
7
 
8
- from .database import DatabaseService, get_database_service, get_db
8
+ from .database import DatabaseServiceManager, get_db, get_database_service
9
9
  from .exceptions import CustomException, ErrorCode
10
10
  from .config import get_settings
11
11
 
12
12
  class ServiceRegistry:
13
- """서비스 레지스트리를 관리하는 클래스"""
13
+ """서비스 레지스트리 클래스"""
14
14
  def __init__(self):
15
- self._services: Dict[str, Tuple[Type, Type]] = {}
16
- self._initialized = False
17
-
18
- def clear(self):
19
- """등록된 모든 서비스를 초기화합니다."""
20
- self._services.clear()
21
- self._initialized = False
22
-
23
- def register(self, name: str, repository_class: Type, service_class: Type):
24
- """서비스를 레지스트리에 등록
25
-
15
+ self._services: Dict[str, Tuple[Type[Any], Type[Any]]] = {}
16
+
17
+ def register(self, name: str, repository_class: Type[Any], service_class: Type[Any]) -> None:
18
+ """서비스를 등록합니다.
19
+
26
20
  Args:
27
21
  name (str): 서비스 이름
28
- repository_class (Type): Repository 클래스
29
- service_class (Type): Service 클래스
30
-
31
- Raises:
32
- CustomException: 이미 등록된 서비스인 경우
22
+ repository_class (Type[Any]): 레포지토리 클래스
23
+ service_class (Type[Any]): 서비스 클래스
33
24
  """
34
- try:
35
- if name in self._services:
36
- logging.warning(f"Service '{name}' is already registered. Skipping...")
37
- return
38
-
39
- if not repository_class or not service_class:
40
- raise CustomException(
41
- ErrorCode.INTERNAL_ERROR,
42
- detail=f"Invalid service classes for '{name}'",
43
- source_function="ServiceRegistry.register"
44
- )
45
-
46
- self._services[name] = (repository_class, service_class)
47
- logging.info(f"Service '{name}' registered successfully")
48
-
49
- except Exception as e:
50
- raise CustomException(
51
- ErrorCode.INTERNAL_ERROR,
52
- detail=f"Failed to register service '{name}': {str(e)}",
53
- source_function="ServiceRegistry.register",
54
- original_error=e
55
- )
56
-
57
- def get(self, name: str) -> Tuple[Type, Type]:
58
- """등록된 서비스를 조회
59
-
25
+ self._services[name] = (repository_class, service_class)
26
+
27
+ def get(self, name: str) -> Tuple[Type[Any], Type[Any]]:
28
+ """등록된 서비스를 가져옵니다.
29
+
60
30
  Args:
61
31
  name (str): 서비스 이름
62
-
32
+
63
33
  Returns:
64
- Tuple[Type, Type]: (Repository 클래스, Service 클래스) 튜플
65
-
34
+ Tuple[Type[Any], Type[Any]]: (레포지토리 클래스, 서비스 클래스)
35
+
66
36
  Raises:
67
37
  CustomException: 등록되지 않은 서비스인 경우
68
38
  """
69
39
  if name not in self._services:
70
40
  raise CustomException(
71
- ErrorCode.SERVICE_NOT_REGISTERED,
41
+ ErrorCode.NOT_FOUND,
72
42
  detail=f"Service '{name}' is not registered",
73
43
  source_function="ServiceRegistry.get"
74
44
  )
75
45
  return self._services[name]
76
-
77
- def is_initialized(self) -> bool:
78
- """서비스 레지스트리 초기화 여부를 반환합니다."""
79
- return self._initialized
80
-
81
- def set_initialized(self):
82
- """서비스 레지스트리를 초기화 상태로 설정합니다."""
83
- self._initialized = True
84
46
 
85
47
  # ServiceRegistry 초기화
86
48
  service_registry = ServiceRegistry()
87
49
 
88
- def get_service(name: str):
50
+ async def get_service(name: str):
89
51
  """등록된 서비스를 가져오는 의존성 함수
90
52
 
91
53
  Args:
@@ -97,11 +59,11 @@ def get_service(name: str):
97
59
  Raises:
98
60
  CustomException: 서비스 생성 실패 시
99
61
  """
100
- def _get_service(db_service: DatabaseService = Depends(get_database_service)):
62
+ async def _get_service(db_service = Depends(get_database_service)):
101
63
  try:
102
64
  repository_class, service_class = service_registry.get(name)
103
65
  repository = repository_class(db_service)
104
- return service_class(repository, db_service)
66
+ return service_class(repository)
105
67
  except CustomException as e:
106
68
  raise e
107
69
  except Exception as e:
@@ -116,13 +78,13 @@ def get_service(name: str):
116
78
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/users/token")
117
79
  async def get_current_user(
118
80
  token: str = Depends(oauth2_scheme),
119
- db_service: DatabaseService = Depends(get_database_service)
81
+ db_service: DatabaseServiceManager = Depends(get_database_service)
120
82
  ):
121
83
  """현재 사용자를 가져오는 의존성 함수
122
84
 
123
85
  Args:
124
86
  token (str): OAuth2 토큰
125
- db_service (DatabaseService): DatabaseService 객체
87
+ db_service (DatabaseServiceManager): DatabaseServiceManager 객체
126
88
 
127
89
  Returns:
128
90
  User: 현재 사용자
@@ -0,0 +1,2 @@
1
+ """버전 정보"""
2
+ __version__ = "0.2.43"
@@ -1,2 +0,0 @@
1
- """버전 정보"""
2
- __version__ = "0.2.41"
File without changes
File without changes
File without changes
File without changes