A python client for Business Central OData API.
Project description
Dynite
A robust and efficient Python client for Microsoft Business Central OData API.
Overview
Dynite simplifies interactions with the Microsoft Business Central OData API by providing a high-level, Pythonic interface for fetching business data. It automatically handles authentication, pagination, retry logic, and error handling, allowing developers to focus on their business logic rather than API mechanics.
Key Features
- 🚀 Simple API: Easy-to-use client for Business Central OData endpoints
- 📄 Automatic Pagination: Handles large datasets with transparent pagination
- 🔄 Retry Logic: Configurable retry mechanism for transient failures
- 🛡️ Error Handling: Comprehensive exception hierarchy for different error types
- 📝 Type Hints: Full type annotations for better IDE support and code completion
- 📊 Logging: Built-in logging support for debugging and monitoring
- 🐍 Modern Python: Requires Python 3.13+ for optimal performance and features
Installation
From PyPI (Recommended)
pip install dynite
From Source
# Clone the repository
git clone https://github.com/prabhuakshay/dynite.git
cd dynite
# Install with uv (recommended)
uv pip install -e .
# Or with pip
pip install -e .
Development Installation
# Install with development dependencies
uv pip install -e ".[dev]"
# Or with pip
pip install -e ".[dev]"
Quick Start
from dynite import Dynite
# Initialize the client
client = Dynite(
base_url="https://api.businesscentral.dynamics.com/v2.0/tenant_id/api/v2.0",
auth=("username", "password")
)
# Fetch all companies
companies = client.get_records("companies")
print(f"Found {len(companies)} companies")
# Fetch customers with filtering
customers = client.get_records("customers", params={"$filter": "status eq 'active'"})
API Reference
Dynite Class
The main client class for interacting with Business Central OData API.
Constructor
Dynite(
base_url: str,
auth: tuple[str, str],
timeout: int = 30,
retries: int = 3
)
Parameters:
base_url(str): The base URL for the Business Central OData API endpoint. Must be a valid HTTPS URL.auth(tuple[str, str]): Authentication credentials as (username, password) tuple for basic authentication.timeout(int, optional): Request timeout in seconds. Defaults to 30 seconds.retries(int, optional): Number of retry attempts for failed requests. Defaults to 3.
Raises:
InvalidURLError: If the provided base_url is not a valid URL.
Example:
client = Dynite(
base_url="https://api.businesscentral.dynamics.com/v2.0/your-tenant-id/api/v2.0",
auth=("your-username", "your-password"),
timeout=60,
retries=5
)
Methods
get_records(endpoint: str, params: dict[str, str] | None = None) -> list[dict[str, Any]]
Retrieves all records from the specified API endpoint with automatic pagination.
This method handles OData pagination transparently, following @odata.nextLink until all records are retrieved. It first fetches the record count to provide progress information.
Parameters:
endpoint(str): The API endpoint path (e.g., "companies", "customers", "salesOrders"). Leading slashes are automatically handled.params(dict[str, str], optional): OData query parameters for filtering, sorting, expanding, etc.
Returns:
list[dict[str, Any]]: List of records as dictionaries, where each dictionary represents one entity with all its fields.
Raises:
InvalidResponseError: If the API returns invalid JSON or unexpected response format.FailedRequestError: If the HTTP request fails after all retry attempts.
Examples:
# Basic usage
companies = client.get_records("companies")
# With filtering
active_customers = client.get_records("customers", {
"$filter": "status eq 'active'",
"$orderby": "name",
"$top": "100"
})
# With expansion (related entities)
sales_orders = client.get_records("salesOrders", {
"$expand": "customer,salesOrderLines",
"$filter": "status eq 'open'"
})
Advanced Usage
Query Parameters
Dynite supports all standard OData query parameters:
# Filtering
params = {"$filter": "amount gt 1000 and date gt 2024-01-01"}
# Sorting
params = {"$orderby": "createdDate desc, amount asc"}
# Selecting specific fields
params = {"$select": "id,name,email,status"}
# Expanding related entities
params = {"$expand": "customer,salesOrderLines"}
# Pagination (handled automatically, but can be limited)
params = {"$top": "500"}
# Combining parameters
params = {
"$filter": "status eq 'active'",
"$orderby": "name",
"$select": "id,name,email",
"$expand": "contact",
"$top": "1000"
}
records = client.get_records("customers", params=params)
Error Handling
Dynite provides a comprehensive exception hierarchy for different error scenarios:
from dynite.exceptions import (
DyniteError, # Base exception for all Dynite errors
InvalidURLError, # Invalid base URL provided
InvalidResponseError, # Malformed API response
FailedRequestError # HTTP request failure
)
try:
records = client.get_records("companies")
print(f"Retrieved {len(records)} companies")
except InvalidURLError as e:
print(f"Invalid API URL: {e}")
# Check your base_url configuration
except FailedRequestError as e:
print(f"Request failed: {e}")
# Check network connectivity, credentials, or API availability
except InvalidResponseError as e:
print(f"Invalid API response: {e}")
# API returned unexpected data format
except DyniteError as e:
print(f"Dynite error: {e}")
# Catch-all for any Dynite-related error
Logging
Dynite uses Python's standard logging module. Configure logging to monitor API interactions:
import logging
# Basic logging
logging.basicConfig(level=logging.INFO)
# Detailed logging with timestamps
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# File logging
logging.basicConfig(
filename='dynite.log',
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# Create logger and set level
logger = logging.getLogger('dynite')
logger.setLevel(logging.DEBUG)
Environment Variables
Configure the client using environment variables for better security:
export BUSINESS_CENTRAL_URL="https://api.businesscentral.dynamics.com/v2.0/your-tenant-id/api/v2.0"
export BUSINESS_CENTRAL_USER="your_username"
export BUSINESS_CENTRAL_PASSWORD="your_password"
import os
from dynite import Dynite
client = Dynite(
base_url=os.getenv("BUSINESS_CENTRAL_URL"),
auth=(os.getenv("BUSINESS_CENTRAL_USER"), os.getenv("BUSINESS_CENTRAL_PASSWORD"))
)
Custom Session Configuration
For advanced use cases, you can modify the client's session:
from dynite import Dynite
import requests
client = Dynite(base_url="...", auth=("user", "pass"))
# Add custom headers
client.session.headers.update({
"User-Agent": "MyApp/1.0",
"X-Custom-Header": "value"
})
# Configure proxies
client.session.proxies = {
"https": "https://proxy.company.com:8080"
}
# Add custom authentication
from requests.auth import HTTPBasicAuth
client.session.auth = HTTPBasicAuth("user", "pass")
Configuration Options
Timeout Settings
# Short timeout for fast networks
client = Dynite(base_url="...", auth=("user", "pass"), timeout=10)
# Long timeout for slow networks
client = Dynite(base_url="...", auth=("user", "pass"), timeout=120)
Retry Configuration
# Aggressive retries for unreliable networks
client = Dynite(base_url="...", auth=("user", "pass"), retries=10)
# No retries for debugging
client = Dynite(base_url="...", auth=("user", "pass"), retries=0)
Business Central API Integration
Authentication
Dynite uses Basic Authentication with Business Central. Ensure your user account has appropriate permissions:
- Web Service Access Key: Generate a web service access key in Business Central
- User Permissions: Assign appropriate permissions to the user account
- Company Access: Ensure the user has access to the required companies
Common Endpoints
# Company management
companies = client.get_records("companies")
# Customer management
customers = client.get_records("customers")
customer_details = client.get_records("customers", params={"$expand": "customerFinancialDetails"})
# Vendor management
vendors = client.get_records("vendors")
# Item management
items = client.get_records("items")
item_variants = client.get_records("itemVariants")
# Sales orders
sales_orders = client.get_records("salesOrders", params={"$expand": "salesOrderLines"})
# Purchase orders
purchase_orders = client.get_records("purchaseOrders")
# General ledger entries
gl_entries = client.get_records("generalLedgerEntries")
# Dimensions
dimensions = client.get_records("dimensions")
Working with Large Datasets
Dynite automatically handles pagination, but you can optimize performance:
# Process records in batches to manage memory
all_customers = []
page_size = 1000
# Note: $top limits the total results, not page size
customers = client.get_records("customers", params={"$top": str(page_size)})
# Process in chunks
for i in range(0, len(customers), 100):
batch = customers[i:i+100]
# Process batch
process_customer_batch(batch)
Troubleshooting
Common Issues
Authentication Errors
Problem: FailedRequestError: 401 Unauthorized
Solutions:
- Verify username and password
- Check if web service access key is valid
- Ensure user has API permissions
- Confirm the account isn't locked
Invalid URL Errors
Problem: InvalidURLError: Invalid URL
Solutions:
- Verify the base URL format
- Ensure HTTPS is used
- Check tenant ID is correct
- Confirm the URL ends with the API version
Timeout Errors
Problem: FailedRequestError: Request timed out
Solutions:
- Increase timeout value:
Dynite(..., timeout=60) - Check network connectivity
- Verify Business Central service availability
- Consider API rate limiting
Large Dataset Issues
Problem: Memory errors with large result sets
Solutions:
- Use
$topparameter to limit results - Process records in batches
- Use
$selectto retrieve only needed fields - Implement pagination manually if needed
Debug Mode
Enable detailed logging for troubleshooting:
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('dynite')
logger.setLevel(logging.DEBUG)
# This will show:
# - URL construction
# - Request/response details
# - Pagination progress
# - Error details
Network Debugging
For network-level issues:
import requests
import logging
# Enable requests logging
logging.getLogger("requests.packages.urllib3").setLevel(logging.DEBUG)
logging.getLogger("requests.packages.urllib3.connectionpool").setLevel(logging.DEBUG)
Development
Prerequisites
- Python 3.13+
- uv (recommended) or pip
Setup Development Environment
# Clone repository
git clone https://github.com/prabhuakshay/dynite.git
cd dynite
# Install with development dependencies
uv pip install -e ".[dev]"
# Run all checks
task
Available Tasks
| Task | Description |
|---|---|
task format |
Format code with Ruff |
task lint |
Run linting checks |
task fix |
Auto-fix linting issues |
task type-check |
Run type checking |
task test |
Run test suite |
task test-cov |
Run tests with coverage |
task clean |
Clean cache files |
task pre-commit |
Run pre-commit hooks |
task |
Run all checks (default) |
Code Quality Tools
This project uses industry-standard tools for code quality:
- Ruff: Fast Python linter and formatter (replaces flake8, isort, black)
- BasedPyright: Strict type checking (Pyright fork)
- Pytest: Comprehensive test framework with coverage reporting
- Pre-commit: Git hooks for automated quality checks
Testing
# Run all tests
task test
# Run with coverage
task test-cov
# Run specific test file
pytest tests/test_exceptions.py
# Run specific test
pytest tests/test_exceptions.py::TestDyniteExceptions::test_dynite_error_inherits_exception
Contributing
We welcome contributions! Please follow these steps:
- Fork the repository on GitHub
- Create a feature branch:
git checkout -b feature/your-feature-name - Make your changes following the code style
- Add tests for new functionality
- Run all checks:
task - Commit your changes:
git commit -m "Add your feature" - Push to your fork:
git push origin feature/your-feature-name - Create a Pull Request on GitHub
Code Style
This project follows strict code quality standards:
- Google-style docstrings for all public functions/classes
- Type hints for all function parameters and return values
- 88 character line length (Black/Ruff standard)
- Descriptive variable names and comprehensive comments
- Comprehensive test coverage (aim for 90%+)
Release Process
- Update version in
pyproject.toml - Update
CHANGELOG.md - Create git tag:
git tag v1.2.3 - Push tag:
git push origin v1.2.3 - GitHub Actions will automatically publish to PyPI
Examples
Complete Customer Management Script
#!/usr/bin/env python3
"""
Complete example of customer data management with Dynite.
"""
import os
from dynite import Dynite
from dynite.exceptions import DyniteError
def main():
# Initialize client
client = Dynite(
base_url=os.getenv("BC_URL"),
auth=(os.getenv("BC_USER"), os.getenv("BC_PASSWORD")),
timeout=60
)
try:
# Get all active customers
customers = client.get_records("customers", {
"$filter": "status eq 'active'",
"$orderby": "name",
"$select": "id,name,email,phoneNumber",
"$expand": "contact"
})
print(f"Found {len(customers)} active customers")
# Process customers
for customer in customers:
print(f"- {customer['name']} ({customer['email']})")
# Get customer count
count_params = {"$filter": "status eq 'active'"}
# Note: This would require a separate method for count-only queries
# count = client.get_record_count("customers", count_params)
except DyniteError as e:
print(f"Error: {e}")
return 1
return 0
if __name__ == "__main__":
exit(main())
Sales Order Processing
from dynite import Dynite
from datetime import datetime, timedelta
client = Dynite(base_url="...", auth=("user", "pass"))
# Get recent sales orders
thirty_days_ago = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
orders = client.get_records("salesOrders", {
"$filter": f"orderDate gt {thirty_days_ago}",
"$expand": "customer,salesOrderLines",
"$orderby": "orderDate desc"
})
total_value = 0
for order in orders:
order_total = sum(line['lineAmount'] for line in order['salesOrderLines'])
total_value += order_total
print(f"Order {order['number']}: ${order_total:.2f}")
print(f"Total value of recent orders: ${total_value:.2f}")
FAQ
Q: Does Dynite support OAuth authentication?
A: Currently, Dynite only supports Basic Authentication. OAuth support may be added in future versions.
Q: Can I use Dynite with on-premises Business Central?
A: Yes, as long as the Business Central instance exposes the OData v4.0 API over HTTP/HTTPS.
Q: How does pagination work?
A: Dynite automatically follows OData @odata.nextLink responses to retrieve all pages. You don't need to handle pagination manually.
Q: What are the rate limits?
A: Rate limits depend on your Business Central licensing and configuration. Dynite includes retry logic for transient failures.
Q: Can I modify data (POST/PUT/DELETE)?
A: Currently, Dynite only supports read operations (GET). Write operations may be added in future versions.
Q: How do I handle large result sets?
A: Use the $top parameter to limit results, and process data in batches to manage memory usage.
Version History
Please see CHANGELOG for version history
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Contributing
See the Contributing section above for detailed guidelines.
Project details
Release history Release notifications | RSS feed
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 dynite-0.2.0.tar.gz.
File metadata
- Download URL: dynite-0.2.0.tar.gz
- Upload date:
- Size: 46.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.22 {"installer":{"name":"uv","version":"0.9.22","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 |
d540ae194ce1560bbd543075f6409347b191bdac40f541040085fc019e031cc5
|
|
| MD5 |
da25c9d342ade092dee2c164ccecb163
|
|
| BLAKE2b-256 |
47e7f815d9b7b8c2c7cfe03afe5ccff92850ca9384ef8fc8ccf4585f9c392030
|
File details
Details for the file dynite-0.2.0-py3-none-any.whl.
File metadata
- Download URL: dynite-0.2.0-py3-none-any.whl
- Upload date:
- Size: 15.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.22 {"installer":{"name":"uv","version":"0.9.22","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 |
eddf2a1d96e5acccb30d3c3f1568243954eaaf59466f54855a3d33f8e16f0a5d
|
|
| MD5 |
d35976566bd55f31662ace15ede4a6e0
|
|
| BLAKE2b-256 |
6270c73c2c83ccb27f3a1078bdb28a97bc28500001e94e8a6cdf3df004b7b4e1
|