Skip to content
Open
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
# Release History

## 2.5.1 (Unreleased)
## 2.6.0b1 (Unreleased)

### Features Added

- Feature flags created via the dedicated feature flag resource endpoint (`FeatureFlagClient`/`FeatureFlag` in `azure-appconfiguration`) are now loaded automatically alongside key-value based feature flags whenever `feature_flag_enabled=True`. Both kinds are merged into the same `feature_management.feature_flags` list, with resource-based feature flags taking precedence over key-value based ones when they share the same name. No new `load()` options are required to opt in, and existing `feature_flag_selectors` filter both kinds.

### Breaking Changes

### Bugs Fixed

### Other Changes

- Bumped minimum dependency on `azure-core` to `>=1.31.0`.
- Bumped minimum dependency on `azure-appconfiguration` to `>=1.10.0b1` for `FeatureFlagClient`/`FeatureFlag` support.

## 2.5.0 (2026-05-22)

Expand Down
72 changes: 72 additions & 0 deletions sdk/appconfiguration/azure-appconfiguration-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,72 @@ config = load(

<!-- END SNIPPET -->

### Loading Enhanced Feature Flags

Feature flags can also be created using the dedicated feature flag endpoint (via `FeatureFlagClient`/`FeatureFlag` in `azure-appconfiguration`), instead of as key-value configuration settings. These are referred to as enhanced feature flags. No additional `load()` options are required to load them — it happens automatically whenever `feature_flag_enabled=True`, and they are merged into the same `feature_management.feature_flags` list as key-value based feature flags, with enhanced feature flags taking precedence when both share the same name.

<!-- SNIPPET:enhanced_feature_flag_sample.enhanced_feature_flag_loading -->

```python
from azure.appconfiguration.provider import load

# Feature flags loaded from the enhanced feature flag endpoint are merged into the same
# feature_management.feature_flags list as key-value based feature flags.
config = load(endpoint=endpoint, credential=credential, feature_flag_enabled=True, **kwargs)
feature_flags = config["feature_management"]["feature_flags"]
enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("name") == "EnhancedFeatureBeta")
print(enhanced_flag_beta["enabled"])
```

<!-- END SNIPPET -->

`FeatureFlagSelector` is the dedicated selector type for filtering enhanced feature flags by name, label, or tags, and is the recommended way to select enhanced feature flags.

<!-- SNIPPET:enhanced_feature_flag_sample.enhanced_feature_flag_selector_with_feature_flag_selector -->

```python
from azure.appconfiguration.provider import load, FeatureFlagSelector

# FeatureFlagSelector is the dedicated selector type for filtering enhanced feature flags.
config = load(
endpoint=endpoint,
credential=credential,
feature_flag_enabled=True,
feature_flag_selectors=[FeatureFlagSelector(name_filter="Enhanced*")],
**kwargs,
)
feature_flags = config["feature_management"]["feature_flags"]
enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("id") == "EnhancedFeatureBeta")
print(enhanced_flag_beta["enabled"])
```

<!-- END SNIPPET -->

The same `SettingSelector` used to filter key-value based feature flags also filters enhanced feature flags, by name, label, or tags. A list of `feature_flag_selectors` must contain either `SettingSelector` or `FeatureFlagSelector` instances, but not both. Note that selectors with a `snapshot_name` are not currently supported for enhanced feature flags and are skipped when loading them.

<!-- SNIPPET:enhanced_feature_flag_sample.enhanced_feature_flag_selector -->

```python
from azure.appconfiguration.provider import load, SettingSelector

# The same SettingSelector used to filter key-value based feature flags also filters enhanced feature
# flags, by name/label/tags.
config = load(
endpoint=endpoint,
credential=credential,
feature_flag_enabled=True,
feature_flag_selectors=[SettingSelector(key_filter="Enhanced*")],
**kwargs,
)
feature_flags = config["feature_management"]["feature_flags"]
enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("name") == "EnhancedFeatureBeta")
print(enhanced_flag_beta["enabled"])
```

<!-- END SNIPPET -->

Existing customers using key-value based feature flags do not need to make any code changes to benefit from this feature. If enhanced feature flags are created in the same App Configuration store, the provider will automatically load and merge them alongside the existing key-value based feature flags whenever `feature_flag_enabled=True`.

## JSON Content Type

Configuration settings with a JSON content type (e.g., `application/json`) are automatically deserialized into their corresponding Python objects when loaded by the provider.
Expand Down Expand Up @@ -469,6 +535,12 @@ This library uses the standard [logging](https://docs.python.org/3/library/loggi
* **Configuration not refreshing** — Make sure you are calling `config.refresh()` periodically (e.g., before each request in a web app). The provider does not auto-refresh in the background.
* **Startup failures** — If the store is unreachable during startup, the provider will retry until `startup_timeout` (default 100 seconds) is exceeded. Increase this value if your store is expected to have high latency.

## Testing

(This content is for `azure-appconfiguration-provider` package developer only)

See [tests/README.md](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration-provider/tests/README.md) for instructions on running unit and integration tests, working with recordings, and setting up environment variables for local testing.

## Next steps

Check out our Django and Flask examples to see how to use the provider in a web application.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "python",
"TagPrefix": "python/appconfiguration/azure-appconfiguration-provider",
"Tag": "python/appconfiguration/azure-appconfiguration-provider_34a63910b7"
"Tag": "python/appconfiguration/azure-appconfiguration-provider_fb87386e95"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ._azureappconfigurationprovider import AzureAppConfigurationProvider
from ._models import (
AzureAppConfigurationKeyVaultOptions,
FeatureFlagSelector,
SettingSelector,
WatchKey,
)
Expand All @@ -18,6 +19,7 @@
"load",
"AzureAppConfigurationProvider",
"AzureAppConfigurationKeyVaultOptions",
"FeatureFlagSelector",
Comment thread
yuanqu72 marked this conversation as resolved.
"SettingSelector",
"WatchKey",
]
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)
from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module
ConfigurationSetting,
FeatureFlag,
FeatureFlagConfigurationSetting,
SecretReferenceConfigurationSetting,
)
Expand Down Expand Up @@ -107,6 +108,7 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f
)
configuration_settings: List[ConfigurationSetting] = []
feature_flags: Optional[List[FeatureFlagConfigurationSetting]] = None
enhanced_feature_flags: Optional[List[FeatureFlag]] = None

# Timer needs to be reset even if no refresh happened if time had passed
configuration_refresh_attempted = False
Expand All @@ -115,6 +117,7 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f
existing_feature_flag_usage = self._tracing_context.feature_filter_usage.copy()
page_etags: List[List[str]] = []
feature_flag_page_etags: List[List[str]] = []
enhanced_feature_flag_etags: List[List[str]] = []
try:
if self._refresh_enabled and not self._watched_settings and self._refresh_timer.needs_refresh():
configuration_refresh_attempted = True
Expand Down Expand Up @@ -148,6 +151,16 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f
self._feature_flag_selectors, headers=headers, **kwargs
)

# Enhanced feature flags are loaded independently of the key-value based feature flags, using their
# own page-level etag state, since they are a separate resource type with a separate
# change-detection mechanism.
if not self._enhanced_feature_flag_etags or client.check_enhanced_feature_flag_etags(
self._enhanced_feature_flag_selectors, self._enhanced_feature_flag_etags, headers=headers, **kwargs
):
enhanced_feature_flags, enhanced_feature_flag_etags = client.load_enhanced_feature_flags(
self._enhanced_feature_flag_selectors, headers=headers, **kwargs
)

# Default to existing settings if no refresh occurred
processed_settings = self._dict

Expand All @@ -157,20 +170,24 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f
# Configuration Settings have been refreshed
processed_settings = self._process_configurations(configuration_settings, client)

processed_settings = self._process_feature_flags(processed_settings, processed_feature_flags, feature_flags)
processed_settings = self._process_and_merge_feature_flags(
processed_settings, processed_feature_flags, feature_flags, enhanced_feature_flags
)
self._dict = processed_settings
if settings_refreshed:
self._page_etags = page_etags
# Update the watch keys that have changed
self._watched_settings.update(updated_watched_settings)
if feature_flags is not None:
self._feature_flag_page_etags = feature_flag_page_etags
if enhanced_feature_flags is not None:
self._enhanced_feature_flag_etags = enhanced_feature_flag_etags
# Reset timers at the same time as they should load from the same store.
if configuration_refresh_attempted:
self._refresh_timer.reset()
if self._feature_flag_refresh_enabled and feature_flag_refresh_attempted:
self._feature_flag_refresh_timer.reset()
if (settings_refreshed or feature_flags) and self._on_refresh_success:
if (settings_refreshed or feature_flags or enhanced_feature_flags) and self._on_refresh_success:
self._on_refresh_success()
except AzureError as e:
logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint)
Expand Down Expand Up @@ -278,14 +295,22 @@ def _try_initialize(self, startup_exceptions: List[Exception], **kwargs: Any) ->
processed_settings = self._process_configurations(configuration_settings, client)

feature_flag_page_etags: List[List[str]] = []
enhanced_feature_flag_etags: List[List[str]] = []
if self._feature_flag_enabled:
feature_flags: List[FeatureFlagConfigurationSetting]
feature_flags, feature_flag_page_etags = client.load_feature_flags(
self._feature_flag_selectors,
headers=headers,
**kwargs,
)
processed_settings = self._process_feature_flags(processed_settings, [], feature_flags)
enhanced_feature_flags, enhanced_feature_flag_etags = client.load_enhanced_feature_flags(
self._enhanced_feature_flag_selectors,
headers=headers,
**kwargs,
)
processed_settings = self._process_and_merge_feature_flags(
processed_settings, [], feature_flags, enhanced_feature_flags
)
for (key, label), etag in self._watched_settings.items():
if not etag:
try:
Expand All @@ -310,6 +335,7 @@ def _try_initialize(self, startup_exceptions: List[Exception], **kwargs: Any) ->
self._dict = processed_settings
self._page_etags = page_etags
self._feature_flag_page_etags = feature_flag_page_etags
self._enhanced_feature_flag_etags = enhanced_feature_flag_etags
return True
except AzureError as e:
logger.warning("Failed to load configurations from endpoint %s.\n %s", client.endpoint, e.message)
Expand Down Expand Up @@ -375,7 +401,7 @@ def _process_configurations(
self._configuration_mapper(setting)
if isinstance(setting, FeatureFlagConfigurationSetting):
# Feature flags are not processed like other settings
feature_flag_value = self._process_feature_flag(setting)
feature_flag_value = self._process_kv_feature_flag(setting)
feature_flags_processed.append(feature_flag_value)
else:
key = self._process_key_name(setting)
Expand Down
Loading
Loading