aiteamutils 0.2.58__py3-none-any.whl → 0.2.60__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- )