diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index c09c492b8eda..b612cb2b1b0f 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_32bd63579a" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_3e69808293" } diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_snapshot_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_snapshot_sample.py index 413010a5b789..281061b14b46 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_snapshot_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_snapshot_sample.py @@ -7,49 +7,110 @@ import asyncio from azure.appconfiguration.provider.aio import load from azure.appconfiguration.provider import SettingSelector -from sample_utilities import get_client_modifications +from azure.appconfiguration.aio import AzureAppConfigurationClient # type:ignore +from azure.appconfiguration import ( # type:ignore + ConfigurationSettingsFilter, + ConfigurationSetting, + FeatureFlagConfigurationSetting, +) +from azure.identity.aio import DefaultAzureCredential import os +import uuid async def main(): - kwargs = get_client_modifications() - connection_string = os.environ["APPCONFIGURATION_CONNECTION_STRING"] + endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"] + credential = DefaultAzureCredential() - # Loading configuration settings from a snapshot - # Note: The snapshot must already exist in your App Configuration store - snapshot_name = "my-snapshot-name" + # Step 1: Create a snapshot + # First, we'll create some configuration settings and then create a snapshot containing them + client = AzureAppConfigurationClient(endpoint, credential) + # Create sample configuration settings (these will be included in the snapshot) + sample_settings = [ + ConfigurationSetting(key="app/settings/message", value="Hello from snapshot!"), + ConfigurationSetting(key="app/settings/fontSize", value="14"), + ConfigurationSetting(key="app/settings/backgroundColor", value="#FFFFFF"), + ] + + # Create a feature flag (also included in the snapshot) + sample_feature_flag = FeatureFlagConfigurationSetting( + feature_id="Beta", + enabled=True, + description="Beta feature flag from snapshot sample", + ) + + # Override settings with "prod" label (used in mixed selects, not in snapshot) + override_settings = [ + ConfigurationSetting(key="override.message", value="Production override!", label="prod"), + ConfigurationSetting(key="override.fontSize", value="16", label="prod"), + ] + + print("Creating sample configuration settings...") + for setting in sample_settings: + await client.set_configuration_setting(setting) + print(f" Created: {setting.key} = {setting.value}") + + # Create the feature flag + await client.set_configuration_setting(sample_feature_flag) + print(f" Created feature flag: {sample_feature_flag.feature_id} = {sample_feature_flag.enabled}") + + for setting in override_settings: + await client.set_configuration_setting(setting) + print(f" Created: {setting.key} = {setting.value} (label: {setting.label})") + + # Generate a unique snapshot name + snapshot_name = f"sample-snapshot-{uuid.uuid4().hex[:8]}" + + # Create snapshot with filters for app settings and feature flags (retention_period=3600 seconds = 1 hour) + snapshot_filters = [ + ConfigurationSettingsFilter(key="app/*"), + ConfigurationSettingsFilter(key=".appconfig.featureflag/*"), + ] + + poller = await client.begin_create_snapshot(name=snapshot_name, filters=snapshot_filters, retention_period=3600) + created_snapshot = await poller.result() + print(f"Created snapshot: {created_snapshot.name} with status: {created_snapshot.status}") + + # Step 2: Loading configuration settings from the snapshot snapshot_selects = [SettingSelector(snapshot_name=snapshot_name)] - config = await load(connection_string=connection_string, selects=snapshot_selects, **kwargs) + config = await load(endpoint=endpoint, credential=credential, selects=snapshot_selects) print("Configuration settings from snapshot:") for key, value in config.items(): print(f"{key}: {value}") + await config.close() - # You can also combine snapshot-based selectors with regular selectors - # The snapshot settings and filtered settings will be merged, with later selectors taking precedence + # Step 3: Combine snapshot with regular selectors (later selectors take precedence) mixed_selects = [ SettingSelector(snapshot_name=snapshot_name), # Load all settings from snapshot SettingSelector(key_filter="override.*", label_filter="prod"), # Also load specific override settings ] - config_mixed = await load(connection_string=connection_string, selects=mixed_selects, **kwargs) + config_mixed = await load(endpoint=endpoint, credential=credential, selects=mixed_selects) print("\nMixed configuration (snapshot + filtered settings):") for key, value in config_mixed.items(): print(f"{key}: {value}") + await config_mixed.close() - # Loading feature flags from a snapshot - # To load feature flags from a snapshot, include the snapshot selector in the 'selects' parameter and set feature_flag_enabled=True. + # Step 4: Load feature flags from the snapshot (requires feature_flag_enabled=True) feature_flag_selects = [SettingSelector(snapshot_name=snapshot_name)] config_with_flags = await load( - connection_string=connection_string, + endpoint=endpoint, + credential=credential, selects=feature_flag_selects, feature_flag_enabled=True, - **kwargs, - ) - print( - f"\nConfiguration includes feature flags: {any(key.startswith('.appconfig.featureflag/') for key in config_with_flags.keys())}" ) + print(f"\nFeature flags loaded: {'feature_management' in config_with_flags}") + if "feature_management" in config_with_flags: + feature_flags = config_with_flags["feature_management"].get("feature_flags", []) + for flag in feature_flags: + print(f" {flag['id']}: enabled={flag['enabled']}") + + await client.close() + await config_with_flags.close() + await credential.close() + if __name__ == "__main__": asyncio.run(main()) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/snapshot_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/snapshot_sample.py index 5222879e5431..2f0b650ad74b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/snapshot_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/snapshot_sample.py @@ -3,52 +3,104 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- - -from azure.appconfiguration.provider import load, SettingSelector -from sample_utilities import get_authority, get_credential, get_client_modifications import os +import uuid +from azure.identity import DefaultAzureCredential +from azure.appconfiguration.provider import load, SettingSelector +from azure.appconfiguration import ( # type:ignore + AzureAppConfigurationClient, + ConfigurationSettingsFilter, + ConfigurationSnapshot, + ConfigurationSetting, + FeatureFlagConfigurationSetting, +) + +endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"] +credential = DefaultAzureCredential() + +# Step 1: Create a snapshot +# First, we'll create some configuration settings and then create a snapshot containing them +client = AzureAppConfigurationClient(endpoint, credential) + + +# Create sample configuration settings (these will be included in the snapshot) +sample_settings = [ + ConfigurationSetting(key="app/settings/message", value="Hello from snapshot!"), + ConfigurationSetting(key="app/settings/fontSize", value="14"), + ConfigurationSetting(key="app/settings/backgroundColor", value="#FFFFFF"), +] + +# Create a feature flag (also included in the snapshot) +sample_feature_flag = FeatureFlagConfigurationSetting( + feature_id="Beta", + enabled=True, + description="Beta feature flag from snapshot sample", +) + +# Override settings with "prod" label (used in mixed selects, not in snapshot) +override_settings = [ + ConfigurationSetting(key="override.message", value="Production override!", label="prod"), + ConfigurationSetting(key="override.fontSize", value="16", label="prod"), +] + +print("Creating sample configuration settings...") +for setting in sample_settings: + client.set_configuration_setting(setting) + print(f" Created: {setting.key} = {setting.value}") + +# Create the feature flag +client.set_configuration_setting(sample_feature_flag) +print(f" Created feature flag: {sample_feature_flag.feature_id} = {sample_feature_flag.enabled}") -endpoint = os.environ.get("APPCONFIGURATION_ENDPOINT_STRING") -authority = get_authority(endpoint) -credential = get_credential(authority) -kwargs = get_client_modifications() +for setting in override_settings: + client.set_configuration_setting(setting) + print(f" Created: {setting.key} = {setting.value} (label: {setting.label})") -# Connecting to Azure App Configuration using AAD -config = load(endpoint=endpoint, credential=credential, **kwargs) +# Generate a unique snapshot name +snapshot_name = f"sample-snapshot-{uuid.uuid4().hex[:8]}" -# Loading configuration settings from a snapshot -# Note: The snapshot must already exist in your App Configuration store -snapshot_name = "my-snapshot-name" +# Create snapshot with filters for app settings and feature flags (retention_period=3600 seconds = 1 hour) +snapshot_filters = [ + ConfigurationSettingsFilter(key="app/*"), + ConfigurationSettingsFilter(key=".appconfig.featureflag/*"), +] + +created_snapshot = client.begin_create_snapshot( + name=snapshot_name, filters=snapshot_filters, retention_period=3600 +).result() +print(f"Created snapshot: {created_snapshot.name} with status: {created_snapshot.status}") + + +# Step 2: Loading configuration settings from the snapshot snapshot_selects = [SettingSelector(snapshot_name=snapshot_name)] -config = load(endpoint=endpoint, credential=credential, selects=snapshot_selects, **kwargs) +config = load(endpoint=endpoint, credential=credential, selects=snapshot_selects) print("Configuration settings from snapshot:") for key, value in config.items(): print(f"{key}: {value}") -# You can also combine snapshot-based selectors with regular selectors -# The snapshot settings and filtered settings will be merged, with later selectors taking precedence +# Step 3: Combine snapshot with regular selectors (later selectors take precedence) mixed_selects = [ SettingSelector(snapshot_name=snapshot_name), # Load all settings from snapshot SettingSelector(key_filter="override.*", label_filter="prod"), # Also load specific override settings ] -config_mixed = load(endpoint=endpoint, credential=credential, selects=mixed_selects, **kwargs) +config_mixed = load(endpoint=endpoint, credential=credential, selects=mixed_selects) print("\nMixed configuration (snapshot + filtered settings):") for key, value in config_mixed.items(): print(f"{key}: {value}") -# Loading feature flags from a snapshot -# To load feature flags from a snapshot, include the snapshot selector in the `selects` parameter and set `feature_flag_enabled=True`. +# Step 4: Load feature flags from the snapshot (requires feature_flag_enabled=True) feature_flag_selects = [SettingSelector(snapshot_name=snapshot_name)] config_with_flags = load( endpoint=endpoint, credential=credential, selects=feature_flag_selects, feature_flag_enabled=True, - **kwargs, ) -print( - f"\nConfiguration includes feature flags: {any(key.startswith('.appconfig.featureflag/') for key in config_with_flags.keys())}" -) +print(f"\nFeature flags loaded: {'feature_management' in config_with_flags}") +if "feature_management" in config_with_flags: + feature_flags = config_with_flags["feature_management"].get("feature_flags", []) + for flag in feature_flags: + print(f" {flag['id']}: enabled={flag['enabled']}") diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_snapshots.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_snapshots.py index 50ae76f0bc1c..926542d82872 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_snapshots.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_snapshots.py @@ -168,6 +168,7 @@ async def test_snapshot_selector_parameter_validation_in_provider(self, appconfi feature_flag_selectors=[SettingSelector(snapshot_name="test-snapshot")], ) + @pytest.mark.live_test_only # Needed to fix an azure core dependency compatibility issue @app_config_decorator_async @recorded_by_proxy_async async def test_create_snapshot_and_load_provider(self, appconfiguration_connection_string, **kwargs): diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_snapshots.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_snapshots.py index 11865016978d..3a31c3ac874c 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_snapshots.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_snapshots.py @@ -166,6 +166,7 @@ def test_snapshot_selector_parameter_validation_in_provider(self, appconfigurati feature_flag_selectors=[SettingSelector(snapshot_name="test-snapshot")], ) + @pytest.mark.live_test_only # Needed to fix an azure core dependency compatibility issue @app_config_decorator @recorded_by_proxy def test_create_snapshot_and_load_provider(self, appconfiguration_connection_string, **kwargs): diff --git a/sdk/appconfiguration/azure-appconfiguration/README.md b/sdk/appconfiguration/azure-appconfiguration/README.md index 4b59281924b0..1ba482518ad6 100644 --- a/sdk/appconfiguration/azure-appconfiguration/README.md +++ b/sdk/appconfiguration/azure-appconfiguration/README.md @@ -71,7 +71,7 @@ client = AzureAppConfigurationClient.from_connection_string(CONNECTION_STRING) -#### Use AAD token +#### Use Entra ID token Here we demonstrate using [DefaultAzureCredential][default_cred_ref] to authenticate as a service principal. However, [AzureAppConfigurationClient][configuration_client_class] @@ -79,6 +79,18 @@ accepts any [azure-identity][azure_identity] credential. See the [azure-identity][azure_identity] documentation for more information about other credentials. + + +```python + + ENDPOINT = os.environ["APPCONFIGURATION_ENDPOINT"] + credential = DefaultAzureCredential() + # Create app config client + client = AzureAppConfigurationClient(base_url=ENDPOINT, credential=credential) +``` + + + ##### Create a service principal (optional) This [Azure CLI][azure_cli] snippet shows how to create a new service principal. Before using it, replace "your-application-name" with diff --git a/sdk/appconfiguration/azure-appconfiguration/assets.json b/sdk/appconfiguration/azure-appconfiguration/assets.json index 3d5f769f30fe..dc13685404d8 100644 --- a/sdk/appconfiguration/azure-appconfiguration/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration", - "Tag": "python/appconfiguration/azure-appconfiguration_7b8ff3a790" + "Tag": "python/appconfiguration/azure-appconfiguration_e031d16e39" } diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/hello_world_entra_id_sample.py b/sdk/appconfiguration/azure-appconfiguration/samples/hello_world_entra_id_sample.py new file mode 100644 index 000000000000..e539d26ddea1 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/samples/hello_world_entra_id_sample.py @@ -0,0 +1,70 @@ +# coding: utf-8 + +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +""" +FILE: hello_world_entra_id_sample.py + +DESCRIPTION: + This sample demos how to add/update/retrieve/delete configuration settings synchronously. + +USAGE: python hello_world_entra_id_sample.py + + Set the environment variables with your own values before running the sample: + 1) APPCONFIGURATION_CONNECTION_STRING: Connection String used to access the Azure App Configuration. +""" +import os +from azure.appconfiguration import AzureAppConfigurationClient +from azure.identity import DefaultAzureCredential +from azure.appconfiguration import ConfigurationSetting + + +def main(): + # [START create_app_config_client] + + ENDPOINT = os.environ["APPCONFIGURATION_ENDPOINT"] + credential = DefaultAzureCredential() + # Create app config client + client = AzureAppConfigurationClient(base_url=ENDPOINT, credential=credential) + # [END create_app_config_client] + + print("Add new configuration setting") + # [START create_config_setting] + config_setting = ConfigurationSetting( + key="MyKey", label="MyLabel", value="my value", content_type="my content type", tags={"my tag": "my tag value"} + ) + added_config_setting = client.add_configuration_setting(config_setting) + # [END create_config_setting] + print("New configuration setting:") + print(added_config_setting) + print("") + + print("Set configuration setting") + # [START set_config_setting] + added_config_setting.value = "new value" + added_config_setting.content_type = "new content type" + updated_config_setting = client.set_configuration_setting(added_config_setting) + # [END set_config_setting] + print(updated_config_setting) + print("") + + print("Get configuration setting") + # [START get_config_setting] + fetched_config_setting = client.get_configuration_setting(key="MyKey", label="MyLabel") + # [END get_config_setting] + print("Fetched configuration setting:") + print(fetched_config_setting) + print("") + + print("Delete configuration setting") + # [START delete_config_setting] + client.delete_configuration_setting(key="MyKey", label="MyLabel") + # [END delete_config_setting] + + +if __name__ == "__main__": + main() diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/hello_world_sample_entra_id_and_bleu.py b/sdk/appconfiguration/azure-appconfiguration/samples/hello_world_sample_entra_id_and_bleu.py index 420b650193e2..ad91cf88cd2b 100644 --- a/sdk/appconfiguration/azure-appconfiguration/samples/hello_world_sample_entra_id_and_bleu.py +++ b/sdk/appconfiguration/azure-appconfiguration/samples/hello_world_sample_entra_id_and_bleu.py @@ -23,7 +23,7 @@ 4) AZURE_CLIENT_SECRET: Your application client secret For Azure Bleu (French Sovereign Cloud): - - Use credential_scopes: ["https://appconfig.sovcloud-api.fr/.default"] + - Use audience: ["https://appconfig.sovcloud-api.fr/"] DefaultAzureCredential will attempt multiple authentication methods: - Environment variables (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET) @@ -33,21 +33,20 @@ - Azure PowerShell - Interactive browser """ +import os +from azure.appconfiguration import AzureAppConfigurationClient +from azure.identity import DefaultAzureCredential from azure.appconfiguration import ConfigurationSetting def main(): # [START create_app_config_client_entra_id] - import os - from azure.appconfiguration import AzureAppConfigurationClient - from azure.identity import DefaultAzureCredential - ENDPOINT = os.environ["APPCONFIGURATION_ENDPOINT"] # Create app config client with Entra ID authentication credential = DefaultAzureCredential() client = AzureAppConfigurationClient( - base_url=ENDPOINT, credential=credential, credential_scopes=["https://appconfig.sovcloud-api.fr/.default"] + base_url=ENDPOINT, credential=credential, audience="https://appconfig.sovcloud-api.fr/" ) # [END create_app_config_client_entra_id] diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py index cb73966af567..d52458ab630f 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py @@ -1067,7 +1067,8 @@ def test_list_snapshots(self, appconfiguration_connection_string, **kwargs): set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") self.set_up(appconfiguration_connection_string) - result = self.client.list_snapshots() + # Only list "ready" snapshots to avoid counting archived snapshots that may expire during test runs + result = self.client.list_snapshots(status=["ready"]) initial_snapshots = len(list(result)) variables = kwargs.pop("variables", {}) @@ -1085,7 +1086,7 @@ def test_list_snapshots(self, appconfiguration_connection_string, **kwargs): created_snapshot2 = response2.result() assert created_snapshot2.status == "ready" - result = self.client.list_snapshots() + result = self.client.list_snapshots(status=["ready"]) assert len(list(result)) == initial_snapshots + 2 self.tear_down()