muscles-data-sql 2.0.0__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.
- muscles_data_sql-2.0.0/PKG-INFO +223 -0
- muscles_data_sql-2.0.0/README.md +192 -0
- muscles_data_sql-2.0.0/pyproject.toml +52 -0
- muscles_data_sql-2.0.0/setup.cfg +4 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/__init__.py +114 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/cli.py +76 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/config.py +50 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/connections.py +105 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/data_adapter.py +71 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/drivers.py +52 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/engine.py +19 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/inspect.py +26 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/mapping.py +71 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/migration_lock.py +53 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/model_mapper.py +30 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/native.py +131 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/native_migrations.py +200 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/providers.py +30 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/query.py +33 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/repository.py +438 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/sqlalchemy_compat.py +18 -0
- muscles_data_sql-2.0.0/src/muscles/data/sql/uow.py +47 -0
- muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/PKG-INFO +223 -0
- muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/SOURCES.txt +41 -0
- muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/dependency_links.txt +1 -0
- muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/entry_points.txt +2 -0
- muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/requires.txt +17 -0
- muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/top_level.txt +1 -0
- muscles_data_sql-2.0.0/tests/test_cli.py +27 -0
- muscles_data_sql-2.0.0/tests/test_connections.py +89 -0
- muscles_data_sql-2.0.0/tests/test_data_policies.py +126 -0
- muscles_data_sql-2.0.0/tests/test_di_core_compat_smoke.py +17 -0
- muscles_data_sql-2.0.0/tests/test_distribution_name.py +9 -0
- muscles_data_sql-2.0.0/tests/test_driver_catalog.py +47 -0
- muscles_data_sql-2.0.0/tests/test_engine_and_inspect.py +12 -0
- muscles_data_sql-2.0.0/tests/test_lazy_imports.py +43 -0
- muscles_data_sql-2.0.0/tests/test_mapping.py +52 -0
- muscles_data_sql-2.0.0/tests/test_migration_lock.py +34 -0
- muscles_data_sql-2.0.0/tests/test_native_migrations.py +181 -0
- muscles_data_sql-2.0.0/tests/test_native_sql.py +302 -0
- muscles_data_sql-2.0.0/tests/test_package_contract.py +13 -0
- muscles_data_sql-2.0.0/tests/test_repository_and_uow.py +183 -0
- muscles_data_sql-2.0.0/tests/test_sqlalchemy_compat.py +28 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: muscles-data-sql
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Muscles SQL data layer for repositories, migrations and AI-first workflow.
|
|
5
|
+
Author-email: "Denis B." <denis@butko.info>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/butkoden/muscles-data-sql
|
|
8
|
+
Project-URL: Repository, https://github.com/butkoden/muscles-data-sql
|
|
9
|
+
Project-URL: Documentation, https://github.com/butkoden/muscles-data-sql#readme
|
|
10
|
+
Project-URL: Releases, https://github.com/butkoden/muscles-data-sql/releases
|
|
11
|
+
Project-URL: muscles, https://pypi.org/project/muscles/
|
|
12
|
+
Project-URL: muscles-documents, https://pypi.org/project/muscles-documents/
|
|
13
|
+
Project-URL: muscles-ai, https://pypi.org/project/muscles-ai/
|
|
14
|
+
Project-URL: muscles-otel, https://pypi.org/project/muscles-otel/
|
|
15
|
+
Project-URL: muscles-benchmarks, https://pypi.org/project/muscles-benchmarks/
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Provides-Extra: postgres
|
|
19
|
+
Requires-Dist: psycopg[binary]<4.0,>=3.1; extra == "postgres"
|
|
20
|
+
Provides-Extra: data
|
|
21
|
+
Requires-Dist: muscles-data<1.0.0,>=0.2.0; extra == "data"
|
|
22
|
+
Provides-Extra: legacy
|
|
23
|
+
Requires-Dist: muscles<2.0.0,>=1.0.0rc1; extra == "legacy"
|
|
24
|
+
Requires-Dist: muscles-data<1.0.0,>=0.2.0; extra == "legacy"
|
|
25
|
+
Requires-Dist: SQLAlchemy<3.0,>=2.0; extra == "legacy"
|
|
26
|
+
Requires-Dist: click<9.0,>=8.1; extra == "legacy"
|
|
27
|
+
Requires-Dist: pydantic<3.0,>=2.7; extra == "legacy"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: muscles-data-sql[legacy]; extra == "dev"
|
|
31
|
+
|
|
32
|
+
# Muscles Data SQL
|
|
33
|
+
|
|
34
|
+
Muscles SQL is a data layer package for SQL databases:
|
|
35
|
+
- model-to-table mapping
|
|
36
|
+
- engine and session management
|
|
37
|
+
- repository CRUD and advanced query API (filters/operators/joins/aggregates)
|
|
38
|
+
- transactions and Unit of Work (nested/savepoint/retry helpers)
|
|
39
|
+
- native SQL migrations through the shared `muscles-data` migration registry
|
|
40
|
+
- inspect/doctor support with machine-readable diagnostics
|
|
41
|
+
|
|
42
|
+
## Related Repositories
|
|
43
|
+
|
|
44
|
+
- [`muscles`](https://github.com/butkoden/muscles) - core schemas, actions, DI and canonical documentation.
|
|
45
|
+
- [`muscles-documents`](https://github.com/butkoden/muscles-documents) - document metadata/state can use SQL persistence in applications.
|
|
46
|
+
- [`muscles-ai`](https://github.com/butkoden/muscles-ai) - AI/RAG flows can use SQL-backed application state without making SQL generic storage.
|
|
47
|
+
- [`muscles-otel`](https://github.com/butkoden/muscles-otel) - observability hooks around SQL-backed flows.
|
|
48
|
+
- [`muscles-benchmarks`](https://github.com/butkoden/muscles-benchmarks) - SQL transaction and mapping regression checks.
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
This low-level API is useful for standalone scripts and libraries that explicitly
|
|
53
|
+
own their database lifetime. Application packages should use the shared
|
|
54
|
+
`app.resource` integration below instead of constructing `SqlDatabase` objects.
|
|
55
|
+
|
|
56
|
+
The lightweight SQL API has no mandatory third-party dependencies. SQLite uses
|
|
57
|
+
Python's standard library; PostgreSQL requires the `postgres` extra. The
|
|
58
|
+
named-resource integration requires `data`; SQLAlchemy repositories and CLI
|
|
59
|
+
require `legacy`. In this checkout install from the library
|
|
60
|
+
directory using `pip install -e '.[postgres]'`, `.[data]` or `.[legacy]` as
|
|
61
|
+
needed. Publishing the renamed distribution is a separate step.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from muscles.data.sql import SqlDatabase
|
|
65
|
+
|
|
66
|
+
db = SqlDatabase.sqlite("app.db")
|
|
67
|
+
try:
|
|
68
|
+
with db.transaction() as tx:
|
|
69
|
+
tx.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER, name TEXT)")
|
|
70
|
+
tx.execute("INSERT INTO users VALUES (?, ?)", (1, "Denis"))
|
|
71
|
+
users = tx.fetch_all("SELECT id, name FROM users WHERE id = ?", (1,))
|
|
72
|
+
finally:
|
|
73
|
+
db.close()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`SqlDatabase.postgres(dsn)` uses psycopg and its native parameter syntax. A
|
|
77
|
+
database owns one connection and is not a connection pool. Transactions from
|
|
78
|
+
different threads are serialized on that connection; nested transactions in the
|
|
79
|
+
same thread are rejected. Direct SQL does not enforce data policies;
|
|
80
|
+
the caller must ensure access restrictions. Commit occurs on successful exit,
|
|
81
|
+
and exceptions roll the transaction back.
|
|
82
|
+
|
|
83
|
+
## `muscles-data` Resource Integration
|
|
84
|
+
|
|
85
|
+
The application composition root owns the catalog and `DataRuntime`. Register
|
|
86
|
+
`NativeSqlFactory` there once, and load `data.resources` from the central
|
|
87
|
+
application configuration:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from muscles.data import DataConfig, DataRuntime
|
|
91
|
+
from muscles.data.catalog import DataAdapterCatalog
|
|
92
|
+
from muscles.data.sql.data_adapter import NativeSqlFactory
|
|
93
|
+
|
|
94
|
+
class App:
|
|
95
|
+
def __init__(self):
|
|
96
|
+
# self.config is the application's shared Muscles configuration.
|
|
97
|
+
catalog = DataAdapterCatalog.with_defaults()
|
|
98
|
+
catalog.register(NativeSqlFactory())
|
|
99
|
+
self.resource = DataRuntime(
|
|
100
|
+
config=DataConfig.from_raw({
|
|
101
|
+
"resources": self.config.get("data.resources").value() or {},
|
|
102
|
+
}),
|
|
103
|
+
catalog=catalog,
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The database resource itself is declared once in the application's central
|
|
108
|
+
configuration, for example with `type: native_sql`, `driver: postgres` and
|
|
109
|
+
`url_env: DATABASE_URL`. A package uses the existing resource and does not
|
|
110
|
+
create another catalog, runtime or connection configuration:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from muscles.data.ports import SqlTransactionPort
|
|
114
|
+
|
|
115
|
+
sql = app.resource.connect("database", SqlTransactionPort)
|
|
116
|
+
with sql.transaction() as tx:
|
|
117
|
+
tx.execute("CREATE TABLE IF NOT EXISTS audit (id INTEGER PRIMARY KEY)")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The adapter opens its native connection lazily on the first transaction and
|
|
121
|
+
closes it with the owning `DataRuntime`. Direct `SqlDatabase` examples above
|
|
122
|
+
are for standalone low-level use; application package code should resolve its
|
|
123
|
+
port through `app.resource`.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
python -m venv .venv
|
|
127
|
+
source .venv/bin/activate
|
|
128
|
+
pip install -e .
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For local development with the test and legacy dependencies:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip install -e ".[dev]"
|
|
135
|
+
pytest -q
|
|
136
|
+
muscles-data-sql doctor --url sqlite:///./app.db
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Named SQL Connections
|
|
140
|
+
|
|
141
|
+
`muscles-data-sql` can manage multiple SQL connections without becoming a generic
|
|
142
|
+
storage registry. The registry is SQL-only: it owns SQL connection configs,
|
|
143
|
+
lazy SQLAlchemy `EngineManager` instances, sessions, inspect and doctor reports.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from muscles.data.sql import SqlConnectionConfig, SqlConnectionRegistry
|
|
147
|
+
|
|
148
|
+
registry = SqlConnectionRegistry(
|
|
149
|
+
[
|
|
150
|
+
SqlConnectionConfig(name="default", url="sqlite:///./app.db"),
|
|
151
|
+
SqlConnectionConfig(name="analytics", url="sqlite:///./analytics.db", role="read"),
|
|
152
|
+
]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
session = registry.session("analytics")
|
|
156
|
+
report = registry.inspect("analytics")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
CLI diagnostics can read a JSON config:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"connections": {
|
|
164
|
+
"default": "sqlite:///./app.db",
|
|
165
|
+
"analytics": {"url": "sqlite:///./analytics.db", "role": "read"}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
muscles-data-sql inspect --config sql-connections.json --connection analytics
|
|
172
|
+
muscles-data-sql doctor --config sql-connections.json --all
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Diagnostic output uses safe URLs and does not print passwords from DSNs.
|
|
176
|
+
|
|
177
|
+
## Advanced Query Example
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from sqlalchemy import func
|
|
181
|
+
from muscles.data.sql import FilterClause, JoinClause, QuerySpec, SqlRepository
|
|
182
|
+
|
|
183
|
+
spec = QuerySpec(
|
|
184
|
+
filters=[FilterClause("status", "eq", "active")],
|
|
185
|
+
joins=[JoinClause(table=orders, on=users.c.id == orders.c.user_id)],
|
|
186
|
+
select_columns=[users.c.id, func.count(orders.c.id).label("orders_total")],
|
|
187
|
+
group_by=[users.c.id],
|
|
188
|
+
order_by=[users.c.id.asc()],
|
|
189
|
+
)
|
|
190
|
+
rows = SqlRepository(session, users).aggregate(spec)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Data operation policies
|
|
194
|
+
|
|
195
|
+
`SqlRepository` accepts the policy pipeline and a request-scoped context from
|
|
196
|
+
`muscles-data`. Policy predicates are combined with caller filters using AND;
|
|
197
|
+
they apply to reads, counts, updates, deletes, creates, bulk creates and upserts.
|
|
198
|
+
For update/delete, predicates are part of the SQL statement. Create and changed
|
|
199
|
+
values are checked before execution. Under an active policy pipeline, upsert
|
|
200
|
+
uses the constrained repository path instead of a native conflict statement.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from muscles.data import DataOperationContext
|
|
204
|
+
from muscles.data.sql import SqlRepository
|
|
205
|
+
|
|
206
|
+
repo = SqlRepository(
|
|
207
|
+
session,
|
|
208
|
+
documents,
|
|
209
|
+
policies=data_runtime.policies,
|
|
210
|
+
context=DataOperationContext(
|
|
211
|
+
subject_type="account",
|
|
212
|
+
subject_uid=account_uid,
|
|
213
|
+
attributes={"workspace_uid": workspace_uid},
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
rows = repo.find({"owner_uid": account_uid}, limit=50)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The policy contract is backend-neutral; each adapter is responsible for
|
|
220
|
+
translating predicates and enforcing them before pagination or mutation. Direct
|
|
221
|
+
SQLAlchemy session use is outside this enforcement boundary.
|
|
222
|
+
|
|
223
|
+
Repository: https://github.com/butkoden/muscles-data-sql
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Muscles Data SQL
|
|
2
|
+
|
|
3
|
+
Muscles SQL is a data layer package for SQL databases:
|
|
4
|
+
- model-to-table mapping
|
|
5
|
+
- engine and session management
|
|
6
|
+
- repository CRUD and advanced query API (filters/operators/joins/aggregates)
|
|
7
|
+
- transactions and Unit of Work (nested/savepoint/retry helpers)
|
|
8
|
+
- native SQL migrations through the shared `muscles-data` migration registry
|
|
9
|
+
- inspect/doctor support with machine-readable diagnostics
|
|
10
|
+
|
|
11
|
+
## Related Repositories
|
|
12
|
+
|
|
13
|
+
- [`muscles`](https://github.com/butkoden/muscles) - core schemas, actions, DI and canonical documentation.
|
|
14
|
+
- [`muscles-documents`](https://github.com/butkoden/muscles-documents) - document metadata/state can use SQL persistence in applications.
|
|
15
|
+
- [`muscles-ai`](https://github.com/butkoden/muscles-ai) - AI/RAG flows can use SQL-backed application state without making SQL generic storage.
|
|
16
|
+
- [`muscles-otel`](https://github.com/butkoden/muscles-otel) - observability hooks around SQL-backed flows.
|
|
17
|
+
- [`muscles-benchmarks`](https://github.com/butkoden/muscles-benchmarks) - SQL transaction and mapping regression checks.
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
This low-level API is useful for standalone scripts and libraries that explicitly
|
|
22
|
+
own their database lifetime. Application packages should use the shared
|
|
23
|
+
`app.resource` integration below instead of constructing `SqlDatabase` objects.
|
|
24
|
+
|
|
25
|
+
The lightweight SQL API has no mandatory third-party dependencies. SQLite uses
|
|
26
|
+
Python's standard library; PostgreSQL requires the `postgres` extra. The
|
|
27
|
+
named-resource integration requires `data`; SQLAlchemy repositories and CLI
|
|
28
|
+
require `legacy`. In this checkout install from the library
|
|
29
|
+
directory using `pip install -e '.[postgres]'`, `.[data]` or `.[legacy]` as
|
|
30
|
+
needed. Publishing the renamed distribution is a separate step.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from muscles.data.sql import SqlDatabase
|
|
34
|
+
|
|
35
|
+
db = SqlDatabase.sqlite("app.db")
|
|
36
|
+
try:
|
|
37
|
+
with db.transaction() as tx:
|
|
38
|
+
tx.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER, name TEXT)")
|
|
39
|
+
tx.execute("INSERT INTO users VALUES (?, ?)", (1, "Denis"))
|
|
40
|
+
users = tx.fetch_all("SELECT id, name FROM users WHERE id = ?", (1,))
|
|
41
|
+
finally:
|
|
42
|
+
db.close()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`SqlDatabase.postgres(dsn)` uses psycopg and its native parameter syntax. A
|
|
46
|
+
database owns one connection and is not a connection pool. Transactions from
|
|
47
|
+
different threads are serialized on that connection; nested transactions in the
|
|
48
|
+
same thread are rejected. Direct SQL does not enforce data policies;
|
|
49
|
+
the caller must ensure access restrictions. Commit occurs on successful exit,
|
|
50
|
+
and exceptions roll the transaction back.
|
|
51
|
+
|
|
52
|
+
## `muscles-data` Resource Integration
|
|
53
|
+
|
|
54
|
+
The application composition root owns the catalog and `DataRuntime`. Register
|
|
55
|
+
`NativeSqlFactory` there once, and load `data.resources` from the central
|
|
56
|
+
application configuration:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from muscles.data import DataConfig, DataRuntime
|
|
60
|
+
from muscles.data.catalog import DataAdapterCatalog
|
|
61
|
+
from muscles.data.sql.data_adapter import NativeSqlFactory
|
|
62
|
+
|
|
63
|
+
class App:
|
|
64
|
+
def __init__(self):
|
|
65
|
+
# self.config is the application's shared Muscles configuration.
|
|
66
|
+
catalog = DataAdapterCatalog.with_defaults()
|
|
67
|
+
catalog.register(NativeSqlFactory())
|
|
68
|
+
self.resource = DataRuntime(
|
|
69
|
+
config=DataConfig.from_raw({
|
|
70
|
+
"resources": self.config.get("data.resources").value() or {},
|
|
71
|
+
}),
|
|
72
|
+
catalog=catalog,
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The database resource itself is declared once in the application's central
|
|
77
|
+
configuration, for example with `type: native_sql`, `driver: postgres` and
|
|
78
|
+
`url_env: DATABASE_URL`. A package uses the existing resource and does not
|
|
79
|
+
create another catalog, runtime or connection configuration:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from muscles.data.ports import SqlTransactionPort
|
|
83
|
+
|
|
84
|
+
sql = app.resource.connect("database", SqlTransactionPort)
|
|
85
|
+
with sql.transaction() as tx:
|
|
86
|
+
tx.execute("CREATE TABLE IF NOT EXISTS audit (id INTEGER PRIMARY KEY)")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The adapter opens its native connection lazily on the first transaction and
|
|
90
|
+
closes it with the owning `DataRuntime`. Direct `SqlDatabase` examples above
|
|
91
|
+
are for standalone low-level use; application package code should resolve its
|
|
92
|
+
port through `app.resource`.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python -m venv .venv
|
|
96
|
+
source .venv/bin/activate
|
|
97
|
+
pip install -e .
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For local development with the test and legacy dependencies:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install -e ".[dev]"
|
|
104
|
+
pytest -q
|
|
105
|
+
muscles-data-sql doctor --url sqlite:///./app.db
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Named SQL Connections
|
|
109
|
+
|
|
110
|
+
`muscles-data-sql` can manage multiple SQL connections without becoming a generic
|
|
111
|
+
storage registry. The registry is SQL-only: it owns SQL connection configs,
|
|
112
|
+
lazy SQLAlchemy `EngineManager` instances, sessions, inspect and doctor reports.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from muscles.data.sql import SqlConnectionConfig, SqlConnectionRegistry
|
|
116
|
+
|
|
117
|
+
registry = SqlConnectionRegistry(
|
|
118
|
+
[
|
|
119
|
+
SqlConnectionConfig(name="default", url="sqlite:///./app.db"),
|
|
120
|
+
SqlConnectionConfig(name="analytics", url="sqlite:///./analytics.db", role="read"),
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
session = registry.session("analytics")
|
|
125
|
+
report = registry.inspect("analytics")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
CLI diagnostics can read a JSON config:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"connections": {
|
|
133
|
+
"default": "sqlite:///./app.db",
|
|
134
|
+
"analytics": {"url": "sqlite:///./analytics.db", "role": "read"}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
muscles-data-sql inspect --config sql-connections.json --connection analytics
|
|
141
|
+
muscles-data-sql doctor --config sql-connections.json --all
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Diagnostic output uses safe URLs and does not print passwords from DSNs.
|
|
145
|
+
|
|
146
|
+
## Advanced Query Example
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from sqlalchemy import func
|
|
150
|
+
from muscles.data.sql import FilterClause, JoinClause, QuerySpec, SqlRepository
|
|
151
|
+
|
|
152
|
+
spec = QuerySpec(
|
|
153
|
+
filters=[FilterClause("status", "eq", "active")],
|
|
154
|
+
joins=[JoinClause(table=orders, on=users.c.id == orders.c.user_id)],
|
|
155
|
+
select_columns=[users.c.id, func.count(orders.c.id).label("orders_total")],
|
|
156
|
+
group_by=[users.c.id],
|
|
157
|
+
order_by=[users.c.id.asc()],
|
|
158
|
+
)
|
|
159
|
+
rows = SqlRepository(session, users).aggregate(spec)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Data operation policies
|
|
163
|
+
|
|
164
|
+
`SqlRepository` accepts the policy pipeline and a request-scoped context from
|
|
165
|
+
`muscles-data`. Policy predicates are combined with caller filters using AND;
|
|
166
|
+
they apply to reads, counts, updates, deletes, creates, bulk creates and upserts.
|
|
167
|
+
For update/delete, predicates are part of the SQL statement. Create and changed
|
|
168
|
+
values are checked before execution. Under an active policy pipeline, upsert
|
|
169
|
+
uses the constrained repository path instead of a native conflict statement.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from muscles.data import DataOperationContext
|
|
173
|
+
from muscles.data.sql import SqlRepository
|
|
174
|
+
|
|
175
|
+
repo = SqlRepository(
|
|
176
|
+
session,
|
|
177
|
+
documents,
|
|
178
|
+
policies=data_runtime.policies,
|
|
179
|
+
context=DataOperationContext(
|
|
180
|
+
subject_type="account",
|
|
181
|
+
subject_uid=account_uid,
|
|
182
|
+
attributes={"workspace_uid": workspace_uid},
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
rows = repo.find({"owner_uid": account_uid}, limit=50)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The policy contract is backend-neutral; each adapter is responsible for
|
|
189
|
+
translating predicates and enforcing them before pagination or mutation. Direct
|
|
190
|
+
SQLAlchemy session use is outside this enforcement boundary.
|
|
191
|
+
|
|
192
|
+
Repository: https://github.com/butkoden/muscles-data-sql
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "muscles-data-sql"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Muscles SQL data layer for repositories, migrations and AI-first workflow."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Denis B.", email = "denis@butko.info" }]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
postgres = ["psycopg[binary]>=3.1,<4.0"]
|
|
17
|
+
data = ["muscles-data>=0.2.0,<1.0.0"]
|
|
18
|
+
legacy = [
|
|
19
|
+
"muscles>=1.0.0rc1,<2.0.0",
|
|
20
|
+
"muscles-data>=0.2.0,<1.0.0",
|
|
21
|
+
"SQLAlchemy>=2.0,<3.0",
|
|
22
|
+
"click>=8.1,<9.0",
|
|
23
|
+
"pydantic>=2.7,<3.0",
|
|
24
|
+
]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.0,<9.0",
|
|
27
|
+
"muscles-data-sql[legacy]",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/butkoden/muscles-data-sql"
|
|
32
|
+
Repository = "https://github.com/butkoden/muscles-data-sql"
|
|
33
|
+
Documentation = "https://github.com/butkoden/muscles-data-sql#readme"
|
|
34
|
+
Releases = "https://github.com/butkoden/muscles-data-sql/releases"
|
|
35
|
+
"muscles" = "https://pypi.org/project/muscles/"
|
|
36
|
+
"muscles-documents" = "https://pypi.org/project/muscles-documents/"
|
|
37
|
+
"muscles-ai" = "https://pypi.org/project/muscles-ai/"
|
|
38
|
+
"muscles-otel" = "https://pypi.org/project/muscles-otel/"
|
|
39
|
+
"muscles-benchmarks" = "https://pypi.org/project/muscles-benchmarks/"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
muscles-data-sql = "muscles.data.sql.cli:main"
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools]
|
|
48
|
+
package-dir = {"" = "src"}
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.packages.find]
|
|
51
|
+
where = ["src"]
|
|
52
|
+
include = ["muscles.data.sql*"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .native_migrations import NativeSqlMigration, NativeSqlMigrationBackend, NativeSqlMigrationSourceBackend
|
|
7
|
+
from .drivers import SqlDriverCatalog
|
|
8
|
+
from .native import SqlDatabase, SqlTransaction
|
|
9
|
+
from .model_mapper import ModelMapper
|
|
10
|
+
from .config import DatabaseConfig
|
|
11
|
+
from .config import SqlConnectionConfig
|
|
12
|
+
from .connections import SqlConnectionRegistry, UnknownSqlConnection
|
|
13
|
+
from .engine import EngineManager
|
|
14
|
+
from .mapping import map_model
|
|
15
|
+
from .providers import SqlResourceGeneratorProvider
|
|
16
|
+
from .query import FilterClause, JoinClause, QuerySpec
|
|
17
|
+
from .repository import SqlRepository
|
|
18
|
+
from .uow import UnitOfWork
|
|
19
|
+
from .sqlalchemy_compat import text
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"NativeSqlMigration",
|
|
23
|
+
"NativeSqlMigrationBackend",
|
|
24
|
+
"NativeSqlMigrationSourceBackend",
|
|
25
|
+
"SqlDriverCatalog",
|
|
26
|
+
"SqlDatabase",
|
|
27
|
+
"SqlTransaction",
|
|
28
|
+
"ModelMapper",
|
|
29
|
+
"DatabaseConfig",
|
|
30
|
+
"SqlConnectionConfig",
|
|
31
|
+
"SqlConnectionRegistry",
|
|
32
|
+
"UnknownSqlConnection",
|
|
33
|
+
"EngineManager",
|
|
34
|
+
"map_model",
|
|
35
|
+
"SqlRepository",
|
|
36
|
+
"UnitOfWork",
|
|
37
|
+
"text",
|
|
38
|
+
"QuerySpec",
|
|
39
|
+
"FilterClause",
|
|
40
|
+
"JoinClause",
|
|
41
|
+
"SqlResourceGeneratorProvider",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __getattr__(name: str):
|
|
46
|
+
if name in {"NativeSqlMigration", "NativeSqlMigrationBackend", "NativeSqlMigrationSourceBackend"}:
|
|
47
|
+
from . import native_migrations
|
|
48
|
+
|
|
49
|
+
return getattr(native_migrations, name)
|
|
50
|
+
if name == "SqlDriverCatalog":
|
|
51
|
+
from .drivers import SqlDriverCatalog
|
|
52
|
+
|
|
53
|
+
return SqlDriverCatalog
|
|
54
|
+
if name == "ModelMapper":
|
|
55
|
+
from .model_mapper import ModelMapper
|
|
56
|
+
|
|
57
|
+
return ModelMapper
|
|
58
|
+
if name in {"SqlDatabase", "SqlTransaction"}:
|
|
59
|
+
from .native import SqlDatabase, SqlTransaction
|
|
60
|
+
|
|
61
|
+
return {"SqlDatabase": SqlDatabase, "SqlTransaction": SqlTransaction}[name]
|
|
62
|
+
if name == "DatabaseConfig":
|
|
63
|
+
from .config import DatabaseConfig
|
|
64
|
+
|
|
65
|
+
return DatabaseConfig
|
|
66
|
+
if name == "SqlConnectionConfig":
|
|
67
|
+
from .config import SqlConnectionConfig
|
|
68
|
+
|
|
69
|
+
return SqlConnectionConfig
|
|
70
|
+
if name == "SqlConnectionRegistry":
|
|
71
|
+
from .connections import SqlConnectionRegistry
|
|
72
|
+
|
|
73
|
+
return SqlConnectionRegistry
|
|
74
|
+
if name == "UnknownSqlConnection":
|
|
75
|
+
from .connections import UnknownSqlConnection
|
|
76
|
+
|
|
77
|
+
return UnknownSqlConnection
|
|
78
|
+
if name == "EngineManager":
|
|
79
|
+
from .engine import EngineManager
|
|
80
|
+
|
|
81
|
+
return EngineManager
|
|
82
|
+
if name == "map_model":
|
|
83
|
+
from .mapping import map_model
|
|
84
|
+
|
|
85
|
+
return map_model
|
|
86
|
+
if name == "SqlRepository":
|
|
87
|
+
from .repository import SqlRepository
|
|
88
|
+
|
|
89
|
+
return SqlRepository
|
|
90
|
+
if name == "UnitOfWork":
|
|
91
|
+
from .uow import UnitOfWork
|
|
92
|
+
|
|
93
|
+
return UnitOfWork
|
|
94
|
+
if name == "text":
|
|
95
|
+
from .sqlalchemy_compat import text
|
|
96
|
+
|
|
97
|
+
return text
|
|
98
|
+
if name == "QuerySpec":
|
|
99
|
+
from .query import QuerySpec
|
|
100
|
+
|
|
101
|
+
return QuerySpec
|
|
102
|
+
if name == "FilterClause":
|
|
103
|
+
from .query import FilterClause
|
|
104
|
+
|
|
105
|
+
return FilterClause
|
|
106
|
+
if name == "JoinClause":
|
|
107
|
+
from .query import JoinClause
|
|
108
|
+
|
|
109
|
+
return JoinClause
|
|
110
|
+
if name == "SqlResourceGeneratorProvider":
|
|
111
|
+
from .providers import SqlResourceGeneratorProvider
|
|
112
|
+
|
|
113
|
+
return SqlResourceGeneratorProvider
|
|
114
|
+
raise AttributeError(name)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from .config import DatabaseConfig
|
|
7
|
+
from .connections import SqlConnectionRegistry, load_connection_registry
|
|
8
|
+
from .providers import SqlGenerationRequest, SqlResourceGeneratorProvider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def main():
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _registry_from_options(url: str | None, config_path: str | None) -> SqlConnectionRegistry:
|
|
17
|
+
if config_path:
|
|
18
|
+
return load_connection_registry(config_path)
|
|
19
|
+
if url:
|
|
20
|
+
return SqlConnectionRegistry.from_database_config(DatabaseConfig(url=url))
|
|
21
|
+
raise click.UsageError("Use --url or --config.")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@main.command("doctor")
|
|
25
|
+
@click.option("--url", required=False)
|
|
26
|
+
@click.option("--config", "config_path", type=click.Path(exists=True, dir_okay=False), required=False)
|
|
27
|
+
@click.option("--connection", default="default", show_default=True)
|
|
28
|
+
@click.option("--all", "all_connections", is_flag=True, default=False)
|
|
29
|
+
def doctor(url: str | None, config_path: str | None, connection: str, all_connections: bool):
|
|
30
|
+
registry = _registry_from_options(url, config_path)
|
|
31
|
+
report = registry.inspect_all() if all_connections else registry.inspect(connection)
|
|
32
|
+
click.echo(json.dumps(report, ensure_ascii=False))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@main.command("inspect")
|
|
36
|
+
@click.option("--url", required=False)
|
|
37
|
+
@click.option("--config", "config_path", type=click.Path(exists=True, dir_okay=False), required=False)
|
|
38
|
+
@click.option("--connection", default="default", show_default=True)
|
|
39
|
+
def inspect_cmd(url: str | None, config_path: str | None, connection: str):
|
|
40
|
+
registry = _registry_from_options(url, config_path)
|
|
41
|
+
click.echo(json.dumps(registry.inspect(connection), ensure_ascii=False))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@main.group("generate")
|
|
45
|
+
def generate():
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@generate.command("resource")
|
|
50
|
+
@click.argument("name")
|
|
51
|
+
@click.option("--sql", is_flag=True, default=False)
|
|
52
|
+
def generate_resource(name: str, sql: bool):
|
|
53
|
+
if sql:
|
|
54
|
+
provider = SqlResourceGeneratorProvider()
|
|
55
|
+
generated = provider.generate(
|
|
56
|
+
project_root=Path.cwd(),
|
|
57
|
+
request=SqlGenerationRequest(
|
|
58
|
+
generator_type="sql-resource",
|
|
59
|
+
name=name,
|
|
60
|
+
force=False,
|
|
61
|
+
with_tests=False,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
click.echo(generated[0])
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
root = Path("generated")
|
|
68
|
+
root.mkdir(exist_ok=True)
|
|
69
|
+
target = root / f"{name}.py"
|
|
70
|
+
content = f"class {name.title().replace('_', '')}Resource:\n sql_enabled = {sql}\n"
|
|
71
|
+
target.write_text(content, encoding="utf-8")
|
|
72
|
+
click.echo(str(target))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|