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 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.
@@ -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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlarec"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "A context-aware Active Record API for synchronous SQLAlchemy 2."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -24,4 +24,4 @@ __all__ = [
24
24
  "new_session",
25
25
  ]
26
26
 
27
- __version__ = "0.1.0"
27
+ __version__ = "0.1.1"
@@ -391,7 +391,7 @@ wheels = [
391
391
 
392
392
  [[package]]
393
393
  name = "sqlarec"
394
- version = "0.1.0"
394
+ version = "0.1.1"
395
395
  source = { editable = "." }
396
396
  dependencies = [
397
397
  { name = "sqlalchemy" },
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