aiteamutils 0.2.53__py3-none-any.whl → 0.2.55__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/__init__.py CHANGED
@@ -1,11 +1,5 @@
1
1
  from .base_model import Base
2
- from .database import (
3
- DatabaseService,
4
- DatabaseServiceManager,
5
- get_db,
6
- get_database_service,
7
- lifespan
8
- )
2
+ from .database import DatabaseService
9
3
  from .exceptions import (
10
4
  CustomException,
11
5
  ErrorCode,
@@ -36,10 +30,6 @@ __all__ = [
36
30
 
37
31
  # Database
38
32
  "DatabaseService",
39
- "DatabaseServiceManager",
40
- "get_db",
41
- "get_database_service",
42
- "lifespan",
43
33
 
44
34
  # Exceptions
45
35
  "CustomException",
aiteamutils/database.py CHANGED
@@ -1,1083 +1,207 @@
1
- import asyncio
2
- import logging
3
- from typing import Any, Dict, Optional, Type, AsyncGenerator, TypeVar, List, Union
4
- from sqlalchemy import select, update, and_, Table
5
- from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, AsyncEngine
6
- from sqlalchemy.orm import sessionmaker, Load, joinedload
1
+ """데이터베이스 유틸리티 모듈."""
2
+ from typing import Any, Dict, Optional, Type, List, Union
3
+ from sqlalchemy import select, and_
4
+ from sqlalchemy.ext.asyncio import AsyncSession
7
5
  from sqlalchemy.exc import IntegrityError, SQLAlchemyError
8
- from sqlalchemy.pool import QueuePool
9
- from contextlib import asynccontextmanager
10
- from sqlalchemy import or_
11
- from fastapi import Request, Depends, FastAPI
12
- from ulid import ULID
13
- from sqlalchemy.sql import Select
14
6
 
15
7
  from .exceptions import ErrorCode, CustomException
16
- from .base_model import Base, BaseColumn
17
- from .enums import ActivityType
18
-
19
- T = TypeVar("T", bound=BaseColumn)
20
-
21
- class DatabaseServiceManager:
22
- _instance: Optional['DatabaseService'] = None
23
- _lock = asyncio.Lock()
24
-
25
- @classmethod
26
- async def get_instance(
27
- cls,
28
- db_url: str = None,
29
- db_echo: bool = False,
30
- db_pool_size: int = 5,
31
- db_max_overflow: int = 10,
32
- db_pool_timeout: int = 30,
33
- db_pool_recycle: int = 1800,
34
- **kwargs
35
- ) -> 'DatabaseService':
36
- """데이터베이스 서비스의 싱글톤 인스턴스를 반환합니다.
37
-
38
- Args:
39
- db_url (str, optional): 데이터베이스 URL
40
- db_echo (bool, optional): SQL 로깅 여부
41
- db_pool_size (int, optional): DB 커넥션 풀 크기
42
- db_max_overflow (int, optional): 최대 초과 커넥션 수
43
- db_pool_timeout (int, optional): 커넥션 풀 타임아웃
44
- db_pool_recycle (int, optional): 커넥션 재활용 시간
45
-
46
- Returns:
47
- DatabaseService: 데이터베이스 서비스 인스턴스
48
-
49
- Raises:
50
- CustomException: 데이터베이스 초기화 실패 시
51
- """
52
- async with cls._lock:
53
- if not cls._instance:
54
- if not db_url:
55
- raise CustomException(
56
- ErrorCode.DB_CONNECTION_ERROR,
57
- detail="Database URL is required for initialization",
58
- source_function="DatabaseServiceManager.get_instance"
59
- )
60
- try:
61
- cls._instance = DatabaseService(
62
- db_url=db_url,
63
- db_echo=db_echo,
64
- db_pool_size=db_pool_size,
65
- db_max_overflow=db_max_overflow,
66
- db_pool_timeout=db_pool_timeout,
67
- db_pool_recycle=db_pool_recycle,
68
- **kwargs
69
- )
70
- logging.info("Database service initialized successfully")
71
- except Exception as e:
72
- logging.error(f"Failed to initialize database service: {str(e)}")
73
- raise CustomException(
74
- ErrorCode.DB_CONNECTION_ERROR,
75
- detail=f"Failed to initialize database service: {str(e)}",
76
- source_function="DatabaseServiceManager.get_instance",
77
- original_error=e
78
- )
79
- return cls._instance
80
-
81
- @classmethod
82
- async def cleanup(cls) -> None:
83
- """데이터베이스 서비스 인스턴스를 정리합니다."""
84
- async with cls._lock:
85
- if cls._instance:
86
- try:
87
- if cls._instance.engine:
88
- await cls._instance.engine.dispose()
89
- cls._instance = None
90
- logging.info("Database service cleaned up successfully")
91
- except Exception as e:
92
- logging.error(f"Error during database service cleanup: {str(e)}")
93
- raise CustomException(
94
- ErrorCode.DB_CONNECTION_ERROR,
95
- detail=f"Failed to cleanup database service: {str(e)}",
96
- source_function="DatabaseServiceManager.cleanup",
97
- original_error=e
98
- )
99
-
100
- @classmethod
101
- async def is_initialized(cls) -> bool:
102
- """데이터베이스 서비스가 초기화되었는지 확인합니다."""
103
- return cls._instance is not None
8
+ from .base_model import Base
104
9
 
105
10
  class DatabaseService:
106
- def __init__(
107
- self,
108
- db_url: str = None,
109
- session: AsyncSession = None,
110
- db_echo: bool = False,
111
- db_pool_size: int = 5,
112
- db_max_overflow: int = 10,
113
- db_pool_timeout: int = 30,
114
- db_pool_recycle: int = 1800
115
- ):
116
- """DatabaseService 초기화.
11
+ def __init__(self, session: AsyncSession):
12
+ """DatabaseService 초기화
117
13
 
118
14
  Args:
119
- db_url (str, optional): 데이터베이스 URL
120
- session (AsyncSession, optional): 기존 세션
121
- db_echo (bool, optional): SQL 로깅 여부
122
- db_pool_size (int, optional): DB 커넥션 풀 크기
123
- db_max_overflow (int, optional): 최대 초과 커넥션 수
124
- db_pool_timeout (int, optional): 커넥션 풀 타임아웃
125
- db_pool_recycle (int, optional): 커넥션 재활용 시간
126
- """
127
- if db_url:
128
- self.engine = create_async_engine(
129
- db_url,
130
- echo=db_echo,
131
- pool_size=db_pool_size,
132
- max_overflow=db_max_overflow,
133
- pool_timeout=db_pool_timeout,
134
- pool_recycle=db_pool_recycle,
135
- pool_pre_ping=True,
136
- poolclass=QueuePool,
137
- )
138
- self.session_factory = sessionmaker(
139
- bind=self.engine,
140
- class_=AsyncSession,
141
- expire_on_commit=False
142
- )
143
- self.db = None
144
- elif session:
145
- self.engine = session.bind
146
- self.session_factory = sessionmaker(
147
- bind=self.engine,
148
- class_=AsyncSession,
149
- expire_on_commit=False
150
- )
151
- self.db = session
152
- else:
153
- raise CustomException(
154
- ErrorCode.DB_CONNECTION_ERROR,
155
- detail="Either db_url or session must be provided",
156
- source_function="DatabaseService.__init__"
157
- )
158
-
159
- async def is_connected(self) -> bool:
160
- """데이터베이스 연결 상태를 확인합니다."""
161
- try:
162
- if not self.engine:
163
- return False
164
- async with self.engine.connect() as conn:
165
- await conn.execute(select(1))
166
- return True
167
- except Exception:
168
- return False
169
-
170
- async def retry_connection(self, retries: int = 3, delay: int = 2) -> bool:
171
- """데이터베이스 연결을 재시도합니다.
172
-
173
- Args:
174
- retries (int): 재시도 횟수
175
- delay (int): 재시도 간 대기 시간(초)
176
-
177
- Returns:
178
- bool: 연결 성공 여부
15
+ session (AsyncSession): 외부에서 주입받은 데이터베이스 세션
179
16
  """
180
- for attempt in range(retries):
181
- try:
182
- if await self.is_connected():
183
- return True
184
- await asyncio.sleep(delay)
185
- except Exception as e:
186
- logging.error(f"Connection retry attempt {attempt + 1} failed: {str(e)}")
187
- if attempt == retries - 1:
188
- return False
189
- return False
190
-
191
- @asynccontextmanager
192
- async def get_session(self) -> AsyncGenerator[AsyncSession, None]:
193
- """데이터베이스 세션을 생성하고 반환하는 비동기 컨텍스트 매니저."""
194
- if self.session_factory is None:
17
+ self._session = session
18
+
19
+ @property
20
+ def session(self) -> AsyncSession:
21
+ """현재 세션을 반환합니다."""
22
+ if self._session is None:
195
23
  raise CustomException(
196
24
  ErrorCode.DB_CONNECTION_ERROR,
197
- detail="session_factory",
198
- source_function="DatabaseService.get_session"
25
+ detail="session",
26
+ source_function="DatabaseService.session"
199
27
  )
200
-
201
- async with self.session_factory() as session:
202
- try:
203
- yield session
204
- finally:
205
- await session.close()
206
-
207
- def preprocess_data(self, model: Type[Base], input_data: Dict[str, Any], existing_data: Dict[str, Any] = None) -> Dict[str, Any]:
208
- """입력 데이터를 전처리하여 extra_data로 분리
209
-
210
- Args:
211
- model (Type[Base]): SQLAlchemy 모델 클래스
212
- input_data (Dict[str, Any]): 입력 데이터
213
- existing_data (Dict[str, Any], optional): 기존 데이터. Defaults to None.
214
-
215
- Returns:
216
- Dict[str, Any]: 전처리된 데이터
217
- """
218
- model_attrs = {
219
- attr for attr in dir(model)
220
- if not attr.startswith('_') and not callable(getattr(model, attr))
221
- }
222
- model_data = {}
223
- extra_data = {}
224
-
225
- # 기존 extra_data가 있으면 복사
226
- if existing_data and "extra_data" in existing_data:
227
- extra_data = existing_data["extra_data"].copy()
28
+ return self._session
228
29
 
229
- # 스웨거 자동생성 필드 패턴
230
- swagger_patterns = {"additionalProp1", "additionalProp2", "additionalProp3"}
231
-
232
- # 모든 필드와 extra_data 분리
233
- for key, value in input_data.items():
234
- # 스웨거 자동생성 필드는 무시
235
- if key in swagger_patterns:
236
- continue
237
-
238
- if key in model_attrs:
239
- model_data[key] = value
240
- else:
241
- extra_data[key] = value
242
-
243
- # extra_data가 있고, 모델이 extra_data 속성을 가지고 있으면 추가
244
- if extra_data and "extra_data" in model_attrs:
245
- model_data["extra_data"] = extra_data
246
-
247
- return model_data
248
-
249
- ############################
250
- # 2. 트랜잭션 및 세션 관리 #
251
- ############################
252
- @asynccontextmanager
253
- async def transaction(self):
254
- """트랜잭션 컨텍스트 매니저
255
-
256
- 트랜잭션 범위를 명시적으로 관리합니다.
257
- with 문을 벗어날 때 자동으로 commit 또는 rollback됩니다.
258
-
259
- Example:
260
- async with db_service.transaction():
261
- await db_service.create_entity(...)
262
- await db_service.update_entity(...)
263
- """
264
- try:
265
- yield
266
- await self.db.commit()
267
- except Exception as e:
268
- await self.db.rollback()
269
- raise e
270
-
271
- #######################
272
- # 3. 데이터 처리 #
273
- #######################
274
- async def create_entity(self, model: Type[Base], entity_data: Dict[str, Any]) -> Any:
275
- """새로운 엔티티를 생성합니다.
30
+ async def create_entity(
31
+ self,
32
+ model: Type[Base],
33
+ entity_data: Dict[str, Any]
34
+ ) -> Any:
35
+ """엔티티를 생성합니다.
276
36
 
277
37
  Args:
278
- model (Type[Base]): 생성할 모델 클래스
279
- entity_data (Dict[str, Any]): 엔티티 데이터
38
+ model: 모델 클래스
39
+ entity_data: 생성할 엔티티 데이터
280
40
 
281
41
  Returns:
282
- Any: 생성된 엔티티
42
+ 생성된 엔티티
283
43
 
284
44
  Raises:
285
- CustomException: 데이터베이스 오류 발생
45
+ CustomException: 엔티티 생성 실패 시
286
46
  """
287
47
  try:
288
- # 데이터 전처리 및 모델 인스턴스 생성
289
- processed_data = self.preprocess_data(model, entity_data)
290
-
291
- # 외래 키 필드 검증
292
- foreign_key_fields = {
293
- field: value for field, value in processed_data.items()
294
- if any(fk.parent.name == field for fk in model.__table__.foreign_keys)
295
- }
296
- if foreign_key_fields:
297
- await self.validate_foreign_key_fields(model, foreign_key_fields)
298
-
299
- entity = model(**processed_data)
300
-
301
- self.db.add(entity)
302
- await self.db.flush()
303
- await self.db.commit()
304
- await self.db.refresh(entity)
48
+ entity = model(**entity_data)
49
+ self.session.add(entity)
50
+ await self.session.flush()
51
+ await self.session.refresh(entity)
305
52
  return entity
306
53
  except IntegrityError as e:
307
- await self.db.rollback()
308
- error_str = str(e)
309
-
310
- if "duplicate key" in error_str.lower():
311
- # 중복 키 에러 처리
312
- field = None
313
- value = None
314
-
315
- # 에러 메시지에서 필드와 값 추출
316
- if "Key (" in error_str and ") already exists" in error_str:
317
- field_value = error_str.split("Key (")[1].split(") already exists")[0]
318
- if "=" in field_value:
319
- field, value = field_value.split("=")
320
- field = field.strip("() ")
321
- value = value.strip("() ")
322
-
323
- if not field:
324
- field = "id" # 기본값
325
- value = str(entity_data.get(field, ""))
326
-
327
- raise CustomException(
328
- ErrorCode.DUPLICATE_ERROR,
329
- detail=f"{model.__tablename__}|{field}|{value}",
330
- source_function="DatabaseService.create_entity",
331
- original_error=e
332
- )
333
- elif "violates foreign key constraint" in error_str.lower():
334
- # 외래키 위반 에러는 validate_foreign_key_fields에서 이미 처리됨
335
- raise CustomException(
336
- ErrorCode.FOREIGN_KEY_VIOLATION,
337
- detail=error_str,
338
- source_function="DatabaseService.create_entity",
339
- original_error=e
340
- )
341
- else:
342
- raise CustomException(
343
- ErrorCode.DB_CREATE_ERROR,
344
- detail=f"Failed to create {model.__name__}: {str(e)}",
345
- source_function="DatabaseService.create_entity",
346
- original_error=e
347
- )
348
- except CustomException as e:
349
- await self.db.rollback()
350
- raise e
351
- except Exception as e:
352
- await self.db.rollback()
54
+ await self.session.rollback()
353
55
  raise CustomException(
354
- ErrorCode.UNEXPECTED_ERROR,
355
- detail=f"Unexpected error while creating {model.__name__}: {str(e)}",
56
+ ErrorCode.DB_INTEGRITY_ERROR,
57
+ detail=str(e),
356
58
  source_function="DatabaseService.create_entity",
357
59
  original_error=e
358
60
  )
359
-
360
- async def retrieve_entity(
361
- self,
362
- model: Type[T],
363
- conditions: Dict[str, Any],
364
- join_options: Optional[Union[Load, List[Load]]] = None
365
- ) -> Optional[T]:
366
- """조건에 맞는 단일 엔티티를 조회합니다.
367
-
368
- Args:
369
- model: 모델 클래스
370
- conditions: 조회 조건
371
- join_options: SQLAlchemy의 joinedload 옵션 또는 옵션 리스트
372
-
373
- Returns:
374
- Optional[T]: 조회된 엔티티 또는 None
375
-
376
- Raises:
377
- CustomException: 데이터베이스 작업 중 오류 발생 시
378
- """
379
- try:
380
- stmt = select(model)
381
-
382
- # Join 옵션 적용
383
- if join_options is not None:
384
- if isinstance(join_options, list):
385
- stmt = stmt.options(*join_options)
386
- else:
387
- stmt = stmt.options(join_options)
388
-
389
- # 조건 적용
390
- for key, value in conditions.items():
391
- stmt = stmt.where(getattr(model, key) == value)
392
-
393
- result = await self.execute_query(stmt)
394
- return result.unique().scalar_one_or_none()
395
-
396
61
  except Exception as e:
62
+ await self.session.rollback()
397
63
  raise CustomException(
398
- ErrorCode.DB_QUERY_ERROR,
64
+ ErrorCode.DB_CREATE_ERROR,
399
65
  detail=str(e),
400
- source_function="DatabaseService.retrieve_entity",
66
+ source_function="DatabaseService.create_entity",
401
67
  original_error=e
402
68
  )
403
69
 
404
- async def update_entity(
70
+ async def get_entity(
405
71
  self,
406
72
  model: Type[Base],
407
- conditions: Dict[str, Any],
408
- update_data: Dict[str, Any]
409
- ) -> Optional[Base]:
410
- """엔티티를 업데이트합니다.
411
-
73
+ filters: Dict[str, Any]
74
+ ) -> Optional[Any]:
75
+ """필터 조건으로 엔티티를 조회합니다.
76
+
412
77
  Args:
413
- model (Type[Base]): 엔티티 모델 클래스
414
- conditions (Dict[str, Any]): 업데이트할 엔티티 조회 조건
415
- update_data (Dict[str, Any]): 업데이트할 데이터
416
-
417
- Returns:
418
- Optional[Base]: 업데이트된 엔티티
419
-
420
- Raises:
421
- CustomException: 데이터베이스 오류 발생
422
- """
423
- try:
424
- # 엔티티 조회
425
- stmt = select(model)
426
- for key, value in conditions.items():
427
- stmt = stmt.where(getattr(model, key) == value)
428
-
429
- result = await self.db.execute(stmt)
430
- entity = result.scalar_one_or_none()
431
-
432
- if not entity:
433
- return None
434
-
435
- # 기존 데이터를 딕셔너리로 변환
436
- existing_data = {
437
- column.name: getattr(entity, column.name)
438
- for column in entity.__table__.columns
439
- }
440
-
441
- # 데이터 전처리
442
- processed_data = self.preprocess_data(model, update_data, existing_data)
443
-
444
- # UPDATE 문 생성 및 실행
445
- update_stmt = (
446
- update(model)
447
- .where(and_(*[getattr(model, key) == value for key, value in conditions.items()]))
448
- .values(**processed_data)
449
- .returning(model)
450
- )
451
-
452
- result = await self.db.execute(update_stmt)
453
- await self.db.commit()
454
-
455
- # 업데이트된 엔티티 반환
456
- updated_entity = result.scalar_one()
457
- return updated_entity
78
+ model: 모델 클래스
79
+ filters: 필터 조건
458
80
 
459
- except SQLAlchemyError as e:
460
- await self.db.rollback()
461
- raise CustomException(
462
- ErrorCode.DB_UPDATE_ERROR,
463
- detail=f"Failed to update {model.__name__}: {str(e)}",
464
- source_function="DatabaseService.update_entity",
465
- original_error=e
466
- )
467
- except Exception as e:
468
- await self.db.rollback()
469
- raise CustomException(
470
- ErrorCode.UNEXPECTED_ERROR,
471
- detail=f"Unexpected error while updating {model.__name__}: {str(e)}",
472
- source_function="DatabaseService.update_entity",
473
- original_error=e
474
- )
475
-
476
- async def delete_entity(self, entity: T) -> None:
477
- """엔티티를 실제로 삭제합니다.
478
-
479
- Args:
480
- entity (T): 삭제할 엔티티
481
-
482
- Raises:
483
- CustomException: 데이터베이스 오류 발생
484
- """
485
- try:
486
- await self.db.delete(entity)
487
- await self.db.flush()
488
- await self.db.commit()
489
- except SQLAlchemyError as e:
490
- await self.db.rollback()
491
- raise CustomException(
492
- ErrorCode.DB_DELETE_ERROR,
493
- detail=f"Failed to delete entity: {str(e)}",
494
- source_function="DatabaseService.delete_entity",
495
- original_error=e
496
- )
497
- except Exception as e:
498
- await self.db.rollback()
499
- raise CustomException(
500
- ErrorCode.UNEXPECTED_ERROR,
501
- detail=f"Unexpected error while deleting entity: {str(e)}",
502
- source_function="DatabaseService.delete_entity",
503
- original_error=e
504
- )
505
-
506
- async def soft_delete_entity(self, model: Type[T], ulid: str) -> Optional[T]:
507
- """엔티티를 소프트 삭제합니다 (is_deleted = True).
508
-
509
- Args:
510
- model (Type[T]): 엔티티 모델
511
- ulid (str): 삭제할 엔티티의 ULID
512
-
513
81
  Returns:
514
- Optional[T]: 삭제된 엔티티, 없으면 None
515
-
82
+ 조회된 엔티티 또는 None
83
+
516
84
  Raises:
517
- CustomException: 데이터베이스 오류 발생
85
+ CustomException: 조회 실패
518
86
  """
519
87
  try:
520
- # 1. 엔티티 조회
521
- stmt = select(model).where(
522
- and_(
523
- model.ulid == ulid,
524
- model.is_deleted == False
525
- )
526
- )
527
- result = await self.db.execute(stmt)
528
- entity = result.scalar_one_or_none()
529
-
530
- if not entity:
531
- return None
532
-
533
- # 2. 소프트 삭제 처리
534
- stmt = update(model).where(
535
- model.ulid == ulid
536
- ).values(
537
- is_deleted=True
538
- )
539
- await self.db.execute(stmt)
540
- await self.db.commit()
541
-
542
- # 3. 업데이트된 엔티티 반환
543
- return entity
544
- except SQLAlchemyError as e:
545
- await self.db.rollback()
546
- raise CustomException(
547
- ErrorCode.DB_DELETE_ERROR,
548
- detail=f"Failed to soft delete {model.__name__}: {str(e)}",
549
- source_function="DatabaseService.soft_delete_entity",
550
- original_error=e
551
- )
88
+ stmt = select(model).filter_by(**filters)
89
+ result = await self.session.execute(stmt)
90
+ return result.scalars().first()
552
91
  except Exception as e:
553
- await self.db.rollback()
554
92
  raise CustomException(
555
- ErrorCode.UNEXPECTED_ERROR,
556
- detail=f"Unexpected error while soft deleting {model.__name__}: {str(e)}",
557
- source_function="DatabaseService.soft_delete_entity",
93
+ ErrorCode.DB_QUERY_ERROR,
94
+ detail=str(e),
95
+ source_function="DatabaseService.get_entity",
558
96
  original_error=e
559
97
  )
560
98
 
561
99
  async def list_entities(
562
100
  self,
563
- model: Type[T],
564
- skip: int = 0,
565
- limit: int = 100,
101
+ model: Type[Base],
566
102
  filters: Optional[Dict[str, Any]] = None,
567
- joins: Optional[List[Any]] = None
568
- ) -> List[T]:
103
+ skip: int = 0,
104
+ limit: int = 100
105
+ ) -> List[Any]:
569
106
  """엔티티 목록을 조회합니다.
570
107
 
571
108
  Args:
572
- model (Type[T]): 엔티티 모델
573
- skip (int): 건너뛸 레코드 수
574
- limit (int): 조회할 최대 레코드 수
575
- filters (Optional[Dict[str, Any]]): 필터 조건
576
- - field: value -> field = value
577
- - field__ilike: value -> field ILIKE value
578
- - search: [(field, pattern), ...] -> OR(field ILIKE pattern, ...)
579
- joins (Optional[List[Any]]): 조인할 관계들 (joinedload 객체 리스트)
109
+ model: 모델 클래스
110
+ filters: 필터 조건
111
+ skip: 건너뛸 레코드 수
112
+ limit: 조회할 최대 레코드
580
113
 
581
114
  Returns:
582
- List[T]: 조회된 엔티티 목록
115
+ 엔티티 목록
583
116
 
584
117
  Raises:
585
- CustomException: 데이터베이스 오류 발생
118
+ CustomException: 조회 실패
586
119
  """
587
120
  try:
588
- query = select(model)
589
- conditions = []
590
-
121
+ stmt = select(model)
591
122
  if filters:
592
- for key, value in filters.items():
593
- if key == "search" and isinstance(value, list):
594
- # 전체 검색 조건
595
- search_conditions = []
596
- for field_name, pattern in value:
597
- field = getattr(model, field_name)
598
- search_conditions.append(field.ilike(pattern))
599
- if search_conditions:
600
- conditions.append(or_(*search_conditions))
601
- elif "__ilike" in key:
602
- # ILIKE 검색
603
- field_name = key.replace("__ilike", "")
604
- field = getattr(model, field_name)
605
- conditions.append(field.ilike(value))
606
- else:
607
- # 일반 필터
608
- field = getattr(model, key)
609
- conditions.append(field == value)
610
-
611
- if conditions:
612
- query = query.where(and_(*conditions))
613
-
614
- if joins:
615
- for join_option in joins:
616
- query = query.options(join_option)
617
-
618
- query = query.offset(skip).limit(limit)
619
- result = await self.db.execute(query)
620
- return result.scalars().unique().all()
621
- except SQLAlchemyError as e:
622
- raise CustomException(
623
- ErrorCode.DB_READ_ERROR,
624
- detail=f"Failed to list {model.__name__}: {str(e)}",
625
- source_function="DatabaseService.list_entities",
626
- original_error=e
627
- )
628
- except Exception as e:
629
- raise CustomException(
630
- ErrorCode.UNEXPECTED_ERROR,
631
- detail=f"Unexpected error while listing {model.__name__}: {str(e)}",
632
- source_function="DatabaseService.list_entities",
633
- original_error=e
634
- )
635
-
636
- ######################
637
- # 4. 검증 #
638
- ######################
639
- async def validate_unique_fields(
640
- self,
641
- table_or_model: Union[Table, Type[Any]],
642
- fields: Dict[str, Any],
643
- source_function: str,
644
- error_code: ErrorCode = ErrorCode.DUPLICATE_ERROR
645
- ) -> None:
646
- """
647
- 데이터베이스에서 필드의 유일성을 검증합니다.
648
-
649
- Args:
650
- table_or_model: 검증할 테이블 또는 모델 클래스
651
- fields: 검증할 필드와 값의 딕셔너리 {"field_name": value}
652
- source_function: 호출한 함수명
653
- error_code: 사용할 에러 코드 (기본값: DUPLICATE_ERROR)
654
-
655
- Raises:
656
- CustomException: 중복된 값이 존재할 경우
657
- """
658
- try:
659
- conditions = []
660
- for field_name, value in fields.items():
661
- conditions.append(getattr(table_or_model, field_name) == value)
662
-
663
- query = select(table_or_model).where(or_(*conditions))
664
- result = await self.db.execute(query)
665
- existing = result.scalar_one_or_none()
666
-
667
- if existing:
668
- table_name = table_or_model.name if hasattr(table_or_model, 'name') else table_or_model.__tablename__
669
- # 단일 필드인 경우
670
- if len(fields) == 1:
671
- field_name, value = next(iter(fields.items()))
672
- detail = f"{table_name}|{field_name}|{value}"
673
- # 복수 필드인 경우
674
- else:
675
- fields_str = "|".join(f"{k}:{v}" for k, v in fields.items())
676
- detail = f"{table_name}|{fields_str}"
677
-
678
- raise CustomException(
679
- error_code,
680
- detail=detail,
681
- source_function="DatabaseService.validate_unique_fields"
682
- )
683
-
684
- except CustomException as e:
685
- raise CustomException(
686
- e.error_code,
687
- detail=e.detail,
688
- source_function="DatabaseService.validate_unique_fields",
689
- original_error=e.original_error,
690
- parent_source_function=e.source_function
691
- )
123
+ stmt = stmt.filter_by(**filters)
124
+ stmt = stmt.offset(skip).limit(limit)
125
+ result = await self.session.execute(stmt)
126
+ return result.scalars().all()
692
127
  except Exception as e:
693
128
  raise CustomException(
694
129
  ErrorCode.DB_QUERY_ERROR,
695
130
  detail=str(e),
696
- source_function="DatabaseService.validate_unique_fields",
131
+ source_function="DatabaseService.list_entities",
697
132
  original_error=e
698
133
  )
699
134
 
700
- async def validate_foreign_key_fields(
135
+ async def update_entity(
701
136
  self,
702
- model: Type[T],
703
- fields: Dict[str, Any]
704
- ) -> None:
705
- """외래 키 필드를 검증합니다.
137
+ entity: Base,
138
+ update_data: Dict[str, Any]
139
+ ) -> Any:
140
+ """엔티티를 수정합니다.
706
141
 
707
142
  Args:
708
- model (Type[T]): 검증할 모델 클래스
709
- fields (Dict[str, Any]): 검증할 외래 키 필드와 값
143
+ entity: 수정할 엔티티
144
+ update_data: 수정할 데이터
710
145
 
711
- Raises:
712
- CustomException: 참조하는 레코드가 존재하지 않는 경우
713
- """
714
- for field, value in fields.items():
715
- # 외래 키 관계 정보 가져오기
716
- foreign_key = next(
717
- (fk for fk in model.__table__.foreign_keys if fk.parent.name == field),
718
- None
719
- )
720
- if foreign_key and value:
721
- # 참조하는 테이블에서 레코드 존재 여부 확인
722
- referenced_table = foreign_key.column.table
723
- query = select(referenced_table).where(
724
- and_(
725
- foreign_key.column == value,
726
- getattr(referenced_table.c, 'is_deleted', None) == False
727
- )
728
- )
729
- result = await self.db.execute(query)
730
- if not result.scalar_one_or_none():
731
- raise CustomException(
732
- ErrorCode.FOREIGN_KEY_VIOLATION,
733
- detail=f"{referenced_table.name}|{field}|{value}",
734
- source_function="DatabaseService.validate_foreign_key_fields"
735
- )
736
-
737
- #######################
738
- # 5. 쿼리 실행 #
739
- #######################
740
- async def create_log(self, model: Type[Base], log_data: Dict[str, Any], request: Request = None) -> None:
741
- """로그를 생성합니다.
742
-
743
- Args:
744
- model: 로그 모델 클래스
745
- log_data: 로그 데이터
746
- request: FastAPI 요청 객체
747
-
748
146
  Returns:
749
- 생성된 로그 엔티티
750
-
751
- Raises:
752
- CustomException: 로그 생성 실패 시
753
- """
754
- try:
755
- # 공통 필드 추가 (ULID를 문자열로 변환)
756
- log_data["ulid"] = str(ULID())
757
-
758
- # request가 있는 경우 user-agent와 ip 정보 추가
759
- if request:
760
- log_data["user_agent"] = request.headers.get("user-agent")
761
- log_data["ip_address"] = request.headers.get("x-forwarded-for")
762
-
763
- # 데이터 전처리
764
- processed_data = self.preprocess_data(model, log_data)
765
- entity = model(**processed_data)
766
-
767
- async with self.get_session() as session:
768
- # 로그 엔티티 저장
769
- session.add(entity)
770
- await session.flush()
771
- await session.commit()
772
- return entity
773
-
774
- except Exception as e:
775
- logging.error(f"Failed to create log: {str(e)}")
776
- # 로그 생성 실패는 원래 작업에 영향을 주지 않도록 함
777
- return None
778
-
779
- async def soft_delete(
780
- self,
781
- model: Type[T],
782
- entity_id: str,
783
- source_function: str = None,
784
- request: Request = None
785
- ) -> None:
786
- """엔티티를 소프트 삭제합니다.
787
-
788
- Args:
789
- model: 모델 클래스
790
- entity_id: 삭제할 엔티티의 ID
791
- source_function: 호출한 함수명
792
- request: FastAPI 요청 객체
147
+ 수정된 엔티티
793
148
 
794
149
  Raises:
795
- CustomException: 데이터베이스 작업 실패 시
150
+ CustomException: 수정 실패 시
796
151
  """
797
152
  try:
798
- # 1. 엔티티 조회
799
- stmt = select(model).where(
800
- and_(
801
- model.ulid == entity_id,
802
- model.is_deleted == False
803
- )
804
- )
805
- result = await self.db.execute(stmt)
806
- entity = result.scalar_one_or_none()
807
-
808
- if not entity:
809
- raise CustomException(
810
- ErrorCode.NOT_FOUND,
811
- detail=f"{model.__name__}|{entity_id}",
812
- source_function=source_function or "DatabaseService.soft_delete"
813
- )
814
-
815
- # 2. 소프트 삭제 처리
816
- stmt = update(model).where(
817
- model.ulid == entity_id
818
- ).values(
819
- is_deleted=True
820
- )
821
- await self.db.execute(stmt)
822
- await self.db.commit()
823
-
824
- # 3. 삭제 로그 생성
825
- if request:
826
- activity_type = f"{model.__tablename__.upper()}_DELETED"
827
- await self.create_log({
828
- "type": activity_type,
829
- "fk_table": model.__tablename__,
830
- "extra_data": {
831
- f"{model.__tablename__}_ulid": entity_id
832
- }
833
- }, request)
834
-
835
- except CustomException as e:
836
- await self.db.rollback()
837
- raise CustomException(
838
- e.error_code,
839
- detail=e.detail,
840
- source_function=source_function or "DatabaseService.soft_delete",
841
- original_error=e.original_error,
842
- parent_source_function=e.source_function
843
- )
844
- except Exception as e:
845
- await self.db.rollback()
153
+ for key, value in update_data.items():
154
+ setattr(entity, key, value)
155
+ await self.session.flush()
156
+ await self.session.refresh(entity)
157
+ return entity
158
+ except IntegrityError as e:
159
+ await self.session.rollback()
846
160
  raise CustomException(
847
- ErrorCode.DB_DELETE_ERROR,
161
+ ErrorCode.DB_INTEGRITY_ERROR,
848
162
  detail=str(e),
849
- source_function=source_function or "DatabaseService.soft_delete",
850
- original_error=e
851
- )
852
-
853
- async def get_entity(self, model: Type[T], ulid: str) -> Optional[T]:
854
- """ULID로 엔티티를 조회합니다.
855
-
856
- Args:
857
- model (Type[T]): 엔티티 모델
858
- ulid (str): 조회할 엔티티의 ULID
859
-
860
- Returns:
861
- Optional[T]: 조회된 엔티티 또는 None
862
-
863
- Raises:
864
- CustomException: 데이터베이스 오류 발생
865
- """
866
- try:
867
- query = select(model).where(
868
- and_(
869
- model.ulid == ulid,
870
- model.is_deleted == False
871
- )
872
- )
873
- result = await self.db.execute(query)
874
- return result.scalar_one_or_none()
875
- except SQLAlchemyError as e:
876
- raise CustomException(
877
- ErrorCode.DB_READ_ERROR,
878
- detail=f"Failed to get {model.__name__}: {str(e)}",
879
- source_function="DatabaseService.get_entity",
163
+ source_function="DatabaseService.update_entity",
880
164
  original_error=e
881
165
  )
882
166
  except Exception as e:
167
+ await self.session.rollback()
883
168
  raise CustomException(
884
- ErrorCode.UNEXPECTED_ERROR,
885
- detail=f"Unexpected error while getting {model.__name__}: {str(e)}",
886
- source_function="DatabaseService.get_entity",
887
- original_error=e
888
- )
889
-
890
- async def execute_query(self, query: Select) -> Any:
891
- """SQL 쿼리를 실행하고 결과를 반환합니다.
892
-
893
- Args:
894
- query (Select): 실행할 SQLAlchemy 쿼리
895
-
896
- Returns:
897
- Any: 쿼리 실행 결과
898
-
899
- Raises:
900
- CustomException: 데이터베이스 작업 중 오류 발생 시
901
- """
902
- try:
903
- if self.db is not None:
904
- return await self.db.execute(query)
905
-
906
- async with self.get_session() as session:
907
- result = await session.execute(query)
908
- return result
909
- except Exception as e:
910
- raise CustomException(
911
- ErrorCode.DB_QUERY_ERROR,
169
+ ErrorCode.DB_UPDATE_ERROR,
912
170
  detail=str(e),
913
- source_function=f"{self.__class__.__name__}.execute_query",
171
+ source_function="DatabaseService.update_entity",
914
172
  original_error=e
915
173
  )
916
174
 
917
- async def execute(self, stmt):
918
- """SQL 문을 실행합니다.
175
+ async def delete_entity(
176
+ self,
177
+ entity: Base,
178
+ soft_delete: bool = True
179
+ ) -> bool:
180
+ """엔티티를 삭제합니다.
919
181
 
920
182
  Args:
921
- stmt: 실행할 SQL 문
183
+ entity: 삭제할 엔티티
184
+ soft_delete: 소프트 삭제 여부
922
185
 
923
186
  Returns:
924
- Result: 실행 결과
187
+ 삭제 성공 여부
925
188
 
926
189
  Raises:
927
- CustomException: 데이터베이스 오류 발생
190
+ CustomException: 삭제 실패
928
191
  """
929
192
  try:
930
- return await self.db.execute(stmt)
931
- except SQLAlchemyError as e:
932
- raise CustomException(
933
- ErrorCode.DB_QUERY_ERROR,
934
- detail=str(e),
935
- source_function="DatabaseService.execute",
936
- original_error=e
937
- )
193
+ if soft_delete:
194
+ entity.is_deleted = True
195
+ await self.session.flush()
196
+ else:
197
+ await self.session.delete(entity)
198
+ await self.session.flush()
199
+ return True
938
200
  except Exception as e:
201
+ await self.session.rollback()
939
202
  raise CustomException(
940
- ErrorCode.UNEXPECTED_ERROR,
941
- detail=str(e),
942
- source_function="DatabaseService.execute",
943
- original_error=e
944
- )
945
-
946
- async def commit(self) -> None:
947
- """현재 세션의 변경사항을 데이터베이스에 커밋합니다."""
948
- try:
949
- await self.db.commit()
950
- except SQLAlchemyError as e:
951
- await self.db.rollback()
952
- raise CustomException(
953
- ErrorCode.DB_QUERY_ERROR,
954
- detail=str(e),
955
- source_function="DatabaseService.commit",
956
- original_error=e
957
- )
958
-
959
- async def rollback(self) -> None:
960
- """현재 세션의 변경사항을 롤백합니다."""
961
- try:
962
- await self.db.rollback()
963
- except SQLAlchemyError as e:
964
- raise CustomException(
965
- ErrorCode.DB_QUERY_ERROR,
966
- detail=str(e),
967
- source_function="DatabaseService.rollback",
968
- original_error=e
969
- )
970
-
971
- async def flush(self) -> None:
972
- """현재 세션의 변경사항을 데이터베이스에 플러시합니다."""
973
- try:
974
- await self.db.flush()
975
- except SQLAlchemyError as e:
976
- await self.db.rollback()
977
- raise CustomException(
978
- ErrorCode.DB_QUERY_ERROR,
979
- detail=str(e),
980
- source_function="DatabaseService.flush",
981
- original_error=e
982
- )
983
-
984
- async def refresh(self, entity: Any) -> None:
985
- """엔티티를 데이터베이스의 최신 상태로 리프레시합니다.
986
-
987
- Args:
988
- entity: 리프레시할 엔티티
989
- """
990
- try:
991
- await self.db.refresh(entity)
992
- except SQLAlchemyError as e:
993
- raise CustomException(
994
- ErrorCode.DB_QUERY_ERROR,
203
+ ErrorCode.DB_DELETE_ERROR,
995
204
  detail=str(e),
996
- source_function="DatabaseService.refresh",
997
- original_error=e
998
- )
999
-
1000
- async def create_session(self) -> AsyncSession:
1001
- """새로운 데이터베이스 세션을 생성합니다.
1002
-
1003
- Returns:
1004
- AsyncSession: 생성된 세션
1005
-
1006
- Raises:
1007
- CustomException: 세션 생성 실패 시
1008
- """
1009
- if self.session_factory is None:
1010
- raise CustomException(
1011
- ErrorCode.DB_CONNECTION_ERROR,
1012
- detail="session_factory is not initialized",
1013
- source_function="DatabaseService.create_session"
1014
- )
1015
-
1016
- return self.session_factory()
1017
-
1018
- async def get_db() -> AsyncGenerator[AsyncSession, None]:
1019
- """데이터베이스 세션 의존성
1020
-
1021
- Yields:
1022
- AsyncSession: 데이터베이스 세션
1023
-
1024
- Raises:
1025
- CustomException: 데이터베이스 연결 오류
1026
- """
1027
- db_service = await get_database_service()
1028
-
1029
- async with db_service.get_session() as session:
1030
- try:
1031
- # 세션이 유효한지 확인
1032
- await session.execute(select(1))
1033
- yield session
1034
- except Exception as e:
1035
- if isinstance(e, CustomException):
1036
- raise e
1037
- raise CustomException(
1038
- ErrorCode.DB_CONNECTION_ERROR,
1039
- detail=f"Failed to get database session: {str(e)}",
1040
- source_function="get_db",
205
+ source_function="DatabaseService.delete_entity",
1041
206
  original_error=e
1042
- )
1043
-
1044
- async def get_database_service() -> DatabaseService:
1045
- """DatabaseService 의존성
1046
-
1047
- Returns:
1048
- DatabaseService: DatabaseService 인스턴스
1049
-
1050
- Raises:
1051
- CustomException: 데이터베이스 서비스가 초기화되지 않은 경우
1052
- """
1053
- if not await DatabaseServiceManager.is_initialized():
1054
- raise CustomException(
1055
- ErrorCode.DB_CONNECTION_ERROR,
1056
- detail="Database service is not initialized",
1057
- source_function="get_database_service"
1058
- )
1059
-
1060
- return await DatabaseServiceManager.get_instance()
1061
-
1062
- @asynccontextmanager
1063
- async def lifespan(app: FastAPI):
1064
- """FastAPI 애플리케이션 라이프사이클 관리자.
1065
-
1066
- Args:
1067
- app (FastAPI): FastAPI 애플리케이션 인스턴스
1068
- """
1069
- try:
1070
- # 시작 시 초기화
1071
- if not await DatabaseServiceManager.is_initialized():
1072
- await DatabaseServiceManager.get_instance(
1073
- db_url=app.state.settings.DATABASE_URL,
1074
- db_echo=app.state.settings.DB_ECHO,
1075
- db_pool_size=app.state.settings.DB_POOL_SIZE,
1076
- db_max_overflow=app.state.settings.DB_MAX_OVERFLOW,
1077
- db_pool_timeout=app.state.settings.DB_POOL_TIMEOUT,
1078
- db_pool_recycle=app.state.settings.DB_POOL_RECYCLE
1079
- )
1080
- yield
1081
- finally:
1082
- # 종료 시 정리
1083
- await DatabaseServiceManager.cleanup()
207
+ )
@@ -9,33 +9,13 @@ from .exceptions import CustomException, ErrorCode
9
9
  from .config import get_settings
10
10
  from .base_service import BaseService
11
11
  from .base_repository import BaseRepository
12
- from .database import db_manager
12
+ from .database import DatabaseService
13
13
 
14
14
  T = TypeVar("T", bound=BaseService)
15
15
  R = TypeVar("R", bound=BaseRepository)
16
16
 
17
17
  _service_registry: Dict[str, Dict[str, Any]] = {}
18
18
 
19
- async def get_db() -> AsyncGenerator[AsyncSession, None]:
20
- """데이터베이스 세션을 반환합니다.
21
-
22
- Yields:
23
- AsyncSession: 데이터베이스 세션
24
-
25
- Raises:
26
- CustomException: 세션 생성 실패 시
27
- """
28
- try:
29
- async with db_manager.get_session() as session:
30
- yield session
31
- except Exception as e:
32
- raise CustomException(
33
- ErrorCode.DATABASE_ERROR,
34
- detail=str(e),
35
- source_function="dependencies.get_db",
36
- original_error=e
37
- )
38
-
39
19
  def register_service(
40
20
  service_class: Type[T],
41
21
  repository_class: Optional[Type[R]] = None,
@@ -86,6 +66,9 @@ async def _get_service(
86
66
  repository_class = service_info["repository_class"]
87
67
  dependencies = service_info["dependencies"]
88
68
 
69
+ # 데이터베이스 서비스 생성
70
+ db_service = DatabaseService(session=session)
71
+
89
72
  # 저장소 인스턴스 생성
90
73
  repository = None
91
74
  if repository_class:
@@ -93,8 +76,8 @@ async def _get_service(
93
76
 
94
77
  # 서비스 인스턴스 생성
95
78
  service = service_class(
79
+ db=db_service,
96
80
  repository=repository,
97
- session=session,
98
81
  request=request,
99
82
  **dependencies
100
83
  )
@@ -122,14 +105,14 @@ def get_service(service_name: str) -> Callable:
122
105
  """
123
106
  async def _get_service_dependency(
124
107
  request: Request,
125
- session: AsyncSession = Depends(get_db)
108
+ session: AsyncSession
126
109
  ) -> BaseService:
127
110
  return await _get_service(service_name, session, request)
128
111
  return _get_service_dependency
129
112
 
130
113
  async def get_current_user(
131
114
  request: Request,
132
- session: AsyncSession = Depends(get_db),
115
+ session: AsyncSession,
133
116
  auth_service: BaseService = Depends(get_service("AuthService"))
134
117
  ) -> Dict[str, Any]:
135
118
  """현재 사용자 정보를 반환합니다.
aiteamutils/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  """버전 정보"""
2
- __version__ = "0.2.53"
2
+ __version__ = "0.2.55"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.53
3
+ Version: 0.2.55
4
4
  Summary: AI Team Utilities
5
5
  Project-URL: Homepage, https://github.com/yourusername/aiteamutils
6
6
  Project-URL: Issues, https://github.com/yourusername/aiteamutils/issues
@@ -1,16 +1,16 @@
1
- aiteamutils/__init__.py,sha256=IAvWobxODQeMIgttFf3e1IGMO-DktLyUmnHeKqGDZWg,1346
1
+ aiteamutils/__init__.py,sha256=h7iFifWvRlaComsk4VPapRr16YhD19Kp1LuBzLHqdgQ,1170
2
2
  aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
3
3
  aiteamutils/base_repository.py,sha256=vqsundoN0h7FVvgqTBEnnJNMcFpvMK0s_nxBWdIYg-U,7846
4
4
  aiteamutils/base_service.py,sha256=s2AcA-6_ogOQKgt2xf_3AG2s6tqBceU4nJoXO1II7S8,24588
5
5
  aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
6
6
  aiteamutils/config.py,sha256=OM_b7g8sqZ3zY_DSF9ry-zn5wn4dlXdx5OhjfTGr0TE,2876
7
- aiteamutils/database.py,sha256=uSMNWJge5RuQI7zJBJVX24fAHcSPChl-95_2TwK_GOw,39654
8
- aiteamutils/dependencies.py,sha256=q-OrEOJh4xEpc7ag6nTyey1pQwK9G0ZEDgXB_iTbaM0,6449
7
+ aiteamutils/database.py,sha256=x0x5gnSyGfwo_klL9O65RnGOQID6c9tH2miwFveVyoE,6326
8
+ aiteamutils/dependencies.py,sha256=UrPjMfUlmG1uGGeWI0H7fINfxChL-LTfD-odFUA5xqQ,5965
9
9
  aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
10
10
  aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
11
11
  aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
12
12
  aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
13
- aiteamutils/version.py,sha256=l6fF0dCvrM5gxQHard3VFOKO2YQLlkx_RlQ9Azm0-pk,42
14
- aiteamutils-0.2.53.dist-info/METADATA,sha256=LKH9v0dzZbm_PRCEZ-NS-ASC1p6pRyU3h8Mk6Nw4IYE,1718
15
- aiteamutils-0.2.53.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- aiteamutils-0.2.53.dist-info/RECORD,,
13
+ aiteamutils/version.py,sha256=q4mITsP_7Bmz6MAA1ME4HFQVgT80ojkzg8h-5uUxde4,42
14
+ aiteamutils-0.2.55.dist-info/METADATA,sha256=UrrWJHMtqDp8HsWEk3hfzMkKcfm11VbgJv21yorOR1s,1718
15
+ aiteamutils-0.2.55.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ aiteamutils-0.2.55.dist-info/RECORD,,