SARepo 0.1.7__tar.gz → 0.1.8__tar.gz
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-0.1.8/PKG-INFO +157 -0
- sarepo-0.1.8/README.md +145 -0
- sarepo-0.1.8/SARepo/repo.py +109 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/SARepo/sa_repo.py +199 -69
- sarepo-0.1.8/SARepo.egg-info/PKG-INFO +157 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/pyproject.toml +1 -1
- {sarepo-0.1.7 → sarepo-0.1.8}/tests/test_sync_basic.py +23 -23
- sarepo-0.1.7/PKG-INFO +0 -90
- sarepo-0.1.7/README.md +0 -78
- sarepo-0.1.7/SARepo/repo.py +0 -74
- sarepo-0.1.7/SARepo.egg-info/PKG-INFO +0 -90
- {sarepo-0.1.7 → sarepo-0.1.8}/LICENSE +0 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/SARepo/__init__.py +0 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/SARepo/base.py +0 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/SARepo/models.py +0 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/SARepo/specs.py +0 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/SARepo/uow.py +0 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/SARepo.egg-info/SOURCES.txt +0 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/SARepo.egg-info/dependency_links.txt +0 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/SARepo.egg-info/requires.txt +0 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/SARepo.egg-info/top_level.txt +0 -0
- {sarepo-0.1.7 → sarepo-0.1.8}/setup.cfg +0 -0
sarepo-0.1.8/PKG-INFO
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: SARepo
|
3
|
+
Version: 0.1.8
|
4
|
+
Summary: Minimal, explicit Repository & Unit-of-Work layer over SQLAlchemy 2.x
|
5
|
+
Author: nurbergenovv
|
6
|
+
License: MIT
|
7
|
+
Requires-Python: >=3.11
|
8
|
+
Description-Content-Type: text/markdown
|
9
|
+
License-File: LICENSE
|
10
|
+
Requires-Dist: SQLAlchemy>=2.0
|
11
|
+
Dynamic: license-file
|
12
|
+
|
13
|
+
# SARepo
|
14
|
+
|
15
|
+
**Minimal, explicit Repository + Unit of Work layer on top of SQLAlchemy 2.x (sync & async).**
|
16
|
+
No magic method names — just clean typed APIs, composable specs, pagination, and optional soft-delete.
|
17
|
+
|
18
|
+
---
|
19
|
+
|
20
|
+
## 🚀 Quick Start (Sync)
|
21
|
+
|
22
|
+
```python
|
23
|
+
from sqlalchemy import create_engine, String
|
24
|
+
from sqlalchemy.orm import sessionmaker, DeclarativeBase, Mapped, mapped_column
|
25
|
+
|
26
|
+
from SARepo.sa_repo import SARepository
|
27
|
+
from SARepo.base import PageRequest
|
28
|
+
from SARepo.specs import and_specs, ilike
|
29
|
+
|
30
|
+
|
31
|
+
class Base(DeclarativeBase):
|
32
|
+
pass
|
33
|
+
|
34
|
+
|
35
|
+
class Request(Base):
|
36
|
+
__tablename__ = "requests"
|
37
|
+
|
38
|
+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
39
|
+
title: Mapped[str] = mapped_column(String(255))
|
40
|
+
status: Mapped[str] = mapped_column(String(50))
|
41
|
+
|
42
|
+
|
43
|
+
# Setup database
|
44
|
+
engine = create_engine("sqlite+pysqlite:///:memory:", echo=False)
|
45
|
+
Base.metadata.create_all(engine)
|
46
|
+
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
|
47
|
+
|
48
|
+
# Usage
|
49
|
+
with SessionLocal() as session:
|
50
|
+
repo = SARepository(Request, session)
|
51
|
+
|
52
|
+
# Create
|
53
|
+
r = repo.add(Request(title="Hello", status="NEW"))
|
54
|
+
session.commit()
|
55
|
+
|
56
|
+
# Query + Paginate
|
57
|
+
page = repo.page(PageRequest(0, 10), spec=ilike(Request.title, "%He%"))
|
58
|
+
print(page.total, [i.title for i in page.items])
|
59
|
+
```
|
60
|
+
|
61
|
+
---
|
62
|
+
|
63
|
+
## ⚙️ Async Quick Start
|
64
|
+
|
65
|
+
```python
|
66
|
+
import asyncio
|
67
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
68
|
+
|
69
|
+
from SARepo.sa_repo import SAAsyncRepository
|
70
|
+
from SARepo.base import PageRequest
|
71
|
+
|
72
|
+
|
73
|
+
async def main():
|
74
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
|
75
|
+
async with engine.begin() as conn:
|
76
|
+
await conn.run_sync(Base.metadata.create_all)
|
77
|
+
|
78
|
+
SessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)
|
79
|
+
|
80
|
+
async with SessionLocal() as session:
|
81
|
+
repo = SAAsyncRepository(Request, session)
|
82
|
+
|
83
|
+
await repo.add(Request(title="Hello", status="NEW"))
|
84
|
+
await session.commit()
|
85
|
+
|
86
|
+
page = await repo.page(PageRequest(0, 10))
|
87
|
+
print(page.total)
|
88
|
+
|
89
|
+
|
90
|
+
asyncio.run(main())
|
91
|
+
```
|
92
|
+
|
93
|
+
---
|
94
|
+
|
95
|
+
## 🧠 Features
|
96
|
+
|
97
|
+
✅ **SARepository / SAAsyncRepository**
|
98
|
+
- Clean CRUD operations (`get`, `add`, `update`, `remove`, etc.)
|
99
|
+
- `page()` method with total count
|
100
|
+
|
101
|
+
✅ **Composable Specs**
|
102
|
+
- Combine filters easily (`eq`, `ilike`, `and_specs`, `not_deleted`)
|
103
|
+
|
104
|
+
✅ **Soft Delete**
|
105
|
+
- Automatically filters out rows with `is_deleted=True` if the model has this column
|
106
|
+
|
107
|
+
✅ **Typed Protocol Interface**
|
108
|
+
- `CrudRepository` protocol for structural typing & IDE autocompletion
|
109
|
+
|
110
|
+
✅ **Unit of Work**
|
111
|
+
- Minimal sync & async helpers for transactional operations
|
112
|
+
|
113
|
+
---
|
114
|
+
|
115
|
+
## 🧩 Example: Spec Composition
|
116
|
+
|
117
|
+
```python
|
118
|
+
from SARepo.specs import eq, ilike, and_specs
|
119
|
+
|
120
|
+
spec = and_specs(
|
121
|
+
eq(Request.status, "NEW"),
|
122
|
+
ilike(Request.title, "%Hello%")
|
123
|
+
)
|
124
|
+
|
125
|
+
page = repo.page(PageRequest(0, 10), spec=spec)
|
126
|
+
```
|
127
|
+
|
128
|
+
---
|
129
|
+
|
130
|
+
## 📜 License
|
131
|
+
|
132
|
+
**MIT License**
|
133
|
+
Copyright (c) 2025
|
134
|
+
|
135
|
+
---
|
136
|
+
|
137
|
+
## 🧭 Project Structure (Example)
|
138
|
+
|
139
|
+
```
|
140
|
+
SARepo/
|
141
|
+
│
|
142
|
+
├── sa_repo.py # Core Repository classes (sync & async)
|
143
|
+
├── base.py # Pagination, PageRequest, DTO helpers
|
144
|
+
├── specs.py # Composable query specs
|
145
|
+
├── uow.py # Optional Unit of Work layer
|
146
|
+
├── __init__.py
|
147
|
+
```
|
148
|
+
|
149
|
+
---
|
150
|
+
|
151
|
+
### ✅ Philosophy
|
152
|
+
|
153
|
+
- No ORM “magic” — only explicit SQLAlchemy 2.0 APIs
|
154
|
+
- One repository → one model
|
155
|
+
- Predictable transaction scope
|
156
|
+
- Works with Pydantic DTOs or dataclasses
|
157
|
+
- 100 % type-safe
|
sarepo-0.1.8/README.md
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
# SARepo
|
2
|
+
|
3
|
+
**Minimal, explicit Repository + Unit of Work layer on top of SQLAlchemy 2.x (sync & async).**
|
4
|
+
No magic method names — just clean typed APIs, composable specs, pagination, and optional soft-delete.
|
5
|
+
|
6
|
+
---
|
7
|
+
|
8
|
+
## 🚀 Quick Start (Sync)
|
9
|
+
|
10
|
+
```python
|
11
|
+
from sqlalchemy import create_engine, String
|
12
|
+
from sqlalchemy.orm import sessionmaker, DeclarativeBase, Mapped, mapped_column
|
13
|
+
|
14
|
+
from SARepo.sa_repo import SARepository
|
15
|
+
from SARepo.base import PageRequest
|
16
|
+
from SARepo.specs import and_specs, ilike
|
17
|
+
|
18
|
+
|
19
|
+
class Base(DeclarativeBase):
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
class Request(Base):
|
24
|
+
__tablename__ = "requests"
|
25
|
+
|
26
|
+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
27
|
+
title: Mapped[str] = mapped_column(String(255))
|
28
|
+
status: Mapped[str] = mapped_column(String(50))
|
29
|
+
|
30
|
+
|
31
|
+
# Setup database
|
32
|
+
engine = create_engine("sqlite+pysqlite:///:memory:", echo=False)
|
33
|
+
Base.metadata.create_all(engine)
|
34
|
+
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
|
35
|
+
|
36
|
+
# Usage
|
37
|
+
with SessionLocal() as session:
|
38
|
+
repo = SARepository(Request, session)
|
39
|
+
|
40
|
+
# Create
|
41
|
+
r = repo.add(Request(title="Hello", status="NEW"))
|
42
|
+
session.commit()
|
43
|
+
|
44
|
+
# Query + Paginate
|
45
|
+
page = repo.page(PageRequest(0, 10), spec=ilike(Request.title, "%He%"))
|
46
|
+
print(page.total, [i.title for i in page.items])
|
47
|
+
```
|
48
|
+
|
49
|
+
---
|
50
|
+
|
51
|
+
## ⚙️ Async Quick Start
|
52
|
+
|
53
|
+
```python
|
54
|
+
import asyncio
|
55
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
56
|
+
|
57
|
+
from SARepo.sa_repo import SAAsyncRepository
|
58
|
+
from SARepo.base import PageRequest
|
59
|
+
|
60
|
+
|
61
|
+
async def main():
|
62
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
|
63
|
+
async with engine.begin() as conn:
|
64
|
+
await conn.run_sync(Base.metadata.create_all)
|
65
|
+
|
66
|
+
SessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)
|
67
|
+
|
68
|
+
async with SessionLocal() as session:
|
69
|
+
repo = SAAsyncRepository(Request, session)
|
70
|
+
|
71
|
+
await repo.add(Request(title="Hello", status="NEW"))
|
72
|
+
await session.commit()
|
73
|
+
|
74
|
+
page = await repo.page(PageRequest(0, 10))
|
75
|
+
print(page.total)
|
76
|
+
|
77
|
+
|
78
|
+
asyncio.run(main())
|
79
|
+
```
|
80
|
+
|
81
|
+
---
|
82
|
+
|
83
|
+
## 🧠 Features
|
84
|
+
|
85
|
+
✅ **SARepository / SAAsyncRepository**
|
86
|
+
- Clean CRUD operations (`get`, `add`, `update`, `remove`, etc.)
|
87
|
+
- `page()` method with total count
|
88
|
+
|
89
|
+
✅ **Composable Specs**
|
90
|
+
- Combine filters easily (`eq`, `ilike`, `and_specs`, `not_deleted`)
|
91
|
+
|
92
|
+
✅ **Soft Delete**
|
93
|
+
- Automatically filters out rows with `is_deleted=True` if the model has this column
|
94
|
+
|
95
|
+
✅ **Typed Protocol Interface**
|
96
|
+
- `CrudRepository` protocol for structural typing & IDE autocompletion
|
97
|
+
|
98
|
+
✅ **Unit of Work**
|
99
|
+
- Minimal sync & async helpers for transactional operations
|
100
|
+
|
101
|
+
---
|
102
|
+
|
103
|
+
## 🧩 Example: Spec Composition
|
104
|
+
|
105
|
+
```python
|
106
|
+
from SARepo.specs import eq, ilike, and_specs
|
107
|
+
|
108
|
+
spec = and_specs(
|
109
|
+
eq(Request.status, "NEW"),
|
110
|
+
ilike(Request.title, "%Hello%")
|
111
|
+
)
|
112
|
+
|
113
|
+
page = repo.page(PageRequest(0, 10), spec=spec)
|
114
|
+
```
|
115
|
+
|
116
|
+
---
|
117
|
+
|
118
|
+
## 📜 License
|
119
|
+
|
120
|
+
**MIT License**
|
121
|
+
Copyright (c) 2025
|
122
|
+
|
123
|
+
---
|
124
|
+
|
125
|
+
## 🧭 Project Structure (Example)
|
126
|
+
|
127
|
+
```
|
128
|
+
SARepo/
|
129
|
+
│
|
130
|
+
├── sa_repo.py # Core Repository classes (sync & async)
|
131
|
+
├── base.py # Pagination, PageRequest, DTO helpers
|
132
|
+
├── specs.py # Composable query specs
|
133
|
+
├── uow.py # Optional Unit of Work layer
|
134
|
+
├── __init__.py
|
135
|
+
```
|
136
|
+
|
137
|
+
---
|
138
|
+
|
139
|
+
### ✅ Philosophy
|
140
|
+
|
141
|
+
- No ORM “magic” — only explicit SQLAlchemy 2.0 APIs
|
142
|
+
- One repository → one model
|
143
|
+
- Predictable transaction scope
|
144
|
+
- Works with Pydantic DTOs or dataclasses
|
145
|
+
- 100 % type-safe
|
@@ -0,0 +1,109 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any, Generic, List, Optional, Protocol, Type, TypeVar, runtime_checkable
|
4
|
+
|
5
|
+
from SARepo.sa_repo import Spec
|
6
|
+
from .base import Page, PageRequest
|
7
|
+
|
8
|
+
T = TypeVar("T")
|
9
|
+
|
10
|
+
|
11
|
+
@runtime_checkable
|
12
|
+
class CrudRepository(Protocol, Generic[T]):
|
13
|
+
"""
|
14
|
+
Контракт CRUD-репозитория для SQLAlchemy-подобной реализации.
|
15
|
+
Реализация должна соответствовать сигнатурам ниже.
|
16
|
+
"""
|
17
|
+
|
18
|
+
model: Type[T]
|
19
|
+
|
20
|
+
|
21
|
+
def getAll(
|
22
|
+
self,
|
23
|
+
limit: Optional[int] = None,
|
24
|
+
*,
|
25
|
+
include_deleted: bool = False,
|
26
|
+
order_by=None,
|
27
|
+
**filters: Any,
|
28
|
+
) -> List[T]:
|
29
|
+
...
|
30
|
+
|
31
|
+
def get(self, id_: Any = None, *, include_deleted: bool = False, **filters: Any) -> T:
|
32
|
+
"""
|
33
|
+
Должен вернуть объект T или бросить исключение (например, NotFoundError),
|
34
|
+
если объект не найден. Если хочешь Optional — измени сигнатуру.
|
35
|
+
"""
|
36
|
+
...
|
37
|
+
|
38
|
+
def try_get(self, id_: Any = None, *, include_deleted: bool = False, **filters: Any) -> Optional[T]:
|
39
|
+
...
|
40
|
+
|
41
|
+
def add(self, entity: T) -> T:
|
42
|
+
...
|
43
|
+
|
44
|
+
def update(self, entity: T) -> T:
|
45
|
+
...
|
46
|
+
|
47
|
+
def remove(self, entity: Optional[T] = None, id: Optional[Any] = None) -> bool:
|
48
|
+
...
|
49
|
+
|
50
|
+
def delete_by_id(self, id_: Any) -> bool:
|
51
|
+
...
|
52
|
+
|
53
|
+
def page(
|
54
|
+
self,
|
55
|
+
page: PageRequest,
|
56
|
+
spec: Optional[Spec] = None,
|
57
|
+
order_by=None,
|
58
|
+
*,
|
59
|
+
include_deleted: bool = False,
|
60
|
+
) -> Page[T]:
|
61
|
+
...
|
62
|
+
|
63
|
+
def get_all_by_column(
|
64
|
+
self,
|
65
|
+
column_name: str,
|
66
|
+
value: Any,
|
67
|
+
*,
|
68
|
+
limit: Optional[int] = None,
|
69
|
+
order_by=None,
|
70
|
+
include_deleted: bool = False,
|
71
|
+
**extra_filters: Any,
|
72
|
+
) -> list[T]:
|
73
|
+
...
|
74
|
+
|
75
|
+
def find_all_by_column(
|
76
|
+
self,
|
77
|
+
column_name: str,
|
78
|
+
value: Any,
|
79
|
+
*,
|
80
|
+
limit: Optional[int] = None,
|
81
|
+
order_by=None,
|
82
|
+
include_deleted: bool = False,
|
83
|
+
**extra_filters: Any,
|
84
|
+
) -> list[T]:
|
85
|
+
...
|
86
|
+
|
87
|
+
def get_or_create(self, defaults: Optional[dict] = None, **unique_filters: Any) -> tuple[T, bool]:
|
88
|
+
...
|
89
|
+
|
90
|
+
def raw_query(self, sql: str, params: Optional[dict] = None) -> list[dict]:
|
91
|
+
...
|
92
|
+
|
93
|
+
def aggregate_avg(self, column_name: str, **filters: Any) -> Optional[float]:
|
94
|
+
...
|
95
|
+
|
96
|
+
def aggregate_min(self, column_name: str, **filters: Any):
|
97
|
+
...
|
98
|
+
|
99
|
+
def aggregate_max(self, column_name: str, **filters: Any):
|
100
|
+
...
|
101
|
+
|
102
|
+
def aggregate_sum(self, column_name: str, **filters: Any):
|
103
|
+
...
|
104
|
+
|
105
|
+
def count(self, **filters: Any) -> int:
|
106
|
+
...
|
107
|
+
|
108
|
+
def restore(self, id_: Any) -> bool:
|
109
|
+
...
|