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