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.
Files changed (43) hide show
  1. muscles_data_sql-2.0.0/PKG-INFO +223 -0
  2. muscles_data_sql-2.0.0/README.md +192 -0
  3. muscles_data_sql-2.0.0/pyproject.toml +52 -0
  4. muscles_data_sql-2.0.0/setup.cfg +4 -0
  5. muscles_data_sql-2.0.0/src/muscles/data/sql/__init__.py +114 -0
  6. muscles_data_sql-2.0.0/src/muscles/data/sql/cli.py +76 -0
  7. muscles_data_sql-2.0.0/src/muscles/data/sql/config.py +50 -0
  8. muscles_data_sql-2.0.0/src/muscles/data/sql/connections.py +105 -0
  9. muscles_data_sql-2.0.0/src/muscles/data/sql/data_adapter.py +71 -0
  10. muscles_data_sql-2.0.0/src/muscles/data/sql/drivers.py +52 -0
  11. muscles_data_sql-2.0.0/src/muscles/data/sql/engine.py +19 -0
  12. muscles_data_sql-2.0.0/src/muscles/data/sql/inspect.py +26 -0
  13. muscles_data_sql-2.0.0/src/muscles/data/sql/mapping.py +71 -0
  14. muscles_data_sql-2.0.0/src/muscles/data/sql/migration_lock.py +53 -0
  15. muscles_data_sql-2.0.0/src/muscles/data/sql/model_mapper.py +30 -0
  16. muscles_data_sql-2.0.0/src/muscles/data/sql/native.py +131 -0
  17. muscles_data_sql-2.0.0/src/muscles/data/sql/native_migrations.py +200 -0
  18. muscles_data_sql-2.0.0/src/muscles/data/sql/providers.py +30 -0
  19. muscles_data_sql-2.0.0/src/muscles/data/sql/query.py +33 -0
  20. muscles_data_sql-2.0.0/src/muscles/data/sql/repository.py +438 -0
  21. muscles_data_sql-2.0.0/src/muscles/data/sql/sqlalchemy_compat.py +18 -0
  22. muscles_data_sql-2.0.0/src/muscles/data/sql/uow.py +47 -0
  23. muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/PKG-INFO +223 -0
  24. muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/SOURCES.txt +41 -0
  25. muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/dependency_links.txt +1 -0
  26. muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/entry_points.txt +2 -0
  27. muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/requires.txt +17 -0
  28. muscles_data_sql-2.0.0/src/muscles_data_sql.egg-info/top_level.txt +1 -0
  29. muscles_data_sql-2.0.0/tests/test_cli.py +27 -0
  30. muscles_data_sql-2.0.0/tests/test_connections.py +89 -0
  31. muscles_data_sql-2.0.0/tests/test_data_policies.py +126 -0
  32. muscles_data_sql-2.0.0/tests/test_di_core_compat_smoke.py +17 -0
  33. muscles_data_sql-2.0.0/tests/test_distribution_name.py +9 -0
  34. muscles_data_sql-2.0.0/tests/test_driver_catalog.py +47 -0
  35. muscles_data_sql-2.0.0/tests/test_engine_and_inspect.py +12 -0
  36. muscles_data_sql-2.0.0/tests/test_lazy_imports.py +43 -0
  37. muscles_data_sql-2.0.0/tests/test_mapping.py +52 -0
  38. muscles_data_sql-2.0.0/tests/test_migration_lock.py +34 -0
  39. muscles_data_sql-2.0.0/tests/test_native_migrations.py +181 -0
  40. muscles_data_sql-2.0.0/tests/test_native_sql.py +302 -0
  41. muscles_data_sql-2.0.0/tests/test_package_contract.py +13 -0
  42. muscles_data_sql-2.0.0/tests/test_repository_and_uow.py +183 -0
  43. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()