diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md index ea7197a37848..ef33cbaba6ff 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md @@ -1,9 +1,12 @@ # Release History -## 1.0.1 (Unreleased) +## 1.1.0b1 (Unreleased) ### Features Added +* New API for Azure App Configuration Provider, `refresh`, which can be used to refresh the configuration from the Azure App Configuration service. `refresh` by default can check every 30 seconds for changes to specified sentinel keys. If a change is detected then all configurations are reloaded. Sentinel keys can be set by passing a list of `SentinelKey`'s to `refresh_on`. +* Added support for customer provided user agent prefix. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index d08f5c03672d..5e0ba3575c54 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_7138010c10" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_91de50b929" } diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 816219c6aa7e..f0bca8bd64aa 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -191,11 +191,12 @@ def _buildprovider( headers = _get_headers(key_vault_options, **kwargs) retry_total = kwargs.pop("retry_total", 2) retry_backoff_max = kwargs.pop("retry_backoff_max", 60) + user_agent = kwargs.pop("user_agent", "") + " " + USER_AGENT if connection_string: provider._client = AzureAppConfigurationClient.from_connection_string( connection_string, - user_agent=USER_AGENT, + user_agent=user_agent, headers=headers, retry_total=retry_total, retry_backoff_max=retry_backoff_max, @@ -205,7 +206,7 @@ def _buildprovider( provider._client = AzureAppConfigurationClient( endpoint, credential, - user_agent=USER_AGENT, + user_agent=user_agent, headers=headers, retry_total=retry_total, retry_backoff_max=retry_backoff_max, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py index 9825929bcbb2..c446f8d6248d 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py @@ -4,4 +4,4 @@ # license information. # ------------------------------------------------------------------------- -VERSION = "1.0.1" +VERSION = "1.1.0b1" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index db7da65f66f3..3cbc0f694176 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -4,7 +4,22 @@ # license information. # ------------------------------------------------------------------------- import json -from typing import Any, Dict, Iterable, Mapping, Optional, overload, List, Tuple, TYPE_CHECKING, Union +from asyncio.locks import Lock +import logging +from typing import ( + Any, + Awaitable, + Callable, + Dict, + Iterable, + Mapping, + Optional, + overload, + List, + Tuple, + TYPE_CHECKING, + Union, +) from azure.appconfiguration import ( # pylint:disable=no-name-in-module FeatureFlagConfigurationSetting, @@ -13,6 +28,8 @@ from azure.appconfiguration.aio import AzureAppConfigurationClient from azure.keyvault.secrets.aio import SecretClient from azure.keyvault.secrets import KeyVaultSecretIdentifier +from azure.core import MatchConditions +from azure.core.exceptions import HttpResponseError, ServiceRequestError, ServiceResponseError from .._models import AzureAppConfigurationKeyVaultOptions, SettingSelector from .._constants import ( @@ -20,7 +37,13 @@ FEATURE_FLAG_PREFIX, EMPTY_LABEL, ) -from .._azureappconfigurationprovider import _is_json_content_type, _get_headers +from .._azureappconfigurationprovider import ( + _is_json_content_type, + _get_headers, + _RefreshTimer, + _build_sentinel, + _is_retryable_error, +) from .._user_agent import USER_AGENT if TYPE_CHECKING: @@ -37,6 +60,9 @@ async def load( selects: Optional[List[SettingSelector]] = None, trim_prefixes: Optional[List[str]] = None, key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = None, + refresh_on: Optional[List[Tuple[str, str]]] = None, + refresh_interval: int = 30, + on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] = None, **kwargs ) -> "AzureAppConfigurationProvider": """ @@ -51,6 +77,14 @@ async def load( :paramtype trim_prefixes: Optional[List[str]] :keyword key_vault_options: Options for resolving Key Vault references :paramtype key_vault_options: ~azure.appconfiguration.provider.AzureAppConfigurationKeyVaultOptions + :keyword refresh_on: One or more settings whose modification will trigger a full refresh after a fixed interval. + This should be a list of Key-Label pairs for specific settings (filters and wildcards are not supported). + :paramtype refresh_on: List[Tuple[str, str]] + :keyword int refresh_interval: The minimum time in seconds between when a call to `refresh` will actually trigger a + service call to update the settings. Default value is 30 seconds. + :paramtype on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] + :keyword on_refresh_error: Optional callback to be invoked when an error occurs while refreshing settings. If not + specified, errors will be raised. """ @@ -61,6 +95,9 @@ async def load( selects: Optional[List[SettingSelector]] = None, trim_prefixes: Optional[List[str]] = None, key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = None, + refresh_on: Optional[List[Tuple[str, str]]] = None, + refresh_interval: int = 30, + on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] = None, **kwargs ) -> "AzureAppConfigurationProvider": """ @@ -73,6 +110,14 @@ async def load( :paramtype trim_prefixes: Optional[List[str]] :keyword key_vault_options: Options for resolving Key Vault references :paramtype key_vault_options: ~azure.appconfiguration.provider.AzureAppConfigurationKeyVaultOptions + :keyword refresh_on: One or more settings whose modification will trigger a full refresh after a fixed interval. + This should be a list of Key-Label pairs for specific settings (filters and wildcards are not supported). + :paramtype refresh_on: List[Tuple[str, str]] + :keyword int refresh_interval: The minimum time in seconds between when a call to `refresh` will actually trigger a + service call to update the settings. Default value is 30 seconds. + :paramtype on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] + :keyword on_refresh_error: Optional callback to be invoked when an error occurs while refreshing settings. If not + specified, errors will be raised. """ @@ -83,9 +128,6 @@ async def load(*args, **kwargs) -> "AzureAppConfigurationProvider": endpoint: Optional[str] = kwargs.pop("endpoint", None) credential: Optional["AsyncTokenCredential"] = kwargs.pop("credential", None) connection_string: Optional[str] = kwargs.pop("connection_string", None) - key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = kwargs.pop("key_vault_options", None) - selects: List[SettingSelector] = kwargs.pop("selects", [SettingSelector(key_filter="*", label_filter=EMPTY_LABEL)]) - trim_prefixes: List[str] = kwargs.pop("trim_prefixes", []) # Update endpoint and credential if specified positionally. if len(args) > 2: @@ -104,63 +146,31 @@ async def load(*args, **kwargs) -> "AzureAppConfigurationProvider": if (endpoint or credential) and connection_string: raise ValueError("Please pass either endpoint and credential, or a connection string.") - provider = _buildprovider(connection_string, endpoint, credential, key_vault_options) + provider = _buildprovider(connection_string, endpoint, credential, **kwargs) + await provider._load_all() - provider._trim_prefixes = sorted(trim_prefixes, key=len, reverse=True) - - for select in selects: - configurations = provider._client.list_configuration_settings( - key_filter=select.key_filter, label_filter=select.label_filter - ) - async for config in configurations: - trimmed_key = config.key - # Trim the key if it starts with one of the prefixes provided - for trim in provider._trim_prefixes: - if config.key.startswith(trim): - trimmed_key = config.key[len(trim) :] - break - - if isinstance(config, SecretReferenceConfigurationSetting): - secret = await _resolve_keyvault_reference(config, key_vault_options, provider) - provider._dict[trimmed_key] = secret - elif isinstance(config, FeatureFlagConfigurationSetting): - feature_management = provider._dict.get(FEATURE_MANAGEMENT_KEY, {}) - if trimmed_key.startswith(FEATURE_FLAG_PREFIX): - feature_management[trimmed_key[len(FEATURE_FLAG_PREFIX) :]] = config.value - else: - feature_management[trimmed_key] = config.value - if FEATURE_MANAGEMENT_KEY not in provider.keys(): - provider._dict[FEATURE_MANAGEMENT_KEY] = feature_management - elif _is_json_content_type(config.content_type): - try: - j_object = json.loads(config.value) - provider._dict[trimmed_key] = j_object - except json.JSONDecodeError: - # If the value is not a valid JSON, treat it like regular string value - provider._dict[trimmed_key] = config.value - else: - provider._dict[trimmed_key] = config.value + # Refresh-All sentinels are not updated on load_all, as they are not necessarily included in the provider. + for (key, label), etag in provider._refresh_on.items(): + if not etag: + sentinel = await provider._client.get_configuration_setting(key, label) + provider._refresh_on[(key, label)] = sentinel.etag return provider def _buildprovider( - connection_string: Optional[str], - endpoint: Optional[str], - credential: Optional["AsyncTokenCredential"], - key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions], - **kwargs + connection_string: Optional[str], endpoint: Optional[str], credential: Optional["AsyncTokenCredential"], **kwargs ) -> "AzureAppConfigurationProvider": # pylint:disable=protected-access - provider = AzureAppConfigurationProvider() - headers = _get_headers(key_vault_options, **kwargs) - + provider = AzureAppConfigurationProvider(**kwargs) + headers = _get_headers(provider._key_vault_options, **kwargs) retry_total = kwargs.pop("retry_total", 2) retry_backoff_max = kwargs.pop("retry_backoff_max", 60) + user_agent = kwargs.pop("user_agent", "") + " " + USER_AGENT if connection_string: provider._client = AzureAppConfigurationClient.from_connection_string( connection_string, - user_agent=USER_AGENT, + user_agent=user_agent, headers=headers, retry_total=retry_total, retry_backoff_max=retry_backoff_max, @@ -170,7 +180,7 @@ def _buildprovider( provider._client = AzureAppConfigurationClient( endpoint, credential, - user_agent=USER_AGENT, + user_agent=user_agent, headers=headers, retry_total=retry_total, retry_backoff_max=retry_backoff_max, @@ -179,10 +189,9 @@ def _buildprovider( return provider -async def _resolve_keyvault_reference( - config, key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions], provider: "AzureAppConfigurationProvider" -) -> str: - if key_vault_options is None: +async def _resolve_keyvault_reference(config, provider: "AzureAppConfigurationProvider") -> str: + # pylint:disable=protected-access + if provider._key_vault_options is None: raise ValueError("Key Vault options must be set to resolve Key Vault references.") if config.secret_id is None: @@ -195,8 +204,8 @@ async def _resolve_keyvault_reference( # pylint:disable=protected-access referenced_client = provider._secret_clients.get(vault_url, None) - vault_config = key_vault_options.client_configs.get(vault_url, {}) - credential = vault_config.pop("credential", key_vault_options.credential) + vault_config = provider._key_vault_options.client_configs.get(vault_url, {}) + credential = vault_config.pop("credential", provider._key_vault_options.credential) if referenced_client is None and credential is not None: referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) @@ -207,8 +216,8 @@ async def _resolve_keyvault_reference( await referenced_client.get_secret(key_vault_identifier.name, version=key_vault_identifier.version) ).value - if key_vault_options.secret_resolver is not None: - resolved = key_vault_options.secret_resolver(config.secret_id) + if provider._key_vault_options.secret_resolver is not None: + resolved = provider._key_vault_options.secret_resolver(config.secret_id) try: # Secret resolver was async return await resolved @@ -226,11 +235,113 @@ class AzureAppConfigurationProvider(Mapping[str, Union[str, JSON]]): keys. Enables resolution of Key Vault references in configuration settings. """ - def __init__(self) -> None: + def __init__(self, **kwargs) -> None: self._dict: Dict[str, str] = {} self._trim_prefixes: List[str] = [] self._client: Optional[AzureAppConfigurationClient] = None self._secret_clients: Dict[str, SecretClient] = {} + self._key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = kwargs.pop("key_vault_options", None) + self._selects: List[SettingSelector] = kwargs.pop( + "selects", [SettingSelector(key_filter="*", label_filter=EMPTY_LABEL)] + ) + + trim_prefixes: List[str] = kwargs.pop("trim_prefixes", []) + self._trim_prefixes = sorted(trim_prefixes, key=len, reverse=True) + + refresh_on: List[Tuple[str, str]] = kwargs.pop("refresh_on", None) or [] + self._refresh_on: Mapping[Tuple[str, str] : Optional[str]] = {_build_sentinel(s): None for s in refresh_on} + self._refresh_timer: _RefreshTimer = _RefreshTimer(**kwargs) + self._on_refresh_error: Optional[Callable[[Exception], None]] = kwargs.pop("on_refresh_error", None) + self._update_lock = Lock() + + async def refresh(self, **kwargs) -> None: + if not self._refresh_on: + logging.debug("Refresh called but no refresh options set.") + return + + try: + async with self._update_lock: + if not self._refresh_timer.needs_refresh(): + logging.debug("Refresh called but refresh interval not elapsed.") + return + for (key, label), etag in self._refresh_on.items(): + updated_sentinel = await self._client.get_configuration_setting( + key=key, label=label, etag=etag, match_condition=MatchConditions.IfModified, **kwargs + ) + if updated_sentinel is not None: + logging.debug( + "Refresh all triggered by key: %s label %s.", + key, + label, + ) + await self._load_all(**kwargs) + self._refresh_on[(key, label)] = updated_sentinel.etag + self._refresh_timer.reset() + return + except (ServiceRequestError, ServiceResponseError) as e: + logging.debug("Failed to refresh, retrying: %r", e) + self._refresh_timer.retry() + except HttpResponseError as e: + # If we get an error we should retry sooner than the next refresh interval + self._refresh_timer.retry() + if _is_retryable_error(e): + return + if self._on_refresh_error: + await self._on_refresh_error(e) + return + raise + except Exception as e: + if self._on_refresh_error: + await self._on_refresh_error(e) + return + raise + + async def _load_all(self, **kwargs): + configuration_settings = {} + for select in self._selects: + configurations = self._client.list_configuration_settings( + key_filter=select.key_filter, label_filter=select.label_filter, **kwargs + ) + async for config in configurations: + key = self._process_key_name(config) + value = await self._process_key_value(config) + + if isinstance(config, FeatureFlagConfigurationSetting): + feature_management = configuration_settings.get(FEATURE_MANAGEMENT_KEY, {}) + feature_management[key] = value + if FEATURE_MANAGEMENT_KEY not in configuration_settings: + configuration_settings[FEATURE_MANAGEMENT_KEY] = feature_management + else: + configuration_settings[key] = value + # Every time we run load_all, we should update the etag of our refresh sentinels + # so they stay up-to-date. + # Sentinel keys will have unprocessed key names, so we need to use the original key. + if (config.key, config.label) in self._refresh_on: + self._refresh_on[(config.key, config.label)] = config.etag + self._dict = configuration_settings + + def _process_key_name(self, config): + trimmed_key = config.key + # Trim the key if it starts with one of the prefixes provided + for trim in self._trim_prefixes: + if config.key.startswith(trim): + trimmed_key = config.key[len(trim) :] + break + if isinstance(config, FeatureFlagConfigurationSetting) and trimmed_key.startswith(FEATURE_FLAG_PREFIX): + return trimmed_key[len(FEATURE_FLAG_PREFIX) :] + return trimmed_key + + async def _process_key_value(self, config): + if isinstance(config, SecretReferenceConfigurationSetting): + return await _resolve_keyvault_reference(config, self) + if _is_json_content_type(config.content_type) and not isinstance(config, FeatureFlagConfigurationSetting): + # Feature flags are of type json, but don't treat them as such + try: + return json.loads(config.value) + except json.JSONDecodeError: + # If the value is not a valid JSON, treat it like regular string value + return config.value + return config.value def __getitem__(self, key: str) -> str: # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py index 355d3db169c7..7d07a3991da6 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py @@ -22,12 +22,21 @@ async def create_aad_client( trim_prefixes=[], selects={SettingSelector(key_filter="*", label_filter="\0")}, keyvault_secret_url=None, + refresh_on=None, + refresh_interval=30, ): cred = self.get_credential(AzureAppConfigurationClient, is_async=True) + client = AzureAppConfigurationClient(appconfiguration_endpoint_string, cred) await setup_configs(client, keyvault_secret_url) return await load( - credential=cred, endpoint=appconfiguration_endpoint_string, trim_prefixes=trim_prefixes, selects=selects + credential=cred, + endpoint=appconfiguration_endpoint_string, + trim_prefixes=trim_prefixes, + selects=selects, + refresh_on=refresh_on, + refresh_interval=refresh_interval, + user_agent="SDK/Integration", ) async def create_client( @@ -36,11 +45,18 @@ async def create_client( trim_prefixes=[], selects={SettingSelector(key_filter="*", label_filter="\0")}, keyvault_secret_url=None, + refresh_on=None, + refresh_interval=30, ): client = AzureAppConfigurationClient.from_connection_string(appconfiguration_connection_string) await setup_configs(client, keyvault_secret_url) return await load( - connection_string=appconfiguration_connection_string, trim_prefixes=trim_prefixes, selects=selects + connection_string=appconfiguration_connection_string, + trim_prefixes=trim_prefixes, + selects=selects, + refresh_on=refresh_on, + refresh_interval=refresh_interval, + user_agent="SDK/Integration", ) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/conftest.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/conftest.py index 6681c92f63ff..9f15289561c6 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/conftest.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/conftest.py @@ -33,5 +33,7 @@ def add_sanitizers(test_proxy): value="https://fake-key-vault.vault.azure.net/", target=os.environ.get("APPCONFIGURATION_KEY_VAULT_REFERENCE", "https://fake-key-vault.vault.azure.net/"), ) - set_custom_default_matcher(ignored_headers="x-ms-content-sha256") + + add_general_regex_sanitizer(value="api-version=1970-01-01", regex="api-version=.+") + set_custom_default_matcher(ignored_headers="x-ms-content-sha256, Accept") add_oauth_response_sanitizer() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py index 27c9c577ec23..7aa7af9affbc 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py @@ -20,10 +20,8 @@ async def test_provider_creation(self, appconfiguration_connection_string, appco ) as client: assert client["message"] == "hi" assert client["my_json"]["key"] == "value" - assert ( - client["FeatureManagementFeatureFlags"]["Alpha"] - == '{"enabled": false, "conditions": {"client_filters": []}}' - ) + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] # method: provider_trim_prefixes @app_config_decorator_async @@ -41,10 +39,8 @@ async def test_provider_trim_prefixes( assert client["my_json"]["key"] == "value" assert client["trimmed"] == "key" assert "test.trimmed" not in client - assert ( - client["FeatureManagementFeatureFlags"]["Alpha"] - == '{"enabled": false, "conditions": {"client_filters": []}}' - ) + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] # method: provider_selectors @app_config_decorator_async diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_aad.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_aad.py index 41fe6fb04cef..db451fa41fde 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_aad.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_aad.py @@ -21,10 +21,8 @@ async def test_provider_creation_aad(self, appconfiguration_endpoint_string, app ) as client: assert client.get("message") == "hi" assert client["my_json"]["key"] == "value" - assert ( - client["FeatureManagementFeatureFlags"]["Alpha"] - == '{"enabled": false, "conditions": {"client_filters": []}}' - ) + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] # method: provider_trim_prefixes @app_config_decorator_async @@ -39,11 +37,8 @@ async def test_provider_trim_prefixes(self, appconfiguration_endpoint_string, ap assert client["message"] == "hi" assert client["my_json"]["key"] == "value" assert client["trimmed"] == "key" - assert "test.trimmed" not in client - assert ( - client["FeatureManagementFeatureFlags"]["Alpha"] - == '{"enabled": false, "conditions": {"client_filters": []}}' - ) + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] # method: provider_selectors @app_config_decorator_async diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py new file mode 100644 index 000000000000..03c83fcf8020 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py @@ -0,0 +1,98 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import time +import unittest +from azure.appconfiguration.provider import SettingSelector, SentinelKey +from azure.appconfiguration.provider.aio import load +from devtools_testutils.aio import recorded_by_proxy_async +from azure.appconfiguration.aio import AzureAppConfigurationClient +from async_preparers import app_config_decorator_async +from asynctestcase import AppConfigTestCase + + +class TestAppConfigurationProvider(AppConfigTestCase, unittest.TestCase): + # method: refresh + @app_config_decorator_async + @recorded_by_proxy_async + async def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url): + async with await self.create_aad_client( + appconfiguration_endpoint_string, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + refresh_on=[SentinelKey("refresh_message")], + refresh_interval=1, + ) as client: + assert client["refresh_message"] == "original value" + assert client["my_json"]["key"] == "value" + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] + setting = await client._client.get_configuration_setting(key="refresh_message") + setting.value = "updated value" + await client._client.set_configuration_setting(setting) + + # Waiting for the refresh interval to pass + time.sleep(2) + + await client.refresh() + assert client["refresh_message"] == "updated value" + + setting.value = "original value" + await client._client.set_configuration_setting(setting) + + # Waiting for the refresh interval to pass + time.sleep(2) + + await client.refresh() + assert client["refresh_message"] == "original value" + + setting.value = "updated value 2" + await client._client.set_configuration_setting(setting) + + # Not waiting for the refresh interval to pass + await client.refresh() + assert client["refresh_message"] == "original value" + + setting.value = "original value" + await client._client.set_configuration_setting(setting) + + await client.refresh() + assert client["refresh_message"] == "original value" + + # method: refresh + @app_config_decorator_async + @recorded_by_proxy_async + async def test_empty_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url): + async with await self.create_aad_client( + appconfiguration_endpoint_string, keyvault_secret_url=appconfiguration_keyvault_secret_url + ) as client: + assert client["refresh_message"] == "original value" + assert client["non_refreshed_message"] == "Static" + assert client["my_json"]["key"] == "value" + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] + setting = await client._client.get_configuration_setting(key="refresh_message") + setting.value = "updated value" + await client._client.set_configuration_setting(setting) + static_setting = await client._client.get_configuration_setting(key="non_refreshed_message") + static_setting.value = "updated static" + await client._client.set_configuration_setting(static_setting) + + # Waiting for the refresh interval to pass + time.sleep(2) + + await client.refresh() + assert client["refresh_message"] == "original value" + assert client["non_refreshed_message"] == "Static" + + setting.value = "original value" + await client._client.set_configuration_setting(setting) + static_setting.value = "Static" + await client._client.set_configuration_setting(static_setting) + + def my_callback(self): + assert True + + def my_callback_on_fail(self): + assert False diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py index 3b04c5f2af4c..d7a39c4a8bdf 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py @@ -19,10 +19,8 @@ def test_provider_creation(self, appconfiguration_connection_string, appconfigur ) assert client["message"] == "hi" assert client["my_json"]["key"] == "value" - assert ( - client["FeatureManagementFeatureFlags"]["Alpha"] - == '{"enabled": false, "conditions": {"client_filters": []}}' - ) + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] # method: provider_trim_prefixes @recorded_by_proxy @@ -38,10 +36,8 @@ def test_provider_trim_prefixes(self, appconfiguration_connection_string, appcon assert client["my_json"]["key"] == "value" assert client["trimmed"] == "key" assert "test.trimmed" not in client - assert ( - client["FeatureManagementFeatureFlags"]["Alpha"] - == '{"enabled": false, "conditions": {"client_filters": []}}' - ) + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] # method: provider_selectors @recorded_by_proxy diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_aad.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_aad.py index 034f8c180f89..6399845380fd 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_aad.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_aad.py @@ -19,10 +19,8 @@ def test_provider_creation_aad(self, appconfiguration_endpoint_string, appconfig ) assert client["message"] == "hi" assert client["my_json"]["key"] == "value" - assert ( - client["FeatureManagementFeatureFlags"]["Alpha"] - == '{"enabled": false, "conditions": {"client_filters": []}}' - ) + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] # method: provider_trim_prefixes @recorded_by_proxy @@ -38,10 +36,8 @@ def test_provider_trim_prefixes(self, appconfiguration_endpoint_string, appconfi assert client["my_json"]["key"] == "value" assert client["trimmed"] == "key" assert "test.trimmed" not in client - assert ( - client["FeatureManagementFeatureFlags"]["Alpha"] - == '{"enabled": false, "conditions": {"client_filters": []}}' - ) + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] # method: provider_selectors @recorded_by_proxy diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py index 0ebd8e343dcf..e28c35d5918e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py @@ -25,10 +25,9 @@ def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvau ) assert client["refresh_message"] == "original value" assert client["my_json"]["key"] == "value" - assert ( - client["FeatureManagementFeatureFlags"]["Alpha"] - == '{"enabled": false, "conditions": {"client_filters": []}}' - ) + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] + setting = client._client.get_configuration_setting(key="refresh_message") setting.value = "updated value" client._client.set_configuration_setting(setting) @@ -71,10 +70,9 @@ def test_empty_refresh(self, appconfiguration_endpoint_string, appconfiguration_ assert client["refresh_message"] == "original value" assert client["non_refreshed_message"] == "Static" assert client["my_json"]["key"] == "value" - assert ( - client["FeatureManagementFeatureFlags"]["Alpha"] - == '{"enabled": false, "conditions": {"client_filters": []}}' - ) + assert "FeatureManagementFeatureFlags" in client + assert "Alpha" in client["FeatureManagementFeatureFlags"] + setting = client._client.get_configuration_setting(key="refresh_message") setting.value = "updated value" client._client.set_configuration_setting(setting) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py index 06e6107ddca5..13552fbe35bf 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py @@ -31,6 +31,7 @@ def create_aad_client( selects=selects, refresh_on=refresh_on, refresh_interval=refresh_interval, + user_agent="SDK/Integration", ) def create_client( @@ -50,6 +51,7 @@ def create_client( selects=selects, refresh_on=refresh_on, refresh_interval=refresh_interval, + user_agent="SDK/Integration", )