---
title: 'Async Usage'
description: 'Learn how to use Monzoh with asynchronous programming patterns'
---

## Overview

While Monzoh is primarily designed as a synchronous library, you can easily integrate it with asynchronous applications using Python's `asyncio` and thread execution patterns. This guide shows you how to use Monzoh in async contexts without blocking your event loop.

## Why Async Integration?

Integrating Monzoh with async frameworks is useful for:

- **Web Applications**: FastAPI, Starlette, and other async web frameworks
- **Concurrent Processing**: Handle multiple API calls simultaneously  
- **Non-blocking Operations**: Keep your application responsive during API calls
- **Background Tasks**: Process Monzo data without blocking the main thread

## Basic Async Integration

### Using asyncio.to_thread() (Python 3.9+)

The recommended approach for Python 3.9+ is using `asyncio.to_thread()`:

```python
import asyncio
from monzoh import MonzoClient

async def get_accounts_async():
    """Get accounts asynchronously without blocking the event loop."""
    client = MonzoClient()
    
    # Run the synchronous call in a thread pool
    accounts = await asyncio.to_thread(client.accounts.list)
    return accounts

async def get_account_balance_async(account_id: str):
    """Get account balance asynchronously."""
    client = MonzoClient()
    
    balance = await asyncio.to_thread(
        client.accounts.get_balance,
        account_id=account_id
    )
    return balance

# Usage
async def main():
    accounts = await get_accounts_async()
    print(f"Found {len(accounts)} accounts")
    
    if accounts:
        balance = await get_account_balance_async(accounts[0].id)
        print(f"Balance: £{balance.balance / 100:.2f}")

# Run the async function
asyncio.run(main())
```

## FastAPI Integration

Monzoh integrates seamlessly with FastAPI applications:

```python
from fastapi import FastAPI, HTTPException, Depends
from typing import List
import asyncio
from monzoh import MonzoClient, MonzoError
from monzoh.models import Account, Balance, Transaction

app = FastAPI(title="Monzo API Proxy", version="1.0.0")

def get_monzo_client() -> MonzoClient:
    return MonzoClient()

@app.get("/accounts", response_model=List[Account])
async def get_accounts(client: MonzoClient = Depends(get_monzo_client)):
    """Get all accounts."""
    try:
        accounts = await asyncio.to_thread(client.accounts.list)
        return accounts
    except MonzoError as e:
        raise HTTPException(status_code=400, detail=str(e))

@app.get("/accounts/{account_id}/balance", response_model=Balance)
async def get_account_balance(
    account_id: str,
    client: MonzoClient = Depends(get_monzo_client)
):
    """Get account balance."""
    try:
        balance = await asyncio.to_thread(
            client.accounts.get_balance,
            account_id=account_id
        )
        return balance
    except MonzoError as e:
        raise HTTPException(status_code=400, detail=str(e))

@app.get("/accounts/{account_id}/transactions", response_model=List[Transaction])
async def get_transactions(
    account_id: str,
    limit: int = 100,
    client: MonzoClient = Depends(get_monzo_client)
):
    """Get account transactions."""
    try:
        transactions = await asyncio.to_thread(
            client.transactions.list,
            account_id=account_id,
            limit=limit
        )
        return transactions
    except MonzoError as e:
        raise HTTPException(status_code=400, detail=str(e))

@app.get("/dashboard/{account_id}")
async def get_dashboard_data(
    account_id: str,
    client: MonzoClient = Depends(get_monzo_client)
):
    """Get dashboard data with concurrent API calls."""
    try:
        # Make multiple API calls concurrently
        balance_task = asyncio.to_thread(
            client.accounts.get_balance,
            account_id=account_id
        )
        transactions_task = asyncio.to_thread(
            client.transactions.list,
            account_id=account_id,
            limit=20
        )
        pots_task = asyncio.to_thread(client.pots.list)
        
        balance, transactions, pots = await asyncio.gather(
            balance_task,
            transactions_task,
            pots_task,
            return_exceptions=True
        )
        
        # Handle any exceptions
        if isinstance(balance, Exception):
            raise balance
        if isinstance(transactions, Exception):
            raise transactions
        if isinstance(pots, Exception):
            raise pots
        
        return {
            "balance": {
                "current": balance.balance / 100,
                "spend_today": balance.spend_today / 100,
                "currency": balance.currency
            },
            "recent_transactions": [
                {
                    "id": t.id,
                    "description": t.description,
                    "amount": t.amount / 100,
                    "created": t.created
                }
                for t in transactions[:10]
            ],
            "pots_summary": {
                "total_pots": len(pots),
                "total_saved": sum(p.balance for p in pots) / 100
            }
        }
        
    except MonzoError as e:
        raise HTTPException(status_code=400, detail=str(e))

# Run with: uvicorn main:app --reload
```

## Background Tasks with Celery

For background processing with Celery:

```python
from celery import Celery
from monzoh import MonzoClient, MonzoError
import logging

# Configure Celery
celery_app = Celery('monzo_tasks')
celery_app.config_from_object('celeryconfig')

logger = logging.getLogger(__name__)

@celery_app.task(bind=True, max_retries=3)
def sync_account_data(self, account_id: str):
    """Background task to sync account data."""
    try:
        client = MonzoClient()
        
        # Get latest account data
        balance = client.accounts.get_balance(account_id=account_id)
        transactions = client.transactions.list(
            account_id=account_id,
            limit=100
        )
        
        # Process and store data (your business logic here)
        result = {
            "account_id": account_id,
            "balance": balance.balance / 100,
            "transaction_count": len(transactions),
            "last_sync": "2023-01-01T00:00:00Z"  # Current timestamp
        }
        
        logger.info(f"Synced data for account {account_id}")
        return result
        
    except MonzoError as e:
        logger.error(f"Monzo API error for account {account_id}: {e}")
        # Retry with exponential backoff
        raise self.retry(countdown=60 * (2 ** self.request.retries))

@celery_app.task
def sync_all_accounts():
    """Sync data for all accounts."""
    try:
        client = MonzoClient()
        accounts = client.accounts.list()
        
        # Queue individual sync tasks
        for account in accounts:
            sync_account_data.delay(account.id)
            
        return f"Queued sync for {len(accounts)} accounts"
        
    except MonzoError as e:
        logger.error(f"Failed to list accounts: {e}")
        raise

# Schedule periodic sync (in celeryconfig.py)
# from celery.schedules import crontab
# 
# beat_schedule = {
#     'sync-accounts-hourly': {
#         'task': 'tasks.sync_all_accounts',
#         'schedule': crontab(minute=0),  # Every hour
#     },
# }
```

## Async Context Managers

Create async context managers for resource management:

```python
import asyncio
from contextlib import asynccontextmanager
from monzoh import MonzoClient, MonzoError

@asynccontextmanager
async def async_monzo_client():
    """Async context manager for Monzo client."""
    client = None
    try:
        client = await asyncio.to_thread(MonzoClient)
        yield client
    except MonzoError as e:
        print(f"Monzo client error: {e}")
        raise
    finally:
        # Cleanup if needed
        if client and hasattr(client, 'close'):
            await asyncio.to_thread(client.close)

# Usage
async def get_account_summary():
    async with async_monzo_client() as client:
        accounts = await asyncio.to_thread(client.accounts.list)
        
        summaries = []
        for account in accounts:
            balance = await asyncio.to_thread(
                client.accounts.get_balance,
                account_id=account.id
            )
            summaries.append({
                "account": account.description,
                "balance": balance.balance / 100
            })
        
        return summaries
```

## Concurrent API Calls

Process multiple accounts or operations concurrently:

```python
import asyncio
from monzoh import MonzoClient
from typing import List, Dict, Any

async def process_account_concurrently(
    client: MonzoClient,
    account_id: str
) -> Dict[str, Any]:
    """Process a single account's data concurrently."""
    
    # Make multiple calls for this account concurrently
    balance_task = asyncio.to_thread(
        client.accounts.get_balance,
        account_id=account_id
    )
    transactions_task = asyncio.to_thread(
        client.transactions.list,
        account_id=account_id,
        limit=50
    )
    
    balance, transactions = await asyncio.gather(
        balance_task,
        transactions_task
    )
    
    # Calculate spending statistics
    spending_today = balance.spend_today / 100
    total_spent = sum(
        abs(t.amount) for t in transactions 
        if t.amount < 0
    ) / 100
    
    return {
        "account_id": account_id,
        "balance": balance.balance / 100,
        "spending_today": spending_today,
        "total_spent_recent": total_spent,
        "transaction_count": len(transactions)
    }

async def get_financial_overview():
    """Get overview of all accounts concurrently."""
    client = MonzoClient()
    
    # Get all accounts first
    accounts = await asyncio.to_thread(client.accounts.list)
    
    # Process all accounts concurrently
    tasks = [
        process_account_concurrently(client, account.id)
        for account in accounts
    ]
    
    account_summaries = await asyncio.gather(*tasks)
    
    # Aggregate data
    total_balance = sum(summary["balance"] for summary in account_summaries)
    total_spending = sum(summary["spending_today"] for summary in account_summaries)
    
    return {
        "overview": {
            "total_balance": total_balance,
            "total_spending_today": total_spending,
            "account_count": len(accounts)
        },
        "accounts": account_summaries
    }

# Usage
async def main():
    overview = await get_financial_overview()
    print(f"Total balance: £{overview['overview']['total_balance']:.2f}")
    print(f"Spending today: £{overview['overview']['total_spending_today']:.2f}")

asyncio.run(main())
```

## Error Handling in Async Context

Handle errors properly in async operations:

```python
import asyncio
import logging
from monzoh import (
    MonzoClient,
    MonzoError,
    MonzoRateLimitError,
    MonzoNetworkError
)

logger = logging.getLogger(__name__)

async def robust_api_call(operation, *args, max_retries=3, **kwargs):
    """Make a robust async API call with retries."""
    
    for attempt in range(max_retries):
        try:
            result = await asyncio.to_thread(operation, *args, **kwargs)
            return result
            
        except MonzoRateLimitError as e:
            if attempt < max_retries - 1:
                delay = (2 ** attempt) * 60  # Exponential backoff in minutes
                logger.warning(f"Rate limited, retrying in {delay}s: {e}")
                await asyncio.sleep(delay)
            else:
                logger.error(f"Rate limit exceeded after {max_retries} attempts")
                raise
                
        except MonzoNetworkError as e:
            if attempt < max_retries - 1:
                delay = 2 ** attempt  # Exponential backoff in seconds
                logger.warning(f"Network error, retrying in {delay}s: {e}")
                await asyncio.sleep(delay)
            else:
                logger.error(f"Network error after {max_retries} attempts")
                raise
                
        except MonzoError as e:
            logger.error(f"Non-retryable Monzo error: {e}")
            raise

async def safe_get_accounts():
    """Safely get accounts with error handling."""
    client = MonzoClient()
    
    try:
        accounts = await robust_api_call(client.accounts.list)
        return accounts
    except MonzoError as e:
        logger.error(f"Failed to get accounts: {e}")
        return []

async def main():
    accounts = await safe_get_accounts()
    if accounts:
        print(f"Successfully retrieved {len(accounts)} accounts")
    else:
        print("Failed to retrieve accounts or no accounts found")

asyncio.run(main())
```

## Performance Considerations

### Connection Pooling

When making many concurrent requests, consider connection pooling:

```python
import asyncio
import httpx
from monzoh import MonzoClient

async def create_optimized_clients(num_clients=5):
    """Create multiple clients with shared connection pool."""
    
    # Shared HTTP client with connection pooling
    http_client = httpx.Client(
        limits=httpx.Limits(
            max_connections=10,
            max_keepalive_connections=5
        ),
        timeout=30.0
    )
    
    clients = []
    for _ in range(num_clients):
        client = MonzoClient(http_client=http_client)
        clients.append(client)
    
    return clients, http_client

async def batch_process_accounts():
    """Process accounts in batches for better performance."""
    clients, http_client = await asyncio.to_thread(create_optimized_clients)
    
    try:
        # Get accounts using first client
        accounts = await asyncio.to_thread(clients[0].accounts.list)
        
        # Process accounts in batches
        batch_size = 5
        for i in range(0, len(accounts), batch_size):
            batch = accounts[i:i + batch_size]
            
            # Process batch concurrently
            tasks = [
                asyncio.to_thread(
                    clients[j % len(clients)].accounts.get_balance,
                    account.id
                )
                for j, account in enumerate(batch)
            ]
            
            balances = await asyncio.gather(*tasks)
            
            for account, balance in zip(batch, balances):
                print(f"{account.description}: £{balance.balance / 100:.2f}")
    
    finally:
        # Clean up
        await asyncio.to_thread(http_client.close)

asyncio.run(batch_process_accounts())
```

## Best Practices

1. **Use `asyncio.to_thread()`** for Python 3.9+ - it's the recommended approach
2. **Limit concurrency** to avoid overwhelming the API or your system
3. **Handle errors gracefully** with proper retry logic for transient failures
4. **Use connection pooling** when making many concurrent requests
5. **Clean up resources** properly in async context managers
6. **Monitor performance** and adjust concurrency levels based on your needs

Async integration allows you to build responsive applications that can handle multiple Monzo API operations efficiently without blocking your application's main execution flow.
