Python client for Sanity.io CMS HTTP API. Sanity is a hosted CMS solution for content management. This project is not affiliated with Sanity.io and is a third-party package.
ℹ️ Note: This package is an active fork of the original project at OmniPro-Group/sanity-python.
Available on pypi as package python-sanity
Install with uv:
uv add python-sanityInstall with pip:
pip install python-sanityYou can pass parameters to the client constructor directly, but it is recommended to use environment variables.
| Variable | Description | Required | Default Value |
|---|---|---|---|
| SANITY_PROJECT_ID | The project ID | Yes | |
| SANITY_DATASET | The dataset to use | No | production |
| SANITY_API_TOKEN | The API token | No (required for mutations) | |
| SANITY_LOG_LEVEL | Level of logging | No | INFO |
- AsyncClient: Full async/await support for all operations
- Optional Logger: Logger parameter is now optional, uses built-in logger with
SANITY_LOG_LEVELsupport - httpx: Migrated from
requeststohttpxfor better async support and HTTP/2 - Automatic Retries: Configurable retry logic with exponential backoff
- Better Error Handling: Specific exception types (
SanityAuthError,SanityRateLimitError, etc.) - New API Parameters:
- Query:
perspective,result_source_map,tag,return_query - Mutation:
auto_generate_array_keys,skip_cross_dataset_references_validation,transaction_id
- Query:
- Context Managers: Both
ClientandAsyncClientsupport context managers - Updated API Version: Default API version updated to
2025-02-19
fromsanityimportClient# Simple initialization (logger is now optional!)client=Client() # Uses environment variables# Or with explicit parametersclient=Client(
project_id="your-project-id",
dataset="production",
token="your-api-token", # Optional for read-only queriesuse_cdn=True
)
# Query with GROQresult=client.query(
groq="*[_type == 'post'] | order(publishedAt desc)[0...10]",
variables={"limit": 10}
)
# Mutationstransactions= [{
"createOrReplace": {
"_id": "post.123",
"_type": "post",
"title": "Hello World",
"publishedAt": "2025-01-15T00:00:00Z"
}
}]
result=client.mutate(
transactions=transactions,
return_documents=True
)
# Upload assetsresult=client.assets(
file_path="https://example.com/image.png"
)fromsanityimportAsyncClientimportasyncioasyncdefmain():
# Use async context managerasyncwithAsyncClient() asclient:
# Async queryresult=awaitclient.query(
groq="*[_type == 'post']",
perspective="published"
)
# Async mutationresult=awaitclient.mutate(
transactions=[{
"create": {
"_type": "post",
"title": "Async Post"
}
}]
)
# Async asset uploadresult=awaitclient.assets(
file_path="/path/to/image.png"
)
asyncio.run(main())fromsanityimportClient, TimeoutConfig, RetryConfig# Custom timeouts and retriesclient=Client(
timeout=TimeoutConfig(
connect=5.0,
read=30.0,
write=30.0,
pool=5.0
),
retry_config=RetryConfig(
max_retries=5,
backoff_factor=1.0
),
http2=True
)fromsanityimport (
Client,
SanityAuthError,
SanityRateLimitError,
SanityValidationError
)
client=Client()
try:
result=client.query(groq="*[_type == 'post']")
exceptSanityAuthErrorase:
print(f"Authentication failed: {e.message}")
exceptSanityRateLimitErrorase:
print(f"Rate limited, retry after {e.retry_after}s")
exceptSanityValidationErrorase:
print(f"Validation error: {e.response_body}")None! v0.2.0 is fully backward compatible.
Logger is now optional:
# Old way (still works)importloggingclient=Client(logger=logging.getLogger(__name__)) # New way (simpler)client=Client() # Uses built-in logger with SANITY_LOG_LEVEL
Use context managers for cleanup:
# RecommendedwithClient() asclient: result=client.query(groq="*[_type == 'post']")
Try async for better performance:
fromsanityimportAsyncClientasyncwithAsyncClient() asclient: result=awaitclient.query(groq="*[_type == 'post']")
Use new parameters:
# Query with perspectiveresult=client.query( groq="*[_type == 'post']", perspective="published", # drafts, published, rawtag="my-app" ) # Mutations with new optionsresult=client.mutate( transactions=[...], auto_generate_array_keys=True, transaction_id="my-custom-id" )