Odyn is a python client for Business Central Web Services API.
Project description
Odyn
Async-first Python client for Microsoft Dynamics 365 Business Central on-premises OData Web Services. Returns native Polars DataFrames with built-in caching, rate limiting, retry logic, and a fluent query builder.
Scope: Odyn targets Business Central OData Web Services only. It is not designed for the standard Business Central API v2.0 endpoints.
Why Odyn
Integrating with Business Central Web Services means dealing with NTLM/Basic auth, manual OData query strings, pagination loops, and converting JSON blobs into something useful. Odyn handles all of that behind a single async call that returns a Polars DataFrame.
- Async + sync — built on httpx; sync wrapper included for scripts and notebooks
- Polars DataFrames — columnar, fast, zero-copy where possible
- Fluent query builder — type-safe filters via the
Fsingleton, method chaining, raw escape hatch - Parquet caching — local file cache with TTL, SHA256 keys, hit/miss stats
- Resilience — exponential backoff with jitter, rate limiting via token bucket, concurrency control
- Batch operations — concurrent chunked lookups with progress callbacks
- Delta sync —
get_since()/get_before()for incremental loads - Streaming — page-by-page async iteration for large datasets
- Hooks — plug in request/response observers for logging, metrics, or tracing
Requirements
- Python 3.12+
- httpx >= 0.28
- polars >= 1.36
- aiolimiter >= 1.2
Installation
pip install odyn
Or with uv:
uv add odyn
Quick Start
Async
import asyncio
from odyn import BCWebServiceClient, BasicAuth
from odyn.query import ODataQuery, F
async def main():
async with BCWebServiceClient.create(
server="https://bc-server:7048",
instance="BC210",
auth=BasicAuth("DOMAIN\\user", "password"),
company="CRONUS International Ltd.",
) as client:
# All customers as a Polars DataFrame
customers = await client.get("customers")
# Filtered query
query = (
ODataQuery()
.select("No", "Name", "Balance_LCY")
.filter(F.Balance_LCY > 1000)
.filter(F.Blocked == False)
.order_by("Balance_LCY desc")
.top(50)
)
top_customers = await client.get("customers", query=query)
# Single record by key
customer = await client.get_by_key("customers", "C00010")
asyncio.run(main())
Sync
from odyn import BCWebServiceClientSync, BasicAuth
with BCWebServiceClientSync.create(
server="https://bc-server:7048",
instance="BC210",
auth=BasicAuth("user", "password"),
company="CRONUS",
) as client:
df = client.get("customers")
print(df)
API Key Authentication
from odyn import BCWebServiceClient, APIKeyAuth
auth = APIKeyAuth("my-secret-api-key")
# Or with a Bearer token in the Authorization header
auth = APIKeyAuth("my-key", header_name="Authorization", prefix="Bearer")
Query Builder
from odyn.query import ODataQuery, F
query = (
ODataQuery()
.select("No", "Name", "Balance_LCY")
.filter(F.Status == "Active") # eq
.filter(F.Balance_LCY > 1000) # gt
.filter(F.Type.is_in(["Customer", "Vendor"])) # IN via OR chain
.expand("SalesLines")
.order_by("Name asc")
.top(100)
.skip(50)
)
# Raw OData for functions not covered by the DSL
query = ODataQuery().filter_raw("contains(Name, 'Corp')")
# Combine expressions with & and |
expr = (F.Status == "Active") & (F.Balance_LCY > 0)
expr = (F.City == "London") | (F.City == "Berlin")
Caching
async with BCWebServiceClient.create(
server="https://bc-server:7048",
instance="BC210",
auth=BasicAuth("user", "pass"),
cache_dir="~/.cache/odyn",
cache_ttl=3600, # 1 hour
) as client:
df = await client.get("customers") # cache miss — fetches from API
df = await client.get("customers") # cache hit — reads Parquet
df = await client.get("customers", use_cache=False) # force refresh
client.cleanup_cache() # remove expired entries
Batch Operations
customer_ids = ["C001", "C002", ..., "C500"]
df = await client.get_batch(
"customers",
field="No",
values=customer_ids,
batch_size=50,
select=["No", "Name", "Balance_LCY"],
)
Delta Sync
from datetime import datetime, timedelta, timezone
since = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
updated = await client.get_since("customers", since)
Streaming
async for page in client.get_stream("largeDataset"):
process(page) # each page is a Polars DataFrame
Documentation
Full documentation lives in docs/:
| Guide | Description |
|---|---|
| Getting Started | Installation, prerequisites, first connection |
| Client | Creating and configuring the client |
| Authentication | BasicAuth, APIKeyAuth, custom headers |
| Query Builder | Filters, select, expand, order, the F singleton |
| Caching | ParquetCache, TTL, cache management |
| Sync Client | Synchronous wrapper for non-async contexts |
| Advanced | Hooks, streaming, batch ops, delta sync, concurrency |
| Exceptions | Exception hierarchy and error handling |
| API Reference | Every class, method, parameter, and type |
| Troubleshooting | Common issues and solutions |
| LLM Context | Single-file complete reference for AI assistants |
License
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 odyn-0.5.0.tar.gz.
File metadata
- Download URL: odyn-0.5.0.tar.gz
- Upload date:
- Size: 31.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3bc9accfd4ff50fb69111da6bb8b983b0b663113afb020e64d4834ee1d16e103
|
|
| MD5 |
9797e286574aff97cd0138b36f3e0379
|
|
| BLAKE2b-256 |
c2b264a049c2bc68d5dfd8e29b8305c0a86f78735d09971e4d5647c938d9e5c4
|
File details
Details for the file odyn-0.5.0-py3-none-any.whl.
File metadata
- Download URL: odyn-0.5.0-py3-none-any.whl
- Upload date:
- Size: 35.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a4c8bb8f372fc80cad5699d3dbbef3bbbb59d4bab4ad6f69e6ec2723465bfd0
|
|
| MD5 |
15f2300662ae03e8b78b276e8f20aa31
|
|
| BLAKE2b-256 |
4df784182dbaf3ec97b3d242202c413262667faf8a8338d61749b60ee62cc87d
|