aiteamutils 0.2.139__py3-none-any.whl → 0.2.141__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 +5 -0
- aiteamutils/base_service.py +219 -78
- aiteamutils/files.py +7 -4
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.139.dist-info → aiteamutils-0.2.141.dist-info}/METADATA +1 -1
- {aiteamutils-0.2.139.dist-info → aiteamutils-0.2.141.dist-info}/RECORD +7 -7
- {aiteamutils-0.2.139.dist-info → aiteamutils-0.2.141.dist-info}/WHEEL +0 -0
aiteamutils/base_model.py
CHANGED
@@ -109,6 +109,11 @@ class BaseFileModel(BaseColumn):
|
|
109
109
|
nullable=False,
|
110
110
|
doc="엔티티 ULID"
|
111
111
|
)
|
112
|
+
column_name: Mapped[str] = mapped_column(
|
113
|
+
String,
|
114
|
+
nullable=False,
|
115
|
+
doc="컬럼 이름"
|
116
|
+
)
|
112
117
|
original_name: Mapped[str] = mapped_column(
|
113
118
|
String,
|
114
119
|
nullable=False,
|
aiteamutils/base_service.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
#기본 라이브러리
|
2
2
|
from fastapi import Request
|
3
|
-
from typing import TypeVar, Generic, Type, Dict, Any, Union, List, Optional, Literal
|
3
|
+
from typing import TypeVar, Generic, Type, Dict, Any, Union, List, Optional, Literal, Tuple
|
4
4
|
from sqlalchemy.orm import DeclarativeBase
|
5
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
6
6
|
from datetime import datetime
|
7
7
|
from ulid import ULID
|
8
|
+
from sqlalchemy import text
|
8
9
|
|
9
10
|
#패키지 라이브러리
|
10
11
|
from .exceptions import ErrorCode, CustomException
|
@@ -35,42 +36,31 @@ class BaseService(Generic[ModelType]):
|
|
35
36
|
#######################
|
36
37
|
# 입력 및 수정, 삭제 #
|
37
38
|
#######################
|
38
|
-
async def
|
39
|
+
async def _process_files(
|
39
40
|
self,
|
40
|
-
request: Request,
|
41
41
|
entity_data: Dict[str, Any],
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
role_permission: str | None = None,
|
48
|
-
token_settings: Dict[str, Any] | None = None,
|
49
|
-
storage_dir: str | None = None
|
50
|
-
) -> ModelType:
|
51
|
-
|
52
|
-
if role_permission:
|
53
|
-
permission_result = await verify_role_permission(
|
54
|
-
request=request,
|
55
|
-
role_permission=role_permission,
|
56
|
-
token_settings=token_settings,
|
57
|
-
org_ulid_position=org_ulid_position
|
58
|
-
)
|
59
|
-
|
60
|
-
if not permission_result:
|
61
|
-
raise CustomException(
|
62
|
-
ErrorCode.FORBIDDEN,
|
63
|
-
detail=f"{role_permission}",
|
64
|
-
source_function=f"base_service.{self.__class__.__name__}.create.permission_result"
|
65
|
-
)
|
42
|
+
entity_result: Any,
|
43
|
+
storage_dir: str,
|
44
|
+
operation: Literal["create", "update", "delete"] = "create"
|
45
|
+
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
46
|
+
"""파일 처리를 위한 내부 메서드
|
66
47
|
|
48
|
+
Args:
|
49
|
+
entity_data (Dict[str, Any]): 엔티티 데이터
|
50
|
+
entity_result (Any): 생성/수정된 엔티티 결과
|
51
|
+
storage_dir (str): 저장 디렉토리 경로
|
52
|
+
operation (str): 수행할 작업 유형 ("create", "update", "delete")
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Tuple[Dict[str, Any], Dict[str, Any]]: (처리된 엔티티 데이터, 파일 정보)
|
56
|
+
"""
|
67
57
|
try:
|
68
|
-
# 파일 데이터 분리
|
69
58
|
entity_data_copy = entity_data.copy()
|
70
|
-
|
59
|
+
file_infos = {}
|
71
60
|
|
72
|
-
#
|
73
|
-
|
61
|
+
# 파일 데이터 분리
|
62
|
+
separated_files = {}
|
63
|
+
if operation != "delete" and 'extra_data' in entity_data_copy and isinstance(entity_data_copy['extra_data'], dict):
|
74
64
|
extra_data = entity_data_copy['extra_data'].copy()
|
75
65
|
file_fields = {k: v for k, v in extra_data.items() if k.endswith('_files')}
|
76
66
|
|
@@ -78,18 +68,144 @@ class BaseService(Generic[ModelType]):
|
|
78
68
|
raise CustomException(
|
79
69
|
ErrorCode.INVALID_INPUT,
|
80
70
|
detail="storage_dir is required for file upload",
|
81
|
-
source_function=f"
|
71
|
+
source_function=f"{self.__class__.__name__}._process_files"
|
82
72
|
)
|
83
73
|
|
84
74
|
# 파일 필드 분리 및 제거
|
85
75
|
for field_name, files in file_fields.items():
|
86
76
|
if files:
|
87
77
|
separated_files[field_name] = files
|
88
|
-
# extra_data에서 파일 필드 제거
|
89
78
|
del extra_data[field_name]
|
90
79
|
|
91
80
|
entity_data_copy['extra_data'] = extra_data
|
92
81
|
|
82
|
+
# 기존 파일 삭제 (update 또는 delete 작업 시)
|
83
|
+
if operation in ["update", "delete"]:
|
84
|
+
from .files import FileHandler
|
85
|
+
# files 테이블에서 기존 파일 정보 조회
|
86
|
+
existing_files = await self.db_session.execute(
|
87
|
+
text("""
|
88
|
+
SELECT storage_path
|
89
|
+
FROM files
|
90
|
+
WHERE entity_name = :entity_name
|
91
|
+
AND entity_ulid = :entity_ulid
|
92
|
+
"""),
|
93
|
+
{
|
94
|
+
"entity_name": self.model.__tablename__,
|
95
|
+
"entity_ulid": entity_result.ulid
|
96
|
+
}
|
97
|
+
)
|
98
|
+
existing_files = existing_files.fetchall()
|
99
|
+
|
100
|
+
# 기존 파일 삭제
|
101
|
+
for file_info in existing_files:
|
102
|
+
await FileHandler.delete_files(file_info[0])
|
103
|
+
|
104
|
+
# files 테이블에서 레코드 삭제
|
105
|
+
await self.db_session.execute(
|
106
|
+
text("""
|
107
|
+
DELETE FROM files
|
108
|
+
WHERE entity_name = :entity_name
|
109
|
+
AND entity_ulid = :entity_ulid
|
110
|
+
"""),
|
111
|
+
{
|
112
|
+
"entity_name": self.model.__tablename__,
|
113
|
+
"entity_ulid": entity_result.ulid
|
114
|
+
}
|
115
|
+
)
|
116
|
+
|
117
|
+
# 새 파일 저장 (create 또는 update 작업 시)
|
118
|
+
if operation != "delete" and separated_files:
|
119
|
+
from .files import FileHandler
|
120
|
+
for field_name, files in separated_files.items():
|
121
|
+
saved_files = await FileHandler.save_files(
|
122
|
+
files=files,
|
123
|
+
storage_dir=storage_dir,
|
124
|
+
entity_name=self.model.__tablename__,
|
125
|
+
entity_ulid=entity_result.ulid,
|
126
|
+
db_session=self.db_session,
|
127
|
+
column_name=field_name.replace('_files', '')
|
128
|
+
)
|
129
|
+
file_infos[field_name] = saved_files
|
130
|
+
|
131
|
+
# extra_data 업데이트 - 파일 정보 캐싱
|
132
|
+
if not hasattr(entity_result, 'extra_data'):
|
133
|
+
entity_result.extra_data = {}
|
134
|
+
if not entity_result.extra_data:
|
135
|
+
entity_result.extra_data = {}
|
136
|
+
|
137
|
+
entity_result.extra_data[field_name] = [
|
138
|
+
{
|
139
|
+
'original_name': f['original_name'],
|
140
|
+
'storage_path': f['storage_path'],
|
141
|
+
'mime_type': f['mime_type'],
|
142
|
+
'size': f['size'],
|
143
|
+
'checksum': f['checksum'],
|
144
|
+
'column_name': f['column_name']
|
145
|
+
} for f in saved_files
|
146
|
+
]
|
147
|
+
|
148
|
+
# extra_data 업데이트된 엔티티 저장
|
149
|
+
await self.db_session.flush()
|
150
|
+
|
151
|
+
return entity_data_copy, file_infos
|
152
|
+
|
153
|
+
except CustomException as e:
|
154
|
+
# 파일 저장 실패 시 저장된 파일들 삭제
|
155
|
+
if file_infos:
|
156
|
+
from .files import FileHandler
|
157
|
+
for file_list in file_infos.values():
|
158
|
+
for file_info in file_list:
|
159
|
+
await FileHandler.delete_files(file_info["storage_path"])
|
160
|
+
raise CustomException(
|
161
|
+
e.error_code,
|
162
|
+
detail=e.detail,
|
163
|
+
source_function=f"{self.__class__.__name__}._process_files",
|
164
|
+
original_error=e
|
165
|
+
)
|
166
|
+
except Exception as e:
|
167
|
+
# 파일 저장 실패 시 저장된 파일들 삭제
|
168
|
+
if file_infos:
|
169
|
+
from .files import FileHandler
|
170
|
+
for file_list in file_infos.values():
|
171
|
+
for file_info in file_list:
|
172
|
+
await FileHandler.delete_files(file_info["storage_path"])
|
173
|
+
raise CustomException(
|
174
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
175
|
+
detail=str(e),
|
176
|
+
source_function=f"{self.__class__.__name__}._process_files",
|
177
|
+
original_error=e
|
178
|
+
)
|
179
|
+
|
180
|
+
async def create(
|
181
|
+
self,
|
182
|
+
request: Request,
|
183
|
+
entity_data: Dict[str, Any],
|
184
|
+
response_model: Any = None,
|
185
|
+
exclude_entities: List[str] | None = None,
|
186
|
+
unique_check: List[Dict[str, Any]] | None = None,
|
187
|
+
fk_check: List[Dict[str, Any]] | None = None,
|
188
|
+
org_ulid_position: str = "organization_ulid",
|
189
|
+
role_permission: str | None = None,
|
190
|
+
token_settings: Dict[str, Any] | None = None,
|
191
|
+
storage_dir: str | None = None
|
192
|
+
) -> ModelType:
|
193
|
+
try:
|
194
|
+
if role_permission:
|
195
|
+
permission_result = await verify_role_permission(
|
196
|
+
request=request,
|
197
|
+
role_permission=role_permission,
|
198
|
+
token_settings=token_settings,
|
199
|
+
org_ulid_position=org_ulid_position
|
200
|
+
)
|
201
|
+
|
202
|
+
if not permission_result:
|
203
|
+
raise CustomException(
|
204
|
+
ErrorCode.FORBIDDEN,
|
205
|
+
detail=f"{role_permission}",
|
206
|
+
source_function=f"base_service.{self.__class__.__name__}.create.permission_result"
|
207
|
+
)
|
208
|
+
|
93
209
|
async with self.db_session.begin():
|
94
210
|
# 고유 검사 수행
|
95
211
|
if unique_check:
|
@@ -98,44 +214,30 @@ class BaseService(Generic[ModelType]):
|
|
98
214
|
if fk_check:
|
99
215
|
await validate_unique_fields(self.db_session, fk_check, find_value=False)
|
100
216
|
|
101
|
-
# 엔티티 생성
|
217
|
+
# 파일 데이터 분리를 위한 임시 엔티티 생성
|
218
|
+
temp_entity = type('TempEntity', (), {'ulid': str(ULID())})()
|
219
|
+
|
220
|
+
# 파일 데이터 분리
|
221
|
+
entity_data_copy, _ = await self._process_files(
|
222
|
+
entity_data=entity_data,
|
223
|
+
entity_result=temp_entity,
|
224
|
+
storage_dir=storage_dir,
|
225
|
+
operation="create"
|
226
|
+
)
|
227
|
+
|
228
|
+
# 엔티티 생성 (파일 데이터가 제거된 상태)
|
102
229
|
result = await self.repository.create(
|
103
230
|
entity_data=entity_data_copy,
|
104
231
|
exclude_entities=exclude_entities
|
105
232
|
)
|
106
233
|
|
107
|
-
# 파일 처리
|
108
|
-
file_infos =
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
storage_dir=storage_dir,
|
115
|
-
entity_name=self.model.__tablename__,
|
116
|
-
entity_ulid=result.ulid,
|
117
|
-
db_session=self.db_session
|
118
|
-
)
|
119
|
-
file_infos[field_name] = saved_files
|
120
|
-
|
121
|
-
# extra_data 업데이트
|
122
|
-
if not hasattr(result, 'extra_data'):
|
123
|
-
result.extra_data = {}
|
124
|
-
if not result.extra_data:
|
125
|
-
result.extra_data = {}
|
126
|
-
|
127
|
-
result.extra_data[field_name] = [
|
128
|
-
{
|
129
|
-
'original_name': f['original_name'],
|
130
|
-
'storage_path': f['storage_path'],
|
131
|
-
'mime_type': f['mime_type'],
|
132
|
-
'size': f['size'],
|
133
|
-
'checksum': f['checksum']
|
134
|
-
} for f in saved_files
|
135
|
-
]
|
136
|
-
|
137
|
-
# extra_data 업데이트된 엔티티 저장
|
138
|
-
await self.db_session.flush()
|
234
|
+
# 실제 파일 처리
|
235
|
+
_, file_infos = await self._process_files(
|
236
|
+
entity_data=entity_data,
|
237
|
+
entity_result=result,
|
238
|
+
storage_dir=storage_dir,
|
239
|
+
operation="create"
|
240
|
+
)
|
139
241
|
|
140
242
|
# 결과 반환
|
141
243
|
if response_model:
|
@@ -148,22 +250,15 @@ class BaseService(Generic[ModelType]):
|
|
148
250
|
return result
|
149
251
|
|
150
252
|
except CustomException as e:
|
151
|
-
# 파일 저장 실패 시 저장된 파일들 삭제
|
152
|
-
if 'file_infos' in locals():
|
153
|
-
for file_list in file_infos.values():
|
154
|
-
for file_info in file_list:
|
155
|
-
from .files import FileHandler
|
156
|
-
await FileHandler.delete_files(file_info["storage_path"])
|
157
253
|
raise e
|
158
254
|
except Exception as e:
|
159
|
-
# 다른 예외 처리
|
160
255
|
raise CustomException(
|
161
256
|
ErrorCode.INTERNAL_ERROR,
|
162
257
|
detail=str(e),
|
163
258
|
source_function=f"base_service.{self.__class__.__name__}.create",
|
164
259
|
original_error=e
|
165
260
|
)
|
166
|
-
|
261
|
+
|
167
262
|
async def update(
|
168
263
|
self,
|
169
264
|
request: Request,
|
@@ -175,7 +270,8 @@ class BaseService(Generic[ModelType]):
|
|
175
270
|
response_model: Any = None,
|
176
271
|
org_ulid_position: str = "organization_ulid",
|
177
272
|
role_permission: str = "update",
|
178
|
-
token_settings: Dict[str, Any] | None = None
|
273
|
+
token_settings: Dict[str, Any] | None = None,
|
274
|
+
storage_dir: str | None = None
|
179
275
|
) -> ModelType:
|
180
276
|
try:
|
181
277
|
async with self.db_session.begin():
|
@@ -201,16 +297,47 @@ class BaseService(Generic[ModelType]):
|
|
201
297
|
|
202
298
|
conditions = {"ulid": ulid}
|
203
299
|
|
204
|
-
|
300
|
+
# 기존 엔티티 조회
|
301
|
+
existing_entity = await self.repository.get(conditions=conditions)
|
302
|
+
if not existing_entity:
|
303
|
+
raise CustomException(
|
304
|
+
ErrorCode.NOT_FOUND,
|
305
|
+
detail=str(ulid or conditions),
|
306
|
+
source_function=f"base_service.{self.__class__.__name__}.update.get_entity"
|
307
|
+
)
|
308
|
+
|
309
|
+
# 파일 데이터 분리
|
310
|
+
entity_data_copy, _ = await self._process_files(
|
205
311
|
entity_data=entity_data,
|
312
|
+
entity_result=existing_entity,
|
313
|
+
storage_dir=storage_dir,
|
314
|
+
operation="update"
|
315
|
+
)
|
316
|
+
|
317
|
+
# 엔티티 수정 (파일 데이터가 제거된 상태)
|
318
|
+
result = await self.repository.update(
|
319
|
+
entity_data=entity_data_copy,
|
206
320
|
conditions=conditions,
|
207
321
|
exclude_entities=exclude_entities
|
208
322
|
)
|
209
323
|
|
324
|
+
# 실제 파일 처리
|
325
|
+
_, file_infos = await self._process_files(
|
326
|
+
entity_data=entity_data,
|
327
|
+
entity_result=result,
|
328
|
+
storage_dir=storage_dir,
|
329
|
+
operation="update"
|
330
|
+
)
|
331
|
+
|
210
332
|
if response_model:
|
211
|
-
|
333
|
+
processed_result = process_response(result, response_model)
|
334
|
+
# 파일 정보 추가
|
335
|
+
for key, value in file_infos.items():
|
336
|
+
processed_result[key] = value
|
337
|
+
return processed_result
|
212
338
|
else:
|
213
339
|
return result
|
340
|
+
|
214
341
|
except CustomException as e:
|
215
342
|
raise e
|
216
343
|
except Exception as e:
|
@@ -228,7 +355,8 @@ class BaseService(Generic[ModelType]):
|
|
228
355
|
conditions: Dict[str, Any] | None = None,
|
229
356
|
org_ulid_position: str = "organization_ulid",
|
230
357
|
role_permission: str = "delete",
|
231
|
-
token_settings: Dict[str, Any] | None = None
|
358
|
+
token_settings: Dict[str, Any] | None = None,
|
359
|
+
storage_dir: str | None = None
|
232
360
|
) -> bool:
|
233
361
|
try:
|
234
362
|
if not ULID.from_str(ulid):
|
@@ -251,6 +379,19 @@ class BaseService(Generic[ModelType]):
|
|
251
379
|
|
252
380
|
conditions["is_deleted"] = False
|
253
381
|
|
382
|
+
# 엔티티 조회 (파일 삭제를 위해)
|
383
|
+
entity = await self.repository.get(conditions=conditions)
|
384
|
+
if not entity:
|
385
|
+
return False
|
386
|
+
|
387
|
+
# 파일 처리 (삭제)
|
388
|
+
_, _ = await self._process_files(
|
389
|
+
entity_data={},
|
390
|
+
entity_result=entity,
|
391
|
+
storage_dir=storage_dir,
|
392
|
+
operation="delete"
|
393
|
+
)
|
394
|
+
|
254
395
|
return await self.repository.delete(
|
255
396
|
conditions=conditions
|
256
397
|
)
|
aiteamutils/files.py
CHANGED
@@ -141,7 +141,8 @@ class FileHandler:
|
|
141
141
|
storage_dir: str,
|
142
142
|
entity_name: str,
|
143
143
|
entity_ulid: str,
|
144
|
-
db_session: AsyncSession
|
144
|
+
db_session: AsyncSession,
|
145
|
+
column_name: str = None
|
145
146
|
) -> List[Dict[str, Any]]:
|
146
147
|
"""파일(들)을 저장하고 메타데이터 반환
|
147
148
|
|
@@ -151,6 +152,7 @@ class FileHandler:
|
|
151
152
|
entity_name (str): 엔티티 이름
|
152
153
|
entity_ulid (str): 엔티티 ULID
|
153
154
|
db_session (AsyncSession): DB 세션
|
155
|
+
column_name (str): 파일이 속한 컬럼 이름
|
154
156
|
|
155
157
|
Returns:
|
156
158
|
List[Dict[str, Any]]: 저장된 파일들의 메타데이터 리스트
|
@@ -174,10 +176,10 @@ class FileHandler:
|
|
174
176
|
text("""
|
175
177
|
INSERT INTO files (
|
176
178
|
entity_name, entity_ulid, original_name, storage_path,
|
177
|
-
mime_type, size, checksum
|
179
|
+
mime_type, size, checksum, column_name
|
178
180
|
) VALUES (
|
179
181
|
:entity_name, :entity_ulid, :original_name, :storage_path,
|
180
|
-
:mime_type, :size, :checksum
|
182
|
+
:mime_type, :size, :checksum, :column_name
|
181
183
|
) RETURNING *
|
182
184
|
"""),
|
183
185
|
{
|
@@ -187,7 +189,8 @@ class FileHandler:
|
|
187
189
|
"storage_path": file_info["storage_path"],
|
188
190
|
"mime_type": file_info["mime_type"],
|
189
191
|
"size": file_info["size"],
|
190
|
-
"checksum": file_info["checksum"]
|
192
|
+
"checksum": file_info["checksum"],
|
193
|
+
"column_name": column_name
|
191
194
|
}
|
192
195
|
)
|
193
196
|
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.141"
|
@@ -1,16 +1,16 @@
|
|
1
1
|
aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
|
2
|
-
aiteamutils/base_model.py,sha256=
|
2
|
+
aiteamutils/base_model.py,sha256=yBZqzTDF9PA4wCAvmYfG12FdVwLtxOEUCcA3z2i6fXU,4176
|
3
3
|
aiteamutils/base_repository.py,sha256=Oy2zE1i5qx60Xf1tnsaKLyFWapiPqt5JH8NejwNrPWg,4647
|
4
|
-
aiteamutils/base_service.py,sha256=
|
4
|
+
aiteamutils/base_service.py,sha256=3M2Mnt5Woz9upNethadxrRTd8kpmxgE_uPIgYYjqTWA,20223
|
5
5
|
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
6
|
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
7
|
aiteamutils/database.py,sha256=msvBKtxWeQVOo0v2Q9i2azuTNtnUItuNNar52gdRZTo,20418
|
8
8
|
aiteamutils/enums.py,sha256=7WLqlcJqQWtETAga2WAxNp3dJTQIAd2TW-4WzkoHHa8,2498
|
9
9
|
aiteamutils/exceptions.py,sha256=pgf3ersezObyl17wAO3I2fb8m9t2OzWDX1mSjwAWm2Y,16035
|
10
|
-
aiteamutils/files.py,sha256=
|
10
|
+
aiteamutils/files.py,sha256=Dfi1rYMBZwV-3GMqZ77_I4BNMpnyL-OBaj9WwZVhCbs,8999
|
11
11
|
aiteamutils/security.py,sha256=McUl3t5Z5SyUDVUHymHdDkYyF4YSeg4g9fFMML4W6Kw,11630
|
12
12
|
aiteamutils/validators.py,sha256=_WHN6jqJQzKM5uPTg-Da8U2qqevS84XeKMkCCF4C_lY,9591
|
13
|
-
aiteamutils/version.py,sha256=
|
14
|
-
aiteamutils-0.2.
|
15
|
-
aiteamutils-0.2.
|
16
|
-
aiteamutils-0.2.
|
13
|
+
aiteamutils/version.py,sha256=0QCUDt0xcylZJRMZSBiqnFqBsyUsdARPHNxrKiKuxJs,43
|
14
|
+
aiteamutils-0.2.141.dist-info/METADATA,sha256=p5daz8rrb2jslha3DxpLf38auLU9H7tIv8va5uxj_Ak,1743
|
15
|
+
aiteamutils-0.2.141.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.141.dist-info/RECORD,,
|
File without changes
|