FolioClient is a modern, async-capable Python library that provides a comprehensive interface to FOLIO Library Systems Platform APIs. Built on HTTPX with robust authentication, automatic token management, and full support for both synchronous and asynchronous operations.
- Full async/await support - All API methods now have async counterparts for high-performance concurrent operations
- HTTPX-based - Modern HTTP client with HTTP/2 support, connection pooling, and better performance
- Context manager support - Proper resource cleanup with
async withsyntax
- Cookie-based authentication - Improved session management with automatic cookie handling
- RTR (Refresh Token Rotation) support - Seamless token refresh without re-authentication
- Multi-tenant ECS support - Easy tenant switching in consortial environments
- Automatic token lifecycle management - Tokens refreshed transparently when needed
- Complete REST operations - GET, POST, PUT, DELETE with both sync and async variants
- Intelligent pagination -
folio_get_all()automatically handles large result sets - CQL query support - Full Contextual Query Language support for complex searches
- Flexible response handling - Extract specific keys or work with full responses
- Pre-configured HTTP clients -
get_folio_http_client()andget_folio_http_client_async()for advanced use cases - Cached reference data - Common inventory data cached as properties for performance
- JSON Schema validation - Latest FOLIO schemas fetched automatically
- FOLIO-specific exceptions - Meaningful error types (FolioAuthenticationError, FolioValidationError, etc.)
- Intelligent retry logic - Modern tenacity-based exponential backoff with configurable limits
- Granular timeout control - Configure connection, read, write, and pool timeouts via environment variables or constructor
Requirements: Python 3.10+ (v1.0.0+)
pip install folioclientuv pip install folioclient # Using uv (recommended)For experimental performance improvements:
pip install folioclient[orjson] # Experimental: faster JSON processingFor development:
git clone https://github.com/FOLIO-FSE/FolioClient.git
cd FolioClient
uv sync # Install with development dependenciesimport os
from folioclient import FolioClient
fc = FolioClient(
"https://folio-snapshot-okapi.dev.folio.org",
"diku",
"diku_admin",
os.environ.get("FOLIO_PASSWORD")
) # Best Practice: use an environment variable to store your passwordsfrom folioclient import FolioAuthenticationError, FolioResourceNotFoundError
try:
# Basic query, limit=100
instances = fc.folio_get("/instance-storage/instances", key="instances", query_params={"limit": 100})
# mod-search query for all instances without holdings records, expand all sub-objects
instance_search = fc.folio_get(
"/search/instances",
key="instances",
query='cql.allRecords=1 not holdingsId=""',
query_params={
"expandAll": True,
"limit": 100
}
)
except FolioAuthenticationError:
print("π Authentication failed - check your credentials")
except FolioResourceNotFoundError:
print("π Endpoint not found - check your FOLIO version")NOTE: mod-search has a hard limit of 100, with a maximum offset of 900 (will only return the first 1000)
import asyncio
from folioclient import FolioClient
async def main():
async with FolioClient(
"https://folio-snapshot-okapi.dev.folio.org",
"diku",
"diku_admin",
os.environ.get("FOLIO_PASSWORD")
) as fc:
# Async queries for better performance
instances = await fc.folio_get_async(
"/instance-storage/instances",
key="instances",
query_params={"limit": 100}
)
# Process multiple requests concurrently
tasks = [
fc.folio_get_async("/instance-storage/instances", query_params={"offset": i*100, "limit": 100})
for i in range(10)
]
results = await asyncio.gather(*tasks)
# Async bulk operations
async for instance in fc.folio_get_all_async("/instance-storage/instances", key="instances"):
# Process each instance as it's fetched
print(f"Processing instance: {instance['title']}")
# Run the async function
asyncio.run(main())# Get all instances. When performing this operation, you should sort results by id to avoid random reordering of results
get_all_instances = fc.folio_get_all(
"/instance-storage/instances",
key="instances",
limit=1000,
query="cql.allRecords=1 sortBy id"
)
"""
Now you can iterate over get_all_instances, and FolioClient will retrieve them in batches of 1000,
yielding each record until all records matching the query are retrieved.
"""
for instance in get_all_instances:
...FolioClient provides both synchronous and asynchronous methods for all standard HTTP operations:
# Synchronous operations
instance = instances[0]
put_response = fc.folio_put(f"/instance-storage/instances/{instance['id']}", payload=instance)
post_response = fc.folio_post("/users", payload=new_user)
delete_response = fc.folio_delete(f"/users/{user_id}")
# π Asynchronous operations (New in v1.0.0)
put_response = await fc.folio_put_async(f"/instance-storage/instances/{instance['id']}", payload=instance)
post_response = await fc.folio_post_async("/users", payload=new_user)
delete_response = await fc.folio_delete_async(f"/users/{user_id}")
# Concurrent operations for better performance
tasks = [
fc.folio_put_async(f"/instance-storage/instances/{inst['id']}", payload=inst)
for inst in instances_to_update
]
results = await asyncio.gather(*tasks)FolioClient v1.0.0 introduces cookie-based authentication with automatic token refresh. Your auth token is managed transparently with RTR (Refresh Token Rotation) support:
# The token is accessible as a property of the FolioClient instance
auth_token = fc.okapi_token
print(auth_token)
# eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJNQXJBbm10WUV2azV6TTdtQ3puMmIzZDJlZ1NsNk5rZUsxRjBaV1cxd1d3In0...
# Headers automatically include valid auth tokens
print(fc.okapi_headers)
# {'x-okapi-tenant': 'diku', 'x-okapi-token': 'eyJhbGciOiJSUzI1NiIs...', 'content-type': 'application/json'}For advanced scenarios, get pre-configured HTTPX clients with built-in FOLIO authentication:
# Synchronous client
with fc.get_folio_http_client() as client:
response = client.get("/instance-storage/instances?limit=10")
instances = response.json()
# Asynchronous client
async with fc.get_folio_http_client_async() as client:
response = await client.get("/instance-storage/instances?limit=10")
instances = response.json()
# Perform multiple concurrent requests
tasks = [
client.get(f"/instance-storage/instances/{instance_id}")
for instance_id in instance_ids
]
responses = await asyncio.gather(*tasks)The pre-configured clients include:
- β Automatic authentication with cookie-based sessions
- β Modern retry logic powered by tenacity for robust error handling
- β Proper FOLIO headers (tenant, content-type)
- β Base URL configuration - just use relative paths
- β SSL verification settings from your FolioClient instance
FolioClient v1.0.0 introduces a comprehensive FOLIO-specific exception hierarchy that provides semantic context for different types of errors encountered when working with FOLIO APIs.
All FOLIO exceptions inherit from FolioError and preserve the original HTTPX exception information:
from folioclient import (
# Base exceptions
FolioError, FolioClientClosed,
# Connection errors
FolioConnectionError, FolioSystemUnavailableError, FolioTimeoutError,
# 4xx Client errors
FolioAuthenticationError, # 401 - Invalid credentials/expired tokens
FolioPermissionError, # 403 - Insufficient permissions
FolioResourceNotFoundError, # 404 - Resource doesn't exist
FolioValidationError, # 422 - Data validation failures
FolioDataConflictError, # 409 - Optimistic locking, duplicates
# 5xx Server errors
FolioInternalServerError, # 500 - Unexpected server errors
FolioBadGatewayError, # 502 - Invalid module responses
FolioServiceUnavailableError, # 503 - Temporary unavailability
FolioGatewayTimeoutError, # 504 - Module response timeouts
)All FolioClient HTTP convenience methods automatically convert HTTPX exceptions to appropriate FOLIO exceptions:
try:
# This will automatically raise FOLIO-specific exceptions
users = fc.folio_get("/users", query_params={"limit": 1000})
except FolioAuthenticationError:
print("π Authentication failed - check credentials or token expiry")
except FolioPermissionError:
print("π« Insufficient permissions for this operation")
except FolioResourceNotFoundError:
print("π Resource not found - check endpoint or record ID")
except FolioValidationError as e:
print(f"π Data validation failed: {e}")
except FolioInternalServerError:
print("π₯ FOLIO server error - try again later")
except FolioConnectionError:
print("π Network connectivity issue")FOLIO exceptions preserve all original request/response information:
try:
fc.folio_post("/users", payload=invalid_user_data)
except FolioValidationError as e:
print(f"Status: {e.response.status_code}")
print(f"Error details: {e.response.text}")
print(f"Request URL: {e.request.url}")
print(f"Original cause: {e.__cause__}") # Original httpx exceptionFor custom functions, use the @folio_errors decorator to automatically convert exceptions:
from folioclient.exceptions import folio_errors
from folioclient import FolioAuthenticationError
import httpx
@folio_errors
def custom_folio_request():
# Any httpx.HTTPStatusError will be converted to appropriate FOLIO exception
response = httpx.get("https://folio-instance.org/some-endpoint")
response.raise_for_status()
return response.json()
# Usage
try:
data = custom_folio_request()
except FolioAuthenticationError:
print("Authentication issue in custom request")FolioClient includes automatic retry logic powered by the modern tenacity library for certain error conditions:
- Authorization errors (403) - triggers automatic re-login and retry
- Server errors (502, 503, 504) - retries with exponential backoff
- Connection errors - retries with exponential backoff
- Remote protocol errors - recreates HTTP client and retries
Note: Authentication errors (401) are handled automatically by FolioAuth
# Configure retry behavior via environment variables:
# Server error retries:
FOLIOCLIENT_MAX_SERVER_ERROR_RETRIES=3 # (default: 0 - no retries)
FOLIOCLIENT_SERVER_ERROR_RETRY_DELAY=10.0 # (default: 10 seconds initial delay)
FOLIOCLIENT_SERVER_ERROR_RETRY_FACTOR=3.0 # (default: 3x exponential backoff)
FOLIOCLIENT_SERVER_ERROR_MAX_WAIT=unlimited # (default: no cap on wait time)
# - Set to a number (e.g., "60") for max wait time in seconds
# - Set to "unlimited", "inf", or "none" for no cap
# Auth error retries:
FOLIOCLIENT_MAX_AUTH_ERROR_RETRIES=2 (default: 0 - no retries)
FOLIOCLIENT_AUTH_ERROR_RETRY_DELAY=10.0 (default: 10 seconds initial delay)
FOLIOCLIENT_AUTH_ERROR_RETRY_FACTOR=3.0 (default: 3x exponential backoff)
FOLIOCLIENT_AUTH_ERROR_MAX_WAIT=60.0 (default: 60 seconds max wait)
# - Auth errors typically resolve quickly, so lower default cap
# Legacy environment variable support (for backward compatibility):
# SERVER_ERROR_RETRIES_MAX, SERVER_ERROR_RETRY_DELAY, SERVER_ERROR_RETRY_FACTOR
# AUTH_ERROR_RETRIES_MAX, AUTH_ERROR_RETRY_DELAY, AUTH_ERROR_RETRY_FACTORFolioClient provides granular timeout control for HTTP connections. Configure using environment variables or constructor parameters:
# Environment variables for global timeout configuration:
FOLIOCLIENT_CONNECT_TIMEOUT=30.0 # (default: None - unlimited)
FOLIOCLIENT_READ_TIMEOUT=300.0 # (default: None - unlimited)
FOLIOCLIENT_WRITE_TIMEOUT=30.0 # (default: None - unlimited)
FOLIOCLIENT_POOL_TIMEOUT=10.0 # (default: None - unlimited)
# Legacy timeout support:
FOLIOCLIENT_HTTP_TIMEOUT=60 # (applies to all timeout types, default: None - unlimited)# Constructor timeout configuration:
from folioclient import FolioClient
import httpx
# No timeout parameter - uses environment variables if set
client = FolioClient(
"https://folio.example.com", "tenant", "user", "pass"
# Will use FOLIOCLIENT_*_TIMEOUT environment variables
)
# Explicit timeout=None - ignores all environment variables
client = FolioClient(
"https://folio.example.com", "tenant", "user", "pass",
timeout=None # Forces httpx defaults, ignores environment
)
# Single timeout value
client = FolioClient(
"https://folio.example.com", "tenant", "user", "pass",
timeout=60.0
)
# Granular timeout dictionary
client = FolioClient(
"https://folio.example.com", "tenant", "user", "pass",
timeout={
"connect": 15.0,
"read": 180.0,
"write": 30.0,
"pool": 10.0
}
)
# httpx.Timeout object
timeout_obj = httpx.Timeout(connect=15.0, read=180.0, write=30.0, pool=10.0)
client = FolioClient(
"https://folio.example.com", "tenant", "user", "pass",
timeout=timeout_obj
)For custom HTTP implementations, access FOLIO headers with valid auth tokens:
import requests
import aiohttp
import asyncio
# Synchronous with requests
with requests.Session() as session:
response = session.get(
fc.gateway_url + "/instance-storage/instances",
headers=fc.okapi_headers
)
response.raise_for_status()
instances = response.json()
# Asynchronous with aiohttp
async def fetch_with_aiohttp():
async with aiohttp.ClientSession(headers=fc.okapi_headers) as session:
async with session.get(fc.gateway_url + "/instance-storage/instances") as response:
response.raise_for_status()
return await response.json()
instances = asyncio.run(fetch_with_aiohttp())FolioClient v1.0.0 provides improved support for FOLIO ECS (consortial) environments:
# Check if connected to ECS environment
if fc.is_ecs:
print(f"Consortium: {fc.ecs_consortium['name']}")
print(f"Members: {[member['name'] for member in fc.ecs_members]}")
# Switch between tenants seamlessly
print(f"Current tenant: {fc.tenant_id}") # 'cs01'
# Switch to member tenant
fc.tenant_id = "cs01m0001"
print(f"Switched to: {fc.tenant_id}") # 'cs01m0001'
# Reset back to original tenant
del fc.tenant_id
print(f"Back to: {fc.tenant_id}") # 'cs01'
# All API calls automatically use the correct tenant context
instances = fc.folio_get("/instance-storage/instances", query_params={"limit": 10})FolioClient v1.0.0 introduces several backwards-incompatible changes. Please review before upgrading:
- Python 3.10+ required - Dropped support for Python 3.8 and 3.9
- Timeout parameter behavior - The
timeoutparameter now distinguishes betweenNone(explicit no timeout) and unset (use environment defaults) - Property return types - Some properties now return different object types for better type safety
http_timeoutproperty - Now returnshttpx.Timeoutobject instead of original parameter value. Will be removed in future release.okapi_urlproperty - Usegateway_urlinsteadokapi_headersproperty - Usefolio_headersinsteadokapi_tokenproperty - Useaccess_tokeninsteadfolio_token_expiresproperty - Useaccess_token_expiresinstead
get_random_objects
For detailed usage and migration guidance, see the documentation.