Skip to main content

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 F singleton, 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 syncget_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

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

odyn-0.5.1.tar.gz (31.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

odyn-0.5.1-py3-none-any.whl (35.4 kB view details)

Uploaded Python 3

File details

Details for the file odyn-0.5.1.tar.gz.

File metadata

  • Download URL: odyn-0.5.1.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

Hashes for odyn-0.5.1.tar.gz
Algorithm Hash digest
SHA256 586337a61d15162bc62c92eac06e0d5e932aa9e8219fa1f4591b35e2e84ff3b5
MD5 033a4ff1c1cb784ca7e459e5d3a63a23
BLAKE2b-256 a30d96566d1995b1cf9d05f96be2aca485931e7ecd69c643f0101323bf0729e0

See more details on using hashes here.

File details

Details for the file odyn-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: odyn-0.5.1-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

Hashes for odyn-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7c7efc45b7b2f12cb4721fc48818c502c8fc094c08368566acf8c806fe28f4f0
MD5 d1b167304a240f70108c56389dac1528
BLAKE2b-256 438e5dfd74c0c05419d7693585638d5ebdd5812497b326289ff471cf3893bfe1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page