aiteamutils 0.2.72__py3-none-any.whl → 0.2.75__py3-none-any.whl
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.
- aiteamutils/base_model.py +9 -2
- aiteamutils/base_repository.py +83 -5
- aiteamutils/base_service.py +201 -21
- aiteamutils/database.py +393 -54
- aiteamutils/exceptions.py +6 -2
- aiteamutils/security.py +28 -23
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.72.dist-info → aiteamutils-0.2.75.dist-info}/METADATA +1 -1
- aiteamutils-0.2.75.dist-info/RECORD +15 -0
- aiteamutils-0.2.72.dist-info/RECORD +0 -15
- {aiteamutils-0.2.72.dist-info → aiteamutils-0.2.75.dist-info}/WHEEL +0 -0
    
        aiteamutils/base_model.py
    CHANGED
    
    | @@ -23,10 +23,17 @@ class BaseColumn(Base): | |
| 23 23 | 
             
                    doc="ULID",
         | 
| 24 24 | 
             
                    nullable=False
         | 
| 25 25 | 
             
                )
         | 
| 26 | 
            -
                created_at: Mapped[datetime] = mapped_column( | 
| 26 | 
            +
                created_at: Mapped[datetime] = mapped_column(
         | 
| 27 | 
            +
                    default=datetime.utcnow,
         | 
| 28 | 
            +
                    index=True
         | 
| 29 | 
            +
                )
         | 
| 27 30 | 
             
                updated_at: Mapped[datetime] = mapped_column(
         | 
| 28 31 | 
             
                    default=datetime.utcnow,
         | 
| 29 | 
            -
                    onupdate=datetime.utcnow
         | 
| 32 | 
            +
                    onupdate=datetime.utcnow,
         | 
| 33 | 
            +
                    index=True
         | 
| 34 | 
            +
                )
         | 
| 35 | 
            +
                deleted_at: Mapped[datetime] = mapped_column(
         | 
| 36 | 
            +
                    default=None,
         | 
| 30 37 | 
             
                )
         | 
| 31 38 | 
             
                is_deleted: Mapped[bool] = mapped_column(
         | 
| 32 39 | 
             
                    default=False,
         | 
    
        aiteamutils/base_repository.py
    CHANGED
    
    | @@ -7,7 +7,13 @@ from sqlalchemy import select | |
| 7 7 |  | 
| 8 8 | 
             
            #패키지 라이브러리
         | 
| 9 9 | 
             
            from .exceptions import ErrorCode, CustomException
         | 
| 10 | 
            -
            from .database import  | 
| 10 | 
            +
            from .database import (
         | 
| 11 | 
            +
                list_entities,
         | 
| 12 | 
            +
                get_entity,
         | 
| 13 | 
            +
                create_entity,
         | 
| 14 | 
            +
                update_entity,
         | 
| 15 | 
            +
                delete_entity
         | 
| 16 | 
            +
            )
         | 
| 11 17 |  | 
| 12 18 | 
             
            ModelType = TypeVar("ModelType", bound=DeclarativeBase)
         | 
| 13 19 |  | 
| @@ -29,13 +35,62 @@ class BaseRepository(Generic[ModelType]): | |
| 29 35 | 
             
                            source_function=f"{self.__class__.__name__}.session"
         | 
| 30 36 | 
             
                        )
         | 
| 31 37 | 
             
                    self._session = value
         | 
| 38 | 
            +
               
         | 
| 39 | 
            +
                #######################
         | 
| 40 | 
            +
                # 입력 및 수정, 삭제 #
         | 
| 41 | 
            +
                ####################### 
         | 
| 42 | 
            +
                async def create(self, entity_data: Dict[str, Any]) -> ModelType:
         | 
| 43 | 
            +
                    try:
         | 
| 44 | 
            +
                        return await create_entity(
         | 
| 45 | 
            +
                            session=self.session,
         | 
| 46 | 
            +
                            model=self.model,
         | 
| 47 | 
            +
                            entity_data=entity_data
         | 
| 48 | 
            +
                        )
         | 
| 49 | 
            +
                    except CustomException as e:
         | 
| 50 | 
            +
                        raise e
         | 
| 51 | 
            +
                    except Exception as e:
         | 
| 52 | 
            +
                        raise CustomException(
         | 
| 53 | 
            +
                            ErrorCode.INTERNAL_ERROR,
         | 
| 54 | 
            +
                            detail=str(e),
         | 
| 55 | 
            +
                            source_function=f"{self.__class__.__name__}.create",
         | 
| 56 | 
            +
                            original_error=e
         | 
| 57 | 
            +
                        )
         | 
| 58 | 
            +
                    
         | 
| 59 | 
            +
                async def update(
         | 
| 60 | 
            +
                    self,
         | 
| 61 | 
            +
                    update_data: Dict[str, Any],
         | 
| 62 | 
            +
                    conditions: Dict[str, Any]
         | 
| 63 | 
            +
                ) -> ModelType:
         | 
| 64 | 
            +
                    try:
         | 
| 65 | 
            +
                        return await update_entity(
         | 
| 66 | 
            +
                            session=self.session,
         | 
| 67 | 
            +
                            model=self.model,
         | 
| 68 | 
            +
                            update_data=update_data,
         | 
| 69 | 
            +
                            conditions=conditions
         | 
| 70 | 
            +
                        )
         | 
| 71 | 
            +
                    except CustomException as e:
         | 
| 72 | 
            +
                        raise e
         | 
| 73 | 
            +
                    
         | 
| 74 | 
            +
                async def delete(
         | 
| 75 | 
            +
                    self,
         | 
| 76 | 
            +
                    conditions: Dict[str, Any]
         | 
| 77 | 
            +
                ) -> None:
         | 
| 78 | 
            +
                    await delete_entity(
         | 
| 79 | 
            +
                        session=self.session,
         | 
| 80 | 
            +
                        model=self.model,
         | 
| 81 | 
            +
                        conditions=conditions
         | 
| 82 | 
            +
                    )
         | 
| 32 83 |  | 
| 84 | 
            +
                #########################
         | 
| 85 | 
            +
                # 조회 및 검색 메서드 #
         | 
| 86 | 
            +
                #########################
         | 
| 33 87 | 
             
                async def list(
         | 
| 34 88 | 
             
                    self,
         | 
| 35 89 | 
             
                    skip: int = 0,
         | 
| 36 90 | 
             
                    limit: int = 100,
         | 
| 37 91 | 
             
                    filters: Optional[Dict[str, Any]] = None,
         | 
| 38 | 
            -
                     | 
| 92 | 
            +
                    explicit_joins: Optional[List[Any]] = None,
         | 
| 93 | 
            +
                    loading_joins: Optional[List[Any]] = None
         | 
| 39 94 | 
             
                ) -> List[ModelType]:
         | 
| 40 95 | 
             
                    """
         | 
| 41 96 | 
             
                    엔티티 목록 조회.
         | 
| @@ -48,11 +103,10 @@ class BaseRepository(Generic[ModelType]): | |
| 48 103 | 
             
                            skip=skip,
         | 
| 49 104 | 
             
                            limit=limit,
         | 
| 50 105 | 
             
                            filters=filters,
         | 
| 51 | 
            -
                             | 
| 106 | 
            +
                            explicit_joins=explicit_joins,
         | 
| 107 | 
            +
                            loading_joins=loading_joins
         | 
| 52 108 | 
             
                        )
         | 
| 53 109 | 
             
                    except CustomException as e:
         | 
| 54 | 
            -
                        e.detail = f"Repository list error for {self.model.__tablename__}: {e.detail}"
         | 
| 55 | 
            -
                        e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
         | 
| 56 110 | 
             
                        raise e
         | 
| 57 111 | 
             
                    except Exception as e:
         | 
| 58 112 | 
             
                        raise CustomException(
         | 
| @@ -61,3 +115,27 @@ class BaseRepository(Generic[ModelType]): | |
| 61 115 | 
             
                            source_function=f"{self.__class__.__name__}.list",
         | 
| 62 116 | 
             
                            original_error=e
         | 
| 63 117 | 
             
                        )
         | 
| 118 | 
            +
                    
         | 
| 119 | 
            +
                async def get(
         | 
| 120 | 
            +
                    self,
         | 
| 121 | 
            +
                    conditions: Dict[str, Any] | None = None,
         | 
| 122 | 
            +
                    explicit_joins: Optional[List[Any]] = None,
         | 
| 123 | 
            +
                    loading_joins: Optional[List[Any]] = None
         | 
| 124 | 
            +
                ) -> ModelType:
         | 
| 125 | 
            +
                    try:
         | 
| 126 | 
            +
                        return await get_entity(
         | 
| 127 | 
            +
                            session=self.session,
         | 
| 128 | 
            +
                            model=self.model,
         | 
| 129 | 
            +
                            conditions=conditions,
         | 
| 130 | 
            +
                            explicit_joins=explicit_joins,
         | 
| 131 | 
            +
                            loading_joins=loading_joins
         | 
| 132 | 
            +
                        )
         | 
| 133 | 
            +
                    except CustomException as e:
         | 
| 134 | 
            +
                        raise e
         | 
| 135 | 
            +
                    except Exception as e:
         | 
| 136 | 
            +
                        raise CustomException(
         | 
| 137 | 
            +
                            ErrorCode.INTERNAL_ERROR,
         | 
| 138 | 
            +
                            detail=str(e),
         | 
| 139 | 
            +
                            source_function=f"{self.__class__.__name__}.get",
         | 
| 140 | 
            +
                            original_error=e
         | 
| 141 | 
            +
                        )
         | 
    
        aiteamutils/base_service.py
    CHANGED
    
    | @@ -1,18 +1,19 @@ | |
| 1 1 | 
             
            #기본 라이브러리
         | 
| 2 2 | 
             
            from fastapi import Request
         | 
| 3 | 
            -
            from typing import TypeVar, Generic, Type, Dict, Any, Union, List
         | 
| 3 | 
            +
            from typing import TypeVar, Generic, Type, Dict, Any, Union, List, Optional
         | 
| 4 4 | 
             
            from sqlalchemy.orm import DeclarativeBase
         | 
| 5 5 | 
             
            from sqlalchemy.ext.asyncio import AsyncSession
         | 
| 6 6 | 
             
            from datetime import datetime
         | 
| 7 | 
            +
            from ulid import ULID
         | 
| 7 8 |  | 
| 8 9 | 
             
            #패키지 라이브러리
         | 
| 9 10 | 
             
            from .exceptions import ErrorCode, CustomException
         | 
| 10 11 | 
             
            from .base_repository import BaseRepository
         | 
| 11 12 | 
             
            from .database import (
         | 
| 12 13 | 
             
                process_response,
         | 
| 13 | 
            -
                 | 
| 14 | 
            +
                validate_unique_fields
         | 
| 14 15 | 
             
            )
         | 
| 15 | 
            -
             | 
| 16 | 
            +
            from .security import hash_password
         | 
| 16 17 | 
             
            ModelType = TypeVar("ModelType", bound=DeclarativeBase)
         | 
| 17 18 |  | 
| 18 19 | 
             
            class BaseService(Generic[ModelType]):
         | 
| @@ -30,27 +31,151 @@ class BaseService(Generic[ModelType]): | |
| 30 31 | 
             
                    self.repository = repository
         | 
| 31 32 | 
             
                    self.db_session = db_session
         | 
| 32 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 | 
            +
                        # 고유 검사 수행
         | 
| 50 | 
            +
                        if unique_check:
         | 
| 51 | 
            +
                            await validate_unique_fields(self.db_session, unique_check, find_value=True)
         | 
| 52 | 
            +
                        # 외래 키 검사 수행
         | 
| 53 | 
            +
                        if fk_check:
         | 
| 54 | 
            +
                            await validate_unique_fields(self.db_session, fk_check, find_value=False)
         | 
| 55 | 
            +
                        # 비밀번호가 있으면 해시 처리
         | 
| 56 | 
            +
                        if "password" in entity_data:
         | 
| 57 | 
            +
                            entity_data["password"] = hash_password(entity_data["password"])
         | 
| 58 | 
            +
                        # 제외할 엔티티가 있으면 제외
         | 
| 59 | 
            +
                        if exclude_entities:
         | 
| 60 | 
            +
                            entity_data = {k: v for k, v in entity_data.items() if k not in exclude_entities}
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                        # repository의 create 메서드를 트랜잭션 내에서 실행
         | 
| 63 | 
            +
                        return await self.repository.create(entity_data)
         | 
| 64 | 
            +
                    except CustomException as e:
         | 
| 65 | 
            +
                        raise e
         | 
| 66 | 
            +
                    except Exception as e:
         | 
| 67 | 
            +
                        # 다른 예외 처리
         | 
| 68 | 
            +
                        raise CustomException(
         | 
| 69 | 
            +
                            ErrorCode.INTERNAL_ERROR,
         | 
| 70 | 
            +
                            detail=str(e),
         | 
| 71 | 
            +
                            source_function=f"{self.__class__.__name__}.create",
         | 
| 72 | 
            +
                            original_error=e
         | 
| 73 | 
            +
                        )
         | 
| 74 | 
            +
                    
         | 
| 75 | 
            +
                async def update(
         | 
| 76 | 
            +
                    self,
         | 
| 77 | 
            +
                    ulid: str | None = None,
         | 
| 78 | 
            +
                    update_data: Dict[str, Any] | None = None,
         | 
| 79 | 
            +
                    conditions: Dict[str, Any] | None = None,
         | 
| 80 | 
            +
                    unique_check: List[Dict[str, Any]] | None = None,
         | 
| 81 | 
            +
                    exclude_entities: List[str] | None = None
         | 
| 82 | 
            +
                ) -> ModelType:
         | 
| 83 | 
            +
                    try:
         | 
| 84 | 
            +
                        # 고유 검사 수행
         | 
| 85 | 
            +
                        if unique_check:
         | 
| 86 | 
            +
                            await validate_unique_fields(self.db_session, unique_check, find_value=True)
         | 
| 87 | 
            +
                        # 비밀번호가 있으면 해시 처리
         | 
| 88 | 
            +
                        if "password" in update_data:
         | 
| 89 | 
            +
                            update_data["password"] = hash_password(update_data["password"])
         | 
| 90 | 
            +
                        # 제외할 엔티티가 있으면 제외
         | 
| 91 | 
            +
                        if exclude_entities:
         | 
| 92 | 
            +
                            update_data = {k: v for k, v in update_data.items() if k not in exclude_entities}
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                        if not ulid and not conditions:
         | 
| 95 | 
            +
                            raise CustomException(
         | 
| 96 | 
            +
                                ErrorCode.INVALID_INPUT,
         | 
| 97 | 
            +
                                detail="Either 'ulid' or 'conditions' must be provided.",
         | 
| 98 | 
            +
                                source_function="database.update_entity"
         | 
| 99 | 
            +
                            )
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                        # ulid로 조건 생성
         | 
| 102 | 
            +
                        if ulid:
         | 
| 103 | 
            +
                            if not ULID.from_str(ulid):
         | 
| 104 | 
            +
                                raise CustomException(
         | 
| 105 | 
            +
                                    ErrorCode.VALIDATION_ERROR,
         | 
| 106 | 
            +
                                    detail=ulid,
         | 
| 107 | 
            +
                                    source_function=f"{self.__class__.__name__}.update"
         | 
| 108 | 
            +
                                )
         | 
| 109 | 
            +
                            
         | 
| 110 | 
            +
                            conditions = {"ulid": ulid}
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                        return await self.repository.update(
         | 
| 113 | 
            +
                            update_data=update_data,
         | 
| 114 | 
            +
                            conditions=conditions
         | 
| 115 | 
            +
                        )
         | 
| 116 | 
            +
                    except CustomException as e:
         | 
| 117 | 
            +
                        raise e
         | 
| 118 | 
            +
                    except Exception as e:
         | 
| 119 | 
            +
                        raise CustomException(
         | 
| 120 | 
            +
                            ErrorCode.INTERNAL_ERROR,
         | 
| 121 | 
            +
                            detail=str(e),
         | 
| 122 | 
            +
                            source_function=f"{self.__class__.__name__}.update",
         | 
| 123 | 
            +
                            original_error=e
         | 
| 124 | 
            +
                        )
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                async def delete(
         | 
| 127 | 
            +
                    self,
         | 
| 128 | 
            +
                    ulid: str | None = None,
         | 
| 129 | 
            +
                    conditions: Dict[str, Any] | None = None
         | 
| 130 | 
            +
                ) -> None:
         | 
| 131 | 
            +
                    try:
         | 
| 132 | 
            +
                        if not ULID.from_str(ulid):
         | 
| 133 | 
            +
                            raise CustomException(
         | 
| 134 | 
            +
                                ErrorCode.VALIDATION_ERROR,
         | 
| 135 | 
            +
                                detail=ulid,
         | 
| 136 | 
            +
                                source_function=f"{self.__class__.__name__}.delete"
         | 
| 137 | 
            +
                            )
         | 
| 138 | 
            +
                        
         | 
| 139 | 
            +
                        if not ulid and not conditions:
         | 
| 140 | 
            +
                            raise CustomException(
         | 
| 141 | 
            +
                                ErrorCode.INVALID_INPUT,
         | 
| 142 | 
            +
                                detail="Either 'ulid' or 'conditions' must be provided.",
         | 
| 143 | 
            +
                                source_function="database.update_entity"
         | 
| 144 | 
            +
                            )
         | 
| 33 145 |  | 
| 146 | 
            +
                        # ulid로 조건 생성
         | 
| 147 | 
            +
                        if ulid:
         | 
| 148 | 
            +
                            conditions = {"ulid": ulid}
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                        conditions["is_deleted"] = False
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                        return await self.repository.delete(
         | 
| 153 | 
            +
                            conditions=conditions
         | 
| 154 | 
            +
                        )
         | 
| 155 | 
            +
                    except CustomException as e:
         | 
| 156 | 
            +
                        raise e
         | 
| 157 | 
            +
                    except Exception as e:
         | 
| 158 | 
            +
                        raise CustomException(
         | 
| 159 | 
            +
                            ErrorCode.INTERNAL_ERROR,
         | 
| 160 | 
            +
                            detail=str(e),
         | 
| 161 | 
            +
                            source_function=f"{self.__class__.__name__}.delete",
         | 
| 162 | 
            +
                            original_error=e
         | 
| 163 | 
            +
                        )
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                #########################
         | 
| 166 | 
            +
                # 조회 및 검색 메서드 #
         | 
| 167 | 
            +
                #########################
         | 
| 34 168 | 
             
                async def list(
         | 
| 35 169 | 
             
                    self,
         | 
| 36 170 | 
             
                    skip: int = 0,
         | 
| 37 171 | 
             
                    limit: int = 100,
         | 
| 38 | 
            -
                    filters: Dict[str, Any] | None = None,
         | 
| 39 | 
            -
                    search_params: Dict[str, Any] | None = None,
         | 
| 172 | 
            +
                    filters: List[Dict[str, Any]] | None = None,
         | 
| 40 173 | 
             
                    model_name: str | None = None,
         | 
| 41 | 
            -
                     | 
| 42 | 
            -
                     | 
| 174 | 
            +
                    response_model: Any = None,
         | 
| 175 | 
            +
                    explicit_joins: Optional[List[Any]] = None,
         | 
| 176 | 
            +
                    loading_joins: Optional[List[Any]] = None
         | 
| 43 177 | 
             
                ) -> List[Dict[str, Any]]:
         | 
| 44 178 | 
             
                    try:
         | 
| 45 | 
            -
                        # 검색 조건 처리 및 필터 병합
         | 
| 46 | 
            -
                        if search_params:
         | 
| 47 | 
            -
                            search_filters = build_search_filters(request, search_params)
         | 
| 48 | 
            -
                            if filters:
         | 
| 49 | 
            -
                                # 기존 filters와 search_filters 병합 (search_filters가 우선 적용)
         | 
| 50 | 
            -
                                filters.update(search_filters)
         | 
| 51 | 
            -
                            else:
         | 
| 52 | 
            -
                                filters = search_filters
         | 
| 53 | 
            -
             | 
| 54 179 | 
             
                        # 모델 이름을 통한 동적 처리
         | 
| 55 180 | 
             
                        if model_name:
         | 
| 56 181 | 
             
                            if model_name not in self.additional_models:
         | 
| @@ -60,19 +185,74 @@ class BaseService(Generic[ModelType]): | |
| 60 185 | 
             
                                    source_function=f"{self.__class__.__name__}.list"
         | 
| 61 186 | 
             
                                )
         | 
| 62 187 | 
             
                            model = self.additional_models[model_name]
         | 
| 63 | 
            -
                             | 
| 188 | 
            +
                            entities = await self.repository.list(
         | 
| 189 | 
            +
                                skip=skip,
         | 
| 190 | 
            +
                                limit=limit,
         | 
| 191 | 
            +
                                filters=filters,
         | 
| 192 | 
            +
                                model=model
         | 
| 193 | 
            +
                            )
         | 
| 194 | 
            +
                            return [process_response(entity, response_model) for entity in entities]
         | 
| 64 195 |  | 
| 65 | 
            -
                         | 
| 196 | 
            +
                        entities = await self.repository.list(
         | 
| 197 | 
            +
                            skip=skip,
         | 
| 198 | 
            +
                            limit=limit,
         | 
| 199 | 
            +
                            filters=filters,
         | 
| 200 | 
            +
                            explicit_joins=explicit_joins,
         | 
| 201 | 
            +
                            loading_joins=loading_joins
         | 
| 202 | 
            +
                        )
         | 
| 203 | 
            +
                        return [process_response(entity, response_model) for entity in entities]
         | 
| 204 | 
            +
                    
         | 
| 66 205 | 
             
                    except CustomException as e:
         | 
| 67 | 
            -
                        e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
         | 
| 68 | 
            -
                        e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
         | 
| 69 206 | 
             
                        raise e
         | 
| 70 207 | 
             
                    except Exception as e:
         | 
| 71 208 | 
             
                        raise CustomException(
         | 
| 72 209 | 
             
                            ErrorCode.INTERNAL_ERROR,
         | 
| 73 | 
            -
                            detail=str(e),
         | 
| 74 210 | 
             
                            source_function=f"{self.__class__.__name__}.list",
         | 
| 75 211 | 
             
                            original_error=e
         | 
| 76 212 | 
             
                        )
         | 
| 213 | 
            +
                    
         | 
| 214 | 
            +
                async def get(
         | 
| 215 | 
            +
                    self,
         | 
| 216 | 
            +
                    ulid: str,
         | 
| 217 | 
            +
                    model_name: str | None = None,
         | 
| 218 | 
            +
                    response_model: Any = None,
         | 
| 219 | 
            +
                    conditions: Dict[str, Any] | None = None,
         | 
| 220 | 
            +
                    explicit_joins: Optional[List[Any]] = None,
         | 
| 221 | 
            +
                    loading_joins: Optional[List[Any]] = None
         | 
| 222 | 
            +
                ):
         | 
| 223 | 
            +
                    try:
         | 
| 224 | 
            +
                        if not ulid and not conditions:
         | 
| 225 | 
            +
                            raise CustomException(
         | 
| 226 | 
            +
                                ErrorCode.INVALID_INPUT,
         | 
| 227 | 
            +
                                detail="Either 'ulid' or 'conditions' must be provided.",
         | 
| 228 | 
            +
                                source_function="database.update_entity"
         | 
| 229 | 
            +
                            )
         | 
| 230 | 
            +
             | 
| 231 | 
            +
                        # ulid로 조건 생성
         | 
| 232 | 
            +
                        if ulid:
         | 
| 233 | 
            +
                            if not ULID.from_str(ulid):
         | 
| 234 | 
            +
                                raise CustomException(
         | 
| 235 | 
            +
                                    ErrorCode.VALIDATION_ERROR,
         | 
| 236 | 
            +
                                    detail=ulid,
         | 
| 237 | 
            +
                                    source_function=f"{self.__class__.__name__}.update"
         | 
| 238 | 
            +
                                )
         | 
| 239 | 
            +
                            
         | 
| 240 | 
            +
                            conditions = {"ulid": ulid}
         | 
| 241 | 
            +
                        
         | 
| 242 | 
            +
                        entity = await self.repository.get(
         | 
| 243 | 
            +
                            conditions=conditions,
         | 
| 244 | 
            +
                            explicit_joins=explicit_joins,
         | 
| 245 | 
            +
                            loading_joins=loading_joins
         | 
| 246 | 
            +
                        )
         | 
| 247 | 
            +
                        return process_response(entity, response_model)
         | 
| 77 248 |  | 
| 249 | 
            +
                    except CustomException as e:
         | 
| 250 | 
            +
                        raise e
         | 
| 251 | 
            +
                    except Exception as e:
         | 
| 252 | 
            +
                        raise CustomException(
         | 
| 253 | 
            +
                            ErrorCode.INTERNAL_ERROR,
         | 
| 254 | 
            +
                            detail=str(e),
         | 
| 255 | 
            +
                            source_function=f"{self.__class__.__name__}.get",
         | 
| 256 | 
            +
                            original_error=e
         | 
| 257 | 
            +
                        )
         | 
| 78 258 |  | 
    
        aiteamutils/database.py
    CHANGED
    
    | @@ -1,10 +1,24 @@ | |
| 1 1 | 
             
            #기본 라이브러리
         | 
| 2 | 
            -
            from typing import  | 
| 2 | 
            +
            from typing import (
         | 
| 3 | 
            +
                TypeVar, 
         | 
| 4 | 
            +
                Generic, 
         | 
| 5 | 
            +
                Type, 
         | 
| 6 | 
            +
                Any, 
         | 
| 7 | 
            +
                Dict, 
         | 
| 8 | 
            +
                List, 
         | 
| 9 | 
            +
                Optional, 
         | 
| 10 | 
            +
                AsyncGenerator
         | 
| 11 | 
            +
            )
         | 
| 3 12 | 
             
            from sqlalchemy.ext.asyncio import AsyncSession
         | 
| 4 | 
            -
            from sqlalchemy import select, and_
         | 
| 5 | 
            -
            from sqlalchemy.orm import DeclarativeBase
         | 
| 13 | 
            +
            from sqlalchemy import select, and_, or_
         | 
| 14 | 
            +
            from sqlalchemy.orm import DeclarativeBase, joinedload, selectinload
         | 
| 6 15 | 
             
            from sqlalchemy.exc import SQLAlchemyError
         | 
| 7 16 | 
             
            from datetime import datetime
         | 
| 17 | 
            +
            from contextlib import asynccontextmanager
         | 
| 18 | 
            +
            from sqlalchemy.ext.asyncio import AsyncSession
         | 
| 19 | 
            +
            from fastapi import Request
         | 
| 20 | 
            +
            from ulid import ULID
         | 
| 21 | 
            +
            from sqlalchemy import MetaData, Table, insert
         | 
| 8 22 |  | 
| 9 23 | 
             
            #패키지 라이브러리
         | 
| 10 24 | 
             
            from .exceptions import ErrorCode, CustomException
         | 
| @@ -15,8 +29,62 @@ ModelType = TypeVar("ModelType", bound=DeclarativeBase) | |
| 15 29 | 
             
            ##################
         | 
| 16 30 | 
             
            # 전처리 #
         | 
| 17 31 | 
             
            ##################
         | 
| 32 | 
            +
            def process_entity_data(
         | 
| 33 | 
            +
                model: Type[ModelType],
         | 
| 34 | 
            +
                entity_data: Dict[str, Any],
         | 
| 35 | 
            +
                existing_data: Dict[str, Any] = None
         | 
| 36 | 
            +
            ) -> Dict[str, Any]:
         | 
| 37 | 
            +
                """
         | 
| 38 | 
            +
                엔티티 데이터를 전처리하고 모델 속성과 extra_data를 분리합니다.
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                이 함수는 다음과 같은 작업을 수행합니다:
         | 
| 41 | 
            +
                1. 모델의 기본 속성을 식별합니다.
         | 
| 42 | 
            +
                2. Swagger 자동 생성 속성을 제외합니다.
         | 
| 43 | 
            +
                3. 모델 속성에 해당하는 데이터는 model_data에 저장
         | 
| 44 | 
            +
                4. 모델 속성에 없는 데이터는 extra_data에 저장
         | 
| 45 | 
            +
                5. 기존 엔티티 데이터의 extra_data를 유지할 수 있습니다.
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                Args:
         | 
| 48 | 
            +
                    model (Type[ModelType]): 데이터 모델 클래스
         | 
| 49 | 
            +
                    entity_data (Dict[str, Any]): 처리할 엔티티 데이터
         | 
| 50 | 
            +
                    existing_entity_data (Dict[str, Any], optional): 기존 엔티티 데이터. Defaults to None.
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                Returns:
         | 
| 53 | 
            +
                    Dict[str, Any]: 전처리된 모델 데이터 (extra_data 포함)
         | 
| 54 | 
            +
                """
         | 
| 55 | 
            +
                # 모델의 속성을 가져와서 전처리합니다.
         | 
| 56 | 
            +
                model_attr = {
         | 
| 57 | 
            +
                    attr for attr in dir(model)
         | 
| 58 | 
            +
                    if not attr.startswith('_') and not callable(getattr(model, attr))
         | 
| 59 | 
            +
                }
         | 
| 60 | 
            +
                model_data = {}
         | 
| 61 | 
            +
                extra_data = {}
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                # 기존 엔티티 데이터가 있으면 추가
         | 
| 64 | 
            +
                if existing_data and "extra_data" in existing_data:
         | 
| 65 | 
            +
                    extra_data = existing_data["extra_data"].copy()
         | 
| 66 | 
            +
             | 
| 67 | 
            +
             | 
| 68 | 
            +
                # Swagger 자동 생성 속성 패턴
         | 
| 69 | 
            +
                swagger_patterns = {"additionalProp1", "additionalProp2", "additionalProp3"}
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                for key, value in entity_data.items():
         | 
| 72 | 
            +
                    # Swagger 자동 생성 속성 무시
         | 
| 73 | 
            +
                    if key in swagger_patterns:
         | 
| 74 | 
            +
                        continue
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                    # 모델 속성에 있는 경우 model_data에 추가
         | 
| 77 | 
            +
                    if key in model_attr:
         | 
| 78 | 
            +
                        model_data[key] = value
         | 
| 79 | 
            +
                    # 모델 속성에 없는 경우 extra_data에 추가
         | 
| 80 | 
            +
                    else:
         | 
| 81 | 
            +
                        extra_data[key] = value
         | 
| 18 82 |  | 
| 83 | 
            +
                # extra_data가 있고 모델에 extra_data 속성이 있는 경우 추가
         | 
| 84 | 
            +
                if extra_data and "extra_data" in model_attr:
         | 
| 85 | 
            +
                    model_data["extra_data"] = extra_data
         | 
| 19 86 |  | 
| 87 | 
            +
                return model_data
         | 
| 20 88 |  | 
| 21 89 | 
             
            ##################
         | 
| 22 90 | 
             
            # 응답 처리 #
         | 
| @@ -115,81 +183,224 @@ def process_response( | |
| 115 183 |  | 
| 116 184 | 
             
                return result
         | 
| 117 185 |  | 
| 118 | 
            -
             | 
| 119 186 | 
             
            ##################
         | 
| 120 187 | 
             
            # 조건 처리 #
         | 
| 121 188 | 
             
            ##################
         | 
| 122 | 
            -
            def build_search_filters(
         | 
| 123 | 
            -
                    request: Dict[str, Any],
         | 
| 124 | 
            -
                    search_params: Dict[str, Dict[str, Any]]
         | 
| 125 | 
            -
            ) -> Dict[str, Any]:
         | 
| 126 | 
            -
                """
         | 
| 127 | 
            -
                요청 데이터와 검색 파라미터를 기반으로 필터 조건을 생성합니다.
         | 
| 128 | 
            -
             | 
| 129 | 
            -
                Args:
         | 
| 130 | 
            -
                    request: 요청 데이터 (key-value 형태).
         | 
| 131 | 
            -
                    search_params: 검색 조건 설정을 위한 파라미터.
         | 
| 132 | 
            -
             | 
| 133 | 
            -
                Returns:
         | 
| 134 | 
            -
                    filters: 필터 조건 딕셔너리.
         | 
| 135 | 
            -
                """
         | 
| 136 | 
            -
                filters = {}
         | 
| 137 | 
            -
                for key, param in search_params.items():
         | 
| 138 | 
            -
                    value = request.get(key)
         | 
| 139 | 
            -
                    if value is not None:
         | 
| 140 | 
            -
                        if param["like"]:
         | 
| 141 | 
            -
                            filters[key] = {"field": param["fields"][0], "operator": "like", "value": f"%{value}%"}
         | 
| 142 | 
            -
                        else:
         | 
| 143 | 
            -
                            filters[key] = {"field": param["fields"][0], "operator": "eq", "value": value}
         | 
| 144 | 
            -
                return filters
         | 
| 145 | 
            -
             | 
| 146 189 | 
             
            def build_conditions(
         | 
| 147 | 
            -
             | 
| 148 | 
            -
             | 
| 190 | 
            +
                filters: List[Dict[str, Any]],
         | 
| 191 | 
            +
                model: Type[ModelType]
         | 
| 149 192 | 
             
            ) -> List[Any]:
         | 
| 150 193 | 
             
                """
         | 
| 151 194 | 
             
                필터 조건을 기반으로 SQLAlchemy 조건 리스트를 생성합니다.
         | 
| 152 195 |  | 
| 153 196 | 
             
                Args:
         | 
| 154 | 
            -
                    filters: 필터 조건  | 
| 197 | 
            +
                    filters: 필터 조건 리스트.
         | 
| 155 198 | 
             
                    model: SQLAlchemy 모델 클래스.
         | 
| 156 199 |  | 
| 157 200 | 
             
                Returns:
         | 
| 158 201 | 
             
                    List[Any]: SQLAlchemy 조건 리스트.
         | 
| 159 202 | 
             
                """
         | 
| 160 203 | 
             
                conditions = []
         | 
| 161 | 
            -
                for filter_data in filters.values():
         | 
| 162 | 
            -
                    if "." in filter_data["field"]:
         | 
| 163 | 
            -
                        # 관계를 따라 암묵적으로 연결된 모델의 필드 가져오기
         | 
| 164 | 
            -
                        related_model_name, field_name = filter_data["field"].split(".")
         | 
| 165 | 
            -
                        relationship_property = getattr(model, related_model_name)
         | 
| 166 | 
            -
                        related_model = relationship_property.property.mapper.class_
         | 
| 167 | 
            -
                        field = getattr(related_model, field_name)
         | 
| 168 | 
            -
                    else:
         | 
| 169 | 
            -
                        # 현재 모델의 필드 가져오기
         | 
| 170 | 
            -
                        field = getattr(model, filter_data["field"])
         | 
| 171 204 |  | 
| 172 | 
            -
             | 
| 173 | 
            -
                     | 
| 174 | 
            -
                    value  | 
| 205 | 
            +
                for filter_item in filters:
         | 
| 206 | 
            +
                    value = filter_item.get("value")
         | 
| 207 | 
            +
                    if not value:  # 값이 없으면 건너뜀
         | 
| 208 | 
            +
                        continue
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                    operator = filter_item.get("operator", "eq")
         | 
| 211 | 
            +
                    or_conditions = []
         | 
| 175 212 |  | 
| 176 | 
            -
                     | 
| 177 | 
            -
                         | 
| 178 | 
            -
             | 
| 179 | 
            -
                         | 
| 213 | 
            +
                    for field_path in filter_item.get("fields", []):
         | 
| 214 | 
            +
                        current_model = model
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                        # 관계를 따라 필드 가져오기
         | 
| 217 | 
            +
                        for part in field_path.split(".")[:-1]:
         | 
| 218 | 
            +
                            relationship_property = getattr(current_model, part)
         | 
| 219 | 
            +
                            current_model = relationship_property.property.mapper.class_
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                        field = getattr(current_model, field_path.split(".")[-1])
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                        # 조건 생성
         | 
| 224 | 
            +
                        if operator == "like":
         | 
| 225 | 
            +
                            or_conditions.append(field.ilike(f"%{value}%"))
         | 
| 226 | 
            +
                        elif operator == "eq":
         | 
| 227 | 
            +
                            or_conditions.append(field == value)
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                    if or_conditions:  # OR 조건이 있을 때만 추가
         | 
| 230 | 
            +
                        conditions.append(or_(*or_conditions))
         | 
| 180 231 |  | 
| 181 232 | 
             
                return conditions
         | 
| 182 233 |  | 
| 183 234 | 
             
            ##################
         | 
| 184 235 | 
             
            # 쿼리 실행 #
         | 
| 185 236 | 
             
            ##################
         | 
| 237 | 
            +
            async def create_entity(
         | 
| 238 | 
            +
                session: AsyncSession,
         | 
| 239 | 
            +
                model: Type[ModelType],
         | 
| 240 | 
            +
                entity_data: Dict[str, Any]
         | 
| 241 | 
            +
            ) -> ModelType:
         | 
| 242 | 
            +
                """
         | 
| 243 | 
            +
                새로운 엔티티를 데이터베이스에 생성합니다.
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                Args:
         | 
| 246 | 
            +
                    session (AsyncSession): 데이터베이스 세션
         | 
| 247 | 
            +
                    model (Type[ModelType]): 생성할 모델 클래스
         | 
| 248 | 
            +
                    entity_data (Dict[str, Any]): 엔티티 생성에 필요한 데이터
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                Returns:
         | 
| 251 | 
            +
                    ModelType: 생성된 엔티티
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                Raises:
         | 
| 254 | 
            +
                    CustomException: 엔티티 생성 중 발생하는 데이터베이스 오류
         | 
| 255 | 
            +
                """
         | 
| 256 | 
            +
                try:
         | 
| 257 | 
            +
                    # 엔티티 데이터 전처리
         | 
| 258 | 
            +
                    processed_data = process_entity_data(model, entity_data)
         | 
| 259 | 
            +
             | 
| 260 | 
            +
                    # 외래 키 필드 검증
         | 
| 261 | 
            +
                    
         | 
| 262 | 
            +
                    # 새로운 엔티티 생성
         | 
| 263 | 
            +
                    entity = model(**processed_data)
         | 
| 264 | 
            +
                    
         | 
| 265 | 
            +
                    # 세션에 엔티티 추가
         | 
| 266 | 
            +
                    session.add(entity)
         | 
| 267 | 
            +
                    
         | 
| 268 | 
            +
                    # 데이터베이스에 커밋
         | 
| 269 | 
            +
                    await session.flush()
         | 
| 270 | 
            +
                    await session.commit()
         | 
| 271 | 
            +
                    await session.refresh(entity)
         | 
| 272 | 
            +
                    
         | 
| 273 | 
            +
                    # 생성된 엔티티 반환
         | 
| 274 | 
            +
                    return entity
         | 
| 275 | 
            +
                
         | 
| 276 | 
            +
                except SQLAlchemyError as e:
         | 
| 277 | 
            +
                    # 데이터베이스 오류 발생 시 CustomException으로 변환
         | 
| 278 | 
            +
                    raise CustomException(
         | 
| 279 | 
            +
                        ErrorCode.DB_CREATE_ERROR,
         | 
| 280 | 
            +
                        detail=f"{model.__name__}|{str(e)}",
         | 
| 281 | 
            +
                        source_function="database.create_entity",
         | 
| 282 | 
            +
                        original_error=e
         | 
| 283 | 
            +
                    )
         | 
| 284 | 
            +
             | 
| 285 | 
            +
            async def update_entity(
         | 
| 286 | 
            +
                session: AsyncSession,
         | 
| 287 | 
            +
                model: Type[ModelType],
         | 
| 288 | 
            +
                conditions: Dict[str, Any],
         | 
| 289 | 
            +
                update_data: Dict[str, Any]
         | 
| 290 | 
            +
            ) -> ModelType:
         | 
| 291 | 
            +
                """
         | 
| 292 | 
            +
                조건을 기반으로 엔티티를 조회하고 업데이트합니다.
         | 
| 293 | 
            +
             | 
| 294 | 
            +
                Args:
         | 
| 295 | 
            +
                    session (AsyncSession): 데이터베이스 세션
         | 
| 296 | 
            +
                    model (Type[ModelType]): 업데이트할 모델 클래스
         | 
| 297 | 
            +
                    conditions (Dict[str, Any]): 엔티티 조회 조건 
         | 
| 298 | 
            +
                        conditions = {"user_id": 1, "status": "active"}
         | 
| 299 | 
            +
                    update_data (Dict[str, Any]): 업데이트할 데이터
         | 
| 300 | 
            +
                        update_data = {"status": "inactive"}
         | 
| 301 | 
            +
                Returns:
         | 
| 302 | 
            +
                    ModelType: 업데이트된 엔티티
         | 
| 303 | 
            +
             | 
| 304 | 
            +
                Raises:
         | 
| 305 | 
            +
                    CustomException: 엔티티 조회 또는 업데이트 중 발생하는 데이터베이스 오류
         | 
| 306 | 
            +
                """
         | 
| 307 | 
            +
                try:
         | 
| 308 | 
            +
                    # 조건 기반 엔티티 조회
         | 
| 309 | 
            +
                    stmt = select(model)
         | 
| 310 | 
            +
                    for key, value in conditions.items():
         | 
| 311 | 
            +
                        stmt = stmt.where(getattr(model, key) == value)
         | 
| 312 | 
            +
             | 
| 313 | 
            +
                    result = await session.execute(stmt)
         | 
| 314 | 
            +
                    entity = result.scalar_one_or_none()
         | 
| 315 | 
            +
             | 
| 316 | 
            +
                    if not entity:
         | 
| 317 | 
            +
                        raise CustomException(
         | 
| 318 | 
            +
                            ErrorCode.NOT_FOUND,
         | 
| 319 | 
            +
                            detail=f"{model.__name__}|{conditions}.",
         | 
| 320 | 
            +
                            source_function="database.update_entity"
         | 
| 321 | 
            +
                        )
         | 
| 322 | 
            +
             | 
| 323 | 
            +
                    # 기존 데이터를 딕셔너리로 변환
         | 
| 324 | 
            +
                    existing_data = {
         | 
| 325 | 
            +
                        column.name: getattr(entity, column.name)
         | 
| 326 | 
            +
                        for column in entity.__table__.columns
         | 
| 327 | 
            +
                    }
         | 
| 328 | 
            +
             | 
| 329 | 
            +
                    # 데이터 병합 및 전처리
         | 
| 330 | 
            +
                    processed_data = process_entity_data(model, update_data, existing_data)
         | 
| 331 | 
            +
             | 
| 332 | 
            +
                    # 엔티티 데이터 업데이트
         | 
| 333 | 
            +
                    for key, value in processed_data.items():
         | 
| 334 | 
            +
                        if hasattr(entity, key):
         | 
| 335 | 
            +
                            setattr(entity, key, value)
         | 
| 336 | 
            +
             | 
| 337 | 
            +
                    # 변경 사항 커밋
         | 
| 338 | 
            +
                    await session.flush()
         | 
| 339 | 
            +
                    await session.commit()
         | 
| 340 | 
            +
                    await session.refresh(entity)
         | 
| 341 | 
            +
             | 
| 342 | 
            +
                    return entity
         | 
| 343 | 
            +
             | 
| 344 | 
            +
                except SQLAlchemyError as e:
         | 
| 345 | 
            +
                    raise CustomException(
         | 
| 346 | 
            +
                        ErrorCode.DB_UPDATE_ERROR,
         | 
| 347 | 
            +
                        detail=f"{model.__name__}|{conditions}",
         | 
| 348 | 
            +
                        source_function="database.update_entity",
         | 
| 349 | 
            +
                        original_error=e
         | 
| 350 | 
            +
                    )
         | 
| 351 | 
            +
             | 
| 352 | 
            +
            async def delete_entity(
         | 
| 353 | 
            +
                session: AsyncSession,
         | 
| 354 | 
            +
                model: Type[ModelType],
         | 
| 355 | 
            +
                conditions: Dict[str, Any]
         | 
| 356 | 
            +
            ) -> None:
         | 
| 357 | 
            +
                try:
         | 
| 358 | 
            +
                    stmt = select(model)
         | 
| 359 | 
            +
                    for key, value in conditions.items():
         | 
| 360 | 
            +
                        stmt = stmt.where(getattr(model, key) == value)
         | 
| 361 | 
            +
             | 
| 362 | 
            +
                    result = await session.execute(stmt)
         | 
| 363 | 
            +
                    entity = result.scalar_one_or_none()
         | 
| 364 | 
            +
             | 
| 365 | 
            +
                    if not entity:
         | 
| 366 | 
            +
                        raise CustomException(
         | 
| 367 | 
            +
                            ErrorCode.NOT_FOUND,
         | 
| 368 | 
            +
                            detail=f"{model.__name__}|{conditions}.",
         | 
| 369 | 
            +
                            source_function="database.delete_entity"
         | 
| 370 | 
            +
                        )
         | 
| 371 | 
            +
             | 
| 372 | 
            +
                    entity.is_deleted = True
         | 
| 373 | 
            +
                    entity.deleted_at = datetime.now()
         | 
| 374 | 
            +
             | 
| 375 | 
            +
                    await session.flush()
         | 
| 376 | 
            +
                    await session.commit()
         | 
| 377 | 
            +
                    await session.refresh(entity)
         | 
| 378 | 
            +
             | 
| 379 | 
            +
                except SQLAlchemyError as e:
         | 
| 380 | 
            +
                    raise CustomException(
         | 
| 381 | 
            +
                        ErrorCode.DB_DELETE_ERROR,
         | 
| 382 | 
            +
                        detail=f"{model.__name__}|{conditions}",
         | 
| 383 | 
            +
                        source_function="database.delete_entity",
         | 
| 384 | 
            +
                        original_error=e
         | 
| 385 | 
            +
                    )
         | 
| 386 | 
            +
             | 
| 387 | 
            +
            async def purge_entity(
         | 
| 388 | 
            +
                session: AsyncSession,
         | 
| 389 | 
            +
                model: Type[ModelType],
         | 
| 390 | 
            +
                entity: ModelType
         | 
| 391 | 
            +
            ) -> None:
         | 
| 392 | 
            +
                # 엔티티를 영구 삭제합니다.
         | 
| 393 | 
            +
                await session.delete(entity)
         | 
| 394 | 
            +
                await session.commit()
         | 
| 395 | 
            +
             | 
| 186 396 | 
             
            async def list_entities(
         | 
| 187 397 | 
             
                session: AsyncSession,
         | 
| 188 398 | 
             
                model: Type[ModelType],
         | 
| 189 399 | 
             
                skip: int = 0,
         | 
| 190 400 | 
             
                limit: int = 100,
         | 
| 191 401 | 
             
                filters: Optional[Dict[str, Any]] = None,
         | 
| 192 | 
            -
                 | 
| 402 | 
            +
                explicit_joins: Optional[List[Any]] = None,
         | 
| 403 | 
            +
                loading_joins: Optional[List[Any]] = None
         | 
| 193 404 | 
             
            ) -> List[Dict[str, Any]]:
         | 
| 194 405 | 
             
                """
         | 
| 195 406 | 
             
                엔터티 리스트를 필터 및 조건에 따라 가져오는 함수.
         | 
| @@ -220,20 +431,26 @@ async def list_entities( | |
| 220 431 | 
             
                try:
         | 
| 221 432 | 
             
                    query = select(model)
         | 
| 222 433 |  | 
| 434 | 
            +
                    # 명시적 조인 적용
         | 
| 435 | 
            +
                    if explicit_joins:
         | 
| 436 | 
            +
                        for join_target in explicit_joins:
         | 
| 437 | 
            +
                            query = query.join(join_target)  # 명시적으로 정의된 조인 추가
         | 
| 438 | 
            +
             | 
| 439 | 
            +
                    # 조인 로딩 적용
         | 
| 440 | 
            +
                    if loading_joins:
         | 
| 441 | 
            +
                        for join_option in loading_joins:
         | 
| 442 | 
            +
                            query = query.options(join_option)
         | 
| 443 | 
            +
             | 
| 223 444 | 
             
                    # 필터 조건 적용
         | 
| 224 445 | 
             
                    if filters:
         | 
| 225 446 | 
             
                        conditions = build_conditions(filters, model)
         | 
| 226 447 | 
             
                        query = query.where(and_(*conditions))
         | 
| 227 448 |  | 
| 228 | 
            -
                    # 조인 로딩 적용
         | 
| 229 | 
            -
                    if joins:
         | 
| 230 | 
            -
                        for join_option in joins:
         | 
| 231 | 
            -
                            query = query.options(join_option)
         | 
| 232 | 
            -
             | 
| 233 449 | 
             
                    # 페이지네이션 적용
         | 
| 234 450 | 
             
                    query = query.limit(limit).offset(skip)
         | 
| 235 451 |  | 
| 236 452 | 
             
                    result = await session.execute(query)
         | 
| 453 | 
            +
                    
         | 
| 237 454 | 
             
                    return result.scalars().unique().all()
         | 
| 238 455 | 
             
                except SQLAlchemyError as e:
         | 
| 239 456 | 
             
                    raise CustomException(
         | 
| @@ -242,3 +459,125 @@ async def list_entities( | |
| 242 459 | 
             
                        source_function="database.list_entities",
         | 
| 243 460 | 
             
                        original_error=e
         | 
| 244 461 | 
             
                    )
         | 
| 462 | 
            +
             | 
| 463 | 
            +
            async def get_entity(
         | 
| 464 | 
            +
                session: AsyncSession,
         | 
| 465 | 
            +
                model: Type[ModelType],
         | 
| 466 | 
            +
                conditions: Dict[str, Any],
         | 
| 467 | 
            +
                explicit_joins: Optional[List[Any]] = None,
         | 
| 468 | 
            +
                loading_joins: Optional[List[Any]] = None
         | 
| 469 | 
            +
            ) -> ModelType:
         | 
| 470 | 
            +
                try:
         | 
| 471 | 
            +
                    query = select(model)
         | 
| 472 | 
            +
             | 
| 473 | 
            +
                    if explicit_joins:
         | 
| 474 | 
            +
                        for join_target in explicit_joins:
         | 
| 475 | 
            +
                            query = query.join(join_target)
         | 
| 476 | 
            +
             | 
| 477 | 
            +
                    if loading_joins:
         | 
| 478 | 
            +
                        for join_option in loading_joins:
         | 
| 479 | 
            +
                            query = query.options(join_option)
         | 
| 480 | 
            +
             | 
| 481 | 
            +
                    if conditions:
         | 
| 482 | 
            +
                        for key, value in conditions.items():
         | 
| 483 | 
            +
                            query = query.where(getattr(model, key) == value)
         | 
| 484 | 
            +
             | 
| 485 | 
            +
                    result = await session.execute(query)
         | 
| 486 | 
            +
                    return result.scalars().unique().one_or_none()
         | 
| 487 | 
            +
             | 
| 488 | 
            +
                except SQLAlchemyError as e:
         | 
| 489 | 
            +
                    raise CustomException(
         | 
| 490 | 
            +
                        ErrorCode.DB_READ_ERROR,
         | 
| 491 | 
            +
                        detail=str(e),
         | 
| 492 | 
            +
                        source_function="database.get_entity",
         | 
| 493 | 
            +
                        original_error=str(e)
         | 
| 494 | 
            +
                    )
         | 
| 495 | 
            +
                
         | 
| 496 | 
            +
            ##################
         | 
| 497 | 
            +
            # 로그 등 #
         | 
| 498 | 
            +
            ##################
         | 
| 499 | 
            +
            async def log_create(
         | 
| 500 | 
            +
                session: AsyncSession,
         | 
| 501 | 
            +
                table_name: str,
         | 
| 502 | 
            +
                log_data: Dict[str, Any],
         | 
| 503 | 
            +
                request: Request = None
         | 
| 504 | 
            +
            ) -> None:
         | 
| 505 | 
            +
                try:
         | 
| 506 | 
            +
                    # ULID 생성
         | 
| 507 | 
            +
                    if "ulid" not in log_data:
         | 
| 508 | 
            +
                        log_data["ulid"] = ULID()
         | 
| 509 | 
            +
             | 
| 510 | 
            +
                    # 사용자 에이전트 및 IP 주소 추가
         | 
| 511 | 
            +
                    if request:
         | 
| 512 | 
            +
                        log_data["user_agent"] = request.headers.get("user-agent")
         | 
| 513 | 
            +
                        log_data["ip_address"] = request.headers.get("x-forwarded-for") or request.client.host
         | 
| 514 | 
            +
             | 
| 515 | 
            +
                    # 동적으로 테이블 로드
         | 
| 516 | 
            +
                    metadata = MetaData(bind=session.bind)
         | 
| 517 | 
            +
                    table = Table(table_name, metadata, autoload_with=session.bind)
         | 
| 518 | 
            +
             | 
| 519 | 
            +
                    # 데이터 삽입
         | 
| 520 | 
            +
                    insert_stmt = insert(table).values(log_data)
         | 
| 521 | 
            +
                    await session.execute(insert_stmt)
         | 
| 522 | 
            +
                    await session.commit()
         | 
| 523 | 
            +
             | 
| 524 | 
            +
                except Exception as e:
         | 
| 525 | 
            +
                    raise CustomException(
         | 
| 526 | 
            +
                        ErrorCode.INTERNAL_ERROR,
         | 
| 527 | 
            +
                        detail=f"{model.__name__}|{str(e)}",
         | 
| 528 | 
            +
                        source_function="database.log_create",
         | 
| 529 | 
            +
                        original_error=e
         | 
| 530 | 
            +
                    )
         | 
| 531 | 
            +
             | 
| 532 | 
            +
            ######################
         | 
| 533 | 
            +
            # 검증 #
         | 
| 534 | 
            +
            ######################
         | 
| 535 | 
            +
            async def validate_unique_fields(
         | 
| 536 | 
            +
                session: AsyncSession,
         | 
| 537 | 
            +
                unique_check: List[Dict[str, Any]] | None = None,
         | 
| 538 | 
            +
                find_value: bool = True  # True: 값이 있는지, False: 값이 없는지 확인
         | 
| 539 | 
            +
            ) -> None:
         | 
| 540 | 
            +
                try:
         | 
| 541 | 
            +
                    for check in unique_check:
         | 
| 542 | 
            +
                        value = check["value"]
         | 
| 543 | 
            +
                        model = check["model"]
         | 
| 544 | 
            +
                        fields = check["fields"]
         | 
| 545 | 
            +
             | 
| 546 | 
            +
                        # 여러 개의 컬럼이 있을 경우 모든 조건을 만족해야 한다
         | 
| 547 | 
            +
                        conditions = [getattr(model, column) == value for column in fields]
         | 
| 548 | 
            +
             | 
| 549 | 
            +
                        # 쿼리 실행
         | 
| 550 | 
            +
                        query = select(model).where(or_(*conditions))
         | 
| 551 | 
            +
                        result = await session.execute(query)
         | 
| 552 | 
            +
                        existing = result.scalar_one_or_none()
         | 
| 553 | 
            +
             | 
| 554 | 
            +
                        # 값이 있는지 확인 (find_value=True) 또는 값이 없는지 확인 (find_value=False)
         | 
| 555 | 
            +
                        if find_value and existing:  # 값이 존재하는 경우
         | 
| 556 | 
            +
                            raise CustomException(
         | 
| 557 | 
            +
                                ErrorCode.FIELD_INVALID_UNIQUE,
         | 
| 558 | 
            +
                                detail=f"{model.name}|{value}",
         | 
| 559 | 
            +
                                source_function="database.validate_unique_fields.existing"
         | 
| 560 | 
            +
                            )
         | 
| 561 | 
            +
                        elif not find_value and not existing:  # 값이 존재하지 않는 경우
         | 
| 562 | 
            +
                            raise CustomException(
         | 
| 563 | 
            +
                                ErrorCode.FIELD_INVALID_NOT_EXIST,
         | 
| 564 | 
            +
                                detail=f"{model.name}|{value}",
         | 
| 565 | 
            +
                                source_function="database.validate_unique_fields.not_existing"
         | 
| 566 | 
            +
                            )
         | 
| 567 | 
            +
             | 
| 568 | 
            +
                except CustomException as e:
         | 
| 569 | 
            +
                    # 특정 CustomException 처리
         | 
| 570 | 
            +
                    raise CustomException(
         | 
| 571 | 
            +
                        e.error_code,
         | 
| 572 | 
            +
                        detail=str(e),
         | 
| 573 | 
            +
                        source_function="database.validate_unique_fields.Exception",
         | 
| 574 | 
            +
                        original_error=e
         | 
| 575 | 
            +
                    )
         | 
| 576 | 
            +
                except Exception as e:
         | 
| 577 | 
            +
                    # 알 수 없는 예외 처리
         | 
| 578 | 
            +
                    raise CustomException(
         | 
| 579 | 
            +
                        ErrorCode.INTERNAL_ERROR,
         | 
| 580 | 
            +
                        detail=f"Unexpected error: {str(e)}",
         | 
| 581 | 
            +
                        source_function="database.validate_unique_fields.Exception",
         | 
| 582 | 
            +
                        original_error=e
         | 
| 583 | 
            +
                    )
         | 
    
        aiteamutils/exceptions.py
    CHANGED
    
    | @@ -64,12 +64,16 @@ class ErrorCode(Enum): | |
| 64 64 | 
             
                VALIDATION_ERROR = ErrorResponse(4001, "VALIDATION_ERROR", 422, "유효성 검사 오류")
         | 
| 65 65 | 
             
                FIELD_INVALID_FORMAT = ErrorResponse(4002, "FIELD_INVALID_FORMAT", 400, "잘못된 형식입니다")
         | 
| 66 66 | 
             
                REQUIRED_FIELD_MISSING = ErrorResponse(4003, "VALIDATION_REQUIRED_FIELD_MISSING", 400, "필수 필드가 누락되었습니다")
         | 
| 67 | 
            +
                FIELD_INVALID_UNIQUE = ErrorResponse(4004, "VALIDATION_FIELD_INVALID_UNIQUE", 400, "중복된 값이 존재합니다")
         | 
| 68 | 
            +
                FIELD_INVALID_NOT_EXIST = ErrorResponse(4005, "VALIDATION_FIELD_INVALID_NOT_EXIST", 400, "존재하지 않는 값입니다")
         | 
| 67 69 |  | 
| 68 70 | 
             
                # General 에러: 5000번대
         | 
| 69 71 | 
             
                NOT_FOUND = ErrorResponse(5001, "GENERAL_NOT_FOUND", 404, "리소스를 찾을 수 없습니다")
         | 
| 70 72 | 
             
                INTERNAL_ERROR = ErrorResponse(5002, "GENERAL_INTERNAL_ERROR", 500, "내부 서버 오류")
         | 
| 71 73 | 
             
                SERVICE_UNAVAILABLE = ErrorResponse(5003, "GENERAL_SERVICE_UNAVAILABLE", 503, "서비스를 사용할 수 없습니다")
         | 
| 72 74 | 
             
                SERVICE_NOT_REGISTERED = ErrorResponse(5003, "GENERAL_SERVICE_UNAVAILABLE", 503, "서비스를 사용할 수 없습니다")
         | 
| 75 | 
            +
                LOGIN_ERROR = ErrorResponse(5004, "LOGIN_ERROR", 401, "로그인 오류")
         | 
| 76 | 
            +
                TOKEN_ERROR = ErrorResponse(5005, "TOKEN_ERROR", 401, "토큰 오류")
         | 
| 73 77 |  | 
| 74 78 | 
             
            class CustomException(Exception):
         | 
| 75 79 | 
             
                """사용자 정의 예외 클래스"""
         | 
| @@ -98,7 +102,7 @@ class CustomException(Exception): | |
| 98 102 |  | 
| 99 103 | 
             
                    self.error_chain.append({
         | 
| 100 104 | 
             
                        "error_code": str(self.error_code),
         | 
| 101 | 
            -
                        "detail": str(self.detail),
         | 
| 105 | 
            +
                        "detail": str(self.detail) if self.detail else None,
         | 
| 102 106 | 
             
                        "source_function": self.source_function,
         | 
| 103 107 | 
             
                        "original_error": str(self.original_error) if self.original_error else None
         | 
| 104 108 | 
             
                    })
         | 
| @@ -115,7 +119,7 @@ class CustomException(Exception): | |
| 115 119 | 
             
                    """에러 정보를 딕셔너리로 반환합니다."""
         | 
| 116 120 | 
             
                    return {
         | 
| 117 121 | 
             
                        "error_code": self.error_code.name,
         | 
| 118 | 
            -
                        "detail": self.detail,
         | 
| 122 | 
            +
                        "detail": self.detail if self.detail else None,
         | 
| 119 123 | 
             
                        "source_function": self.source_function,
         | 
| 120 124 | 
             
                        "error_chain": self.error_chain,
         | 
| 121 125 | 
             
                        "original_error": str(self.original_error) if self.original_error else None
         | 
    
        aiteamutils/security.py
    CHANGED
    
    | @@ -6,10 +6,11 @@ from functools import wraps | |
| 6 6 | 
             
            from jose import jwt, JWTError
         | 
| 7 7 | 
             
            from passlib.context import CryptContext
         | 
| 8 8 | 
             
            import logging
         | 
| 9 | 
            +
            from sqlalchemy.ext.asyncio import AsyncSession
         | 
| 9 10 |  | 
| 10 11 | 
             
            from .exceptions import CustomException, ErrorCode
         | 
| 11 12 | 
             
            from .enums import ActivityType
         | 
| 12 | 
            -
            from . | 
| 13 | 
            +
            from .database import log_create
         | 
| 13 14 |  | 
| 14 15 | 
             
            pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
         | 
| 15 16 |  | 
| @@ -115,7 +116,6 @@ def rate_limit( | |
| 115 116 | 
             
                            except Exception as e:
         | 
| 116 117 | 
             
                                raise CustomException(
         | 
| 117 118 | 
             
                                    ErrorCode.INTERNAL_ERROR,
         | 
| 118 | 
            -
                                    detail=str(e),
         | 
| 119 119 | 
             
                                    source_function=func.__name__,
         | 
| 120 120 | 
             
                                    original_error=e
         | 
| 121 121 | 
             
                                )
         | 
| @@ -125,7 +125,6 @@ def rate_limit( | |
| 125 125 | 
             
                        except Exception as e:
         | 
| 126 126 | 
             
                            raise CustomException(
         | 
| 127 127 | 
             
                                ErrorCode.INTERNAL_ERROR,
         | 
| 128 | 
            -
                                detail=str(e),
         | 
| 129 128 | 
             
                                source_function="rate_limit",
         | 
| 130 129 | 
             
                                original_error=e
         | 
| 131 130 | 
             
                            )
         | 
| @@ -136,20 +135,20 @@ def rate_limit( | |
| 136 135 | 
             
            async def create_jwt_token(
         | 
| 137 136 | 
             
                user_data: Dict[str, Any],
         | 
| 138 137 | 
             
                token_type: Literal["access", "refresh"],
         | 
| 139 | 
            -
                 | 
| 140 | 
            -
                 | 
| 138 | 
            +
                db_session: AsyncSession,
         | 
| 139 | 
            +
                token_settings: Dict[str, str],
         | 
| 140 | 
            +
                request: Optional[Request] = None,
         | 
| 141 141 | 
             
            ) -> str:
         | 
| 142 142 | 
             
                """JWT 토큰을 생성하고 로그를 기록합니다."""
         | 
| 143 143 | 
             
                try:
         | 
| 144 | 
            -
                     | 
| 145 | 
            -
                    
         | 
| 144 | 
            +
                    # 토큰 데이터 구성
         | 
| 146 145 | 
             
                    if token_type == "access":
         | 
| 147 | 
            -
                        expires_at = datetime.now(UTC) + timedelta(minutes= | 
| 146 | 
            +
                        expires_at = datetime.now(UTC) + timedelta(minutes=token_settings.ACCESS_TOKEN_EXPIRE_MINUTES)
         | 
| 148 147 | 
             
                        token_data = {
         | 
| 149 148 | 
             
                            # 등록 클레임
         | 
| 150 | 
            -
                            "iss":  | 
| 149 | 
            +
                            "iss": token_settings.TOKEN_ISSUER,
         | 
| 151 150 | 
             
                            "sub": user_data["username"],
         | 
| 152 | 
            -
                            "aud":  | 
| 151 | 
            +
                            "aud": token_settings.TOKEN_AUDIENCE,
         | 
| 153 152 | 
             
                            "exp": expires_at,
         | 
| 154 153 |  | 
| 155 154 | 
             
                            # 공개 클레임
         | 
| @@ -172,22 +171,23 @@ async def create_jwt_token( | |
| 172 171 | 
             
                    else:  # refresh token
         | 
| 173 172 | 
             
                        expires_at = datetime.now(UTC) + timedelta(days=14)
         | 
| 174 173 | 
             
                        token_data = {
         | 
| 175 | 
            -
                            "iss":  | 
| 174 | 
            +
                            "iss": token_settings.TOKEN_ISSUER,
         | 
| 176 175 | 
             
                            "sub": user_data["username"],
         | 
| 177 176 | 
             
                            "exp": expires_at,
         | 
| 178 177 | 
             
                            "token_type": token_type,
         | 
| 179 178 | 
             
                            "user_ulid": user_data["ulid"]
         | 
| 180 179 | 
             
                        }
         | 
| 181 180 |  | 
| 181 | 
            +
                    # JWT 토큰 생성
         | 
| 182 182 | 
             
                    try:
         | 
| 183 183 | 
             
                        token = jwt.encode(
         | 
| 184 184 | 
             
                            token_data,
         | 
| 185 | 
            -
                             | 
| 186 | 
            -
                            algorithm= | 
| 185 | 
            +
                            token_settings.JWT_SECRET,
         | 
| 186 | 
            +
                            algorithm=token_settings.JWT_ALGORITHM
         | 
| 187 187 | 
             
                        )
         | 
| 188 188 | 
             
                    except Exception as e:
         | 
| 189 189 | 
             
                        raise CustomException(
         | 
| 190 | 
            -
                            ErrorCode. | 
| 190 | 
            +
                            ErrorCode.TOKEN_ERROR,
         | 
| 191 191 | 
             
                            detail=f"token|{token_type}",
         | 
| 192 192 | 
             
                            source_function="security.create_jwt_token",
         | 
| 193 193 | 
             
                            original_error=e
         | 
| @@ -195,14 +195,18 @@ async def create_jwt_token( | |
| 195 195 |  | 
| 196 196 | 
             
                    # 로그 생성
         | 
| 197 197 | 
             
                    try:
         | 
| 198 | 
            -
                         | 
| 199 | 
            -
             | 
| 200 | 
            -
                             | 
| 201 | 
            -
             | 
| 202 | 
            -
             | 
| 203 | 
            -
             | 
| 204 | 
            -
             | 
| 205 | 
            -
             | 
| 198 | 
            +
                        log_data = {
         | 
| 199 | 
            +
                            "type": ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED,
         | 
| 200 | 
            +
                            "user_ulid": user_data["ulid"],
         | 
| 201 | 
            +
                            "token": token
         | 
| 202 | 
            +
                        }
         | 
| 203 | 
            +
             | 
| 204 | 
            +
                        # log_create 함수 호출
         | 
| 205 | 
            +
                        await log_create(
         | 
| 206 | 
            +
                            session=db_session,
         | 
| 207 | 
            +
                            table_name="user_logs",
         | 
| 208 | 
            +
                            log_data=log_data,
         | 
| 209 | 
            +
                            request=request
         | 
| 206 210 | 
             
                        )
         | 
| 207 211 | 
             
                    except Exception as e:
         | 
| 208 212 | 
             
                        # 로그 생성 실패는 토큰 생성에 영향을 주지 않음
         | 
| @@ -217,9 +221,10 @@ async def create_jwt_token( | |
| 217 221 | 
             
                        ErrorCode.INTERNAL_ERROR,
         | 
| 218 222 | 
             
                        detail=str(e),
         | 
| 219 223 | 
             
                        source_function="security.create_jwt_token",
         | 
| 220 | 
            -
                        original_error=e
         | 
| 224 | 
            +
                        original_error=str(e)
         | 
| 221 225 | 
             
                    )
         | 
| 222 226 |  | 
| 227 | 
            +
             | 
| 223 228 | 
             
            async def verify_jwt_token(
         | 
| 224 229 | 
             
                token: str,
         | 
| 225 230 | 
             
                expected_type: Optional[Literal["access", "refresh"]] = None
         | 
    
        aiteamutils/version.py
    CHANGED
    
    | @@ -1,2 +1,2 @@ | |
| 1 1 | 
             
            """버전 정보"""
         | 
| 2 | 
            -
            __version__ = "0.2. | 
| 2 | 
            +
            __version__ = "0.2.75"
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
         | 
| 2 | 
            +
            aiteamutils/base_model.py,sha256=TJiiA7bFhePObPJEzF1Qz4ekPfFoIxKGpe848BBdHLc,2840
         | 
| 3 | 
            +
            aiteamutils/base_repository.py,sha256=oTZP3mrkqZwAGv8cNqi0tp9wellvTuO6EXN1P1o7zRs,4285
         | 
| 4 | 
            +
            aiteamutils/base_service.py,sha256=tkGGVN8wrx9dmCVVnRSsoYGPmVD2H7NmEz-PoifIvW0,9300
         | 
| 5 | 
            +
            aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
         | 
| 6 | 
            +
            aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
         | 
| 7 | 
            +
            aiteamutils/database.py,sha256=8T8c7RpcPEcBXO-AU_-1UhnTI4RwkPLxWF0eWQYwnME,19080
         | 
| 8 | 
            +
            aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
         | 
| 9 | 
            +
            aiteamutils/exceptions.py,sha256=3FUCIqXgYmMqonnMgUlh-J2xtApiiCgg4WM-2UV4vmQ,15823
         | 
| 10 | 
            +
            aiteamutils/security.py,sha256=7cKY6XhiSEkQJe-XYE2tyYc2u1JLYeXUEjg7AUzEnQY,10277
         | 
| 11 | 
            +
            aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
         | 
| 12 | 
            +
            aiteamutils/version.py,sha256=FgfB6O4LkLSpldQbG5FG5Fai_x5fo9cg9AQ85w_3RF8,42
         | 
| 13 | 
            +
            aiteamutils-0.2.75.dist-info/METADATA,sha256=QtxRHW3XcGt5NndEB95Fue80VRGgM54zWbLtssOtkcI,1718
         | 
| 14 | 
            +
            aiteamutils-0.2.75.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
         | 
| 15 | 
            +
            aiteamutils-0.2.75.dist-info/RECORD,,
         | 
| @@ -1,15 +0,0 @@ | |
| 1 | 
            -
            aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
         | 
| 2 | 
            -
            aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
         | 
| 3 | 
            -
            aiteamutils/base_repository.py,sha256=tG_xz4hHYAN3-wkrLvEPxyTucV4pzT6dihoKVJp2JIc,2079
         | 
| 4 | 
            -
            aiteamutils/base_service.py,sha256=CfNBAgFREqOC1791ex2plKLZiSZ1U5el4GvvkVsN4qU,2901
         | 
| 5 | 
            -
            aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
         | 
| 6 | 
            -
            aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
         | 
| 7 | 
            -
            aiteamutils/database.py,sha256=CbX7eNFwqz9O4ywVQMLlLrb6hDUJPtsgGg_Hgef-p2I,8126
         | 
| 8 | 
            -
            aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
         | 
| 9 | 
            -
            aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
         | 
| 10 | 
            -
            aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
         | 
| 11 | 
            -
            aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
         | 
| 12 | 
            -
            aiteamutils/version.py,sha256=9RIZRehNxp6dA61_1-bTUirXc1q9QDosp-QZ_woFu0g,42
         | 
| 13 | 
            -
            aiteamutils-0.2.72.dist-info/METADATA,sha256=WsFA3r2H1XUIsDXmhenz97uj3eT5jyZpN4CbNbps8rA,1718
         | 
| 14 | 
            -
            aiteamutils-0.2.72.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
         | 
| 15 | 
            -
            aiteamutils-0.2.72.dist-info/RECORD,,
         | 
| 
            File without changes
         |