Alter Vault Python SDK - OAuth token management with policy enforcement
Project description
Alter SDK for Python
Official Python SDK for Alter Vault - OAuth token management with policy enforcement.
Features
- 🔐 Secure Token Management: Retrieve OAuth tokens from Alter Vault with policy enforcement
- 🎯 Provider-Specific Wrappers: Native support for Google, GitHub, and more
- 🌐 Generic HTTP Client: Fallback for any OAuth provider
- 📝 Comprehensive Audit Logging: All API calls logged automatically
- ⚡ Backend Token Caching: Redis-based caching for <10ms response times
- 🛡️ Real-time Policy Enforcement: Every token request checked against current policies
- 🔄 Automatic Token Refresh: Tokens refreshed transparently
- 🎭 Decorator Pattern: Wraps official SDKs without breaking compatibility
Installation
# Core SDK only
pip install alter-sdk
# With Google API support
pip install alter-sdk[google]
# With GitHub support
pip install alter-sdk[github]
# With all providers
pip install alter-sdk[all]
Quick Start
import asyncio
from alter_sdk import AlterVault, Provider
async def main():
# Initialize SDK
vault = AlterVault(
api_key="alter_key_...",
app_id="your-app-id"
)
# Get provider client (tokens injected automatically - never exposed)
google = await vault.get_client(
provider=Provider.GOOGLE,
user={"user_id": "alice", "email": "alice@example.com"}
)
# Use Google APIs normally - no token handling needed
calendar = await google.build("calendar", "v3")
events = calendar.events().list(calendarId="primary", maxResults=10).execute()
for event in events.get("items", []):
print(f"Event: {event['summary']}")
# Clean up
await vault.close()
asyncio.run(main())
Key Principle: Zero Token Exposure
Tokens are managed automatically by the SDK and never exposed to developers. All token caching is handled by the backend for performance. You only interact with provider APIs through get_client() or call_api() - tokens are injected behind the scenes.
Architecture: No SDK-Side Caching
The SDK has zero client-side token caching to ensure:
- ✅ Real-time policy enforcement on every request
- ✅ Complete audit trail (backend logs all token access)
- ✅ Instant revocation (no SDK cache delays)
- ✅ Fast performance via backend Redis cache (<10ms)
Every token request goes to the backend, which handles caching efficiently with Redis (5-15min TTL). This architecture ensures security without sacrificing performance.
Usage Examples
Google Calendar API
from alter_sdk import AlterVault, Provider
async def list_calendar_events():
vault = AlterVault(
api_key="alter_key_...",
app_id="your-app-id"
)
# Get Google client wrapper (tokens hidden)
google = await vault.get_client(
provider=Provider.GOOGLE,
user={"user_id": "alice", "email": "alice@example.com"}
)
# Use Google API normally - token injected automatically
calendar = await google.build("calendar", "v3")
events = calendar.events().list(
calendarId="primary",
maxResults=10
).execute()
for event in events.get("items", []):
print(f"Event: {event['summary']}")
await vault.close()
GitHub API
from alter_sdk import AlterVault, Provider
async def list_github_repos():
vault = AlterVault(
api_key="alter_key_...",
app_id="your-app-id"
)
# Get GitHub client wrapper (tokens hidden)
github = await vault.get_client(
provider=Provider.GITHUB,
user={"user_id": "bob", "username": "bob"}
)
# Use PyGithub normally
user = github.get_user()
repos = user.get_repos()
for repo in repos:
print(f"Repo: {repo.name} - {repo.description}")
await vault.close()
Generic API Calls
For providers without dedicated wrappers, use call_api():
from alter_sdk import AlterVault, Provider
async def call_custom_api():
vault = AlterVault(
api_key="alter_key_...",
app_id="your-app-id"
)
# Call any provider API - OAuth token injected automatically
response = await vault.call_api(
provider=Provider.STRIPE,
method="GET",
endpoint="https://api.stripe.com/v1/customers",
user={"org_id": "acme"},
params={"limit": 10}
)
customers = response.json()["data"]
# POST request example
response = await vault.call_api(
provider=Provider.SHOPIFY,
method="POST",
endpoint="https://my-store.myshopify.com/admin/api/2024-01/products.json",
user={"store_id": "my-store"},
body={"product": {"title": "New Product", "price": "29.99"}}
)
product = response.json()["product"]
await vault.close()
Using as Context Manager
from alter_sdk import AlterVault, Provider
async def with_context_manager():
async with AlterVault(api_key="alter_key_...", app_id="your-app-id") as vault:
# Use provider client - tokens handled automatically
google = await vault.get_client(Provider.GOOGLE, user={"user_id": "alice"})
calendar = await google.build("calendar", "v3")
events = calendar.events().list(calendarId="primary").execute()
# Automatically closed
Configuration
vault = AlterVault(
api_key="alter_key_...", # Required: Your Alter Vault API key
app_id="your-app-id", # Required: Your application ID (UUID)
base_url="https://api.alter.com", # Optional: Custom API URL
enable_audit_logging=True, # Optional: Enable audit logs (default: True)
timeout=30.0 # Optional: HTTP timeout in seconds
)
Error Handling
from alter_sdk import AlterVault, Provider
from alter_sdk.exceptions import (
PolicyViolationError, # Policy denied access (403)
PolicyServiceUnavailableError, # Policy service unavailable (503)
ConnectionNotFoundError, # No OAuth connection found
TokenExpiredError, # Token refresh failed
NetworkError, # Backend unreachable
ProviderAPIError # Provider API error
)
try:
# Get client - tokens handled automatically
google = await vault.get_client(Provider.GOOGLE, user={"user_id": "alice"})
calendar = await google.build("calendar", "v3")
events = calendar.events().list(calendarId="primary").execute()
except PolicyViolationError as e:
# Policy denied access - check policy configuration
print(f"Policy violation: {e.message}")
print(f"Details: {e.details}")
# Handle: User needs to contact admin to adjust policy or verify attributes
except PolicyServiceUnavailableError as e:
# Policy service unavailable - retry after delay
print(f"Policy service unavailable: {e.message}")
print(f"Retry after: {e.retry_after} seconds")
# Handle: Retry after delay or use fallback logic
except ConnectionNotFoundError as e:
# No OAuth connection found - user needs to authenticate
print(f"Connection not found: {e.message}")
# Handle: Redirect user to OAuth flow
except TokenExpiredError as e:
# Token refresh failed - user needs to re-authenticate
print(f"Token expired: {e.connection_id}")
# Handle: Redirect user to re-authenticate
except NetworkError as e:
# Backend unreachable - network or infrastructure issue
print(f"Network error: {e.message}")
# Handle: Retry with backoff or show error message
Policy-Related Exceptions
⚠️ CRITICAL: Always Handle Policy Exceptions
Policy enforcement happens on every token retrieval and can raise two exceptions:
1. PolicyViolationError (HTTP 403)
When: Token access denied by configured policy rules (scopes, time, IP, attributes)
Exception Details:
class PolicyViolationError(AlterSDKException):
message: str # Human-readable denial reason
error_code: str # "policy_violation"
details: dict # {resource_id, action, violation_type}
Common Causes:
- OAuth scope not in policy's
allowed_scopeslist - Request outside business hours (
business_hours_onlypolicy) - Request on weekend (
weekdays_onlypolicy) - Client IP not in
ip_allowlist - Connection missing required attributes (
required_attributespolicy)
Example:
try:
google = await vault.get_client(Provider.GOOGLE, user={"user_id": "alice"})
except PolicyViolationError as e:
if "scope" in e.message.lower():
print("❌ Scope not allowed. Contact admin to update policy.")
elif "business hours" in e.message.lower():
print("❌ Access restricted to business hours (9am-5pm)")
elif "ip" in e.message.lower():
print("❌ IP address not in allowlist. Use VPN or office network.")
elif "attribute" in e.message.lower():
print("❌ Connection missing required attributes")
Resolution:
- Check policy configuration in Alter Vault Dashboard → App → Policies
- Verify connection attributes match policy requirements
- Contact administrator to adjust policy if legitimate use case
- Ensure client IP is in allowlist (if using IP-based policies)
2. PolicyServiceUnavailableError (HTTP 503)
When: Cerbos policy service unavailable (system fails closed - denies all access)
Exception Details:
class PolicyServiceUnavailableError(AlterSDKException):
message: str # "Policy enforcement service temporarily unavailable"
retry_after: int # Suggested retry delay in seconds (default: 60)
System Behavior: FAIL CLOSED - If policy service is down, all token access is denied to maintain security.
Example:
import asyncio
try:
google = await vault.get_client(Provider.GOOGLE, user={"user_id": "alice"})
except PolicyServiceUnavailableError as e:
print(f"⚠️ Policy service unavailable. Retrying after {e.retry_after} seconds...")
await asyncio.sleep(e.retry_after)
# Retry logic here
Resolution:
- Retry after
retry_afterseconds (exponential backoff recommended) - Check system status page
- Contact support if issue persists beyond 5 minutes
- Implement fallback logic for degraded mode (optional)
Best Practices for Policy Exception Handling
1. Always catch policy exceptions explicitly:
# ✅ GOOD: Explicit policy exception handling
try:
client = await vault.get_client(Provider.GOOGLE, user={"user_id": "alice"})
except PolicyViolationError as e:
# Handle policy violation
log.warning(f"Policy violation for user alice: {e.message}")
return None
except PolicyServiceUnavailableError as e:
# Handle service unavailable
log.error(f"Policy service down, retry after {e.retry_after}s")
raise
# ❌ BAD: Catching all exceptions hides policy issues
try:
client = await vault.get_client(Provider.GOOGLE, user={"user_id": "alice"})
except Exception as e: # Don't do this!
pass
2. Log policy violations for debugging:
try:
client = await vault.get_client(Provider.GOOGLE, user={"user_id": "alice"})
except PolicyViolationError as e:
logger.warning(
"Policy violation",
extra={
"user": "alice",
"provider": "google",
"error": e.message,
"details": e.details
}
)
# Show user-friendly error
raise
3. Implement retry logic for service unavailable:
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=60)
)
async def get_google_client_with_retry(vault, user):
try:
return await vault.get_client(Provider.GOOGLE, user=user)
except PolicyServiceUnavailableError:
# Retry on 503
raise
except PolicyViolationError:
# Don't retry on 403 (policy violation)
raise
4. Provide clear user feedback:
try:
client = await vault.get_client(Provider.GOOGLE, user={"user_id": "alice"})
except PolicyViolationError as e:
if "scope" in e.message.lower():
return {"error": "Your integration doesn't have permission to access this data. Contact your administrator."}
elif "business hours" in e.message.lower():
return {"error": "This feature is only available during business hours (9am-5pm)."}
elif "ip" in e.message.lower():
return {"error": "This feature requires VPN connection."}
See Also: Policy Enforcement Documentation for complete policy configuration details.
Audit Logging
All API calls are automatically logged to the backend:
from alter_sdk import AlterVault, Provider
# All calls through get_client() are logged automatically
google = await vault.get_client(Provider.GOOGLE, user={"user_id": "alice"})
calendar = await google.build("calendar", "v3")
events = calendar.events().list(calendarId="primary").execute()
# ↑ Logged automatically to backend
# All calls through call_api() are also logged automatically
response = await vault.call_api(
provider=Provider.STRIPE,
method="GET",
endpoint="https://api.stripe.com/v1/customers",
user={"org_id": "acme"}
)
# ↑ Logged automatically to backend
Audit logs never raise exceptions - if logging fails, a warning is logged but your application continues running.
Policy Enforcement
Policies are enforced at backend using Cerbos:
| Policy Type | Examples |
|---|---|
| Scope restrictions | Only allowed OAuth scopes |
| Time-based access | Business hours, weekdays |
| Rate limiting | Max retrievals per day |
| User context | Tier-based, role-based, verified users |
| Geographic | IP allowlist, country-based |
Policies configured in Alter Vault dashboard per app.
Architecture
See ALTER_PYTHON_SDK_ARCHITECTURE.md for comprehensive documentation on:
- Token retrieval flow
- No SDK-side caching (security design)
- Backend cache architecture
- Security guarantees
- Performance characteristics
- Error handling patterns
- Testing strategy
- Migration notes
Key Points:
- ✅ Zero token exposure (never visible to developers)
- ✅ No SDK-side caching (real-time policy enforcement)
- ✅ Backend Redis cache (5-10ms response times)
- ✅ Complete audit trail (all token access logged)
- ✅ Instant revocation (no SDK cache delays)
Development
# Install dependencies
poetry install
# Run tests
pytest
# Type checking
mypy alter_sdk
# Linting
ruff check alter_sdk
black alter_sdk
Requirements
- Python 3.11+
- httpx, pydantic
Optional: google-api-python-client, PyGithub (for provider wrappers)
License
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file alter_sdk-0.1.1.tar.gz.
File metadata
- Download URL: alter_sdk-0.1.1.tar.gz
- Upload date:
- Size: 26.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd2524fd3fc8a61ef4693c2075fc6b1757c018537c59962550a3e1d12716cad5
|
|
| MD5 |
840bd7f5c63db333ff57cc5a46134806
|
|
| BLAKE2b-256 |
379575febaa334355e4d6b2f077e51a6e5424a1d6cbdc0028c5f247107fdb865
|
Provenance
The following attestation bundles were made for alter_sdk-0.1.1.tar.gz:
Publisher:
python-sdk-release.yml on AlterAIDev/Alter-Vault
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
alter_sdk-0.1.1.tar.gz -
Subject digest:
bd2524fd3fc8a61ef4693c2075fc6b1757c018537c59962550a3e1d12716cad5 - Sigstore transparency entry: 850189194
- Sigstore integration time:
-
Permalink:
AlterAIDev/Alter-Vault@59bff8d93bfa9fddc481b01fd177cccb785e3908 -
Branch / Tag:
refs/tags/python-sdk-v0.1.1 - Owner: https://github.com/AlterAIDev
-
Access:
internal
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-sdk-release.yml@59bff8d93bfa9fddc481b01fd177cccb785e3908 -
Trigger Event:
push
-
Statement type:
File details
Details for the file alter_sdk-0.1.1-py3-none-any.whl.
File metadata
- Download URL: alter_sdk-0.1.1-py3-none-any.whl
- Upload date:
- Size: 29.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
90756df9482016837bb8535160913610eb48b66622201d2c3bb45324337db658
|
|
| MD5 |
260a995dc9ef3315a83b5abd9188be38
|
|
| BLAKE2b-256 |
0fab471dcdb64d03d79b37ec832c666e32e50ba9188879a232b9334a2c1b0d16
|
Provenance
The following attestation bundles were made for alter_sdk-0.1.1-py3-none-any.whl:
Publisher:
python-sdk-release.yml on AlterAIDev/Alter-Vault
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
alter_sdk-0.1.1-py3-none-any.whl -
Subject digest:
90756df9482016837bb8535160913610eb48b66622201d2c3bb45324337db658 - Sigstore transparency entry: 850189200
- Sigstore integration time:
-
Permalink:
AlterAIDev/Alter-Vault@59bff8d93bfa9fddc481b01fd177cccb785e3908 -
Branch / Tag:
refs/tags/python-sdk-v0.1.1 - Owner: https://github.com/AlterAIDev
-
Access:
internal
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-sdk-release.yml@59bff8d93bfa9fddc481b01fd177cccb785e3908 -
Trigger Event:
push
-
Statement type: