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
Metadata
Release files for sqlalchemy-odata 0.2.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sqlalchemy_odata-0.2.3.tar.gz | 19.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sqlalchemy_odata-0.2.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.4 kB
Release files / sqlalchemy_odata-0.2.3.tar.gz
| Download URL | sqlalchemy_odata-0.2.3.tar.gz |
|---|---|
| Size | 19.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
616c8fd79f420c459044823dbbf8c1cd2d10e56ee91dcfb10d0661c9f651b4e8
|
|
BLAKE2b-256 checksum How to use checksums |
3ad506f5f07e057d0dd5c07e92a1647375be7ee6f077c97df4fed5569d4e3b8b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 26, 2026.
Transparency logRelease files / sqlalchemy_odata-0.2.3-py3-none-any.whl
| Download URL | sqlalchemy_odata-0.2.3-py3-none-any.whl |
|---|---|
| Size | 13.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
02cf73cd8e15f574e0e40b4dd883ad135ca1c1b804acb93d60d82844617e5666
|
|
BLAKE2b-256 checksum How to use checksums |
0e10e49d97370c28171d8542693e9b157204d8cf15df1a03796bc14536d06938
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 26, 2026.
Transparency log