sqlalchemy-load 0.2.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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(tree:*)",
5
+ "Bash(uv run python:*)",
6
+ "Bash(uv run pytest:*)",
7
+ "WebSearch"
8
+ ]
9
+ }
10
+ }
@@ -0,0 +1,51 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+ env/
28
+
29
+ # Testing
30
+ .pytest_cache/
31
+ .coverage
32
+ htmlcov/
33
+ .tox/
34
+ .nox/
35
+
36
+ # IDE
37
+ .idea/
38
+ .vscode/
39
+ *.swp
40
+ *.swo
41
+ *~
42
+
43
+ # Distribution
44
+ *.manifest
45
+ *.spec
46
+
47
+ # mypy
48
+ .mypy_cache/
49
+
50
+ # ruff
51
+ .ruff_cache/
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlalchemy-load
3
+ Version: 0.2.0
4
+ Summary: Generate SQLAlchemy query optimization options from simplified field selection syntax
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: sqlalchemy>=2.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
10
+ Requires-Dist: pytest>=7.0; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # SQLAlchemy Load Generator
14
+
15
+ Generate SQLAlchemy query optimization options (`selectinload` + `load_only`) from simplified field selection syntax.
16
+
17
+ ## Why This Library?
18
+
19
+ SQLAlchemy's query options (`selectinload`, `joinedload`, `load_only`) are powerful but **painful to write**, especially with nested relationships.
20
+
21
+ ### The Problem
22
+
23
+ **1. Verbose nested syntax**
24
+
25
+ ```python
26
+ # Loading User -> Posts -> Comments requires deep nesting
27
+ stmt = select(User).options(
28
+ selectinload(User.posts).options(
29
+ load_only(Post.id, Post.title),
30
+ selectinload(Post.comments).options(
31
+ load_only(Comment.id, Comment.content)
32
+ )
33
+ )
34
+ )
35
+ ```
36
+
37
+ **2. Coupled with query logic**
38
+
39
+ You must decide what to load at query time, mixing data requirements with query construction. Different API endpoints need different loading strategies, leading to duplicated query code.
40
+
41
+ **3. Dynamic composition is awkward**
42
+
43
+ ```python
44
+ # Conditionally adding options requires extra logic
45
+ options = []
46
+ if need_posts:
47
+ options.append(selectinload(User.posts))
48
+ if need_comments:
49
+ options.append(selectinload(User.posts).selectinload(Post.comments))
50
+ stmt = select(User).options(*options)
51
+ ```
52
+
53
+ **4. Easy to cause N+1 or over-fetching**
54
+
55
+ - Forget `selectinload` → N+1 queries
56
+ - Load unnecessary fields → wasted memory
57
+
58
+ ### The Solution
59
+
60
+ This library provides a **declarative syntax** similar to GraphQL:
61
+
62
+ ```python
63
+ # Before: verbose, nested, error-prone
64
+ stmt = select(User).options(
65
+ selectinload(User.posts).options(
66
+ load_only(Post.id, Post.title),
67
+ selectinload(Post.comments).options(
68
+ load_only(Comment.id, Comment.content)
69
+ )
70
+ )
71
+ )
72
+
73
+ # After: clean, declarative, optimized
74
+ generator = LoadGenerator(Base)
75
+ options = generator.generate(User, "{ id name posts { title comments { content } } }")
76
+ stmt = select(User).options(*options)
77
+ ```
78
+
79
+ ## Installation
80
+
81
+ ```bash
82
+ pip install sqlalchemy-load
83
+ ```
84
+
85
+ ## Usage
86
+
87
+ ```python
88
+ from sqlalchemy import select
89
+ from sqlalchemy.orm import DeclarativeBase
90
+ from sqlalchemy_load import LoadGenerator
91
+
92
+ # Initialize with your DeclarativeBase
93
+ generator = LoadGenerator(Base)
94
+
95
+ # Generate options using simplified syntax
96
+ options = generator.generate(User, "{ id name posts { title comments { content } } }")
97
+
98
+ # Use with SQLAlchemy query
99
+ stmt = select(User).options(*options)
100
+ ```
101
+
102
+ ## Syntax
103
+
104
+ The simplified syntax is similar to GraphQL but without commas:
105
+
106
+ ```
107
+ { field1 field2 relationship { nested_field } }
108
+ ```
109
+
110
+ - Fields are space-separated
111
+ - Relationships use `{ }` for nested selection
112
+ - Commas are optional and ignored
113
+
114
+ ### Examples
115
+
116
+ ```python
117
+ # Simple fields
118
+ generator.generate(User, "{ id name email }")
119
+
120
+ # Nested relationships
121
+ generator.generate(User, "{ id posts { title content } }")
122
+
123
+ # Deeply nested
124
+ generator.generate(User, "{ id posts { title comments { content author } } }")
125
+
126
+ # Multiple relationships
127
+ generator.generate(User, "{ name posts { title } profile { bio } }")
128
+
129
+ # Different models with same generator
130
+ generator.generate(Post, "{ title content author { name } }")
131
+ generator.generate(Comment, "{ content post { title } }")
132
+ ```
133
+
134
+ ## API
135
+
136
+ ### `LoadGenerator(base_class)`
137
+
138
+ Create a generator with a SQLAlchemy `DeclarativeBase`. Preloads metadata for all models in the registry for optimal performance.
139
+
140
+ ```python
141
+ from sqlalchemy.orm import DeclarativeBase
142
+
143
+ class Base(DeclarativeBase):
144
+ pass
145
+
146
+ generator = LoadGenerator(Base)
147
+ ```
148
+
149
+ ### `generator.generate(model_class, query_string) -> list`
150
+
151
+ Generate SQLAlchemy options from a query string.
152
+
153
+ ```python
154
+ options = generator.generate(User, "{ id name posts { title } }")
155
+ stmt = select(User).options(*options)
156
+ ```
157
+
158
+ ## Features
159
+
160
+ - **Preloaded metadata**: All model metadata is cached at initialization for fast lookups
161
+ - **Result caching**: Same query returns cached result, avoiding redundant computation
162
+ - **Parse caching**: Query string parsing is cached with `lru_cache`
163
+ - **Automatic primary key inclusion**: Primary keys are always included in `load_only`
164
+ - **Relationship detection**: Automatically detects SQLAlchemy relationships
165
+ - **Nested loading**: Recursively generates `selectinload` with nested `load_only`
166
+ - **Error handling**: Clear errors for invalid fields, relationships, or syntax
167
+
168
+ ## Error Handling
169
+
170
+ ```python
171
+ from sqlalchemy_load import (
172
+ LoadGenerator,
173
+ ParseError,
174
+ FieldNotFoundError,
175
+ RelationshipNotFoundError,
176
+ )
177
+
178
+ generator = LoadGenerator(Base)
179
+
180
+ # Syntax error
181
+ try:
182
+ generator.generate(User, "{ id name") # Missing closing brace
183
+ except ParseError as e:
184
+ print(f"Syntax error: {e}")
185
+
186
+ # Field doesn't exist
187
+ try:
188
+ generator.generate(User, "{ nonexistent }")
189
+ except FieldNotFoundError as e:
190
+ print(f"Field not found: {e}")
191
+
192
+ # Relationship doesn't exist
193
+ try:
194
+ generator.generate(User, "{ notarelationship { id } }")
195
+ except RelationshipNotFoundError as e:
196
+ print(f"Relationship not found: {e}")
197
+ ```
198
+
199
+ ## Requirements
200
+
201
+ - Python >= 3.10
202
+ - SQLAlchemy >= 2.0
203
+
204
+ ## License
205
+
206
+ MIT
@@ -0,0 +1,194 @@
1
+ # SQLAlchemy Load Generator
2
+
3
+ Generate SQLAlchemy query optimization options (`selectinload` + `load_only`) from simplified field selection syntax.
4
+
5
+ ## Why This Library?
6
+
7
+ SQLAlchemy's query options (`selectinload`, `joinedload`, `load_only`) are powerful but **painful to write**, especially with nested relationships.
8
+
9
+ ### The Problem
10
+
11
+ **1. Verbose nested syntax**
12
+
13
+ ```python
14
+ # Loading User -> Posts -> Comments requires deep nesting
15
+ stmt = select(User).options(
16
+ selectinload(User.posts).options(
17
+ load_only(Post.id, Post.title),
18
+ selectinload(Post.comments).options(
19
+ load_only(Comment.id, Comment.content)
20
+ )
21
+ )
22
+ )
23
+ ```
24
+
25
+ **2. Coupled with query logic**
26
+
27
+ You must decide what to load at query time, mixing data requirements with query construction. Different API endpoints need different loading strategies, leading to duplicated query code.
28
+
29
+ **3. Dynamic composition is awkward**
30
+
31
+ ```python
32
+ # Conditionally adding options requires extra logic
33
+ options = []
34
+ if need_posts:
35
+ options.append(selectinload(User.posts))
36
+ if need_comments:
37
+ options.append(selectinload(User.posts).selectinload(Post.comments))
38
+ stmt = select(User).options(*options)
39
+ ```
40
+
41
+ **4. Easy to cause N+1 or over-fetching**
42
+
43
+ - Forget `selectinload` → N+1 queries
44
+ - Load unnecessary fields → wasted memory
45
+
46
+ ### The Solution
47
+
48
+ This library provides a **declarative syntax** similar to GraphQL:
49
+
50
+ ```python
51
+ # Before: verbose, nested, error-prone
52
+ stmt = select(User).options(
53
+ selectinload(User.posts).options(
54
+ load_only(Post.id, Post.title),
55
+ selectinload(Post.comments).options(
56
+ load_only(Comment.id, Comment.content)
57
+ )
58
+ )
59
+ )
60
+
61
+ # After: clean, declarative, optimized
62
+ generator = LoadGenerator(Base)
63
+ options = generator.generate(User, "{ id name posts { title comments { content } } }")
64
+ stmt = select(User).options(*options)
65
+ ```
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ pip install sqlalchemy-load
71
+ ```
72
+
73
+ ## Usage
74
+
75
+ ```python
76
+ from sqlalchemy import select
77
+ from sqlalchemy.orm import DeclarativeBase
78
+ from sqlalchemy_load import LoadGenerator
79
+
80
+ # Initialize with your DeclarativeBase
81
+ generator = LoadGenerator(Base)
82
+
83
+ # Generate options using simplified syntax
84
+ options = generator.generate(User, "{ id name posts { title comments { content } } }")
85
+
86
+ # Use with SQLAlchemy query
87
+ stmt = select(User).options(*options)
88
+ ```
89
+
90
+ ## Syntax
91
+
92
+ The simplified syntax is similar to GraphQL but without commas:
93
+
94
+ ```
95
+ { field1 field2 relationship { nested_field } }
96
+ ```
97
+
98
+ - Fields are space-separated
99
+ - Relationships use `{ }` for nested selection
100
+ - Commas are optional and ignored
101
+
102
+ ### Examples
103
+
104
+ ```python
105
+ # Simple fields
106
+ generator.generate(User, "{ id name email }")
107
+
108
+ # Nested relationships
109
+ generator.generate(User, "{ id posts { title content } }")
110
+
111
+ # Deeply nested
112
+ generator.generate(User, "{ id posts { title comments { content author } } }")
113
+
114
+ # Multiple relationships
115
+ generator.generate(User, "{ name posts { title } profile { bio } }")
116
+
117
+ # Different models with same generator
118
+ generator.generate(Post, "{ title content author { name } }")
119
+ generator.generate(Comment, "{ content post { title } }")
120
+ ```
121
+
122
+ ## API
123
+
124
+ ### `LoadGenerator(base_class)`
125
+
126
+ Create a generator with a SQLAlchemy `DeclarativeBase`. Preloads metadata for all models in the registry for optimal performance.
127
+
128
+ ```python
129
+ from sqlalchemy.orm import DeclarativeBase
130
+
131
+ class Base(DeclarativeBase):
132
+ pass
133
+
134
+ generator = LoadGenerator(Base)
135
+ ```
136
+
137
+ ### `generator.generate(model_class, query_string) -> list`
138
+
139
+ Generate SQLAlchemy options from a query string.
140
+
141
+ ```python
142
+ options = generator.generate(User, "{ id name posts { title } }")
143
+ stmt = select(User).options(*options)
144
+ ```
145
+
146
+ ## Features
147
+
148
+ - **Preloaded metadata**: All model metadata is cached at initialization for fast lookups
149
+ - **Result caching**: Same query returns cached result, avoiding redundant computation
150
+ - **Parse caching**: Query string parsing is cached with `lru_cache`
151
+ - **Automatic primary key inclusion**: Primary keys are always included in `load_only`
152
+ - **Relationship detection**: Automatically detects SQLAlchemy relationships
153
+ - **Nested loading**: Recursively generates `selectinload` with nested `load_only`
154
+ - **Error handling**: Clear errors for invalid fields, relationships, or syntax
155
+
156
+ ## Error Handling
157
+
158
+ ```python
159
+ from sqlalchemy_load import (
160
+ LoadGenerator,
161
+ ParseError,
162
+ FieldNotFoundError,
163
+ RelationshipNotFoundError,
164
+ )
165
+
166
+ generator = LoadGenerator(Base)
167
+
168
+ # Syntax error
169
+ try:
170
+ generator.generate(User, "{ id name") # Missing closing brace
171
+ except ParseError as e:
172
+ print(f"Syntax error: {e}")
173
+
174
+ # Field doesn't exist
175
+ try:
176
+ generator.generate(User, "{ nonexistent }")
177
+ except FieldNotFoundError as e:
178
+ print(f"Field not found: {e}")
179
+
180
+ # Relationship doesn't exist
181
+ try:
182
+ generator.generate(User, "{ notarelationship { id } }")
183
+ except RelationshipNotFoundError as e:
184
+ print(f"Relationship not found: {e}")
185
+ ```
186
+
187
+ ## Requirements
188
+
189
+ - Python >= 3.10
190
+ - SQLAlchemy >= 2.0
191
+
192
+ ## License
193
+
194
+ MIT