aiteamutils 0.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,158 @@
1
+ from typing import Type, Dict, Tuple, Any
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from fastapi import Depends, status
4
+ from fastapi.security import OAuth2PasswordBearer
5
+ from jose import JWTError, jwt
6
+
7
+ from .database import get_db, DatabaseService
8
+ from .exceptions import CustomException, ErrorCode
9
+
10
+ class Settings:
11
+ """기본 설정 클래스"""
12
+ def __init__(self, jwt_secret: str, jwt_algorithm: str = "HS256"):
13
+ self.JWT_SECRET = jwt_secret
14
+ self.JWT_ALGORITHM = jwt_algorithm
15
+
16
+ _settings: Settings | None = None
17
+
18
+ def init_settings(jwt_secret: str, jwt_algorithm: str = "HS256"):
19
+ """설정 초기화 함수
20
+
21
+ Args:
22
+ jwt_secret (str): JWT 시크릿 키
23
+ jwt_algorithm (str, optional): JWT 알고리즘. Defaults to "HS256".
24
+ """
25
+ global _settings
26
+ _settings = Settings(jwt_secret, jwt_algorithm)
27
+
28
+ def get_settings() -> Settings:
29
+ """현재 설정을 반환하는 함수
30
+
31
+ Returns:
32
+ Settings: 설정 객체
33
+
34
+ Raises:
35
+ RuntimeError: 설정이 초기화되지 않은 경우
36
+ """
37
+ if _settings is None:
38
+ raise RuntimeError("Settings not initialized. Call init_settings first.")
39
+ return _settings
40
+
41
+ class ServiceRegistry:
42
+ """서비스 레지스트리를 관리하는 클래스"""
43
+ def __init__(self):
44
+ self._services: Dict[str, Tuple[Type, Type]] = {}
45
+
46
+ def clear(self):
47
+ """등록된 모든 서비스를 초기화합니다."""
48
+ self._services.clear()
49
+
50
+ def register(self, name: str, repository_class: Type, service_class: Type):
51
+ """서비스를 레지스트리에 등록
52
+
53
+ Args:
54
+ name (str): 서비스 이름
55
+ repository_class (Type): Repository 클래스
56
+ service_class (Type): Service 클래스
57
+
58
+ Raises:
59
+ ValueError: 이미 등록된 서비스인 경우
60
+ """
61
+ if name in self._services:
62
+ raise ValueError(f"Service '{name}' is already registered.")
63
+ self._services[name] = (repository_class, service_class)
64
+
65
+ def get(self, name: str) -> Tuple[Type, Type]:
66
+ """등록된 서비스를 조회
67
+
68
+ Args:
69
+ name (str): 서비스 이름
70
+
71
+ Returns:
72
+ Tuple[Type, Type]: (Repository 클래스, Service 클래스) 튜플
73
+
74
+ Raises:
75
+ ValueError: 등록되지 않은 서비스인 경우
76
+ """
77
+ if name not in self._services:
78
+ raise ValueError(f"Service '{name}' is not registered.")
79
+ return self._services[name]
80
+
81
+ # ServiceRegistry 초기화
82
+ service_registry = ServiceRegistry()
83
+
84
+ def get_database_service(db: AsyncSession = Depends(get_db)) -> DatabaseService:
85
+ """DatabaseService 의존성
86
+
87
+ Args:
88
+ db (AsyncSession): 데이터베이스 세션
89
+
90
+ Returns:
91
+ DatabaseService: DatabaseService 인스턴스
92
+ """
93
+ return DatabaseService(db)
94
+
95
+ def get_service(name: str):
96
+ """등록된 서비스를 가져오는 의존성 함수
97
+
98
+ Args:
99
+ name (str): 서비스 이름
100
+
101
+ Returns:
102
+ Callable: 서비스 인스턴스를 반환하는 의존성 함수
103
+ """
104
+ def _get_service(db_service: DatabaseService = Depends(get_database_service)):
105
+ repository_class, service_class = service_registry.get(name)
106
+ repository = repository_class(db_service)
107
+ return service_class(repository, db_service)
108
+ return _get_service
109
+
110
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/users/token")
111
+ async def get_current_user(
112
+ token: str = Depends(oauth2_scheme),
113
+ db_service: DatabaseService = Depends(get_database_service)
114
+ ):
115
+ """현재 사용자를 가져오는 의존성 함수
116
+
117
+ Args:
118
+ token (str): OAuth2 토큰
119
+ db_service (DatabaseService): DatabaseService 객체
120
+
121
+ Returns:
122
+ User: 현재 사용자
123
+
124
+ Raises:
125
+ CustomException: 인증 실패 시 예외
126
+ """
127
+ try:
128
+ settings = get_settings()
129
+ payload = jwt.decode(
130
+ token,
131
+ settings.JWT_SECRET,
132
+ algorithms=[settings.JWT_ALGORITHM],
133
+ audience="ai-team"
134
+ )
135
+ user_ulid = payload.get("sub")
136
+ if not user_ulid:
137
+ raise CustomException(
138
+ ErrorCode.INVALID_TOKEN,
139
+ source_function="dependencies.py / get_current_user"
140
+ )
141
+ except JWTError:
142
+ raise CustomException(
143
+ ErrorCode.INVALID_TOKEN,
144
+ detail=token[:10] + "...",
145
+ source_function="dependencies.py / get_current_user"
146
+ )
147
+
148
+ from app.user.repository import UserRepository
149
+ user_repo = UserRepository(db_service)
150
+ user = await user_repo.get_user(user_ulid, by="ulid")
151
+
152
+ if not user:
153
+ raise CustomException(
154
+ ErrorCode.USER_NOT_FOUND,
155
+ source_function="dependencies.py / get_current_user"
156
+ )
157
+
158
+ return user
aiteamutils/enums.py ADDED
@@ -0,0 +1,23 @@
1
+ """시스템 전체에서 사용되는 열거형 정의."""
2
+ from enum import Enum
3
+
4
+ class UserStatus(str, Enum):
5
+ """사용자 상태."""
6
+ PENDING = "PENDING"
7
+ ACTIVE = "ACTIVE"
8
+ INACTIVE = "INACTIVE"
9
+ BLOCKED = "BLOCKED"
10
+
11
+ class ActivityType(str, Enum):
12
+ """시스템 활동 유형."""
13
+ # 인증 관련
14
+ ACCESS_TOKEN_ISSUED = "ACCESS_TOKEN_ISSUED"
15
+ REFRESH_TOKEN_ISSUED = "REFRESH_TOKEN_ISSUED"
16
+ LOGIN = "LOGIN"
17
+ LOGOUT = "LOGOUT"
18
+
19
+ # 사용자 관련
20
+ USER_CREATED = "USER_CREATED"
21
+ USER_UPDATED = "USER_UPDATED"
22
+ USER_DELETED = "USER_DELETED"
23
+ PASSWORD_CHANGE = "PASSWORD_CHANGE"
@@ -0,0 +1,333 @@
1
+ """예외 처리 모듈."""
2
+ import logging
3
+ from enum import Enum
4
+ from typing import Dict, Any, Optional, Tuple, List
5
+ from fastapi import Request
6
+ from fastapi.responses import JSONResponse
7
+ from fastapi.exceptions import RequestValidationError
8
+ from sqlalchemy.exc import IntegrityError, SQLAlchemyError, NoResultFound, MultipleResultsFound, ProgrammingError, OperationalError
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class ErrorResponse:
13
+ def __init__(self, code: int, value: str, status_code: int, detail_template: Optional[str] = None):
14
+ self.code = code
15
+ self.value = value
16
+ self.status_code = status_code
17
+ self.detail_template = detail_template
18
+
19
+ class ErrorCode(Enum):
20
+ # Auth 관련 에러: 1000번대
21
+ INVALID_CREDENTIALS = ErrorResponse(1001, "AUTH_INVALID_CREDENTIALS", 401, "잘못된 인증 정보입니다")
22
+ TOKEN_EXPIRED = ErrorResponse(1002, "AUTH_TOKEN_EXPIRED", 401, "토큰이 만료되었습니다")
23
+ INVALID_TOKEN = ErrorResponse(1003, "AUTH_INVALID_TOKEN", 401, "유효하지 않은 토큰입니다")
24
+ UNAUTHORIZED = ErrorResponse(1004, "AUTH_UNAUTHORIZED", 401, "인증이 필요합니다")
25
+ FORBIDDEN = ErrorResponse(1005, "AUTH_FORBIDDEN", 403, "접근 권한이 없습니다")
26
+ RATE_LIMIT_EXCEEDED = ErrorResponse(1006, "AUTH_RATE_LIMIT_EXCEEDED", 429, "너무 많은 요청이 발생했습니다")
27
+ INVALID_PASSWORD = ErrorResponse(1007, "AUTH_INVALID_PASSWORD", 401, "잘못된 비밀번호입니다")
28
+
29
+ # User 관련 에러: 2000번대
30
+ USER_NOT_FOUND = ErrorResponse(2001, "USER_NOT_FOUND", 404, "사용자를 찾을 수 없습니다")
31
+ USER_ALREADY_EXISTS = ErrorResponse(2002, "USER_ALREADY_EXISTS", 409, "이미 존재하는 사용자입니다")
32
+ INVALID_USER_DATA = ErrorResponse(2003, "USER_INVALID_DATA", 400, "잘못된 사용자 데이터입니다")
33
+ FIELD_INVALID_USERNAME = ErrorResponse(2005, "FIELD_INVALID_USERNAME", 400, "잘못된 사용자명입니다")
34
+ FIELD_INVALID_EMAIL = ErrorResponse(2006, "FIELD_INVALID_EMAIL", 400, "잘못된 이메일 형식입니다")
35
+ FIELD_INVALID_PHONE = ErrorResponse(2007, "FIELD_INVALID_PHONE", 400, "잘못된 전화번호 형식입니다")
36
+
37
+ # Password 관련 에러: 2100번대
38
+ FIELD_INVALID_PASSWORD_LENGTH = ErrorResponse(2101, "FIELD_INVALID_PASSWORD_LENGTH", 400, "비밀번호는 최소 8자 이상이어야 합니다")
39
+ FIELD_INVALID_PASSWORD_UPPER = ErrorResponse(2102, "FIELD_INVALID_PASSWORD_UPPER", 400, "비밀번호는 최소 1개의 대문자를 포함해야 합니다")
40
+ FIELD_INVALID_PASSWORD_NUMBER = ErrorResponse(2103, "FIELD_INVALID_PASSWORD_NUMBER", 400, "비밀번호는 최소 1개의 숫자를 포함해야 합니다")
41
+ FIELD_INVALID_PASSWORD_SPECIAL = ErrorResponse(2104, "FIELD_INVALID_PASSWORD_SPECIAL", 400, "비밀번호는 최소 1개의 특수문자를 포함해야 합니다")
42
+ FIELD_INVALID_PASSWORD_MISMATCH = ErrorResponse(2105, "FIELD_INVALID_PASSWORD_MISMATCH", 400, "새 비밀번호와 확인 비밀번호가 일치하지 않습니다")
43
+
44
+ # ID 형식 관련 에러: 2200번대
45
+ FIELD_INVALID_ID_LENGTH = ErrorResponse(2201, "FIELD_INVALID_ID_LENGTH", 400, "ID는 최소 3자 이상이어야 합니다")
46
+ FIELD_INVALID_ID_CHARS = ErrorResponse(2202, "FIELD_INVALID_ID_CHARS", 400, "ID는 영문 소문자, 숫자, 언더스코어만 사용할 수 있습니다")
47
+
48
+ # Database 관련 에러: 3000번대 세분화
49
+ DB_CONNECTION_ERROR = ErrorResponse(3001, "DB_CONNECTION_ERROR", 500, "데이터베이스 연결 오류")
50
+ DB_QUERY_ERROR = ErrorResponse(3002, "DB_QUERY_ERROR", 500, "데이터베이스 쿼리 오류")
51
+ DUPLICATE_ERROR = ErrorResponse(3003, "DB_DUPLICATE_ERROR", 409, "중복된 데이터가 존재합니다")
52
+ FOREIGN_KEY_VIOLATION = ErrorResponse(3004, "DB_FOREIGN_KEY_VIOLATION", 400, "참조하는 데이터가 존재하지 않습니다")
53
+ TRANSACTION_ERROR = ErrorResponse(3005, "DB_TRANSACTION_ERROR", 500, "트랜잭션 처리 중 오류가 발생했습니다")
54
+ DB_READ_ERROR = ErrorResponse(3006, "DB_READ_ERROR", 500, "데이터베이스 읽기 오류가 발생했습니다")
55
+ DB_CREATE_ERROR = ErrorResponse(3007, "DB_CREATE_ERROR", 500, "데이터베이스 생성 오류가 발생했습니다")
56
+ DB_UPDATE_ERROR = ErrorResponse(3008, "DB_UPDATE_ERROR", 500, "데이터베이스 업데이트 오류가 발생했습니다")
57
+ DB_DELETE_ERROR = ErrorResponse(3009, "DB_DELETE_ERROR", 500, "데이터베이스 삭제 오류가 발생했습니다")
58
+ DB_MULTIPLE_RESULTS = ErrorResponse(3010, "DB_MULTIPLE_RESULTS", 500, "중복된 데이터가 조회되었습니다")
59
+ DB_NO_RESULT = ErrorResponse(3011, "DB_NO_RESULT", 404, "데이터를 찾을 수 없습니다")
60
+ DB_INVALID_QUERY = ErrorResponse(3012, "DB_INVALID_QUERY", 500, "잘못된 쿼리 구문입니다")
61
+ DB_OPERATIONAL_ERROR = ErrorResponse(3013, "DB_OPERATIONAL_ERROR", 500, "데이터베이스 작업 중 오류가 발생했습니다")
62
+
63
+ # Validation 관련 에러: 4000번대
64
+ VALIDATION_ERROR = ErrorResponse(4001, "VALIDATION_ERROR", 422, "유효성 검사 오류")
65
+ FIELD_INVALID_FORMAT = ErrorResponse(4002, "FIELD_INVALID_FORMAT", 400, "잘못된 형식입니다")
66
+ REQUIRED_FIELD_MISSING = ErrorResponse(4003, "VALIDATION_REQUIRED_FIELD_MISSING", 400, "필수 필드가 누락되었습니다")
67
+
68
+ # General 에러: 5000번대
69
+ NOT_FOUND = ErrorResponse(5001, "GENERAL_NOT_FOUND", 404, "리소스를 찾을 수 없습니다")
70
+ INTERNAL_ERROR = ErrorResponse(5002, "GENERAL_INTERNAL_ERROR", 500, "내부 서버 오류")
71
+ SERVICE_UNAVAILABLE = ErrorResponse(5003, "GENERAL_SERVICE_UNAVAILABLE", 503, "서비스를 사용할 수 없습니다")
72
+
73
+ class CustomException(Exception):
74
+ """사용자 정의 예외 클래스"""
75
+
76
+ def __init__(
77
+ self,
78
+ error_code: ErrorCode,
79
+ detail: str,
80
+ source_function: str,
81
+ original_error: Optional[Exception] = None,
82
+ parent_source_function: Optional[str] = None
83
+ ):
84
+ self.error_code = error_code
85
+ self.detail = detail
86
+ self.source_function = source_function
87
+ self.original_error = original_error
88
+
89
+ # 상위 함수 경로 연결
90
+ if parent_source_function:
91
+ self.source_function = f"{parent_source_function}/{self.source_function}"
92
+
93
+ # 에러 체인 구성
94
+ self.error_chain = []
95
+ if isinstance(original_error, CustomException):
96
+ self.error_chain = original_error.error_chain.copy()
97
+
98
+ self.error_chain.append({
99
+ "error_code": str(self.error_code),
100
+ "detail": str(self.detail),
101
+ "source_function": self.source_function,
102
+ "original_error": str(self.original_error) if self.original_error else None
103
+ })
104
+
105
+ def get_error_chain(self) -> List[Dict[str, Any]]:
106
+ """에러 발생 경로 상세 정보를 반환합니다."""
107
+ return self.error_chain
108
+
109
+ def get_original_error(self) -> Optional[Exception]:
110
+ """원본 에러를 반환합니다."""
111
+ return self.original_error
112
+
113
+ def to_dict(self) -> Dict[str, Any]:
114
+ """에러 정보를 딕셔너리로 반환합니다."""
115
+ return {
116
+ "error_code": self.error_code.name,
117
+ "detail": self.detail,
118
+ "source_function": self.source_function,
119
+ "error_chain": self.error_chain,
120
+ "original_error": str(self.original_error) if self.original_error else None
121
+ }
122
+
123
+ def get_error_details(request: Request, exc: CustomException) -> Tuple[Dict[str, Any], Dict[str, Any]]:
124
+ """에러 상세 정보를 생성합니다.
125
+
126
+ Args:
127
+ request (Request): FastAPI 요청 객체
128
+ exc (CustomException): 발생한 예외
129
+
130
+ Returns:
131
+ Tuple[Dict[str, Any], Dict[str, Any]]: (로그 데이터, 응답 데이터)
132
+ """
133
+ # 공통 데이터
134
+ base_data = {
135
+ "error_code": exc.error_code.value.code,
136
+ "error_type": exc.error_code.name,
137
+ "status_code": exc.error_code.value.status_code,
138
+ "path": request.url.path,
139
+ "method": request.method,
140
+ "client_ip": request.client.host,
141
+ "user_agent": request.headers.get("user-agent"),
142
+ "detail": exc.detail,
143
+ }
144
+
145
+ # 로그 데이터에는 error_chain, source_function, detail 포함
146
+ log_data = {
147
+ **base_data,
148
+ "source_function": exc.source_function,
149
+ "error_chain": exc.error_chain,
150
+ "original_error": str(exc.original_error) if exc.original_error else None
151
+ }
152
+
153
+ # 응답 데이터에는 error_chain, source_function, detail 제외
154
+ response_data = base_data.copy()
155
+
156
+ return log_data, response_data
157
+
158
+ def log_error(log_data: Dict[str, Any]):
159
+ """로그 데이터 기록 (다중 줄 포맷)"""
160
+ log_message = ["Request failed:"]
161
+
162
+ # 기본 에러 정보
163
+ basic_info = [
164
+ "error_code", "error_type", "status_code", "path",
165
+ "method", "client_ip", "user_agent"
166
+ ]
167
+ for key in basic_info:
168
+ if key in log_data:
169
+ log_message.append(f" {key}: {log_data[key]}")
170
+
171
+ # 상세 에러 정보
172
+ if "detail" in log_data:
173
+ log_message.append(" detail:")
174
+ for line in str(log_data["detail"]).split("\n"):
175
+ log_message.append(f" {line}")
176
+
177
+ # 에러 체인 정보
178
+ if "error_chain" in log_data:
179
+ log_message.append(" error_chain:")
180
+ for error in log_data["error_chain"]:
181
+ log_message.append(" Error:")
182
+ for key, value in error.items():
183
+ if value:
184
+ if key == "detail":
185
+ log_message.append(" detail:")
186
+ for line in str(value).split("\n"):
187
+ log_message.append(f" {line}")
188
+ else:
189
+ log_message.append(f" {key}: {value}")
190
+
191
+ # 원본 에러 정보
192
+ if "original_error" in log_data and log_data["original_error"]:
193
+ log_message.append(" original_error:")
194
+ for line in str(log_data["original_error"]).split("\n"):
195
+ log_message.append(f" {line}")
196
+
197
+ logger.error("\n".join(log_message), extra=log_data)
198
+
199
+ async def custom_exception_handler(request: Request, exc: CustomException):
200
+ """CustomException에 대한 기본 핸들러"""
201
+ # 로그 데이터와 응답 데이터 생성
202
+ log_data, response_data = get_error_details(
203
+ request=request,
204
+ exc=exc,
205
+ )
206
+
207
+ # 로그 기록
208
+ log_error(log_data)
209
+
210
+ # 클라이언트 응답 반환
211
+ return JSONResponse(
212
+ status_code=exc.error_code.value.status_code,
213
+ content=response_data,
214
+ )
215
+
216
+ async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
217
+ """FastAPI의 RequestValidationError를 처리합니다."""
218
+ missing_fields = []
219
+ for error in exc.errors():
220
+ if error["type"] == "missing":
221
+ missing_fields.append(error["loc"][1])
222
+
223
+ error = CustomException(
224
+ ErrorCode.REQUIRED_FIELD_MISSING,
225
+ detail="|".join(missing_fields),
226
+ source_function="FastAPI.request_validation_handler",
227
+ original_error=exc
228
+ )
229
+ return await custom_exception_handler(request, error)
230
+
231
+ async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError):
232
+ """SQLAlchemy 관련 예외 처리"""
233
+ error_str = str(exc)
234
+ error_class = exc.__class__.__name__
235
+ error_module = exc.__class__.__module__
236
+
237
+ # 상세 에러 정보 구성
238
+ error_details = f"{error_module}.{error_class}: {error_str}"
239
+
240
+ # 스택 트레이스가 있다면 포함
241
+ if hasattr(exc, '__traceback__'):
242
+ import traceback
243
+ stack_trace = ''.join(traceback.format_tb(exc.__traceback__))
244
+ error_details = f"{error_details}\nStack trace:\n{stack_trace}"
245
+
246
+ # SQL 쿼리 정보 추가 (가능한 경우)
247
+ if hasattr(exc, 'statement'):
248
+ error_details = f"{error_details}\nFailed SQL Query: {exc.statement}"
249
+ if hasattr(exc, 'params'):
250
+ error_details = f"{error_details}\nQuery Parameters: {exc.params}"
251
+
252
+ error = None
253
+
254
+ # SQLAlchemy 예외 타입별 세분화된 처리
255
+ if isinstance(exc, IntegrityError):
256
+ if "violates foreign key constraint" in error_str:
257
+ error = CustomException(
258
+ ErrorCode.FOREIGN_KEY_VIOLATION,
259
+ detail=error_details,
260
+ source_function="DatabaseService.execute_operation",
261
+ original_error=exc
262
+ )
263
+ elif "duplicate key" in error_str:
264
+ error = CustomException(
265
+ ErrorCode.DUPLICATE_ERROR,
266
+ detail=error_details,
267
+ source_function="DatabaseService.execute_operation",
268
+ original_error=exc
269
+ )
270
+ else:
271
+ error = CustomException(
272
+ ErrorCode.DB_QUERY_ERROR,
273
+ detail=error_details,
274
+ source_function="DatabaseService.execute_operation",
275
+ original_error=exc
276
+ )
277
+ elif isinstance(exc, NoResultFound):
278
+ error = CustomException(
279
+ ErrorCode.DB_NO_RESULT,
280
+ detail=error_details,
281
+ source_function="DatabaseService.execute_operation",
282
+ original_error=exc
283
+ )
284
+ elif isinstance(exc, MultipleResultsFound):
285
+ error = CustomException(
286
+ ErrorCode.DB_MULTIPLE_RESULTS,
287
+ detail=error_details,
288
+ source_function="DatabaseService.execute_operation",
289
+ original_error=exc
290
+ )
291
+ elif isinstance(exc, ProgrammingError):
292
+ error = CustomException(
293
+ ErrorCode.DB_INVALID_QUERY,
294
+ detail=error_details,
295
+ source_function="DatabaseService.execute_operation",
296
+ original_error=exc
297
+ )
298
+ elif isinstance(exc, OperationalError):
299
+ error = CustomException(
300
+ ErrorCode.DB_OPERATIONAL_ERROR,
301
+ detail=error_details,
302
+ source_function="DatabaseService.execute_operation",
303
+ original_error=exc
304
+ )
305
+ else:
306
+ error = CustomException(
307
+ ErrorCode.DB_QUERY_ERROR,
308
+ detail=error_details,
309
+ source_function="DatabaseService.execute_operation",
310
+ original_error=exc
311
+ )
312
+
313
+ # 로그에 추가 정보 기록
314
+ logger.error(
315
+ "Database Error Details:\n"
316
+ f"Error Type: {error_class}\n"
317
+ f"Error Module: {error_module}\n"
318
+ f"Error Message: {error_str}\n"
319
+ f"Request Path: {request.url.path}\n"
320
+ f"Request Method: {request.method}\n"
321
+ f"Client IP: {request.client.host}"
322
+ )
323
+
324
+ return await custom_exception_handler(request, error)
325
+
326
+ async def generic_exception_handler(request: Request, exc: Exception):
327
+ """처리되지 않은 예외를 위한 기본 핸들러"""
328
+ error = CustomException(
329
+ ErrorCode.INTERNAL_ERROR,
330
+ detail=str(exc),
331
+ source_function="GenericExceptionHandler.handle_exception"
332
+ )
333
+ return await custom_exception_handler(request, error)