Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions env_config/README.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the updating of all existing samples to leverage the one-liner for loading from env config will be a separate PR?

Copy link
Contributor Author

@THardy98 THardy98 Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - I am updating samples to have their own TOML config (or shared) and loading from there.

Would need to release new version of Python SDK with temporalio/sdk-python#1004 included, to avoid samples needing to have TOML config for the default profile.

But I'd rather update these samples than wait for another release. Once the new release is out, we can update it accordingly (would be a small diff).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Temporal External Client Configuration Samples

This directory contains Python samples that demonstrate how to use the Temporal SDK's external client configuration feature. This feature allows you to configure a `temporalio.client.Client` using a TOML file and/or environment variables, decoupling connection settings from your application code.

## Prerequisites

To run these samples successfully, you must have a local Temporal development server running. You can start one easily using `temporal server start-dev`.

## Configuration File

The `config.toml` file defines three profiles for different environments:

- `[profile.default]`: A working configuration for local development.
- `[profile.staging]`: A configuration with an intentionally **incorrect** address (`localhost:9999`) to demonstrate how it can be corrected by an override.
- `[profile.prod]`: A non-runnable, illustrative-only configuration showing a realistic setup for Temporal Cloud with placeholder credentials. This profile is not used by the samples but serves as a reference.

## Samples

The following Python scripts demonstrate different ways to load and use these configuration profiles. Each runnable sample highlights a unique feature.

### `load_default.py`

This sample shows the most common use case: loading the `default` profile from the `config.toml` file.

**To run this sample:**

```bash
python3 env_config/load_default.py
```

### `load_profile.py`

This sample demonstrates loading the `staging` profile by name (which has an incorrect address) and then correcting the address using an environment variable. This highlights how environment variables can be used to fix or override file-based configuration.

**To run this sample:**

```bash
python3 env_config/load_profile.py
```

## Running the Samples

You can run each sample script directly from the root of the `samples-python` repository. Ensure you have the necessary dependencies installed by running `pip install -e .` (or the equivalent for your environment).
1 change: 1 addition & 0 deletions env_config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

40 changes: 40 additions & 0 deletions env_config/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# This is a sample configuration file for demonstrating Temporal's environment
# configuration feature. It defines multiple profiles for different environments,
# such as local development, production, and staging.

# Default profile for local development
[profile.default]
address = "localhost:7233"
namespace = "default"

# Optional: Add custom gRPC headers
[profile.default.grpc_meta]
my-custom-header = "development-value"
trace-id = "dev-trace-123"

# Staging profile with inline certificate data
[profile.staging]
address = "localhost:9999"
namespace = "staging"

# An example production profile for Temporal Cloud
[profile.prod]
address = "your-namespace.a1b2c.tmprl.cloud:7233"
namespace = "your-namespace"
# Replace with your actual Temporal Cloud API key
api_key = "your-api-key-here"

# TLS configuration for production
[profile.prod.tls]
# TLS is auto-enabled when an API key is present, but you can configure it
# explicitly.
# disabled = false

# Use certificate files for mTLS. Replace with actual paths.
client_cert_path = "/etc/temporal/certs/client.pem"
client_key_path = "/etc/temporal/certs/client.key"

# Custom headers for production
[profile.prod.grpc_meta]
environment = "production"
service-version = "v1.2.3"
46 changes: 46 additions & 0 deletions env_config/load_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
This sample demonstrates loading the default environment configuration profile
from a TOML file.
"""

import asyncio
from pathlib import Path

from temporalio.client import Client
from temporalio.envconfig import ClientConfig


async def main():
"""
Loads the default profile from the config.toml file in this directory.
"""
print("--- Loading default profile from config.toml ---")

# For this sample to be self-contained, we explicitly provide the path to
# the config.toml file included in this directory.
# By default though, the config.toml file will be loaded from
# ~/.config/temporalio/temporal.toml (or the equivalent standard config directory on your OS).
config_file = Path(__file__).parent / "config.toml"

# load_client_connect_config is a helper that loads a profile and prepares
# the config dictionary for Client.connect. By default, it loads the
# "default" profile.
connect_config = ClientConfig.load_client_connect_config(
config_file=str(config_file)
)

print(f"Loaded 'default' profile from {config_file}.")
print(f" Address: {connect_config.get('target_host')}")
print(f" Namespace: {connect_config.get('namespace')}")
print(f" gRPC Metadata: {connect_config.get('rpc_metadata')}")

print("\nAttempting to connect to client...")
try:
await Client.connect(**connect_config) # type: ignore
print("✅ Client connected successfully!")
except Exception as e:
print(f"❌ Failed to connect: {e}")


if __name__ == "__main__":
asyncio.run(main())
56 changes: 56 additions & 0 deletions env_config/load_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
This sample demonstrates loading a named environment configuration profile and
overriding its values with environment variables.
"""

import asyncio
from pathlib import Path

from temporalio.client import Client
from temporalio.envconfig import ClientConfig


async def main():
"""
Demonstrates loading a named profile and overriding it with env vars.
"""
print("--- Loading 'staging' profile with environment variable overrides ---")

config_file = Path(__file__).parent / "config.toml"
profile_name = "staging"

# In a real application, these would be set in your shell or deployment
# environment (e.g., `export TEMPORAL_ADDRESS=localhost:7233`).
# For this sample, we pass them as a dictionary to demonstrate.
override_env = {
"TEMPORAL_ADDRESS": "localhost:7233",
}
print("The 'staging' profile in config.toml has an incorrect address.")
print("Using mock environment variables to override and correct it:")
for key, value in override_env.items():
print(f" {key}={value}")

# Load the 'staging' profile and apply environment variable overrides.
connect_config = ClientConfig.load_client_connect_config(
profile=profile_name,
config_file=str(config_file),
override_env_vars=override_env,
)

print(f"\nLoaded '{profile_name}' profile from {config_file} with overrides.")
print(f" Address: {connect_config.get('target_host')}")
print(f" Namespace: {connect_config.get('namespace')}")
print(
"\nNote how the incorrect address from the file was corrected by the env var."
)

print("\nAttempting to connect to client...")
try:
await Client.connect(**connect_config) # type: ignore
print("✅ Client connected successfully!")
except Exception as e:
print(f"❌ Failed to connect: {e}")


if __name__ == "__main__":
asyncio.run(main())
Loading