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
48 changes: 48 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,48 @@ config = load(

<!-- END SNIPPET -->

### Loading Enhanced Feature Flags

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

<!-- 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 -->

The same `SettingSelector` used to filter key-value based feature flags also filters enhanced feature flags, by name, label, or tags. Note that selectors with a `snapshot_name` are not currently supported by the enhanced feature flag endpoint and are skipped when loading enhanced feature flags.

<!-- 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 -->

## 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 +511,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 @@ -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