From 196d681993649059e97ac2f848b584536b7690f2 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Fri, 25 Jul 2025 13:13:18 -0400 Subject: [PATCH 1/4] External client configuration sample --- env_config/README.md | 43 +++++++++++++++++++++++++++++ env_config/__init__.py | 1 + env_config/config.toml | 40 +++++++++++++++++++++++++++ env_config/load_default.py | 46 +++++++++++++++++++++++++++++++ env_config/load_profile.py | 56 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 env_config/README.md create mode 100644 env_config/__init__.py create mode 100644 env_config/config.toml create mode 100644 env_config/load_default.py create mode 100644 env_config/load_profile.py diff --git a/env_config/README.md b/env_config/README.md new file mode 100644 index 00000000..11f38f25 --- /dev/null +++ b/env_config/README.md @@ -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). \ No newline at end of file diff --git a/env_config/__init__.py b/env_config/__init__.py new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/env_config/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/env_config/config.toml b/env_config/config.toml new file mode 100644 index 00000000..81f07f78 --- /dev/null +++ b/env_config/config.toml @@ -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" \ No newline at end of file diff --git a/env_config/load_default.py b/env_config/load_default.py new file mode 100644 index 00000000..ab3bad14 --- /dev/null +++ b/env_config/load_default.py @@ -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()) diff --git a/env_config/load_profile.py b/env_config/load_profile.py new file mode 100644 index 00000000..7c7a0cf6 --- /dev/null +++ b/env_config/load_profile.py @@ -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()) From a9d0eb1ae29a913476e20916172017aef3fea2af Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 11 Aug 2025 23:24:40 -0400 Subject: [PATCH 2/4] update sample --- README.md | 1 + env_config/README.md | 4 ++-- env_config/load_profile.py | 32 ++++++++++++-------------------- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 4a4bb829..43d2cba6 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Some examples require extra dependencies. See each sample's directory for specif * [custom_metric](custom_metric) - Custom metric to record the workflow type in the activity schedule to start latency. * [dsl](dsl) - DSL workflow that executes steps defined in a YAML file. * [encryption](encryption) - Apply end-to-end encryption for all input/output. +* [env_config](env_config) - Load client configuration from TOML files with programmatic overrides. * [gevent_async](gevent_async) - Combine gevent and Temporal. * [langchain](langchain) - Orchestrate workflows for LangChain. * [message_passing/introduction](message_passing/introduction/) - Introduction to queries, signals, and updates. diff --git a/env_config/README.md b/env_config/README.md index 11f38f25..eb1fc60e 100644 --- a/env_config/README.md +++ b/env_config/README.md @@ -1,6 +1,6 @@ # 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. +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 programmatic overrides, decoupling connection settings from your application code. ## Prerequisites @@ -30,7 +30,7 @@ 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. +This sample demonstrates loading the `staging` profile by name (which has an incorrect address) and then correcting the address programmatically. This highlights the recommended approach for overriding configuration values at runtime. **To run this sample:** diff --git a/env_config/load_profile.py b/env_config/load_profile.py index 7c7a0cf6..6f8fd988 100644 --- a/env_config/load_profile.py +++ b/env_config/load_profile.py @@ -1,6 +1,6 @@ """ This sample demonstrates loading a named environment configuration profile and -overriding its values with environment variables. +programmatically overriding its values. """ import asyncio @@ -12,37 +12,29 @@ async def main(): """ - Demonstrates loading a named profile and overriding it with env vars. + Demonstrates loading a named profile and overriding values programmatically. """ - print("--- Loading 'staging' profile with environment variable overrides ---") + print("--- Loading 'staging' profile with programmatic 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. + print("The 'staging' profile in config.toml has an incorrect address (localhost:9999).") + print("We'll programmatically override it to the correct address.") + + # Load the 'staging' profile. connect_config = ClientConfig.load_client_connect_config( profile=profile_name, config_file=str(config_file), - override_env_vars=override_env, ) + # Override the target host to the correct address. + # This is the recommended way to override configuration values. + connect_config["target_host"] = "localhost:7233" + print(f"\nLoaded '{profile_name}' profile from {config_file} with overrides.") - print(f" Address: {connect_config.get('target_host')}") + print(f" Address: {connect_config.get('target_host')} (overridden from localhost:9999)") 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: From 473de516b87978dcec32271ab9df2f27002b642c Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 11 Aug 2025 23:28:33 -0400 Subject: [PATCH 3/4] linting --- env_config/load_profile.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/env_config/load_profile.py b/env_config/load_profile.py index 6f8fd988..fe4f51cf 100644 --- a/env_config/load_profile.py +++ b/env_config/load_profile.py @@ -19,7 +19,9 @@ async def main(): config_file = Path(__file__).parent / "config.toml" profile_name = "staging" - print("The 'staging' profile in config.toml has an incorrect address (localhost:9999).") + print( + "The 'staging' profile in config.toml has an incorrect address (localhost:9999)." + ) print("We'll programmatically override it to the correct address.") # Load the 'staging' profile. @@ -33,7 +35,9 @@ async def main(): connect_config["target_host"] = "localhost:7233" print(f"\nLoaded '{profile_name}' profile from {config_file} with overrides.") - print(f" Address: {connect_config.get('target_host')} (overridden from localhost:9999)") + print( + f" Address: {connect_config.get('target_host')} (overridden from localhost:9999)" + ) print(f" Namespace: {connect_config.get('namespace')}") print("\nAttempting to connect to client...") From 61fc2fcf890da297cce1998b8088b4df9e941e4d Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 12 Aug 2025 10:41:53 -0400 Subject: [PATCH 4/4] address review --- env_config/README.md | 8 ++++---- env_config/{load_default.py => load_from_file.py} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename env_config/{load_default.py => load_from_file.py} (100%) diff --git a/env_config/README.md b/env_config/README.md index eb1fc60e..6496900a 100644 --- a/env_config/README.md +++ b/env_config/README.md @@ -4,7 +4,7 @@ This directory contains Python samples that demonstrate how to use the Temporal ## 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`. +To run, first see [README.md](../README.md) for prerequisites. ## Configuration File @@ -18,14 +18,14 @@ The `config.toml` file defines three profiles for different environments: The following Python scripts demonstrate different ways to load and use these configuration profiles. Each runnable sample highlights a unique feature. -### `load_default.py` +### `load_from_file.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 +uv run env_config/load_from_file.py ``` ### `load_profile.py` @@ -35,7 +35,7 @@ This sample demonstrates loading the `staging` profile by name (which has an inc **To run this sample:** ```bash -python3 env_config/load_profile.py +uv run env_config/load_profile.py ``` ## Running the Samples diff --git a/env_config/load_default.py b/env_config/load_from_file.py similarity index 100% rename from env_config/load_default.py rename to env_config/load_from_file.py