aiteamutils 0.2.75__py3-none-any.whl → 0.2.77__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/base_model.py CHANGED
@@ -1,4 +1,4 @@
1
- from datetime import datetime, timezone
1
+ from datetime import datetime, timedelta, timezone
2
2
  from typing import Any, Dict, TypeVar, Generic, Optional
3
3
  from ulid import ULID
4
4
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
@@ -24,16 +24,17 @@ class BaseColumn(Base):
24
24
  nullable=False
25
25
  )
26
26
  created_at: Mapped[datetime] = mapped_column(
27
- default=datetime.utcnow,
27
+ default=datetime.now(timezone.utc),
28
28
  index=True
29
29
  )
30
30
  updated_at: Mapped[datetime] = mapped_column(
31
- default=datetime.utcnow,
32
- onupdate=datetime.utcnow,
31
+ default=datetime.now(timezone.utc),
32
+ onupdate=datetime.now(timezone.utc),
33
33
  index=True
34
34
  )
35
35
  deleted_at: Mapped[datetime] = mapped_column(
36
36
  default=None,
37
+ nullable=True
37
38
  )
38
39
  is_deleted: Mapped[bool] = mapped_column(
39
40
  default=False,
@@ -39,12 +39,17 @@ class BaseRepository(Generic[ModelType]):
39
39
  #######################
40
40
  # 입력 및 수정, 삭제 #
41
41
  #######################
42
- async def create(self, entity_data: Dict[str, Any]) -> ModelType:
42
+ async def create(
43
+ self,
44
+ entity_data: Dict[str, Any],
45
+ exclude_entities: List[str] | None = None
46
+ ) -> ModelType:
43
47
  try:
44
48
  return await create_entity(
45
49
  session=self.session,
46
50
  model=self.model,
47
- entity_data=entity_data
51
+ entity_data=entity_data,
52
+ exclude_entities=exclude_entities
48
53
  )
49
54
  except CustomException as e:
50
55
  raise e
@@ -46,21 +46,19 @@ class BaseService(Generic[ModelType]):
46
46
  ) -> ModelType:
47
47
 
48
48
  try:
49
- # 고유 검사 수행
50
- if unique_check:
51
- await validate_unique_fields(self.db_session, unique_check, find_value=True)
52
- # 외래 검사 수행
53
- if fk_check:
54
- await validate_unique_fields(self.db_session, fk_check, find_value=False)
55
- # 비밀번호가 있으면 해시 처리
56
- if "password" in entity_data:
57
- entity_data["password"] = hash_password(entity_data["password"])
58
- # 제외할 엔티티가 있으면 제외
59
- if exclude_entities:
60
- entity_data = {k: v for k, v in entity_data.items() if k not in exclude_entities}
49
+ async with self.db_session.begin():
50
+ # 고유 검사 수행
51
+ if unique_check:
52
+ await validate_unique_fields(self.db_session, unique_check, find_value=True)
53
+ # 외래 키 검사 수행
54
+ if fk_check:
55
+ await validate_unique_fields(self.db_session, fk_check, find_value=False)
61
56
 
62
- # repository의 create 메서드를 트랜잭션 내에서 실행
63
- return await self.repository.create(entity_data)
57
+ # repository의 create 메서드를 트랜잭션 내에서 실행
58
+ return await self.repository.create(
59
+ entity_data=entity_data,
60
+ exclude_entities=exclude_entities
61
+ )
64
62
  except CustomException as e:
65
63
  raise e
66
64
  except Exception as e:
aiteamutils/database.py CHANGED
@@ -23,7 +23,6 @@ from sqlalchemy import MetaData, Table, insert
23
23
  #패키지 라이브러리
24
24
  from .exceptions import ErrorCode, CustomException
25
25
 
26
-
27
26
  ModelType = TypeVar("ModelType", bound=DeclarativeBase)
28
27
 
29
28
  ##################
@@ -32,8 +31,11 @@ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
32
31
  def process_entity_data(
33
32
  model: Type[ModelType],
34
33
  entity_data: Dict[str, Any],
35
- existing_data: Dict[str, Any] = None
34
+ existing_data: Dict[str, Any] = None,
35
+ exclude_entities: List[str] | None = None
36
36
  ) -> Dict[str, Any]:
37
+ from .security import hash_password
38
+
37
39
  """
38
40
  엔티티 데이터를 전처리하고 모델 속성과 extra_data를 분리합니다.
39
41
 
@@ -64,6 +66,9 @@ def process_entity_data(
64
66
  if existing_data and "extra_data" in existing_data:
65
67
  extra_data = existing_data["extra_data"].copy()
66
68
 
69
+ # 제외할 엔티티가 있으면 제거
70
+ if exclude_entities:
71
+ entity_data = {k: v for k, v in entity_data.items() if k not in exclude_entities}
67
72
 
68
73
  # Swagger 자동 생성 속성 패턴
69
74
  swagger_patterns = {"additionalProp1", "additionalProp2", "additionalProp3"}
@@ -73,9 +78,14 @@ def process_entity_data(
73
78
  if key in swagger_patterns:
74
79
  continue
75
80
 
81
+ # 패스워드 필드 처리
82
+ if key == "password":
83
+ value = hash_password(value) # 비밀번호 암호화
84
+
76
85
  # 모델 속성에 있는 경우 model_data에 추가
77
86
  if key in model_attr:
78
87
  model_data[key] = value
88
+
79
89
  # 모델 속성에 없는 경우 extra_data에 추가
80
90
  else:
81
91
  extra_data[key] = value
@@ -237,7 +247,8 @@ def build_conditions(
237
247
  async def create_entity(
238
248
  session: AsyncSession,
239
249
  model: Type[ModelType],
240
- entity_data: Dict[str, Any]
250
+ entity_data: Dict[str, Any],
251
+ exclude_entities: List[str] | None = None
241
252
  ) -> ModelType:
242
253
  """
243
254
  새로운 엔티티를 데이터베이스에 생성합니다.
@@ -255,9 +266,11 @@ async def create_entity(
255
266
  """
256
267
  try:
257
268
  # 엔티티 데이터 전처리
258
- processed_data = process_entity_data(model, entity_data)
259
-
260
- # 외래 키 필드 검증
269
+ processed_data = process_entity_data(
270
+ model=model,
271
+ entity_data=entity_data,
272
+ exclude_entities=exclude_entities
273
+ )
261
274
 
262
275
  # 새로운 엔티티 생성
263
276
  entity = model(**processed_data)
@@ -267,7 +280,6 @@ async def create_entity(
267
280
 
268
281
  # 데이터베이스에 커밋
269
282
  await session.flush()
270
- await session.commit()
271
283
  await session.refresh(entity)
272
284
 
273
285
  # 생성된 엔티티 반환
@@ -336,7 +348,6 @@ async def update_entity(
336
348
 
337
349
  # 변경 사항 커밋
338
350
  await session.flush()
339
- await session.commit()
340
351
  await session.refresh(entity)
341
352
 
342
353
  return entity
@@ -373,7 +384,6 @@ async def delete_entity(
373
384
  entity.deleted_at = datetime.now()
374
385
 
375
386
  await session.flush()
376
- await session.commit()
377
387
  await session.refresh(entity)
378
388
 
379
389
  except SQLAlchemyError as e:
@@ -498,33 +508,25 @@ async def get_entity(
498
508
  ##################
499
509
  async def log_create(
500
510
  session: AsyncSession,
501
- table_name: str,
511
+ model: str,
502
512
  log_data: Dict[str, Any],
503
- request: Request = None
513
+ request: Optional[Request] = None
504
514
  ) -> None:
505
515
  try:
506
- # ULID 생성
507
- if "ulid" not in log_data:
508
- log_data["ulid"] = ULID()
509
-
510
516
  # 사용자 에이전트 및 IP 주소 추가
511
517
  if request:
512
518
  log_data["user_agent"] = request.headers.get("user-agent")
513
519
  log_data["ip_address"] = request.headers.get("x-forwarded-for") or request.client.host
514
520
 
515
- # 동적으로 테이블 로드
516
- metadata = MetaData(bind=session.bind)
517
- table = Table(table_name, metadata, autoload_with=session.bind)
518
-
519
- # 데이터 삽입
520
- insert_stmt = insert(table).values(log_data)
521
- await session.execute(insert_stmt)
522
- await session.commit()
523
-
521
+ await create_entity(
522
+ session=session,
523
+ model=model,
524
+ entity_data=log_data
525
+ )
524
526
  except Exception as e:
525
527
  raise CustomException(
526
528
  ErrorCode.INTERNAL_ERROR,
527
- detail=f"{model.__name__}|{str(e)}",
529
+ detail=f"{model}|{str(e)}",
528
530
  source_function="database.log_create",
529
531
  original_error=e
530
532
  )
aiteamutils/security.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """보안 관련 유틸리티."""
2
- from datetime import datetime, timedelta, UTC
2
+ from datetime import datetime, timedelta, timezone
3
3
  from typing import Dict, Any, Optional, Literal, Callable, TYPE_CHECKING
4
4
  from fastapi import Request, HTTPException, status
5
5
  from functools import wraps
@@ -85,7 +85,7 @@ def rate_limit(
85
85
  rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
86
86
 
87
87
  try:
88
- now = datetime.now(UTC)
88
+ now = datetime.now(timezone.utc)
89
89
 
90
90
  # 현재 rate limit 정보 가져오기
91
91
  rate_info = _rate_limits.get(rate_limit_key)
@@ -136,54 +136,54 @@ async def create_jwt_token(
136
136
  user_data: Dict[str, Any],
137
137
  token_type: Literal["access", "refresh"],
138
138
  db_session: AsyncSession,
139
- token_settings: Dict[str, str],
140
- request: Optional[Request] = None,
139
+ token_settings: Dict[str, Any],
141
140
  ) -> str:
142
141
  """JWT 토큰을 생성하고 로그를 기록합니다."""
143
142
  try:
144
143
  # 토큰 데이터 구성
145
144
  if token_type == "access":
146
- expires_at = datetime.now(UTC) + timedelta(minutes=token_settings.ACCESS_TOKEN_EXPIRE_MINUTES)
145
+ expires_at = datetime.now(timezone.utc) + timedelta(minutes=token_settings["ACCESS_TOKEN_EXPIRE_MINUTES"])
146
+
147
147
  token_data = {
148
148
  # 등록 클레임
149
- "iss": token_settings.TOKEN_ISSUER,
150
- "sub": user_data["username"],
151
- "aud": token_settings.TOKEN_AUDIENCE,
149
+ "iss": token_settings["TOKEN_ISSUER"],
150
+ "sub": user_data.username,
151
+ "aud": token_settings["TOKEN_AUDIENCE"],
152
152
  "exp": expires_at,
153
153
 
154
154
  # 공개 클레임
155
- "username": user_data["username"],
156
- "name": user_data.get("name"),
155
+ "username": user_data.username,
156
+ "name": user_data.name,
157
157
 
158
158
  # 비공개 클레임
159
- "user_ulid": user_data["ulid"],
160
- "role_ulid": user_data.get("role_ulid"),
161
- "status": user_data.get("status"),
162
- "last_login": datetime.now(UTC).isoformat(),
159
+ "user_ulid": user_data.ulid,
160
+ "role_ulid": user_data.role_ulid,
161
+ "status": user_data.status,
162
+ "last_login": datetime.now(timezone.utc).isoformat(),
163
163
  "token_type": token_type,
164
164
 
165
165
  # 조직 관련 클레임
166
- "organization_ulid": user_data.get("organization_ulid"),
167
- "organization_id": user_data.get("organization_id"),
168
- "organization_name": user_data.get("organization_name"),
169
- "company_name": user_data.get("company_name")
166
+ "organization_ulid": user_data.role.organization.ulid,
167
+ "organization_id": user_data.role.organization.id,
168
+ "organization_name": user_data.role.organization.name,
169
+ "company_name": user_data.role.organization.company.name
170
170
  }
171
171
  else: # refresh token
172
- expires_at = datetime.now(UTC) + timedelta(days=14)
172
+ expires_at = datetime.now(timezone.utc) + timedelta(days=14)
173
173
  token_data = {
174
- "iss": token_settings.TOKEN_ISSUER,
175
- "sub": user_data["username"],
174
+ "iss": token_settings["TOKEN_ISSUER"],
175
+ "sub": user_data.username,
176
176
  "exp": expires_at,
177
177
  "token_type": token_type,
178
- "user_ulid": user_data["ulid"]
178
+ "user_ulid": user_data.ulid
179
179
  }
180
180
 
181
181
  # JWT 토큰 생성
182
182
  try:
183
183
  token = jwt.encode(
184
184
  token_data,
185
- token_settings.JWT_SECRET,
186
- algorithm=token_settings.JWT_ALGORITHM
185
+ token_settings["JWT_SECRET"],
186
+ algorithm=token_settings["JWT_ALGORITHM"]
187
187
  )
188
188
  except Exception as e:
189
189
  raise CustomException(
@@ -192,26 +192,6 @@ async def create_jwt_token(
192
192
  source_function="security.create_jwt_token",
193
193
  original_error=e
194
194
  )
195
-
196
- # 로그 생성
197
- try:
198
- log_data = {
199
- "type": ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED,
200
- "user_ulid": user_data["ulid"],
201
- "token": token
202
- }
203
-
204
- # log_create 함수 호출
205
- await log_create(
206
- session=db_session,
207
- table_name="user_logs",
208
- log_data=log_data,
209
- request=request
210
- )
211
- except Exception as e:
212
- # 로그 생성 실패는 토큰 생성에 영향을 주지 않음
213
- logging.error(f"Failed to create token log: {str(e)}")
214
-
215
195
  return token
216
196
 
217
197
  except CustomException as e:
@@ -227,19 +207,18 @@ async def create_jwt_token(
227
207
 
228
208
  async def verify_jwt_token(
229
209
  token: str,
230
- expected_type: Optional[Literal["access", "refresh"]] = None
210
+ expected_type: Optional[Literal["access", "refresh"]] = None,
211
+ token_settings: Dict[str, Any] = None
231
212
  ) -> Dict[str, Any]:
232
213
  """JWT 토큰을 검증합니다."""
233
214
  try:
234
- settings = get_settings()
235
-
236
215
  # 토큰 디코딩
237
216
  payload = jwt.decode(
238
217
  token,
239
- settings.jwt_secret,
240
- algorithms=[settings.jwt_algorithm],
241
- audience=settings.token_audience,
242
- issuer=settings.token_issuer
218
+ token_settings["JWT_SECRET"],
219
+ algorithms=[token_settings["JWT_ALGORITHM"]],
220
+ audience=token_settings["TOKEN_AUDIENCE"],
221
+ issuer=token_settings["TOKEN_ISSUER"]
243
222
  )
244
223
 
245
224
  # 토큰 타입 검증
@@ -277,14 +256,16 @@ async def verify_jwt_token(
277
256
  original_error=e
278
257
  )
279
258
 
280
- def validate_token(token: str) -> Dict[str, Any]:
259
+ def validate_token(
260
+ token: str,
261
+ token_settings: Dict[str, Any] = None
262
+ ) -> Dict[str, Any]:
281
263
  """JWT 토큰을 검증하고 페이로드를 반환합니다."""
282
264
  try:
283
- settings = get_settings()
284
265
  payload = jwt.decode(
285
266
  token,
286
- settings.jwt_secret,
287
- algorithms=[settings.jwt_algorithm]
267
+ token_settings["JWT_SECRET"],
268
+ algorithms=[token_settings["JWT_ALGORITHM"]]
288
269
  )
289
270
  return payload
290
271
  except JWTError as e:
aiteamutils/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  """버전 정보"""
2
- __version__ = "0.2.75"
2
+ __version__ = "0.2.77"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiteamutils
3
- Version: 0.2.75
3
+ Version: 0.2.77
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
@@ -0,0 +1,15 @@
1
+ aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
2
+ aiteamutils/base_model.py,sha256=SWifZPzlUGyKAjmV7Uoe7PUhedm215efD7zOay8DhNc,2906
3
+ aiteamutils/base_repository.py,sha256=VLCLLVkNnYLmZ6EMcr3Tu0RkMqDyMhtCO3M_2j6q-R8,4409
4
+ aiteamutils/base_service.py,sha256=GmO_fqrSEbIs_Jc5BoRBTLfaJaS6CIxYPAs4B4TdtaU,9123
5
+ aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
6
+ aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
7
+ aiteamutils/database.py,sha256=zf5Ab5ZFYQBrtvxW9te9RoVsWlMkOpTdmJ8rDwfvINs,19192
8
+ aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
9
+ aiteamutils/exceptions.py,sha256=3FUCIqXgYmMqonnMgUlh-J2xtApiiCgg4WM-2UV4vmQ,15823
10
+ aiteamutils/security.py,sha256=I8X_ABREYHObUZt1xi70v0dTCGUgGCmI6NxieZCN1qo,9648
11
+ aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
12
+ aiteamutils/version.py,sha256=73Vlr-_6fNkFw1Zd12TlIw0hBU7AOnfd-bV9rDxyz3Q,42
13
+ aiteamutils-0.2.77.dist-info/METADATA,sha256=64hwUhsBPM-DwlehLu8fXmUwJOnkRQSKWr05BxUFNxI,1718
14
+ aiteamutils-0.2.77.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ aiteamutils-0.2.77.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
2
- aiteamutils/base_model.py,sha256=TJiiA7bFhePObPJEzF1Qz4ekPfFoIxKGpe848BBdHLc,2840
3
- aiteamutils/base_repository.py,sha256=oTZP3mrkqZwAGv8cNqi0tp9wellvTuO6EXN1P1o7zRs,4285
4
- aiteamutils/base_service.py,sha256=tkGGVN8wrx9dmCVVnRSsoYGPmVD2H7NmEz-PoifIvW0,9300
5
- aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
6
- aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
7
- aiteamutils/database.py,sha256=8T8c7RpcPEcBXO-AU_-1UhnTI4RwkPLxWF0eWQYwnME,19080
8
- aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
9
- aiteamutils/exceptions.py,sha256=3FUCIqXgYmMqonnMgUlh-J2xtApiiCgg4WM-2UV4vmQ,15823
10
- aiteamutils/security.py,sha256=7cKY6XhiSEkQJe-XYE2tyYc2u1JLYeXUEjg7AUzEnQY,10277
11
- aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
12
- aiteamutils/version.py,sha256=FgfB6O4LkLSpldQbG5FG5Fai_x5fo9cg9AQ85w_3RF8,42
13
- aiteamutils-0.2.75.dist-info/METADATA,sha256=QtxRHW3XcGt5NndEB95Fue80VRGgM54zWbLtssOtkcI,1718
14
- aiteamutils-0.2.75.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- aiteamutils-0.2.75.dist-info/RECORD,,