Skip to main content

FolioClient

test and lint
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.

Features

🚀 Modern Async Architecture (New in v1.0.0)

  • 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 with syntax

🔐 Enhanced Authentication

  • 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

📡 Comprehensive API Coverage

  • 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

🏗️ Developer Experience

  • Pre-configured HTTP clients - get_folio_http_client() and get_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

Installing

Requirements: Python 3.10+ (v1.0.0+)

pip install folioclient
uv pip install folioclient  # Using uv (recommended)

For experimental performance improvements:

pip install folioclient[orjson]  # Experimental: faster JSON processing

For development:

git clone https://github.com/FOLIO-FSE/FolioClient.git
cd FolioClient
uv sync  # Install with development dependencies

Basic Usage

Create a new FolioClient instance

import 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 passwords

Query an endpoint in FOLIO

from 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)

🆕 Async API Operations (New in v1.0.0)

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 records matching a query without retrieving all records at once

# 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:
    ...

🔧 Convenience Methods for FOLIO HTTP Operations

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)

🆕 Custom Request Headers (New)

Every folio_get, folio_put, folio_post, and folio_delete method (and their _async and folio_get_all* counterparts) accepts an optional headers keyword argument. These headers are sent in addition to the standard FOLIO authentication/tenant headers on that single request - they are not stored on the client or applied to any other calls. This is useful for APIs that require additional context via headers, such as mod-custom-fields, which scopes custom fields to a module via the x-okapi-module-id header:

# mod-custom-fields requires an x-okapi-module-id header to scope custom fields
custom_fields = fc.folio_get(
    "/custom-fields",
    key="customFields",
    headers={"x-okapi-module-id": "mod-users-3.14.2"}
)

# Also supported on write operations and their async/paginated counterparts
await fc.folio_post_async(
    "/custom-fields",
    payload=new_custom_field,
    headers={"x-okapi-module-id": "mod-users-3.14.2"}
)

🔐 Enhanced Authentication & Token Management

Automatic Token Lifecycle

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'}

🆕 Using Pre-configured HTTPX Clients (New)

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

🚨 FOLIO Exception System (New in v1.0.0)

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.

Exception Hierarchy

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
)

Automatic Exception Handling

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")

Exception Information

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 exception

Custom Exception Handling with Decorator

For 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")

Built-in Retry Logic

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_FACTOR

Timeout Configuration

FolioClient 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
)

Custom HTTP Requests with FOLIO Headers

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())

🌐 Enhanced ECS (Consortial) Support

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})

⚠️ Backwards-Incompatible Changes and Deprecations (v1.0.0)

FolioClient v1.0.0 introduces several backwards-incompatible changes. Please review before upgrading:

Major Changes

  • Python 3.10+ required - Dropped support for Python 3.8 and 3.9
  • Timeout parameter behavior - The timeout parameter now distinguishes between None (explicit no timeout) and unset (use environment defaults)
  • Property return types - Some properties now return different object types for better type safety

Deprecated Properties

  • http_timeout property - Now returns httpx.Timeout object instead of original parameter value. Will be removed in future release.
  • okapi_url property - Use gateway_url instead
  • okapi_headers property - Use folio_headers instead
  • okapi_token property - Use access_token instead
  • folio_token_expires property - Use access_token_expires instead

Removed methods

  • get_random_objects

For detailed usage and migration guidance, see the documentation.

Release files for folioclient 1.0.13

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for folioclient 1.0.13
File Size Uploaded
folioclient-1.0.13.tar.gz 40.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for folioclient 1.0.13
File Interpreter ABI Platform
folioclient-1.0.13-py3-none-any.whl Python 3 none any Details

Total release size: 82.8 kB

Release files / folioclient-1.0.13.tar.gz

Download URL folioclient-1.0.13.tar.gz
Size 40.6 kB
Tags Source
SHA-256 checksum
How to use checksums
449a425b6f6057a361f7007403744519175d1c97a5c456baa9f0a953cfe4d80b
BLAKE2b-256 checksum
How to use checksums
54959af371aa0872a802dd976967d42008b7cbeb7a6f1cf7f67085f04f7f270a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release files / folioclient-1.0.13-py3-none-any.whl

Download URL folioclient-1.0.13-py3-none-any.whl
Size 42.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dce5341a9db5388ec9d0477e55b3a64c84883c371ee542a97096c74dc87047aa
BLAKE2b-256 checksum
How to use checksums
d831c61a0fb9e85ff8f5e109c65f8bd26d4f9e80dc454f47753ac1e06e7ae3d1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.13 This release

2 release files

1.0.11

2 release files

1.0.10

2 release files

1.0.8

2 release files

1.0.7

2 release files

1.0.6

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.61.3

2 release files

0.61.2

2 release files

0.61.1

2 release files

0.61.0

2 release files

0.60.7

2 release files

0.60.6

2 release files

0.60.5

2 release files

0.60.4

2 release files

0.60.3

2 release files

0.60.2

2 release files

0.60.1

2 release files

0.50.1

2 release files

0.50.0

2 release files

0.43.0

2 release files

0.42.0

2 release files

0.41.0

2 release files

0.40.2

2 release files

0.40.1

2 release files

0.40.0

2 release files

0.39

2 release files

0.38

1 release file

0.37

1 release file

0.36

1 release file

0.35

1 release file

0.34

1 release file

0.33

1 release file

0.32

1 release file

0.31

1 release file

0.30

1 release file

0.29

1 release file

0.28

1 release file

0.27

1 release file

0.26

1 release file

0.25

1 release file

0.24

1 release file

0.23

1 release file

0.22

1 release file

0.21

1 release file

0.20

1 release file

0.19

1 release file

0.18

1 release file

0.17

1 release file

0.16

1 release file

0.15

1 release file

0.14

1 release file

0.13

1 release file

0.12

1 release file

0.11

1 release file

0.10

1 release file

0.9

1 release file

0.8

1 release file

0.7

1 release file

0.6

1 release file

0.5

1 release file

0.4

1 release file

0.3

1 release file

0.2

1 release file

0.1

1 release file

0.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page