diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/api.metadata.yml b/sdk/appconfiguration/azure-appconfiguration-provider/api.metadata.yml index 54ff9f5af349..66cde6bb9551 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/api.metadata.yml +++ b/sdk/appconfiguration/azure-appconfiguration-provider/api.metadata.yml @@ -1,3 +1,3 @@ apiMdSha256: 1b252a78094b95bf515ebe5ec67fc2de12ce5b595bad25438a58241ca7153b84 -parserVersion: 0.3.28 +parserVersion: 0.3.30 pythonVersion: 3.14.3 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 a3f8f826679c..330e9981631a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -34,7 +34,6 @@ from ._user_agent import USER_AGENT from ._utils import get_startup_backoff -JSON = Mapping[str, Any] logger = logging.getLogger(__name__) @@ -96,7 +95,19 @@ def __init__(self, **kwargs: Any) -> None: self._on_refresh_error: Optional[Callable[[Exception], None]] = kwargs.pop("on_refresh_error", None) self._configuration_mapper: Optional[Callable] = kwargs.pop("configuration_mapper", None) - def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_failover_request: bool, **kwargs): + def _attempt_refresh( + self, client: ConfigurationClient, replica_count: int, is_failover_request: bool, **kwargs + ) -> None: + """ + Attempts to refresh configuration settings and feature flags using a single client. + + :param client: The configuration client to attempt the refresh against. + :type client: ~azure.appconfiguration.provider.ConfigurationClient + :param replica_count: The number of replica clients available, used for correlation telemetry. + :type replica_count: int + :param is_failover_request: Whether this attempt is a failover from a previously failed client. + :type is_failover_request: bool + """ settings_refreshed = False headers = self._update_correlation_context_header( kwargs.pop("headers", {}), diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index e5b6240d74e6..4d9ee90978a6 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -179,7 +179,7 @@ def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str seed=123abc\ndefault_when_enabled=Control\npercentiles=0,Control,20;20,Test,100\nvariants=Control,standard;Test,special # pylint:disable=line-too-long :param Dict[str, JSON] feature_flag_value: The feature to generate an allocation ID for. - :rtype: str + :rtype: Optional[str] :return: The allocation ID. """ @@ -303,11 +303,11 @@ def values(self) -> ValuesView[Union[str, Mapping[str, Any]]]: resolved. :return: A list of values loaded from Azure App Configuration. The values are either Strings or JSON objects, - based on there content type. + based on their content type. :rtype: ValuesView[Union[str, Mapping[str, Any]]] """ with self._update_lock: - return (self._dict).values() + return self._dict.values() @overload def get(self, key: str, default: None = None) -> Union[str, JSON, None]: ... @@ -440,7 +440,9 @@ def _update_correlation_context_header( def _deduplicate_settings(self, configuration_settings: List[ConfigurationSetting]) -> List[ConfigurationSetting]: """ - Deduplicates configuration settings by key. + Deduplicates configuration settings by key, ignoring label. The provider exposes a flat + key->value mapping, so when multiple selectors return the same key the last one wins, + regardless of label. :param List[ConfigurationSetting] configuration_settings: The list of configuration settings to deduplicate :return: A list of unique configuration settings diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py index 651a55577222..a5d0033647a8 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py @@ -7,7 +7,7 @@ import time import random from dataclasses import dataclass -from typing import Tuple, Union, Dict, List, Optional, Mapping +from typing import Tuple, Union, List, Optional, Mapping from typing_extensions import Self from azure.core import MatchConditions from azure.core.tracing.decorator import distributed_trace @@ -101,14 +101,14 @@ def from_connection_string( ) def _check_configuration_setting( - self, key: str, label: str, etag: Optional[str], headers: Dict[str, str], **kwargs + self, key: str, label: str, etag: Optional[str], headers: Mapping[str, str], **kwargs ) -> Tuple[bool, Union[ConfigurationSetting, None]]: """ Checks if the configuration setting have been updated since the last refresh. - :param str key: key to check for chances + :param str key: key to check for changes :param str label: label to check for changes - :param str etag: etag to check for changes + :param Optional[str] etag: etag to check for changes :param Mapping[str, str] headers: headers to use for the request :return: A tuple with the first item being true/false if a change is detected. The second item is the updated value if a change was detected. @@ -284,7 +284,7 @@ def check_feature_flag_page_etags( @distributed_trace def get_updated_watched_settings( - self, watched_settings: Mapping[Tuple[str, str], Optional[str]], headers: Dict[str, str], **kwargs + self, watched_settings: Mapping[Tuple[str, str], Optional[str]], headers: Mapping[str, str], **kwargs ) -> Mapping[Tuple[str, str], Optional[str]]: """ Checks if any of the watch keys have changed, and updates them if they have. @@ -292,8 +292,8 @@ def get_updated_watched_settings( :param Mapping[Tuple[str, str], Optional[str]] watched_settings: The configuration settings to check for changes :param Mapping[str, str] headers: The headers to use for the request - :return: Updated value of the configuration watched settings. - :rtype: Union[Dict[Tuple[str, str], str], None] + :return: Updated value of the configuration watched settings. Empty if no change was detected. + :rtype: Mapping[Tuple[str, str], Optional[str]] """ updated_watched_settings = dict(watched_settings) trigger_refresh = False @@ -319,8 +319,8 @@ def get_configuration_setting(self, key: str, label: str, **kwargs) -> Optional[ :param str key: The key of the configuration setting :param str label: The label of the configuration setting - :return: The configuration setting - :rtype: ConfigurationSetting + :return: The configuration setting, or None when the supplied ETag has not been modified. + :rtype: Optional[ConfigurationSetting] """ return self._client.get_configuration_setting(key=key, label=label, **kwargs) @@ -403,12 +403,12 @@ def __init__( endpoint: str, credential: Optional["TokenCredential"], user_agent: str, - retry_total, - retry_backoff_max, - replica_discovery_enabled, - min_backoff_sec, - max_backoff_sec, - load_balancing_enabled, + retry_total: int, + retry_backoff_max: int, + replica_discovery_enabled: bool, + min_backoff_sec: int, + max_backoff_sec: int, + load_balancing_enabled: bool, **kwargs, ): super(ConfigurationClientManager, self).__init__( @@ -445,6 +445,7 @@ def get_next_active_client(self) -> Optional[_ConfigurationClientWrapper]: method returns None. :return: The next client to be used for the request. + :rtype: Optional[_ConfigurationClientWrapper] """ if not self._active_clients: self._last_active_client_name = "" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py index cb085760012d..46218378629b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py @@ -5,13 +5,11 @@ # ------------------------------------------------------------------------- import random from dataclasses import dataclass -from typing import Optional, Mapping, Any +from typing import Optional FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL = 3600 # 1 hour in seconds MINIMAL_CLIENT_REFRESH_INTERVAL = 30 # 30 seconds -JSON = Mapping[str, Any] - @dataclass class _ConfigurationClientWrapperBase: @@ -23,12 +21,12 @@ def __init__( self, endpoint: str, user_agent: str, - retry_total, - retry_backoff_max, - replica_discovery_enabled, - min_backoff_sec, - max_backoff_sec, - _load_balancing_enabled: bool, + retry_total: int, + retry_backoff_max: int, + replica_discovery_enabled: bool, + min_backoff_sec: int, + max_backoff_sec: int, + load_balancing_enabled: bool, **kwargs, ): self._last_active_client_name = "" @@ -41,9 +39,10 @@ def __init__( self._args = dict(kwargs) self._min_backoff_sec = min_backoff_sec self._max_backoff_sec = max_backoff_sec - self._load_balancing_enabled = _load_balancing_enabled + self._load_balancing_enabled = load_balancing_enabled def _calculate_backoff(self, attempts: int) -> float: + max_attempts = 63 ms_per_second = 1000 # 1 Second in milliseconds @@ -55,8 +54,7 @@ def _calculate_backoff(self, attempts: int) -> float: calculated_milliseconds = max(1, min_backoff_milliseconds) * (1 << min(attempts, max_attempts)) - if calculated_milliseconds > max_backoff_milliseconds or calculated_milliseconds <= 0: - calculated_milliseconds = max_backoff_milliseconds + calculated_milliseconds = min(calculated_milliseconds, max_backoff_milliseconds) return min_backoff_milliseconds + ( random.uniform(0.0, 1.0) * (calculated_milliseconds - min_backoff_milliseconds) # nosec 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 3e68591bb46c..da34535a7329 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -19,11 +19,11 @@ # Environment Variable Constants # ------------------------------------------------------------------------ REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE = "AZURE_APP_CONFIGURATION_TRACING_DISABLED" -AzureFunctionEnvironmentVariable = "FUNCTIONS_EXTENSION_VERSION" -AzureWebAppEnvironmentVariable = "WEBSITE_SITE_NAME" -ContainerAppEnvironmentVariable = "CONTAINER_APP_NAME" -KubernetesEnvironmentVariable = "KUBERNETES_PORT" -ServiceFabricEnvironmentVariable = "Fabric_NodeName" # cspell:disable-line +AZURE_FUNCTION_ENVIRONMENT_VARIABLE = "FUNCTIONS_EXTENSION_VERSION" +AZURE_WEB_APP_ENVIRONMENT_VARIABLE = "WEBSITE_SITE_NAME" +CONTAINER_APP_ENVIRONMENT_VARIABLE = "CONTAINER_APP_NAME" +KUBERNETES_ENVIRONMENT_VARIABLE = "KUBERNETES_PORT" +SERVICE_FABRIC_ENVIRONMENT_VARIABLE = "Fabric_NodeName" # cspell:disable-line # ------------------------------------------------------------------------ # Telemetry and Tracing Constants @@ -38,9 +38,9 @@ APP_CONFIG_AI_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/" APP_CONFIG_AICC_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/chat-completion" -# ============================================================================= +# ------------------------------------------------------------------------ # Startup Retry Constants -# ============================================================================= +# ------------------------------------------------------------------------ # Timeout DEFAULT_STARTUP_TIMEOUT = 100 # seconds diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_json.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_json.py index 3cbabde25f61..a38a79ed1cf5 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_json.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_json.py @@ -43,7 +43,7 @@ def _find_string_end(text: str, index: int) -> int: def remove_json_comments(text: str) -> str: """ - Removes comments from a JSON file. Supports //, and /* ... */ comments. + Removes comments from a JSON string. Supports //, and /* ... */ comments. Returns as string. :param text: The input JSON string with comments. diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py index d147236c1bf4..83ee142510a4 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py @@ -3,14 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- -from typing import Mapping, Any, Dict +from typing import Any, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier from azure.core.exceptions import ServiceRequestError from ._secret_provider_base import _SecretProviderBase -JSON = Mapping[str, Any] - class SecretProvider(_SecretProviderBase): diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py index 7b1abb9d97dc..11832cd663ce 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py @@ -4,9 +4,7 @@ # license information. # ------------------------------------------------------------------------- from typing import ( - Mapping, Any, - TypeVar, Optional, Dict, Tuple, @@ -15,9 +13,6 @@ from azure.keyvault.secrets import KeyVaultSecretIdentifier from .._azureappconfigurationproviderbase import _RefreshTimer -JSON = Mapping[str, Any] -_T = TypeVar("_T") - class _SecretProviderBase: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_load.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_load.py index 6660ba2a6786..f763c626a512 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_load.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_load.py @@ -5,6 +5,7 @@ # ------------------------------------------------------------------------- import datetime from typing import ( + Any, Callable, List, Mapping, @@ -24,10 +25,11 @@ ) from ._azureappconfigurationprovider import ( AzureAppConfigurationProvider, - JSON, _buildprovider, ) +JSON = Mapping[str, Any] + @overload def load( # pylint: disable=docstring-keyword-should-match-keyword-only @@ -132,7 +134,7 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only :keyword str connection_string: Connection string for App Configuration resource. :keyword Optional[List[~azure.appconfiguration.provider.SettingSelector]] selects: List of setting selectors to filter configuration settings - :keyword trim_prefixes: Optional[List[str]] trim_prefixes: List of prefixes to trim from configuration keys + :keyword Optional[List[str]] trim_prefixes: List of prefixes to trim from configuration keys :keyword ~azure.core.credentials.TokenCredential keyvault_credential: A credential for authenticating with the key vault. This is optional if keyvault_client_configs is provided. :keyword Mapping[str, Mapping] keyvault_client_configs: A Mapping of SecretClient endpoints to client @@ -145,9 +147,6 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only :keyword List[Tuple[str, str]] refresh_on: One or more settings whose modification will trigger a full refresh after a fixed interval. This should be a list of Key-Label pairs for specific settings (filters and wildcards are not supported). - :keyword refresh_on: One or more settings whose modification will trigger a full refresh after a fixed interval. - This should be a list of Key-Label pairs for specific settings (filters and wildcards are not supported). - :paramtype refresh_on: List[Tuple[str, str]] :keyword int refresh_interval: The minimum time in seconds between when a call to `refresh` will actually trigger a service call to update the settings. Default value is 30 seconds. :keyword refresh_enabled: Optional flag to enable or disable refreshing of configuration settings. Defaults to diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py index c43d3cdc91c0..9419cb2685ae 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py @@ -35,7 +35,7 @@ def __init__( :keyword client_configs: A Mapping of SecretClient endpoints to client configurations from azure-keyvault-secrets. This is optional if credential is provided. If a credential isn't provided a credential will need to be in each set for each. - :paramtype client_configs: Mapping[Url, Mapping] + :paramtype client_configs: Mapping[str, Mapping[str, Any]] :keyword secret_resolver: A function that takes a URI and returns a value. :paramtype secret_resolver: Callable[[str], str] """ @@ -52,20 +52,20 @@ class SettingSelector: :keyword key_filter: A filter to select configuration settings and feature flags based on their keys. Cannot be used with snapshot_name. - :type key_filter: str + :paramtype key_filter: Optional[str] :keyword label_filter: A filter to select configuration settings and feature flags based on their labels. Default value is \0 i.e. (No Label) as seen in the portal. Cannot be used with snapshot_name. - :type label_filter: Optional[str] + :paramtype label_filter: Optional[str] :keyword tag_filters: A filter to select configuration settings and feature flags based on their tags. This is a list of strings that will be used to match tags on the configuration settings. Reserved characters (\\*, \\, ,) must be escaped with backslash if they are part of the value. Tag filters must follow the format "tagName=tagValue", for empty values use "tagName=" and for null values use "tagName=\\0". Cannot be used with snapshot_name. - :type tag_filters: Optional[List[str]] + :paramtype tag_filters: Optional[List[str]] :keyword snapshot_name: The name of the snapshot to load configuration settings from. When specified, all configuration settings from the snapshot will be loaded. Cannot be used with key_filter, label_filter, or tag_filters. - :type snapshot_name: Optional[str] + :paramtype snapshot_name: Optional[str] """ def __init__( diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_refresh_timer.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_refresh_timer.py index 9fa84c06867e..c11842a4a3d0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_refresh_timer.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_refresh_timer.py @@ -48,8 +48,7 @@ def _calculate_backoff(self) -> float: calculated_milliseconds = max(1, min_backoff_milliseconds) * (1 << min(self._attempts, max_attempts)) - if calculated_milliseconds > max_backoff_milliseconds or calculated_milliseconds <= 0: - calculated_milliseconds = max_backoff_milliseconds + calculated_milliseconds = min(calculated_milliseconds, max_backoff_milliseconds) return min_backoff_milliseconds + ( random.uniform(0.0, 1.0) * (calculated_milliseconds - min_backoff_milliseconds) # nosec diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py index bc308d0bf1ac..4c1b0a46c6d0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -8,13 +8,14 @@ from importlib.metadata import version, PackageNotFoundError from ._constants import ( REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, - ServiceFabricEnvironmentVariable, - AzureFunctionEnvironmentVariable, - AzureWebAppEnvironmentVariable, - ContainerAppEnvironmentVariable, - KubernetesEnvironmentVariable, + SERVICE_FABRIC_ENVIRONMENT_VARIABLE, + AZURE_FUNCTION_ENVIRONMENT_VARIABLE, + AZURE_WEB_APP_ENVIRONMENT_VARIABLE, + CONTAINER_APP_ENVIRONMENT_VARIABLE, + KUBERNETES_ENVIRONMENT_VARIABLE, APP_CONFIG_AI_MIME_PROFILE, APP_CONFIG_AICC_MIME_PROFILE, + SNAPSHOT_REFERENCE_TAG, ) # Feature flag filter names @@ -29,17 +30,11 @@ FEATURE_FLAG_USES_TELEMETRY_TAG = "Telemetry" FEATURE_FLAG_USES_SEED_TAG = "Seed" -FEATURE_FLAG_MAX_VARIANTS_KEY = "MaxVariants" -FEATURE_FLAG_FEATURES_KEY = "FFFeatures" - -CHAT_COMPLETION_PROFILE = "chat-completion" - # Feature flag constants for telemetry LOAD_BALANCING_FEATURE = "LB" AI_CONFIGURATION_FEATURE = "AI" AI_CHAT_COMPLETION_FEATURE = "AICC" -SNAPSHOT_REFERENCE_TAG = "SnapshotRef" # Correlation context constants FEATUREMANAGEMENT_PACKAGE = "featuremanagement" @@ -240,15 +235,13 @@ def update_feature_filter_telemetry(self, feature_flag) -> None: :param feature_flag: The feature flag to analyze for filter usage. :type feature_flag: FeatureFlagConfigurationSetting """ - # Constants are already imported at module level - if feature_flag.filters: - for filter in feature_flag.filters: - if filter.get("name") in PERCENTAGE_FILTER_NAMES: + for feature_filter in feature_flag.filters: + if feature_filter.get("name") in PERCENTAGE_FILTER_NAMES: self.feature_filter_usage[PERCENTAGE_FILTER_KEY] = True - elif filter.get("name") in TIME_WINDOW_FILTER_NAMES: + elif feature_filter.get("name") in TIME_WINDOW_FILTER_NAMES: self.feature_filter_usage[TIME_WINDOW_FILTER_KEY] = True - elif filter.get("name") in TARGETING_FILTER_NAMES: + elif feature_filter.get("name") in TARGETING_FILTER_NAMES: self.feature_filter_usage[TARGETING_FILTER_KEY] = True else: self.feature_filter_usage[CUSTOM_FILTER_KEY] = True @@ -322,15 +315,15 @@ def get_host_type() -> str: :return: The detected host type. :rtype: str """ - if os.environ.get(AzureFunctionEnvironmentVariable): + if os.environ.get(AZURE_FUNCTION_ENVIRONMENT_VARIABLE): return HostType.AZURE_FUNCTION - if os.environ.get(AzureWebAppEnvironmentVariable): + if os.environ.get(AZURE_WEB_APP_ENVIRONMENT_VARIABLE): return HostType.AZURE_WEB_APP - if os.environ.get(ContainerAppEnvironmentVariable): + if os.environ.get(CONTAINER_APP_ENVIRONMENT_VARIABLE): return HostType.CONTAINER_APP - if os.environ.get(KubernetesEnvironmentVariable): + if os.environ.get(KUBERNETES_ENVIRONMENT_VARIABLE): return HostType.KUBERNETES - if os.environ.get(ServiceFabricEnvironmentVariable): + if os.environ.get(SERVICE_FABRIC_ENVIRONMENT_VARIABLE): return HostType.SERVICE_FABRIC return HostType.UNIDENTIFIED diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_snapshot_reference_parser.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_snapshot_reference_parser.py index 08213687f188..f1dda27a84b0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_snapshot_reference_parser.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_snapshot_reference_parser.py @@ -20,7 +20,8 @@ def parse(setting: Optional[ConfigurationSetting]) -> str: """ Parse a snapshot reference from a configuration setting containing snapshot reference JSON. - :param Optional[ConfigurationSetting] setting: The configuration setting containing the snapshot reference JSON + :param setting: The configuration setting containing the snapshot reference JSON + :type setting: Optional[ConfigurationSetting] :return: The snapshot name extracted from the reference :rtype: str :raises ValueError: When the setting is None @@ -63,13 +64,15 @@ def parse(setting: Optional[ConfigurationSetting]) -> str: f"property must be a string value, but found {type(snapshot_name).__name__}." ) - if not snapshot_name.strip(): + snapshot_name = snapshot_name.strip() + + if not snapshot_name: raise ValueError( f"Invalid snapshot reference format for key '{setting.key}' " f"(label: '{setting.label}'). Snapshot name cannot be empty or whitespace." ) - return snapshot_name.strip() + return snapshot_name except json.JSONDecodeError as json_ex: raise ValueError( diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py index be1b4d6a850e..6d03d58fa994 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py @@ -9,7 +9,6 @@ from typing import ( Any, Dict, - Mapping, Optional, Tuple, ) @@ -21,9 +20,6 @@ STARTUP_BACKOFF_INTERVALS, ) - -JSON = Mapping[str, Any] - min_uptime = 5 @@ -47,7 +43,7 @@ def get_startup_backoff(elapsed_seconds: float, attempts: int) -> Tuple[float, b :param elapsed_seconds: The time elapsed since startup began, in seconds. :type elapsed_seconds: float - :param attempts: The number of retry attempts made (1-based). + :param attempts: The number of retry attempts made (0-based). :type attempts: int :return: A tuple where the first element is the backoff duration in seconds, and the second element indicates if the fixed backoff window has been exceeded. @@ -194,18 +190,16 @@ def _calculate_backoff_duration(attempts: int) -> float: """ attempts += 1 if attempts < 1: - raise ValueError("Number of attempts must be at least 1.") + raise ValueError("Number of attempts must be at least 0.") if attempts == 1: return MIN_STARTUP_EXPONENTIAL_BACKOFF_DURATION # Calculate exponential backoff: min * 2^(attempts-1) - # Cap the shift amount to prevent overflow safe_shift = min(attempts - 1, 63) calculated = MIN_STARTUP_EXPONENTIAL_BACKOFF_DURATION * (1 << safe_shift) # Cap at max duration - if calculated > MAX_STARTUP_BACKOFF_DURATION or calculated <= 0: # Check for overflow - calculated = MAX_STARTUP_BACKOFF_DURATION + calculated = min(calculated, MAX_STARTUP_BACKOFF_DURATION) return _jitter(calculated, JITTER_RATIO) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_client_manager.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_client_manager.py index abcb6233e9a1..6afdb0fed5d1 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_client_manager.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_client_manager.py @@ -7,7 +7,7 @@ import time import random from dataclasses import dataclass -from typing import Tuple, Union, Dict, List, Optional, Mapping, TYPE_CHECKING +from typing import Tuple, Union, List, Optional, Mapping, TYPE_CHECKING from typing_extensions import Self from azure.core import MatchConditions from azure.core.tracing.decorator import distributed_trace @@ -103,14 +103,14 @@ def from_connection_string( ) async def _check_configuration_setting( - self, key: str, label: str, etag: Optional[str], headers: Dict[str, str], **kwargs + self, key: str, label: str, etag: Optional[str], headers: Mapping[str, str], **kwargs ) -> Tuple[bool, Union[ConfigurationSetting, None]]: """ Checks if the configuration setting have been updated since the last refresh. - :param str key: key to check for chances + :param str key: key to check for changes :param str label: label to check for changes - :param str etag: etag to check for changes + :param Optional[str] etag: etag to check for changes :param Mapping[str, str] headers: headers to use for the request :return: A tuple with the first item being true/false if a change is detected. The second item is the updated value if a change was detected. @@ -286,7 +286,7 @@ async def check_feature_flag_page_etags( @distributed_trace async def get_updated_watched_settings( - self, watched_settings: Mapping[Tuple[str, str], Optional[str]], headers: Dict[str, str], **kwargs + self, watched_settings: Mapping[Tuple[str, str], Optional[str]], headers: Mapping[str, str], **kwargs ) -> Mapping[Tuple[str, str], Optional[str]]: """ Checks if any of the watch keys have changed, and updates them if they have. @@ -294,8 +294,8 @@ async def get_updated_watched_settings( :param Mapping[Tuple[str, str], Optional[str]] watched_settings: The configuration settings to check for changes :param Mapping[str, str] headers: The headers to use for the request - :return: Updated value of the configuration watched settings. - :rtype: Union[Dict[Tuple[str, str], str], None] + :return: Updated value of the configuration watched settings. Empty if no change was detected. + :rtype: Mapping[Tuple[str, str], Optional[str]] """ updated_watched_settings = dict(watched_settings) trigger_refresh = False @@ -321,8 +321,8 @@ async def get_configuration_setting(self, key: str, label: str, **kwargs) -> Opt :param str key: The key of the configuration setting :param str label: The label of the configuration setting - :return: The configuration setting - :rtype: ConfigurationSetting + :return: The configuration setting, or None when the supplied ETag has not been modified. + :rtype: Optional[ConfigurationSetting] """ return await self._client.get_configuration_setting(key=key, label=label, **kwargs) @@ -405,12 +405,12 @@ def __init__( endpoint: str, credential: Optional["AsyncTokenCredential"], user_agent: str, - retry_total, - retry_backoff_max, - replica_discovery_enabled, - min_backoff_sec, - max_backoff_sec, - load_balancing_enabled, + retry_total: int, + retry_backoff_max: int, + replica_discovery_enabled: bool, + min_backoff_sec: int, + max_backoff_sec: int, + load_balancing_enabled: bool, **kwargs, ): super(AsyncConfigurationClientManager, self).__init__( @@ -447,6 +447,7 @@ def get_next_active_client(self) -> Optional[_AsyncConfigurationClientWrapper]: method returns None. :return: The next client to be used for the request. + :rtype: Optional[_AsyncConfigurationClientWrapper] """ if not self._active_clients: self._last_active_client_name = "" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_load.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_load.py index 81a8854340aa..bf2f36732906 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_load.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_load.py @@ -135,7 +135,7 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only :keyword str connection_string: Connection string for App Configuration resource. :keyword Optional[List[~azure.appconfiguration.provider.SettingSelector]] selects: List of setting selectors to filter configuration settings - :keyword trim_prefixes: Optional[List[str]] trim_prefixes: List of prefixes to trim from configuration keys + :keyword Optional[List[str]] trim_prefixes: List of prefixes to trim from configuration keys :keyword ~azure.core.credentials_async.AsyncTokenCredential keyvault_credential: A credential for authenticating with the key vault. This is optional if keyvault_client_configs is provided. :keyword Mapping[str, Mapping] keyvault_client_configs: A Mapping of SecretClient endpoints to client @@ -148,9 +148,6 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only :keyword List[Tuple[str, str]] refresh_on: One or more settings whose modification will trigger a full refresh after a fixed interval. This should be a list of Key-Label pairs for specific settings (filters and wildcards are not supported). - :keyword refresh_on: One or more settings whose modification will trigger a full refresh after a fixed interval. - This should be a list of Key-Label pairs for specific settings (filters and wildcards are not supported). - :paramtype refresh_on: List[Tuple[str, str]] :keyword int refresh_interval: The minimum time in seconds between when a call to `refresh` will actually trigger a service call to update the settings. Default value is 30 seconds. :keyword refresh_enabled: Optional flag to enable or disable refreshing of configuration settings. Defaults to diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 458bd10bcce8..99d03a587468 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -41,7 +41,6 @@ from .._user_agent import USER_AGENT from .._utils import get_startup_backoff -JSON = Mapping[str, Any] logger = logging.getLogger(__name__) @@ -110,6 +109,16 @@ def __init__(self, **kwargs: Any) -> None: async def _attempt_refresh( self, client: ConfigurationClient, replica_count: int, is_failover_request: bool, **kwargs ): + """ + Attempts to refresh configuration settings and feature flags using a single client. + + :param client: The configuration client to attempt the refresh against. + :type client: ~azure.appconfiguration.provider.aio.ConfigurationClient + :param replica_count: The number of replica clients available, used for correlation telemetry. + :type replica_count: int + :param is_failover_request: Whether this attempt is a failover from a previously failed client. + :type is_failover_request: bool + """ settings_refreshed = False headers = self._update_correlation_context_header( kwargs.pop("headers", {}), diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py index 70c61840d087..b043e3c30d67 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py @@ -4,15 +4,13 @@ # license information. # ------------------------------------------------------------------------- import inspect -from typing import Mapping, Any, Dict +from typing import Any, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets import KeyVaultSecretIdentifier from azure.keyvault.secrets.aio import SecretClient from azure.core.exceptions import ServiceRequestError from ..._key_vault._secret_provider_base import _SecretProviderBase -JSON = Mapping[str, Any] - class SecretProvider(_SecretProviderBase): diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py index ad3cf6285d8b..97084765b698 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py @@ -24,11 +24,11 @@ ) from azure.appconfiguration.provider._constants import ( REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, - ServiceFabricEnvironmentVariable, - AzureFunctionEnvironmentVariable, - AzureWebAppEnvironmentVariable, - ContainerAppEnvironmentVariable, - KubernetesEnvironmentVariable, + SERVICE_FABRIC_ENVIRONMENT_VARIABLE, + AZURE_FUNCTION_ENVIRONMENT_VARIABLE, + AZURE_WEB_APP_ENVIRONMENT_VARIABLE, + CONTAINER_APP_ENVIRONMENT_VARIABLE, + KUBERNETES_ENVIRONMENT_VARIABLE, APP_CONFIG_AI_MIME_PROFILE, APP_CONFIG_AICC_MIME_PROFILE, SNAPSHOT_REFERENCE_TAG, @@ -115,31 +115,31 @@ def test_get_host_type_unidentified(self): def test_get_host_type_azure_function(self): """Test host type detection for Azure Functions.""" - with patch.dict(os.environ, {AzureFunctionEnvironmentVariable: "test_value"}): + with patch.dict(os.environ, {AZURE_FUNCTION_ENVIRONMENT_VARIABLE: "test_value"}): result = _RequestTracingContext.get_host_type() self.assertEqual(result, HostType.AZURE_FUNCTION) def test_get_host_type_azure_web_app(self): """Test host type detection for Azure Web App.""" - with patch.dict(os.environ, {AzureWebAppEnvironmentVariable: "test_value"}): + with patch.dict(os.environ, {AZURE_WEB_APP_ENVIRONMENT_VARIABLE: "test_value"}): result = _RequestTracingContext.get_host_type() self.assertEqual(result, HostType.AZURE_WEB_APP) def test_get_host_type_container_app(self): """Test host type detection for Container App.""" - with patch.dict(os.environ, {ContainerAppEnvironmentVariable: "test_value"}): + with patch.dict(os.environ, {CONTAINER_APP_ENVIRONMENT_VARIABLE: "test_value"}): result = _RequestTracingContext.get_host_type() self.assertEqual(result, HostType.CONTAINER_APP) def test_get_host_type_kubernetes(self): """Test host type detection for Kubernetes.""" - with patch.dict(os.environ, {KubernetesEnvironmentVariable: "test_value"}): + with patch.dict(os.environ, {KUBERNETES_ENVIRONMENT_VARIABLE: "test_value"}): result = _RequestTracingContext.get_host_type() self.assertEqual(result, HostType.KUBERNETES) def test_get_host_type_service_fabric(self): """Test host type detection for Service Fabric.""" - with patch.dict(os.environ, {ServiceFabricEnvironmentVariable: "test_value"}): + with patch.dict(os.environ, {SERVICE_FABRIC_ENVIRONMENT_VARIABLE: "test_value"}): result = _RequestTracingContext.get_host_type() self.assertEqual(result, HostType.SERVICE_FABRIC) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_utils.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_utils.py index 9b2f23668dfd..2e7a38f4fe26 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_utils.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_utils.py @@ -183,11 +183,11 @@ def test_caps_at_max_duration(self): self.assertLessEqual(result, MAX_STARTUP_BACKOFF_DURATION * (1 + JITTER_RATIO)) def test_invalid_attempts_raises_error(self): - """Test that attempts < 1 raises ValueError.""" + """Test that attempts < 0 raises ValueError.""" # Input -1 -> internal 0, which is < 1 with self.assertRaises(ValueError) as context: _calculate_backoff_duration(-1) - self.assertIn("Number of attempts must be at least 1", str(context.exception)) + self.assertIn("Number of attempts must be at least 0", str(context.exception)) with self.assertRaises(ValueError): _calculate_backoff_duration(-2)