Best Practices

This guide covers best practices for using the YouVersion Bible Client effectively and efficiently.

Client Management

Reuse Client Instances

Good: Reuse a single client instance for multiple requests:

async with AsyncClient() as client:
    votd = await client.verse_of_the_day()
    highlights = await client.highlights()
    notes = await client.notes()
    # All requests use the same authenticated session

Bad: Creating new clients for each request:

# Inefficient - creates new client each time
votd = await AsyncClient().verse_of_the_day()
highlights = await AsyncClient().highlights()  # New authentication!

Use Context Managers

Always use context managers for automatic cleanup:

# Good - automatic cleanup
async with AsyncClient() as client:
    result = await client.verse_of_the_day()

# Bad - manual cleanup required
client = AsyncClient()
result = await client.verse_of_the_day()
await client.close()  # Easy to forget!

Concurrency

Use AsyncClient for Concurrent Operations

Good: Make concurrent requests with AsyncClient:

import asyncio
from youversion.clients import AsyncClient

async def get_all_data():
    async with AsyncClient() as client:
        # All requests happen concurrently
        votd, highlights, notes = await asyncio.gather(
            client.verse_of_the_day(),
            client.highlights(),
            client.notes()
        )
        return votd, highlights, notes

results = asyncio.run(get_all_data())

Less Efficient: Sequential requests:

# Slower - requests happen one after another
votd = await client.verse_of_the_day()
highlights = await client.highlights()  # Waits for votd
notes = await client.notes()  # Waits for highlights

Batch Operations

Batch similar operations together:

import asyncio
from youversion.clients import AsyncClient

async def get_all_moments():
    async with AsyncClient() as client:
        # Get all moment types concurrently
        results = await asyncio.gather(
            client.highlights(page=1),
            client.notes(page=1),
            client.bookmarks(page=1),
            client.my_images(page=1),
            client.badges(page=1),
            return_exceptions=True  # Don't fail if one fails
        )
        return results

results = asyncio.run(get_all_moments())

Pagination

Handle Pagination Efficiently

Good: Use async iteration for pagination:

import asyncio
from youversion.clients import AsyncClient

async def get_all_highlights():
    async with AsyncClient() as client:
        all_highlights = []
        page = 1

        while True:
            highlights = await client.highlights(page=page)
            if not highlights:
                break

            all_highlights.extend(highlights)
            page += 1

            # Optional: limit pages to avoid excessive requests
            if page > 10:
                break

        return all_highlights

highlights = asyncio.run(get_all_highlights())

Better: Use concurrent pagination with limits:

async def get_pages_concurrently(max_pages=5):
    async with AsyncClient() as client:
        # Get multiple pages concurrently
        tasks = [
            client.highlights(page=i)
            for i in range(1, max_pages + 1)
        ]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        # Flatten results
        all_highlights = []
        for result in results:
            if isinstance(result, list):
                all_highlights.extend(result)
        return all_highlights

Data Processing

Process Data Efficiently

Good: Process data in batches:

from youversion.clients import SyncClient

with SyncClient() as client:
    highlights = client.highlights(page=1)

    # Process in batches
    batch_size = 10
    for i in range(0, len(highlights), batch_size):
        batch = highlights[i:i + batch_size]
        process_batch(batch)

Good: Use list comprehensions for filtering:

with SyncClient() as client:
    highlights = client.highlights(page=1)

    # Filter highlights
    recent = [h for h in highlights if h.time_ago == "just now"]
    long_highlights = [h for h in highlights if len(h.text or "") > 100]

Error Handling

Comprehensive Error Handling

Always implement proper error handling:

import asyncio
import httpx
from youversion.clients import AsyncClient

async def safe_api_call():
    try:
        async with AsyncClient() as client:
            return await client.verse_of_the_day()
    except ValueError as e:
        # Handle configuration errors
        print(f"Configuration error: {e}")
        return None
    except httpx.HTTPStatusError as e:
        # Handle HTTP errors
        if e.response.status_code == 429:
            print("Rate limited - waiting...")
            await asyncio.sleep(60)
            # Retry
            async with AsyncClient() as client:
                return await client.verse_of_the_day()
        return None
    except Exception as e:
        # Handle unexpected errors
        print(f"Unexpected error: {e}")
        return None

result = asyncio.run(safe_api_call())

Caching

Implement Caching for Expensive Operations

Cache results that don’t change frequently:

import asyncio
import time
from functools import lru_cache
from youversion.clients import SyncClient

# Cache Bible versions (rarely change)
@lru_cache(maxsize=1)
def get_cached_versions():
    with SyncClient() as client:
        return client.get_bible_versions("eng", "all")

# First call - hits API
versions1 = get_cached_versions()

# Second call - uses cache
versions2 = get_cached_versions()  # Much faster!

Custom Caching

Implement time-based caching:

import time
from youversion.clients import AsyncClient

class CachedClient:
    def __init__(self, cache_ttl=300):  # 5 minutes
        self.cache = {}
        self.cache_ttl = cache_ttl

    async def get_cached_votd(self):
        now = time.time()
        cache_key = 'votd'

        if cache_key in self.cache:
            data, timestamp = self.cache[cache_key]
            if now - timestamp < self.cache_ttl:
                return data

        async with AsyncClient() as client:
            votd = await client.verse_of_the_day()
            self.cache[cache_key] = (votd, now)
            return votd

cached_client = CachedClient()
votd = await cached_client.get_cached_votd()

Performance Optimization

Minimize API Calls

Good: Get all needed data in one session:

async with AsyncClient() as client:
    # Single session, multiple calls
    votd = await client.verse_of_the_day()
    highlights = await client.highlights()
    notes = await client.notes()

Bad: Multiple sessions:

# Multiple authentication sessions
votd = await AsyncClient().verse_of_the_day()
highlights = await AsyncClient().highlights()  # New auth!

Use Appropriate Client Type

  • Use AsyncClient for async applications and concurrent operations

  • Use SyncClient for simple scripts and synchronous code

  • Don’t mix both in the same application

Resource Management

Clean Up Resources

Always close clients properly:

# Good - automatic cleanup
async with AsyncClient() as client:
    result = await client.verse_of_the_day()

# Also good - explicit cleanup
client = AsyncClient()
try:
    result = await client.verse_of_the_day()
finally:
    await client.close()

Memory Management

For large datasets, process in chunks:

async def process_large_dataset():
    async with AsyncClient() as client:
        page = 1
        while True:
            highlights = await client.highlights(page=page)
            if not highlights:
                break

            # Process chunk
            for highlight in highlights:
                process_highlight(highlight)

            page += 1

            # Optional: clear memory
            del highlights

Security

Secure Credential Storage

Good: Use environment variables:

import os
from youversion.clients import AsyncClient

username = os.getenv("YOUVERSION_USERNAME")
password = os.getenv("YOUVERSION_PASSWORD")

client = AsyncClient(username=username, password=password)

Bad: Hardcode credentials:

# Never do this!
client = AsyncClient(
    username="my_username",  # Exposed in code!
    password="my_password"   # Security risk!
)

Validate Input

Always validate user input:

def safe_search(query: str):
    if not query or len(query) < 2:
        raise ValueError("Query must be at least 2 characters")
    if len(query) > 100:
        raise ValueError("Query too long")

    with SyncClient() as client:
        return client.search_bible(query)

Code Organization

Separate Concerns

Organize code into logical modules:

# api_client.py
from youversion.clients import AsyncClient

class BibleAPIClient:
    def __init__(self):
        self.client = AsyncClient()

    async def get_daily_verse(self):
        return await self.client.verse_of_the_day()

# data_processor.py
class DataProcessor:
    def process_highlights(self, highlights):
        # Process highlights
        pass

# main.py
from api_client import BibleAPIClient
from data_processor import DataProcessor

async def main():
    client = BibleAPIClient()
    processor = DataProcessor()

    votd = await client.get_daily_verse()
    # Process data...

Use Type Hints

Always use type hints for better code clarity:

from typing import List, Optional
from youversion.clients import AsyncClient

async def get_highlights(
    page: int = 1
) -> List[dict]:
    """Get highlights for a page.

    Args:
        page: Page number

    Returns:
        List of highlight dictionaries
    """
    async with AsyncClient() as client:
        return await client.highlights(page=page)

Testing

Mock API Calls in Tests

Use mocks for unit tests:

from unittest.mock import AsyncMock, patch
import pytest
from youversion.clients import AsyncClient

@pytest.mark.asyncio
async def test_verse_of_the_day():
    with patch('youversion.clients.AsyncClient') as mock_client:
        mock_votd = AsyncMock()
        mock_votd.usfm = ["JHN.3.16"]
        mock_votd.day = 1

        mock_client.return_value.__aenter__.return_value.verse_of_the_day.return_value = mock_votd

        async with AsyncClient() as client:
            votd = await client.verse_of_the_day()
            assert votd.usfm == ["JHN.3.16"]

Documentation

Document Your Code

Add docstrings to your functions:

async def get_user_highlights(
    page: int = 1,
    limit: Optional[int] = None
) -> List[dict]:
    """Get user highlights with optional limit.

    Args:
        page: Page number to retrieve
        limit: Maximum number of highlights to return

    Returns:
        List of highlight dictionaries

    Raises:
        ValueError: If page < 1
        httpx.HTTPStatusError: If API call fails
    """
    if page < 1:
        raise ValueError("Page must be >= 1")

    async with AsyncClient() as client:
        highlights = await client.highlights(page=page)
        if limit:
            return highlights[:limit]
        return highlights

Summary

Key Takeaways:

  1. Reuse client instances - Don’t create new clients for each request

  2. Use context managers - Automatic cleanup and error handling

  3. Leverage concurrency - Use AsyncClient for concurrent operations

  4. Handle errors properly - Implement comprehensive error handling

  5. Cache expensive operations - Reduce API calls with caching

  6. Secure credentials - Never hardcode credentials

  7. Organize code - Separate concerns and use type hints

  8. Test thoroughly - Mock API calls in unit tests