# Microsoft Dataverse SDK for Python
[](https://pypi.org/project/crmadminbrasil-crmadminbrasil-dataverse-sdk/)
[](https://pypi.org/project/crmadminbrasil-crmadminbrasil-dataverse-sdk/)
[](https://opensource.org/licenses/MIT)
[](https://github.com/psf/black)
[](https://github.com/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk/actions)
[](https://codecov.io/gh/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk)
A comprehensive, enterprise-ready Python SDK for Microsoft Dataverse with async support, advanced features, and production-grade reliability.
## 🚀 Features
### Core Capabilities
- **100% Async Support**: Built with `httpx` and `asyncio` for high-performance async operations
- **Enterprise Ready**: Connection pooling, retry logic, rate limiting, and comprehensive error handling
- **Type Safety**: Full type hints with Pydantic models for strong typing and validation
- **Extensible**: Hook system for custom logging, telemetry, and request/response interceptors
### Operations
- **Complete CRUD**: Create, Read, Update, Delete, and Upsert operations
- **Bulk Operations**: High-performance batch processing with auto-chunking and parallel execution
- **Advanced Queries**: OData queries, FetchXML support, and intelligent pagination
- **Associations**: Entity relationship management (associate/disassociate)
- **Metadata**: Entity and attribute metadata retrieval
- **File Operations**: Attachment upload/download support
### Developer Experience
- **CLI Tool**: Full-featured command-line interface for all operations
- **Rich Documentation**: Comprehensive docs with examples and best practices
- **Testing**: Extensive test suite with unit and integration tests
- **CI/CD Ready**: GitHub Actions workflows and PyPI publishing automation
## 📦 Installation
### From PyPI (Recommended)
```bash
pip install crmadminbrasil-dataverse-sdk
```
### Development Installation
```bash
git clone https://github.com/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk.git
cd crmadminbrasil-dataverse-sdk
pip install -e ".[dev]"
```
### Optional Dependencies
```bash
# For telemetry support
pip install "crmadminbrasil-dataverse-sdk[telemetry]"
# For documentation
pip install "crmadminbrasil-dataverse-sdk[docs]"
# All optional dependencies
pip install "crmadminbrasil-dataverse-sdk[dev,telemetry,docs]"
```
## 🔧 Quick Start
### Basic Usage
```python
import asyncio
from dataverse_sdk import DataverseSDK
async def main():
# Initialize SDK
sdk = DataverseSDK(
dataverse_url="https://yourorg.crm.dynamics.com",
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
)
async with sdk:
# Create an account
account_data = {
"name": "Contoso Ltd",
"websiteurl": "https://contoso.com",
"telephone1": "555-0123"
}
account_id = await sdk.create("accounts", account_data)
print(f"Created account: {account_id}")
# Query accounts
accounts = await sdk.query("accounts", {
"select": ["name", "websiteurl", "telephone1"],
"filter": "statecode eq 0",
"top": 10
})
for account in accounts.value:
print(f"Account: {account['name']}")
if __name__ == "__main__":
asyncio.run(main())
```
### Environment Configuration
Create a `.env` file:
```env
DATAVERSE_URL=https://yourorg.crm.dynamics.com
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
AZURE_TENANT_ID=your-tenant-id
```
The SDK automatically loads environment variables:
```python
from dataverse_sdk import DataverseSDK
# Configuration loaded from environment
sdk = DataverseSDK()
```
## 📚 Documentation
### Table of Contents
- [Authentication](#authentication)
- [CRUD Operations](#crud-operations)
- [Query Operations](#query-operations)
- [Bulk Operations](#bulk-operations)
- [FetchXML Queries](#fetchxml-queries)
- [Entity Associations](#entity-associations)
- [Metadata Operations](#metadata-operations)
- [CLI Usage](#cli-usage)
- [Configuration](#configuration)
- [Error Handling](#error-handling)
- [Hooks and Extensibility](#hooks-and-extensibility)
- [Performance Optimization](#performance-optimization)
- [Testing](#testing)
- [Contributing](#contributing)
## 🔐 Authentication
The SDK supports multiple authentication flows for different scenarios:
### Client Credentials Flow (Service-to-Service)
Best for server applications and automation:
```python
from dataverse_sdk import DataverseSDK
sdk = DataverseSDK(
dataverse_url="https://yourorg.crm.dynamics.com",
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
)
async with sdk:
# SDK automatically uses client credentials flow
accounts = await sdk.query("accounts", {"top": 5})
```
### Device Code Flow (CLI Applications)
Best for command-line tools and development:
```python
from dataverse_sdk.auth import DataverseAuthenticator
authenticator = DataverseAuthenticator(
client_id="your-client-id",
tenant_id="your-tenant-id",
dataverse_url="https://yourorg.crm.dynamics.com",
)
# This will prompt user to visit a URL and enter a code
token = await authenticator.authenticate_device_code()
```
### Interactive Flow (Desktop Applications)
For applications with user interaction:
```python
# Interactive flow with local redirect
token = await authenticator.authenticate_interactive(
redirect_uri="http://localhost:8080",
port=8080
)
```
### Token Caching
The SDK automatically caches tokens to minimize authentication requests:
```python
# Tokens are cached automatically
sdk = DataverseSDK(...)
async with sdk:
# First request authenticates and caches token
await sdk.query("accounts", {"top": 1})
# Subsequent requests use cached token
await sdk.query("contacts", {"top": 1})
# Clear cache if needed
await sdk.clear_auth_cache()
```
## 📝 CRUD Operations
### Create Operations
```python
# Create a single entity
account_data = {
"name": "Acme Corporation",
"websiteurl": "https://acme.com",
"telephone1": "555-0123",
"description": "Leading provider of innovative solutions"
}
account_id = await sdk.create("accounts", account_data)
print(f"Created account: {account_id}")
# Create with return data
account = await sdk.create("accounts", account_data, return_record=True)
print(f"Created account: {account['name']} ({account['accountid']})")
```
### Read Operations
```python
# Read by ID
account = await sdk.read("accounts", account_id)
print(f"Account name: {account['name']}")
# Read with specific fields
account = await sdk.read(
"accounts",
account_id,
select=["name", "websiteurl", "telephone1"]
)
# Read with related entities
account = await sdk.read(
"accounts",
account_id,
expand=["primarycontactid", "createdby"]
)
```
### Update Operations
```python
# Update entity
update_data = {
"websiteurl": "https://newacme.com",
"description": "Updated description"
}
await sdk.update("accounts", account_id, update_data)
# Update with return data
updated_account = await sdk.update(
"accounts",
account_id,
update_data,
return_record=True
)
```
### Delete Operations
```python
# Delete entity
await sdk.delete("accounts", account_id)
```
### Upsert Operations
```python
# Upsert (create or update)
account_data = {
"name": "Upsert Test Account",
"websiteurl": "https://upsert.com"
}
result = await sdk.upsert("accounts", account_data)
if result.was_created:
print(f"Created new account: {result.entity_id}")
else:
print(f"Updated existing account: {result.entity_id}")
# Upsert with alternate key
result = await sdk.upsert(
"accounts",
account_data,
alternate_key={"accountnumber": "ACC-001"}
)
```
## 🔍 Query Operations
### Basic Queries
```python
from dataverse_sdk.models import QueryOptions
# Simple query
accounts = await sdk.query("accounts", {
"select": ["name", "websiteurl"],
"top": 10
})
for account in accounts.value:
print(f"{account['name']}: {account['websiteurl']}")
# Using QueryOptions model
options = QueryOptions(
select=["name", "websiteurl", "telephone1"],
filter="statecode eq 0",
order_by=["name asc"],
top=20
)
accounts = await sdk.query("accounts", options)
```
### Advanced Filtering
```python
# Complex filters
accounts = await sdk.query("accounts", {
"select": ["name", "revenue"],
"filter": "revenue gt 1000000 and statecode eq 0",
"order_by": ["revenue desc"]
})
# String operations
accounts = await sdk.query("accounts", {
"select": ["name"],
"filter": "contains(name, 'Microsoft') or startswith(name, 'Contoso')"
})
# Date filtering
from datetime import datetime, timedelta
last_month = datetime.now() - timedelta(days=30)
recent_accounts = await sdk.query("accounts", {
"select": ["name", "createdon"],
"filter": f"createdon gt {last_month.isoformat()}"
})
```
### Pagination
```python
# Manual pagination
result = await sdk.query("accounts", {
"select": ["name"],
"top": 100
})
all_accounts = result.value
while result.has_more:
# Get next page
result = await sdk.query("accounts", {
"select": ["name"],
"top": 100,
"skip": len(all_accounts)
})
all_accounts.extend(result.value)
# Automatic pagination
all_accounts = await sdk.query_all("accounts", {
"select": ["name", "websiteurl"],
"filter": "statecode eq 0"
}, max_records=1000)
```
### Related Entity Expansion
```python
# Expand related entities
accounts = await sdk.query("accounts", {
"select": ["name", "websiteurl"],
"expand": [
"primarycontactid($select=fullname,emailaddress1)",
"createdby($select=fullname)"
],
"top": 10
})
for account in accounts.value:
print(f"Account: {account['name']}")
if account.get('primarycontactid'):
contact = account['primarycontactid']
print(f" Primary Contact: {contact['fullname']}")
```
## ⚡ Bulk Operations
### Bulk Create
```python
# Prepare data
contacts = [
{
"firstname": "John",
"lastname": "Doe",
"emailaddress1": "john.doe@example.com"
},
{
"firstname": "Jane",
"lastname": "Smith",
"emailaddress1": "jane.smith@example.com"
},
# ... more contacts
]
# Bulk create with automatic batching
result = await sdk.bulk_create(
"contacts",
contacts,
batch_size=100, # Process in batches of 100
parallel=True # Execute batches in parallel
)
print(f"Processed: {result.total_processed}")
print(f"Successful: {result.successful}")
print(f"Failed: {result.failed}")
print(f"Success rate: {result.success_rate:.1f}%")
if result.has_errors:
print("Errors:")
for error in result.errors[:5]: # Show first 5 errors
print(f" - {error}")
```
### Bulk Update
```python
# Prepare updates (must include entity ID)
updates = [
{
"id": "contact-id-1",
"jobtitle": "Senior Developer"
},
{
"id": "contact-id-2",
"jobtitle": "Project Manager"
},
# ... more updates
]
result = await sdk.bulk_update("contacts", updates)
```
### Bulk Delete
```python
# Delete multiple entities
contact_ids = [
"contact-id-1",
"contact-id-2",
"contact-id-3",
# ... more IDs
]
result = await sdk.bulk_delete("contacts", contact_ids)
```
### Custom Batch Operations
```python
from dataverse_sdk.batch import BatchProcessor
# Create custom batch processor
batch_processor = BatchProcessor(
client=sdk.client,
default_batch_size=50,
max_parallel_batches=3
)
# Custom operations
operations = [
{
"method": "POST",
"url": "accounts",
"body": {"name": "Account 1"}
},
{
"method": "PATCH",
"url": "accounts(existing-id)",
"body": {"description": "Updated"}
},
{
"method": "DELETE",
"url": "accounts(delete-id)"
}
]
result = await batch_processor.execute_bulk_operation(
operations,
parallel=True,
transactional=False
)
```
## 📊 FetchXML Queries
### Basic FetchXML
```python
# Execute FetchXML string
fetchxml = """
<fetch top="10">
<entity name="account">
<attribute name="name" />
<attribute name="websiteurl" />
<attribute name="telephone1" />
<filter type="and">
<condition attribute="statecode" operator="eq" value="0" />
<condition attribute="revenue" operator="gt" value="1000000" />
</filter>
<order attribute="revenue" descending="true" />
</entity>
</fetch>
"""
accounts = await sdk.fetch_xml(fetchxml)
for account in accounts:
print(f"{account['name']}: {account.get('revenue', 'N/A')}")
```
### FetchXML with Linked Entities
```python
fetchxml = """
<fetch top="5">
<entity name="account">
<attribute name="name" />
<attribute name="websiteurl" />
<link-entity name="contact" from="parentcustomerid" to="accountid" alias="contact">
<attribute name="fullname" />
<attribute name="emailaddress1" />
<filter type="and">
<condition attribute="statecode" operator="eq" value="0" />
</filter>
</link-entity>
<filter type="and">
<condition attribute="statecode" operator="eq" value="0" />
</filter>
</entity>
</fetch>
"""
results = await sdk.fetch_xml(fetchxml)
```
### FetchXML Builder (Programmatic)
```python
from dataverse_sdk.models import FetchXMLQuery
# Build FetchXML programmatically
query = FetchXMLQuery(
entity="account",
attributes=["name", "websiteurl", "revenue"],
filters=[{
"type": "and",
"conditions": [
{"attribute": "statecode", "operator": "eq", "value": "0"},
{"attribute": "revenue", "operator": "gt", "value": "1000000"}
]
}],
orders=[
{"attribute": "revenue", "descending": True}
],
top=10
)
# Convert to FetchXML and execute
fetchxml_string = query.to_fetchxml()
accounts = await sdk.fetch_xml(fetchxml_string)
```
## 🔗 Entity Associations
### Associate Entities
```python
# Associate account with contact
await sdk.associate(
primary_entity_type="accounts",
primary_entity_id=account_id,
relationship_name="account_primary_contact",
related_entity_type="contacts",
related_entity_id=contact_id
)
# Many-to-many association
await sdk.associate(
primary_entity_type="systemusers",
primary_entity_id=user_id,
relationship_name="systemuserroles_association",
related_entity_type="roles",
related_entity_id=role_id
)
```
### Disassociate Entities
```python
# Remove association
await sdk.disassociate(
primary_entity_type="accounts",
primary_entity_id=account_id,
relationship_name="account_primary_contact"
)
# Remove specific many-to-many association
await sdk.disassociate(
primary_entity_type="systemusers",
primary_entity_id=user_id,
relationship_name="systemuserroles_association",
related_entity_id=role_id
)
```
## 🔍 Metadata Operations
### Entity Metadata
```python
# Get entity metadata
account_metadata = await sdk.get_entity_metadata("account")
print(f"Display Name: {account_metadata['DisplayName']['UserLocalizedLabel']['Label']}")
print(f"Logical Name: {account_metadata['LogicalName']}")
print(f"Primary Key: {account_metadata['PrimaryIdAttribute']}")
print(f"Primary Name: {account_metadata['PrimaryNameAttribute']}")
# List all attributes
for attr in account_metadata['Attributes']:
print(f" {attr['LogicalName']}: {attr['AttributeType']}")
```
### Attribute Metadata
```python
# Get specific attribute metadata
name_attr = await sdk.get_attribute_metadata("account", "name")
print(f"Display Name: {name_attr['DisplayName']['UserLocalizedLabel']['Label']}")
print(f"Type: {name_attr['AttributeType']}")
print(f"Max Length: {name_attr.get('MaxLength', 'N/A')}")
print(f"Required: {name_attr['RequiredLevel']['Value']}")
```
### Generate Entity Models
```python
# Generate Pydantic models from metadata (utility function)
from dataverse_sdk.utils import generate_entity_model
# This would generate a Pydantic model class
AccountModel = await generate_entity_model(sdk, "account")
# Use the generated model
account_data = {
"name": "Test Account",
"websiteurl": "https://test.com"
}
# Validate data with the model
account = AccountModel(**account_data)
```
## 🖥️ CLI Usage
The SDK includes a powerful command-line interface for all operations:
### Installation and Setup
```bash
# Install the SDK
pip install crmadminbrasil-dataverse-sdk
# Initialize configuration
dv-cli config init
# Follow prompts to enter your Dataverse URL, Client ID, etc.
# Test connection
dv-cli config test
```
### Entity Operations
```bash
# List entities
dv-cli entity list accounts --select name,websiteurl --top 10
dv-cli entity list contacts --filter "statecode eq 0" --order-by createdon
# Get specific entity
dv-cli entity get accounts 12345678-1234-1234-1234-123456789012
# Create entity
dv-cli entity create accounts --file account_data.json
echo '{"name": "CLI Test Account"}' | dv-cli entity create accounts
# Update entity
dv-cli entity update accounts 12345678-1234-1234-1234-123456789012 --file updates.json
# Delete entity
dv-cli entity delete accounts 12345678-1234-1234-1234-123456789012 --yes
```
### Bulk Operations
```bash
# Bulk create from JSON file
dv-cli bulk create contacts --file contacts.json --batch-size 100
# Bulk operations with progress
dv-cli bulk create accounts --file large_accounts.json --parallel
```
### Data Export/Import
```bash
# Export data
dv-cli data export accounts --output accounts_backup.json
dv-cli data export contacts --filter "statecode eq 0" --select firstname,lastname,emailaddress1
# Import data
dv-cli data import accounts --file accounts_backup.json
```
### FetchXML Operations
```bash
# Execute FetchXML from file
dv-cli fetchxml execute --file complex_query.xml
# Save FetchXML results
dv-cli fetchxml execute --file query.xml --output results.json
```
### Configuration Management
```bash
# View current configuration
dv-cli config show
# Update configuration
dv-cli config set dataverse_url https://neworg.crm.dynamics.com
dv-cli config set log_level DEBUG
# Use different config file
dv-cli --config-file prod-config.json entity list accounts
```
### Output Formats
```bash
# Table format (default)
dv-cli entity list accounts --top 5
# JSON format
dv-cli entity list accounts --top 5 --output json
# Save to file
dv-cli entity list accounts --output json > accounts.json
```
## ⚙️ Configuration
### Environment Variables
```bash
# Required
export DATAVERSE_URL="https://yourorg.crm.dynamics.com"
export AZURE_CLIENT_ID="your-client-id"
export AZURE_TENANT_ID="your-tenant-id"
# Optional
export AZURE_CLIENT_SECRET="your-client-secret"
export AZURE_AUTHORITY="https://login.microsoftonline.com/your-tenant-id"
export AZURE_SCOPE="https://yourorg.crm.dynamics.com/.default"
# SDK Configuration
export MAX_CONNECTIONS=100
export MAX_RETRIES=3
export DEFAULT_BATCH_SIZE=100
export LOG_LEVEL=INFO
```
### Configuration File
Create `dataverse-config.json`:
```json
{
"dataverse_url": "https://yourorg.crm.dynamics.com",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"tenant_id": "your-tenant-id",
"max_connections": 100,
"max_retries": 3,
"default_batch_size": 100,
"log_level": "INFO"
}
```
### Programmatic Configuration
```python
from dataverse_sdk import DataverseSDK
from dataverse_sdk.utils import Config
# Custom configuration
config = Config(
max_connections=50,
max_retries=5,
default_batch_size=200,
connect_timeout=15.0,
read_timeout=60.0,
debug=True
)
sdk = DataverseSDK(
dataverse_url="https://yourorg.crm.dynamics.com",
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
config=config
)
```
### Multi-Environment Setup
```python
# Development environment
dev_sdk = DataverseSDK(
dataverse_url="https://dev-org.crm.dynamics.com",
client_id="dev-client-id",
client_secret="dev-client-secret",
tenant_id="dev-tenant-id"
)
# Production environment
prod_sdk = DataverseSDK(
dataverse_url="https://prod-org.crm.dynamics.com",
client_id="prod-client-id",
client_secret="prod-client-secret",
tenant_id="prod-tenant-id"
)
# Use different configurations
async def sync_data():
async with dev_sdk as dev, prod_sdk as prod:
# Get data from dev
dev_accounts = await dev.query_all("accounts", {"select": ["name"]})
# Create in prod
await prod.bulk_create("accounts", dev_accounts)
```
## 🚨 Error Handling
### Exception Types
```python
from dataverse_sdk.exceptions import (
DataverseSDKError, # Base exception
AuthenticationError, # Authentication failures
AuthorizationError, # Permission issues
ConnectionError, # Network connectivity
TimeoutError, # Request timeouts
RateLimitError, # Rate limiting
ValidationError, # Data validation
EntityNotFoundError, # Entity not found
APIError, # API errors
BatchOperationError, # Batch operation failures
)
# Specific error handling
try:
account = await sdk.read("accounts", "invalid-id")
except EntityNotFoundError as e:
print(f"Account not found: {e.entity_id}")
except AuthenticationError as e:
print(f"Authentication failed: {e.message}")
except APIError as e:
print(f"API error {e.status_code}: {e.message}")
print(f"Error details: {e.response_data}")
```
### Retry and Rate Limiting
```python
from dataverse_sdk.exceptions import RateLimitError
import asyncio
async def robust_operation():
max_attempts = 3
attempt = 0
while attempt < max_attempts:
try:
result = await sdk.query("accounts", {"top": 1000})
return result
except RateLimitError as e:
attempt += 1
if attempt >= max_attempts:
raise
# Wait for the suggested retry time
wait_time = e.retry_after or 60
print(f"Rate limited. Waiting {wait_time} seconds...")
await asyncio.sleep(wait_time)
except ConnectionError as e:
attempt += 1
if attempt >= max_attempts:
raise
# Exponential backoff for connection errors
wait_time = 2 ** attempt
print(f"Connection error. Retrying in {wait_time} seconds...")
await asyncio.sleep(wait_time)
```
### Comprehensive Error Handling
```python
async def safe_bulk_operation(entities):
try:
result = await sdk.bulk_create("accounts", entities)
if result.has_errors:
print(f"Bulk operation completed with {result.failed} errors:")
for error in result.errors:
print(f" - {error}")
return result
except BatchOperationError as e:
print(f"Batch operation failed: {e.message}")
print(f"Failed operations: {len(e.failed_operations)}")
# Retry failed operations individually
for failed_op in e.failed_operations:
try:
await sdk.create("accounts", failed_op["data"])
except Exception as retry_error:
print(f"Retry failed: {retry_error}")
except Exception as e:
print(f"Unexpected error: {e}")
raise
```
## 🔌 Hooks and Extensibility
### Built-in Hooks
```python
from dataverse_sdk.hooks import (
HookType,
logging_hook,
telemetry_hook,
retry_logging_hook
)
# Register built-in hooks
sdk.register_hook(HookType.BEFORE_REQUEST, logging_hook)
sdk.register_hook(HookType.AFTER_RESPONSE, telemetry_hook)
sdk.register_hook(HookType.ON_RETRY, retry_logging_hook)
```
### Custom Hooks
```python
from dataverse_sdk.hooks import HookContext, HookType
def custom_request_hook(context: HookContext) -> None:
"""Custom hook to modify requests."""
if context.hook_type == HookType.BEFORE_REQUEST:
# Add custom headers
context.request_data["headers"]["X-Custom-Header"] = "MyValue"
# Log request details
print(f"Making request to: {context.request_data['url']}")
def custom_response_hook(context: HookContext) -> None:
"""Custom hook to process responses."""
if context.hook_type == HookType.AFTER_RESPONSE:
# Log response time
response_time = context.metadata.get("response_time", 0)
print(f"Request completed in {response_time:.2f}s")
# Store metrics
context.set_custom_data("response_time", response_time)
# Register custom hooks
sdk.register_hook(HookType.BEFORE_REQUEST, custom_request_hook, priority=10)
sdk.register_hook(HookType.AFTER_RESPONSE, custom_response_hook)
```
### Async Hooks
```python
async def async_telemetry_hook(context: HookContext) -> None:
"""Async hook for telemetry."""
if context.hook_type == HookType.AFTER_RESPONSE:
# Send telemetry data asynchronously
telemetry_data = {
"url": context.request_data["url"],
"method": context.request_data["method"],
"status_code": context.response_data["status_code"],
"response_time": context.metadata.get("response_time")
}
# Send to telemetry service (example)
await send_telemetry(telemetry_data)
# Register async hook
sdk.register_hook(HookType.AFTER_RESPONSE, async_telemetry_hook)
```
### Hook Decorators
```python
from dataverse_sdk.hooks import hook, HookType
@hook(HookType.BEFORE_REQUEST, priority=5)
def request_validator(context: HookContext) -> None:
"""Validate requests before sending."""
url = context.request_data["url"]
method = context.request_data["method"]
# Custom validation logic
if method == "DELETE" and "accounts" in url:
print("Warning: Deleting account!")
@hook(HookType.ON_ERROR)
def error_notifier(context: HookContext) -> None:
"""Notify on errors."""
error = context.error
url = context.request_data["url"]
# Send notification (example)
send_error_notification(f"Error in {url}: {error}")
```
### OpenTelemetry Integration
```python
from opentelemetry import trace
from dataverse_sdk.hooks import HookContext, HookType
tracer = trace.get_tracer(__name__)
def opentelemetry_hook(context: HookContext) -> None:
"""OpenTelemetry integration hook."""
if context.hook_type == HookType.BEFORE_REQUEST:
# Start span
span = tracer.start_span(f"dataverse_{context.request_data['method']}")
span.set_attribute("http.url", context.request_data["url"])
span.set_attribute("http.method", context.request_data["method"])
context.set_custom_data("span", span)
elif context.hook_type == HookType.AFTER_RESPONSE:
# End span
span = context.get_custom_data("span")
if span:
span.set_attribute("http.status_code", context.response_data["status_code"])
span.end()
# Register OpenTelemetry hook
sdk.register_hook(HookType.BEFORE_REQUEST, opentelemetry_hook)
sdk.register_hook(HookType.AFTER_RESPONSE, opentelemetry_hook)
```
## ⚡ Performance Optimization
### Connection Pooling
```python
from dataverse_sdk.utils import Config
# Optimize connection settings
config = Config(
max_connections=200, # Total connection pool size
max_keepalive_connections=50, # Keep-alive connections
keepalive_expiry=30, # Keep-alive timeout (seconds)
connect_timeout=10.0, # Connection timeout
read_timeout=30.0, # Read timeout
)
sdk = DataverseSDK(config=config)
```
### Batch Size Optimization
```python
# Optimize batch sizes based on data size
small_records = [...] # Small records
large_records = [...] # Records with many fields
# Use larger batches for small records
await sdk.bulk_create("contacts", small_records, batch_size=500)
# Use smaller batches for large records
await sdk.bulk_create("accounts", large_records, batch_size=50)
```
### Parallel Processing
```python
import asyncio
async def parallel_queries():
# Execute multiple queries in parallel
tasks = [
sdk.query("accounts", {"select": ["name"], "top": 100}),
sdk.query("contacts", {"select": ["fullname"], "top": 100}),
sdk.query("opportunities", {"select": ["name"], "top": 100}),
]
results = await asyncio.gather(*tasks)
accounts, contacts, opportunities = results
return {
"accounts": accounts.value,
"contacts": contacts.value,
"opportunities": opportunities.value
}
# Execute parallel queries
data = await parallel_queries()
```
### Memory-Efficient Streaming
```python
async def process_large_dataset():
"""Process large datasets efficiently."""
page_size = 1000
processed_count = 0
# Process in chunks to avoid memory issues
options = QueryOptions(
select=["accountid", "name"],
top=page_size
)
while True:
result = await sdk.query("accounts", options)
if not result.value:
break
# Process current batch
for account in result.value:
# Process individual account
await process_account(account)
processed_count += 1
# Check for more data
if not result.has_more:
break
# Update options for next page
options.skip = processed_count
print(f"Processed {processed_count} accounts")
```
### Caching Strategies
```python
from functools import lru_cache
import asyncio
class CachedDataverseSDK:
def __init__(self, sdk):
self.sdk = sdk
self._metadata_cache = {}
@lru_cache(maxsize=100)
async def get_cached_entity_metadata(self, entity_type: str):
"""Cache entity metadata to avoid repeated API calls."""
if entity_type not in self._metadata_cache:
metadata = await self.sdk.get_entity_metadata(entity_type)
self._metadata_cache[entity_type] = metadata
return self._metadata_cache[entity_type]
async def bulk_create_with_validation(self, entity_type: str, entities: list):
"""Bulk create with cached metadata validation."""
# Get cached metadata
metadata = await self.get_cached_entity_metadata(entity_type)
# Validate entities against metadata
validated_entities = []
for entity in entities:
if self._validate_entity(entity, metadata):
validated_entities.append(entity)
# Bulk create validated entities
return await self.sdk.bulk_create(entity_type, validated_entities)
# Use cached SDK
cached_sdk = CachedDataverseSDK(sdk)
```
## 🧪 Testing
### Unit Tests
```python
import pytest
from unittest.mock import AsyncMock, patch
from dataverse_sdk import DataverseSDK
@pytest.mark.asyncio
async def test_account_creation():
"""Test account creation."""
sdk = DataverseSDK(
dataverse_url="https://test.crm.dynamics.com",
client_id="test-client",
tenant_id="test-tenant"
)
# Mock the create method
sdk.create = AsyncMock(return_value="12345678-1234-1234-1234-123456789012")
account_data = {"name": "Test Account"}
account_id = await sdk.create("accounts", account_data)
assert account_id == "12345678-1234-1234-1234-123456789012"
sdk.create.assert_called_once_with("accounts", account_data)
@pytest.mark.asyncio
async def test_query_with_filter():
"""Test query with filter."""
sdk = DataverseSDK(
dataverse_url="https://test.crm.dynamics.com",
client_id="test-client",
tenant_id="test-tenant"
)
# Mock query response
mock_response = {
"value": [{"accountid": "123", "name": "Test Account"}],
"@odata.count": 1
}
with patch.object(sdk, 'query', return_value=mock_response):
result = await sdk.query("accounts", {"filter": "name eq 'Test'"})
assert len(result["value"]) == 1
assert result["value"][0]["name"] == "Test Account"
```
### Integration Tests
```python
import pytest
import os
from dataverse_sdk import DataverseSDK
# Skip if no credentials
pytestmark = pytest.mark.skipif(
not os.getenv("DATAVERSE_URL"),
reason="Integration tests require Dataverse credentials"
)
@pytest.fixture
async def sdk():
"""SDK fixture for integration tests."""
sdk_instance = DataverseSDK()
async with sdk_instance as sdk:
yield sdk
@pytest.mark.asyncio
async def test_real_account_operations(sdk):
"""Test real account operations."""
# Create test account
account_data = {
"name": "Integration Test Account",
"description": "Created by integration test"
}
account_id = await sdk.create("accounts", account_data)
try:
# Read account
account = await sdk.read("accounts", account_id)
assert account["name"] == account_data["name"]
# Update account
await sdk.update("accounts", account_id, {"description": "Updated"})
# Verify update
updated_account = await sdk.read("accounts", account_id)
assert updated_account["description"] == "Updated"
finally:
# Clean up
await sdk.delete("accounts", account_id)
```
### Performance Tests
```python
import pytest
import time
from dataverse_sdk import DataverseSDK
@pytest.mark.slow
@pytest.mark.asyncio
async def test_bulk_operation_performance(sdk):
"""Test bulk operation performance."""
# Create test data
test_contacts = [
{"firstname": f"Test{i}", "lastname": "Contact"}
for i in range(100)
]
start_time = time.time()
# Bulk create
result = await sdk.bulk_create("contacts", test_contacts)
end_time = time.time()
duration = end_time - start_time
# Performance assertions
assert duration < 30.0 # Should complete within 30 seconds
assert result.success_rate > 90.0 # At least 90% success rate
print(f"Bulk created {result.successful} contacts in {duration:.2f}s")
```
### Running Tests
```bash
# Install test dependencies
pip install -e ".[dev]"
# Run unit tests
pytest tests/unit/
# Run integration tests (requires credentials)
pytest tests/integration/
# Run with coverage
pytest --cov=dataverse_sdk --cov-report=html
# Run performance tests
pytest -m slow
# Run specific test
pytest tests/unit/test_auth.py::TestDataverseAuthenticator::test_client_credentials
```
## 📚 Advanced Examples
### Data Migration Script
```python
import asyncio
from dataverse_sdk import DataverseSDK
async def migrate_accounts():
"""Migrate accounts from one environment to another."""
# Source environment
source_sdk = DataverseSDK(
dataverse_url="https://source.crm.dynamics.com",
client_id="source-client-id",
client_secret="source-secret",
tenant_id="source-tenant"
)
# Target environment
target_sdk = DataverseSDK(
dataverse_url="https://target.crm.dynamics.com",
client_id="target-client-id",
client_secret="target-secret",
tenant_id="target-tenant"
)
async with source_sdk as source, target_sdk as target:
# Export accounts from source
print("Exporting accounts from source...")
accounts = await source.query_all("accounts", {
"select": ["name", "websiteurl", "telephone1", "description"],
"filter": "statecode eq 0"
})
print(f"Found {len(accounts)} accounts to migrate")
# Import to target
print("Importing accounts to target...")
result = await target.bulk_create("accounts", accounts, batch_size=100)
print(f"Migration completed:")
print(f" Successful: {result.successful}")
print(f" Failed: {result.failed}")
print(f" Success rate: {result.success_rate:.1f}%")
if __name__ == "__main__":
asyncio.run(migrate_accounts())
```
### Data Synchronization
```python
import asyncio
from datetime import datetime, timedelta
from dataverse_sdk import DataverseSDK
class DataSynchronizer:
def __init__(self, primary_sdk, secondary_sdk):
self.primary = primary_sdk
self.secondary = secondary_sdk
async def sync_entity(self, entity_type: str, sync_field: str = "modifiedon"):
"""Sync entities based on modification date."""
# Get last sync time (stored somewhere)
last_sync = await self.get_last_sync_time(entity_type)
# Query modified records from primary
filter_expr = f"{sync_field} gt {last_sync.isoformat()}"
modified_records = await self.primary.query_all(entity_type, {
"filter": filter_expr,
"order_by": [f"{sync_field} asc"]
})
if not modified_records:
print(f"No modified {entity_type} found")
return
print(f"Syncing {len(modified_records)} modified {entity_type}")
# Upsert to secondary (assuming alternate key exists)
for record in modified_records:
try:
await self.secondary.upsert(
entity_type,
record,
alternate_key={"name": record["name"]} # Adjust key as needed
)
except Exception as e:
print(f"Failed to sync {record.get('name', 'unknown')}: {e}")
# Update last sync time
await self.update_last_sync_time(entity_type, datetime.now())
async def get_last_sync_time(self, entity_type: str) -> datetime:
"""Get last sync time (implement based on your storage)."""
# This could be stored in a database, file, or Dataverse itself
return datetime.now() - timedelta(hours=1) # Default to 1 hour ago
async def update_last_sync_time(self, entity_type: str, sync_time: datetime):
"""Update last sync time (implement based on your storage)."""
pass
async def run_sync():
primary_sdk = DataverseSDK(...) # Primary environment
secondary_sdk = DataverseSDK(...) # Secondary environment
async with primary_sdk as primary, secondary_sdk as secondary:
synchronizer = DataSynchronizer(primary, secondary)
# Sync different entity types
await synchronizer.sync_entity("accounts")
await synchronizer.sync_entity("contacts")
await synchronizer.sync_entity("opportunities")
if __name__ == "__main__":
asyncio.run(run_sync())
```
### Custom Entity Manager
```python
from dataverse_sdk import DataverseSDK
from dataverse_sdk.models import Entity
from typing import List, Optional
class EntityManager:
"""High-level entity manager with business logic."""
def __init__(self, sdk: DataverseSDK):
self.sdk = sdk
async def create_account_with_contacts(
self,
account_data: dict,
contacts_data: List[dict]
) -> dict:
"""Create account with associated contacts."""
# Create account
account_id = await self.sdk.create("accounts", account_data)
try:
# Create contacts and associate with account
contact_ids = []
for contact_data in contacts_data:
contact_data["parentcustomerid@odata.bind"] = f"accounts({account_id})"
contact_id = await self.sdk.create("contacts", contact_data)
contact_ids.append(contact_id)
return {
"account_id": account_id,
"contact_ids": contact_ids,
"success": True
}
except Exception as e:
# Rollback: delete account if contact creation fails
try:
await self.sdk.delete("accounts", account_id)
except:
pass # Ignore rollback errors
raise e
async def get_account_summary(self, account_id: str) -> dict:
"""Get comprehensive account summary."""
# Get account with related data
account = await self.sdk.read("accounts", account_id, expand=[
"primarycontactid($select=fullname,emailaddress1)",
"account_parent_account($select=name)",
"contact_customer_accounts($select=fullname,emailaddress1;$top=5)"
])
# Get additional statistics
contact_count_result = await self.sdk.query("contacts", {
"filter": f"parentcustomerid eq '{account_id}'",
"count": True,
"top": 0 # Just get count
})
opportunity_count_result = await self.sdk.query("opportunities", {
"filter": f"customerid eq '{account_id}'",
"count": True,
"top": 0
})
return {
"account": account,
"contact_count": contact_count_result.total_count,
"opportunity_count": opportunity_count_result.total_count,
"primary_contact": account.get("primarycontactid"),
"parent_account": account.get("account_parent_account"),
"recent_contacts": account.get("contact_customer_accounts", [])
}
# Usage
async def main():
sdk = DataverseSDK(...)
async with sdk:
manager = EntityManager(sdk)
# Create account with contacts
result = await manager.create_account_with_contacts(
account_data={"name": "Acme Corp", "websiteurl": "https://acme.com"},
contacts_data=[
{"firstname": "John", "lastname": "Doe", "emailaddress1": "john@acme.com"},
{"firstname": "Jane", "lastname": "Smith", "emailaddress1": "jane@acme.com"}
]
)
# Get account summary
summary = await manager.get_account_summary(result["account_id"])
print(f"Account: {summary['account']['name']}")
print(f"Contacts: {summary['contact_count']}")
print(f"Opportunities: {summary['opportunity_count']}")
```
## 🤝 Contributing
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
### Development Setup
```bash
# Clone the repository
git clone https://github.com/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk.git
cd crmadminbrasil-dataverse-sdk
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install development dependencies
pip install -e ".[dev]"
# Install pre-commit hooks
pre-commit install
# Run tests
pytest
# Run linting
black dataverse_sdk/
isort dataverse_sdk/
flake8 dataverse_sdk/
mypy dataverse_sdk/
```
### Code Style
We use several tools to maintain code quality:
- **Black**: Code formatting
- **isort**: Import sorting
- **flake8**: Linting
- **mypy**: Type checking
- **bandit**: Security analysis
### Testing Guidelines
- Write tests for all new features
- Maintain test coverage above 90%
- Include both unit and integration tests
- Use descriptive test names and docstrings
### Documentation
- Update README.md for new features
- Add docstrings to all public methods
- Include type hints for all parameters and return values
- Provide usage examples
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
- Microsoft Dataverse team for the excellent API
- The Python async community for inspiration
- All contributors who help improve this SDK
## 📞 Support
- **Documentation**: [https://crmadminbrasil-dataverse-sdk.readthedocs.io](https://crmadminbrasil-dataverse-sdk.readthedocs.io)
- **Issues**: [GitHub Issues](https://github.com/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk/issues)
- **Discussions**: [GitHub Discussions](https://github.com/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk/discussions)
- **Email**: support@crmadminbrasil-dataverse-sdk.com
## 🗺️ Roadmap
### Version 1.1
- [ ] WebSocket support for real-time notifications
- [ ] Enhanced FetchXML builder with GUI
- [ ] Plugin system for custom entity types
- [ ] Performance monitoring dashboard
### Version 1.2
- [ ] GraphQL-style query interface
- [ ] Built-in data validation rules
- [ ] Advanced caching strategies
- [ ] Multi-tenant management tools
### Version 2.0
- [ ] Support for Dataverse for Teams
- [ ] AI-powered query optimization
- [ ] Visual query builder
- [ ] Enterprise governance features
---
**Made with ❤️ by the Dataverse SDK Team**
## ⚡ **Performance & Benchmarks**
O SDK foi projetado para ser extremamente performático, capaz de lidar com milhões de registros em uso diário:
### **Configurações Otimizadas**
- **Batch Size**: > 100 registros por lote (padrão: 500)
- **Paralelismo**: Até 32 operações simultâneas
- **Pool de Conexões**: 100 conexões simultâneas
- **Throughput**: > 1000 registros/segundo
### **Testes de Performance**
```bash
# Executar benchmarks de performance
cd benchmarks/
pip install -r requirements.txt
python benchmark_bulk_create.py
# Stress test com milhões de registros
python stress_test.py
```
### **Resultados Típicos**
- ✅ **Criação em massa**: 1000+ registros/segundo
- ✅ **Consultas**: < 100ms para consultas simples
- ✅ **Bulk operations**: 10000+ registros/minuto
- ✅ **Memória**: < 500MB para 100k registros
## 📁 **Estrutura do Projeto**
```
dataverse-sdk/
├── 📦 dataverse_sdk/ # Código principal do SDK
├── 🖥️ cli/ # Interface de linha de comando
├── 🧪 tests/ # Testes unitários e integração
├── 📚 examples/ # Exemplos de uso
├── ⚡ benchmarks/ # Testes de performance
├── 🔧 scripts/ # Scripts utilitários
├── 📖 docs/ # Documentação completa
│ ├── getting-started/ # Guias iniciais
│ ├── guides/ # Guias avançados
│ ├── tutorials/ # Tutoriais
│ ├── api-reference/ # Referência da API
│ ├── contributing/ # Guias de contribuição
│ ├── deployment/ # Guias de deployment
│ └── jekyll/ # Site GitHub Pages
└── 🤖 .github/ # Configurações GitHub
```
## 🔗 **Links da Documentação**
- **[📖 Documentação Completa](docs/)** - Toda a documentação organizada
- **[🚀 Início Rápido](docs/getting-started/quickstart.md)** - Primeiros passos
- **[🏢 Configuração Corporativa](docs/deployment/CORPORATE_SETUP_GUIDE.md)** - Para ambientes empresariais
- **[⚡ Benchmarks](benchmarks/)** - Testes de performance
- **[🤝 Contribuição](docs/contributing/CONTRIBUTING.md)** - Como contribuir
- **[📋 API Reference](docs/api-reference/dataverse-sdk.md)** - Documentação técnica
Raw data
{
"_id": null,
"home_page": null,
"name": "crmadminbrasil-dataverse-sdk",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": "Dataverse SDK Team <team@dataverse-sdk.com>",
"keywords": "dataverse, dynamics, crm, microsoft, async, sdk",
"author": null,
"author_email": "Dataverse SDK Team <team@dataverse-sdk.com>",
"download_url": "https://files.pythonhosted.org/packages/fb/1e/e21921c5df048d3348be01b47f787d9a937939238ff99a60976b0bfde4c6/crmadminbrasil_dataverse_sdk-1.1.4.tar.gz",
"platform": null,
"description": "# Microsoft Dataverse SDK for Python\n\n[](https://pypi.org/project/crmadminbrasil-crmadminbrasil-dataverse-sdk/)\n[](https://pypi.org/project/crmadminbrasil-crmadminbrasil-dataverse-sdk/)\n[](https://opensource.org/licenses/MIT)\n[](https://github.com/psf/black)\n[](https://github.com/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk/actions)\n[](https://codecov.io/gh/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk)\n\nA comprehensive, enterprise-ready Python SDK for Microsoft Dataverse with async support, advanced features, and production-grade reliability.\n\n## \ud83d\ude80 Features\n\n### Core Capabilities\n- **100% Async Support**: Built with `httpx` and `asyncio` for high-performance async operations\n- **Enterprise Ready**: Connection pooling, retry logic, rate limiting, and comprehensive error handling\n- **Type Safety**: Full type hints with Pydantic models for strong typing and validation\n- **Extensible**: Hook system for custom logging, telemetry, and request/response interceptors\n\n### Operations\n- **Complete CRUD**: Create, Read, Update, Delete, and Upsert operations\n- **Bulk Operations**: High-performance batch processing with auto-chunking and parallel execution\n- **Advanced Queries**: OData queries, FetchXML support, and intelligent pagination\n- **Associations**: Entity relationship management (associate/disassociate)\n- **Metadata**: Entity and attribute metadata retrieval\n- **File Operations**: Attachment upload/download support\n\n### Developer Experience\n- **CLI Tool**: Full-featured command-line interface for all operations\n- **Rich Documentation**: Comprehensive docs with examples and best practices\n- **Testing**: Extensive test suite with unit and integration tests\n- **CI/CD Ready**: GitHub Actions workflows and PyPI publishing automation\n\n## \ud83d\udce6 Installation\n\n### From PyPI (Recommended)\n```bash\npip install crmadminbrasil-dataverse-sdk\n```\n\n### Development Installation\n```bash\ngit clone https://github.com/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk.git\ncd crmadminbrasil-dataverse-sdk\npip install -e \".[dev]\"\n```\n\n### Optional Dependencies\n```bash\n# For telemetry support\npip install \"crmadminbrasil-dataverse-sdk[telemetry]\"\n\n# For documentation\npip install \"crmadminbrasil-dataverse-sdk[docs]\"\n\n# All optional dependencies\npip install \"crmadminbrasil-dataverse-sdk[dev,telemetry,docs]\"\n```\n\n## \ud83d\udd27 Quick Start\n\n### Basic Usage\n\n```python\nimport asyncio\nfrom dataverse_sdk import DataverseSDK\n\nasync def main():\n # Initialize SDK\n sdk = DataverseSDK(\n dataverse_url=\"https://yourorg.crm.dynamics.com\",\n client_id=\"your-client-id\",\n client_secret=\"your-client-secret\",\n tenant_id=\"your-tenant-id\",\n )\n \n async with sdk:\n # Create an account\n account_data = {\n \"name\": \"Contoso Ltd\",\n \"websiteurl\": \"https://contoso.com\",\n \"telephone1\": \"555-0123\"\n }\n account_id = await sdk.create(\"accounts\", account_data)\n print(f\"Created account: {account_id}\")\n \n # Query accounts\n accounts = await sdk.query(\"accounts\", {\n \"select\": [\"name\", \"websiteurl\", \"telephone1\"],\n \"filter\": \"statecode eq 0\",\n \"top\": 10\n })\n \n for account in accounts.value:\n print(f\"Account: {account['name']}\")\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n```\n\n### Environment Configuration\n\nCreate a `.env` file:\n```env\nDATAVERSE_URL=https://yourorg.crm.dynamics.com\nAZURE_CLIENT_ID=your-client-id\nAZURE_CLIENT_SECRET=your-client-secret\nAZURE_TENANT_ID=your-tenant-id\n```\n\nThe SDK automatically loads environment variables:\n```python\nfrom dataverse_sdk import DataverseSDK\n\n# Configuration loaded from environment\nsdk = DataverseSDK()\n```\n\n## \ud83d\udcda Documentation\n\n### Table of Contents\n- [Authentication](#authentication)\n- [CRUD Operations](#crud-operations)\n- [Query Operations](#query-operations)\n- [Bulk Operations](#bulk-operations)\n- [FetchXML Queries](#fetchxml-queries)\n- [Entity Associations](#entity-associations)\n- [Metadata Operations](#metadata-operations)\n- [CLI Usage](#cli-usage)\n- [Configuration](#configuration)\n- [Error Handling](#error-handling)\n- [Hooks and Extensibility](#hooks-and-extensibility)\n- [Performance Optimization](#performance-optimization)\n- [Testing](#testing)\n- [Contributing](#contributing)\n\n\n\n\n## \ud83d\udd10 Authentication\n\nThe SDK supports multiple authentication flows for different scenarios:\n\n### Client Credentials Flow (Service-to-Service)\nBest for server applications and automation:\n\n```python\nfrom dataverse_sdk import DataverseSDK\n\nsdk = DataverseSDK(\n dataverse_url=\"https://yourorg.crm.dynamics.com\",\n client_id=\"your-client-id\",\n client_secret=\"your-client-secret\",\n tenant_id=\"your-tenant-id\",\n)\n\nasync with sdk:\n # SDK automatically uses client credentials flow\n accounts = await sdk.query(\"accounts\", {\"top\": 5})\n```\n\n### Device Code Flow (CLI Applications)\nBest for command-line tools and development:\n\n```python\nfrom dataverse_sdk.auth import DataverseAuthenticator\n\nauthenticator = DataverseAuthenticator(\n client_id=\"your-client-id\",\n tenant_id=\"your-tenant-id\",\n dataverse_url=\"https://yourorg.crm.dynamics.com\",\n)\n\n# This will prompt user to visit a URL and enter a code\ntoken = await authenticator.authenticate_device_code()\n```\n\n### Interactive Flow (Desktop Applications)\nFor applications with user interaction:\n\n```python\n# Interactive flow with local redirect\ntoken = await authenticator.authenticate_interactive(\n redirect_uri=\"http://localhost:8080\",\n port=8080\n)\n```\n\n### Token Caching\nThe SDK automatically caches tokens to minimize authentication requests:\n\n```python\n# Tokens are cached automatically\nsdk = DataverseSDK(...)\n\nasync with sdk:\n # First request authenticates and caches token\n await sdk.query(\"accounts\", {\"top\": 1})\n \n # Subsequent requests use cached token\n await sdk.query(\"contacts\", {\"top\": 1})\n \n # Clear cache if needed\n await sdk.clear_auth_cache()\n```\n\n## \ud83d\udcdd CRUD Operations\n\n### Create Operations\n\n```python\n# Create a single entity\naccount_data = {\n \"name\": \"Acme Corporation\",\n \"websiteurl\": \"https://acme.com\",\n \"telephone1\": \"555-0123\",\n \"description\": \"Leading provider of innovative solutions\"\n}\n\naccount_id = await sdk.create(\"accounts\", account_data)\nprint(f\"Created account: {account_id}\")\n\n# Create with return data\naccount = await sdk.create(\"accounts\", account_data, return_record=True)\nprint(f\"Created account: {account['name']} ({account['accountid']})\")\n```\n\n### Read Operations\n\n```python\n# Read by ID\naccount = await sdk.read(\"accounts\", account_id)\nprint(f\"Account name: {account['name']}\")\n\n# Read with specific fields\naccount = await sdk.read(\n \"accounts\", \n account_id,\n select=[\"name\", \"websiteurl\", \"telephone1\"]\n)\n\n# Read with related entities\naccount = await sdk.read(\n \"accounts\",\n account_id,\n expand=[\"primarycontactid\", \"createdby\"]\n)\n```\n\n### Update Operations\n\n```python\n# Update entity\nupdate_data = {\n \"websiteurl\": \"https://newacme.com\",\n \"description\": \"Updated description\"\n}\n\nawait sdk.update(\"accounts\", account_id, update_data)\n\n# Update with return data\nupdated_account = await sdk.update(\n \"accounts\", \n account_id, \n update_data, \n return_record=True\n)\n```\n\n### Delete Operations\n\n```python\n# Delete entity\nawait sdk.delete(\"accounts\", account_id)\n```\n\n### Upsert Operations\n\n```python\n# Upsert (create or update)\naccount_data = {\n \"name\": \"Upsert Test Account\",\n \"websiteurl\": \"https://upsert.com\"\n}\n\nresult = await sdk.upsert(\"accounts\", account_data)\nif result.was_created:\n print(f\"Created new account: {result.entity_id}\")\nelse:\n print(f\"Updated existing account: {result.entity_id}\")\n\n# Upsert with alternate key\nresult = await sdk.upsert(\n \"accounts\",\n account_data,\n alternate_key={\"accountnumber\": \"ACC-001\"}\n)\n```\n\n## \ud83d\udd0d Query Operations\n\n### Basic Queries\n\n```python\nfrom dataverse_sdk.models import QueryOptions\n\n# Simple query\naccounts = await sdk.query(\"accounts\", {\n \"select\": [\"name\", \"websiteurl\"],\n \"top\": 10\n})\n\nfor account in accounts.value:\n print(f\"{account['name']}: {account['websiteurl']}\")\n\n# Using QueryOptions model\noptions = QueryOptions(\n select=[\"name\", \"websiteurl\", \"telephone1\"],\n filter=\"statecode eq 0\",\n order_by=[\"name asc\"],\n top=20\n)\n\naccounts = await sdk.query(\"accounts\", options)\n```\n\n### Advanced Filtering\n\n```python\n# Complex filters\naccounts = await sdk.query(\"accounts\", {\n \"select\": [\"name\", \"revenue\"],\n \"filter\": \"revenue gt 1000000 and statecode eq 0\",\n \"order_by\": [\"revenue desc\"]\n})\n\n# String operations\naccounts = await sdk.query(\"accounts\", {\n \"select\": [\"name\"],\n \"filter\": \"contains(name, 'Microsoft') or startswith(name, 'Contoso')\"\n})\n\n# Date filtering\nfrom datetime import datetime, timedelta\n\nlast_month = datetime.now() - timedelta(days=30)\nrecent_accounts = await sdk.query(\"accounts\", {\n \"select\": [\"name\", \"createdon\"],\n \"filter\": f\"createdon gt {last_month.isoformat()}\"\n})\n```\n\n### Pagination\n\n```python\n# Manual pagination\nresult = await sdk.query(\"accounts\", {\n \"select\": [\"name\"],\n \"top\": 100\n})\n\nall_accounts = result.value\nwhile result.has_more:\n # Get next page\n result = await sdk.query(\"accounts\", {\n \"select\": [\"name\"],\n \"top\": 100,\n \"skip\": len(all_accounts)\n })\n all_accounts.extend(result.value)\n\n# Automatic pagination\nall_accounts = await sdk.query_all(\"accounts\", {\n \"select\": [\"name\", \"websiteurl\"],\n \"filter\": \"statecode eq 0\"\n}, max_records=1000)\n```\n\n### Related Entity Expansion\n\n```python\n# Expand related entities\naccounts = await sdk.query(\"accounts\", {\n \"select\": [\"name\", \"websiteurl\"],\n \"expand\": [\n \"primarycontactid($select=fullname,emailaddress1)\",\n \"createdby($select=fullname)\"\n ],\n \"top\": 10\n})\n\nfor account in accounts.value:\n print(f\"Account: {account['name']}\")\n if account.get('primarycontactid'):\n contact = account['primarycontactid']\n print(f\" Primary Contact: {contact['fullname']}\")\n```\n\n## \u26a1 Bulk Operations\n\n### Bulk Create\n\n```python\n# Prepare data\ncontacts = [\n {\n \"firstname\": \"John\",\n \"lastname\": \"Doe\",\n \"emailaddress1\": \"john.doe@example.com\"\n },\n {\n \"firstname\": \"Jane\",\n \"lastname\": \"Smith\",\n \"emailaddress1\": \"jane.smith@example.com\"\n },\n # ... more contacts\n]\n\n# Bulk create with automatic batching\nresult = await sdk.bulk_create(\n \"contacts\",\n contacts,\n batch_size=100, # Process in batches of 100\n parallel=True # Execute batches in parallel\n)\n\nprint(f\"Processed: {result.total_processed}\")\nprint(f\"Successful: {result.successful}\")\nprint(f\"Failed: {result.failed}\")\nprint(f\"Success rate: {result.success_rate:.1f}%\")\n\nif result.has_errors:\n print(\"Errors:\")\n for error in result.errors[:5]: # Show first 5 errors\n print(f\" - {error}\")\n```\n\n### Bulk Update\n\n```python\n# Prepare updates (must include entity ID)\nupdates = [\n {\n \"id\": \"contact-id-1\",\n \"jobtitle\": \"Senior Developer\"\n },\n {\n \"id\": \"contact-id-2\", \n \"jobtitle\": \"Project Manager\"\n },\n # ... more updates\n]\n\nresult = await sdk.bulk_update(\"contacts\", updates)\n```\n\n### Bulk Delete\n\n```python\n# Delete multiple entities\ncontact_ids = [\n \"contact-id-1\",\n \"contact-id-2\",\n \"contact-id-3\",\n # ... more IDs\n]\n\nresult = await sdk.bulk_delete(\"contacts\", contact_ids)\n```\n\n### Custom Batch Operations\n\n```python\nfrom dataverse_sdk.batch import BatchProcessor\n\n# Create custom batch processor\nbatch_processor = BatchProcessor(\n client=sdk.client,\n default_batch_size=50,\n max_parallel_batches=3\n)\n\n# Custom operations\noperations = [\n {\n \"method\": \"POST\",\n \"url\": \"accounts\",\n \"body\": {\"name\": \"Account 1\"}\n },\n {\n \"method\": \"PATCH\", \n \"url\": \"accounts(existing-id)\",\n \"body\": {\"description\": \"Updated\"}\n },\n {\n \"method\": \"DELETE\",\n \"url\": \"accounts(delete-id)\"\n }\n]\n\nresult = await batch_processor.execute_bulk_operation(\n operations,\n parallel=True,\n transactional=False\n)\n```\n\n## \ud83d\udcca FetchXML Queries\n\n### Basic FetchXML\n\n```python\n# Execute FetchXML string\nfetchxml = \"\"\"\n<fetch top=\"10\">\n <entity name=\"account\">\n <attribute name=\"name\" />\n <attribute name=\"websiteurl\" />\n <attribute name=\"telephone1\" />\n <filter type=\"and\">\n <condition attribute=\"statecode\" operator=\"eq\" value=\"0\" />\n <condition attribute=\"revenue\" operator=\"gt\" value=\"1000000\" />\n </filter>\n <order attribute=\"revenue\" descending=\"true\" />\n </entity>\n</fetch>\n\"\"\"\n\naccounts = await sdk.fetch_xml(fetchxml)\nfor account in accounts:\n print(f\"{account['name']}: {account.get('revenue', 'N/A')}\")\n```\n\n### FetchXML with Linked Entities\n\n```python\nfetchxml = \"\"\"\n<fetch top=\"5\">\n <entity name=\"account\">\n <attribute name=\"name\" />\n <attribute name=\"websiteurl\" />\n <link-entity name=\"contact\" from=\"parentcustomerid\" to=\"accountid\" alias=\"contact\">\n <attribute name=\"fullname\" />\n <attribute name=\"emailaddress1\" />\n <filter type=\"and\">\n <condition attribute=\"statecode\" operator=\"eq\" value=\"0\" />\n </filter>\n </link-entity>\n <filter type=\"and\">\n <condition attribute=\"statecode\" operator=\"eq\" value=\"0\" />\n </filter>\n </entity>\n</fetch>\n\"\"\"\n\nresults = await sdk.fetch_xml(fetchxml)\n```\n\n### FetchXML Builder (Programmatic)\n\n```python\nfrom dataverse_sdk.models import FetchXMLQuery\n\n# Build FetchXML programmatically\nquery = FetchXMLQuery(\n entity=\"account\",\n attributes=[\"name\", \"websiteurl\", \"revenue\"],\n filters=[{\n \"type\": \"and\",\n \"conditions\": [\n {\"attribute\": \"statecode\", \"operator\": \"eq\", \"value\": \"0\"},\n {\"attribute\": \"revenue\", \"operator\": \"gt\", \"value\": \"1000000\"}\n ]\n }],\n orders=[\n {\"attribute\": \"revenue\", \"descending\": True}\n ],\n top=10\n)\n\n# Convert to FetchXML and execute\nfetchxml_string = query.to_fetchxml()\naccounts = await sdk.fetch_xml(fetchxml_string)\n```\n\n## \ud83d\udd17 Entity Associations\n\n### Associate Entities\n\n```python\n# Associate account with contact\nawait sdk.associate(\n primary_entity_type=\"accounts\",\n primary_entity_id=account_id,\n relationship_name=\"account_primary_contact\",\n related_entity_type=\"contacts\",\n related_entity_id=contact_id\n)\n\n# Many-to-many association\nawait sdk.associate(\n primary_entity_type=\"systemusers\",\n primary_entity_id=user_id,\n relationship_name=\"systemuserroles_association\",\n related_entity_type=\"roles\",\n related_entity_id=role_id\n)\n```\n\n### Disassociate Entities\n\n```python\n# Remove association\nawait sdk.disassociate(\n primary_entity_type=\"accounts\",\n primary_entity_id=account_id,\n relationship_name=\"account_primary_contact\"\n)\n\n# Remove specific many-to-many association\nawait sdk.disassociate(\n primary_entity_type=\"systemusers\",\n primary_entity_id=user_id,\n relationship_name=\"systemuserroles_association\",\n related_entity_id=role_id\n)\n```\n\n## \ud83d\udd0d Metadata Operations\n\n### Entity Metadata\n\n```python\n# Get entity metadata\naccount_metadata = await sdk.get_entity_metadata(\"account\")\n\nprint(f\"Display Name: {account_metadata['DisplayName']['UserLocalizedLabel']['Label']}\")\nprint(f\"Logical Name: {account_metadata['LogicalName']}\")\nprint(f\"Primary Key: {account_metadata['PrimaryIdAttribute']}\")\nprint(f\"Primary Name: {account_metadata['PrimaryNameAttribute']}\")\n\n# List all attributes\nfor attr in account_metadata['Attributes']:\n print(f\" {attr['LogicalName']}: {attr['AttributeType']}\")\n```\n\n### Attribute Metadata\n\n```python\n# Get specific attribute metadata\nname_attr = await sdk.get_attribute_metadata(\"account\", \"name\")\n\nprint(f\"Display Name: {name_attr['DisplayName']['UserLocalizedLabel']['Label']}\")\nprint(f\"Type: {name_attr['AttributeType']}\")\nprint(f\"Max Length: {name_attr.get('MaxLength', 'N/A')}\")\nprint(f\"Required: {name_attr['RequiredLevel']['Value']}\")\n```\n\n### Generate Entity Models\n\n```python\n# Generate Pydantic models from metadata (utility function)\nfrom dataverse_sdk.utils import generate_entity_model\n\n# This would generate a Pydantic model class\nAccountModel = await generate_entity_model(sdk, \"account\")\n\n# Use the generated model\naccount_data = {\n \"name\": \"Test Account\",\n \"websiteurl\": \"https://test.com\"\n}\n\n# Validate data with the model\naccount = AccountModel(**account_data)\n```\n\n\n## \ud83d\udda5\ufe0f CLI Usage\n\nThe SDK includes a powerful command-line interface for all operations:\n\n### Installation and Setup\n\n```bash\n# Install the SDK\npip install crmadminbrasil-dataverse-sdk\n\n# Initialize configuration\ndv-cli config init\n# Follow prompts to enter your Dataverse URL, Client ID, etc.\n\n# Test connection\ndv-cli config test\n```\n\n### Entity Operations\n\n```bash\n# List entities\ndv-cli entity list accounts --select name,websiteurl --top 10\ndv-cli entity list contacts --filter \"statecode eq 0\" --order-by createdon\n\n# Get specific entity\ndv-cli entity get accounts 12345678-1234-1234-1234-123456789012\n\n# Create entity\ndv-cli entity create accounts --file account_data.json\necho '{\"name\": \"CLI Test Account\"}' | dv-cli entity create accounts\n\n# Update entity\ndv-cli entity update accounts 12345678-1234-1234-1234-123456789012 --file updates.json\n\n# Delete entity\ndv-cli entity delete accounts 12345678-1234-1234-1234-123456789012 --yes\n```\n\n### Bulk Operations\n\n```bash\n# Bulk create from JSON file\ndv-cli bulk create contacts --file contacts.json --batch-size 100\n\n# Bulk operations with progress\ndv-cli bulk create accounts --file large_accounts.json --parallel\n```\n\n### Data Export/Import\n\n```bash\n# Export data\ndv-cli data export accounts --output accounts_backup.json\ndv-cli data export contacts --filter \"statecode eq 0\" --select firstname,lastname,emailaddress1\n\n# Import data\ndv-cli data import accounts --file accounts_backup.json\n```\n\n### FetchXML Operations\n\n```bash\n# Execute FetchXML from file\ndv-cli fetchxml execute --file complex_query.xml\n\n# Save FetchXML results\ndv-cli fetchxml execute --file query.xml --output results.json\n```\n\n### Configuration Management\n\n```bash\n# View current configuration\ndv-cli config show\n\n# Update configuration\ndv-cli config set dataverse_url https://neworg.crm.dynamics.com\ndv-cli config set log_level DEBUG\n\n# Use different config file\ndv-cli --config-file prod-config.json entity list accounts\n```\n\n### Output Formats\n\n```bash\n# Table format (default)\ndv-cli entity list accounts --top 5\n\n# JSON format\ndv-cli entity list accounts --top 5 --output json\n\n# Save to file\ndv-cli entity list accounts --output json > accounts.json\n```\n\n## \u2699\ufe0f Configuration\n\n### Environment Variables\n\n```bash\n# Required\nexport DATAVERSE_URL=\"https://yourorg.crm.dynamics.com\"\nexport AZURE_CLIENT_ID=\"your-client-id\"\nexport AZURE_TENANT_ID=\"your-tenant-id\"\n\n# Optional\nexport AZURE_CLIENT_SECRET=\"your-client-secret\"\nexport AZURE_AUTHORITY=\"https://login.microsoftonline.com/your-tenant-id\"\nexport AZURE_SCOPE=\"https://yourorg.crm.dynamics.com/.default\"\n\n# SDK Configuration\nexport MAX_CONNECTIONS=100\nexport MAX_RETRIES=3\nexport DEFAULT_BATCH_SIZE=100\nexport LOG_LEVEL=INFO\n```\n\n### Configuration File\n\nCreate `dataverse-config.json`:\n\n```json\n{\n \"dataverse_url\": \"https://yourorg.crm.dynamics.com\",\n \"client_id\": \"your-client-id\",\n \"client_secret\": \"your-client-secret\",\n \"tenant_id\": \"your-tenant-id\",\n \"max_connections\": 100,\n \"max_retries\": 3,\n \"default_batch_size\": 100,\n \"log_level\": \"INFO\"\n}\n```\n\n### Programmatic Configuration\n\n```python\nfrom dataverse_sdk import DataverseSDK\nfrom dataverse_sdk.utils import Config\n\n# Custom configuration\nconfig = Config(\n max_connections=50,\n max_retries=5,\n default_batch_size=200,\n connect_timeout=15.0,\n read_timeout=60.0,\n debug=True\n)\n\nsdk = DataverseSDK(\n dataverse_url=\"https://yourorg.crm.dynamics.com\",\n client_id=\"your-client-id\",\n client_secret=\"your-client-secret\", \n tenant_id=\"your-tenant-id\",\n config=config\n)\n```\n\n### Multi-Environment Setup\n\n```python\n# Development environment\ndev_sdk = DataverseSDK(\n dataverse_url=\"https://dev-org.crm.dynamics.com\",\n client_id=\"dev-client-id\",\n client_secret=\"dev-client-secret\",\n tenant_id=\"dev-tenant-id\"\n)\n\n# Production environment\nprod_sdk = DataverseSDK(\n dataverse_url=\"https://prod-org.crm.dynamics.com\",\n client_id=\"prod-client-id\",\n client_secret=\"prod-client-secret\",\n tenant_id=\"prod-tenant-id\"\n)\n\n# Use different configurations\nasync def sync_data():\n async with dev_sdk as dev, prod_sdk as prod:\n # Get data from dev\n dev_accounts = await dev.query_all(\"accounts\", {\"select\": [\"name\"]})\n \n # Create in prod\n await prod.bulk_create(\"accounts\", dev_accounts)\n```\n\n## \ud83d\udea8 Error Handling\n\n### Exception Types\n\n```python\nfrom dataverse_sdk.exceptions import (\n DataverseSDKError, # Base exception\n AuthenticationError, # Authentication failures\n AuthorizationError, # Permission issues\n ConnectionError, # Network connectivity\n TimeoutError, # Request timeouts\n RateLimitError, # Rate limiting\n ValidationError, # Data validation\n EntityNotFoundError, # Entity not found\n APIError, # API errors\n BatchOperationError, # Batch operation failures\n)\n\n# Specific error handling\ntry:\n account = await sdk.read(\"accounts\", \"invalid-id\")\nexcept EntityNotFoundError as e:\n print(f\"Account not found: {e.entity_id}\")\nexcept AuthenticationError as e:\n print(f\"Authentication failed: {e.message}\")\nexcept APIError as e:\n print(f\"API error {e.status_code}: {e.message}\")\n print(f\"Error details: {e.response_data}\")\n```\n\n### Retry and Rate Limiting\n\n```python\nfrom dataverse_sdk.exceptions import RateLimitError\nimport asyncio\n\nasync def robust_operation():\n max_attempts = 3\n attempt = 0\n \n while attempt < max_attempts:\n try:\n result = await sdk.query(\"accounts\", {\"top\": 1000})\n return result\n \n except RateLimitError as e:\n attempt += 1\n if attempt >= max_attempts:\n raise\n \n # Wait for the suggested retry time\n wait_time = e.retry_after or 60\n print(f\"Rate limited. Waiting {wait_time} seconds...\")\n await asyncio.sleep(wait_time)\n \n except ConnectionError as e:\n attempt += 1\n if attempt >= max_attempts:\n raise\n \n # Exponential backoff for connection errors\n wait_time = 2 ** attempt\n print(f\"Connection error. Retrying in {wait_time} seconds...\")\n await asyncio.sleep(wait_time)\n```\n\n### Comprehensive Error Handling\n\n```python\nasync def safe_bulk_operation(entities):\n try:\n result = await sdk.bulk_create(\"accounts\", entities)\n \n if result.has_errors:\n print(f\"Bulk operation completed with {result.failed} errors:\")\n for error in result.errors:\n print(f\" - {error}\")\n \n return result\n \n except BatchOperationError as e:\n print(f\"Batch operation failed: {e.message}\")\n print(f\"Failed operations: {len(e.failed_operations)}\")\n \n # Retry failed operations individually\n for failed_op in e.failed_operations:\n try:\n await sdk.create(\"accounts\", failed_op[\"data\"])\n except Exception as retry_error:\n print(f\"Retry failed: {retry_error}\")\n \n except Exception as e:\n print(f\"Unexpected error: {e}\")\n raise\n```\n\n## \ud83d\udd0c Hooks and Extensibility\n\n### Built-in Hooks\n\n```python\nfrom dataverse_sdk.hooks import (\n HookType,\n logging_hook,\n telemetry_hook,\n retry_logging_hook\n)\n\n# Register built-in hooks\nsdk.register_hook(HookType.BEFORE_REQUEST, logging_hook)\nsdk.register_hook(HookType.AFTER_RESPONSE, telemetry_hook)\nsdk.register_hook(HookType.ON_RETRY, retry_logging_hook)\n```\n\n### Custom Hooks\n\n```python\nfrom dataverse_sdk.hooks import HookContext, HookType\n\ndef custom_request_hook(context: HookContext) -> None:\n \"\"\"Custom hook to modify requests.\"\"\"\n if context.hook_type == HookType.BEFORE_REQUEST:\n # Add custom headers\n context.request_data[\"headers\"][\"X-Custom-Header\"] = \"MyValue\"\n \n # Log request details\n print(f\"Making request to: {context.request_data['url']}\")\n\ndef custom_response_hook(context: HookContext) -> None:\n \"\"\"Custom hook to process responses.\"\"\"\n if context.hook_type == HookType.AFTER_RESPONSE:\n # Log response time\n response_time = context.metadata.get(\"response_time\", 0)\n print(f\"Request completed in {response_time:.2f}s\")\n \n # Store metrics\n context.set_custom_data(\"response_time\", response_time)\n\n# Register custom hooks\nsdk.register_hook(HookType.BEFORE_REQUEST, custom_request_hook, priority=10)\nsdk.register_hook(HookType.AFTER_RESPONSE, custom_response_hook)\n```\n\n### Async Hooks\n\n```python\nasync def async_telemetry_hook(context: HookContext) -> None:\n \"\"\"Async hook for telemetry.\"\"\"\n if context.hook_type == HookType.AFTER_RESPONSE:\n # Send telemetry data asynchronously\n telemetry_data = {\n \"url\": context.request_data[\"url\"],\n \"method\": context.request_data[\"method\"],\n \"status_code\": context.response_data[\"status_code\"],\n \"response_time\": context.metadata.get(\"response_time\")\n }\n \n # Send to telemetry service (example)\n await send_telemetry(telemetry_data)\n\n# Register async hook\nsdk.register_hook(HookType.AFTER_RESPONSE, async_telemetry_hook)\n```\n\n### Hook Decorators\n\n```python\nfrom dataverse_sdk.hooks import hook, HookType\n\n@hook(HookType.BEFORE_REQUEST, priority=5)\ndef request_validator(context: HookContext) -> None:\n \"\"\"Validate requests before sending.\"\"\"\n url = context.request_data[\"url\"]\n method = context.request_data[\"method\"]\n \n # Custom validation logic\n if method == \"DELETE\" and \"accounts\" in url:\n print(\"Warning: Deleting account!\")\n\n@hook(HookType.ON_ERROR)\ndef error_notifier(context: HookContext) -> None:\n \"\"\"Notify on errors.\"\"\"\n error = context.error\n url = context.request_data[\"url\"]\n \n # Send notification (example)\n send_error_notification(f\"Error in {url}: {error}\")\n```\n\n### OpenTelemetry Integration\n\n```python\nfrom opentelemetry import trace\nfrom dataverse_sdk.hooks import HookContext, HookType\n\ntracer = trace.get_tracer(__name__)\n\ndef opentelemetry_hook(context: HookContext) -> None:\n \"\"\"OpenTelemetry integration hook.\"\"\"\n if context.hook_type == HookType.BEFORE_REQUEST:\n # Start span\n span = tracer.start_span(f\"dataverse_{context.request_data['method']}\")\n span.set_attribute(\"http.url\", context.request_data[\"url\"])\n span.set_attribute(\"http.method\", context.request_data[\"method\"])\n context.set_custom_data(\"span\", span)\n \n elif context.hook_type == HookType.AFTER_RESPONSE:\n # End span\n span = context.get_custom_data(\"span\")\n if span:\n span.set_attribute(\"http.status_code\", context.response_data[\"status_code\"])\n span.end()\n\n# Register OpenTelemetry hook\nsdk.register_hook(HookType.BEFORE_REQUEST, opentelemetry_hook)\nsdk.register_hook(HookType.AFTER_RESPONSE, opentelemetry_hook)\n```\n\n\n## \u26a1 Performance Optimization\n\n### Connection Pooling\n\n```python\nfrom dataverse_sdk.utils import Config\n\n# Optimize connection settings\nconfig = Config(\n max_connections=200, # Total connection pool size\n max_keepalive_connections=50, # Keep-alive connections\n keepalive_expiry=30, # Keep-alive timeout (seconds)\n connect_timeout=10.0, # Connection timeout\n read_timeout=30.0, # Read timeout\n)\n\nsdk = DataverseSDK(config=config)\n```\n\n### Batch Size Optimization\n\n```python\n# Optimize batch sizes based on data size\nsmall_records = [...] # Small records\nlarge_records = [...] # Records with many fields\n\n# Use larger batches for small records\nawait sdk.bulk_create(\"contacts\", small_records, batch_size=500)\n\n# Use smaller batches for large records\nawait sdk.bulk_create(\"accounts\", large_records, batch_size=50)\n```\n\n### Parallel Processing\n\n```python\nimport asyncio\n\nasync def parallel_queries():\n # Execute multiple queries in parallel\n tasks = [\n sdk.query(\"accounts\", {\"select\": [\"name\"], \"top\": 100}),\n sdk.query(\"contacts\", {\"select\": [\"fullname\"], \"top\": 100}),\n sdk.query(\"opportunities\", {\"select\": [\"name\"], \"top\": 100}),\n ]\n \n results = await asyncio.gather(*tasks)\n accounts, contacts, opportunities = results\n \n return {\n \"accounts\": accounts.value,\n \"contacts\": contacts.value,\n \"opportunities\": opportunities.value\n }\n\n# Execute parallel queries\ndata = await parallel_queries()\n```\n\n### Memory-Efficient Streaming\n\n```python\nasync def process_large_dataset():\n \"\"\"Process large datasets efficiently.\"\"\"\n page_size = 1000\n processed_count = 0\n \n # Process in chunks to avoid memory issues\n options = QueryOptions(\n select=[\"accountid\", \"name\"],\n top=page_size\n )\n \n while True:\n result = await sdk.query(\"accounts\", options)\n \n if not result.value:\n break\n \n # Process current batch\n for account in result.value:\n # Process individual account\n await process_account(account)\n processed_count += 1\n \n # Check for more data\n if not result.has_more:\n break\n \n # Update options for next page\n options.skip = processed_count\n \n print(f\"Processed {processed_count} accounts\")\n```\n\n### Caching Strategies\n\n```python\nfrom functools import lru_cache\nimport asyncio\n\nclass CachedDataverseSDK:\n def __init__(self, sdk):\n self.sdk = sdk\n self._metadata_cache = {}\n \n @lru_cache(maxsize=100)\n async def get_cached_entity_metadata(self, entity_type: str):\n \"\"\"Cache entity metadata to avoid repeated API calls.\"\"\"\n if entity_type not in self._metadata_cache:\n metadata = await self.sdk.get_entity_metadata(entity_type)\n self._metadata_cache[entity_type] = metadata\n \n return self._metadata_cache[entity_type]\n \n async def bulk_create_with_validation(self, entity_type: str, entities: list):\n \"\"\"Bulk create with cached metadata validation.\"\"\"\n # Get cached metadata\n metadata = await self.get_cached_entity_metadata(entity_type)\n \n # Validate entities against metadata\n validated_entities = []\n for entity in entities:\n if self._validate_entity(entity, metadata):\n validated_entities.append(entity)\n \n # Bulk create validated entities\n return await self.sdk.bulk_create(entity_type, validated_entities)\n\n# Use cached SDK\ncached_sdk = CachedDataverseSDK(sdk)\n```\n\n## \ud83e\uddea Testing\n\n### Unit Tests\n\n```python\nimport pytest\nfrom unittest.mock import AsyncMock, patch\nfrom dataverse_sdk import DataverseSDK\n\n@pytest.mark.asyncio\nasync def test_account_creation():\n \"\"\"Test account creation.\"\"\"\n sdk = DataverseSDK(\n dataverse_url=\"https://test.crm.dynamics.com\",\n client_id=\"test-client\",\n tenant_id=\"test-tenant\"\n )\n \n # Mock the create method\n sdk.create = AsyncMock(return_value=\"12345678-1234-1234-1234-123456789012\")\n \n account_data = {\"name\": \"Test Account\"}\n account_id = await sdk.create(\"accounts\", account_data)\n \n assert account_id == \"12345678-1234-1234-1234-123456789012\"\n sdk.create.assert_called_once_with(\"accounts\", account_data)\n\n@pytest.mark.asyncio\nasync def test_query_with_filter():\n \"\"\"Test query with filter.\"\"\"\n sdk = DataverseSDK(\n dataverse_url=\"https://test.crm.dynamics.com\",\n client_id=\"test-client\",\n tenant_id=\"test-tenant\"\n )\n \n # Mock query response\n mock_response = {\n \"value\": [{\"accountid\": \"123\", \"name\": \"Test Account\"}],\n \"@odata.count\": 1\n }\n \n with patch.object(sdk, 'query', return_value=mock_response):\n result = await sdk.query(\"accounts\", {\"filter\": \"name eq 'Test'\"})\n assert len(result[\"value\"]) == 1\n assert result[\"value\"][0][\"name\"] == \"Test Account\"\n```\n\n### Integration Tests\n\n```python\nimport pytest\nimport os\nfrom dataverse_sdk import DataverseSDK\n\n# Skip if no credentials\npytestmark = pytest.mark.skipif(\n not os.getenv(\"DATAVERSE_URL\"),\n reason=\"Integration tests require Dataverse credentials\"\n)\n\n@pytest.fixture\nasync def sdk():\n \"\"\"SDK fixture for integration tests.\"\"\"\n sdk_instance = DataverseSDK()\n async with sdk_instance as sdk:\n yield sdk\n\n@pytest.mark.asyncio\nasync def test_real_account_operations(sdk):\n \"\"\"Test real account operations.\"\"\"\n # Create test account\n account_data = {\n \"name\": \"Integration Test Account\",\n \"description\": \"Created by integration test\"\n }\n \n account_id = await sdk.create(\"accounts\", account_data)\n \n try:\n # Read account\n account = await sdk.read(\"accounts\", account_id)\n assert account[\"name\"] == account_data[\"name\"]\n \n # Update account\n await sdk.update(\"accounts\", account_id, {\"description\": \"Updated\"})\n \n # Verify update\n updated_account = await sdk.read(\"accounts\", account_id)\n assert updated_account[\"description\"] == \"Updated\"\n \n finally:\n # Clean up\n await sdk.delete(\"accounts\", account_id)\n```\n\n### Performance Tests\n\n```python\nimport pytest\nimport time\nfrom dataverse_sdk import DataverseSDK\n\n@pytest.mark.slow\n@pytest.mark.asyncio\nasync def test_bulk_operation_performance(sdk):\n \"\"\"Test bulk operation performance.\"\"\"\n # Create test data\n test_contacts = [\n {\"firstname\": f\"Test{i}\", \"lastname\": \"Contact\"}\n for i in range(100)\n ]\n \n start_time = time.time()\n \n # Bulk create\n result = await sdk.bulk_create(\"contacts\", test_contacts)\n \n end_time = time.time()\n duration = end_time - start_time\n \n # Performance assertions\n assert duration < 30.0 # Should complete within 30 seconds\n assert result.success_rate > 90.0 # At least 90% success rate\n \n print(f\"Bulk created {result.successful} contacts in {duration:.2f}s\")\n```\n\n### Running Tests\n\n```bash\n# Install test dependencies\npip install -e \".[dev]\"\n\n# Run unit tests\npytest tests/unit/\n\n# Run integration tests (requires credentials)\npytest tests/integration/\n\n# Run with coverage\npytest --cov=dataverse_sdk --cov-report=html\n\n# Run performance tests\npytest -m slow\n\n# Run specific test\npytest tests/unit/test_auth.py::TestDataverseAuthenticator::test_client_credentials\n```\n\n## \ud83d\udcda Advanced Examples\n\n### Data Migration Script\n\n```python\nimport asyncio\nfrom dataverse_sdk import DataverseSDK\n\nasync def migrate_accounts():\n \"\"\"Migrate accounts from one environment to another.\"\"\"\n \n # Source environment\n source_sdk = DataverseSDK(\n dataverse_url=\"https://source.crm.dynamics.com\",\n client_id=\"source-client-id\",\n client_secret=\"source-secret\",\n tenant_id=\"source-tenant\"\n )\n \n # Target environment\n target_sdk = DataverseSDK(\n dataverse_url=\"https://target.crm.dynamics.com\",\n client_id=\"target-client-id\",\n client_secret=\"target-secret\",\n tenant_id=\"target-tenant\"\n )\n \n async with source_sdk as source, target_sdk as target:\n # Export accounts from source\n print(\"Exporting accounts from source...\")\n accounts = await source.query_all(\"accounts\", {\n \"select\": [\"name\", \"websiteurl\", \"telephone1\", \"description\"],\n \"filter\": \"statecode eq 0\"\n })\n \n print(f\"Found {len(accounts)} accounts to migrate\")\n \n # Import to target\n print(\"Importing accounts to target...\")\n result = await target.bulk_create(\"accounts\", accounts, batch_size=100)\n \n print(f\"Migration completed:\")\n print(f\" Successful: {result.successful}\")\n print(f\" Failed: {result.failed}\")\n print(f\" Success rate: {result.success_rate:.1f}%\")\n\nif __name__ == \"__main__\":\n asyncio.run(migrate_accounts())\n```\n\n### Data Synchronization\n\n```python\nimport asyncio\nfrom datetime import datetime, timedelta\nfrom dataverse_sdk import DataverseSDK\n\nclass DataSynchronizer:\n def __init__(self, primary_sdk, secondary_sdk):\n self.primary = primary_sdk\n self.secondary = secondary_sdk\n \n async def sync_entity(self, entity_type: str, sync_field: str = \"modifiedon\"):\n \"\"\"Sync entities based on modification date.\"\"\"\n \n # Get last sync time (stored somewhere)\n last_sync = await self.get_last_sync_time(entity_type)\n \n # Query modified records from primary\n filter_expr = f\"{sync_field} gt {last_sync.isoformat()}\"\n modified_records = await self.primary.query_all(entity_type, {\n \"filter\": filter_expr,\n \"order_by\": [f\"{sync_field} asc\"]\n })\n \n if not modified_records:\n print(f\"No modified {entity_type} found\")\n return\n \n print(f\"Syncing {len(modified_records)} modified {entity_type}\")\n \n # Upsert to secondary (assuming alternate key exists)\n for record in modified_records:\n try:\n await self.secondary.upsert(\n entity_type,\n record,\n alternate_key={\"name\": record[\"name\"]} # Adjust key as needed\n )\n except Exception as e:\n print(f\"Failed to sync {record.get('name', 'unknown')}: {e}\")\n \n # Update last sync time\n await self.update_last_sync_time(entity_type, datetime.now())\n \n async def get_last_sync_time(self, entity_type: str) -> datetime:\n \"\"\"Get last sync time (implement based on your storage).\"\"\"\n # This could be stored in a database, file, or Dataverse itself\n return datetime.now() - timedelta(hours=1) # Default to 1 hour ago\n \n async def update_last_sync_time(self, entity_type: str, sync_time: datetime):\n \"\"\"Update last sync time (implement based on your storage).\"\"\"\n pass\n\nasync def run_sync():\n primary_sdk = DataverseSDK(...) # Primary environment\n secondary_sdk = DataverseSDK(...) # Secondary environment\n \n async with primary_sdk as primary, secondary_sdk as secondary:\n synchronizer = DataSynchronizer(primary, secondary)\n \n # Sync different entity types\n await synchronizer.sync_entity(\"accounts\")\n await synchronizer.sync_entity(\"contacts\")\n await synchronizer.sync_entity(\"opportunities\")\n\nif __name__ == \"__main__\":\n asyncio.run(run_sync())\n```\n\n### Custom Entity Manager\n\n```python\nfrom dataverse_sdk import DataverseSDK\nfrom dataverse_sdk.models import Entity\nfrom typing import List, Optional\n\nclass EntityManager:\n \"\"\"High-level entity manager with business logic.\"\"\"\n \n def __init__(self, sdk: DataverseSDK):\n self.sdk = sdk\n \n async def create_account_with_contacts(\n self,\n account_data: dict,\n contacts_data: List[dict]\n ) -> dict:\n \"\"\"Create account with associated contacts.\"\"\"\n \n # Create account\n account_id = await self.sdk.create(\"accounts\", account_data)\n \n try:\n # Create contacts and associate with account\n contact_ids = []\n for contact_data in contacts_data:\n contact_data[\"parentcustomerid@odata.bind\"] = f\"accounts({account_id})\"\n contact_id = await self.sdk.create(\"contacts\", contact_data)\n contact_ids.append(contact_id)\n \n return {\n \"account_id\": account_id,\n \"contact_ids\": contact_ids,\n \"success\": True\n }\n \n except Exception as e:\n # Rollback: delete account if contact creation fails\n try:\n await self.sdk.delete(\"accounts\", account_id)\n except:\n pass # Ignore rollback errors\n \n raise e\n \n async def get_account_summary(self, account_id: str) -> dict:\n \"\"\"Get comprehensive account summary.\"\"\"\n \n # Get account with related data\n account = await self.sdk.read(\"accounts\", account_id, expand=[\n \"primarycontactid($select=fullname,emailaddress1)\",\n \"account_parent_account($select=name)\",\n \"contact_customer_accounts($select=fullname,emailaddress1;$top=5)\"\n ])\n \n # Get additional statistics\n contact_count_result = await self.sdk.query(\"contacts\", {\n \"filter\": f\"parentcustomerid eq '{account_id}'\",\n \"count\": True,\n \"top\": 0 # Just get count\n })\n \n opportunity_count_result = await self.sdk.query(\"opportunities\", {\n \"filter\": f\"customerid eq '{account_id}'\",\n \"count\": True,\n \"top\": 0\n })\n \n return {\n \"account\": account,\n \"contact_count\": contact_count_result.total_count,\n \"opportunity_count\": opportunity_count_result.total_count,\n \"primary_contact\": account.get(\"primarycontactid\"),\n \"parent_account\": account.get(\"account_parent_account\"),\n \"recent_contacts\": account.get(\"contact_customer_accounts\", [])\n }\n\n# Usage\nasync def main():\n sdk = DataverseSDK(...)\n \n async with sdk:\n manager = EntityManager(sdk)\n \n # Create account with contacts\n result = await manager.create_account_with_contacts(\n account_data={\"name\": \"Acme Corp\", \"websiteurl\": \"https://acme.com\"},\n contacts_data=[\n {\"firstname\": \"John\", \"lastname\": \"Doe\", \"emailaddress1\": \"john@acme.com\"},\n {\"firstname\": \"Jane\", \"lastname\": \"Smith\", \"emailaddress1\": \"jane@acme.com\"}\n ]\n )\n \n # Get account summary\n summary = await manager.get_account_summary(result[\"account_id\"])\n print(f\"Account: {summary['account']['name']}\")\n print(f\"Contacts: {summary['contact_count']}\")\n print(f\"Opportunities: {summary['opportunity_count']}\")\n```\n\n## \ud83e\udd1d Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.\n\n### Development Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk.git\ncd crmadminbrasil-dataverse-sdk\n\n# Create virtual environment\npython -m venv venv\nsource venv/bin/activate # On Windows: venv\\Scripts\\activate\n\n# Install development dependencies\npip install -e \".[dev]\"\n\n# Install pre-commit hooks\npre-commit install\n\n# Run tests\npytest\n\n# Run linting\nblack dataverse_sdk/\nisort dataverse_sdk/\nflake8 dataverse_sdk/\nmypy dataverse_sdk/\n```\n\n### Code Style\n\nWe use several tools to maintain code quality:\n\n- **Black**: Code formatting\n- **isort**: Import sorting\n- **flake8**: Linting\n- **mypy**: Type checking\n- **bandit**: Security analysis\n\n### Testing Guidelines\n\n- Write tests for all new features\n- Maintain test coverage above 90%\n- Include both unit and integration tests\n- Use descriptive test names and docstrings\n\n### Documentation\n\n- Update README.md for new features\n- Add docstrings to all public methods\n- Include type hints for all parameters and return values\n- Provide usage examples\n\n## \ud83d\udcc4 License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## \ud83d\ude4f Acknowledgments\n\n- Microsoft Dataverse team for the excellent API\n- The Python async community for inspiration\n- All contributors who help improve this SDK\n\n## \ud83d\udcde Support\n\n- **Documentation**: [https://crmadminbrasil-dataverse-sdk.readthedocs.io](https://crmadminbrasil-dataverse-sdk.readthedocs.io)\n- **Issues**: [GitHub Issues](https://github.com/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk/issues)\n- **Discussions**: [GitHub Discussions](https://github.com/crmadminbrasil-dataverse-sdk/crmadminbrasil-dataverse-sdk/discussions)\n- **Email**: support@crmadminbrasil-dataverse-sdk.com\n\n## \ud83d\uddfa\ufe0f Roadmap\n\n### Version 1.1\n- [ ] WebSocket support for real-time notifications\n- [ ] Enhanced FetchXML builder with GUI\n- [ ] Plugin system for custom entity types\n- [ ] Performance monitoring dashboard\n\n### Version 1.2\n- [ ] GraphQL-style query interface\n- [ ] Built-in data validation rules\n- [ ] Advanced caching strategies\n- [ ] Multi-tenant management tools\n\n### Version 2.0\n- [ ] Support for Dataverse for Teams\n- [ ] AI-powered query optimization\n- [ ] Visual query builder\n- [ ] Enterprise governance features\n\n---\n\n**Made with \u2764\ufe0f by the Dataverse SDK Team**\n\n\n\n## \u26a1 **Performance & Benchmarks**\n\nO SDK foi projetado para ser extremamente perform\u00e1tico, capaz de lidar com milh\u00f5es de registros em uso di\u00e1rio:\n\n### **Configura\u00e7\u00f5es Otimizadas**\n- **Batch Size**: > 100 registros por lote (padr\u00e3o: 500)\n- **Paralelismo**: At\u00e9 32 opera\u00e7\u00f5es simult\u00e2neas\n- **Pool de Conex\u00f5es**: 100 conex\u00f5es simult\u00e2neas\n- **Throughput**: > 1000 registros/segundo\n\n### **Testes de Performance**\n```bash\n# Executar benchmarks de performance\ncd benchmarks/\npip install -r requirements.txt\npython benchmark_bulk_create.py\n\n# Stress test com milh\u00f5es de registros\npython stress_test.py\n```\n\n### **Resultados T\u00edpicos**\n- \u2705 **Cria\u00e7\u00e3o em massa**: 1000+ registros/segundo\n- \u2705 **Consultas**: < 100ms para consultas simples\n- \u2705 **Bulk operations**: 10000+ registros/minuto\n- \u2705 **Mem\u00f3ria**: < 500MB para 100k registros\n\n## \ud83d\udcc1 **Estrutura do Projeto**\n\n```\ndataverse-sdk/\n\u251c\u2500\u2500 \ud83d\udce6 dataverse_sdk/ # C\u00f3digo principal do SDK\n\u251c\u2500\u2500 \ud83d\udda5\ufe0f cli/ # Interface de linha de comando\n\u251c\u2500\u2500 \ud83e\uddea tests/ # Testes unit\u00e1rios e integra\u00e7\u00e3o\n\u251c\u2500\u2500 \ud83d\udcda examples/ # Exemplos de uso\n\u251c\u2500\u2500 \u26a1 benchmarks/ # Testes de performance\n\u251c\u2500\u2500 \ud83d\udd27 scripts/ # Scripts utilit\u00e1rios\n\u251c\u2500\u2500 \ud83d\udcd6 docs/ # Documenta\u00e7\u00e3o completa\n\u2502 \u251c\u2500\u2500 getting-started/ # Guias iniciais\n\u2502 \u251c\u2500\u2500 guides/ # Guias avan\u00e7ados\n\u2502 \u251c\u2500\u2500 tutorials/ # Tutoriais\n\u2502 \u251c\u2500\u2500 api-reference/ # Refer\u00eancia da API\n\u2502 \u251c\u2500\u2500 contributing/ # Guias de contribui\u00e7\u00e3o\n\u2502 \u251c\u2500\u2500 deployment/ # Guias de deployment\n\u2502 \u2514\u2500\u2500 jekyll/ # Site GitHub Pages\n\u2514\u2500\u2500 \ud83e\udd16 .github/ # Configura\u00e7\u00f5es GitHub\n```\n\n## \ud83d\udd17 **Links da Documenta\u00e7\u00e3o**\n\n- **[\ud83d\udcd6 Documenta\u00e7\u00e3o Completa](docs/)** - Toda a documenta\u00e7\u00e3o organizada\n- **[\ud83d\ude80 In\u00edcio R\u00e1pido](docs/getting-started/quickstart.md)** - Primeiros passos\n- **[\ud83c\udfe2 Configura\u00e7\u00e3o Corporativa](docs/deployment/CORPORATE_SETUP_GUIDE.md)** - Para ambientes empresariais\n- **[\u26a1 Benchmarks](benchmarks/)** - Testes de performance\n- **[\ud83e\udd1d Contribui\u00e7\u00e3o](docs/contributing/CONTRIBUTING.md)** - Como contribuir\n- **[\ud83d\udccb API Reference](docs/api-reference/dataverse-sdk.md)** - Documenta\u00e7\u00e3o t\u00e9cnica\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Async Python SDK for Microsoft Dataverse with enterprise features",
"version": "1.1.4",
"project_urls": {
"Bug Tracker": "https://github.com/dataverse-sdk/dataverse-sdk/issues",
"Changelog": "https://github.com/dataverse-sdk/dataverse-sdk/blob/main/CHANGELOG.md",
"Documentation": "https://dataverse-sdk.readthedocs.io",
"Homepage": "https://github.com/dataverse-sdk/dataverse-sdk",
"Repository": "https://github.com/dataverse-sdk/dataverse-sdk"
},
"split_keywords": [
"dataverse",
" dynamics",
" crm",
" microsoft",
" async",
" sdk"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "1bc930e60f1752a23a864157cc9f30bd68cded16d03ac111d2bc0b48dd4c7d58",
"md5": "1cb7036784387ec55da8e1f41eaf3adf",
"sha256": "702e6c66f786590459a6aa7d3aff4b4a6dcaa9e4e49324735c398593525353a8"
},
"downloads": -1,
"filename": "crmadminbrasil_dataverse_sdk-1.1.4-py3-none-any.whl",
"has_sig": false,
"md5_digest": "1cb7036784387ec55da8e1f41eaf3adf",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 52992,
"upload_time": "2025-07-15T20:48:00",
"upload_time_iso_8601": "2025-07-15T20:48:00.490225Z",
"url": "https://files.pythonhosted.org/packages/1b/c9/30e60f1752a23a864157cc9f30bd68cded16d03ac111d2bc0b48dd4c7d58/crmadminbrasil_dataverse_sdk-1.1.4-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "fb1ee21921c5df048d3348be01b47f787d9a937939238ff99a60976b0bfde4c6",
"md5": "f73596bc6d2981632591debbb8da4420",
"sha256": "558f74a780ed8a095a5a348552e43cc45f99b23ce83ff3854a10b9b23a482633"
},
"downloads": -1,
"filename": "crmadminbrasil_dataverse_sdk-1.1.4.tar.gz",
"has_sig": false,
"md5_digest": "f73596bc6d2981632591debbb8da4420",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 74547,
"upload_time": "2025-07-15T20:48:01",
"upload_time_iso_8601": "2025-07-15T20:48:01.845156Z",
"url": "https://files.pythonhosted.org/packages/fb/1e/e21921c5df048d3348be01b47f787d9a937939238ff99a60976b0bfde4c6/crmadminbrasil_dataverse_sdk-1.1.4.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-15 20:48:01",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "dataverse-sdk",
"github_project": "dataverse-sdk",
"github_not_found": true,
"lcname": "crmadminbrasil-dataverse-sdk"
}