sqlphilosophy 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.
- sqlphilosophy-0.1.1/PKG-INFO +217 -0
- sqlphilosophy-0.1.1/README.md +178 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/pyproject.toml +15 -2
- sqlphilosophy-0.1.1/src/sqlphilosophy/VERSION +1 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/aio/query.py +51 -46
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/aio/repository.py +23 -18
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/audit/listener.py +3 -2
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/sql.py +9 -6
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/sync/query.py +61 -48
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/sync/repository.py +19 -15
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/types.py +11 -0
- sqlphilosophy-0.1.1/src/sqlphilosophy.egg-info/PKG-INFO +217 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy.egg-info/SOURCES.txt +1 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy.egg-info/requires.txt +6 -2
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_async_repository.py +12 -0
- sqlphilosophy-0.1.1/tests/test_batch7a_behavior.py +155 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_coverage_gaps.py +6 -6
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_import_contract.py +11 -9
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_last_mile.py +4 -4
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_sql_helpers.py +9 -4
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_sql_row_edges.py +2 -2
- sqlphilosophy-0.1.0/PKG-INFO +0 -134
- sqlphilosophy-0.1.0/README.md +0 -99
- sqlphilosophy-0.1.0/src/sqlphilosophy/VERSION +0 -1
- sqlphilosophy-0.1.0/src/sqlphilosophy.egg-info/PKG-INFO +0 -134
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/LICENSE +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/setup.cfg +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/__init__.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/aio/__init__.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/aio/protocols.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/audit/__init__.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/audit/context.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/audit/fields.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/audit/model.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/py.typed +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/sorting.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/sync/__init__.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy/sync/protocols.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy.egg-info/dependency_links.txt +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/src/sqlphilosophy.egg-info/top_level.txt +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_async_last_mile.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_async_query.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_audit.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_close_coverage.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_final_coverage.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_package_boundaries.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_sorting.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_sync_query.py +0 -0
- {sqlphilosophy-0.1.0 → sqlphilosophy-0.1.1}/tests/test_sync_repository.py +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlphilosophy
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Portable SQLAlchemy repository kit: sync and async CRUD, statement builders, sort/pagination, and SQL helpers.
|
|
5
|
+
Author-email: Josh Martin <denverprogrammer@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SignalSafeSoftware/sqlphilosophy
|
|
8
|
+
Project-URL: Repository, https://github.com/SignalSafeSoftware/sqlphilosophy
|
|
9
|
+
Project-URL: Documentation, https://github.com/SignalSafeSoftware/sqlphilosophy#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/SignalSafeSoftware/sqlphilosophy/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/SignalSafeSoftware/sqlphilosophy/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: sqlalchemy,repository-pattern,repository,orm,database,pagination,audit,async
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: <4.0,>=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: sqlalchemy<3,>=2.0
|
|
24
|
+
Provides-Extra: async
|
|
25
|
+
Requires-Dist: greenlet>=3.0; extra == "async"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=6; extra == "dev"
|
|
30
|
+
Requires-Dist: aiosqlite>=0.20; extra == "dev"
|
|
31
|
+
Requires-Dist: greenlet>=3.0; extra == "dev"
|
|
32
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
33
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
34
|
+
Requires-Dist: flake8>=7; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
36
|
+
Requires-Dist: bandit>=1.7; extra == "dev"
|
|
37
|
+
Requires-Dist: types-greenlet>=3.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# sqlphilosophy
|
|
41
|
+
|
|
42
|
+
Portable SQLAlchemy repository kit: sync and async CRUD, fluent statement builders, sort/pagination, Core SQL helpers, and optional audit listeners.
|
|
43
|
+
|
|
44
|
+
| | |
|
|
45
|
+
|---|---|
|
|
46
|
+
| **PyPI** | [`sqlphilosophy`](https://pypi.org/project/sqlphilosophy/) |
|
|
47
|
+
| **GitHub** | [SignalSafeSoftware/sqlphilosophy](https://github.com/SignalSafeSoftware/sqlphilosophy) |
|
|
48
|
+
| **Import** | `sqlphilosophy` (explicit submodules — no root re-exports) |
|
|
49
|
+
| **Python** | 3.12+ |
|
|
50
|
+
| **License** | MIT — see [LICENSE](./LICENSE) |
|
|
51
|
+
|
|
52
|
+
## What this package does
|
|
53
|
+
|
|
54
|
+
- **Repository pattern** for a single mapped model (`BaseRepository`, `AsyncBaseRepository`).
|
|
55
|
+
- **Fluent query builders** with pagination/sort (`StatementQueryBuilder`, `ListQuery`, `SortConfig`).
|
|
56
|
+
- **SQL helpers** for row mapping, partial updates, filters, and developer-defined raw SQL fragments.
|
|
57
|
+
- **Optional audit listeners** and timestamp mixins.
|
|
58
|
+
|
|
59
|
+
## What this package does not do
|
|
60
|
+
|
|
61
|
+
- Migrations, schema design, or connection pooling configuration.
|
|
62
|
+
- Authorization, multi-tenant isolation, or query sandboxing.
|
|
63
|
+
- Automatic commits for normal CRUD — see [Transaction ownership](#transaction-ownership) below.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install sqlphilosophy
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Async ORM (`AsyncSession`) also needs greenlet:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install sqlphilosophy[async]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Requires Python 3.12+ and SQLAlchemy 2.x.
|
|
78
|
+
|
|
79
|
+
## Full example (sync model + session)
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from sqlalchemy import String, create_engine
|
|
83
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, sessionmaker
|
|
84
|
+
|
|
85
|
+
from sqlphilosophy.sorting import ListQuery
|
|
86
|
+
from sqlphilosophy.sync.repository import BaseRepository
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Base(DeclarativeBase):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Widget(Base):
|
|
94
|
+
__tablename__ = "widget"
|
|
95
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
96
|
+
name: Mapped[str] = mapped_column(String(64))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
engine = create_engine("sqlite:///:memory:", future=True)
|
|
100
|
+
Base.metadata.create_all(engine)
|
|
101
|
+
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
|
|
102
|
+
|
|
103
|
+
with SessionLocal() as session:
|
|
104
|
+
repo = BaseRepository(Widget, session)
|
|
105
|
+
widget = repo.create(name="alpha") # stages + flush; does not commit
|
|
106
|
+
session.commit()
|
|
107
|
+
|
|
108
|
+
page = repo.statement().fetch_page(ListQuery.from_page(page=1, size=20))
|
|
109
|
+
assert page.total >= 1
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Async:** swap `Session` → `AsyncSession`, `BaseRepository` → `AsyncBaseRepository` from `sqlphilosophy.aio.repository`, and `await` repository methods.
|
|
113
|
+
|
|
114
|
+
## Package layout
|
|
115
|
+
|
|
116
|
+
| Module | Contents |
|
|
117
|
+
|--------|----------|
|
|
118
|
+
| `sqlphilosophy.types` | Portable typing aliases (`RowMapping`, `PrimaryKey`, `SqlFilter`, …) |
|
|
119
|
+
| `sqlphilosophy.sql` | Row mapping helpers, partial updates, Core table helpers, filter builders |
|
|
120
|
+
| `sqlphilosophy.sorting` | `ListQuery`, `SortConfig`, `SortSpec`, pagination/sort resolution |
|
|
121
|
+
| `sqlphilosophy.sync` | Sync `BaseRepository`, `StatementQueryBuilder`, `RepositoryFactory` protocol |
|
|
122
|
+
| `sqlphilosophy.aio` | Async `AsyncBaseRepository`, `AsyncStatementQueryBuilder`, `AsyncRepositoryFactory` |
|
|
123
|
+
| `sqlphilosophy.audit` | Optional SQLAlchemy audit listeners and timestamp mixins |
|
|
124
|
+
|
|
125
|
+
## Sync usage
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from sqlalchemy.orm import Session
|
|
129
|
+
|
|
130
|
+
from sqlphilosophy.sorting import ListQuery, SortConfig, SortSpec
|
|
131
|
+
from sqlphilosophy.sql import partial_update_model, row_int
|
|
132
|
+
from sqlphilosophy.sync.protocols import RepositoryFactory
|
|
133
|
+
from sqlphilosophy.sync.repository import BaseRepository
|
|
134
|
+
from sqlphilosophy.sync.query import SqlAlchemyStatementBuilder
|
|
135
|
+
|
|
136
|
+
repo = BaseRepository(User, session)
|
|
137
|
+
rows = repo.statement().where(User.active.is_(True)).mappings().all()
|
|
138
|
+
|
|
139
|
+
repo = BaseRepository(User, session, factory)
|
|
140
|
+
page = repo.statement().fetch_page(ListQuery.from_page(page=1, size=20))
|
|
141
|
+
other = repo.for_repo(OrderRepository)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Async usage
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
148
|
+
|
|
149
|
+
from sqlphilosophy.aio.repository import AsyncBaseRepository
|
|
150
|
+
|
|
151
|
+
repo = AsyncBaseRepository(User, session)
|
|
152
|
+
rows = await repo.statement().where(User.active.is_(True)).mappings().all()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Transaction ownership
|
|
156
|
+
|
|
157
|
+
- **`create` / `update` / `delete` helpers** on repositories call `session.flush()` but **do not commit** unless documented otherwise.
|
|
158
|
+
- **`delete_all()`** executes a bulk delete and **does not commit** — the caller owns `session.commit()` / `rollback()` for the work unit.
|
|
159
|
+
- **`batched_purge_ids(...)`** deletes matching rows in batches and **commits after each batch** — treat it as a destructive, application-level operation you must authorize first.
|
|
160
|
+
- Your application owns **`session.commit()` / `rollback()`** for normal request/work-unit boundaries.
|
|
161
|
+
|
|
162
|
+
## Raw SQL trust boundaries
|
|
163
|
+
|
|
164
|
+
The following must be **developer-defined** and must **never** be built from end-user input:
|
|
165
|
+
|
|
166
|
+
- Raw SQL fragments passed to SQL helper functions
|
|
167
|
+
- Literal column names, table names, and `ORDER BY` expressions
|
|
168
|
+
- Sort field allowlists wired into query builders
|
|
169
|
+
|
|
170
|
+
**User-supplied values must use bind parameters** (SQLAlchemy bound values), not string concatenation into SQL text or identifiers. See [SECURITY.md](./SECURITY.md).
|
|
171
|
+
|
|
172
|
+
## Destructive helpers
|
|
173
|
+
|
|
174
|
+
- **`delete_all()`** — removes all rows for the repository model (sync and async variants). Does **not** commit; caller must commit or roll back.
|
|
175
|
+
- **`batched_purge_ids(...)`** — deletes matching rows in batches and commits each batch.
|
|
176
|
+
|
|
177
|
+
Call only after your application has authorized the operation. These helpers assume the caller understands the data loss impact.
|
|
178
|
+
|
|
179
|
+
## Audit mixins
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from sqlphilosophy.audit.context import audit_context
|
|
183
|
+
from sqlphilosophy.audit.listener import configure_audit_listeners
|
|
184
|
+
from sqlphilosophy.audit.model import TimestampModel
|
|
185
|
+
|
|
186
|
+
configure_audit_listeners()
|
|
187
|
+
|
|
188
|
+
with audit_context(actor_id=42):
|
|
189
|
+
session.add(MyModel(name="example"))
|
|
190
|
+
session.flush()
|
|
191
|
+
session.commit()
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Audit listeners record changes; they do **not** enforce access control.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
This repo uses [uv](https://docs.astral.sh/uv/):
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
uv sync --extra dev
|
|
202
|
+
uv run pytest
|
|
203
|
+
uv run flake8 .
|
|
204
|
+
uv run python -m build
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Security
|
|
208
|
+
|
|
209
|
+
See [SECURITY.md](./SECURITY.md) for vulnerability reporting and SQL trust boundaries.
|
|
210
|
+
|
|
211
|
+
## Releasing
|
|
212
|
+
|
|
213
|
+
See [RELEASING.md](./RELEASING.md) for GitHub + PyPI trusted publishing. See [CHANGELOG.md](./CHANGELOG.md).
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# sqlphilosophy
|
|
2
|
+
|
|
3
|
+
Portable SQLAlchemy repository kit: sync and async CRUD, fluent statement builders, sort/pagination, Core SQL helpers, and optional audit listeners.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **PyPI** | [`sqlphilosophy`](https://pypi.org/project/sqlphilosophy/) |
|
|
8
|
+
| **GitHub** | [SignalSafeSoftware/sqlphilosophy](https://github.com/SignalSafeSoftware/sqlphilosophy) |
|
|
9
|
+
| **Import** | `sqlphilosophy` (explicit submodules — no root re-exports) |
|
|
10
|
+
| **Python** | 3.12+ |
|
|
11
|
+
| **License** | MIT — see [LICENSE](./LICENSE) |
|
|
12
|
+
|
|
13
|
+
## What this package does
|
|
14
|
+
|
|
15
|
+
- **Repository pattern** for a single mapped model (`BaseRepository`, `AsyncBaseRepository`).
|
|
16
|
+
- **Fluent query builders** with pagination/sort (`StatementQueryBuilder`, `ListQuery`, `SortConfig`).
|
|
17
|
+
- **SQL helpers** for row mapping, partial updates, filters, and developer-defined raw SQL fragments.
|
|
18
|
+
- **Optional audit listeners** and timestamp mixins.
|
|
19
|
+
|
|
20
|
+
## What this package does not do
|
|
21
|
+
|
|
22
|
+
- Migrations, schema design, or connection pooling configuration.
|
|
23
|
+
- Authorization, multi-tenant isolation, or query sandboxing.
|
|
24
|
+
- Automatic commits for normal CRUD — see [Transaction ownership](#transaction-ownership) below.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install sqlphilosophy
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Async ORM (`AsyncSession`) also needs greenlet:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install sqlphilosophy[async]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python 3.12+ and SQLAlchemy 2.x.
|
|
39
|
+
|
|
40
|
+
## Full example (sync model + session)
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from sqlalchemy import String, create_engine
|
|
44
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, sessionmaker
|
|
45
|
+
|
|
46
|
+
from sqlphilosophy.sorting import ListQuery
|
|
47
|
+
from sqlphilosophy.sync.repository import BaseRepository
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Base(DeclarativeBase):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Widget(Base):
|
|
55
|
+
__tablename__ = "widget"
|
|
56
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
57
|
+
name: Mapped[str] = mapped_column(String(64))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
engine = create_engine("sqlite:///:memory:", future=True)
|
|
61
|
+
Base.metadata.create_all(engine)
|
|
62
|
+
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
|
|
63
|
+
|
|
64
|
+
with SessionLocal() as session:
|
|
65
|
+
repo = BaseRepository(Widget, session)
|
|
66
|
+
widget = repo.create(name="alpha") # stages + flush; does not commit
|
|
67
|
+
session.commit()
|
|
68
|
+
|
|
69
|
+
page = repo.statement().fetch_page(ListQuery.from_page(page=1, size=20))
|
|
70
|
+
assert page.total >= 1
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Async:** swap `Session` → `AsyncSession`, `BaseRepository` → `AsyncBaseRepository` from `sqlphilosophy.aio.repository`, and `await` repository methods.
|
|
74
|
+
|
|
75
|
+
## Package layout
|
|
76
|
+
|
|
77
|
+
| Module | Contents |
|
|
78
|
+
|--------|----------|
|
|
79
|
+
| `sqlphilosophy.types` | Portable typing aliases (`RowMapping`, `PrimaryKey`, `SqlFilter`, …) |
|
|
80
|
+
| `sqlphilosophy.sql` | Row mapping helpers, partial updates, Core table helpers, filter builders |
|
|
81
|
+
| `sqlphilosophy.sorting` | `ListQuery`, `SortConfig`, `SortSpec`, pagination/sort resolution |
|
|
82
|
+
| `sqlphilosophy.sync` | Sync `BaseRepository`, `StatementQueryBuilder`, `RepositoryFactory` protocol |
|
|
83
|
+
| `sqlphilosophy.aio` | Async `AsyncBaseRepository`, `AsyncStatementQueryBuilder`, `AsyncRepositoryFactory` |
|
|
84
|
+
| `sqlphilosophy.audit` | Optional SQLAlchemy audit listeners and timestamp mixins |
|
|
85
|
+
|
|
86
|
+
## Sync usage
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from sqlalchemy.orm import Session
|
|
90
|
+
|
|
91
|
+
from sqlphilosophy.sorting import ListQuery, SortConfig, SortSpec
|
|
92
|
+
from sqlphilosophy.sql import partial_update_model, row_int
|
|
93
|
+
from sqlphilosophy.sync.protocols import RepositoryFactory
|
|
94
|
+
from sqlphilosophy.sync.repository import BaseRepository
|
|
95
|
+
from sqlphilosophy.sync.query import SqlAlchemyStatementBuilder
|
|
96
|
+
|
|
97
|
+
repo = BaseRepository(User, session)
|
|
98
|
+
rows = repo.statement().where(User.active.is_(True)).mappings().all()
|
|
99
|
+
|
|
100
|
+
repo = BaseRepository(User, session, factory)
|
|
101
|
+
page = repo.statement().fetch_page(ListQuery.from_page(page=1, size=20))
|
|
102
|
+
other = repo.for_repo(OrderRepository)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Async usage
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
109
|
+
|
|
110
|
+
from sqlphilosophy.aio.repository import AsyncBaseRepository
|
|
111
|
+
|
|
112
|
+
repo = AsyncBaseRepository(User, session)
|
|
113
|
+
rows = await repo.statement().where(User.active.is_(True)).mappings().all()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Transaction ownership
|
|
117
|
+
|
|
118
|
+
- **`create` / `update` / `delete` helpers** on repositories call `session.flush()` but **do not commit** unless documented otherwise.
|
|
119
|
+
- **`delete_all()`** executes a bulk delete and **does not commit** — the caller owns `session.commit()` / `rollback()` for the work unit.
|
|
120
|
+
- **`batched_purge_ids(...)`** deletes matching rows in batches and **commits after each batch** — treat it as a destructive, application-level operation you must authorize first.
|
|
121
|
+
- Your application owns **`session.commit()` / `rollback()`** for normal request/work-unit boundaries.
|
|
122
|
+
|
|
123
|
+
## Raw SQL trust boundaries
|
|
124
|
+
|
|
125
|
+
The following must be **developer-defined** and must **never** be built from end-user input:
|
|
126
|
+
|
|
127
|
+
- Raw SQL fragments passed to SQL helper functions
|
|
128
|
+
- Literal column names, table names, and `ORDER BY` expressions
|
|
129
|
+
- Sort field allowlists wired into query builders
|
|
130
|
+
|
|
131
|
+
**User-supplied values must use bind parameters** (SQLAlchemy bound values), not string concatenation into SQL text or identifiers. See [SECURITY.md](./SECURITY.md).
|
|
132
|
+
|
|
133
|
+
## Destructive helpers
|
|
134
|
+
|
|
135
|
+
- **`delete_all()`** — removes all rows for the repository model (sync and async variants). Does **not** commit; caller must commit or roll back.
|
|
136
|
+
- **`batched_purge_ids(...)`** — deletes matching rows in batches and commits each batch.
|
|
137
|
+
|
|
138
|
+
Call only after your application has authorized the operation. These helpers assume the caller understands the data loss impact.
|
|
139
|
+
|
|
140
|
+
## Audit mixins
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from sqlphilosophy.audit.context import audit_context
|
|
144
|
+
from sqlphilosophy.audit.listener import configure_audit_listeners
|
|
145
|
+
from sqlphilosophy.audit.model import TimestampModel
|
|
146
|
+
|
|
147
|
+
configure_audit_listeners()
|
|
148
|
+
|
|
149
|
+
with audit_context(actor_id=42):
|
|
150
|
+
session.add(MyModel(name="example"))
|
|
151
|
+
session.flush()
|
|
152
|
+
session.commit()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Audit listeners record changes; they do **not** enforce access control.
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
This repo uses [uv](https://docs.astral.sh/uv/):
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
uv sync --extra dev
|
|
163
|
+
uv run pytest
|
|
164
|
+
uv run flake8 .
|
|
165
|
+
uv run python -m build
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Security
|
|
169
|
+
|
|
170
|
+
See [SECURITY.md](./SECURITY.md) for vulnerability reporting and SQL trust boundaries.
|
|
171
|
+
|
|
172
|
+
## Releasing
|
|
173
|
+
|
|
174
|
+
See [RELEASING.md](./RELEASING.md) for GitHub + PyPI trusted publishing. See [CHANGELOG.md](./CHANGELOG.md).
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -34,11 +34,15 @@ async = [
|
|
|
34
34
|
dev = [
|
|
35
35
|
"pytest>=8",
|
|
36
36
|
"pytest-asyncio>=0.24",
|
|
37
|
-
"pytest-cov>=
|
|
37
|
+
"pytest-cov>=6",
|
|
38
38
|
"aiosqlite>=0.20",
|
|
39
39
|
"greenlet>=3.0",
|
|
40
40
|
"build>=1.2",
|
|
41
|
-
"twine>=
|
|
41
|
+
"twine>=5",
|
|
42
|
+
"flake8>=7",
|
|
43
|
+
"mypy>=1.11",
|
|
44
|
+
"bandit>=1.7",
|
|
45
|
+
"types-greenlet>=3.0",
|
|
42
46
|
]
|
|
43
47
|
|
|
44
48
|
[project.urls]
|
|
@@ -64,6 +68,7 @@ addopts = "--cov=sqlphilosophy --cov-report=term-missing --cov-fail-under=100"
|
|
|
64
68
|
|
|
65
69
|
[tool.coverage.run]
|
|
66
70
|
source = ["sqlphilosophy"]
|
|
71
|
+
omit = ["scripts/*"]
|
|
67
72
|
branch = false
|
|
68
73
|
|
|
69
74
|
[tool.coverage.report]
|
|
@@ -72,3 +77,11 @@ show_missing = true
|
|
|
72
77
|
exclude_also = [
|
|
73
78
|
"raise NotImplementedError",
|
|
74
79
|
]
|
|
80
|
+
|
|
81
|
+
[tool.mypy]
|
|
82
|
+
python_version = "3.12"
|
|
83
|
+
plugins = ["sqlalchemy.ext.mypy.plugin"]
|
|
84
|
+
warn_unused_ignores = true
|
|
85
|
+
|
|
86
|
+
[tool.uv]
|
|
87
|
+
package = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.1
|