sqlalchemy-odata 0.1.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.
- sqlalchemy_odata-0.1.0/LICENSE +21 -0
- sqlalchemy_odata-0.1.0/PKG-INFO +268 -0
- sqlalchemy_odata-0.1.0/README.md +231 -0
- sqlalchemy_odata-0.1.0/pyproject.toml +74 -0
- sqlalchemy_odata-0.1.0/setup.cfg +4 -0
- sqlalchemy_odata-0.1.0/src/shillelagh_odata/__init__.py +3 -0
- sqlalchemy_odata-0.1.0/src/shillelagh_odata/adapter.py +369 -0
- sqlalchemy_odata-0.1.0/src/shillelagh_odata/dialect.py +98 -0
- sqlalchemy_odata-0.1.0/src/shillelagh_odata/engine_spec.py +25 -0
- sqlalchemy_odata-0.1.0/src/shillelagh_odata/py.typed +0 -0
- sqlalchemy_odata-0.1.0/src/sqlalchemy_odata.egg-info/PKG-INFO +268 -0
- sqlalchemy_odata-0.1.0/src/sqlalchemy_odata.egg-info/SOURCES.txt +15 -0
- sqlalchemy_odata-0.1.0/src/sqlalchemy_odata.egg-info/dependency_links.txt +1 -0
- sqlalchemy_odata-0.1.0/src/sqlalchemy_odata.egg-info/entry_points.txt +8 -0
- sqlalchemy_odata-0.1.0/src/sqlalchemy_odata.egg-info/requires.txt +12 -0
- sqlalchemy_odata-0.1.0/src/sqlalchemy_odata.egg-info/top_level.txt +1 -0
- sqlalchemy_odata-0.1.0/tests/test_adapter.py +676 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brandon Jones
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-odata
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SQLAlchemy dialect and Shillelagh adapter for OData v4 — query OData APIs with SQL in Apache Superset
|
|
5
|
+
Author: Brandon Jones
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/brandonjjon/sqlalchemy-odata
|
|
8
|
+
Project-URL: Repository, https://github.com/brandonjjon/sqlalchemy-odata
|
|
9
|
+
Project-URL: Issues, https://github.com/brandonjjon/sqlalchemy-odata/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/brandonjjon/sqlalchemy-odata/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: odata,sqlalchemy,shillelagh,superset,sql,api,odata-v4,connector,rest-api,business-intelligence,data-integration
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: shillelagh<3.0,>=1.2.0
|
|
27
|
+
Requires-Dist: requests>=2.28.0
|
|
28
|
+
Provides-Extra: superset
|
|
29
|
+
Requires-Dist: apache-superset>=3.0.0; extra == "superset"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
33
|
+
Requires-Dist: responses>=0.23.0; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
35
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# sqlalchemy-odata
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/sqlalchemy-odata/)
|
|
41
|
+
[](https://pypi.org/project/sqlalchemy-odata/)
|
|
42
|
+
[](https://opensource.org/licenses/MIT)
|
|
43
|
+
|
|
44
|
+
Open-source OData v4 connector for SQLAlchemy and [Apache Superset](https://superset.apache.org/).
|
|
45
|
+
|
|
46
|
+
A [Shillelagh](https://github.com/betodealmeida/shillelagh) adapter that lets you query any OData v4 API with SQL.
|
|
47
|
+
|
|
48
|
+
## What it does
|
|
49
|
+
|
|
50
|
+
- Connects to any OData v4 service and reads `$metadata` to **auto-discover** all entity sets and their schemas
|
|
51
|
+
- Exposes each entity set as a SQL table (e.g. `Products`, `Orders`, `Customers`)
|
|
52
|
+
- Fetches data via `$top`/`$skip` and `@odata.nextLink` pagination
|
|
53
|
+
- SQLite (via [Shillelagh](https://github.com/betodealmeida/shillelagh)/APSW) handles all SQL operations locally — `SELECT`, `WHERE`, `GROUP BY`, `JOIN`, subqueries, etc.
|
|
54
|
+
- Registers an `odata://` SQLAlchemy dialect for easy connection strings
|
|
55
|
+
- Includes a Superset engine spec so it appears in the "Add Database" dialog
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install sqlalchemy-odata
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For Apache Superset, add to your `requirements-local.txt` or Docker image:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
sqlalchemy-odata
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
Try it with the public [Northwind OData service](https://services.odata.org/) — no auth required:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from sqlalchemy import create_engine, text
|
|
75
|
+
|
|
76
|
+
engine = create_engine("odata://services.odata.org/V4/Northwind/Northwind.svc")
|
|
77
|
+
|
|
78
|
+
with engine.connect() as conn:
|
|
79
|
+
result = conn.execute(text("SELECT ProductName, UnitPrice FROM Products LIMIT 5"))
|
|
80
|
+
for row in result:
|
|
81
|
+
print(row)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
### Connection string
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
odata://username:password@hostname/service-path
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The username and password are passed as HTTP Basic Auth credentials. The service path is the OData service root (everything before the entity set names).
|
|
93
|
+
|
|
94
|
+
HTTPS is used by default. For local development servers (`localhost` / `127.0.0.1`), HTTP is used automatically.
|
|
95
|
+
|
|
96
|
+
> **Note:** Credentials are embedded in the connection string. If you're using Superset, be aware that connection strings are stored in Superset's metadata database. Consider using Superset's [secrets management](https://superset.apache.org/docs/configuration/configuring-superset/#secret-management) for production deployments.
|
|
97
|
+
|
|
98
|
+
### In Python
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from sqlalchemy import create_engine, text
|
|
102
|
+
|
|
103
|
+
engine = create_engine(
|
|
104
|
+
"odata://myuser:mypassword@api.example.com/odata/v1"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
with engine.connect() as conn:
|
|
108
|
+
# Auto-discovers tables from $metadata
|
|
109
|
+
result = conn.execute(text("SELECT * FROM Products LIMIT 10"))
|
|
110
|
+
for row in result:
|
|
111
|
+
print(row)
|
|
112
|
+
|
|
113
|
+
# Full SQL support — GROUP BY, JOIN, subqueries, etc.
|
|
114
|
+
result = conn.execute(text("""
|
|
115
|
+
SELECT Category, COUNT(*) as cnt, AVG(Price) as avg_price
|
|
116
|
+
FROM Products
|
|
117
|
+
WHERE InStock = 1
|
|
118
|
+
GROUP BY Category
|
|
119
|
+
ORDER BY cnt DESC
|
|
120
|
+
"""))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### With HammerTech
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
engine = create_engine(
|
|
127
|
+
"odata://myuser:api_key@us-reporting-01.hammertechonline.com/v0.1"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
with engine.connect() as conn:
|
|
131
|
+
result = conn.execute(text("SELECT * FROM incidents LIMIT 10"))
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### In Apache Superset
|
|
135
|
+
|
|
136
|
+
1. Go to **Settings > Database Connections > + Database**
|
|
137
|
+
2. Select **OData** (or use "Other" and enter the URI manually)
|
|
138
|
+
3. Enter the connection string: `odata://user:pass@host/path`
|
|
139
|
+
4. Click **Connect** — all entity sets appear as tables in SQL Lab
|
|
140
|
+
|
|
141
|
+
### Table discovery
|
|
142
|
+
|
|
143
|
+
Tables are automatically discovered from the OData `$metadata` endpoint. You can also inspect them programmatically:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from sqlalchemy import create_engine, inspect
|
|
147
|
+
|
|
148
|
+
engine = create_engine("odata://user:pass@host/path")
|
|
149
|
+
inspector = inspect(engine)
|
|
150
|
+
print(inspector.get_table_names())
|
|
151
|
+
# ['Customers', 'Orders', 'Products', ...]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## How it works
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
┌──────────────────────────────────────────────────────────┐
|
|
158
|
+
│ Your SQL query │
|
|
159
|
+
│ SELECT * FROM Products WHERE Price > 100 │
|
|
160
|
+
└────────────────────┬─────────────────────────────────────┘
|
|
161
|
+
│
|
|
162
|
+
┌───────────▼───────────┐
|
|
163
|
+
│ SQLite (via APSW) │ Handles SQL parsing,
|
|
164
|
+
│ + Shillelagh │ filtering, joins, etc.
|
|
165
|
+
└───────────┬───────────┘
|
|
166
|
+
│
|
|
167
|
+
┌───────────▼───────────┐
|
|
168
|
+
│ sqlalchemy-odata │ Fetches data from the
|
|
169
|
+
│ ODataAdapter │ OData API via HTTP
|
|
170
|
+
└───────────┬───────────┘
|
|
171
|
+
│
|
|
172
|
+
┌───────────▼───────────┐
|
|
173
|
+
│ OData v4 Service │ $metadata for schema,
|
|
174
|
+
│ (any provider) │ $top/$skip for data
|
|
175
|
+
└───────────────────────┘
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
1. On first query, the adapter fetches the `$metadata` EDMX document to discover entity types, properties, and their EDM types
|
|
179
|
+
2. EDM types are mapped to SQLite types (`Edm.String` -> `TEXT`, `Edm.Int32` -> `INTEGER`, `Edm.DateTimeOffset` -> `TIMESTAMP`, etc.)
|
|
180
|
+
3. Data is fetched via paginated `GET` requests with `$top`/`$skip` parameters (or `@odata.nextLink` if the server provides it)
|
|
181
|
+
4. SQLite handles all query operations (filtering, sorting, grouping, joins) locally
|
|
182
|
+
5. Results are returned through the standard SQLAlchemy/DB-API interface
|
|
183
|
+
|
|
184
|
+
## Supported OData features
|
|
185
|
+
|
|
186
|
+
| Feature | Status |
|
|
187
|
+
|---------|--------|
|
|
188
|
+
| `$metadata` schema discovery | Supported |
|
|
189
|
+
| `$top` / `$skip` pagination | Supported |
|
|
190
|
+
| `@odata.nextLink` pagination | Supported |
|
|
191
|
+
| `@odata.count` | Not yet |
|
|
192
|
+
| Basic Auth | Supported |
|
|
193
|
+
| Bearer Token Auth | Not yet |
|
|
194
|
+
| OAuth2 | Not yet |
|
|
195
|
+
| `$filter` pushdown | Not yet (filtered locally by SQLite) |
|
|
196
|
+
| `$select` pushdown | Not yet (all columns fetched) |
|
|
197
|
+
| `$orderby` pushdown | Not yet (sorted locally by SQLite) |
|
|
198
|
+
| `$expand` (relationships) | Not yet |
|
|
199
|
+
| Write operations (POST/PATCH/DELETE) | Not supported (read-only) |
|
|
200
|
+
|
|
201
|
+
> **Note:** Even without server-side pushdown, all SQL operations work because SQLite handles them locally. Pushdown is a performance optimization for large datasets.
|
|
202
|
+
|
|
203
|
+
## Limitations
|
|
204
|
+
|
|
205
|
+
- **Performance on large datasets:** Without `$filter` pushdown, the adapter fetches all rows from an entity set and filters locally. For entity sets with hundreds of thousands of rows, this can be slow and memory-intensive. Pushdown support is planned for a future release.
|
|
206
|
+
- **Auth:** Only HTTP Basic Auth is currently supported. Bearer tokens and OAuth2 are planned.
|
|
207
|
+
- **Read-only:** Write operations (INSERT, UPDATE, DELETE) are not supported.
|
|
208
|
+
|
|
209
|
+
## Architecture
|
|
210
|
+
|
|
211
|
+
This package provides three components built on the [Shillelagh](https://github.com/betodealmeida/shillelagh) framework:
|
|
212
|
+
|
|
213
|
+
| Component | Purpose |
|
|
214
|
+
|-----------|---------|
|
|
215
|
+
| `shillelagh_odata.adapter` | [Shillelagh adapter](https://shillelagh.readthedocs.io/en/latest/development.html) — fetches data from OData, parses `$metadata` |
|
|
216
|
+
| `shillelagh_odata.dialect` | [SQLAlchemy dialect](https://shillelagh.readthedocs.io/en/latest/development.html#creating-a-custom-sqlalchemy-dialect) (`odata://`) — handles connection strings, table discovery |
|
|
217
|
+
| `shillelagh_odata.engine_spec` | Superset `BaseEngineSpec` subclass — registers OData in the Superset UI |
|
|
218
|
+
|
|
219
|
+
These register via entry points:
|
|
220
|
+
|
|
221
|
+
```toml
|
|
222
|
+
[project.entry-points."shillelagh.adapter"]
|
|
223
|
+
odataapi = "shillelagh_odata.adapter:ODataAdapter"
|
|
224
|
+
|
|
225
|
+
[project.entry-points."sqlalchemy.dialects"]
|
|
226
|
+
odata = "shillelagh_odata.dialect:APSWODataDialect"
|
|
227
|
+
|
|
228
|
+
[project.entry-points."superset.db_engine_specs"]
|
|
229
|
+
odata = "shillelagh_odata.engine_spec:ODataEngineSpec"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Troubleshooting
|
|
233
|
+
|
|
234
|
+
**No tables found / empty table list**
|
|
235
|
+
- Verify your OData service URL is correct and the `$metadata` endpoint is accessible
|
|
236
|
+
- Check credentials — a 401/403 response will result in an empty table list
|
|
237
|
+
- Try accessing `https://your-host/your-path/$metadata` in a browser to verify the service
|
|
238
|
+
|
|
239
|
+
**Empty query results**
|
|
240
|
+
- The entity set may exist in `$metadata` but contain no data
|
|
241
|
+
- Check that the entity set name is spelled exactly as it appears in `$metadata` (case-sensitive)
|
|
242
|
+
|
|
243
|
+
**Connection timeouts**
|
|
244
|
+
- The default timeout is 30 seconds for metadata and 60 seconds for data requests
|
|
245
|
+
- Large entity sets with many pages may take time to fully load
|
|
246
|
+
|
|
247
|
+
**Can't connect to local development server**
|
|
248
|
+
- `localhost` and `127.0.0.1` automatically use HTTP instead of HTTPS
|
|
249
|
+
- For other local hostnames, ensure your server supports HTTPS or use localhost
|
|
250
|
+
|
|
251
|
+
## Development
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
git clone https://github.com/brandonjjon/sqlalchemy-odata.git
|
|
255
|
+
cd sqlalchemy-odata
|
|
256
|
+
pip install -e ".[dev]"
|
|
257
|
+
pytest
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Related projects
|
|
261
|
+
|
|
262
|
+
- [Shillelagh](https://github.com/betodealmeida/shillelagh) — the framework this adapter is built on
|
|
263
|
+
- [Apache Superset](https://github.com/apache/superset) — the BI platform this integrates with
|
|
264
|
+
- [graphql-db-api](https://github.com/cancan101/graphql-db-api) — similar adapter for GraphQL APIs (also built on Shillelagh)
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# sqlalchemy-odata
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/sqlalchemy-odata/)
|
|
4
|
+
[](https://pypi.org/project/sqlalchemy-odata/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Open-source OData v4 connector for SQLAlchemy and [Apache Superset](https://superset.apache.org/).
|
|
8
|
+
|
|
9
|
+
A [Shillelagh](https://github.com/betodealmeida/shillelagh) adapter that lets you query any OData v4 API with SQL.
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- Connects to any OData v4 service and reads `$metadata` to **auto-discover** all entity sets and their schemas
|
|
14
|
+
- Exposes each entity set as a SQL table (e.g. `Products`, `Orders`, `Customers`)
|
|
15
|
+
- Fetches data via `$top`/`$skip` and `@odata.nextLink` pagination
|
|
16
|
+
- SQLite (via [Shillelagh](https://github.com/betodealmeida/shillelagh)/APSW) handles all SQL operations locally — `SELECT`, `WHERE`, `GROUP BY`, `JOIN`, subqueries, etc.
|
|
17
|
+
- Registers an `odata://` SQLAlchemy dialect for easy connection strings
|
|
18
|
+
- Includes a Superset engine spec so it appears in the "Add Database" dialog
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install sqlalchemy-odata
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For Apache Superset, add to your `requirements-local.txt` or Docker image:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
sqlalchemy-odata
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
Try it with the public [Northwind OData service](https://services.odata.org/) — no auth required:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from sqlalchemy import create_engine, text
|
|
38
|
+
|
|
39
|
+
engine = create_engine("odata://services.odata.org/V4/Northwind/Northwind.svc")
|
|
40
|
+
|
|
41
|
+
with engine.connect() as conn:
|
|
42
|
+
result = conn.execute(text("SELECT ProductName, UnitPrice FROM Products LIMIT 5"))
|
|
43
|
+
for row in result:
|
|
44
|
+
print(row)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### Connection string
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
odata://username:password@hostname/service-path
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The username and password are passed as HTTP Basic Auth credentials. The service path is the OData service root (everything before the entity set names).
|
|
56
|
+
|
|
57
|
+
HTTPS is used by default. For local development servers (`localhost` / `127.0.0.1`), HTTP is used automatically.
|
|
58
|
+
|
|
59
|
+
> **Note:** Credentials are embedded in the connection string. If you're using Superset, be aware that connection strings are stored in Superset's metadata database. Consider using Superset's [secrets management](https://superset.apache.org/docs/configuration/configuring-superset/#secret-management) for production deployments.
|
|
60
|
+
|
|
61
|
+
### In Python
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from sqlalchemy import create_engine, text
|
|
65
|
+
|
|
66
|
+
engine = create_engine(
|
|
67
|
+
"odata://myuser:mypassword@api.example.com/odata/v1"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
with engine.connect() as conn:
|
|
71
|
+
# Auto-discovers tables from $metadata
|
|
72
|
+
result = conn.execute(text("SELECT * FROM Products LIMIT 10"))
|
|
73
|
+
for row in result:
|
|
74
|
+
print(row)
|
|
75
|
+
|
|
76
|
+
# Full SQL support — GROUP BY, JOIN, subqueries, etc.
|
|
77
|
+
result = conn.execute(text("""
|
|
78
|
+
SELECT Category, COUNT(*) as cnt, AVG(Price) as avg_price
|
|
79
|
+
FROM Products
|
|
80
|
+
WHERE InStock = 1
|
|
81
|
+
GROUP BY Category
|
|
82
|
+
ORDER BY cnt DESC
|
|
83
|
+
"""))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### With HammerTech
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
engine = create_engine(
|
|
90
|
+
"odata://myuser:api_key@us-reporting-01.hammertechonline.com/v0.1"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
with engine.connect() as conn:
|
|
94
|
+
result = conn.execute(text("SELECT * FROM incidents LIMIT 10"))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### In Apache Superset
|
|
98
|
+
|
|
99
|
+
1. Go to **Settings > Database Connections > + Database**
|
|
100
|
+
2. Select **OData** (or use "Other" and enter the URI manually)
|
|
101
|
+
3. Enter the connection string: `odata://user:pass@host/path`
|
|
102
|
+
4. Click **Connect** — all entity sets appear as tables in SQL Lab
|
|
103
|
+
|
|
104
|
+
### Table discovery
|
|
105
|
+
|
|
106
|
+
Tables are automatically discovered from the OData `$metadata` endpoint. You can also inspect them programmatically:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from sqlalchemy import create_engine, inspect
|
|
110
|
+
|
|
111
|
+
engine = create_engine("odata://user:pass@host/path")
|
|
112
|
+
inspector = inspect(engine)
|
|
113
|
+
print(inspector.get_table_names())
|
|
114
|
+
# ['Customers', 'Orders', 'Products', ...]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## How it works
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
┌──────────────────────────────────────────────────────────┐
|
|
121
|
+
│ Your SQL query │
|
|
122
|
+
│ SELECT * FROM Products WHERE Price > 100 │
|
|
123
|
+
└────────────────────┬─────────────────────────────────────┘
|
|
124
|
+
│
|
|
125
|
+
┌───────────▼───────────┐
|
|
126
|
+
│ SQLite (via APSW) │ Handles SQL parsing,
|
|
127
|
+
│ + Shillelagh │ filtering, joins, etc.
|
|
128
|
+
└───────────┬───────────┘
|
|
129
|
+
│
|
|
130
|
+
┌───────────▼───────────┐
|
|
131
|
+
│ sqlalchemy-odata │ Fetches data from the
|
|
132
|
+
│ ODataAdapter │ OData API via HTTP
|
|
133
|
+
└───────────┬───────────┘
|
|
134
|
+
│
|
|
135
|
+
┌───────────▼───────────┐
|
|
136
|
+
│ OData v4 Service │ $metadata for schema,
|
|
137
|
+
│ (any provider) │ $top/$skip for data
|
|
138
|
+
└───────────────────────┘
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
1. On first query, the adapter fetches the `$metadata` EDMX document to discover entity types, properties, and their EDM types
|
|
142
|
+
2. EDM types are mapped to SQLite types (`Edm.String` -> `TEXT`, `Edm.Int32` -> `INTEGER`, `Edm.DateTimeOffset` -> `TIMESTAMP`, etc.)
|
|
143
|
+
3. Data is fetched via paginated `GET` requests with `$top`/`$skip` parameters (or `@odata.nextLink` if the server provides it)
|
|
144
|
+
4. SQLite handles all query operations (filtering, sorting, grouping, joins) locally
|
|
145
|
+
5. Results are returned through the standard SQLAlchemy/DB-API interface
|
|
146
|
+
|
|
147
|
+
## Supported OData features
|
|
148
|
+
|
|
149
|
+
| Feature | Status |
|
|
150
|
+
|---------|--------|
|
|
151
|
+
| `$metadata` schema discovery | Supported |
|
|
152
|
+
| `$top` / `$skip` pagination | Supported |
|
|
153
|
+
| `@odata.nextLink` pagination | Supported |
|
|
154
|
+
| `@odata.count` | Not yet |
|
|
155
|
+
| Basic Auth | Supported |
|
|
156
|
+
| Bearer Token Auth | Not yet |
|
|
157
|
+
| OAuth2 | Not yet |
|
|
158
|
+
| `$filter` pushdown | Not yet (filtered locally by SQLite) |
|
|
159
|
+
| `$select` pushdown | Not yet (all columns fetched) |
|
|
160
|
+
| `$orderby` pushdown | Not yet (sorted locally by SQLite) |
|
|
161
|
+
| `$expand` (relationships) | Not yet |
|
|
162
|
+
| Write operations (POST/PATCH/DELETE) | Not supported (read-only) |
|
|
163
|
+
|
|
164
|
+
> **Note:** Even without server-side pushdown, all SQL operations work because SQLite handles them locally. Pushdown is a performance optimization for large datasets.
|
|
165
|
+
|
|
166
|
+
## Limitations
|
|
167
|
+
|
|
168
|
+
- **Performance on large datasets:** Without `$filter` pushdown, the adapter fetches all rows from an entity set and filters locally. For entity sets with hundreds of thousands of rows, this can be slow and memory-intensive. Pushdown support is planned for a future release.
|
|
169
|
+
- **Auth:** Only HTTP Basic Auth is currently supported. Bearer tokens and OAuth2 are planned.
|
|
170
|
+
- **Read-only:** Write operations (INSERT, UPDATE, DELETE) are not supported.
|
|
171
|
+
|
|
172
|
+
## Architecture
|
|
173
|
+
|
|
174
|
+
This package provides three components built on the [Shillelagh](https://github.com/betodealmeida/shillelagh) framework:
|
|
175
|
+
|
|
176
|
+
| Component | Purpose |
|
|
177
|
+
|-----------|---------|
|
|
178
|
+
| `shillelagh_odata.adapter` | [Shillelagh adapter](https://shillelagh.readthedocs.io/en/latest/development.html) — fetches data from OData, parses `$metadata` |
|
|
179
|
+
| `shillelagh_odata.dialect` | [SQLAlchemy dialect](https://shillelagh.readthedocs.io/en/latest/development.html#creating-a-custom-sqlalchemy-dialect) (`odata://`) — handles connection strings, table discovery |
|
|
180
|
+
| `shillelagh_odata.engine_spec` | Superset `BaseEngineSpec` subclass — registers OData in the Superset UI |
|
|
181
|
+
|
|
182
|
+
These register via entry points:
|
|
183
|
+
|
|
184
|
+
```toml
|
|
185
|
+
[project.entry-points."shillelagh.adapter"]
|
|
186
|
+
odataapi = "shillelagh_odata.adapter:ODataAdapter"
|
|
187
|
+
|
|
188
|
+
[project.entry-points."sqlalchemy.dialects"]
|
|
189
|
+
odata = "shillelagh_odata.dialect:APSWODataDialect"
|
|
190
|
+
|
|
191
|
+
[project.entry-points."superset.db_engine_specs"]
|
|
192
|
+
odata = "shillelagh_odata.engine_spec:ODataEngineSpec"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Troubleshooting
|
|
196
|
+
|
|
197
|
+
**No tables found / empty table list**
|
|
198
|
+
- Verify your OData service URL is correct and the `$metadata` endpoint is accessible
|
|
199
|
+
- Check credentials — a 401/403 response will result in an empty table list
|
|
200
|
+
- Try accessing `https://your-host/your-path/$metadata` in a browser to verify the service
|
|
201
|
+
|
|
202
|
+
**Empty query results**
|
|
203
|
+
- The entity set may exist in `$metadata` but contain no data
|
|
204
|
+
- Check that the entity set name is spelled exactly as it appears in `$metadata` (case-sensitive)
|
|
205
|
+
|
|
206
|
+
**Connection timeouts**
|
|
207
|
+
- The default timeout is 30 seconds for metadata and 60 seconds for data requests
|
|
208
|
+
- Large entity sets with many pages may take time to fully load
|
|
209
|
+
|
|
210
|
+
**Can't connect to local development server**
|
|
211
|
+
- `localhost` and `127.0.0.1` automatically use HTTP instead of HTTPS
|
|
212
|
+
- For other local hostnames, ensure your server supports HTTPS or use localhost
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
git clone https://github.com/brandonjjon/sqlalchemy-odata.git
|
|
218
|
+
cd sqlalchemy-odata
|
|
219
|
+
pip install -e ".[dev]"
|
|
220
|
+
pytest
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Related projects
|
|
224
|
+
|
|
225
|
+
- [Shillelagh](https://github.com/betodealmeida/shillelagh) — the framework this adapter is built on
|
|
226
|
+
- [Apache Superset](https://github.com/apache/superset) — the BI platform this integrates with
|
|
227
|
+
- [graphql-db-api](https://github.com/cancan101/graphql-db-api) — similar adapter for GraphQL APIs (also built on Shillelagh)
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sqlalchemy-odata"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SQLAlchemy dialect and Shillelagh adapter for OData v4 — query OData APIs with SQL in Apache Superset"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = ["odata", "sqlalchemy", "shillelagh", "superset", "sql", "api", "odata-v4", "connector", "rest-api", "business-intelligence", "data-integration"]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Brandon Jones"},
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Database",
|
|
26
|
+
"Topic :: Database :: Front-Ends",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"shillelagh>=1.2.0,<3.0",
|
|
31
|
+
"requests>=2.28.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
superset = [
|
|
36
|
+
"apache-superset>=3.0.0",
|
|
37
|
+
]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=7.0",
|
|
40
|
+
"pytest-cov>=4.0",
|
|
41
|
+
"responses>=0.23.0",
|
|
42
|
+
"ruff>=0.4.0",
|
|
43
|
+
"build>=1.0.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/brandonjjon/sqlalchemy-odata"
|
|
48
|
+
Repository = "https://github.com/brandonjjon/sqlalchemy-odata"
|
|
49
|
+
Issues = "https://github.com/brandonjjon/sqlalchemy-odata/issues"
|
|
50
|
+
Changelog = "https://github.com/brandonjjon/sqlalchemy-odata/blob/main/CHANGELOG.md"
|
|
51
|
+
|
|
52
|
+
[project.entry-points."shillelagh.adapter"]
|
|
53
|
+
odataapi = "shillelagh_odata.adapter:ODataAdapter"
|
|
54
|
+
|
|
55
|
+
[project.entry-points."sqlalchemy.dialects"]
|
|
56
|
+
odata = "shillelagh_odata.dialect:APSWODataDialect"
|
|
57
|
+
|
|
58
|
+
[project.entry-points."superset.db_engine_specs"]
|
|
59
|
+
odata = "shillelagh_odata.engine_spec:ODataEngineSpec"
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.packages.find]
|
|
62
|
+
where = ["src"]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff]
|
|
68
|
+
target-version = "py39"
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint]
|
|
71
|
+
select = ["E", "F", "I", "W", "UP"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff.format]
|
|
74
|
+
quote-style = "double"
|