diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md index 3007731bf9e6..27fcdd81e5f1 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md @@ -3,15 +3,19 @@ ## 1.0.0b2 (Unreleased) ### Features Added - +* Added Async Support +* Added missing methods for Mapping API * Made load method properties unordered. ### Breaking Changes +* Changes how load works. Moves if from AzureAppConfigurationProvider.load to load_provider. +* Removed custom Key Vault Error +* Removed unneeded __repr__ and copy methods. ### Bugs Fixed ### Other Changes - +* Updated method docs * Fixed load doc that used `selector` instead of `selects`. * Fixed CLI link in Readme. diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py index a29ac854e457..a6044d7c6a86 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py @@ -4,11 +4,15 @@ # license information. # ------------------------------------------------------------------------- -from ._azureappconfigurationprovider import AzureAppConfigurationProvider -from ._azureappconfigurationkeyvaultoptions import AzureAppConfigurationKeyVaultOptions -from ._settingselector import SettingSelector +from ._azureappconfigurationprovider import load_provider, AzureAppConfigurationProvider +from ._models import AzureAppConfigurationKeyVaultOptions, SettingSelector from ._version import VERSION __version__ = VERSION -__all__ = ["AzureAppConfigurationProvider", "AzureAppConfigurationKeyVaultOptions", "SettingSelector"] +__all__ = [ + "load_provider", + "AzureAppConfigurationProvider", + "AzureAppConfigurationKeyVaultOptions", + "SettingSelector" +] diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azure_appconfiguration_provider_error.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azure_appconfiguration_provider_error.py deleted file mode 100644 index 3c7021cb71ba..000000000000 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azure_appconfiguration_provider_error.py +++ /dev/null @@ -1,9 +0,0 @@ -# ------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# ------------------------------------------------------------------------- - - -class KeyVaultReferenceError(ValueError): - """Raised when a Key Vault reference is invalid.""" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationkeyvaultoptions.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationkeyvaultoptions.py deleted file mode 100644 index 7a6519e1b9be..000000000000 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationkeyvaultoptions.py +++ /dev/null @@ -1,29 +0,0 @@ -# ------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# ------------------------------------------------------------------------- - - -class AzureAppConfigurationKeyVaultOptions: - """ - Options for connecting to Key Vault. - - :param credential: A credential for authenticating with the key vault. This is optional if secret_clients is - provided. - :type credential: ~azure.core.credentials.TokenCredential - :param secret_clients: A list of SecretClient from azure-keyvault-secrets. This is optional if credential is - provided. - :type secret_clients: list[~azure.keyvault.secrets.SecretClient] - :param secret_resolver: A function that takes a URI and returns a value. - :type secret_resolver: callable - """ - - def __init__(self, credential=None, secret_clients=None, secret_resolver=None): - # type: (TokenCredential, List[SecretClient], Callable) -> None - self.credential = credential - self.secret_clients = secret_clients - self.secret_resolver = secret_resolver - - if self.secret_clients is None: - self.secret_clients = {} 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 85ca819d5a01..db9d50e5d6af 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -3,209 +3,334 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- - +import os import json -from azure.appconfiguration import AzureAppConfigurationClient +from typing import Any, Dict, Iterable, Mapping, Optional, overload, List, Tuple, TYPE_CHECKING +from azure.appconfiguration import ( + AzureAppConfigurationClient, + FeatureFlagConfigurationSetting, + SecretReferenceConfigurationSetting +) from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier -from azure.core.exceptions import ResourceNotFoundError -from ._settingselector import SettingSelector -from ._azure_appconfiguration_provider_error import KeyVaultReferenceError -from ._constants import KEY_VAULT_REFERENCE_CONTENT_TYPE +from ._models import AzureAppConfigurationKeyVaultOptions, SettingSelector +from ._constants import ( + FEATURE_MANAGEMENT_KEY, + ServiceFabricEnvironmentVariable, + AzureFunctionEnvironmentVariable, + AzureWebAppEnvironmentVariable, + ContainerAppEnvironmentVariable, + KubernetesEnvironmentVariable +) from ._user_agent import USER_AGENT +if TYPE_CHECKING: + from azure.core.credentials import TokenCredential + -class AzureAppConfigurationProvider: +@overload +def load_provider( + endpoint: str, + credential: "TokenCredential", + *, + selects: Optional[List[SettingSelector]] = None, + trimmed_key_prefixes: Optional[List[str]] = None, + key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = None, + **kwargs + ) -> "AzureAppConfigurationProvider": """ - Provides a dictionary-like interface to Azure App Configuration settings. Enables loading of sets of configuration - settings from Azure App Configuration into a Python application. Enables trimming of prefixes from configuration - keys. Enables resolution of Key Vault references in configuration settings. + Loads configuration settings from Azure App Configuration into a Python application. + + :param str endpoint: Endpoint for App Configuration resource. + :param credential: Credential for App Configuration resource. + :type credential: ~azure.core.credentials.TokenCredential + :keyword selects: List of setting selectors to filter configuration settings + :paramtype selects: Optional[List[~azure.appconfiguration.provider.SettingSelector]] + :keyword trimmed_key_prefixes: List of prefixes to trim from configuration keys + :paramtype trimmed_key_prefixes: Optional[List[str]] + :keyword key_vault_options: Options for resolving Key Vault references + :paramtype key_vault_options: ~azure.appconfiguration.provider.AzureAppConfigurationKeyVaultOptions """ + ... + +@overload +def load_provider( + *, + connection_string: str, + selects: Optional[List[SettingSelector]] = None, + trimmed_key_prefixes: Optional[List[str]] = None, + key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = None, + **kwargs + ) -> "AzureAppConfigurationProvider": + """ + Loads configuration settings from Azure App Configuration into a Python application. + + :keyword str connection_string: Connection string for App Configuration resource. + :keyword selects: List of setting selectors to filter configuration settings + :paramtype selects: Optional[List[~azure.appconfiguration.provider.SettingSelector]] + :keyword trimmed_key_prefixes: List of prefixes to trim from configuration keys + :paramtype trimmed_key_prefixes: Optional[List[str]] + :keyword key_vault_options: Options for resolving Key Vault references + :paramtype key_vault_options: ~azure.appconfiguration.provider.AzureAppConfigurationKeyVaultOptions + """ + ... + +def load_provider(*args, **kwargs) -> "AzureAppConfigurationProvider": + #pylint:disable=protected-access + + # Start by parsing kwargs + endpoint: Optional[str] = kwargs.pop("endpoint", None) + credential: Optional["TokenCredential"] = 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("*", "\0")]) + trim_prefixes : List[str] = kwargs.pop("trimmed_key_prefixes", []) + + # Update endpoint and credential if specified positionally. + if len(args) > 2: + raise TypeError( + "Unexpected positional parameters. Please pass either endpoint and credential, or a connection string." + ) + if len(args) == 1: + if endpoint is not None: + raise TypeError("Received multiple values for parameter 'endpoint'.") + endpoint = args[0] + elif len(args) == 2: + if credential is not None: + raise TypeError("Received multiple values for parameter 'credential'.") + endpoint, credential = args - def __init__(self): - # type: () -> None - self._dict = {} - self._trim_prefixes = [] - self._client = None - - @classmethod - def load(cls, *, connection_string=None, endpoint=None, credential=None, **kwargs): - """ - Loads configuration settings from Azure App Configuration into a Python application. - - :keyword connection_string: Connection string (one of connection_string or endpoint and credential must be set) - :type connection_string: str - :keyword endpoint: Endpoint (one of connection_string or endpoint and credential must be set) - :type endpoint: str - :keyword credential: Credential (one of connection_string or endpoint and credential must be set) - :type credential: Union[AppConfigConnectionStringCredential, TokenCredential] - :keyword selects: List of setting selectors to filter configuration settings - :type selects: list[~azure.appconfigurationprovider.SettingSelector] - :keyword trim_prefixes: List of prefixes to trim from configuration keys - :type trim_prefixes: list[str] - :keyword key_vault_options: Options for resolving Key Vault references - :type key_vault_options: ~azure.appconfigurationprovider.KeyVaultOptions - """ - provider = AzureAppConfigurationProvider() - - key_vault_options = kwargs.pop("key_vault_options", None) - - provider.__buildprovider(connection_string, endpoint, credential, key_vault_options) - - selects = kwargs.pop("selects", {SettingSelector("*", "\0")}) - - provider._trim_prefixes = sorted(kwargs.pop("trimmed_key_prefixes", []), key=len, reverse=True) - - provider._dict = {} + if (endpoint or credential) and connection_string: + raise ValueError("Please pass either endpoint and credential, or a connection string.") - secret_clients = key_vault_options.secret_clients if key_vault_options else {} - for select in selects: - configurations = provider._client.list_configuration_settings( - key_filter=select.key_filter, label_filter=select.label_filter - ) - for config in configurations: + provider = _buildprovider(connection_string, endpoint, credential, key_vault_options, **kwargs) + provider._trim_prefixes = sorted(trim_prefixes, key=len, reverse=True) - 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 key_vault_options is not None and len(key_vault_options.secret_clients) > 0: + for secret_client in key_vault_options.secret_clients: + provider._secret_clients[secret_client.vault_url] = secret_client - if config.content_type == KEY_VAULT_REFERENCE_CONTENT_TYPE: - secret = provider.__resolve_keyvault_reference(config, key_vault_options, secret_clients) - provider._dict[trimmed_key] = secret - elif provider.__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: + for select in selects: + configurations = provider._client.list_configuration_settings( + key_filter=select.key_filter, label_filter=select.label_filter + ) + 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 = _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, {}) + 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 + return provider + + +def _get_correlation_context(key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions]) -> str: + correlation_context = "RequestType=Startup" + if key_vault_options and ( + key_vault_options.credential or key_vault_options.secret_clients or key_vault_options.secret_resolver + ): + correlation_context += ",UsesKeyVault" + host_type = "" + if os.environ.get(AzureFunctionEnvironmentVariable) is not None: + host_type = "AzureFunctions" + elif os.environ.get(AzureWebAppEnvironmentVariable) is not None: + host_type = "AzureWebApps" + elif os.environ.get(ContainerAppEnvironmentVariable) is not None: + host_type = "ContainerApps" + elif os.environ.get(KubernetesEnvironmentVariable) is not None: + host_type = "Kubernetes" + elif os.environ.get(ServiceFabricEnvironmentVariable) is not None: + host_type = "ServiceFabric" + if host_type: + correlation_context += ",Host=" + host_type + return correlation_context + + +def _buildprovider( + connection_string: Optional[str], + endpoint: Optional[str], + credential: Optional["TokenCredential"], + key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions], + **kwargs + ) -> "AzureAppConfigurationProvider": + #pylint:disable=protected-access + provider = AzureAppConfigurationProvider() + headers = kwargs.pop("headers", {}) + headers["Correlation-Context"] = _get_correlation_context(key_vault_options) + useragent = USER_AGENT + + if connection_string: + provider._client = AzureAppConfigurationClient.from_connection_string( + connection_string, user_agent=useragent, headers=headers, **kwargs + ) return provider + provider._client = AzureAppConfigurationClient( + endpoint, credential, user_agent=useragent, headers=headers, **kwargs + ) + return provider - def __buildprovider(self, connection_string, endpoint, credential, key_vault_options): - headers = {} - correlation_context = "RequestType=Startup" - if key_vault_options and ( - key_vault_options.credential or key_vault_options.secret_clients or key_vault_options.secret_resolver - ): - correlation_context += ",UsesKeyVault" +def _resolve_keyvault_reference( + config, + key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions], + provider: "AzureAppConfigurationProvider" + ) -> str: + if key_vault_options is None: + raise ValueError("Key Vault options must be set to resolve Key Vault references.") - headers["Correlation-Context"] = correlation_context - useragent = USER_AGENT + if config.secret_id is None: + raise ValueError("Key Vault reference must have a uri value.") - if connection_string and endpoint: - raise AttributeError("Both connection_string and endpoint are set. Only one of these should be set.") + key_vault_identifier = KeyVaultSecretIdentifier(config.secret_id) - if connection_string: - self._client = AzureAppConfigurationClient.from_connection_string( - connection_string, user_agent=useragent, headers=headers - ) - return - self._client = AzureAppConfigurationClient(endpoint, credential, user_agent=useragent, headers=headers) + #pylint:disable=protected-access + referenced_client = provider._secret_clients.get(key_vault_identifier.vault_url, None) - @staticmethod - def __resolve_keyvault_reference(config, key_vault_options, secret_clients): - if key_vault_options is None: - raise AttributeError("Key Vault options must be set to resolve Key Vault references.") + if referenced_client is None and key_vault_options.credential is not None: + referenced_client = SecretClient( + vault_url=key_vault_identifier.vault_url, credential=key_vault_options.credential + ) + provider._secret_clients[key_vault_identifier.vault_url] = referenced_client - if config.secret_id is None: - raise AttributeError("Key Vault reference must have a uri value.") + if referenced_client: + return referenced_client.get_secret(key_vault_identifier.name, version=key_vault_identifier.version).value - key_vault_identifier = KeyVaultSecretIdentifier(config.secret_id) + if key_vault_options.secret_resolver is not None: + return key_vault_options.secret_resolver(config.secret_id) - referenced_client = next( - (client for client in secret_clients if client.vault_url == key_vault_identifier.vault_url), None - ) + raise ValueError( + "No Secret Client found for Key Vault reference %s" % (key_vault_identifier.vault_url) + ) - if referenced_client is None and key_vault_options.credential is not None: - referenced_client = SecretClient( - vault_url=key_vault_identifier.vault_url, credential=key_vault_options.credential - ) - secret_clients[key_vault_identifier.vault_url] = referenced_client - - if referenced_client: - try: - return referenced_client.get_secret( - key_vault_identifier.name, version=key_vault_identifier.version - ).value - except ResourceNotFoundError: - raise KeyVaultReferenceError( - "Key Vault %s does not contain secret %s" - % (key_vault_identifier.vault_url, key_vault_identifier.name) - ) - - if key_vault_options.secret_resolver is not None: - return key_vault_options.secret_resolver(config.secret_id) - raise KeyVaultReferenceError( - "No Secret Client found for Key Vault reference %s" % (key_vault_identifier.vault_url) - ) - @staticmethod - def __is_json_content_type(content_type): - if not content_type: - return False +def _is_json_content_type(content_type: str) -> bool: + if not content_type: + return False - content_type = content_type.strip().lower() - mime_type = content_type.split(";")[0].strip() + content_type = content_type.strip().lower() + mime_type = content_type.split(";")[0].strip() - type_parts = mime_type.split("/") - if len(type_parts) != 2: - return False + type_parts = mime_type.split("/") + if len(type_parts) != 2: + return False - (main_type, sub_type) = type_parts - if main_type != "application": - return False + (main_type, sub_type) = type_parts + if main_type != "application": + return False - sub_types = sub_type.split("+") - if "json" in sub_types: - return True + sub_types = sub_type.split("+") + if "json" in sub_types: + return True - return False + return False - def __getitem__(self, key): - return self._dict[key] - def __repr__(self): - return repr(self._dict) +class AzureAppConfigurationProvider(Mapping[str, str]): + """ + Provides a dictionary-like interface to Azure App Configuration settings. Enables loading of sets of configuration + settings from Azure App Configuration into a Python application. Enables trimming of prefixes from configuration + keys. Enables resolution of Key Vault references in configuration settings. + """ - def __len__(self): - return len(self._dict) + def __init__(self) -> None: + self._dict: Dict[str, str] = {} + self._trim_prefixes: List[str] = [] + self._client: Optional[AzureAppConfigurationClient] = None + self._secret_clients: Dict[str, SecretClient] = {} - def copy(self): + def __getitem__(self, key: str) -> str: """ - Returns a copy of the configuration settings - - type: () -> dict + Returns the value of the specified key. """ - return self._dict.copy() + return self._dict[key] - def __contains__(self, __x: object): - """ - Returns True if the configuration settings contains the specified key + def __iter__(self) -> Iterable[str]: + return self._dict.__iter__() + + def __len__(self) -> int: + return len(self._dict) - type: (object) -> bool + def __contains__(self, __x: object) -> bool: + """ + Returns True if the configuration settings contains the specified key. """ return self._dict.__contains__(__x) - def keys(self): + def keys(self) -> Iterable[str]: """ Returns a list of keys loaded from Azure App Configuration. - - type: () -> list """ return self._dict.keys() - def values(self): + def items(self) -> Iterable[Tuple[str, str]]: + """ + Returns a list of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault references + will be resolved. + """ + return self._dict.items() + + def values(self) -> Iterable[str]: """ Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be resolved. - - type: () -> list """ return self._dict.values() + + def get(self, key: str, default: Optional[str] = None) -> str: + """ + Returns the value of the specified key. If the key does not exist, returns the default value. + """ + return self._dict.get(key, default) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, AzureAppConfigurationProvider): + return False + if self._dict != other._dict: + return False + if self._trim_prefixes != other._trim_prefixes: + return False + if self._client != other._client: + return False + return True + + def __ne__(self, other: Any) -> bool: + return not self == other + + def close(self) -> None: + """ + Closes the connection to Azure App Configuration. + """ + for client in self._secret_clients.values(): + client.close() + self._client.close() + + def __enter__(self) -> "AzureAppConfigurationProvider": + self._client.__enter__() + for client in self._secret_clients.values(): + client.__enter__() + return self + + def __exit__(self, *args) -> None: + self._client.__exit__(*args) + for client in self._secret_clients.values(): + client.__exit__() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py index eb9425333a1d..201cd904562b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -4,4 +4,11 @@ # license information. # ------------------------------------------------------------------------- -KEY_VAULT_REFERENCE_CONTENT_TYPE = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8" +FEATURE_MANAGEMENT_KEY = "FeatureManagement" + +RequestTracingDisabledEnvironmentVariable = "AZURE_APP_CONFIGURATION_TRACING_DISABLED" +AzureFunctionEnvironmentVariable = "FUNCTIONS_EXTENSION_VERSION" +AzureWebAppEnvironmentVariable = "WEBSITE_SITE_NAME" +ContainerAppEnvironmentVariable = "CONTAINER_APP_NAME" +KubernetesEnvironmentVariable = "KUBERNETES_PORT" +ServiceFabricEnvironmentVariable = "Fabric_NvodeName" # cspell:disable-line diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py new file mode 100644 index 000000000000..5f8a60369590 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py @@ -0,0 +1,54 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +from typing import List, Optional, Callable, TYPE_CHECKING, Union, Awaitable + +if TYPE_CHECKING: + from azure.core.credentials import TokenCredential + from azure.core.credentials_async import AsyncTokenCredential + from azure.keyvault.secrets import SecretClient + from azure.keyvault.secrets.aio import SecretClient as AsyncSecretClient + + +class AzureAppConfigurationKeyVaultOptions: + def __init__( + self, + *, + credential: Optional[Union["TokenCredential", "AsyncTokenCredential"]] = None, + secret_clients: Optional[Union[List["SecretClient"], List["AsyncSecretClient"]]] = None, + secret_resolver: Optional[Union[Callable[[str], str], Callable[[str], Awaitable[str]]]] = None + ): + """ + Options for connecting to Key Vault. + + :keyword credential: A credential for authenticating with the key vault. This is optional if secret_clients is + provided. + :paramtype credential: ~azure.core.credentials.TokenCredential + :keyword secret_clients: A list of SecretClient from azure-keyvault-secrets. This is optional if credential is + provided. + :paramtype secret_clients: list[~azure.keyvault.secrets.SecretClient] + :keyword secret_resolver: A function that takes a URI and returns a value. + :paramtype secret_resolver: Callable[[str], str] + """ + self.credential = credential + self.secret_clients = secret_clients or [] + self.secret_resolver = secret_resolver + if self.credential is not None and self.secret_resolver is not None: + raise ValueError("credential and secret_resolver can't both be configured.") + + +class SettingSelector: + """ + Selects a set of configuration settings from Azure App Configuration. + + :param key_filter: A filter to select configuration settings based on their keys. + :type key_filter: str + :param label_filter: A filter to select configuration settings based on their labels. Default is value is '\0' + :type label_filter: str + """ + + def __init__(self, key_filter: str, label_filter: str = "\0"): + self.key_filter = key_filter + self.label_filter = label_filter diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_settingselector.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_settingselector.py deleted file mode 100644 index bef1b4668a6b..000000000000 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_settingselector.py +++ /dev/null @@ -1,21 +0,0 @@ -# ------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# ------------------------------------------------------------------------- - - -class SettingSelector: - """ - Selects a set of configuration settings from Azure App Configuration. - - :param key_filter: A filter to select configuration settings based on their keys. - :type key_filter: str - :param label_filter: A filter to select configuration settings based on their labels. Default is value is '\0' - :type label_filter: str - """ - - def __init__(self, key_filter, label_filter="\0"): - # type: (str, str) -> None - self.key_filter = key_filter - self.label_filter = label_filter diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/__init__.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/__init__.py new file mode 100644 index 000000000000..5a6311f8407e --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/__init__.py @@ -0,0 +1,12 @@ +# ------------------------------------------------------------------------ + # Copyright (c) Microsoft Corporation. All rights reserved. + # Licensed under the MIT License. See License.txt in the project root for + # license information. + # ------------------------------------------------------------------------- + +from ._azureappconfigurationproviderasync import load_provider, AzureAppConfigurationProvider + +__all__ = [ + "load_provider", + "AzureAppConfigurationProvider", +] 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 new file mode 100644 index 000000000000..94252254691e --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -0,0 +1,293 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +import json +from typing import Any, Dict, Iterable, Mapping, Optional, overload, List, Tuple, TYPE_CHECKING + +from azure.appconfiguration import FeatureFlagConfigurationSetting, SecretReferenceConfigurationSetting +from azure.appconfiguration.aio import AzureAppConfigurationClient +from azure.keyvault.secrets.aio import SecretClient +from azure.keyvault.secrets import KeyVaultSecretIdentifier + +from .._models import AzureAppConfigurationKeyVaultOptions, SettingSelector +from .._constants import FEATURE_MANAGEMENT_KEY +from .._azureappconfigurationprovider import _is_json_content_type, _get_correlation_context +from .._user_agent import USER_AGENT + +if TYPE_CHECKING: + from azure.core.credentials_async import AsyncTokenCredential + + +@overload +async def load_provider( + endpoint: str, + credential: "AsyncTokenCredential", + *, + selects: Optional[List[SettingSelector]] = None, + trimmed_key_prefixes: Optional[List[str]] = None, + key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = None, + **kwargs + ) -> "AzureAppConfigurationProvider": + """ + Loads configuration settings from Azure App Configuration into a Python application. + + :param str endpoint: Endpoint for App Configuration resource. + :param credential: Credential for App Configuration resource. + :type credential: ~azure.core.credentials.TokenCredential + :keyword selects: List of setting selectors to filter configuration settings + :paramtype selects: Optional[List[~azure.appconfiguration.provider.SettingSelector]] + :keyword trimmed_key_prefixes: List of prefixes to trim from configuration keys + :paramtype trimmed_key_prefixes: Optional[List[str]] + :keyword key_vault_options: Options for resolving Key Vault references + :paramtype key_vault_options: ~azure.appconfiguration.provider.AzureAppConfigurationKeyVaultOptions + """ + ... + +@overload +async def load_provider( + *, + connection_string: str, + selects: Optional[List[SettingSelector]] = None, + trimmed_key_prefixes: Optional[List[str]] = None, + key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = None, + **kwargs + ) -> "AzureAppConfigurationProvider": + """ + Loads configuration settings from Azure App Configuration into a Python application. + + :keyword str connection_string: Connection string for App Configuration resource. + :keyword selects: List of setting selectors to filter configuration settings + :paramtype selects: Optional[List[~azure.appconfiguration.provider.SettingSelector]] + :keyword trimmed_key_prefixes: List of prefixes to trim from configuration keys + :paramtype trimmed_key_prefixes: Optional[List[str]] + :keyword key_vault_options: Options for resolving Key Vault references + :paramtype key_vault_options: ~azure.appconfiguration.provider.AzureAppConfigurationKeyVaultOptions + """ + ... + + +async def load_provider(*args, **kwargs) -> "AzureAppConfigurationProvider": + #pylint:disable=protected-access + + # Start by parsing kwargs + 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("*", "\0")]) + trim_prefixes : List[str] = kwargs.pop("trimmed_key_prefixes", []) + + # Update endpoint and credential if specified positionally. + if len(args) > 2: + raise TypeError( + "Unexpected positional parameters. Please pass either endpoint and credential, or a connection string." + ) + if len(args) == 1: + if endpoint is not None: + raise TypeError("Received multiple values for parameter 'endpoint'.") + endpoint = args[0] + elif len(args) == 2: + if credential is not None: + raise TypeError("Received multiple values for parameter 'credential'.") + endpoint, credential = args + + 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._trim_prefixes = sorted(trim_prefixes, key=len, reverse=True) + + if key_vault_options is not None and len(key_vault_options.secret_clients) > 0: + for secret_client in key_vault_options.secret_clients: + provider._secret_clients[secret_client.vault_url] = secret_client + + 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, {}) + 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 + return provider + + +def _buildprovider( + connection_string: Optional[str], + endpoint: Optional[str], + credential: Optional["AsyncTokenCredential"], + key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions], + **kwargs + ) -> "AzureAppConfigurationProvider": + #pylint:disable=protected-access + provider = AzureAppConfigurationProvider() + headers = kwargs.pop("headers", {}) + headers["Correlation-Context"] = _get_correlation_context(key_vault_options) + useragent = USER_AGENT + + if connection_string: + provider._client = AzureAppConfigurationClient.from_connection_string( + connection_string, user_agent=useragent, headers=headers, **kwargs + ) + return provider + provider._client = AzureAppConfigurationClient( + endpoint, credential, user_agent=useragent, headers=headers, **kwargs + ) + return provider + + +async def _resolve_keyvault_reference( + config, + key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions], + provider: "AzureAppConfigurationProvider" + ) -> str: + if key_vault_options is None: + raise ValueError("Key Vault options must be set to resolve Key Vault references.") + + if config.secret_id is None: + raise ValueError("Key Vault reference must have a uri value.") + + key_vault_identifier = KeyVaultSecretIdentifier(config.secret_id) + + #pylint:disable=protected-access + referenced_client = provider._secret_clients.get(key_vault_identifier.vault_url, None) + + if referenced_client is None and key_vault_options.credential is not None: + referenced_client = SecretClient( + vault_url=key_vault_identifier.vault_url, credential=key_vault_options.credential + ) + provider._secret_clients[key_vault_identifier.vault_url] = referenced_client + + if referenced_client: + return ( + 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) + try: + # Secret resolver was async + return await resolved + except TypeError: + # Secret resolver was sync + return resolved + + raise ValueError( + "No Secret Client found for Key Vault reference %s" % (key_vault_identifier.vault_url) + ) + + +class AzureAppConfigurationProvider(Mapping[str, str]): + """ + Provides a dictionary-like interface to Azure App Configuration settings. Enables loading of sets of configuration + settings from Azure App Configuration into a Python application. Enables trimming of prefixes from configuration + keys. Enables resolution of Key Vault references in configuration settings. + """ + def __init__(self) -> None: + self._dict: Dict[str, str] = {} + self._trim_prefixes: List[str] = [] + self._client: Optional[AzureAppConfigurationClient] = None + self._secret_clients: Dict[str, SecretClient] = {} + + def __getitem__(self, key: str) -> str: + """ + Returns the value of the specified key. + """ + return self._dict[key] + + def __iter__(self) -> Iterable[str]: + return self._dict.__iter__() + + def __len__(self) -> int: + return len(self._dict) + + def __contains__(self, __x: object) -> bool: + """ + Returns True if the configuration settings contains the specified key. + """ + return self._dict.__contains__(__x) + + def keys(self) -> Iterable[str]: + """ + Returns a list of keys loaded from Azure App Configuration. + """ + return self._dict.keys() + + def items(self) -> Iterable[Tuple[str, str]]: + """ + Returns a list of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault references + will be resolved. + """ + return self._dict.items() + + def values(self) -> Iterable[str]: + """ + Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be + resolved. + """ + return self._dict.values() + + def get(self, key: str, default: Optional[str] = None) -> str: + """ + Returns the value of the specified key. If the key does not exist, returns the default value. + """ + return self._dict.get(key, default) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, AzureAppConfigurationProvider): + return False + if self._dict != other._dict: + return False + if self._trim_prefixes != other._trim_prefixes: + return False + if self._client != other._client: + return False + return True + + def __ne__(self, other: Any) -> bool: + return not self == other + + async def close(self) -> None: + """ + Closes the connection to Azure App Configuration. + """ + for client in self._secret_clients.values(): + await client.close() + await self._client.close() + + async def __aenter__(self) -> "AzureAppConfigurationProvider": + await self._client.__aenter__() + for client in self._secret_clients.values(): + await client.__aenter__() + return self + + async def __aexit__(self, *args) -> None: + await self._client.__aexit__(*args) + for client in self._secret_clients.values(): + await client.__aexit__() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/dev_requirements.txt b/sdk/appconfiguration/azure-appconfiguration-provider/dev_requirements.txt index 8ca0b23f04e5..9ef4f496b7e8 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/dev_requirements.txt +++ b/sdk/appconfiguration/azure-appconfiguration-provider/dev_requirements.txt @@ -1,6 +1,6 @@ -e ../../../tools/azure-devtools -azure-core -azure-appconfiguration +azure-core>=1.24.0 +azure-appconfiguration==1.4.0 azure-identity azure-keyvault-secrets aiohttp>=3.0 diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/aad_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/aad_sample.py index 4c875ad53b86..c616c7ad2af0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/aad_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/aad_sample.py @@ -5,32 +5,31 @@ # ------------------------------------------------------------------------- from azure.appconfiguration.provider import ( - AzureAppConfigurationProvider, + load_provider, SettingSelector ) -from azure.identity import DefaultAzureCredential import os +from sample_utilities import get_authority, get_audience, get_credential endpoint = os.environ.get("AZURE_APPCONFIG_ENDPOINT") -credential = DefaultAzureCredential() +authority = get_authority(endpoint) +audience = get_audience(authority) +credential = get_credential(authority) # Connecting to Azure App Configuration using AAD -config = AzureAppConfigurationProvider.load( - endpoint=endpoint, credential=credential) +config = load_provider(endpoint=endpoint, credential=credential) print(config["message"]) # Connecting to Azure App Configuration using AAD and trimmed key prefixes trimmed = {"test."} -config = AzureAppConfigurationProvider.load( - endpoint=endpoint, credential=credential, trimmed_key_prefixes=trimmed) +config = load_provider(endpoint=endpoint, credential=credential, trimmed_key_prefixes=trimmed) print(config["message"]) # Connection to Azure App Configuration using SettingSelector selects = {SettingSelector("message*", "\0")} -config = AzureAppConfigurationProvider.load( - endpoint=endpoint, credential=credential, selects=selects) +config = load_provider(endpoint=endpoint, credential=credential, selects=selects) print("message found: " + str("message" in config)) print("test.message found: " + str("test.message" in config)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_aad_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_aad_sample.py new file mode 100644 index 000000000000..8892275017a3 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_aad_sample.py @@ -0,0 +1,41 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import asyncio +from azure.appconfiguration.provider.aio import load_provider +from azure.appconfiguration.provider import SettingSelector +import os +from sample_utilities import get_authority, get_audience, get_credential + +async def main(): + endpoint = os.environ.get("AZURE_APPCONFIG_ENDPOINT") + authority = get_authority(endpoint) + credential = get_credential(authority, is_async=True) + + # Connecting to Azure App Configuration using AAD + config = await load_provider(endpoint=endpoint, credential=credential) + print(config["message"]) + + # Connecting to Azure App Configuration using AAD and trimmed key prefixes + trimmed = {"test."} + config = await load_provider(endpoint=endpoint, credential=credential, trimmed_key_prefixes=trimmed) + + print(config["message"]) + + # Connection to Azure App Configuration using SettingSelector + selects = {SettingSelector("message*", "\0")} + config = await load_provider(endpoint=endpoint, credential=credential, selects=selects) + + print("message found: " + str("message" in config)) + print("test.message found: " + str("test.message" in config)) + + + await credential.close() + await config.close() + +if __name__ == "__main__": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + asyncio.run(main()) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_connection_string_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_connection_string_sample.py new file mode 100644 index 000000000000..8ac139cadf2a --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_connection_string_sample.py @@ -0,0 +1,34 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +import asyncio +from azure.appconfiguration.provider.aio import load_provider +from azure.appconfiguration.provider import SettingSelector +import os + +async def main(): + connection_string = os.environ.get("AZURE_APPCONFIG_CONNECTION_STRING") + + # Connecting to Azure App Configuration using connection string + config = await load_provider(connection_string=connection_string) + + print(config["message"]) + print(config["my_json"]["key"]) + + # Connecting to Azure App Configuration using connection string and trimmed key prefixes + trimmed = {"test."} + config = await load_provider(connection_string=connection_string, trimmed_key_prefixes=trimmed) + + print(config["message"]) + + # Connection to Azure App Configuration using SettingSelector + selects = {SettingSelector("message*", "\0")} + config = await load_provider(connection_string=connection_string, selects=selects) + + print("message found: " + str("message" in config)) + print("test.message found: " + str("test.message" in config)) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_key_vault_reference_provided_clients_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_key_vault_reference_provided_clients_sample.py new file mode 100644 index 000000000000..3f46ecd0daf4 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_key_vault_reference_provided_clients_sample.py @@ -0,0 +1,35 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import asyncio +from azure.appconfiguration.provider.aio import load_provider +from azure.appconfiguration.provider import SettingSelector, AzureAppConfigurationKeyVaultOptions +from azure.keyvault.secrets.aio import SecretClient +import os +from sample_utilities import get_authority, get_audience, get_credential + +async def main(): + endpoint = os.environ.get("AZURE_APPCONFIG_ENDPOINT") + key_vault_uri = os.environ.get("AZURE_KEYVAULT_URI") + authority = get_authority(endpoint) + audience = get_audience(authority) + credential = get_credential(authority, is_async=True) + + # Connection to Azure App Configuration using AAD with Provided Client + secret_client = SecretClient(vault_url=key_vault_uri, credential=credential) + selects = {SettingSelector("*", "prod")} + key_vault_options = AzureAppConfigurationKeyVaultOptions(secret_clients=[secret_client]) + config = await load_provider(endpoint=endpoint, credential=credential, key_vault_options=key_vault_options, selects=selects) + + print(config["secret"]) + + await credential.close() + await secret_client.close() + await config.close() + +if __name__ == "__main__": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + asyncio.run(main()) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_key_vault_reference_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_key_vault_reference_sample.py new file mode 100644 index 000000000000..50e30cdf4ce9 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_key_vault_reference_sample.py @@ -0,0 +1,32 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import asyncio +from azure.appconfiguration.provider.aio import load_provider +from azure.appconfiguration.provider import SettingSelector, AzureAppConfigurationKeyVaultOptions +import os +from sample_utilities import get_authority, get_audience, get_credential + +async def main(): + endpoint = os.environ.get("AZURE_APPCONFIG_ENDPOINT") + authority = get_authority(endpoint) + audience = get_audience(authority) + credential = get_credential(authority, is_async=True) + + # Connection to Azure App Configuration using AAD and Resolving Key Vault References + key_vault_options = AzureAppConfigurationKeyVaultOptions(credential=credential) + selects = {SettingSelector("*", "prod")} + + config = await load_provider(endpoint=endpoint, credential=credential, key_vault_options=key_vault_options, selects=selects) + + print(config["secret"]) + + await credential.close() + await config.close() + +if __name__ == "__main__": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + asyncio.run(main()) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/connection_string_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/connection_string_sample.py index c18a5f989fbb..ac7dcd59b4cc 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/connection_string_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/connection_string_sample.py @@ -5,7 +5,7 @@ # ------------------------------------------------------------------------- from azure.appconfiguration.provider import ( - AzureAppConfigurationProvider, + load_provider, SettingSelector ) import os @@ -13,23 +13,20 @@ connection_string = os.environ.get("AZURE_APPCONFIG_CONNECTION_STRING") # Connecting to Azure App Configuration using connection string -config = AzureAppConfigurationProvider.load( - connection_string=connection_string) +config = load_provider(connection_string=connection_string) print(config["message"]) print(config["my_json"]["key"]) # Connecting to Azure App Configuration using connection string and trimmed key prefixes trimmed = {"test."} -config = AzureAppConfigurationProvider.load( - connection_string=connection_string, trimmed_key_prefixes=trimmed) +config = load_provider(connection_string=connection_string, trimmed_key_prefixes=trimmed) print(config["message"]) # Connection to Azure App Configuration using SettingSelector selects = {SettingSelector("message*", "\0")} -config = AzureAppConfigurationProvider.load( - connection_string=connection_string, selects=selects) +config = load_provider(connection_string=connection_string, selects=selects) print("message found: " + str("message" in config)) print("test.message found: " + str("test.message" in config)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_provided_clients_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_provided_clients_sample.py index 8d67f617c34c..2373222fe8b1 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_provided_clients_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_provided_clients_sample.py @@ -5,24 +5,24 @@ # ------------------------------------------------------------------------- from azure.appconfiguration.provider import ( - AzureAppConfigurationProvider, + load_provider, AzureAppConfigurationKeyVaultOptions, SettingSelector ) from azure.keyvault.secrets import SecretClient -from azure.identity import DefaultAzureCredential import os +from sample_utilities import get_authority, get_audience, get_credential endpoint = os.environ.get("AZURE_APPCONFIG_ENDPOINT") key_vault_uri = os.environ.get("AZURE_KEYVAULT_URI") -credential = DefaultAzureCredential() +authority = get_authority(endpoint) +audience = get_audience(authority) +credential = get_credential(authority) # Connection to Azure App Configuration using AAD with Provided Client secret_client = SecretClient(vault_url=key_vault_uri, credential=credential) selects = {SettingSelector("*", "prod")} -key_vault_options = AzureAppConfigurationKeyVaultOptions(secret_clients=[ - secret_client]) -config = AzureAppConfigurationProvider.load( - endpoint=endpoint, credential=credential, key_vault_options=key_vault_options, selects=selects) +key_vault_options = AzureAppConfigurationKeyVaultOptions(secret_clients=[secret_client]) +config = load_provider(endpoint=endpoint, credential=credential, key_vault_options=key_vault_options, selects=selects) print(config["secret"]) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_sample.py index 4e02cb434197..2a44fa87900a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_sample.py @@ -5,21 +5,22 @@ # ------------------------------------------------------------------------- from azure.appconfiguration.provider import ( - AzureAppConfigurationProvider, + load_provider, AzureAppConfigurationKeyVaultOptions, SettingSelector ) -from azure.identity import DefaultAzureCredential import os +from sample_utilities import get_authority, get_audience, get_credential endpoint = os.environ.get("AZURE_APPCONFIG_ENDPOINT") -credential = DefaultAzureCredential() +authority = get_authority(endpoint) +audience = get_audience(authority) +credential = get_credential(authority) # Connection to Azure App Configuration using AAD and Resolving Key Vault References key_vault_options = AzureAppConfigurationKeyVaultOptions(credential=credential) selects = {SettingSelector("*", "prod")} -config = AzureAppConfigurationProvider.load( - endpoint=endpoint, credential=credential, key_vault_options=key_vault_options, selects=selects) +config = load_provider(endpoint=endpoint, credential=credential, key_vault_options=key_vault_options, selects=selects) print(config["secret"]) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/sample_utilities.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/sample_utilities.py new file mode 100644 index 000000000000..82959bfeb487 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/sample_utilities.py @@ -0,0 +1,50 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +""" +FILE: sample_utilities.py +DESCRIPTION: + This file include some utility functions for samples to use: + - get_authority(): get authority of the ConfigurationClient + - get_audience(): get audience of the ConfigurationClient + - get_credential(): get credential of the ConfigurationClient + It is not a file expected to run independently. +""" + +import os +from azure.identity import AzureAuthorityHosts, ClientSecretCredential, DefaultAzureCredential +from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential + +def get_authority(endpoint): + # cSpell:ignore azconfig + if ".azconfig.io" in endpoint: + return AzureAuthorityHosts.AZURE_PUBLIC_CLOUD + if ".azconfig.azure.cn" in endpoint: + return AzureAuthorityHosts.AZURE_CHINA + if ".azconfig.azure.us" in endpoint: + return AzureAuthorityHosts.AZURE_GOVERNMENT + raise ValueError(f"Endpoint ({endpoint}) could not be understood") + +def get_audience(authority): + if authority == AzureAuthorityHosts.AZURE_PUBLIC_CLOUD: + return "https://management.azure.com" + if authority == AzureAuthorityHosts.AZURE_CHINA: + return "https://management.chinacloudapi.cn" + if authority == AzureAuthorityHosts.AZURE_GOVERNMENT: + return "https://management.usgovcloudapi.net" + +def get_credential(authority, **kwargs): + if authority != AzureAuthorityHosts.AZURE_PUBLIC_CLOUD: + return ClientSecretCredential( + tenant_id=os.environ.get("AZURE_TENANT_ID"), + client_id=os.environ.get("AZURE_CLIENT_ID"), + client_secret=os.environ.get("AZURE_CLIENT_SECRET"), + authority=authority + ) + is_async = kwargs.pop("is_async", False) + if is_async: + return AsyncDefaultAzureCredential(**kwargs) + return DefaultAzureCredential(**kwargs) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/setup.py b/sdk/appconfiguration/azure-appconfiguration-provider/setup.py index 6e2821a58357..da8f066ccc45 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/setup.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/setup.py @@ -66,6 +66,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", ], zip_safe=False, @@ -73,8 +74,8 @@ python_requires=">=3.6", install_requires=[ "msrest>=0.6.21", - "azure-core<2.0.0,>=1.2.2", - "azure-appconfiguration<2.0.0,>=1.3.0", + "azure-core<2.0.0,>=1.24.0", + "azure-appconfiguration<2.0.0,>=1.4.0", "azure-keyvault-secrets<5.0.0,>=4.3.0", ], ) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/async_preparers.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/async_preparers.py new file mode 100644 index 000000000000..d2bf0661b1ca --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/async_preparers.py @@ -0,0 +1,31 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from preparers import AppConfigProviderPreparer, trim_kwargs_from_test_function + +def app_config_decorator_async(func, **kwargs): + @AppConfigProviderPreparer() + async def wrapper(*args, **kwargs): + appconfiguration_connection_string = kwargs.pop("appconfiguration_connection_string") + kwargs['appconfiguration_connection_string'] = appconfiguration_connection_string + + trimmed_kwargs = {k:v for k, v in kwargs.items()} + trim_kwargs_from_test_function(func, trimmed_kwargs) + + await func(*args, **trimmed_kwargs) + + return wrapper + +def app_config_aad_decorator_async(func, **kwargs): + @AppConfigProviderPreparer() + async def wrapper(*args, **kwargs): + appconfiguration_endpoint_string = kwargs.pop("appconfiguration_endpoint_string") + kwargs['appconfiguration_endpoint_string'] = appconfiguration_endpoint_string + + trimmed_kwargs = {k:v for k, v in kwargs.items()} + trim_kwargs_from_test_function(func, trimmed_kwargs) + + await func(*args, **trimmed_kwargs) + return wrapper diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider.pyTestAppConfigurationProvidertest_provider_creation.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider.pyTestAppConfigurationProvidertest_provider_creation.json new file mode 100644 index 000000000000..4af995263902 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider.pyTestAppConfigurationProvidertest_provider_creation.json @@ -0,0 +1,87 @@ +{ + "Entries": [ + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=*\u0026label=%00\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "0", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)", + "x-ms-content-sha256": "47DEQpj8HBSa\u002B/TImW\u002B5JCeuQeRkm5NMpJWZG3hSuFU=", + "x-ms-date": "Feb, 08 2023 22:33:28.192807 GMT" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Wed, 08 Feb 2023 22:33:28 GMT", + "Server": "openresty/1.21.4.1", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "f50e2fac-bd9f-4020-b912-f5def2115ba5" + }, + "ResponseBody": { + "items": [ + { + "etag": "5l-CzldxczbRRnDjgiWcnuL__i--zHCffz7xO8ngGos", + "key": ".appconfig.featureflag/Alpha", + "label": null, + "content_type": "application/vnd.microsoft.appconfig.ff\u002Bjson;charset=utf-8", + "value": "{\u0022id\u0022:\u0022Alpha\u0022,\u0022description\u0022:\u0022\u0022,\u0022enabled\u0022:false,\u0022conditions\u0022:{\u0022client_filters\u0022:[]}}", + "tags": {}, + "locked": false, + "last_modified": "2023-01-30T20:25:07\u002B00:00" + }, + { + "etag": "Wfq_mEPSSfl9m4evo5sZDiZMkpn_6kH5xtJVH_D7OQY", + "key": "message", + "label": null, + "content_type": "", + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-25T00:21:41\u002B00:00" + }, + { + "etag": "lO0_bzj8OQZOAMw2U2Ydzd4NJtYpXyQ5CbXktXLRhMs", + "key": "messageVal", + "label": null, + "content_type": null, + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-23T19:14:32\u002B00:00" + }, + { + "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", + "key": "my_json", + "label": null, + "content_type": "application/json", + "value": "{\u0022key\u0022:\u0022value\u0022}", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:35:17\u002B00:00" + }, + { + "etag": "7vw5ma2Cc0-eE1cnRaaCv2_VtAS8BQ5jbHP9eNS_MFA", + "key": "test.trimmed", + "label": null, + "content_type": "key", + "value": "key", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:58:41\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider.pyTestAppConfigurationProvidertest_provider_selectors.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider.pyTestAppConfigurationProvidertest_provider_selectors.json new file mode 100644 index 000000000000..91955a012eed --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider.pyTestAppConfigurationProvidertest_provider_selectors.json @@ -0,0 +1,47 @@ +{ + "Entries": [ + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=message*\u0026label=dev\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "0", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)", + "x-ms-content-sha256": "47DEQpj8HBSa\u002B/TImW\u002B5JCeuQeRkm5NMpJWZG3hSuFU=", + "x-ms-date": "Feb, 08 2023 22:33:30.257649 GMT" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Wed, 08 Feb 2023 22:33:30 GMT", + "Server": "openresty/1.21.4.1", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "43de82df-be64-4d29-b35f-4ac69b0ab300" + }, + "ResponseBody": { + "items": [ + { + "etag": "yEiVkIl-IHlB26quPXo_Z2GbB5yQm_ENTWI2c1k1J4E", + "key": "message", + "label": "dev", + "content_type": "", + "value": "test", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:06:21\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json new file mode 100644 index 000000000000..810328f11fb1 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json @@ -0,0 +1,87 @@ +{ + "Entries": [ + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=*\u0026label=%00\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "0", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)", + "x-ms-content-sha256": "47DEQpj8HBSa\u002B/TImW\u002B5JCeuQeRkm5NMpJWZG3hSuFU=", + "x-ms-date": "Feb, 08 2023 22:33:29.984709 GMT" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Wed, 08 Feb 2023 22:33:30 GMT", + "Server": "openresty/1.21.4.1", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "53b48bf3-e658-4e8e-85f5-ec5a0d1dbb30" + }, + "ResponseBody": { + "items": [ + { + "etag": "5l-CzldxczbRRnDjgiWcnuL__i--zHCffz7xO8ngGos", + "key": ".appconfig.featureflag/Alpha", + "label": null, + "content_type": "application/vnd.microsoft.appconfig.ff\u002Bjson;charset=utf-8", + "value": "{\u0022id\u0022:\u0022Alpha\u0022,\u0022description\u0022:\u0022\u0022,\u0022enabled\u0022:false,\u0022conditions\u0022:{\u0022client_filters\u0022:[]}}", + "tags": {}, + "locked": false, + "last_modified": "2023-01-30T20:25:07\u002B00:00" + }, + { + "etag": "Wfq_mEPSSfl9m4evo5sZDiZMkpn_6kH5xtJVH_D7OQY", + "key": "message", + "label": null, + "content_type": "", + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-25T00:21:41\u002B00:00" + }, + { + "etag": "lO0_bzj8OQZOAMw2U2Ydzd4NJtYpXyQ5CbXktXLRhMs", + "key": "messageVal", + "label": null, + "content_type": null, + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-23T19:14:32\u002B00:00" + }, + { + "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", + "key": "my_json", + "label": null, + "content_type": "application/json", + "value": "{\u0022key\u0022:\u0022value\u0022}", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:35:17\u002B00:00" + }, + { + "etag": "7vw5ma2Cc0-eE1cnRaaCv2_VtAS8BQ5jbHP9eNS_MFA", + "key": "test.trimmed", + "label": null, + "content_type": "key", + "value": "key", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:58:41\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider_aad.pyTestAppConfigurationProvidertest_provider_creation_aad.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider_aad.pyTestAppConfigurationProvidertest_provider_creation_aad.json new file mode 100644 index 000000000000..49971afc8275 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider_aad.pyTestAppConfigurationProvidertest_provider_creation_aad.json @@ -0,0 +1,84 @@ +{ + "Entries": [ + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=*\u0026label=%00\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Wed, 08 Feb 2023 22:33:31 GMT", + "Server": "openresty/1.21.4.1", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "0eae3d98-480b-4384-87f4-3a56f8c1446e" + }, + "ResponseBody": { + "items": [ + { + "etag": "5l-CzldxczbRRnDjgiWcnuL__i--zHCffz7xO8ngGos", + "key": ".appconfig.featureflag/Alpha", + "label": null, + "content_type": "application/vnd.microsoft.appconfig.ff\u002Bjson;charset=utf-8", + "value": "{\u0022id\u0022:\u0022Alpha\u0022,\u0022description\u0022:\u0022\u0022,\u0022enabled\u0022:false,\u0022conditions\u0022:{\u0022client_filters\u0022:[]}}", + "tags": {}, + "locked": false, + "last_modified": "2023-01-30T20:25:07\u002B00:00" + }, + { + "etag": "Wfq_mEPSSfl9m4evo5sZDiZMkpn_6kH5xtJVH_D7OQY", + "key": "message", + "label": null, + "content_type": "", + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-25T00:21:41\u002B00:00" + }, + { + "etag": "lO0_bzj8OQZOAMw2U2Ydzd4NJtYpXyQ5CbXktXLRhMs", + "key": "messageVal", + "label": null, + "content_type": null, + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-23T19:14:32\u002B00:00" + }, + { + "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", + "key": "my_json", + "label": null, + "content_type": "application/json", + "value": "{\u0022key\u0022:\u0022value\u0022}", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:35:17\u002B00:00" + }, + { + "etag": "7vw5ma2Cc0-eE1cnRaaCv2_VtAS8BQ5jbHP9eNS_MFA", + "key": "test.trimmed", + "label": null, + "content_type": "key", + "value": "key", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:58:41\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider_aad.pyTestAppConfigurationProvidertest_provider_selectors.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider_aad.pyTestAppConfigurationProvidertest_provider_selectors.json new file mode 100644 index 000000000000..5e3559a6e349 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider_aad.pyTestAppConfigurationProvidertest_provider_selectors.json @@ -0,0 +1,44 @@ +{ + "Entries": [ + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=message*\u0026label=dev\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Wed, 08 Feb 2023 22:33:32 GMT", + "Server": "openresty/1.21.4.1", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "9614bf72-1c45-4971-b839-8aebf6acaf91" + }, + "ResponseBody": { + "items": [ + { + "etag": "yEiVkIl-IHlB26quPXo_Z2GbB5yQm_ENTWI2c1k1J4E", + "key": "message", + "label": "dev", + "content_type": "", + "value": "test", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:06:21\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider_aad.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider_aad.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json new file mode 100644 index 000000000000..0718b46f8ebd --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_async_provider_aad.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json @@ -0,0 +1,84 @@ +{ + "Entries": [ + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=*\u0026label=%00\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Wed, 08 Feb 2023 22:33:32 GMT", + "Server": "openresty/1.21.4.1", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "f35c4800-039f-42bc-8564-687cb2326aea" + }, + "ResponseBody": { + "items": [ + { + "etag": "5l-CzldxczbRRnDjgiWcnuL__i--zHCffz7xO8ngGos", + "key": ".appconfig.featureflag/Alpha", + "label": null, + "content_type": "application/vnd.microsoft.appconfig.ff\u002Bjson;charset=utf-8", + "value": "{\u0022id\u0022:\u0022Alpha\u0022,\u0022description\u0022:\u0022\u0022,\u0022enabled\u0022:false,\u0022conditions\u0022:{\u0022client_filters\u0022:[]}}", + "tags": {}, + "locked": false, + "last_modified": "2023-01-30T20:25:07\u002B00:00" + }, + { + "etag": "Wfq_mEPSSfl9m4evo5sZDiZMkpn_6kH5xtJVH_D7OQY", + "key": "message", + "label": null, + "content_type": "", + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-25T00:21:41\u002B00:00" + }, + { + "etag": "lO0_bzj8OQZOAMw2U2Ydzd4NJtYpXyQ5CbXktXLRhMs", + "key": "messageVal", + "label": null, + "content_type": null, + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-23T19:14:32\u002B00:00" + }, + { + "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", + "key": "my_json", + "label": null, + "content_type": "application/json", + "value": "{\u0022key\u0022:\u0022value\u0022}", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:35:17\u002B00:00" + }, + { + "etag": "7vw5ma2Cc0-eE1cnRaaCv2_VtAS8BQ5jbHP9eNS_MFA", + "key": "test.trimmed", + "label": null, + "content_type": "key", + "value": "key", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:58:41\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_creation.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_creation.json index fd5b1bf899c6..84a67d49b317 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_creation.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_creation.json @@ -8,9 +8,9 @@ "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "Correlation-Context": "RequestType=Startup", - "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)", + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)", "x-ms-content-sha256": "47DEQpj8HBSa\u002B/TImW\u002B5JCeuQeRkm5NMpJWZG3hSuFU=", - "x-ms-date": "Sep, 27 2022 22:42:40.104313 GMT" + "x-ms-date": "Feb, 08 2023 22:33:33.113853 GMT" }, "RequestBody": null, "StatusCode": 200, @@ -20,24 +20,44 @@ "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", "Connection": "keep-alive", "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:40 GMT", - "Server": "openresty/1.17.8.2", + "Date": "Wed, 08 Feb 2023 22:33:33 GMT", + "Server": "openresty/1.21.4.1", "Strict-Transport-Security": "max-age=15724800; includeSubDomains", - "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", "Transfer-Encoding": "chunked", - "x-ms-correlation-request-id": "c7ce670d-847a-4232-93fc-a4efa00562d4" + "x-ms-correlation-request-id": "0a7bb334-fd3c-4b39-b424-669c4cfa73bb" }, "ResponseBody": { "items": [ { - "etag": "RX6Uzn3YdpSnDk7SX9kzxXwDFdYhR8dGGLEXBZ6i_nM", + "etag": "5l-CzldxczbRRnDjgiWcnuL__i--zHCffz7xO8ngGos", + "key": ".appconfig.featureflag/Alpha", + "label": null, + "content_type": "application/vnd.microsoft.appconfig.ff\u002Bjson;charset=utf-8", + "value": "{\u0022id\u0022:\u0022Alpha\u0022,\u0022description\u0022:\u0022\u0022,\u0022enabled\u0022:false,\u0022conditions\u0022:{\u0022client_filters\u0022:[]}}", + "tags": {}, + "locked": false, + "last_modified": "2023-01-30T20:25:07\u002B00:00" + }, + { + "etag": "Wfq_mEPSSfl9m4evo5sZDiZMkpn_6kH5xtJVH_D7OQY", "key": "message", "label": null, "content_type": "", "value": "hi", "tags": {}, "locked": false, - "last_modified": "2022-09-27T19:41:40\u002B00:00" + "last_modified": "2023-01-25T00:21:41\u002B00:00" + }, + { + "etag": "lO0_bzj8OQZOAMw2U2Ydzd4NJtYpXyQ5CbXktXLRhMs", + "key": "messageVal", + "label": null, + "content_type": null, + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-23T19:14:32\u002B00:00" }, { "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", @@ -64,4 +84,4 @@ } ], "Variables": {} -} \ No newline at end of file +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_selectors.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_selectors.json index 525dccf8aaab..abe032f40608 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_selectors.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_selectors.json @@ -8,9 +8,9 @@ "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "Correlation-Context": "RequestType=Startup", - "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)", + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)", "x-ms-content-sha256": "47DEQpj8HBSa\u002B/TImW\u002B5JCeuQeRkm5NMpJWZG3hSuFU=", - "x-ms-date": "Sep, 27 2022 22:42:40.650526 GMT" + "x-ms-date": "Feb, 08 2023 22:33:33.590834 GMT" }, "RequestBody": null, "StatusCode": 200, @@ -20,12 +20,12 @@ "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", "Connection": "keep-alive", "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:40 GMT", - "Server": "openresty/1.17.8.2", + "Date": "Wed, 08 Feb 2023 22:33:33 GMT", + "Server": "openresty/1.21.4.1", "Strict-Transport-Security": "max-age=15724800; includeSubDomains", - "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", "Transfer-Encoding": "chunked", - "x-ms-correlation-request-id": "06221105-78da-4e5e-8239-20d1d53ea669" + "x-ms-correlation-request-id": "ed6b796e-2b57-45e4-aad0-f9551b7daaa1" }, "ResponseBody": { "items": [ @@ -44,4 +44,4 @@ } ], "Variables": {} -} \ No newline at end of file +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json index 6faf86a93e30..8527138fc1ff 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json @@ -8,9 +8,9 @@ "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "Correlation-Context": "RequestType=Startup", - "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)", + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)", "x-ms-content-sha256": "47DEQpj8HBSa\u002B/TImW\u002B5JCeuQeRkm5NMpJWZG3hSuFU=", - "x-ms-date": "Sep, 27 2022 22:42:40.490235 GMT" + "x-ms-date": "Feb, 08 2023 22:33:33.352066 GMT" }, "RequestBody": null, "StatusCode": 200, @@ -20,24 +20,44 @@ "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", "Connection": "keep-alive", "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:40 GMT", - "Server": "openresty/1.17.8.2", + "Date": "Wed, 08 Feb 2023 22:33:33 GMT", + "Server": "openresty/1.21.4.1", "Strict-Transport-Security": "max-age=15724800; includeSubDomains", - "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", "Transfer-Encoding": "chunked", - "x-ms-correlation-request-id": "55e5698f-c69d-4645-81aa-3f25371d56a4" + "x-ms-correlation-request-id": "b44828ad-3e88-4199-901b-1163c4d6f6af" }, "ResponseBody": { "items": [ { - "etag": "RX6Uzn3YdpSnDk7SX9kzxXwDFdYhR8dGGLEXBZ6i_nM", + "etag": "5l-CzldxczbRRnDjgiWcnuL__i--zHCffz7xO8ngGos", + "key": ".appconfig.featureflag/Alpha", + "label": null, + "content_type": "application/vnd.microsoft.appconfig.ff\u002Bjson;charset=utf-8", + "value": "{\u0022id\u0022:\u0022Alpha\u0022,\u0022description\u0022:\u0022\u0022,\u0022enabled\u0022:false,\u0022conditions\u0022:{\u0022client_filters\u0022:[]}}", + "tags": {}, + "locked": false, + "last_modified": "2023-01-30T20:25:07\u002B00:00" + }, + { + "etag": "Wfq_mEPSSfl9m4evo5sZDiZMkpn_6kH5xtJVH_D7OQY", "key": "message", "label": null, "content_type": "", "value": "hi", "tags": {}, "locked": false, - "last_modified": "2022-09-27T19:41:40\u002B00:00" + "last_modified": "2023-01-25T00:21:41\u002B00:00" + }, + { + "etag": "lO0_bzj8OQZOAMw2U2Ydzd4NJtYpXyQ5CbXktXLRhMs", + "key": "messageVal", + "label": null, + "content_type": null, + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-23T19:14:32\u002B00:00" }, { "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", @@ -64,4 +84,4 @@ } ], "Variables": {} -} \ No newline at end of file +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_creation_aad.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_creation_aad.json index 2d4e0036340f..0ecb9e28f899 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_creation_aad.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_creation_aad.json @@ -7,7 +7,7 @@ "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", - "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + "User-Agent": "azsdk-python-identity/1.12.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" }, "RequestBody": null, "StatusCode": 200, @@ -17,17 +17,16 @@ "Cache-Control": "max-age=86400, private", "Content-Length": "1599", "Content-Type": "application/json; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:40 GMT", + "Date": "Wed, 08 Feb 2023 22:33:33 GMT", "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", "Set-Cookie": [ - "fpc=Aih2w7HdW0RLsLtcNXW6NJo; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", - "esctx=AQABAAAAAAD--DLA3VO7QrddgJg7WevrkYxHw6mf13fuPRCu6v488gBdmaagfm2C8Q8ZiC9pB0nYYkXjS9U5w5mUTT51HcOESfBkGL6zDFmEHCECrlz_k72p8NUOMuj8OHKqWYGUIMnrKLpjfjH0i6lBviGMxCSFlnfEbzZfWev4InHJwQ2u2shZYtE7Y5Hb_K_3kYBp1RkgAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None", - "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", - "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + "fpc=AqTPPhjdgKFNp1GfJ3D4mN_i05sNAgAAADoadtsOAAAA; expires=Fri, 10-Mar-2023 22:33:33 GMT; path=/; secure; HttpOnly; SameSite=None", + "esctx=PAQABAAEAAAD--DLA3VO7QrddgJg7WevrODUWuiJWONlznSbyGnO4nZYG_EQFgG8tJD0_yqJ9NEgz_804pc92nBTvQWPf6_PhviGwUaGpoUBjd4B7d6DWTau3pifZgUTKqvCklV0AOQAF94XcVbQVvT15TWgKAHIJvsdHtFjn92Aix7dDr6gKoayP-vwgkvfqbDQz8Hepe0wgAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" ], "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "X-Content-Type-Options": "nosniff", - "x-ms-ests-server": "2.1.13672.11 - NCUS ProdSlices", + "x-ms-ests-server": "2.1.14601.8 - NCUS ProdSlices", "X-XSS-Protection": "0" }, "ResponseBody": { @@ -105,8 +104,8 @@ "Accept": "application/json", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", - "Cookie": "fpc=Aih2w7HdW0RLsLtcNXW6NJo; stsservicecookie=estsfd; x-ms-gateway-slice=estsfd", - "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + "Cookie": "fpc=AqTPPhjdgKFNp1GfJ3D4mN_i05sNAgAAADoadtsOAAAA; x-ms-gateway-slice=estsfd", + "User-Agent": "azsdk-python-identity/1.12.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" }, "RequestBody": null, "StatusCode": 200, @@ -116,16 +115,15 @@ "Cache-Control": "max-age=86400, private", "Content-Length": "945", "Content-Type": "application/json; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:40 GMT", + "Date": "Wed, 08 Feb 2023 22:33:33 GMT", "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", "Set-Cookie": [ - "fpc=Aih2w7HdW0RLsLtcNXW6NJo; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", - "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", - "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + "fpc=AqTPPhjdgKFNp1GfJ3D4mN_i05sNAgAAADoadtsOAAAA; expires=Fri, 10-Mar-2023 22:33:34 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" ], "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "X-Content-Type-Options": "nosniff", - "x-ms-ests-server": "2.1.13672.11 - SCUS ProdSlices", + "x-ms-ests-server": "2.1.14601.8 - WUS2 ProdSlices", "X-XSS-Protection": "0" }, "ResponseBody": { @@ -183,7 +181,7 @@ "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "Correlation-Context": "RequestType=Startup", - "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)" }, "RequestBody": null, "StatusCode": 200, @@ -193,24 +191,44 @@ "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", "Connection": "keep-alive", "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:41 GMT", - "Server": "openresty/1.17.8.2", + "Date": "Wed, 08 Feb 2023 22:33:34 GMT", + "Server": "openresty/1.21.4.1", "Strict-Transport-Security": "max-age=15724800; includeSubDomains", - "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", "Transfer-Encoding": "chunked", - "x-ms-correlation-request-id": "30054af8-b684-4033-b1b5-9c071f491287" + "x-ms-correlation-request-id": "ebb37104-0aea-4849-b478-b2261c34e096" }, "ResponseBody": { "items": [ { - "etag": "RX6Uzn3YdpSnDk7SX9kzxXwDFdYhR8dGGLEXBZ6i_nM", + "etag": "5l-CzldxczbRRnDjgiWcnuL__i--zHCffz7xO8ngGos", + "key": ".appconfig.featureflag/Alpha", + "label": null, + "content_type": "application/vnd.microsoft.appconfig.ff\u002Bjson;charset=utf-8", + "value": "{\u0022id\u0022:\u0022Alpha\u0022,\u0022description\u0022:\u0022\u0022,\u0022enabled\u0022:false,\u0022conditions\u0022:{\u0022client_filters\u0022:[]}}", + "tags": {}, + "locked": false, + "last_modified": "2023-01-30T20:25:07\u002B00:00" + }, + { + "etag": "Wfq_mEPSSfl9m4evo5sZDiZMkpn_6kH5xtJVH_D7OQY", "key": "message", "label": null, "content_type": "", "value": "hi", "tags": {}, "locked": false, - "last_modified": "2022-09-27T19:41:40\u002B00:00" + "last_modified": "2023-01-25T00:21:41\u002B00:00" + }, + { + "etag": "lO0_bzj8OQZOAMw2U2Ydzd4NJtYpXyQ5CbXktXLRhMs", + "key": "messageVal", + "label": null, + "content_type": null, + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-23T19:14:32\u002B00:00" }, { "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", @@ -237,4 +255,4 @@ } ], "Variables": {} -} \ No newline at end of file +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_selectors.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_selectors.json index 692dc3e00582..84302dc1dfbf 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_selectors.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_selectors.json @@ -7,7 +7,7 @@ "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", - "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + "User-Agent": "azsdk-python-identity/1.12.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" }, "RequestBody": null, "StatusCode": 200, @@ -17,16 +17,15 @@ "Cache-Control": "max-age=86400, private", "Content-Length": "1599", "Content-Type": "application/json; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:41 GMT", + "Date": "Wed, 08 Feb 2023 22:33:34 GMT", "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", "Set-Cookie": [ - "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAgAAAGBzxdoOAAAA; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", - "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", - "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + "fpc=AqTPPhjdgKFNp1GfJ3D4mN_i05sNAgAAADoadtsOAAAA; expires=Fri, 10-Mar-2023 22:33:35 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" ], "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "X-Content-Type-Options": "nosniff", - "x-ms-ests-server": "2.1.13672.11 - EUS ProdSlices", + "x-ms-ests-server": "2.1.14601.8 - EUS ProdSlices", "X-XSS-Protection": "0" }, "ResponseBody": { @@ -104,8 +103,8 @@ "Accept": "application/json", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", - "Cookie": "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAgAAAGBzxdoOAAAA; stsservicecookie=estsfd; x-ms-gateway-slice=estsfd", - "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + "Cookie": "fpc=AqTPPhjdgKFNp1GfJ3D4mN_i05sNAgAAADoadtsOAAAA; x-ms-gateway-slice=estsfd", + "User-Agent": "azsdk-python-identity/1.12.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" }, "RequestBody": null, "StatusCode": 200, @@ -115,16 +114,15 @@ "Cache-Control": "max-age=86400, private", "Content-Length": "945", "Content-Type": "application/json; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:41 GMT", + "Date": "Wed, 08 Feb 2023 22:33:35 GMT", "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", "Set-Cookie": [ - "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAgAAAGBzxdoOAAAA; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", - "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", - "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + "fpc=AqTPPhjdgKFNp1GfJ3D4mN_i05sNAgAAADoadtsOAAAA; expires=Fri, 10-Mar-2023 22:33:35 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" ], "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "X-Content-Type-Options": "nosniff", - "x-ms-ests-server": "2.1.13672.11 - SCUS ProdSlices", + "x-ms-ests-server": "2.1.14526.6 - NCUS ProdSlices", "X-XSS-Protection": "0" }, "ResponseBody": { @@ -182,7 +180,7 @@ "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "Correlation-Context": "RequestType=Startup", - "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)" }, "RequestBody": null, "StatusCode": 200, @@ -192,12 +190,12 @@ "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", "Connection": "keep-alive", "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:42 GMT", - "Server": "openresty/1.17.8.2", + "Date": "Wed, 08 Feb 2023 22:33:35 GMT", + "Server": "openresty/1.21.4.1", "Strict-Transport-Security": "max-age=15724800; includeSubDomains", - "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", "Transfer-Encoding": "chunked", - "x-ms-correlation-request-id": "98fc8948-d720-480a-a4f8-837e65f57cd0" + "x-ms-correlation-request-id": "f4c104ee-e863-44d7-9d1a-b4855456506f" }, "ResponseBody": { "items": [ @@ -216,4 +214,4 @@ } ], "Variables": {} -} \ No newline at end of file +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json index ae777e18c7df..6d28f8d8c56f 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json @@ -7,7 +7,7 @@ "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", - "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + "User-Agent": "azsdk-python-identity/1.12.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" }, "RequestBody": null, "StatusCode": 200, @@ -17,16 +17,15 @@ "Cache-Control": "max-age=86400, private", "Content-Length": "1599", "Content-Type": "application/json; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:41 GMT", + "Date": "Wed, 08 Feb 2023 22:33:34 GMT", "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", "Set-Cookie": [ - "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAQAAAGBzxdoOAAAA; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", - "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", - "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + "fpc=AqTPPhjdgKFNp1GfJ3D4mN_i05sNAgAAADoadtsOAAAA; expires=Fri, 10-Mar-2023 22:33:34 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" ], "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "X-Content-Type-Options": "nosniff", - "x-ms-ests-server": "2.1.13672.11 - NCUS ProdSlices", + "x-ms-ests-server": "2.1.14601.8 - SCUS ProdSlices", "X-XSS-Protection": "0" }, "ResponseBody": { @@ -104,8 +103,8 @@ "Accept": "application/json", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", - "Cookie": "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAQAAAGBzxdoOAAAA; stsservicecookie=estsfd; x-ms-gateway-slice=estsfd", - "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + "Cookie": "fpc=AqTPPhjdgKFNp1GfJ3D4mN_i05sNAgAAADoadtsOAAAA; x-ms-gateway-slice=estsfd", + "User-Agent": "azsdk-python-identity/1.12.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" }, "RequestBody": null, "StatusCode": 200, @@ -115,16 +114,15 @@ "Cache-Control": "max-age=86400, private", "Content-Length": "945", "Content-Type": "application/json; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:41 GMT", + "Date": "Wed, 08 Feb 2023 22:33:34 GMT", "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", "Set-Cookie": [ - "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAQAAAGBzxdoOAAAA; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", - "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", - "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + "fpc=AqTPPhjdgKFNp1GfJ3D4mN_i05sNAgAAADoadtsOAAAA; expires=Fri, 10-Mar-2023 22:33:34 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" ], "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "X-Content-Type-Options": "nosniff", - "x-ms-ests-server": "2.1.13672.11 - EUS ProdSlices", + "x-ms-ests-server": "2.1.14601.8 - EUS ProdSlices", "X-XSS-Protection": "0" }, "ResponseBody": { @@ -182,7 +180,7 @@ "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "Correlation-Context": "RequestType=Startup", - "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + "User-Agent": "python-appconfiguration-provider/1.0.0b2 azsdk-python-appconfiguration/1.4.0b1 Python/3.9.13 (Windows-10-10.0.19044-SP0)" }, "RequestBody": null, "StatusCode": 200, @@ -192,24 +190,44 @@ "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", "Connection": "keep-alive", "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", - "Date": "Tue, 27 Sep 2022 22:42:41 GMT", - "Server": "openresty/1.17.8.2", + "Date": "Wed, 08 Feb 2023 22:33:34 GMT", + "Server": "openresty/1.21.4.1", "Strict-Transport-Security": "max-age=15724800; includeSubDomains", - "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Sync-Token": "zAJw6V16=NzoxNyMyMTg1MjA2NQ==;sn=21852065", "Transfer-Encoding": "chunked", - "x-ms-correlation-request-id": "902e6b7c-5cfa-45b1-a567-c8ca2469af77" + "x-ms-correlation-request-id": "b55b6f70-f8d4-4275-bf6d-5330cf1a14db" }, "ResponseBody": { "items": [ { - "etag": "RX6Uzn3YdpSnDk7SX9kzxXwDFdYhR8dGGLEXBZ6i_nM", + "etag": "5l-CzldxczbRRnDjgiWcnuL__i--zHCffz7xO8ngGos", + "key": ".appconfig.featureflag/Alpha", + "label": null, + "content_type": "application/vnd.microsoft.appconfig.ff\u002Bjson;charset=utf-8", + "value": "{\u0022id\u0022:\u0022Alpha\u0022,\u0022description\u0022:\u0022\u0022,\u0022enabled\u0022:false,\u0022conditions\u0022:{\u0022client_filters\u0022:[]}}", + "tags": {}, + "locked": false, + "last_modified": "2023-01-30T20:25:07\u002B00:00" + }, + { + "etag": "Wfq_mEPSSfl9m4evo5sZDiZMkpn_6kH5xtJVH_D7OQY", "key": "message", "label": null, "content_type": "", "value": "hi", "tags": {}, "locked": false, - "last_modified": "2022-09-27T19:41:40\u002B00:00" + "last_modified": "2023-01-25T00:21:41\u002B00:00" + }, + { + "etag": "lO0_bzj8OQZOAMw2U2Ydzd4NJtYpXyQ5CbXktXLRhMs", + "key": "messageVal", + "label": null, + "content_type": null, + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2023-01-23T19:14:32\u002B00:00" }, { "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", @@ -236,4 +254,4 @@ } ], "Variables": {} -} \ No newline at end of file +} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py new file mode 100644 index 000000000000..493ee31a032f --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py @@ -0,0 +1,46 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from azure.appconfiguration.provider.aio import load_provider +from azure.appconfiguration.provider import SettingSelector +from devtools_testutils import AzureRecordedTestCase +from devtools_testutils.aio import recorded_by_proxy_async +from async_preparers import app_config_decorator_async + +class TestAppConfigurationProvider(AzureRecordedTestCase): + + async def build_provider(self, connection_string, trimmed_key_prefixes=[], selects={SettingSelector("*", "\0")}): + return await load_provider(connection_string=connection_string, trimmed_key_prefixes=trimmed_key_prefixes, selects=selects) + + # method: provider_creation + @app_config_decorator_async + @recorded_by_proxy_async + async def test_provider_creation(self, appconfiguration_connection_string): + async with await self.build_provider(appconfiguration_connection_string) as client: + assert client["message"] == "hi" + assert client["my_json"]["key"] == "value" + assert client["FeatureManagement"][".appconfig.featureflag/Alpha"] == '{\"enabled\": false, \"conditions\": {\"client_filters\": []}}' + + # method: provider_trimmed_key_prefixes + @app_config_decorator_async + @recorded_by_proxy_async + async def test_provider_trimmed_key_prefixes(self, appconfiguration_connection_string): + trimmed = {"test."} + async with await self.build_provider(appconfiguration_connection_string, trimmed_key_prefixes=trimmed) as client: + assert client["message"] == "hi" + assert client["my_json"]["key"] == "value" + assert client["trimmed"] == "key" + assert "test.trimmed" not in client + assert client["FeatureManagement"][".appconfig.featureflag/Alpha"] == '{\"enabled\": false, \"conditions\": {\"client_filters\": []}}' + + # method: provider_selectors + @app_config_decorator_async + @recorded_by_proxy_async + async def test_provider_selectors(self, appconfiguration_connection_string): + selects = {SettingSelector("message*", "dev")} + async with await self.build_provider(appconfiguration_connection_string, selects=selects) as client: + assert client["message"] == "test" + assert "test.trimmed" not in client + assert "FeatureManagement" not in client 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 new file mode 100644 index 000000000000..f1903175ee34 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_aad.py @@ -0,0 +1,48 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from azure.appconfiguration.provider.aio import load_provider +from azure.appconfiguration.provider import SettingSelector +from devtools_testutils import AzureRecordedTestCase +from devtools_testutils.aio import recorded_by_proxy_async +from azure.appconfiguration.aio import AzureAppConfigurationClient +from async_preparers import app_config_decorator_async + +class TestAppConfigurationProvider(AzureRecordedTestCase): + + async def build_provider_aad(self, endpoint, trimmed_key_prefixes=[], selects={SettingSelector("*", "\0")}): + cred = self.get_credential(AzureAppConfigurationClient, is_async=True) + return await load_provider(credential=cred, endpoint=endpoint, trimmed_key_prefixes=trimmed_key_prefixes, selects=selects) + + # method: provider_creation_aad + @app_config_decorator_async + @recorded_by_proxy_async + async def test_provider_creation_aad(self, appconfiguration_endpoint_string): + async with await self.build_provider_aad(appconfiguration_endpoint_string) as client: + assert client["message"] == "hi" + assert client["my_json"]["key"] == "value" + assert client["FeatureManagement"][".appconfig.featureflag/Alpha"] == '{\"enabled\": false, \"conditions\": {\"client_filters\": []}}' + + # method: provider_trimmed_key_prefixes + @app_config_decorator_async + @recorded_by_proxy_async + async def test_provider_trimmed_key_prefixes(self, appconfiguration_endpoint_string): + trimmed = {"test."} + async with await self.build_provider_aad(appconfiguration_endpoint_string, trimmed_key_prefixes=trimmed) as client: + assert client["message"] == "hi" + assert client["my_json"]["key"] == "value" + assert client["trimmed"] == "key" + assert "test.trimmed" not in client + assert client["FeatureManagement"][".appconfig.featureflag/Alpha"] == '{\"enabled\": false, \"conditions\": {\"client_filters\": []}}' + + # method: provider_selectors + @app_config_decorator_async + @recorded_by_proxy_async + async def test_provider_selectors(self, appconfiguration_endpoint_string): + selects = {SettingSelector("message*", "dev")} + async with await self.build_provider_aad(appconfiguration_endpoint_string, selects=selects) as client: + assert client["message"] == "test" + assert "test.trimmed" not in client + assert "FeatureManagement" not in client diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py index ec14c4a5bad8..435eec2cb229 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- from azure.appconfiguration.provider import ( - AzureAppConfigurationProvider, + load_provider, SettingSelector ) from devtools_testutils import ( @@ -17,7 +17,7 @@ class TestAppConfigurationProvider(AzureRecordedTestCase): def build_provider(self, connection_string, trimmed_key_prefixes=[], selects={SettingSelector("*", "\0")}): - return AzureAppConfigurationProvider.load(connection_string=connection_string, trimmed_key_prefixes=trimmed_key_prefixes, selects=selects) + return load_provider(connection_string=connection_string, trimmed_key_prefixes=trimmed_key_prefixes, selects=selects) # method: provider_creation @recorded_by_proxy @@ -26,6 +26,7 @@ def test_provider_creation(self, appconfiguration_connection_string): client = self.build_provider(appconfiguration_connection_string) assert client["message"] == "hi" assert client["my_json"]["key"] == "value" + assert client["FeatureManagement"][".appconfig.featureflag/Alpha"] == '{\"enabled\": false, \"conditions\": {\"client_filters\": []}}' # method: provider_trimmed_key_prefixes @recorded_by_proxy @@ -37,6 +38,7 @@ def test_provider_trimmed_key_prefixes(self, appconfiguration_connection_string) assert client["my_json"]["key"] == "value" assert client["trimmed"] == "key" assert "test.trimmed" not in client + assert client["FeatureManagement"][".appconfig.featureflag/Alpha"] == '{\"enabled\": false, \"conditions\": {\"client_filters\": []}}' # method: provider_selectors @recorded_by_proxy @@ -46,3 +48,4 @@ def test_provider_selectors(self, appconfiguration_connection_string): client = self.build_provider(appconfiguration_connection_string, selects=selects) assert client["message"] == "test" assert "test.trimmed" not in client + assert "FeatureManagement" not in client 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 f68bee1436e1..1378dd6ed663 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_aad.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_aad.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- from azure.appconfiguration.provider import ( - AzureAppConfigurationProvider, + load_provider, SettingSelector ) from devtools_testutils import ( @@ -18,7 +18,7 @@ class TestAppConfigurationProvider(AzureRecordedTestCase): def build_provider_aad(self, endpoint, trimmed_key_prefixes=[], selects={SettingSelector("*", "\0")}): cred = self.get_credential(AzureAppConfigurationClient) - return AzureAppConfigurationProvider.load(credential=cred, endpoint=endpoint, trimmed_key_prefixes=trimmed_key_prefixes, selects=selects) + return load_provider(credential=cred, endpoint=endpoint, trimmed_key_prefixes=trimmed_key_prefixes, selects=selects) # method: provider_creation_aad @recorded_by_proxy @@ -27,6 +27,7 @@ def test_provider_creation_aad(self, appconfiguration_endpoint_string): client = self.build_provider_aad(appconfiguration_endpoint_string) assert client["message"] == "hi" assert client["my_json"]["key"] == "value" + assert client["FeatureManagement"][".appconfig.featureflag/Alpha"] == '{\"enabled\": false, \"conditions\": {\"client_filters\": []}}' # method: provider_trimmed_key_prefixes @recorded_by_proxy @@ -38,6 +39,7 @@ def test_provider_trimmed_key_prefixes(self, appconfiguration_endpoint_string): assert client["my_json"]["key"] == "value" assert client["trimmed"] == "key" assert "test.trimmed" not in client + assert client["FeatureManagement"][".appconfig.featureflag/Alpha"] == '{\"enabled\": false, \"conditions\": {\"client_filters\": []}}' # method: provider_selectors @recorded_by_proxy @@ -47,3 +49,4 @@ def test_provider_selectors(self, appconfiguration_endpoint_string): client = self.build_provider_aad(appconfiguration_endpoint_string, selects=selects) assert client["message"] == "test" assert "test.trimmed" not in client + assert "FeatureManagement" not in client diff --git a/shared_requirements.txt b/shared_requirements.txt index 6755c27542ba..309453450511 100644 --- a/shared_requirements.txt +++ b/shared_requirements.txt @@ -242,7 +242,8 @@ opentelemetry-sdk<2.0.0,>=1.5.0,!=1.10a0 #override azure-eventhub-checkpointstoretable azure-core<2.0.0,>=1.14.0 #override azure-appconfiguration azure-core<2.0.0,>=1.24.0 #override azure-mgmt-maintenance msrest>=0.7.1 -#override azure-appconfiguration-provider azure-appconfiguration<2.0.0,>=1.3.0 +#override azure-appconfiguration-provider azure-core<2.0.0,>=1.24.0 +#override azure-appconfiguration-provider azure-appconfiguration<2.0.0,>=1.4.0 #override azure-appconfiguration-provider azure-keyvault-secrets<5.0.0,>=4.3.0 #override azure-mgmt-datamigration typing-extensions>=4.3.0; python_version<'3.8.0' #override azure-mgmt-purview typing-extensions>=4.3.0; python_version<'3.8.0'