SQLAlchemy dialect and Shillelagh adapter for OData v4 — query OData APIs with SQL in Apache Superset
Project description
sqlalchemy-odata
Open-source OData v4 connector for SQLAlchemy and Apache Superset.
A Shillelagh adapter that lets you query any OData v4 API with SQL.
What it does
- Connects to any OData v4 service and reads
$metadatato auto-discover all entity sets and their schemas - Exposes each entity set as a SQL table (e.g.
Products,Orders,Customers) - Fetches data via
$top/$skipand@odata.nextLinkpagination - SQLite (via Shillelagh/APSW) handles all SQL operations locally —
SELECT,WHERE,GROUP BY,JOIN, subqueries, etc. - Registers an
odata://SQLAlchemy dialect for easy connection strings - Includes a Superset engine spec so it appears in the "Add Database" dialog
Installation
pip install sqlalchemy-odata
For Apache Superset, add to your requirements-local.txt or Docker image:
sqlalchemy-odata
Quick start
Try it with the public Northwind OData service — no auth required:
from sqlalchemy import create_engine, text
engine = create_engine("odata://services.odata.org/V4/Northwind/Northwind.svc")
with engine.connect() as conn:
result = conn.execute(text("SELECT ProductName, UnitPrice FROM Products LIMIT 5"))
for row in result:
print(row)
Usage
Connection string
odata://username:password@hostname/service-path
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).
HTTPS is used by default. For local development servers (localhost / 127.0.0.1), HTTP is used automatically.
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 for production deployments.
In Python
from sqlalchemy import create_engine, text
engine = create_engine(
"odata://myuser:mypassword@api.example.com/odata/v1"
)
with engine.connect() as conn:
# Auto-discovers tables from $metadata
result = conn.execute(text("SELECT * FROM Products LIMIT 10"))
for row in result:
print(row)
# Full SQL support — GROUP BY, JOIN, subqueries, etc.
result = conn.execute(text("""
SELECT Category, COUNT(*) as cnt, AVG(Price) as avg_price
FROM Products
WHERE InStock = 1
GROUP BY Category
ORDER BY cnt DESC
"""))
With HammerTech
engine = create_engine(
"odata://myuser:api_key@us-reporting-01.hammertechonline.com/v0.1"
)
with engine.connect() as conn:
result = conn.execute(text("SELECT * FROM incidents LIMIT 10"))
In Apache Superset
- Go to Settings > Database Connections > + Database
- Select OData (or use "Other" and enter the URI manually)
- Enter the connection string:
odata://user:pass@host/path - Click Connect — all entity sets appear as tables in SQL Lab
Table discovery
Tables are automatically discovered from the OData $metadata endpoint. You can also inspect them programmatically:
from sqlalchemy import create_engine, inspect
engine = create_engine("odata://user:pass@host/path")
inspector = inspect(engine)
print(inspector.get_table_names())
# ['Customers', 'Orders', 'Products', ...]
How it works
┌──────────────────────────────────────────────────────────┐
│ Your SQL query │
│ SELECT * FROM Products WHERE Price > 100 │
└────────────────────┬─────────────────────────────────────┘
│
┌───────────▼───────────┐
│ SQLite (via APSW) │ Handles SQL parsing,
│ + Shillelagh │ filtering, joins, etc.
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ sqlalchemy-odata │ Fetches data from the
│ ODataAdapter │ OData API via HTTP
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ OData v4 Service │ $metadata for schema,
│ (any provider) │ $top/$skip for data
└───────────────────────┘
- On first query, the adapter fetches the
$metadataEDMX document to discover entity types, properties, and their EDM types - EDM types are mapped to SQLite types (
Edm.String->TEXT,Edm.Int32->INTEGER,Edm.DateTimeOffset->TIMESTAMP, etc.) - Data is fetched via paginated
GETrequests with$top/$skipparameters (or@odata.nextLinkif the server provides it) - SQLite handles all query operations (filtering, sorting, grouping, joins) locally
- Results are returned through the standard SQLAlchemy/DB-API interface
Supported OData features
| Feature | Status |
|---|---|
$metadata schema discovery |
Supported |
$top / $skip pagination |
Supported |
@odata.nextLink pagination |
Supported |
@odata.count |
Not yet |
| Basic Auth | Supported |
| Bearer Token Auth | Not yet |
| OAuth2 | Not yet |
$filter pushdown |
Not yet (filtered locally by SQLite) |
$select pushdown |
Not yet (all columns fetched) |
$orderby pushdown |
Not yet (sorted locally by SQLite) |
$expand (relationships) |
Not yet |
| Write operations (POST/PATCH/DELETE) | Not supported (read-only) |
Note: Even without server-side pushdown, all SQL operations work because SQLite handles them locally. Pushdown is a performance optimization for large datasets.
Limitations
- Performance on large datasets: Without
$filterpushdown, 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. - Auth: Only HTTP Basic Auth is currently supported. Bearer tokens and OAuth2 are planned.
- Read-only: Write operations (INSERT, UPDATE, DELETE) are not supported.
Architecture
This package provides three components built on the Shillelagh framework:
| Component | Purpose |
|---|---|
shillelagh_odata.adapter |
Shillelagh adapter — fetches data from OData, parses $metadata |
shillelagh_odata.dialect |
SQLAlchemy dialect (odata://) — handles connection strings, table discovery |
shillelagh_odata.engine_spec |
Superset BaseEngineSpec subclass — registers OData in the Superset UI |
These register via entry points:
[project.entry-points."shillelagh.adapter"]
odataapi = "shillelagh_odata.adapter:ODataAdapter"
[project.entry-points."sqlalchemy.dialects"]
odata = "shillelagh_odata.dialect:APSWODataDialect"
[project.entry-points."superset.db_engine_specs"]
odata = "shillelagh_odata.engine_spec:ODataEngineSpec"
Troubleshooting
No tables found / empty table list
- Verify your OData service URL is correct and the
$metadataendpoint is accessible - Check credentials — a 401/403 response will result in an empty table list
- Try accessing
https://your-host/your-path/$metadatain a browser to verify the service
Empty query results
- The entity set may exist in
$metadatabut contain no data - Check that the entity set name is spelled exactly as it appears in
$metadata(case-sensitive)
Connection timeouts
- The default timeout is 30 seconds for metadata and 60 seconds for data requests
- Large entity sets with many pages may take time to fully load
Can't connect to local development server
localhostand127.0.0.1automatically use HTTP instead of HTTPS- For other local hostnames, ensure your server supports HTTPS or use localhost
Development
git clone https://github.com/brandonjjon/sqlalchemy-odata.git
cd sqlalchemy-odata
pip install -e ".[dev]"
pytest
Related projects
- Shillelagh — the framework this adapter is built on
- Apache Superset — the BI platform this integrates with
- graphql-db-api — similar adapter for GraphQL APIs (also built on Shillelagh)
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sqlalchemy_odata-0.2.0.tar.gz.
File metadata
- Download URL: sqlalchemy_odata-0.2.0.tar.gz
- Upload date:
- Size: 18.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e481a68047c8ebd7e5f3f9a2795a7ee3f6ba940423e1e13d8ab195c543453269
|
|
| MD5 |
19a28e2d64739c42466e0ea480932845
|
|
| BLAKE2b-256 |
f5f3fc72fc775a86f52d8bbffd55842394bdc90cc767b682639312c726d436ec
|
Provenance
The following attestation bundles were made for sqlalchemy_odata-0.2.0.tar.gz:
Publisher:
release-please.yml on brandonjjon/sqlalchemy-odata
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sqlalchemy_odata-0.2.0.tar.gz -
Subject digest:
e481a68047c8ebd7e5f3f9a2795a7ee3f6ba940423e1e13d8ab195c543453269 - Sigstore transparency entry: 1186505424
- Sigstore integration time:
-
Permalink:
brandonjjon/sqlalchemy-odata@d2a44d9afeab7650f117d1df9fc33c2ddd506626 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/brandonjjon
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@d2a44d9afeab7650f117d1df9fc33c2ddd506626 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sqlalchemy_odata-0.2.0-py3-none-any.whl.
File metadata
- Download URL: sqlalchemy_odata-0.2.0-py3-none-any.whl
- Upload date:
- Size: 13.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a71668cc12865aaf9122173305fc961d24367eada697c06eaa637d4f3d5cf6d7
|
|
| MD5 |
0f5b6c0fede3610ab49e82c35231c154
|
|
| BLAKE2b-256 |
15a9fe183c46e4b521f674823f5e97d509f023aa52e52817cd850a0dbb566374
|
Provenance
The following attestation bundles were made for sqlalchemy_odata-0.2.0-py3-none-any.whl:
Publisher:
release-please.yml on brandonjjon/sqlalchemy-odata
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sqlalchemy_odata-0.2.0-py3-none-any.whl -
Subject digest:
a71668cc12865aaf9122173305fc961d24367eada697c06eaa637d4f3d5cf6d7 - Sigstore transparency entry: 1186505427
- Sigstore integration time:
-
Permalink:
brandonjjon/sqlalchemy-odata@d2a44d9afeab7650f117d1df9fc33c2ddd506626 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/brandonjjon
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@d2a44d9afeab7650f117d1df9fc33c2ddd506626 -
Trigger Event:
push
-
Statement type: