Managing Secrets with Python SDK

This guide covers how to manage secrets using the Julep Python SDK. Secrets allow you to securely store and use sensitive information like API keys, passwords, and access tokens in your Julep applications.

Installation

Ensure you have the latest version of the Python SDK:

pip install julep

Authentication

Initialize the client with your API key:

from julep import Julep

client = Julep(api_key="your_api_key")

Creating Secrets

Create a new secret:

secret = client.secrets.create(
    name="stripe_api_key",
    value="sk_test_...",
    description="Stripe API key for payment processing",
    metadata={"environment": "production", "owner": "payments-team"}
)

print(f"Created secret: {secret.name}")

Required Parameters

  • name: The name of the secret (must be a valid identifier)
  • value: The secret value to encrypt and store

Optional Parameters

  • description: A description of what the secret is used for
  • metadata: A dictionary of metadata to associate with the secret

Listing Secrets

List all available secrets:

# List all secrets
secrets = client.secrets.list()
for secret in secrets.items:
    print(f"{secret.name}: {secret.description}")

Pagination

Use pagination to handle large numbers of secrets:

# List with pagination
secrets_page_1 = client.secrets.list(limit=10, offset=0)
secrets_page_2 = client.secrets.list(limit=10, offset=10)

Filtering by Metadata

Filter secrets based on metadata:

# Filter by metadata
production_secrets = client.secrets.list(
    metadata={"environment": "production"}
)

# Filter by multiple metadata fields
team_secrets = client.secrets.list(
    metadata={
        "environment": "production",
        "owner": "payments-team"
    }
)

Retrieving Secrets

Get a specific secret by name:

secret = client.secrets.get(name="stripe_api_key")
print(f"Secret: {secret.name}, Created: {secret.created_at}")

# Access the secret value
print(f"Secret value: {secret.value}")

Note: For security, when listing secrets, the .value field will always show “ENCRYPTED”. The actual secret value is only returned when specifically requesting a single secret by name.

Updating Secrets

Update an existing secret:

updated_secret = client.secrets.update(
    name="stripe_api_key",
    value="sk_test_new_value...",
    description="Updated Stripe API key",
    metadata={"environment": "production", "owner": "payments-team", "rotated": "2025-05-10"}
)

print(f"Updated secret: {updated_secret.name}")

Partial Updates

You can update specific fields without changing others:

# Update only the description
updated_secret = client.secrets.update(
    name="stripe_api_key",
    description="New description for Stripe API key"
)

# Update only the metadata
updated_secret = client.secrets.update(
    name="stripe_api_key",
    metadata={"last_rotated": "2025-05-10"}
)

Deleting Secrets

Delete a secret when it’s no longer needed:

client.secrets.delete(name="stripe_api_key")
print("Secret deleted")

Using Secrets in Tasks

Reference secrets when executing tasks:

from julep import Julep

client = Julep(api_key="your_api_key")

# Define a task that uses a secret
task_definition = {
    "steps": [
        {
            "kind": "tool_call",
            "tool": "openai",
            "operation": "chat",
            "arguments": {
                "model": "gpt-4",
                "messages": [
                    {"role": "user", "content": "What's the weather like?"}
                ]
            },
            "secret_name": "openai_api_key"
        }
    ]
}

# Create and execute the task
task = client.tasks.create(task=task_definition)
execution = client.tasks.execute(task_id=task.id)

Using Secrets in Expressions

You can reference secrets in expressions:

task_definition = {
    "steps": [
        {
            "kind": "transform",
            "expression": "$ f'https://api.example.com/v1?api_key={secrets.api_key}&query={input}'",
            "input": "search query",
            "output": "api_url"
        }
    ]
}

Using Multiple Secrets

For tools that require multiple secrets:

task_definition = {
    "steps": [
        {
            "kind": "tool_call",
            "tool": "database",
            "operation": "query",
            "arguments": {
                "query": "SELECT * FROM users",
                "connection": {
                    "host": "$ secrets.db_host",
                    "user": "$ secrets.db_username",
                    "password": "$ secrets.db_password",
                    "database": "$ secrets.db_name"
                }
            }
        }
    ]
}

Error Handling

Handle common errors when working with secrets:

from julep.exceptions import (
    SecretNotFoundError,
    SecretAlreadyExistsError,
    ValidationError
)

try:
    secret = client.secrets.get(name="non_existent_secret")
except SecretNotFoundError:
    print("Secret not found")

try:
    # Attempt to create a secret with an invalid name
    secret = client.secrets.create(name="invalid name", value="test")
except ValidationError as e:
    print(f"Validation error: {e}")

try:
    # Attempt to create a duplicate secret
    secret = client.secrets.create(name="existing_secret", value="test")
except SecretAlreadyExistsError:
    print("Secret already exists")

Secret Rotation Example

Implement a secret rotation policy:

import uuid
from datetime import datetime

def rotate_secret(client, secret_name, new_value):
    """
    Safely rotate a secret by creating a new one and verifying it works
    before deleting the old one.
    """
    # Create a temporary secret with a random suffix
    temp_name = f"{secret_name}_rotation_{uuid.uuid4().hex[:8]}"
    
    # Create the new secret
    client.secrets.create(
        name=temp_name,
        value=new_value,
        description=f"Temporary rotation for {secret_name}",
        metadata={"rotation_date": datetime.now().isoformat()}
    )
    
    # Here you would test that the new secret works
    # ...
    
    # If tests pass, update metadata on the old secret
    old_secret = client.secrets.get(name=secret_name)
    old_metadata = old_secret.metadata or {}
    old_metadata.update({
        "archived": "true",
        "replaced_by": temp_name,
        "archived_date": datetime.now().isoformat()
    })
    
    client.secrets.update(
        name=secret_name,
        metadata=old_metadata
    )
    
    # Rename the temporary secret to the standard name
    client.secrets.delete(name=secret_name)
    client.secrets.update(
        name=temp_name,
        new_name=secret_name,
        description=old_secret.description,
        metadata={"last_rotated": datetime.now().isoformat()}
    )
    
    return client.secrets.get(name=secret_name)

# Example usage
new_key = "sk_test_new_value_after_rotation"
rotated_secret = rotate_secret(client, "stripe_api_key", new_key)
print(f"Rotated secret: {rotated_secret.name}")

Best Practices

  1. Use a consistent naming convention for all secrets
  2. Add detailed descriptions and metadata to make secrets discoverable
  3. Implement a rotation policy for sensitive secrets
  4. Log secret operations (creation, updates, deletions) but never log values
  5. Use try/except blocks to handle potential errors gracefully
  6. Consider automating secret rotation for important credentials
  7. Use metadata to track important dates and ownership information

Next Steps