sqlarec 0.1.0__tar.gz → 0.1.1__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.
- sqlarec-0.1.1/PKG-INFO +251 -0
- sqlarec-0.1.1/README.md +233 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/pyproject.toml +1 -1
- {sqlarec-0.1.0 → sqlarec-0.1.1}/src/sqlarec/__init__.py +1 -1
- {sqlarec-0.1.0 → sqlarec-0.1.1}/uv.lock +1 -1
- sqlarec-0.1.0/PKG-INFO +0 -324
- sqlarec-0.1.0/README.md +0 -306
- {sqlarec-0.1.0 → sqlarec-0.1.1}/Makefile +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/src/sqlarec/core/__init__.py +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/src/sqlarec/core/base_model.py +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/src/sqlarec/core/query.py +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/src/sqlarec/core/update.py +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/src/sqlarec/database.py +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/src/sqlarec/py.typed +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/src/sqlarec/utils/__init__.py +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/src/sqlarec/utils/identifiers.py +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/tests/conftest.py +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/tests/test_database.py +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/tests/test_models.py +0 -0
- {sqlarec-0.1.0 → sqlarec-0.1.1}/tests/test_query.py +0 -0
sqlarec-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlarec
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A context-aware Active Record API for synchronous SQLAlchemy 2.
|
|
5
|
+
Author: Hamza Senhaji Rhazi
|
|
6
|
+
Keywords: active-record,orm,sql,sqlalchemy
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: sqlalchemy<3,>=2.0
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# sqlarec
|
|
20
|
+
|
|
21
|
+
`sqlarec` adds a context-aware Active Record API to synchronous SQLAlchemy 2.
|
|
22
|
+
It gives models concise query and persistence methods without requiring every
|
|
23
|
+
service function to receive and forward a `Session`.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
user = User.query.where(User.email == "hamza@example.com").one_or_none()
|
|
27
|
+
users = User.query.order_by(User.name).all()
|
|
28
|
+
user = User.create(name="Hamza", email="hamza@example.com")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Your application still creates the session and controls commits, rollbacks, and
|
|
32
|
+
cleanup. `sqlarec` does not depend on a web framework and never commits inside a
|
|
33
|
+
model method.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
Install the published package from PyPI:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv add sqlarec
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
You can also install it with pip:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m pip install sqlarec
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`sqlarec` requires Python 3.11 or later, SQLAlchemy 2, and a synchronous
|
|
50
|
+
SQLAlchemy `Session`.
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
Define models with standard SQLAlchemy mapped columns:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from sqlalchemy import Boolean, String
|
|
58
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
59
|
+
|
|
60
|
+
from sqlarec import BaseModel, init_engine, new_session
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class User(BaseModel):
|
|
64
|
+
__tablename__ = "users"
|
|
65
|
+
|
|
66
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
67
|
+
name: Mapped[str] = mapped_column(String(100))
|
|
68
|
+
email: Mapped[str] = mapped_column(String(255), unique=True)
|
|
69
|
+
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
engine = init_engine("sqlite:///:memory:")
|
|
73
|
+
BaseModel.metadata.create_all(engine)
|
|
74
|
+
|
|
75
|
+
session = new_session()
|
|
76
|
+
BaseModel.register_session_provider(lambda: session)
|
|
77
|
+
|
|
78
|
+
User.create(name="Hamza", email="hamza@example.com")
|
|
79
|
+
session.commit()
|
|
80
|
+
|
|
81
|
+
user = User.query.one()
|
|
82
|
+
print(user.email)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Expected output:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
hamza@example.com
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`BaseModel` inherits from SQLAlchemy's `DeclarativeBase`, so relationships,
|
|
92
|
+
constraints, indexes, and mapper configuration continue to use normal SQLAlchemy
|
|
93
|
+
APIs.
|
|
94
|
+
|
|
95
|
+
## Keep session handling at the application boundary
|
|
96
|
+
|
|
97
|
+
Regular SQLAlchemy often requires passing a session through each application
|
|
98
|
+
layer:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from sqlalchemy import select
|
|
102
|
+
from sqlalchemy.orm import Session
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def find_user(session: Session, email: str) -> User | None:
|
|
106
|
+
return session.scalars(select(User).where(User.email == email)).one_or_none()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
With `sqlarec`, your application registers a zero-argument provider once. Models
|
|
110
|
+
resolve the current session when an operation executes:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from contextvars import ContextVar
|
|
114
|
+
|
|
115
|
+
from sqlalchemy.orm import Session
|
|
116
|
+
|
|
117
|
+
from sqlarec import BaseModel
|
|
118
|
+
|
|
119
|
+
current_session = ContextVar[Session]("current_session")
|
|
120
|
+
BaseModel.register_session_provider(current_session.get)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def find_user(email: str) -> User | None:
|
|
124
|
+
return User.query.where(User.email == email).one_or_none()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The provider can read from application state, a dependency scope, or a
|
|
128
|
+
`ContextVar`. This allows middleware to bind one session to the current request:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from sqlarec import new_session
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def database_middleware(request, handler):
|
|
135
|
+
session = new_session()
|
|
136
|
+
token = current_session.set(session)
|
|
137
|
+
try:
|
|
138
|
+
response = handler(request)
|
|
139
|
+
session.commit()
|
|
140
|
+
return response
|
|
141
|
+
except Exception:
|
|
142
|
+
session.rollback()
|
|
143
|
+
raise
|
|
144
|
+
finally:
|
|
145
|
+
current_session.reset(token)
|
|
146
|
+
session.close()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Handlers and services inside that middleware can use `User.query`,
|
|
150
|
+
`User.create()`, or `User.session` without receiving a session argument.
|
|
151
|
+
Transaction ownership remains explicit at the middleware boundary.
|
|
152
|
+
|
|
153
|
+
## Query models and rows
|
|
154
|
+
|
|
155
|
+
`Model.query` and `Model.select()` return immutable `ModelQuery` wrappers:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
users = User.query.all()
|
|
159
|
+
user = User.query.where(User.email == "hamza@example.com").one_or_none()
|
|
160
|
+
active = User.query.filter_by(active=True).order_by(User.name).limit(20).all()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Pass individual columns to `Model.select()` to receive SQLAlchemy rows:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
rows = User.select(User.id, User.email).order_by(User.id).all()
|
|
167
|
+
mappings = User.select(User.id, User.email).mappings().all()
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
User.query.all() -> Sequence[User]
|
|
172
|
+
User.select().all() -> Sequence[User]
|
|
173
|
+
User.select(User.id, User.email).all() -> Sequence[Row]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Query builders include `where()`, `filter_by()`, `order_by()`, `group_by()`,
|
|
177
|
+
`having()`, `join()`, `outerjoin()`, `limit()`, `offset()`, `distinct()`,
|
|
178
|
+
`options()`, `union()`, and `union_all()`.
|
|
179
|
+
|
|
180
|
+
## Write without hidden commits
|
|
181
|
+
|
|
182
|
+
Create, update, and delete operations flush the current session but do not
|
|
183
|
+
commit:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
user = User.create(name="Hamza", email="hamza@example.com")
|
|
187
|
+
|
|
188
|
+
user.name = "Hamza S."
|
|
189
|
+
user.save()
|
|
190
|
+
|
|
191
|
+
User.update().where(User.active.is_(False)).values(active=True).execute()
|
|
192
|
+
|
|
193
|
+
user.delete()
|
|
194
|
+
session.commit()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Keeping commits outside model methods lets your application commit or roll back
|
|
198
|
+
the complete unit of work atomically.
|
|
199
|
+
|
|
200
|
+
Single primary keys support direct lookup:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
user = User.get_by_pk(42)
|
|
204
|
+
exists = User.exists(42)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
String primary keys without a Python or database default receive a generated
|
|
208
|
+
UUID hex value. Composite primary-key lookup accepts a tuple in mapper-defined
|
|
209
|
+
key order.
|
|
210
|
+
|
|
211
|
+
## Use SQLAlchemy directly when needed
|
|
212
|
+
|
|
213
|
+
Wrappers expose their underlying SQLAlchemy statement:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
query = User.query.where(User.active.is_(True))
|
|
217
|
+
statement = query.statement
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The resolved session is also available on the model:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
result = User.session.execute(custom_statement)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`sqlarec` is an ergonomic layer, not a replacement for SQLAlchemy.
|
|
227
|
+
|
|
228
|
+
## Develop the library
|
|
229
|
+
|
|
230
|
+
Clone the repository, then install the package and development tools:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
uv sync
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
| Command | Purpose |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| `make install` | Install runtime and development dependencies. |
|
|
239
|
+
| `make install-prod` | Install runtime dependencies only. |
|
|
240
|
+
| `make test` | Run pytest. |
|
|
241
|
+
| `make lint` | Check source and tests with Ruff. |
|
|
242
|
+
| `make typecheck` | Check package types with mypy. |
|
|
243
|
+
| `make format` | Format source and tests with Ruff. |
|
|
244
|
+
| `make clean` | Remove Python, pytest, and Ruff caches. |
|
|
245
|
+
|
|
246
|
+
## Current limitations
|
|
247
|
+
|
|
248
|
+
- You must register a session provider before model operations.
|
|
249
|
+
- Only synchronous SQLAlchemy sessions are supported.
|
|
250
|
+
- Query wrappers cover common operations; use the underlying statement for
|
|
251
|
+
advanced SQLAlchemy features.
|
sqlarec-0.1.1/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# sqlarec
|
|
2
|
+
|
|
3
|
+
`sqlarec` adds a context-aware Active Record API to synchronous SQLAlchemy 2.
|
|
4
|
+
It gives models concise query and persistence methods without requiring every
|
|
5
|
+
service function to receive and forward a `Session`.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
user = User.query.where(User.email == "hamza@example.com").one_or_none()
|
|
9
|
+
users = User.query.order_by(User.name).all()
|
|
10
|
+
user = User.create(name="Hamza", email="hamza@example.com")
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Your application still creates the session and controls commits, rollbacks, and
|
|
14
|
+
cleanup. `sqlarec` does not depend on a web framework and never commits inside a
|
|
15
|
+
model method.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Install the published package from PyPI:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv add sqlarec
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
You can also install it with pip:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m pip install sqlarec
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`sqlarec` requires Python 3.11 or later, SQLAlchemy 2, and a synchronous
|
|
32
|
+
SQLAlchemy `Session`.
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
Define models with standard SQLAlchemy mapped columns:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from sqlalchemy import Boolean, String
|
|
40
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
41
|
+
|
|
42
|
+
from sqlarec import BaseModel, init_engine, new_session
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class User(BaseModel):
|
|
46
|
+
__tablename__ = "users"
|
|
47
|
+
|
|
48
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
49
|
+
name: Mapped[str] = mapped_column(String(100))
|
|
50
|
+
email: Mapped[str] = mapped_column(String(255), unique=True)
|
|
51
|
+
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
engine = init_engine("sqlite:///:memory:")
|
|
55
|
+
BaseModel.metadata.create_all(engine)
|
|
56
|
+
|
|
57
|
+
session = new_session()
|
|
58
|
+
BaseModel.register_session_provider(lambda: session)
|
|
59
|
+
|
|
60
|
+
User.create(name="Hamza", email="hamza@example.com")
|
|
61
|
+
session.commit()
|
|
62
|
+
|
|
63
|
+
user = User.query.one()
|
|
64
|
+
print(user.email)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Expected output:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
hamza@example.com
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`BaseModel` inherits from SQLAlchemy's `DeclarativeBase`, so relationships,
|
|
74
|
+
constraints, indexes, and mapper configuration continue to use normal SQLAlchemy
|
|
75
|
+
APIs.
|
|
76
|
+
|
|
77
|
+
## Keep session handling at the application boundary
|
|
78
|
+
|
|
79
|
+
Regular SQLAlchemy often requires passing a session through each application
|
|
80
|
+
layer:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from sqlalchemy import select
|
|
84
|
+
from sqlalchemy.orm import Session
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def find_user(session: Session, email: str) -> User | None:
|
|
88
|
+
return session.scalars(select(User).where(User.email == email)).one_or_none()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
With `sqlarec`, your application registers a zero-argument provider once. Models
|
|
92
|
+
resolve the current session when an operation executes:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from contextvars import ContextVar
|
|
96
|
+
|
|
97
|
+
from sqlalchemy.orm import Session
|
|
98
|
+
|
|
99
|
+
from sqlarec import BaseModel
|
|
100
|
+
|
|
101
|
+
current_session = ContextVar[Session]("current_session")
|
|
102
|
+
BaseModel.register_session_provider(current_session.get)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def find_user(email: str) -> User | None:
|
|
106
|
+
return User.query.where(User.email == email).one_or_none()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The provider can read from application state, a dependency scope, or a
|
|
110
|
+
`ContextVar`. This allows middleware to bind one session to the current request:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from sqlarec import new_session
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def database_middleware(request, handler):
|
|
117
|
+
session = new_session()
|
|
118
|
+
token = current_session.set(session)
|
|
119
|
+
try:
|
|
120
|
+
response = handler(request)
|
|
121
|
+
session.commit()
|
|
122
|
+
return response
|
|
123
|
+
except Exception:
|
|
124
|
+
session.rollback()
|
|
125
|
+
raise
|
|
126
|
+
finally:
|
|
127
|
+
current_session.reset(token)
|
|
128
|
+
session.close()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Handlers and services inside that middleware can use `User.query`,
|
|
132
|
+
`User.create()`, or `User.session` without receiving a session argument.
|
|
133
|
+
Transaction ownership remains explicit at the middleware boundary.
|
|
134
|
+
|
|
135
|
+
## Query models and rows
|
|
136
|
+
|
|
137
|
+
`Model.query` and `Model.select()` return immutable `ModelQuery` wrappers:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
users = User.query.all()
|
|
141
|
+
user = User.query.where(User.email == "hamza@example.com").one_or_none()
|
|
142
|
+
active = User.query.filter_by(active=True).order_by(User.name).limit(20).all()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Pass individual columns to `Model.select()` to receive SQLAlchemy rows:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
rows = User.select(User.id, User.email).order_by(User.id).all()
|
|
149
|
+
mappings = User.select(User.id, User.email).mappings().all()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
User.query.all() -> Sequence[User]
|
|
154
|
+
User.select().all() -> Sequence[User]
|
|
155
|
+
User.select(User.id, User.email).all() -> Sequence[Row]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Query builders include `where()`, `filter_by()`, `order_by()`, `group_by()`,
|
|
159
|
+
`having()`, `join()`, `outerjoin()`, `limit()`, `offset()`, `distinct()`,
|
|
160
|
+
`options()`, `union()`, and `union_all()`.
|
|
161
|
+
|
|
162
|
+
## Write without hidden commits
|
|
163
|
+
|
|
164
|
+
Create, update, and delete operations flush the current session but do not
|
|
165
|
+
commit:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
user = User.create(name="Hamza", email="hamza@example.com")
|
|
169
|
+
|
|
170
|
+
user.name = "Hamza S."
|
|
171
|
+
user.save()
|
|
172
|
+
|
|
173
|
+
User.update().where(User.active.is_(False)).values(active=True).execute()
|
|
174
|
+
|
|
175
|
+
user.delete()
|
|
176
|
+
session.commit()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Keeping commits outside model methods lets your application commit or roll back
|
|
180
|
+
the complete unit of work atomically.
|
|
181
|
+
|
|
182
|
+
Single primary keys support direct lookup:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
user = User.get_by_pk(42)
|
|
186
|
+
exists = User.exists(42)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
String primary keys without a Python or database default receive a generated
|
|
190
|
+
UUID hex value. Composite primary-key lookup accepts a tuple in mapper-defined
|
|
191
|
+
key order.
|
|
192
|
+
|
|
193
|
+
## Use SQLAlchemy directly when needed
|
|
194
|
+
|
|
195
|
+
Wrappers expose their underlying SQLAlchemy statement:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
query = User.query.where(User.active.is_(True))
|
|
199
|
+
statement = query.statement
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The resolved session is also available on the model:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
result = User.session.execute(custom_statement)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`sqlarec` is an ergonomic layer, not a replacement for SQLAlchemy.
|
|
209
|
+
|
|
210
|
+
## Develop the library
|
|
211
|
+
|
|
212
|
+
Clone the repository, then install the package and development tools:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
uv sync
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| Command | Purpose |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| `make install` | Install runtime and development dependencies. |
|
|
221
|
+
| `make install-prod` | Install runtime dependencies only. |
|
|
222
|
+
| `make test` | Run pytest. |
|
|
223
|
+
| `make lint` | Check source and tests with Ruff. |
|
|
224
|
+
| `make typecheck` | Check package types with mypy. |
|
|
225
|
+
| `make format` | Format source and tests with Ruff. |
|
|
226
|
+
| `make clean` | Remove Python, pytest, and Ruff caches. |
|
|
227
|
+
|
|
228
|
+
## Current limitations
|
|
229
|
+
|
|
230
|
+
- You must register a session provider before model operations.
|
|
231
|
+
- Only synchronous SQLAlchemy sessions are supported.
|
|
232
|
+
- Query wrappers cover common operations; use the underlying statement for
|
|
233
|
+
advanced SQLAlchemy features.
|
sqlarec-0.1.0/PKG-INFO
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sqlarec
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: A context-aware Active Record API for synchronous SQLAlchemy 2.
|
|
5
|
-
Author: Hamza Senhaji Rhazi
|
|
6
|
-
Keywords: active-record,orm,sql,sqlalchemy
|
|
7
|
-
Classifier: Development Status :: 3 - Alpha
|
|
8
|
-
Classifier: Intended Audience :: Developers
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
-
Classifier: Typing :: Typed
|
|
15
|
-
Requires-Python: >=3.11
|
|
16
|
-
Requires-Dist: sqlalchemy<3,>=2.0
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
|
|
19
|
-
# sqlarec
|
|
20
|
-
|
|
21
|
-
`sqlarec` adds a small, context-aware Active Record API on top of synchronous
|
|
22
|
-
SQLAlchemy 2. It keeps model operations concise without forcing your application
|
|
23
|
-
to pass a `Session` through every service and repository call.
|
|
24
|
-
|
|
25
|
-
It is designed for applications that want concise model operations:
|
|
26
|
-
|
|
27
|
-
```python
|
|
28
|
-
user = User.query.where(User.email == "hamza@example.com").one_or_none()
|
|
29
|
-
users = User.query.order_by(User.name).all()
|
|
30
|
-
user = User.create(name="Hamza", email="hamza@example.com")
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Why sqlarec
|
|
34
|
-
|
|
35
|
-
Regular SQLAlchemy makes session ownership explicit, but passing the same session
|
|
36
|
-
through every layer can become repetitive:
|
|
37
|
-
|
|
38
|
-
```python
|
|
39
|
-
def find_user(session, email):
|
|
40
|
-
return session.scalars(select(User).where(User.email == email)).one_or_none()
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
`sqlarec` lets your application register a session-provider callback once. Models
|
|
44
|
-
resolve the current session only when they execute an operation:
|
|
45
|
-
|
|
46
|
-
```python
|
|
47
|
-
user = User.query.where(User.email == email).one_or_none()
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
This separates two responsibilities:
|
|
51
|
-
|
|
52
|
-
- Your application creates the session and decides when to commit, roll back, and
|
|
53
|
-
close it.
|
|
54
|
-
- Your models use the current session without receiving it as an argument on
|
|
55
|
-
every call.
|
|
56
|
-
|
|
57
|
-
Register the provider once in your application setup, away from model and
|
|
58
|
-
business logic. A command runner, background-job worker, or web middleware can
|
|
59
|
-
then create the current session and manage its transaction lifecycle. Models use
|
|
60
|
-
that session through `User.query`, `User.create()`, or `User.session` without
|
|
61
|
-
requiring every function to accept and forward a session argument.
|
|
62
|
-
|
|
63
|
-
The following framework-neutral middleware sketch shows the principle:
|
|
64
|
-
|
|
65
|
-
```python
|
|
66
|
-
from contextvars import ContextVar
|
|
67
|
-
|
|
68
|
-
from sqlalchemy.orm import Session
|
|
69
|
-
|
|
70
|
-
from sqlarec import BaseModel, new_session
|
|
71
|
-
|
|
72
|
-
current_session = ContextVar[Session]("current_session")
|
|
73
|
-
|
|
74
|
-
# Register this once during application startup.
|
|
75
|
-
BaseModel.register_session_provider(current_session.get)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def database_middleware(handler):
|
|
79
|
-
def wrapped(request):
|
|
80
|
-
session = new_session()
|
|
81
|
-
token = current_session.set(session)
|
|
82
|
-
try:
|
|
83
|
-
response = handler(request)
|
|
84
|
-
session.commit()
|
|
85
|
-
return response
|
|
86
|
-
except Exception:
|
|
87
|
-
session.rollback()
|
|
88
|
-
raise
|
|
89
|
-
finally:
|
|
90
|
-
current_session.reset(token)
|
|
91
|
-
session.close()
|
|
92
|
-
|
|
93
|
-
return wrapped
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Code executed inside that middleware can use a model from anywhere in the
|
|
97
|
-
application:
|
|
98
|
-
|
|
99
|
-
```python
|
|
100
|
-
@database_middleware
|
|
101
|
-
def get_user(request):
|
|
102
|
-
return User.query.where(User.email == request.email).one_or_none()
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
The handler does not receive a session. `User.query` resolves the session bound
|
|
106
|
-
by the middleware to the current execution context, so concurrent requests do
|
|
107
|
-
not share sessions.
|
|
108
|
-
|
|
109
|
-
As a result, business code remains focused on model operations while the
|
|
110
|
-
application retains an explicit and reliable transaction boundary. `sqlarec`
|
|
111
|
-
does not depend on a web framework and never commits inside model methods.
|
|
112
|
-
|
|
113
|
-
## Requirements
|
|
114
|
-
|
|
115
|
-
- Python 3.11 or later
|
|
116
|
-
- SQLAlchemy 2
|
|
117
|
-
- A synchronous SQLAlchemy `Session`
|
|
118
|
-
- [uv](https://docs.astral.sh/uv/) for development
|
|
119
|
-
|
|
120
|
-
## Install the package
|
|
121
|
-
|
|
122
|
-
Install the project and development tools:
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
uv sync
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Install only runtime dependencies:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
uv sync --no-dev
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## Create your first model
|
|
135
|
-
|
|
136
|
-
Models inherit from `BaseModel` and use standard SQLAlchemy mapped columns:
|
|
137
|
-
|
|
138
|
-
```python
|
|
139
|
-
from sqlalchemy import Boolean, String
|
|
140
|
-
from sqlalchemy.orm import Mapped, mapped_column
|
|
141
|
-
|
|
142
|
-
from sqlarec import BaseModel, init_engine, new_session
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
class User(BaseModel):
|
|
146
|
-
__tablename__ = "users"
|
|
147
|
-
|
|
148
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
149
|
-
name: Mapped[str] = mapped_column(String(100))
|
|
150
|
-
email: Mapped[str] = mapped_column(String(255), unique=True)
|
|
151
|
-
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
engine = init_engine("sqlite:///:memory:")
|
|
155
|
-
BaseModel.metadata.create_all(engine)
|
|
156
|
-
|
|
157
|
-
session = new_session()
|
|
158
|
-
BaseModel.register_session_provider(lambda: session)
|
|
159
|
-
|
|
160
|
-
User.create(name="Hamza", email="hamza@example.com")
|
|
161
|
-
session.commit()
|
|
162
|
-
|
|
163
|
-
user = User.query.one()
|
|
164
|
-
print(user.email)
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
Expected output:
|
|
168
|
-
|
|
169
|
-
```text
|
|
170
|
-
hamza@example.com
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
`BaseModel` inherits from SQLAlchemy's `DeclarativeBase`. Relationships,
|
|
174
|
-
constraints, indexes, and mapper configuration continue to use normal SQLAlchemy
|
|
175
|
-
APIs.
|
|
176
|
-
|
|
177
|
-
## Register the current session
|
|
178
|
-
|
|
179
|
-
Register a zero-argument callback that returns the session your application wants
|
|
180
|
-
models to use:
|
|
181
|
-
|
|
182
|
-
```python
|
|
183
|
-
from sqlarec import BaseModel
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def get_session():
|
|
187
|
-
return session
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
BaseModel.register_session_provider(get_session)
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
Register the provider during application startup, not separately for every
|
|
194
|
-
concurrent request. The provider itself can retrieve a request-, job-, or
|
|
195
|
-
context-local session:
|
|
196
|
-
|
|
197
|
-
```python
|
|
198
|
-
from contextvars import ContextVar
|
|
199
|
-
|
|
200
|
-
from sqlalchemy.orm import Session
|
|
201
|
-
|
|
202
|
-
from sqlarec import BaseModel
|
|
203
|
-
|
|
204
|
-
current_session: ContextVar[Session] = ContextVar("current_session")
|
|
205
|
-
|
|
206
|
-
BaseModel.register_session_provider(current_session.get)
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Middleware can set and reset this context variable around each request. This
|
|
210
|
-
keeps concurrent request sessions isolated while allowing model calls to resolve
|
|
211
|
-
the correct session. Only synchronous sessions are supported.
|
|
212
|
-
|
|
213
|
-
## Query models and rows
|
|
214
|
-
|
|
215
|
-
`Model.query` and `Model.select()` return immutable `ModelQuery` wrappers. Their
|
|
216
|
-
result methods return mapped instances:
|
|
217
|
-
|
|
218
|
-
```python
|
|
219
|
-
users = User.query.all()
|
|
220
|
-
user = User.query.where(User.email == "hamza@example.com").one_or_none()
|
|
221
|
-
active = User.query.filter_by(active=True).order_by(User.name).limit(20).all()
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
Passing columns to `Model.select()` returns a `RowQuery`:
|
|
225
|
-
|
|
226
|
-
```python
|
|
227
|
-
rows = User.select(User.id, User.email).order_by(User.id).all()
|
|
228
|
-
mappings = User.select(User.id, User.email).mappings().all()
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
The result behavior remains explicit:
|
|
232
|
-
|
|
233
|
-
```text
|
|
234
|
-
User.query.all() -> Sequence[User]
|
|
235
|
-
User.select().all() -> Sequence[User]
|
|
236
|
-
User.select(User.id, User.email).all() -> Sequence[Row]
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
Query builders include `where()`, `filter_by()`, `order_by()`, `group_by()`,
|
|
240
|
-
`having()`, `join()`, `outerjoin()`, `limit()`, `offset()`, `distinct()`,
|
|
241
|
-
`options()`, `union()`, and `union_all()`.
|
|
242
|
-
|
|
243
|
-
## Create, update, and delete models
|
|
244
|
-
|
|
245
|
-
Model writes flush the current session but never commit:
|
|
246
|
-
|
|
247
|
-
```python
|
|
248
|
-
user = User.create(name="Hamza", email="hamza@example.com")
|
|
249
|
-
|
|
250
|
-
user.name = "Hamza S."
|
|
251
|
-
user.save()
|
|
252
|
-
|
|
253
|
-
User.update().where(User.active.is_(False)).values(active=True).execute()
|
|
254
|
-
|
|
255
|
-
user.delete()
|
|
256
|
-
session.commit()
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
Keeping the transaction boundary outside model methods lets an application commit
|
|
260
|
-
or roll back a complete unit of work atomically.
|
|
261
|
-
|
|
262
|
-
Single primary keys support direct lookup:
|
|
263
|
-
|
|
264
|
-
```python
|
|
265
|
-
user = User.get_by_pk(42)
|
|
266
|
-
exists = User.exists(42)
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
String primary keys without a Python or database default receive a generated UUID
|
|
270
|
-
hex value. Composite primary-key lookup accepts a tuple in mapper-defined key
|
|
271
|
-
order.
|
|
272
|
-
|
|
273
|
-
## Use SQLAlchemy directly when needed
|
|
274
|
-
|
|
275
|
-
Every query and update wrapper exposes its underlying SQLAlchemy statement:
|
|
276
|
-
|
|
277
|
-
```python
|
|
278
|
-
query = User.query.where(User.active.is_(True))
|
|
279
|
-
statement = query.statement
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
Use the registered session for operations the wrappers do not cover:
|
|
283
|
-
|
|
284
|
-
```python
|
|
285
|
-
result = User.session.execute(custom_statement)
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
`sqlarec` is an ergonomic layer, not a replacement for SQLAlchemy.
|
|
289
|
-
|
|
290
|
-
## Develop the library
|
|
291
|
-
|
|
292
|
-
```text
|
|
293
|
-
sqlarec/
|
|
294
|
-
├── src/sqlarec/
|
|
295
|
-
│ ├── __init__.py
|
|
296
|
-
│ ├── database.py
|
|
297
|
-
│ ├── core/
|
|
298
|
-
│ │ ├── base_model.py
|
|
299
|
-
│ │ ├── query.py
|
|
300
|
-
│ │ └── update.py
|
|
301
|
-
│ └── utils/
|
|
302
|
-
│ └── identifiers.py
|
|
303
|
-
├── tests/
|
|
304
|
-
├── Makefile
|
|
305
|
-
├── pyproject.toml
|
|
306
|
-
└── uv.lock
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
| Command | Purpose |
|
|
310
|
-
| --------------------- | --------------------------------------------- |
|
|
311
|
-
| `make install` | Install runtime and development dependencies. |
|
|
312
|
-
| `make install-prod` | Install runtime dependencies only. |
|
|
313
|
-
| `make test` | Run pytest. |
|
|
314
|
-
| `make lint` | Check source and tests with Ruff. |
|
|
315
|
-
| `make typecheck` | Check package types with mypy. |
|
|
316
|
-
| `make format` | Format source and tests with Ruff. |
|
|
317
|
-
| `make clean` | Remove Python, pytest, and Ruff caches. |
|
|
318
|
-
|
|
319
|
-
## Current limitations
|
|
320
|
-
|
|
321
|
-
- You must register a session provider before model operations.
|
|
322
|
-
- Only synchronous SQLAlchemy sessions are supported.
|
|
323
|
-
- Query wrappers cover common operations; use the underlying statement for
|
|
324
|
-
advanced SQLAlchemy features.
|
sqlarec-0.1.0/README.md
DELETED
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
# sqlarec
|
|
2
|
-
|
|
3
|
-
`sqlarec` adds a small, context-aware Active Record API on top of synchronous
|
|
4
|
-
SQLAlchemy 2. It keeps model operations concise without forcing your application
|
|
5
|
-
to pass a `Session` through every service and repository call.
|
|
6
|
-
|
|
7
|
-
It is designed for applications that want concise model operations:
|
|
8
|
-
|
|
9
|
-
```python
|
|
10
|
-
user = User.query.where(User.email == "hamza@example.com").one_or_none()
|
|
11
|
-
users = User.query.order_by(User.name).all()
|
|
12
|
-
user = User.create(name="Hamza", email="hamza@example.com")
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Why sqlarec
|
|
16
|
-
|
|
17
|
-
Regular SQLAlchemy makes session ownership explicit, but passing the same session
|
|
18
|
-
through every layer can become repetitive:
|
|
19
|
-
|
|
20
|
-
```python
|
|
21
|
-
def find_user(session, email):
|
|
22
|
-
return session.scalars(select(User).where(User.email == email)).one_or_none()
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
`sqlarec` lets your application register a session-provider callback once. Models
|
|
26
|
-
resolve the current session only when they execute an operation:
|
|
27
|
-
|
|
28
|
-
```python
|
|
29
|
-
user = User.query.where(User.email == email).one_or_none()
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
This separates two responsibilities:
|
|
33
|
-
|
|
34
|
-
- Your application creates the session and decides when to commit, roll back, and
|
|
35
|
-
close it.
|
|
36
|
-
- Your models use the current session without receiving it as an argument on
|
|
37
|
-
every call.
|
|
38
|
-
|
|
39
|
-
Register the provider once in your application setup, away from model and
|
|
40
|
-
business logic. A command runner, background-job worker, or web middleware can
|
|
41
|
-
then create the current session and manage its transaction lifecycle. Models use
|
|
42
|
-
that session through `User.query`, `User.create()`, or `User.session` without
|
|
43
|
-
requiring every function to accept and forward a session argument.
|
|
44
|
-
|
|
45
|
-
The following framework-neutral middleware sketch shows the principle:
|
|
46
|
-
|
|
47
|
-
```python
|
|
48
|
-
from contextvars import ContextVar
|
|
49
|
-
|
|
50
|
-
from sqlalchemy.orm import Session
|
|
51
|
-
|
|
52
|
-
from sqlarec import BaseModel, new_session
|
|
53
|
-
|
|
54
|
-
current_session = ContextVar[Session]("current_session")
|
|
55
|
-
|
|
56
|
-
# Register this once during application startup.
|
|
57
|
-
BaseModel.register_session_provider(current_session.get)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def database_middleware(handler):
|
|
61
|
-
def wrapped(request):
|
|
62
|
-
session = new_session()
|
|
63
|
-
token = current_session.set(session)
|
|
64
|
-
try:
|
|
65
|
-
response = handler(request)
|
|
66
|
-
session.commit()
|
|
67
|
-
return response
|
|
68
|
-
except Exception:
|
|
69
|
-
session.rollback()
|
|
70
|
-
raise
|
|
71
|
-
finally:
|
|
72
|
-
current_session.reset(token)
|
|
73
|
-
session.close()
|
|
74
|
-
|
|
75
|
-
return wrapped
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Code executed inside that middleware can use a model from anywhere in the
|
|
79
|
-
application:
|
|
80
|
-
|
|
81
|
-
```python
|
|
82
|
-
@database_middleware
|
|
83
|
-
def get_user(request):
|
|
84
|
-
return User.query.where(User.email == request.email).one_or_none()
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
The handler does not receive a session. `User.query` resolves the session bound
|
|
88
|
-
by the middleware to the current execution context, so concurrent requests do
|
|
89
|
-
not share sessions.
|
|
90
|
-
|
|
91
|
-
As a result, business code remains focused on model operations while the
|
|
92
|
-
application retains an explicit and reliable transaction boundary. `sqlarec`
|
|
93
|
-
does not depend on a web framework and never commits inside model methods.
|
|
94
|
-
|
|
95
|
-
## Requirements
|
|
96
|
-
|
|
97
|
-
- Python 3.11 or later
|
|
98
|
-
- SQLAlchemy 2
|
|
99
|
-
- A synchronous SQLAlchemy `Session`
|
|
100
|
-
- [uv](https://docs.astral.sh/uv/) for development
|
|
101
|
-
|
|
102
|
-
## Install the package
|
|
103
|
-
|
|
104
|
-
Install the project and development tools:
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
uv sync
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Install only runtime dependencies:
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
uv sync --no-dev
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Create your first model
|
|
117
|
-
|
|
118
|
-
Models inherit from `BaseModel` and use standard SQLAlchemy mapped columns:
|
|
119
|
-
|
|
120
|
-
```python
|
|
121
|
-
from sqlalchemy import Boolean, String
|
|
122
|
-
from sqlalchemy.orm import Mapped, mapped_column
|
|
123
|
-
|
|
124
|
-
from sqlarec import BaseModel, init_engine, new_session
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class User(BaseModel):
|
|
128
|
-
__tablename__ = "users"
|
|
129
|
-
|
|
130
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
131
|
-
name: Mapped[str] = mapped_column(String(100))
|
|
132
|
-
email: Mapped[str] = mapped_column(String(255), unique=True)
|
|
133
|
-
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
engine = init_engine("sqlite:///:memory:")
|
|
137
|
-
BaseModel.metadata.create_all(engine)
|
|
138
|
-
|
|
139
|
-
session = new_session()
|
|
140
|
-
BaseModel.register_session_provider(lambda: session)
|
|
141
|
-
|
|
142
|
-
User.create(name="Hamza", email="hamza@example.com")
|
|
143
|
-
session.commit()
|
|
144
|
-
|
|
145
|
-
user = User.query.one()
|
|
146
|
-
print(user.email)
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
Expected output:
|
|
150
|
-
|
|
151
|
-
```text
|
|
152
|
-
hamza@example.com
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
`BaseModel` inherits from SQLAlchemy's `DeclarativeBase`. Relationships,
|
|
156
|
-
constraints, indexes, and mapper configuration continue to use normal SQLAlchemy
|
|
157
|
-
APIs.
|
|
158
|
-
|
|
159
|
-
## Register the current session
|
|
160
|
-
|
|
161
|
-
Register a zero-argument callback that returns the session your application wants
|
|
162
|
-
models to use:
|
|
163
|
-
|
|
164
|
-
```python
|
|
165
|
-
from sqlarec import BaseModel
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def get_session():
|
|
169
|
-
return session
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
BaseModel.register_session_provider(get_session)
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Register the provider during application startup, not separately for every
|
|
176
|
-
concurrent request. The provider itself can retrieve a request-, job-, or
|
|
177
|
-
context-local session:
|
|
178
|
-
|
|
179
|
-
```python
|
|
180
|
-
from contextvars import ContextVar
|
|
181
|
-
|
|
182
|
-
from sqlalchemy.orm import Session
|
|
183
|
-
|
|
184
|
-
from sqlarec import BaseModel
|
|
185
|
-
|
|
186
|
-
current_session: ContextVar[Session] = ContextVar("current_session")
|
|
187
|
-
|
|
188
|
-
BaseModel.register_session_provider(current_session.get)
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
Middleware can set and reset this context variable around each request. This
|
|
192
|
-
keeps concurrent request sessions isolated while allowing model calls to resolve
|
|
193
|
-
the correct session. Only synchronous sessions are supported.
|
|
194
|
-
|
|
195
|
-
## Query models and rows
|
|
196
|
-
|
|
197
|
-
`Model.query` and `Model.select()` return immutable `ModelQuery` wrappers. Their
|
|
198
|
-
result methods return mapped instances:
|
|
199
|
-
|
|
200
|
-
```python
|
|
201
|
-
users = User.query.all()
|
|
202
|
-
user = User.query.where(User.email == "hamza@example.com").one_or_none()
|
|
203
|
-
active = User.query.filter_by(active=True).order_by(User.name).limit(20).all()
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Passing columns to `Model.select()` returns a `RowQuery`:
|
|
207
|
-
|
|
208
|
-
```python
|
|
209
|
-
rows = User.select(User.id, User.email).order_by(User.id).all()
|
|
210
|
-
mappings = User.select(User.id, User.email).mappings().all()
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
The result behavior remains explicit:
|
|
214
|
-
|
|
215
|
-
```text
|
|
216
|
-
User.query.all() -> Sequence[User]
|
|
217
|
-
User.select().all() -> Sequence[User]
|
|
218
|
-
User.select(User.id, User.email).all() -> Sequence[Row]
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
Query builders include `where()`, `filter_by()`, `order_by()`, `group_by()`,
|
|
222
|
-
`having()`, `join()`, `outerjoin()`, `limit()`, `offset()`, `distinct()`,
|
|
223
|
-
`options()`, `union()`, and `union_all()`.
|
|
224
|
-
|
|
225
|
-
## Create, update, and delete models
|
|
226
|
-
|
|
227
|
-
Model writes flush the current session but never commit:
|
|
228
|
-
|
|
229
|
-
```python
|
|
230
|
-
user = User.create(name="Hamza", email="hamza@example.com")
|
|
231
|
-
|
|
232
|
-
user.name = "Hamza S."
|
|
233
|
-
user.save()
|
|
234
|
-
|
|
235
|
-
User.update().where(User.active.is_(False)).values(active=True).execute()
|
|
236
|
-
|
|
237
|
-
user.delete()
|
|
238
|
-
session.commit()
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Keeping the transaction boundary outside model methods lets an application commit
|
|
242
|
-
or roll back a complete unit of work atomically.
|
|
243
|
-
|
|
244
|
-
Single primary keys support direct lookup:
|
|
245
|
-
|
|
246
|
-
```python
|
|
247
|
-
user = User.get_by_pk(42)
|
|
248
|
-
exists = User.exists(42)
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
String primary keys without a Python or database default receive a generated UUID
|
|
252
|
-
hex value. Composite primary-key lookup accepts a tuple in mapper-defined key
|
|
253
|
-
order.
|
|
254
|
-
|
|
255
|
-
## Use SQLAlchemy directly when needed
|
|
256
|
-
|
|
257
|
-
Every query and update wrapper exposes its underlying SQLAlchemy statement:
|
|
258
|
-
|
|
259
|
-
```python
|
|
260
|
-
query = User.query.where(User.active.is_(True))
|
|
261
|
-
statement = query.statement
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
Use the registered session for operations the wrappers do not cover:
|
|
265
|
-
|
|
266
|
-
```python
|
|
267
|
-
result = User.session.execute(custom_statement)
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
`sqlarec` is an ergonomic layer, not a replacement for SQLAlchemy.
|
|
271
|
-
|
|
272
|
-
## Develop the library
|
|
273
|
-
|
|
274
|
-
```text
|
|
275
|
-
sqlarec/
|
|
276
|
-
├── src/sqlarec/
|
|
277
|
-
│ ├── __init__.py
|
|
278
|
-
│ ├── database.py
|
|
279
|
-
│ ├── core/
|
|
280
|
-
│ │ ├── base_model.py
|
|
281
|
-
│ │ ├── query.py
|
|
282
|
-
│ │ └── update.py
|
|
283
|
-
│ └── utils/
|
|
284
|
-
│ └── identifiers.py
|
|
285
|
-
├── tests/
|
|
286
|
-
├── Makefile
|
|
287
|
-
├── pyproject.toml
|
|
288
|
-
└── uv.lock
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
| Command | Purpose |
|
|
292
|
-
| --------------------- | --------------------------------------------- |
|
|
293
|
-
| `make install` | Install runtime and development dependencies. |
|
|
294
|
-
| `make install-prod` | Install runtime dependencies only. |
|
|
295
|
-
| `make test` | Run pytest. |
|
|
296
|
-
| `make lint` | Check source and tests with Ruff. |
|
|
297
|
-
| `make typecheck` | Check package types with mypy. |
|
|
298
|
-
| `make format` | Format source and tests with Ruff. |
|
|
299
|
-
| `make clean` | Remove Python, pytest, and Ruff caches. |
|
|
300
|
-
|
|
301
|
-
## Current limitations
|
|
302
|
-
|
|
303
|
-
- You must register a session provider before model operations.
|
|
304
|
-
- Only synchronous SQLAlchemy sessions are supported.
|
|
305
|
-
- Query wrappers cover common operations; use the underlying statement for
|
|
306
|
-
advanced SQLAlchemy features.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|