SARepo 0.1.3__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 +32 -0
- SARepo/sa_repo.py +241 -6
- {sarepo-0.1.3.dist-info → sarepo-0.1.4.dist-info}/METADATA +1 -1
- sarepo-0.1.4.dist-info/RECORD +12 -0
- sarepo-0.1.3.dist-info/RECORD +0 -12
- {sarepo-0.1.3.dist-info → sarepo-0.1.4.dist-info}/WHEEL +0 -0
- {sarepo-0.1.3.dist-info → sarepo-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {sarepo-0.1.3.dist-info → sarepo-0.1.4.dist-info}/top_level.txt +0 -0
SARepo/repo.py
CHANGED
@@ -14,3 +14,35 @@ class CrudRepository(Protocol, Generic[T]):
|
|
14
14
|
def remove(self, entity: T) -> None: ...
|
15
15
|
def delete_by_id(self, id_: Any) -> bool: ...
|
16
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 inspect, 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
|
|
@@ -78,13 +89,139 @@ class SARepository(Generic[T]):
|
|
78
89
|
stmt.offset(page.page * page.size).limit(page.size)
|
79
90
|
).scalars().all()
|
80
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
|
81
204
|
|
82
205
|
class SAAsyncRepository(Generic[T]):
|
83
206
|
"""Async repository implementation for SQLAlchemy 2.x."""
|
84
207
|
def __init__(self, model: Type[T], session: AsyncSession):
|
85
208
|
self.model = model
|
86
209
|
self.session = session
|
87
|
-
|
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
|
+
|
88
225
|
async def getAll(self, limit: Optional[int] = None) -> List[T]:
|
89
226
|
stmt = select(self.model)
|
90
227
|
if limit is not None:
|
@@ -92,9 +229,6 @@ class SAAsyncRepository(Generic[T]):
|
|
92
229
|
result = await self.session.execute(stmt)
|
93
230
|
return result.scalars().all()
|
94
231
|
|
95
|
-
def _select(self):
|
96
|
-
return select(self.model)
|
97
|
-
|
98
232
|
async def get(self, id_: Any) -> T:
|
99
233
|
obj = await self.session.get(self.model, id_)
|
100
234
|
if not obj:
|
@@ -150,3 +284,104 @@ class SAAsyncRepository(Generic[T]):
|
|
150
284
|
)
|
151
285
|
items = res.scalars().all()
|
152
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.3.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=5Af4SLfbKOMJdX7MOev4OcGIGQ0HzN6QDUktURjasyU,597
|
5
|
-
SARepo/sa_repo.py,sha256=feuwBHAEdW_STJ6fHxYlSO0KvSruoqiB_v1sTCRy6JE,5332
|
6
|
-
SARepo/specs.py,sha256=e-X1cCkva3e71M37hcfbjNDMezZAaOkkRaPToUzhEeU,687
|
7
|
-
SARepo/uow.py,sha256=yPlvi7PoH9x2pLNt6hEin9zT5qG9axUKn_TkZcIuz1s,859
|
8
|
-
sarepo-0.1.3.dist-info/licenses/LICENSE,sha256=px90NOQCrnZ77qoFT8IvAiNS1CXQ2OU27uVBKbki9DM,131
|
9
|
-
sarepo-0.1.3.dist-info/METADATA,sha256=FXf9N-SsghCIQGucviE8GVVxh1viF0BV0FhKkMeg2HQ,2691
|
10
|
-
sarepo-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
-
sarepo-0.1.3.dist-info/top_level.txt,sha256=0k952UYVZLIIv3kZzFlxI0yzBMusZo3-XCQtxms_kS0,7
|
12
|
-
sarepo-0.1.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|