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 new file mode 100644 index 00000000..6496900a --- /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 programmatic overrides, decoupling connection settings from your application code. + +## Prerequisites + +To run, first see [README.md](../README.md) for prerequisites. + +## 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_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 +uv run env_config/load_from_file.py +``` + +### `load_profile.py` + +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:** + +```bash +uv run 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_from_file.py b/env_config/load_from_file.py new file mode 100644 index 00000000..ab3bad14 --- /dev/null +++ b/env_config/load_from_file.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..fe4f51cf --- /dev/null +++ b/env_config/load_profile.py @@ -0,0 +1,52 @@ +""" +This sample demonstrates loading a named environment configuration profile and +programmatically overriding its values. +""" + +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 values programmatically. + """ + print("--- Loading 'staging' profile with programmatic overrides ---") + + config_file = Path(__file__).parent / "config.toml" + profile_name = "staging" + + 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 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')} (overridden from localhost:9999)" + ) + print(f" Namespace: {connect_config.get('namespace')}") + + 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())