SARepo 0.1.2__py3-none-any.whl → 0.1.4__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.
- SARepo/repo.py +33 -0
- SARepo/sa_repo.py +271 -6
- {sarepo-0.1.2.dist-info → sarepo-0.1.4.dist-info}/METADATA +1 -1
- sarepo-0.1.4.dist-info/RECORD +12 -0
- sarepo-0.1.2.dist-info/RECORD +0 -12
- {sarepo-0.1.2.dist-info → sarepo-0.1.4.dist-info}/WHEEL +0 -0
- {sarepo-0.1.2.dist-info → sarepo-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {sarepo-0.1.2.dist-info → sarepo-0.1.4.dist-info}/top_level.txt +0 -0
SARepo/repo.py
CHANGED
@@ -12,4 +12,37 @@ class CrudRepository(Protocol, Generic[T]):
|
|
12
12
|
def add(self, entity: T) -> T: ...
|
13
13
|
def update(self, entity: T) -> T: ...
|
14
14
|
def remove(self, entity: T) -> None: ...
|
15
|
+
def delete_by_id(self, id_: Any) -> bool: ...
|
15
16
|
def page(self, page: PageRequest, spec=None, order_by=None) -> Page[T]: ...
|
17
|
+
def get_all_by_column(
|
18
|
+
self,
|
19
|
+
column_name: str,
|
20
|
+
value: Any,
|
21
|
+
*,
|
22
|
+
limit: Optional[int] = None,
|
23
|
+
order_by=None,
|
24
|
+
include_deleted: bool = False,
|
25
|
+
**extra_filters
|
26
|
+
) -> list[T]: ...
|
27
|
+
def find_all_by_column(
|
28
|
+
self,
|
29
|
+
column_name: str,
|
30
|
+
value: Any,
|
31
|
+
*,
|
32
|
+
limit: Optional[int] = None,
|
33
|
+
order_by=None,
|
34
|
+
include_deleted: bool = False,
|
35
|
+
**extra_filters
|
36
|
+
) -> list[T]: ...
|
37
|
+
def get_or_create(
|
38
|
+
self,
|
39
|
+
defaults: Optional[dict] = None,
|
40
|
+
**unique_filters
|
41
|
+
) -> tuple[T, bool]: ...
|
42
|
+
def raw_query(self, sql: str, params: Optional[dict] = None) -> list[dict]: ...
|
43
|
+
def aggregate_avg(self, column_name: str, **filters) -> Optional[float]: ...
|
44
|
+
def aggregate_min(self, column_name: str, **filters): ...
|
45
|
+
def aggregate_max(self, column_name: str, **filters): ...
|
46
|
+
def aggregate_sum(self, column_name: str, **filters): ...
|
47
|
+
def count(self, **filters) -> int: ...
|
48
|
+
def restore(self, id_: Any) -> bool: ...
|
SARepo/sa_repo.py
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
from typing import List, Type, Generic, TypeVar, Optional, Sequence, Any, Callable
|
3
3
|
from sqlalchemy.orm import Session
|
4
4
|
from sqlalchemy.ext.asyncio import AsyncSession
|
5
|
-
from sqlalchemy import select, func
|
5
|
+
from sqlalchemy import inspect, select, func, text
|
6
6
|
from .base import PageRequest, Page, NotFoundError
|
7
7
|
|
8
8
|
T = TypeVar("T")
|
9
|
-
Spec = Callable
|
9
|
+
Spec = Callable
|
10
10
|
|
11
11
|
class SARepository(Generic[T]):
|
12
12
|
"""Synchronous repository implementation for SQLAlchemy 2.x."""
|
@@ -14,6 +14,17 @@ class SARepository(Generic[T]):
|
|
14
14
|
self.model = model
|
15
15
|
self.session = session
|
16
16
|
|
17
|
+
def _resolve_column(self, column_name: str):
|
18
|
+
try:
|
19
|
+
return getattr(self.model, column_name)
|
20
|
+
except AttributeError as e:
|
21
|
+
raise ValueError(f"Model {self.model.__name__} has no column '{column_name}'") from e
|
22
|
+
|
23
|
+
def _apply_filters(self, stmt, **filters):
|
24
|
+
if filters:
|
25
|
+
stmt = stmt.filter_by(**filters)
|
26
|
+
return stmt
|
27
|
+
|
17
28
|
def _select(self):
|
18
29
|
return select(self.model)
|
19
30
|
|
@@ -45,11 +56,26 @@ class SARepository(Generic[T]):
|
|
45
56
|
return entity
|
46
57
|
|
47
58
|
def remove(self, entity: T) -> None:
|
59
|
+
insp = inspect(entity, raiseerr=False)
|
60
|
+
if not (insp and (insp.persistent or insp.pending)):
|
61
|
+
pk = getattr(entity, "id", None)
|
62
|
+
if pk is None:
|
63
|
+
raise ValueError("remove() needs a persistent entity or an entity with a primary key set")
|
64
|
+
entity = self.session.get(self.model, pk)
|
65
|
+
if entity is None:
|
66
|
+
return
|
48
67
|
if hasattr(entity, "is_deleted"):
|
49
68
|
setattr(entity, "is_deleted", True)
|
50
69
|
else:
|
51
70
|
self.session.delete(entity)
|
52
71
|
|
72
|
+
def delete_by_id(self, id_: Any) -> bool:
|
73
|
+
obj = self.session.get(self.model, id_)
|
74
|
+
if not obj:
|
75
|
+
return False
|
76
|
+
self.remove(obj)
|
77
|
+
return True
|
78
|
+
|
53
79
|
def page(self, page: PageRequest, spec: Optional[Spec] = None, order_by=None) -> Page[T]:
|
54
80
|
stmt = self._select()
|
55
81
|
if spec:
|
@@ -63,13 +89,139 @@ class SARepository(Generic[T]):
|
|
63
89
|
stmt.offset(page.page * page.size).limit(page.size)
|
64
90
|
).scalars().all()
|
65
91
|
return Page(items, total, page.page, page.size)
|
92
|
+
|
93
|
+
def get_all_by_column(
|
94
|
+
self,
|
95
|
+
column_name: str,
|
96
|
+
value: Any,
|
97
|
+
*,
|
98
|
+
limit: Optional[int] = None,
|
99
|
+
order_by=None,
|
100
|
+
include_deleted: bool = False,
|
101
|
+
**extra_filters
|
102
|
+
) -> list[T]:
|
103
|
+
col = self._resolve_column(column_name)
|
104
|
+
stmt = select(self.model).where(col == value)
|
105
|
+
if not include_deleted and hasattr(self.model, "is_deleted"):
|
106
|
+
stmt = stmt.where(self.model.is_deleted == False) # noqa: E712
|
107
|
+
stmt = self._apply_filters(stmt, **extra_filters)
|
108
|
+
if order_by is not None:
|
109
|
+
stmt = stmt.order_by(order_by)
|
110
|
+
if limit is not None:
|
111
|
+
stmt = stmt.limit(limit)
|
112
|
+
res = self.session.execute(stmt)
|
113
|
+
return res.scalars().all()
|
114
|
+
|
115
|
+
# Alias
|
116
|
+
def find_all_by_column(self, *args, **kwargs):
|
117
|
+
return self.get_all_by_column(*args, **kwargs)
|
118
|
+
|
119
|
+
def get_or_create(
|
120
|
+
self,
|
121
|
+
defaults: Optional[dict] = None,
|
122
|
+
**unique_filters
|
123
|
+
) -> tuple[T, bool]:
|
124
|
+
"""
|
125
|
+
Возвращает (obj, created). unique_filters определяют уникальность.
|
126
|
+
defaults дополняют поля при создании.
|
127
|
+
"""
|
128
|
+
stmt = select(self.model).filter_by(**unique_filters)
|
129
|
+
if hasattr(self.model, "is_deleted"):
|
130
|
+
stmt = stmt.where(self.model.is_deleted == False) # noqa: E712
|
131
|
+
obj = self.session.execute(stmt).scalar_one_or_none()
|
132
|
+
if obj:
|
133
|
+
return obj, False
|
134
|
+
payload = {**unique_filters, **(defaults or {})}
|
135
|
+
obj = self.model(**payload) # type: ignore[call-arg]
|
136
|
+
self.session.add(obj)
|
137
|
+
self.session.flush()
|
138
|
+
self.session.refresh(obj)
|
139
|
+
return obj, True
|
140
|
+
|
141
|
+
def raw_query(self, sql: str, params: Optional[dict] = None) -> list[dict]:
|
142
|
+
"""
|
143
|
+
Безопасно выполняет сырой SQL (используй плейсхолдеры :name).
|
144
|
+
Возвращает список dict (строки).
|
145
|
+
"""
|
146
|
+
res = self.session.execute(text(sql), params or {})
|
147
|
+
# mapping() -> RowMapping (dict-like)
|
148
|
+
return [dict(row) for row in res.mappings().all()]
|
149
|
+
|
150
|
+
def aggregate_avg(self, column_name: str, **filters) -> Optional[float]:
|
151
|
+
col = self._resolve_column(column_name)
|
152
|
+
stmt = select(func.avg(col))
|
153
|
+
if hasattr(self.model, "is_deleted"):
|
154
|
+
stmt = stmt.where(self.model.is_deleted == False) # noqa: E712
|
155
|
+
stmt = self._apply_filters(stmt, **filters)
|
156
|
+
return self.session.execute(stmt).scalar()
|
157
|
+
|
158
|
+
def aggregate_min(self, column_name: str, **filters):
|
159
|
+
col = self._resolve_column(column_name)
|
160
|
+
stmt = select(func.min(col))
|
161
|
+
if hasattr(self.model, "is_deleted"):
|
162
|
+
stmt = stmt.where(self.model.is_deleted == False)
|
163
|
+
stmt = self._apply_filters(stmt, **filters)
|
164
|
+
return self.session.execute(stmt).scalar()
|
165
|
+
|
166
|
+
def aggregate_max(self, column_name: str, **filters):
|
167
|
+
col = self._resolve_column(column_name)
|
168
|
+
stmt = select(func.max(col))
|
169
|
+
if hasattr(self.model, "is_deleted"):
|
170
|
+
stmt = stmt.where(self.model.is_deleted == False)
|
171
|
+
stmt = self._apply_filters(stmt, **filters)
|
172
|
+
return self.session.execute(stmt).scalar()
|
173
|
+
|
174
|
+
def aggregate_sum(self, column_name: str, **filters):
|
175
|
+
col = self._resolve_column(column_name)
|
176
|
+
stmt = select(func.sum(col))
|
177
|
+
if hasattr(self.model, "is_deleted"):
|
178
|
+
stmt = stmt.where(self.model.is_deleted == False)
|
179
|
+
stmt = self._apply_filters(stmt, **filters)
|
180
|
+
return self.session.execute(stmt).scalar()
|
181
|
+
|
182
|
+
def count(self, **filters) -> int:
|
183
|
+
stmt = select(func.count()).select_from(self.model)
|
184
|
+
if hasattr(self.model, "is_deleted") and not filters.pop("include_deleted", False):
|
185
|
+
stmt = stmt.where(self.model.is_deleted == False) # noqa: E712
|
186
|
+
if filters:
|
187
|
+
stmt = stmt.filter_by(**filters)
|
188
|
+
return int(self.session.execute(stmt).scalar_one())
|
189
|
+
|
190
|
+
def restore(self, id_: Any) -> bool:
|
191
|
+
"""
|
192
|
+
Для soft-delete: is_deleted=False. Возвращает True, если восстановили.
|
193
|
+
"""
|
194
|
+
if not hasattr(self.model, "is_deleted"):
|
195
|
+
raise RuntimeError(f"{self.model.__name__} has no 'is_deleted' field")
|
196
|
+
obj = self.session.get(self.model, id_)
|
197
|
+
if not obj:
|
198
|
+
return False
|
199
|
+
if getattr(obj, "is_deleted", False):
|
200
|
+
setattr(obj, "is_deleted", False)
|
201
|
+
self.session.flush()
|
202
|
+
return True
|
203
|
+
return False
|
66
204
|
|
67
205
|
class SAAsyncRepository(Generic[T]):
|
68
206
|
"""Async repository implementation for SQLAlchemy 2.x."""
|
69
207
|
def __init__(self, model: Type[T], session: AsyncSession):
|
70
208
|
self.model = model
|
71
209
|
self.session = session
|
72
|
-
|
210
|
+
|
211
|
+
def _resolve_column(self, column_name: str):
|
212
|
+
try:
|
213
|
+
return getattr(self.model, column_name)
|
214
|
+
except AttributeError as e:
|
215
|
+
raise ValueError(f"Model {self.model.__name__} has no column '{column_name}'") from e
|
216
|
+
|
217
|
+
def _apply_filters(self, stmt, **filters):
|
218
|
+
if filters:
|
219
|
+
stmt = stmt.filter_by(**filters)
|
220
|
+
return stmt
|
221
|
+
|
222
|
+
def _select(self):
|
223
|
+
return select(self.model)
|
224
|
+
|
73
225
|
async def getAll(self, limit: Optional[int] = None) -> List[T]:
|
74
226
|
stmt = select(self.model)
|
75
227
|
if limit is not None:
|
@@ -77,9 +229,6 @@ class SAAsyncRepository(Generic[T]):
|
|
77
229
|
result = await self.session.execute(stmt)
|
78
230
|
return result.scalars().all()
|
79
231
|
|
80
|
-
def _select(self):
|
81
|
-
return select(self.model)
|
82
|
-
|
83
232
|
async def get(self, id_: Any) -> T:
|
84
233
|
obj = await self.session.get(self.model, id_)
|
85
234
|
if not obj:
|
@@ -101,11 +250,26 @@ class SAAsyncRepository(Generic[T]):
|
|
101
250
|
return entity
|
102
251
|
|
103
252
|
async def remove(self, entity: T) -> None:
|
253
|
+
insp = inspect(entity, raiseerr=False)
|
254
|
+
if not (insp and (insp.persistent or insp.pending)):
|
255
|
+
pk = getattr(entity, "id", None)
|
256
|
+
if pk is None:
|
257
|
+
raise ValueError("remove() needs a persistent entity or an entity with a primary key set")
|
258
|
+
entity = await self.session.get(self.model, pk)
|
259
|
+
if entity is None:
|
260
|
+
return
|
104
261
|
if hasattr(entity, "is_deleted"):
|
105
262
|
setattr(entity, "is_deleted", True)
|
106
263
|
else:
|
107
264
|
await self.session.delete(entity)
|
108
265
|
|
266
|
+
async def delete_by_id(self, id_: Any) -> bool:
|
267
|
+
obj = await self.session.get(self.model, id_)
|
268
|
+
if not obj:
|
269
|
+
return False
|
270
|
+
await self.remove(obj)
|
271
|
+
return True
|
272
|
+
|
109
273
|
async def page(self, page: PageRequest, spec: Optional[Spec] = None, order_by=None) -> Page[T]:
|
110
274
|
stmt = self._select()
|
111
275
|
if spec:
|
@@ -120,3 +284,104 @@ class SAAsyncRepository(Generic[T]):
|
|
120
284
|
)
|
121
285
|
items = res.scalars().all()
|
122
286
|
return Page(items, total, page.page, page.size)
|
287
|
+
|
288
|
+
async def get_all_by_column(
|
289
|
+
self,
|
290
|
+
column_name: str,
|
291
|
+
value: Any,
|
292
|
+
*,
|
293
|
+
limit: Optional[int] = None,
|
294
|
+
order_by=None,
|
295
|
+
include_deleted: bool = False,
|
296
|
+
**extra_filters
|
297
|
+
) -> list[T]:
|
298
|
+
col = self._resolve_column(column_name)
|
299
|
+
stmt = select(self.model).where(col == value)
|
300
|
+
if not include_deleted and hasattr(self.model, "is_deleted"):
|
301
|
+
stmt = stmt.where(self.model.is_deleted == False) # noqa: E712
|
302
|
+
stmt = self._apply_filters(stmt, **extra_filters)
|
303
|
+
if order_by is not None:
|
304
|
+
stmt = stmt.order_by(order_by)
|
305
|
+
if limit is not None:
|
306
|
+
stmt = stmt.limit(limit)
|
307
|
+
res = await self.session.execute(stmt)
|
308
|
+
return res.scalars().all()
|
309
|
+
|
310
|
+
# Alias
|
311
|
+
async def find_all_by_column(self, *args, **kwargs):
|
312
|
+
return await self.get_all_by_column(*args, **kwargs)
|
313
|
+
|
314
|
+
async def get_or_create(
|
315
|
+
self,
|
316
|
+
defaults: Optional[dict] = None,
|
317
|
+
**unique_filters
|
318
|
+
) -> tuple[T, bool]:
|
319
|
+
stmt = select(self.model).filter_by(**unique_filters)
|
320
|
+
if hasattr(self.model, "is_deleted"):
|
321
|
+
stmt = stmt.where(self.model.is_deleted == False) # noqa: E712
|
322
|
+
obj = (await self.session.execute(stmt)).scalar_one_or_none()
|
323
|
+
if obj:
|
324
|
+
return obj, False
|
325
|
+
payload = {**unique_filters, **(defaults or {})}
|
326
|
+
obj = self.model(**payload) # type: ignore[call-arg]
|
327
|
+
self.session.add(obj)
|
328
|
+
await self.session.flush()
|
329
|
+
await self.session.refresh(obj)
|
330
|
+
return obj, True
|
331
|
+
|
332
|
+
async def raw_query(self, sql: str, params: Optional[dict] = None) -> list[dict]:
|
333
|
+
res = await self.session.execute(text(sql), params or {})
|
334
|
+
return [dict(row) for row in res.mappings().all()]
|
335
|
+
|
336
|
+
async def aggregate_avg(self, column_name: str, **filters) -> Optional[float]:
|
337
|
+
col = self._resolve_column(column_name)
|
338
|
+
stmt = select(func.avg(col))
|
339
|
+
if hasattr(self.model, "is_deleted"):
|
340
|
+
stmt = stmt.where(self.model.is_deleted == False) # noqa: E712
|
341
|
+
stmt = self._apply_filters(stmt, **filters)
|
342
|
+
return (await self.session.execute(stmt)).scalar()
|
343
|
+
|
344
|
+
async def aggregate_min(self, column_name: str, **filters):
|
345
|
+
col = self._resolve_column(column_name)
|
346
|
+
stmt = select(func.min(col))
|
347
|
+
if hasattr(self.model, "is_deleted"):
|
348
|
+
stmt = stmt.where(self.model.is_deleted == False)
|
349
|
+
stmt = self._apply_filters(stmt, **filters)
|
350
|
+
return (await self.session.execute(stmt)).scalar()
|
351
|
+
|
352
|
+
async def aggregate_max(self, column_name: str, **filters):
|
353
|
+
col = self._resolve_column(column_name)
|
354
|
+
stmt = select(func.max(col))
|
355
|
+
if hasattr(self.model, "is_deleted"):
|
356
|
+
stmt = stmt.where(self.model.is_deleted == False)
|
357
|
+
stmt = self._apply_filters(stmt, **filters)
|
358
|
+
return (await self.session.execute(stmt)).scalar()
|
359
|
+
|
360
|
+
async def aggregate_sum(self, column_name: str, **filters):
|
361
|
+
col = self._resolve_column(column_name)
|
362
|
+
stmt = select(func.sum(col))
|
363
|
+
if hasattr(self.model, "is_deleted"):
|
364
|
+
stmt = stmt.where(self.model.is_deleted == False)
|
365
|
+
stmt = self._apply_filters(stmt, **filters)
|
366
|
+
return (await self.session.execute(stmt)).scalar()
|
367
|
+
|
368
|
+
async def count(self, **filters) -> int:
|
369
|
+
include_deleted = bool(filters.pop("include_deleted", False))
|
370
|
+
stmt = select(func.count()).select_from(self.model)
|
371
|
+
if hasattr(self.model, "is_deleted") and not include_deleted:
|
372
|
+
stmt = stmt.where(self.model.is_deleted == False) # noqa: E712
|
373
|
+
if filters:
|
374
|
+
stmt = stmt.filter_by(**filters)
|
375
|
+
return int((await self.session.execute(stmt)).scalar_one())
|
376
|
+
|
377
|
+
async def restore(self, id_: Any) -> bool:
|
378
|
+
if not hasattr(self.model, "is_deleted"):
|
379
|
+
raise RuntimeError(f"{self.model.__name__} has no 'is_deleted' field")
|
380
|
+
obj = await self.session.get(self.model, id_)
|
381
|
+
if not obj:
|
382
|
+
return False
|
383
|
+
if getattr(obj, "is_deleted", False):
|
384
|
+
setattr(obj, "is_deleted", False)
|
385
|
+
await self.session.flush()
|
386
|
+
return True
|
387
|
+
return False
|
@@ -0,0 +1,12 @@
|
|
1
|
+
SARepo/__init__.py,sha256=tNYbuUDloC1qVnXq7uomS3jRcaPUvy0VEZiMdYR6x1M,183
|
2
|
+
SARepo/base.py,sha256=UbAdZ9WYh_o93mrCVL3D8Q0tY_8mvm0HspO_L5m0GTQ,874
|
3
|
+
SARepo/models.py,sha256=ypSmbKAijvNH4WjSeJBgyNT8mKa_e_7-I5kiNisjGAI,570
|
4
|
+
SARepo/repo.py,sha256=WogRS4Ku2DppkLASJdPgK-POBUrUmao9E3NBEiSX2-4,1655
|
5
|
+
SARepo/sa_repo.py,sha256=NSv1UyI6zZ5O_wn0QSq__T5e-lnDGhm5HxtkyDynEe0,14926
|
6
|
+
SARepo/specs.py,sha256=e-X1cCkva3e71M37hcfbjNDMezZAaOkkRaPToUzhEeU,687
|
7
|
+
SARepo/uow.py,sha256=yPlvi7PoH9x2pLNt6hEin9zT5qG9axUKn_TkZcIuz1s,859
|
8
|
+
sarepo-0.1.4.dist-info/licenses/LICENSE,sha256=px90NOQCrnZ77qoFT8IvAiNS1CXQ2OU27uVBKbki9DM,131
|
9
|
+
sarepo-0.1.4.dist-info/METADATA,sha256=D6lgXeMHrohYrdko9BZPOF9W2e3F7u4-hFYrP6qnXFU,2691
|
10
|
+
sarepo-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
sarepo-0.1.4.dist-info/top_level.txt,sha256=0k952UYVZLIIv3kZzFlxI0yzBMusZo3-XCQtxms_kS0,7
|
12
|
+
sarepo-0.1.4.dist-info/RECORD,,
|
sarepo-0.1.2.dist-info/RECORD
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
SARepo/__init__.py,sha256=tNYbuUDloC1qVnXq7uomS3jRcaPUvy0VEZiMdYR6x1M,183
|
2
|
-
SARepo/base.py,sha256=UbAdZ9WYh_o93mrCVL3D8Q0tY_8mvm0HspO_L5m0GTQ,874
|
3
|
-
SARepo/models.py,sha256=ypSmbKAijvNH4WjSeJBgyNT8mKa_e_7-I5kiNisjGAI,570
|
4
|
-
SARepo/repo.py,sha256=QJ_8VHxnGyHIyfjEMyw8-70k9Qhv4VUjQwSN9LnJCC8,547
|
5
|
-
SARepo/sa_repo.py,sha256=P3V1fa8C3beERdGvcBAtDd1psDwpoxxx9r-om2ZwhMk,4139
|
6
|
-
SARepo/specs.py,sha256=e-X1cCkva3e71M37hcfbjNDMezZAaOkkRaPToUzhEeU,687
|
7
|
-
SARepo/uow.py,sha256=yPlvi7PoH9x2pLNt6hEin9zT5qG9axUKn_TkZcIuz1s,859
|
8
|
-
sarepo-0.1.2.dist-info/licenses/LICENSE,sha256=px90NOQCrnZ77qoFT8IvAiNS1CXQ2OU27uVBKbki9DM,131
|
9
|
-
sarepo-0.1.2.dist-info/METADATA,sha256=Ff0HK8YEwHHvZN1Lpg-VwOTvFX0H-jWJk-vpZk0TYIo,2691
|
10
|
-
sarepo-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
-
sarepo-0.1.2.dist-info/top_level.txt,sha256=0k952UYVZLIIv3kZzFlxI0yzBMusZo3-XCQtxms_kS0,7
|
12
|
-
sarepo-0.1.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|