aiteamutils 0.2.0__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.
@@ -0,0 +1,668 @@
1
+ """기본 서비스 모듈."""
2
+ from datetime import datetime
3
+ from typing import TypeVar, Generic, Dict, Any, List, Optional, Type, Union
4
+ from sqlalchemy.orm import DeclarativeBase
5
+ from sqlalchemy.exc import IntegrityError, SQLAlchemyError
6
+ from .database import DatabaseService
7
+ from .exceptions import CustomException, ErrorCode
8
+ from .base_repository import BaseRepository
9
+ from .security import hash_password
10
+ from fastapi import Request
11
+ from ulid import ULID
12
+
13
+ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
14
+
15
+ class BaseService(Generic[ModelType]):
16
+
17
+ ##################
18
+ # 1. 초기화 영역 #
19
+ ##################
20
+ def __init__(
21
+ self,
22
+ repository: BaseRepository[ModelType],
23
+ additional_models: Dict[str, Type[DeclarativeBase]] = None
24
+ ):
25
+ """
26
+ Args:
27
+ repository (BaseRepository[ModelType]): 레포지토리 인스턴스
28
+ additional_models (Dict[str, Type[DeclarativeBase]], optional): 추가 모델 매핑. Defaults to None.
29
+ """
30
+ self.repository = repository
31
+ self.model = repository.model
32
+ self.additional_models = additional_models or {}
33
+ self.db_service = repository.db_service
34
+ self.searchable_fields = {
35
+ "name": {"type": "text", "description": "이름"},
36
+ "organization_ulid": {"type": "exact", "description": "조직 ID"}
37
+ }
38
+
39
+ #########################
40
+ # 2. 이벤트 처리 메서드 #
41
+ #########################
42
+ async def pre_save(self, data: Dict[str, Any]) -> Dict[str, Any]:
43
+ """저장 전 처리를 수행합니다.
44
+
45
+ Args:
46
+ data (Dict[str, Any]): 저장할 데이터
47
+
48
+ Returns:
49
+ Dict[str, Any]: 처리된 데이터
50
+ """
51
+ return data
52
+
53
+ async def post_save(self, entity: ModelType) -> None:
54
+ """저장 후 처리를 수행합니다.
55
+
56
+ Args:
57
+ entity (ModelType): 저장된 엔티티
58
+ """
59
+ pass
60
+
61
+ async def pre_delete(self, ulid: str) -> None:
62
+ """삭제 전 처리를 수행합니다.
63
+
64
+ Args:
65
+ ulid (str): 삭제할 엔티티의 ULID
66
+ """
67
+ pass
68
+
69
+ async def post_delete(self, ulid: str) -> None:
70
+ """삭제 후 처리를 수행합니다.
71
+
72
+ Args:
73
+ ulid (str): 삭제된 엔티티의 ULID
74
+ """
75
+ pass
76
+
77
+ ######################
78
+ # 3. 캐시 관리 메서드 #
79
+ ######################
80
+ async def get_from_cache(self, key: str) -> Optional[Any]:
81
+ """캐시에서 데이터를 조회합니다.
82
+
83
+ Args:
84
+ key (str): 캐시 키
85
+
86
+ Returns:
87
+ Optional[Any]: 캐시된 데이터 또는 None
88
+ """
89
+ return None
90
+
91
+ async def set_to_cache(self, key: str, value: Any, ttl: int = 3600) -> None:
92
+ """데이터를 캐시에 저장합니다.
93
+
94
+ Args:
95
+ key (str): 캐시 키
96
+ value (Any): 저장할 값
97
+ ttl (int, optional): 캐시 유효 시간(초). Defaults to 3600.
98
+ """
99
+ pass
100
+
101
+ async def invalidate_cache(self, key: str) -> None:
102
+ """캐시를 무효화합니다.
103
+
104
+ Args:
105
+ key (str): 캐시 키
106
+ """
107
+ pass
108
+
109
+ ##########################
110
+ # 4. 비즈니스 검증 메서드 #
111
+ ##########################
112
+ def _validate_business_rules(self, data: Dict[str, Any]) -> None:
113
+ """비즈니스 규칙을 검증합니다.
114
+
115
+ Args:
116
+ data (Dict[str, Any]): 검증할 데이터
117
+
118
+ Raises:
119
+ CustomException: 비즈니스 규칙 위반 시
120
+ """
121
+ pass
122
+
123
+ def _validate_permissions(self, request: Request, action: str) -> None:
124
+ """권한을 검증합니다.
125
+
126
+ Args:
127
+ request (Request): FastAPI 요청 객체
128
+ action (str): 수행할 작업
129
+
130
+ Raises:
131
+ CustomException: 권한이 없는 경우
132
+ """
133
+ pass
134
+
135
+ ########################
136
+ # 5. 응답 처리 메서드 #
137
+ ########################
138
+ def _handle_response_model(self, entity: ModelType, response_model: Any) -> Dict[str, Any]:
139
+ """응답 모델에 맞게 데이터를 처리합니다.
140
+
141
+ Args:
142
+ entity (ModelType): 처리할 엔티티
143
+ response_model (Any): 응답 모델
144
+
145
+ Returns:
146
+ Dict[str, Any]: 처리된 데이터
147
+ """
148
+ if not response_model:
149
+ return self._process_response(entity)
150
+
151
+ result = self._process_response(entity)
152
+
153
+ # response_model에 없는 필드 제거
154
+ keys_to_remove = [key for key in result if key not in response_model.model_fields]
155
+ for key in keys_to_remove:
156
+ result.pop(key)
157
+
158
+ # 모델 검증
159
+ return response_model(**result).model_dump()
160
+
161
+ def _handle_exclude_fields(self, data: Dict[str, Any], exclude_fields: List[str]) -> Dict[str, Any]:
162
+ """제외할 필드를 처리합니다.
163
+
164
+ Args:
165
+ data (Dict[str, Any]): 처리할 데이터
166
+ exclude_fields (List[str]): 제외할 필드 목록
167
+
168
+ Returns:
169
+ Dict[str, Any]: 처리된 데이터
170
+ """
171
+ if not exclude_fields:
172
+ return data
173
+
174
+ return {k: v for k, v in data.items() if k not in exclude_fields}
175
+
176
+ def _validate_ulid(self, ulid: str) -> bool:
177
+ """ULID 형식을 검증합니다.
178
+
179
+ Args:
180
+ ulid (str): 검증할 ULID
181
+
182
+ Returns:
183
+ bool: 유효한 ULID 여부
184
+ """
185
+ try:
186
+ ULID.from_str(ulid)
187
+ return True
188
+ except (ValueError, AttributeError):
189
+ return False
190
+
191
+ def _process_columns(self, entity: ModelType, exclude_extra_data: bool = True) -> Dict[str, Any]:
192
+ """엔티티의 컬럼들을 처리합니다.
193
+
194
+ Args:
195
+ entity (ModelType): 처리할 엔티티
196
+ exclude_extra_data (bool, optional): extra_data 컬럼 제외 여부. Defaults to True.
197
+
198
+ Returns:
199
+ Dict[str, Any]: 처리된 컬럼 데이터
200
+ """
201
+ result = {}
202
+ for column in entity.__table__.columns:
203
+ if exclude_extra_data and column.name == 'extra_data':
204
+ continue
205
+
206
+ # 필드 값 처리
207
+ if hasattr(entity, column.name):
208
+ value = getattr(entity, column.name)
209
+ if isinstance(value, datetime):
210
+ value = value.isoformat()
211
+ result[column.name] = value
212
+ elif hasattr(entity, 'extra_data') and isinstance(entity.extra_data, dict):
213
+ result[column.name] = entity.extra_data.get(column.name)
214
+ else:
215
+ result[column.name] = None
216
+
217
+ # extra_data의 내용을 최상위 레벨로 업데이트
218
+ if hasattr(entity, 'extra_data') and isinstance(entity.extra_data, dict):
219
+ result.update(entity.extra_data or {})
220
+
221
+ return result
222
+
223
+ def _process_response(self, entity: ModelType, response_model: Any = None) -> Dict[str, Any]:
224
+ """응답 데이터를 처리합니다.
225
+ extra_data의 내용을 최상위 레벨로 변환하고, 라우터에서 선언한 응답 스키마에 맞게 데이터를 변환합니다.
226
+
227
+ Args:
228
+ entity (ModelType): 처리할 엔티티
229
+ response_model (Any, optional): 응답 스키마. Defaults to None.
230
+
231
+ Returns:
232
+ Dict[str, Any]: 처리된 엔티티 데이터
233
+ """
234
+ if not entity:
235
+ return None
236
+
237
+ # 모든 필드 처리
238
+ result = self._process_columns(entity)
239
+
240
+ # Relationship 처리 (이미 로드된 관계만 처리)
241
+ for relationship in entity.__mapper__.relationships:
242
+ if not relationship.key in entity.__dict__:
243
+ continue
244
+
245
+ try:
246
+ value = getattr(entity, relationship.key)
247
+ # response_model이 있는 경우 해당 필드의 annotation type을 가져옴
248
+ nested_response_model = None
249
+ if response_model and relationship.key in response_model.model_fields:
250
+ field_info = response_model.model_fields[relationship.key]
251
+ nested_response_model = field_info.annotation
252
+
253
+ if value is not None:
254
+ if isinstance(value, list):
255
+ result[relationship.key] = [
256
+ self._process_response(item, nested_response_model)
257
+ for item in value
258
+ ]
259
+ else:
260
+ result[relationship.key] = self._process_response(value, nested_response_model)
261
+ else:
262
+ result[relationship.key] = None
263
+ except Exception:
264
+ result[relationship.key] = None
265
+
266
+ # response_model이 있는 경우 필터링
267
+ if response_model:
268
+ # 현재 키 목록을 저장
269
+ current_keys = list(result.keys())
270
+ # response_model에 없는 키 제거
271
+ for key in current_keys:
272
+ if key not in response_model.model_fields:
273
+ result.pop(key)
274
+ # 모델 검증 및 업데이트
275
+ result.update(response_model(**result).model_dump())
276
+
277
+ return result
278
+
279
+ def _process_basic_fields(self, entity: ModelType) -> Dict[str, Any]:
280
+ """엔티티의 기본 필드만 처리합니다.
281
+
282
+ Args:
283
+ entity (ModelType): 처리할 엔티티
284
+
285
+ Returns:
286
+ Dict[str, Any]: 기본 필드만 포함된 딕셔너리
287
+ """
288
+ if not entity:
289
+ return None
290
+
291
+ return self._process_columns(entity)
292
+
293
+ async def _create_for_model(self, model_name: str, data: Dict[str, Any], exclude_fields: List[str] = None) -> DeclarativeBase:
294
+ """지정된 모델에 대해 새로운 엔티티를 생성합니다.
295
+
296
+ Args:
297
+ model_name (str): 생성할 모델 이름
298
+ data (Dict[str, Any]): 생성할 엔티티 데이터
299
+ exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
300
+
301
+ Returns:
302
+ DeclarativeBase: 생성된 엔티티
303
+
304
+ Raises:
305
+ CustomException: 데이터베이스 작업 중 오류 발생 시
306
+ """
307
+ if model_name not in self.additional_models:
308
+ raise CustomException(
309
+ ErrorCode.INVALID_REQUEST,
310
+ detail=f"Model {model_name} not registered",
311
+ source_function=f"{self.__class__.__name__}._create_for_model"
312
+ )
313
+
314
+ try:
315
+ # 제외할 필드 처리
316
+ if exclude_fields:
317
+ data = {k: v for k, v in data.items() if k not in exclude_fields}
318
+
319
+ return await self.db_service.create_entity(self.additional_models[model_name], data)
320
+ except CustomException as e:
321
+ raise e
322
+ except Exception as e:
323
+ raise CustomException(
324
+ ErrorCode.DB_CREATE_ERROR,
325
+ detail=str(e),
326
+ source_function=f"{self.__class__.__name__}._create_for_model",
327
+ original_error=e
328
+ )
329
+
330
+ def _process_password(self, data: Dict[str, Any]) -> Dict[str, Any]:
331
+ """비밀번호 필드가 있는 경우 해시화합니다.
332
+
333
+ Args:
334
+ data (Dict[str, Any]): 처리할 데이터
335
+
336
+ Returns:
337
+ Dict[str, Any]: 처리된 데이터
338
+ """
339
+ if "password" in data:
340
+ data["password"] = hash_password(data["password"])
341
+ return data
342
+
343
+ #######################
344
+ # 6. CRUD 작업 메서드 #
345
+ #######################
346
+ async def create(self, data: Dict[str, Any], exclude_fields: List[str] = None, model_name: str = None) -> Union[ModelType, DeclarativeBase]:
347
+ """새로운 엔티티를 생성합니다.
348
+
349
+ Args:
350
+ data (Dict[str, Any]): 생성할 엔티티 데이터
351
+ exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
352
+ model_name (str, optional): 생성할 모델 이름. Defaults to None.
353
+
354
+ Returns:
355
+ Union[ModelType, DeclarativeBase]: 생성된 엔티티
356
+
357
+ Raises:
358
+ CustomException: 데이터베이스 작업 중 오류 발생 시
359
+ """
360
+ try:
361
+ # 비밀번호 해시화
362
+ data = self._process_password(data)
363
+
364
+ # 제외할 필드 처리
365
+ if exclude_fields:
366
+ data = {k: v for k, v in data.items() if k not in exclude_fields}
367
+
368
+ if model_name:
369
+ return await self._create_for_model(model_name, data)
370
+
371
+ return await self.repository.create(data)
372
+ except CustomException as e:
373
+ raise e
374
+ except Exception as e:
375
+ raise CustomException(
376
+ ErrorCode.DB_CREATE_ERROR,
377
+ detail=str(e),
378
+ source_function=f"{self.__class__.__name__}.create",
379
+ original_error=e
380
+ )
381
+
382
+ async def update(
383
+ self,
384
+ ulid: str,
385
+ data: Dict[str, Any],
386
+ exclude_fields: List[str] = None,
387
+ model_name: str = None
388
+ ) -> Optional[ModelType]:
389
+ """기존 엔티티를 수정합니다.
390
+
391
+ Args:
392
+ ulid (str): 수정할 엔티티의 ULID
393
+ data (Dict[str, Any]): 수정할 데이터
394
+ exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
395
+ model_name (str, optional): 수정할 모델 이름. Defaults to None.
396
+
397
+ Returns:
398
+ Optional[ModelType]: 수정된 엔티티, 없으면 None
399
+
400
+ Raises:
401
+ CustomException: 데이터베이스 작업 중 오류 발생 시
402
+ """
403
+ try:
404
+ # 비밀번호 해시화
405
+ data = self._process_password(data)
406
+
407
+ # 제외할 필드 처리
408
+ if exclude_fields:
409
+ data = {k: v for k, v in data.items() if k not in exclude_fields}
410
+
411
+ async with self.db_service.transaction():
412
+ if model_name:
413
+ if model_name not in self.additional_models:
414
+ raise CustomException(
415
+ ErrorCode.INVALID_REQUEST,
416
+ detail=f"Model {model_name} not registered",
417
+ source_function=f"{self.__class__.__name__}.update"
418
+ )
419
+ entity = await self.db_service.update_entity(
420
+ self.additional_models[model_name],
421
+ {"ulid": ulid},
422
+ data
423
+ )
424
+ if not entity:
425
+ raise CustomException(
426
+ ErrorCode.NOT_FOUND,
427
+ detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
428
+ source_function=f"{self.__class__.__name__}.update"
429
+ )
430
+ return entity
431
+
432
+ entity = await self.repository.update(ulid, data)
433
+ if not entity:
434
+ raise CustomException(
435
+ ErrorCode.NOT_FOUND,
436
+ detail=f"{self.model.__tablename__}|ulid|{ulid}",
437
+ source_function=f"{self.__class__.__name__}.update"
438
+ )
439
+ return entity
440
+ except CustomException as e:
441
+ raise e
442
+ except Exception as e:
443
+ raise CustomException(
444
+ ErrorCode.DB_UPDATE_ERROR,
445
+ detail=str(e),
446
+ source_function=f"{self.__class__.__name__}.update",
447
+ original_error=e
448
+ )
449
+
450
+ async def delete(self, ulid: str, model_name: str = None) -> bool:
451
+ """엔티티를 소프트 삭제합니다 (is_deleted = True).
452
+
453
+ Args:
454
+ ulid (str): 삭제할 엔티티의 ULID
455
+ model_name (str, optional): 삭제할 모델 이름. Defaults to None.
456
+
457
+ Returns:
458
+ bool: 삭제 성공 여부
459
+
460
+ Raises:
461
+ CustomException: 데이터베이스 작업 중 오류 발생 시
462
+ """
463
+ try:
464
+ if model_name:
465
+ if model_name not in self.additional_models:
466
+ raise CustomException(
467
+ ErrorCode.INVALID_REQUEST,
468
+ detail=f"Model {model_name} not registered",
469
+ source_function=f"{self.__class__.__name__}.delete"
470
+ )
471
+ entity = await self.db_service.soft_delete_entity(self.additional_models[model_name], ulid)
472
+ if not entity:
473
+ raise CustomException(
474
+ ErrorCode.NOT_FOUND,
475
+ detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
476
+ source_function=f"{self.__class__.__name__}.delete"
477
+ )
478
+ return True
479
+
480
+ entity = await self.repository.delete(ulid)
481
+ if not entity:
482
+ raise CustomException(
483
+ ErrorCode.NOT_FOUND,
484
+ detail=f"{self.model.__tablename__}|ulid|{ulid}",
485
+ source_function=f"{self.__class__.__name__}.delete"
486
+ )
487
+ return True
488
+ except CustomException as e:
489
+ raise e
490
+ except Exception as e:
491
+ raise CustomException(
492
+ ErrorCode.DB_DELETE_ERROR,
493
+ detail=str(e),
494
+ source_function=f"{self.__class__.__name__}.delete",
495
+ original_error=e
496
+ )
497
+
498
+ async def real_row_delete(self, ulid: str, model_name: str = None) -> bool:
499
+ """엔티티를 실제로 삭제합니다.
500
+
501
+ Args:
502
+ ulid (str): 삭제할 엔티티의 ULID
503
+ model_name (str, optional): 삭제할 모델 이름. Defaults to None.
504
+
505
+ Returns:
506
+ bool: 삭제 성공 여부
507
+
508
+ Raises:
509
+ CustomException: 데이터베이스 작업 중 오류 발생 시
510
+ """
511
+ try:
512
+ if model_name:
513
+ if model_name not in self.additional_models:
514
+ raise CustomException(
515
+ ErrorCode.INVALID_REQUEST,
516
+ detail=f"Model {model_name} not registered",
517
+ source_function=f"{self.__class__.__name__}.real_row_delete"
518
+ )
519
+ entity = await self.db_service.retrieve_entity(
520
+ self.additional_models[model_name],
521
+ {"ulid": ulid}
522
+ )
523
+ if entity:
524
+ await self.db_service.delete_entity(entity)
525
+ return True
526
+ return False
527
+
528
+ return await self.repository.real_row_delete(ulid)
529
+ except CustomException as e:
530
+ raise e
531
+ except Exception as e:
532
+ raise CustomException(
533
+ ErrorCode.DB_DELETE_ERROR,
534
+ detail=str(e),
535
+ source_function=f"{self.__class__.__name__}.real_row_delete",
536
+ original_error=e
537
+ )
538
+
539
+ #########################
540
+ # 7. 조회 및 검색 메서드 #
541
+ #########################
542
+ async def list(
543
+ self,
544
+ skip: int = 0,
545
+ limit: int = 100,
546
+ filters: Dict[str, Any] | None = None,
547
+ search_params: Dict[str, Any] | None = None,
548
+ model_name: str | None = None,
549
+ request: Request | None = None,
550
+ response_model: Any = None
551
+ ) -> List[Dict[str, Any]]:
552
+ """엔티티 목록을 조회합니다.
553
+
554
+ Args:
555
+ skip (int, optional): 건너뛸 레코드 수. Defaults to 0.
556
+ limit (int, optional): 조회할 최대 레코드 수. Defaults to 100.
557
+ filters (Dict[str, Any] | None, optional): 필터링 조건. Defaults to None.
558
+ search_params (Dict[str, Any] | None, optional): 검색 파라미터. Defaults to None.
559
+ model_name (str | None, optional): 조회할 모델 이름. Defaults to None.
560
+ request (Request | None, optional): 요청 객체. Defaults to None.
561
+ response_model (Any, optional): 응답 스키마. Defaults to None.
562
+
563
+ Returns:
564
+ List[Dict[str, Any]]: 엔티티 목록
565
+ """
566
+ try:
567
+ if model_name:
568
+ if model_name not in self.additional_models:
569
+ raise CustomException(
570
+ ErrorCode.INVALID_REQUEST,
571
+ detail=f"Model {model_name} not registered",
572
+ source_function=f"{self.__class__.__name__}.list"
573
+ )
574
+ entities = await self.db_service.list_entities(
575
+ self.additional_models[model_name],
576
+ skip=skip,
577
+ limit=limit,
578
+ filters=filters
579
+ )
580
+ return [self._process_response(entity, response_model) for entity in entities]
581
+
582
+ entities = await self.repository.list(
583
+ skip=skip,
584
+ limit=limit,
585
+ filters=filters,
586
+ search_params=search_params
587
+ )
588
+ return [self._process_response(entity, response_model) for entity in entities]
589
+
590
+ except CustomException as e:
591
+ e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
592
+ e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
593
+ raise e
594
+ except Exception as e:
595
+ raise CustomException(
596
+ ErrorCode.INTERNAL_ERROR,
597
+ detail=str(e),
598
+ source_function=f"{self.__class__.__name__}.list",
599
+ original_error=e
600
+ )
601
+
602
+ async def get(
603
+ self,
604
+ ulid: str,
605
+ model_name: str | None = None,
606
+ request: Request | None = None,
607
+ response_model: Any = None
608
+ ) -> Optional[Dict[str, Any]]:
609
+ """특정 엔티티를 조회합니다.
610
+
611
+ Args:
612
+ ulid (str): 조회할 엔티티의 ULID
613
+ model_name (str | None, optional): 조회할 모델 이름. Defaults to None.
614
+ request (Request | None, optional): 요청 객체. Defaults to None.
615
+ response_model (Any, optional): 응답 스키마. Defaults to None.
616
+
617
+ Returns:
618
+ Optional[Dict[str, Any]]: 조회된 엔티티, 없으면 None
619
+
620
+ Raises:
621
+ CustomException: 데이터베이스 작업 중 오류 발생 시
622
+ """
623
+ try:
624
+ # ULID 검증
625
+ if not self._validate_ulid(ulid):
626
+ raise CustomException(
627
+ ErrorCode.VALIDATION_ERROR,
628
+ detail=f"Invalid ULID format: {ulid}",
629
+ source_function=f"{self.__class__.__name__}.get"
630
+ )
631
+
632
+ if model_name:
633
+ if model_name not in self.additional_models:
634
+ raise CustomException(
635
+ ErrorCode.INVALID_REQUEST,
636
+ detail=f"Model {model_name} not registered",
637
+ source_function=f"{self.__class__.__name__}.get"
638
+ )
639
+ entity = await self.db_service.retrieve_entity(
640
+ self.additional_models[model_name],
641
+ {"ulid": ulid, "is_deleted": False}
642
+ )
643
+ if not entity:
644
+ raise CustomException(
645
+ ErrorCode.NOT_FOUND,
646
+ detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
647
+ source_function=f"{self.__class__.__name__}.get"
648
+ )
649
+ return self._process_response(entity, response_model)
650
+
651
+ entity = await self.repository.get(ulid)
652
+ if not entity:
653
+ raise CustomException(
654
+ ErrorCode.NOT_FOUND,
655
+ detail=f"{self.model.__tablename__}|ulid|{ulid}",
656
+ source_function=f"{self.__class__.__name__}.get"
657
+ )
658
+
659
+ return self._process_response(entity, response_model)
660
+ except CustomException as e:
661
+ raise e
662
+ except Exception as e:
663
+ raise CustomException(
664
+ ErrorCode.INTERNAL_ERROR,
665
+ detail=str(e),
666
+ source_function=f"{self.__class__.__name__}.get",
667
+ original_error=e
668
+ )