From 5fe5fd477ed357480ab287321996cf11e19c9390 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 1 Jul 2026 09:12:25 -0700 Subject: [PATCH 1/5] Updating Provider Mostly docstring fixes and a couple bug fixes. --- .../_azureappconfigurationprovider.py | 17 +++++-- .../_azureappconfigurationproviderbase.py | 32 +++++++----- .../provider/_client_manager.py | 49 ++++++++++-------- .../provider/_client_manager_base.py | 22 ++++---- .../appconfiguration/provider/_constants.py | 14 ++--- .../appconfiguration/provider/_discovery.py | 4 +- .../azure/appconfiguration/provider/_json.py | 2 +- .../provider/_key_vault/_secret_provider.py | 6 +-- .../_key_vault/_secret_provider_base.py | 8 +-- .../azure/appconfiguration/provider/_load.py | 9 ++-- .../appconfiguration/provider/_models.py | 10 ++-- .../provider/_refresh_timer.py | 2 +- .../provider/_request_tracing_context.py | 37 ++++++-------- .../provider/_snapshot_reference_parser.py | 9 ++-- .../azure/appconfiguration/provider/_utils.py | 13 ++--- .../provider/aio/_async_client_manager.py | 51 +++++++++++-------- .../provider/aio/_async_discovery.py | 4 +- .../provider/aio/_async_load.py | 5 +- .../_azureappconfigurationproviderasync.py | 11 +++- .../aio/_key_vault/_async_secret_provider.py | 6 +-- ...test_configuration_async_client_manager.py | 3 ++ ...ation_async_client_manager_load_balance.py | 3 ++ .../test_configuration_client_manager.py | 3 ++ ...nfiguration_client_manager_load_balance.py | 3 ++ .../tests/test_request_tracing_context.py | 20 ++++---- .../tests/test_utils.py | 2 +- 26 files changed, 188 insertions(+), 157 deletions(-) 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..6977edbf2301 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__) @@ -79,7 +78,7 @@ def __init__(self, **kwargs: Any) -> None: self._startup_timeout: int = kwargs.pop("startup_timeout", DEFAULT_STARTUP_TIMEOUT) self._replica_client_manager = ConfigurationClientManager( - connection_string=kwargs.pop("connection_string"), + connection_string=kwargs.pop("connection_string", None), endpoint=kwargs.pop("endpoint"), credential=kwargs.pop("credential", None), user_agent=user_agent, @@ -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..b2e6152ea64f 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -70,11 +70,13 @@ def is_json_content_type(content_type: str) -> bool: def _build_watched_setting(setting: Union[str, Tuple[str, str]]) -> Tuple[str, str]: - try: - key, label = setting # type:ignore - except (IndexError, ValueError): - key = str(setting) # Ensure key is a string - label = NULL_CHAR + if isinstance(setting, str): + key, label = setting, NULL_CHAR + else: + try: + key, label = setting + except (TypeError, ValueError): + key, label = str(setting), NULL_CHAR if "*" in key or "*" in label: raise ValueError("Wildcard key or label filters are not supported for refresh.") return key, label @@ -155,6 +157,7 @@ def _update_ff_telemetry_metadata( if not endpoint.endswith("/"): endpoint += "/" feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" + #breakpoint() if feature_flag.label and not feature_flag.label.isspace(): feature_flag_reference += f"?label={feature_flag.label}" @@ -179,7 +182,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. """ @@ -264,17 +267,20 @@ def __getitem__(self, key: str) -> Any: return self._dict[key] def __iter__(self) -> Iterator[str]: - return self._dict.__iter__() + with self._update_lock: + return self._dict.__iter__() def __len__(self) -> int: - return len(self._dict) + with self._update_lock: + return len(self._dict) def __contains__(self, __x: object) -> bool: # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype """ Returns True if the configuration settings contains the specified key. """ - return self._dict.__contains__(__x) + with self._update_lock: + return self._dict.__contains__(__x) def keys(self) -> KeysView[str]: """ @@ -303,11 +309,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 +446,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..aeafbbfcadeb 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 @@ -106,10 +106,10 @@ def _check_configuration_setting( """ 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 Mapping[str, str] headers: headers to use for the request + :param Optional[str] etag: etag to check for changes + :param Dict[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. :rtype: Tuple[bool, Union[ConfigurationSetting, None]] @@ -132,7 +132,7 @@ def _check_configuration_setting( self.LOGGER.debug("Refresh all triggered by key: %s label %s.", key, label) return True, None else: - raise e + raise return False, None @distributed_trace @@ -290,10 +290,10 @@ def get_updated_watched_settings( Checks if any of the watch keys have changed, and updates them if they have. :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 + :param Dict[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 if not found + :rtype: Optional[ConfigurationSetting] """ return self._client.get_configuration_setting(key=key, label=label, **kwargs) @@ -343,7 +343,7 @@ def _validate_snapshot(self, snapshot_name: str) -> bool: if e.status_code == 404: self.LOGGER.warning("Snapshot '%s' not found when resolving snapshot.", snapshot_name) return False - raise e + raise if snapshot.composition_type != SnapshotComposition.KEY: raise ValueError(f"Composition type for '{snapshot_name}' must be 'key'.") return True @@ -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 = "" @@ -483,10 +484,12 @@ def refresh_clients(self): if self._next_update_time and self._next_update_time > time.time(): return - failover_endpoints = find_auto_failover_endpoints(self._original_endpoint, self._replica_discovery_enabled) - - if failover_endpoints is None: - # SRV record not found, so we should refresh after a longer interval + try: + failover_endpoints = find_auto_failover_endpoints( + self._original_endpoint, self._replica_discovery_enabled + ) + except TimeoutError: + # SRV record resolution timed out, so we should refresh after a longer interval self._next_update_time = time.time() + FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL return @@ -530,6 +533,12 @@ def refresh_clients(self): ) ) self._next_update_time = time.time() + MINIMAL_CLIENT_REFRESH_INTERVAL + # Close any replica clients that are no longer part of the failover. + retained_endpoints = {self._original_client.endpoint} + retained_endpoints.update(client.endpoint for client in discovered_clients) + for client in self._replica_clients: + if client.endpoint not in retained_endpoints: + client.close() if not self._load_balancing_enabled: random.shuffle(discovered_clients) self._replica_clients = [self._original_client] + discovered_clients 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..0941b86b3dc7 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,14 +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: endpoint: str @@ -23,12 +20,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 +38,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,7 +53,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: + if calculated_milliseconds > max_backoff_milliseconds: calculated_milliseconds = max_backoff_milliseconds return min_backoff_milliseconds + ( 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/_discovery.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_discovery.py index 6dbfe722a964..5f960a1b4e08 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_discovery.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_discovery.py @@ -40,8 +40,8 @@ def find_auto_failover_endpoints(endpoint: str, replica_discovery_enabled: bool) replicas = _find_replicas(origin.target) - if not replicas: - return None # Timeout + if replicas is None: + raise TimeoutError("Timed out while resolving auto-failover replica endpoints.") srv_records = [origin] + replicas endpoints = [] 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..2b6b53da8494 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): @@ -72,7 +70,7 @@ def __get_secret_value(self, key: str, secret_identifier: KeyVaultSecretIdentifi def close(self) -> None: """ - Closes the connection to Azure App Configuration. + Closes the connection to Azure Key Vault. """ for client in self._secret_clients.values(): client.close() 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..81aefc347a4c 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,10 +13,6 @@ from azure.keyvault.secrets import KeyVaultSecretIdentifier from .._azureappconfigurationproviderbase import _RefreshTimer -JSON = Mapping[str, Any] -_T = TypeVar("_T") - - class _SecretProviderBase: def __init__(self, **kwargs: Any) -> None: @@ -31,7 +25,7 @@ def __init__(self, **kwargs: Any) -> None: ) if kwargs.get("secret_refresh_interval", 60) < 1: - raise ValueError("Secret refresh interval must be greater than 1 second.") + raise ValueError("Secret refresh interval must be at least 1 second.") self.secret_refresh_timer: Optional[_RefreshTimer] = ( _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) 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..04c4799ed07e 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,7 +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: + if calculated_milliseconds > max_backoff_milliseconds: calculated_milliseconds = max_backoff_milliseconds return min_backoff_milliseconds + ( 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..bc4f0d77aba2 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. @@ -117,7 +113,7 @@ def process_load_parameters(*args, **kwargs: Any) -> Dict[str, Any]: # Get startup timeout startup_timeout = kwargs.pop("startup_timeout", DEFAULT_STARTUP_TIMEOUT) if startup_timeout < 0: - raise ValueError("Startup timeout must be greater than or equal to 0 seconds.") + raise ValueError("Startup timeout must be at least 1 second.") return { "endpoint": endpoint, @@ -194,18 +190,17 @@ 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 + if calculated > MAX_STARTUP_BACKOFF_DURATION: 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..758caf4177fd 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 @@ -108,10 +108,10 @@ async def _check_configuration_setting( """ 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 Mapping[str, str] headers: headers to use for the request + :param Optional[str] etag: etag to check for changes + :param Dict[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. :rtype: Tuple[bool, Union[ConfigurationSetting, None]] @@ -134,7 +134,7 @@ async def _check_configuration_setting( self.LOGGER.debug("Refresh all triggered by key: %s label %s.", key, label) return True, None else: - raise e + raise return False, None @distributed_trace @@ -292,10 +292,10 @@ async def get_updated_watched_settings( Checks if any of the watch keys have changed, and updates them if they have. :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 + :param Dict[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 if not found + :rtype: Optional[ConfigurationSetting] """ return await self._client.get_configuration_setting(key=key, label=label, **kwargs) @@ -345,7 +345,7 @@ async def _validate_snapshot(self, snapshot_name: str) -> bool: if e.status_code == 404: self.LOGGER.warning("Snapshot '%s' not found when resolving snapshot.", snapshot_name) return False - raise e + raise if snapshot.composition_type != SnapshotComposition.KEY: raise ValueError(f"Composition type for '{snapshot_name}' must be 'key'.") return True @@ -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 = "" @@ -485,12 +486,12 @@ async def refresh_clients(self): if self._next_update_time and self._next_update_time > time.time(): return - failover_endpoints = await find_auto_failover_endpoints( - self._original_endpoint, self._replica_discovery_enabled - ) - - if failover_endpoints is None: - # SRV record not found, so we should refresh after a longer interval + try: + failover_endpoints = await find_auto_failover_endpoints( + self._original_endpoint, self._replica_discovery_enabled + ) + except TimeoutError: + # SRV record resolution timed out, so we should refresh after a longer interval self._next_update_time = time.time() + FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL return @@ -534,6 +535,12 @@ async def refresh_clients(self): ) ) self._next_update_time = time.time() + MINIMAL_CLIENT_REFRESH_INTERVAL + # Close any replica clients that are no longer part of the failover. + retained_endpoints = {self._original_client.endpoint} + retained_endpoints.update(client.endpoint for client in discovered_clients) + for client in self._replica_clients: + if client.endpoint not in retained_endpoints: + await client.close() if not self._load_balancing_enabled: random.shuffle(discovered_clients) self._replica_clients = [self._original_client] + discovered_clients diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_discovery.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_discovery.py index 1713c0d0d833..ecc26d0a7e0e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_discovery.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_discovery.py @@ -25,8 +25,8 @@ async def find_auto_failover_endpoints(endpoint: str, replica_discovery_enabled: replicas = await _find_replicas(origin.target) - if not replicas: - return None # Timeout + if replicas is None: + raise TimeoutError("Timed out while resolving auto-failover replica endpoints.") srv_records = [origin] + replicas endpoints = [] 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..5f32d73b778e 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): @@ -78,7 +76,7 @@ async def __get_secret_value(self, key: str, secret_identifier: KeyVaultSecretId async def close(self) -> None: """ - Closes the connection to Azure App Configuration. + Closes the connection to Azure Key Vault. """ for client in self._secret_clients.values(): await client.close() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager.py index e8c2c66dc2d9..956e986637f0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager.py @@ -27,6 +27,9 @@ def __init__(self, endpoint, connection_string, credential, retry_total, retry_b self.retry_total = retry_total self.retry_backoff = retry_backoff + async def close(self): + pass + @pytest.mark.usefixtures("caplog") class TestAsyncConfigurationClientManager: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager_load_balance.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager_load_balance.py index f252798367b1..7d5ac234453a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager_load_balance.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager_load_balance.py @@ -24,6 +24,9 @@ def __init__(self, endpoint, connection_string, credential, retry_total, retry_b self.retry_total = retry_total self.retry_backoff = retry_backoff + async def close(self): + pass + @pytest.mark.usefixtures("caplog") class TestConfigurationAsyncClientManagerLoadBalance: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager.py index 5edaab158bf4..d2123f6f4f3a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager.py @@ -26,6 +26,9 @@ def __init__(self, endpoint, connection_string, credential, retry_total, retry_b self.retry_total = retry_total self.retry_backoff = retry_backoff + def close(self): + pass + @pytest.mark.usefixtures("caplog") class TestConfigurationClientManager(unittest.TestCase): diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager_load_balance.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager_load_balance.py index f73942364e19..3d0fca436379 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager_load_balance.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager_load_balance.py @@ -25,6 +25,9 @@ def __init__(self, endpoint, connection_string, credential, retry_total, retry_b self.retry_total = retry_total self.retry_backoff = retry_backoff + def close(self): + pass + @pytest.mark.usefixtures("caplog") class TestConfigurationClientManagerLoadBalance(unittest.TestCase): 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..4b1bbb90f8d5 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_utils.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_utils.py @@ -187,7 +187,7 @@ def test_invalid_attempts_raises_error(self): # 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) From ab4fc7af839afbe171fe7bfdd45b5002ae7dc560 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 1 Jul 2026 13:51:17 -0700 Subject: [PATCH 2/5] fixed formatting --- .../provider/_azureappconfigurationproviderbase.py | 1 - .../azure/appconfiguration/provider/_client_manager.py | 4 +--- .../azure/appconfiguration/provider/_client_manager_base.py | 1 + .../provider/_key_vault/_secret_provider_base.py | 1 + 4 files changed, 3 insertions(+), 4 deletions(-) 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 b2e6152ea64f..a97a7081d4e3 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -157,7 +157,6 @@ def _update_ff_telemetry_metadata( if not endpoint.endswith("/"): endpoint += "/" feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" - #breakpoint() if feature_flag.label and not feature_flag.label.isspace(): feature_flag_reference += f"?label={feature_flag.label}" 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 aeafbbfcadeb..ec31ccd368eb 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 @@ -485,9 +485,7 @@ def refresh_clients(self): return try: - failover_endpoints = find_auto_failover_endpoints( - self._original_endpoint, self._replica_discovery_enabled - ) + failover_endpoints = find_auto_failover_endpoints(self._original_endpoint, self._replica_discovery_enabled) except TimeoutError: # SRV record resolution timed out, so we should refresh after a longer interval self._next_update_time = time.time() + FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL 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 0941b86b3dc7..d9a3a18a6aa2 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 @@ -10,6 +10,7 @@ FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL = 3600 # 1 hour in seconds MINIMAL_CLIENT_REFRESH_INTERVAL = 30 # 30 seconds + @dataclass class _ConfigurationClientWrapperBase: endpoint: str 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 81aefc347a4c..a817621cde56 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 @@ -13,6 +13,7 @@ from azure.keyvault.secrets import KeyVaultSecretIdentifier from .._azureappconfigurationproviderbase import _RefreshTimer + class _SecretProviderBase: def __init__(self, **kwargs: Any) -> None: From 26ea971a3d686e06c87673c06c67e6ef4ca828ab Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 1 Jul 2026 13:55:29 -0700 Subject: [PATCH 3/5] pylint updates --- .github/skills/code-review/SKILL.md | 44 ++++++++++++ .github/skills/local-dev-environment/SKILL.md | 71 +++++++++++++++++++ .../provider/_client_manager_base.py | 3 +- .../provider/_refresh_timer.py | 3 +- .../azure/appconfiguration/provider/_utils.py | 3 +- 5 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 .github/skills/code-review/SKILL.md create mode 100644 .github/skills/local-dev-environment/SKILL.md diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md new file mode 100644 index 000000000000..cf8072a77773 --- /dev/null +++ b/.github/skills/code-review/SKILL.md @@ -0,0 +1,44 @@ +--- +name: code-review +description: "Guidelines for reviewing pull requests to the App Configuration packages in this repository (azure-appconfiguration and azure-appconfiguration-provider). USE FOR: reviewing a PR, what to check in a code review, repository rules to enforce during review, giving review feedback, responding to review comments, PR best practices. DO NOT USE FOR: automated code analysis, running linters, creating PRs." +--- + +# Code Review + +How to review a pull request against the App Configuration code in this repository: what to check in the diff, the repository rules to enforce, and how to handle the review conversation. The reviewing guidance is primary; the authoring and feedback sections support both sides of the review. + +**Scope:** the data-plane client library `sdk/appconfiguration/azure-appconfiguration/` and the provider library `sdk/appconfiguration/azure-appconfiguration-provider/`. The management library `azure-mgmt-appconfiguration` follows the separate MGMT review rules. + +## Reviewing a PR + +Work through this when reviewing a diff: + +- **Repository rules are honored** — check the change against every rule in [Repository rules to enforce](#repository-rules-to-enforce) below. +- **Behavior changes are covered by tests** — including the negative/disabled code path, in both sync and async forms. +- **Public API changes are intentional** — new or modified public surface must be reflected in `api.md` and reviewed for breaking changes. +- **Logging isn't noisy** — flag warnings emitted per-call/per-request; prefer `info`/`debug` for non-actionable conditions. +- **Scope is focused** — the PR addresses a single concern; flag unrelated changes for a separate PR. +- **Treat every comment as worth addressing** — nothing is a nit. + +## Repository rules to enforce + +Hard constraints for the App Configuration packages. A change that violates any of these should not pass review. + +- **CHANGELOG is required** — every user-facing change must add an entry under the top `## (Unreleased)` section of the package's `CHANGELOG.md` (Features Added / Breaking Changes / Bugs Fixed / Other Changes). The version in `_version.py` must match the latest version in `CHANGELOG.md`. This is a release-blocking check. +- **Don't hand-edit generated code** — `azure/appconfiguration/_generated/` is produced from TypeSpec (see `tsp-location.yaml`). Changes belong in the TypeSpec source or the hand-written customization layer (the non-`_generated` modules), then regenerate. Flag direct edits to `_generated/`. See the `find-package-skill` and `azsdk-common-generate-sdk-locally` skills. +- **No new dependencies** — minimize them; if one is truly required it must be justified and, where possible, an *optional* dependency. New runtime dependencies require design review. +- **No breaking changes to public API** outside a major version — and even within a major version, minimize them. The breaking-changes check runs in CI; a flagged break needs explicit justification and design review. +- **Prefer public APIs over private modules** — avoid importing from underscore-prefixed/private paths when a public API exists. +- **Sync/async parity** — changes under `azure/appconfiguration/` (or `azure/appconfiguration/provider/`) that have an `aio/` counterpart must be updated in both the sync and async code paths, with matching tests. +- **New features ship with samples and tests** — user-facing features require a `samples/` example and sync + async unit tests under `tests/`. +- **Validation must pass** — run via `azpysdk` from the package directory: black → pylint → mypy → pyright → sphinx → pytest. MyPy, Pylint, Sphinx, and Tests-CI are release-blocking. See the `fix-black`, `fix-pylint`, `fix-mypy`, and `fix-sphinx` skills. +- **New source files** — must include the MIT copyright header and a module docstring. + +## Authoring a PR (so it reviews well) + +- **One issue, one PR** — keep each pull request focused on a single concern; don't mix fixes for multiple issues. +- **Descriptive title** — meaningful in source-control history without extra context. +- **Self-explanatory description** — explain what changed and *why*, link the issue the PR closes, and call out any CHANGELOG entry. +- **Test before requesting review** — run `azpysdk` validation locally and verify your changes work first. +- **Follow existing code style** — consistency matters more than personal preference. +- **Self-review sizable changes** — catch obvious issues before reviewers do. diff --git a/.github/skills/local-dev-environment/SKILL.md b/.github/skills/local-dev-environment/SKILL.md new file mode 100644 index 000000000000..3df53fda195c --- /dev/null +++ b/.github/skills/local-dev-environment/SKILL.md @@ -0,0 +1,71 @@ +--- +name: local-dev-environment +description: Describes the local development environment for THIS repo (azure-sdk-for-python) — the repo-scoped Python virtual environment at .venv and the environment variables defined in .env. USE WHEN: you need to run azpysdk/pylint/mypy/pytest/black/sphinx, activate the virtual environment, locate the Python interpreter, or reference local test/auth environment variables for the appconfiguration packages. Scoped to this repository only; other repos have their own .venv. +--- + +# Local Development Environment (azure-sdk-for-python) + +This skill documents the local dev environment that lives in **this** workspace so you +don't need to be told about it each time. It is repo-scoped — this `.venv` and `.env` +belong only to `c:\Users\mametcal\Projects\SDKs\azure-sdk-for-python`. + +## Virtual Environment (`.venv`) + +- **Location:** `.venv/` at the repo root. +- **Python version:** 3.14.3 (base interpreter: `C:\Users\mametcal\AppData\Local\Programs\Python\Python314`). +- **Interpreter path:** `.venv\Scripts\python.exe` +- **Activate (PowerShell):** `.\.venv\Scripts\Activate.ps1` +- **Activate (cmd):** `.\.venv\Scripts\activate.bat` + +### Key tools already installed on PATH (in `.venv\Scripts`) +- `azpysdk.exe` — primary test/validation runner (e.g. `azpysdk pylint .`, `azpysdk mypy .`, `azpysdk black .`) +- `pylint.exe`, `mypy.exe` / `dmypy.exe`, `black.exe`, `pytest.exe`, `sphinx-build.exe`, `isort.exe` +- SDK engineering tools: `sdk_build.exe`, `sdk_changelog.exe`, `sdk_set_version.exe`, `generate_sdk.exe`, `apistubgen.exe`, `dotenv.exe`, and related `sdk_*` / `generate_*` utilities. + +### Per-package check environments +`azpysdk` creates isolated per-check virtual environments under `.venv\\`. +Currently present: +- `.venv\azure-appconfiguration\` — `.venv_apistub`, `.venv_black`, `.venv_mindependency`, `.venv_mypy`, `.venv_pylint`, `.venv_sphinx`, `.venv_update_snippet` +- `.venv\azure-appconfiguration-provider\` + +You normally do not invoke these directly; `azpysdk` manages them. + +## Environment Variables (`.env`) + +A repo-root `.env` file provides test/auth configuration, primarily for the +**appconfiguration** packages. Load it with `dotenv` or rely on the test harness. + +### Active settings +- **Test mode / auth** + - `AZURE_TEST_RUN_LIVE=true` — tests run in **live** mode (not playback). + - `AZURE_TEST_USE_CLI_AUTH=true` — authenticate via Azure CLI (`az login`) credentials. + - `PYLINTRC=./pylintrc` +- **App Configuration endpoints** (staging) + - `APPCONFIGURATION_ENDPOINT` / `APPCONFIGURATION_ENDPOINT_STRING` → `https://java-sdk-feature-flag-endpoint.appconfig-staging.azure.com` +- **Key Vault references** (placeholder/fake secret URLs under `keyvault-theclassics.vault.azure.net`): + `APPCONFIGURATION_KEYVAULT_SECRET_URL`, `APPCONFIGURATION_KEYVAULT_SECRET_URL2`, + `KEYVAULT_URL`, `APPCONFIGURATION_KEY_VAULT_REFERENCE`, `APPCONFIGURATION_KEY_VAULT_REFERENCE2`, + `KEYVAULT_SECRET_URL` +- **ARM / subscription context** + - `APPCONFIGURATION_SUBSCRIPTION_ID`, `APPCONFIGURATION_RESOURCE_GROUP=rg-mametcalappconfiguration` + - `APPCONFIGURATION_LOCATION=eastus`, `APPCONFIGURATION_ENVIRONMENT=AzureCloud` + - `APPCONFIGURATION_RESOURCE_MANAGER_URL=https://management.azure.com/` + - `APPCONFIGURATION_SERVICE_MANAGEMENT_URL=https://management.core.windows.net/` + - `APPCONFIGURATION_AZURE_AUTHORITY_HOST=https://login.microsoftonline.com` + - `AZURE_SERVICE_DIRECTORY=APPCONFIGURATION` + +### Commented-out / alternate profiles (inactive) +The `.env` also keeps several disabled blocks for switching contexts: a Flask sample +endpoint, old test endpoints with connection strings, ARM template deployment values, +and a service-principal auth block for "Bleu Cloud" (`AZURE_TENANT_ID`, +`AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_AUTHORITY_HOST`). These are commented +out; uncomment only when intentionally switching environments. + +## Notes & cautions +- **Secrets:** `.env` may contain connection strings, secrets, and client secrets. Never + print, echo, or commit these values. Reference variable names, not their contents. +- The active configuration targets the **App Configuration staging** environment with + **live** test runs using **Azure CLI** auth — make sure `az login` is current before + running live tests. +- This environment is specific to this repo; do not assume the same `.venv` path or + `.env` values apply in other workspaces. 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 d9a3a18a6aa2..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 @@ -54,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: - 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/_refresh_timer.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_refresh_timer.py index 04c4799ed07e..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: - 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/_utils.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py index bc4f0d77aba2..33f209ae605b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py @@ -200,7 +200,6 @@ def _calculate_backoff_duration(attempts: int) -> float: calculated = MIN_STARTUP_EXPONENTIAL_BACKOFF_DURATION * (1 << safe_shift) # Cap at max duration - if calculated > MAX_STARTUP_BACKOFF_DURATION: - calculated = MAX_STARTUP_BACKOFF_DURATION + calculated = min(calculated, MAX_STARTUP_BACKOFF_DURATION) return _jitter(calculated, JITTER_RATIO) From 1bbca38007f1dd4e05a64b14c880a5d7731e21c5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 1 Jul 2026 14:12:12 -0700 Subject: [PATCH 4/5] undo skills --- .github/skills/code-review/SKILL.md | 44 ------------ .github/skills/local-dev-environment/SKILL.md | 71 ------------------- 2 files changed, 115 deletions(-) delete mode 100644 .github/skills/code-review/SKILL.md delete mode 100644 .github/skills/local-dev-environment/SKILL.md diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md deleted file mode 100644 index cf8072a77773..000000000000 --- a/.github/skills/code-review/SKILL.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: code-review -description: "Guidelines for reviewing pull requests to the App Configuration packages in this repository (azure-appconfiguration and azure-appconfiguration-provider). USE FOR: reviewing a PR, what to check in a code review, repository rules to enforce during review, giving review feedback, responding to review comments, PR best practices. DO NOT USE FOR: automated code analysis, running linters, creating PRs." ---- - -# Code Review - -How to review a pull request against the App Configuration code in this repository: what to check in the diff, the repository rules to enforce, and how to handle the review conversation. The reviewing guidance is primary; the authoring and feedback sections support both sides of the review. - -**Scope:** the data-plane client library `sdk/appconfiguration/azure-appconfiguration/` and the provider library `sdk/appconfiguration/azure-appconfiguration-provider/`. The management library `azure-mgmt-appconfiguration` follows the separate MGMT review rules. - -## Reviewing a PR - -Work through this when reviewing a diff: - -- **Repository rules are honored** — check the change against every rule in [Repository rules to enforce](#repository-rules-to-enforce) below. -- **Behavior changes are covered by tests** — including the negative/disabled code path, in both sync and async forms. -- **Public API changes are intentional** — new or modified public surface must be reflected in `api.md` and reviewed for breaking changes. -- **Logging isn't noisy** — flag warnings emitted per-call/per-request; prefer `info`/`debug` for non-actionable conditions. -- **Scope is focused** — the PR addresses a single concern; flag unrelated changes for a separate PR. -- **Treat every comment as worth addressing** — nothing is a nit. - -## Repository rules to enforce - -Hard constraints for the App Configuration packages. A change that violates any of these should not pass review. - -- **CHANGELOG is required** — every user-facing change must add an entry under the top `## (Unreleased)` section of the package's `CHANGELOG.md` (Features Added / Breaking Changes / Bugs Fixed / Other Changes). The version in `_version.py` must match the latest version in `CHANGELOG.md`. This is a release-blocking check. -- **Don't hand-edit generated code** — `azure/appconfiguration/_generated/` is produced from TypeSpec (see `tsp-location.yaml`). Changes belong in the TypeSpec source or the hand-written customization layer (the non-`_generated` modules), then regenerate. Flag direct edits to `_generated/`. See the `find-package-skill` and `azsdk-common-generate-sdk-locally` skills. -- **No new dependencies** — minimize them; if one is truly required it must be justified and, where possible, an *optional* dependency. New runtime dependencies require design review. -- **No breaking changes to public API** outside a major version — and even within a major version, minimize them. The breaking-changes check runs in CI; a flagged break needs explicit justification and design review. -- **Prefer public APIs over private modules** — avoid importing from underscore-prefixed/private paths when a public API exists. -- **Sync/async parity** — changes under `azure/appconfiguration/` (or `azure/appconfiguration/provider/`) that have an `aio/` counterpart must be updated in both the sync and async code paths, with matching tests. -- **New features ship with samples and tests** — user-facing features require a `samples/` example and sync + async unit tests under `tests/`. -- **Validation must pass** — run via `azpysdk` from the package directory: black → pylint → mypy → pyright → sphinx → pytest. MyPy, Pylint, Sphinx, and Tests-CI are release-blocking. See the `fix-black`, `fix-pylint`, `fix-mypy`, and `fix-sphinx` skills. -- **New source files** — must include the MIT copyright header and a module docstring. - -## Authoring a PR (so it reviews well) - -- **One issue, one PR** — keep each pull request focused on a single concern; don't mix fixes for multiple issues. -- **Descriptive title** — meaningful in source-control history without extra context. -- **Self-explanatory description** — explain what changed and *why*, link the issue the PR closes, and call out any CHANGELOG entry. -- **Test before requesting review** — run `azpysdk` validation locally and verify your changes work first. -- **Follow existing code style** — consistency matters more than personal preference. -- **Self-review sizable changes** — catch obvious issues before reviewers do. diff --git a/.github/skills/local-dev-environment/SKILL.md b/.github/skills/local-dev-environment/SKILL.md deleted file mode 100644 index 3df53fda195c..000000000000 --- a/.github/skills/local-dev-environment/SKILL.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -name: local-dev-environment -description: Describes the local development environment for THIS repo (azure-sdk-for-python) — the repo-scoped Python virtual environment at .venv and the environment variables defined in .env. USE WHEN: you need to run azpysdk/pylint/mypy/pytest/black/sphinx, activate the virtual environment, locate the Python interpreter, or reference local test/auth environment variables for the appconfiguration packages. Scoped to this repository only; other repos have their own .venv. ---- - -# Local Development Environment (azure-sdk-for-python) - -This skill documents the local dev environment that lives in **this** workspace so you -don't need to be told about it each time. It is repo-scoped — this `.venv` and `.env` -belong only to `c:\Users\mametcal\Projects\SDKs\azure-sdk-for-python`. - -## Virtual Environment (`.venv`) - -- **Location:** `.venv/` at the repo root. -- **Python version:** 3.14.3 (base interpreter: `C:\Users\mametcal\AppData\Local\Programs\Python\Python314`). -- **Interpreter path:** `.venv\Scripts\python.exe` -- **Activate (PowerShell):** `.\.venv\Scripts\Activate.ps1` -- **Activate (cmd):** `.\.venv\Scripts\activate.bat` - -### Key tools already installed on PATH (in `.venv\Scripts`) -- `azpysdk.exe` — primary test/validation runner (e.g. `azpysdk pylint .`, `azpysdk mypy .`, `azpysdk black .`) -- `pylint.exe`, `mypy.exe` / `dmypy.exe`, `black.exe`, `pytest.exe`, `sphinx-build.exe`, `isort.exe` -- SDK engineering tools: `sdk_build.exe`, `sdk_changelog.exe`, `sdk_set_version.exe`, `generate_sdk.exe`, `apistubgen.exe`, `dotenv.exe`, and related `sdk_*` / `generate_*` utilities. - -### Per-package check environments -`azpysdk` creates isolated per-check virtual environments under `.venv\\`. -Currently present: -- `.venv\azure-appconfiguration\` — `.venv_apistub`, `.venv_black`, `.venv_mindependency`, `.venv_mypy`, `.venv_pylint`, `.venv_sphinx`, `.venv_update_snippet` -- `.venv\azure-appconfiguration-provider\` - -You normally do not invoke these directly; `azpysdk` manages them. - -## Environment Variables (`.env`) - -A repo-root `.env` file provides test/auth configuration, primarily for the -**appconfiguration** packages. Load it with `dotenv` or rely on the test harness. - -### Active settings -- **Test mode / auth** - - `AZURE_TEST_RUN_LIVE=true` — tests run in **live** mode (not playback). - - `AZURE_TEST_USE_CLI_AUTH=true` — authenticate via Azure CLI (`az login`) credentials. - - `PYLINTRC=./pylintrc` -- **App Configuration endpoints** (staging) - - `APPCONFIGURATION_ENDPOINT` / `APPCONFIGURATION_ENDPOINT_STRING` → `https://java-sdk-feature-flag-endpoint.appconfig-staging.azure.com` -- **Key Vault references** (placeholder/fake secret URLs under `keyvault-theclassics.vault.azure.net`): - `APPCONFIGURATION_KEYVAULT_SECRET_URL`, `APPCONFIGURATION_KEYVAULT_SECRET_URL2`, - `KEYVAULT_URL`, `APPCONFIGURATION_KEY_VAULT_REFERENCE`, `APPCONFIGURATION_KEY_VAULT_REFERENCE2`, - `KEYVAULT_SECRET_URL` -- **ARM / subscription context** - - `APPCONFIGURATION_SUBSCRIPTION_ID`, `APPCONFIGURATION_RESOURCE_GROUP=rg-mametcalappconfiguration` - - `APPCONFIGURATION_LOCATION=eastus`, `APPCONFIGURATION_ENVIRONMENT=AzureCloud` - - `APPCONFIGURATION_RESOURCE_MANAGER_URL=https://management.azure.com/` - - `APPCONFIGURATION_SERVICE_MANAGEMENT_URL=https://management.core.windows.net/` - - `APPCONFIGURATION_AZURE_AUTHORITY_HOST=https://login.microsoftonline.com` - - `AZURE_SERVICE_DIRECTORY=APPCONFIGURATION` - -### Commented-out / alternate profiles (inactive) -The `.env` also keeps several disabled blocks for switching contexts: a Flask sample -endpoint, old test endpoints with connection strings, ARM template deployment values, -and a service-principal auth block for "Bleu Cloud" (`AZURE_TENANT_ID`, -`AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_AUTHORITY_HOST`). These are commented -out; uncomment only when intentionally switching environments. - -## Notes & cautions -- **Secrets:** `.env` may contain connection strings, secrets, and client secrets. Never - print, echo, or commit these values. Reference variable names, not their contents. -- The active configuration targets the **App Configuration staging** environment with - **live** test runs using **Azure CLI** auth — make sure `az login` is current before - running live tests. -- This environment is specific to this repo; do not assume the same `.venv` path or - `.env` values apply in other workspaces. From c09e52cd44844cf62e79c6b73d363dad35aca59f Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 1 Jul 2026 14:20:17 -0700 Subject: [PATCH 5/5] Update CHANGELOG.md --- .../azure-appconfiguration-provider/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md index 54481723b4d3..74d1ec6ae129 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md @@ -8,6 +8,9 @@ ### Bugs Fixed +- Fixed a resource leak where replica clients that were no longer part of the auto-failover set were not closed during client refresh. +- Fixed auto-failover replica discovery so that a DNS SRV lookup timeout is distinguished from an empty replica list. A timeout now correctly triggers the longer fallback refresh interval, while an empty result refreshes at the normal interval. + ### Other Changes - Bumped minimum dependency on `azure-core` to `>=1.31.0`.