Advanced
Async Operations

Async Operations

The Intapp REST Client supports both synchronous and asynchronous operations, sharing the same interface.

Quick Comparison

Sync MethodAsync MethodDescription
get()aget()GET request
post()apost()POST request
put()aput()PUT request
patch()apatch()PATCH request
delete()adelete()DELETE request
head()ahead()HEAD request (headers only)
options()aoptions()OPTIONS request (discovery)
get_raw()aget_raw()Raw GET response
post_raw()apost_raw()Raw POST response
download_file()adownload_file()Streaming file download
get_paginated()aget_paginated()Paginated GET
post_paginated()apost_paginated()Paginated POST
post_paginated_params()apost_paginated_params()Paginated POST (query params)
parallel_* (batch)aparallel_*Async parallel batch (iterator of tuples; see API reference)
close()aclose()Close client

Basic Async Usage

import asyncio
from intapp_rest_client import RestClient, OAuth2Config
 
async def main():
    async with RestClient(
        base_url="https://api.example.com",
        oauth2_config=OAuth2Config(
            token_url="https://api.example.com/oauth/token",
            client_id="client_id",
            client_secret="secret"
        )
    ) as client:
        # All standard HTTP methods have async versions
        data = await client.aget("/api/v4/resources")
        result = await client.apost("/api/v4/resources", json={"name": "New"})
        await client.adelete("/api/v4/resources/123")
 
asyncio.run(main())

Async Context Manager

The client supports async context managers for automatic cleanup:

async with RestClient(base_url="https://api.example.com", api_key="key") as client:
    data = await client.aget("/api/resources")
# Client is automatically closed

Without context manager:

client = RestClient(base_url="https://api.example.com", api_key="key")
try:
    data = await client.aget("/api/resources")
finally:
    await client.aclose()  # Don't forget to close!

Concurrent Requests

One of the main benefits of async is running requests concurrently:

import asyncio
from intapp_rest_client import RestClient
 
async def fetch_all_data(client):
    # Run multiple requests concurrently
    results = await asyncio.gather(
        client.aget("/api/users"),
        client.aget("/api/companies"),
        client.aget("/api/deals"),
    )
    users, companies, deals = results
    return {"users": users, "companies": companies, "deals": deals}
 
async def main():
    async with RestClient(base_url="https://api.example.com", api_key="key") as client:
        data = await fetch_all_data(client)
        print(f"Fetched {len(data['users'])} users")
 
asyncio.run(main())

With Error Handling

async def fetch_with_fallback(client, endpoints):
    """Fetch from multiple endpoints, handling individual failures."""
    tasks = [client.aget(endpoint) for endpoint in endpoints]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    
    successful = []
    failed = []
    for endpoint, result in zip(endpoints, results):
        if isinstance(result, Exception):
            failed.append((endpoint, result))
        else:
            successful.append(result)
    
    return successful, failed

Async Pagination

Stream large datasets asynchronously:

async def process_all_companies(client):
    async for batch in client.aget_paginated(
        "/api/v4/data/rows/Company",
        page_size=1000,
        data_key="rows"
    ):
        for company in batch:
            await process_company(company)
 
async def main():
    async with RestClient(base_url="https://api.example.com", api_key="key") as client:
        await process_all_companies(client)
 
asyncio.run(main())

POST Pagination

# Pagination in request body
async for batch in client.apost_paginated(
    "/api/v4/query",
    page_size=1000,
    data_key="results",
    json={"filter": {"status": "Active"}}
):
    process_batch(batch)
 
# Pagination in query params (for APIs that use POST with URL pagination)
async for batch in client.apost_paginated_params(
    "/api/v4/data/rows/view/123",
    page_size=1000,
    data_key="rows",
    params={"wrapIntoArrays": "true"}
):
    process_batch(batch)

Async File Operations

Download

For small files, use raw response and write to disk:

async def download_file(client, file_id, output_path):
    response = await client.aget_raw(f"/api/files/{file_id}/download")
    
    import aiofiles
    async with aiofiles.open(output_path, "wb") as f:
        await f.write(response.content)

For large files, use adownload_file() for memory-efficient streaming (no need to load the full response):

async with RestClient(base_url="https://api.example.com", api_key="key") as client:
    size = await client.adownload_file("/api/files/123/download", "/tmp/output.pdf")

Upload

async def upload_file(client, file_path, destination):
    import aiofiles
    async with aiofiles.open(file_path, "rb") as f:
        content = await f.read()
    
    response = await client.apost_raw(
        "/api/files/upload",
        files={"file": (file_path.name, content, "application/octet-stream")}
    )
    return response.json()

Semaphore for Rate Limiting

Control concurrency to avoid overwhelming the API:

async def fetch_with_limit(client, endpoints, max_concurrent=10):
    """Fetch with limited concurrency."""
    semaphore = asyncio.Semaphore(max_concurrent)
    
    async def fetch_one(endpoint):
        async with semaphore:
            return await client.aget(endpoint)
    
    tasks = [fetch_one(endpoint) for endpoint in endpoints]
    return await asyncio.gather(*tasks)
 
async def main():
    async with RestClient(base_url="https://api.example.com", api_key="key") as client:
        endpoints = [f"/api/items/{i}" for i in range(100)]
        results = await fetch_with_limit(client, endpoints, max_concurrent=5)
💡

The client already has built-in retry logic for 429 errors, but a semaphore can proactively prevent hitting rate limits.

Mixing Sync and Async

The same client instance supports both:

client = RestClient(base_url="https://api.example.com", api_key="key")
 
# Sync usage
data = client.get("/api/resources")
 
# Async usage  
async def async_operation():
    return await client.aget("/api/resources")
 
asyncio.run(async_operation())
 
# Remember to close both
client.close()  # Closes sync client
asyncio.run(client.aclose())  # Closes async client
⚠️

When mixing sync and async, you need to close both clients. Use a context manager in async code to avoid forgetting.

Best Practices

1. Use Context Managers

# ✅ Good
async with RestClient(...) as client:
    await client.aget("/api/data")
 
# ❌ Avoid - easy to forget cleanup
client = RestClient(...)
await client.aget("/api/data")
# Did you remember to call await client.aclose()?

2. Limit Concurrency

# ✅ Good - controlled concurrency
semaphore = asyncio.Semaphore(10)
async with semaphore:
    await client.aget(endpoint)
 
# ❌ Avoid - unlimited concurrency can overwhelm APIs
await asyncio.gather(*[client.aget(e) for e in endpoints])  # 1000 concurrent!

3. Handle Exceptions Properly

# ✅ Good - handle individual failures
results = await asyncio.gather(*tasks, return_exceptions=True)
 
# ❌ Avoid - one failure cancels everything
results = await asyncio.gather(*tasks)  # Raises on first error

4. Reuse the Client

# ✅ Good - reuse client
async with RestClient(...) as client:
    for item in items:
        await client.apost("/api/items", json=item)
 
# ❌ Avoid - new client per request
for item in items:
    async with RestClient(...) as client:
        await client.apost("/api/items", json=item)

Next Steps