Skip to content

Commit 33fdc6b

Browse files
yuanqu72Copilot
andcommitted
Fix enhanced feature flag schema mapping, selector type narrowing, async API type hints, Python version floor, and rename tests README to avoid the package-readme section check, all addressing code review feedback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3a11cd2 commit 33fdc6b

13 files changed

Lines changed: 144 additions & 66 deletions

sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
### Breaking Changes
1010

11+
- Raised the minimum supported Python version to 3.10, matching the minimum required by `azure-appconfiguration>=1.10.0b1`. Dropped support for Python 3.7, 3.8, and 3.9.
12+
1113
### Bugs Fixed
1214

1315
### Other Changes

sdk/appconfiguration/azure-appconfiguration-provider/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ This library uses the standard [logging](https://docs.python.org/3/library/loggi
539539

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

542-
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.
542+
See [tests/tests.md](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration-provider/tests/tests.md) for instructions on running unit and integration tests, working with recordings, and setting up environment variables for local testing.
543543

544544
## Next steps
545545

sdk/appconfiguration/azure-appconfiguration-provider/api.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace azure.appconfiguration.provider
88
*,
99
feature_flag_enabled: bool = False,
1010
feature_flag_refresh_enabled: bool = False,
11-
feature_flag_selectors: Optional[List[SettingSelector]] = ...,
11+
feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = ...,
1212
key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = ...,
1313
keyvault_client_configs: Optional[Mapping[str, JSON]] = ...,
1414
keyvault_credential: Optional[TokenCredential] = ...,
@@ -31,7 +31,7 @@ namespace azure.appconfiguration.provider
3131
connection_string: str,
3232
feature_flag_enabled: bool = False,
3333
feature_flag_refresh_enabled: bool = False,
34-
feature_flag_selectors: Optional[List[SettingSelector]] = ...,
34+
feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = ...,
3535
key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = ...,
3636
keyvault_client_configs: Optional[Mapping[str, JSON]] = ...,
3737
keyvault_credential: Optional[TokenCredential] = ...,
@@ -68,6 +68,17 @@ namespace azure.appconfiguration.provider
6868
def refresh(self, **kwargs) -> None: ...
6969

7070

71+
class azure.appconfiguration.provider.FeatureFlagSelector:
72+
73+
def __init__(
74+
self,
75+
*,
76+
label_filter: Optional[str] = NULL_CHAR,
77+
name_filter: Optional[str] = ...,
78+
tag_filters: Optional[List[str]] = ...
79+
): ...
80+
81+
7182
class azure.appconfiguration.provider.SettingSelector:
7283

7384
def __init__(
@@ -94,7 +105,7 @@ namespace azure.appconfiguration.provider.aio
94105
*,
95106
feature_flag_enabled: bool = False,
96107
feature_flag_refresh_enabled: bool = False,
97-
feature_flag_selectors: Optional[List[SettingSelector]] = ...,
108+
feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = ...,
98109
key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = ...,
99110
keyvault_client_configs: Optional[Mapping[str, JSON]] = ...,
100111
keyvault_credential: Optional[AsyncTokenCredential] = ...,
@@ -117,7 +128,7 @@ namespace azure.appconfiguration.provider.aio
117128
connection_string: str,
118129
feature_flag_enabled: bool = False,
119130
feature_flag_refresh_enabled: bool = False,
120-
feature_flag_selectors: Optional[List[SettingSelector]] = ...,
131+
feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = ...,
121132
key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = ...,
122133
keyvault_client_configs: Optional[Mapping[str, JSON]] = ...,
123134
keyvault_credential: Optional[AsyncTokenCredential] = ...,
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
apiMdSha256: 1b252a78094b95bf515ebe5ec67fc2de12ce5b595bad25438a58241ca7153b84
1+
apiMdSha256: 808ba824eedfb7fc2365479c13867983ee557982cd635995a9617e140c1919dc
22
parserVersion: 0.3.28
3-
pythonVersion: 3.14.3
3+
pythonVersion: 3.12.10

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
ItemsView,
2222
ValuesView,
2323
TypeVar,
24+
cast,
2425
)
2526
from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module
2627
ConfigurationSetting,
@@ -95,16 +96,20 @@ def _normalize_feature_flag_selectors(
9596
- kv_selectors: Used to load key-value based feature flags (``SettingSelector.key_filter`` is used as the key
9697
filter).
9798
- enhanced_selectors: Used to load enhanced feature flags from the dedicated feature flag resource endpoint
98-
(``FeatureFlagSelector.name_filter`` is used as the name filter).
99+
(``FeatureFlagSelector.name_filter`` is used as the name filter).
99100
100101
:param selectors: The customer-provided feature flag selectors, or None to use the default (all feature flags
101102
without a label).
102103
:type selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]]
103104
:return: A tuple of (kv_selectors, enhanced_selectors).
104105
:rtype: Tuple[List[SettingSelector], List[FeatureFlagSelector]]
105106
"""
106-
if not selectors:
107+
if selectors is None:
107108
return [SettingSelector(key_filter="*")], [FeatureFlagSelector(name_filter="*")]
109+
if not selectors:
110+
# An explicitly empty collection of selectors means no feature flags should be loaded, unlike None
111+
# which falls back to the default of loading all unlabeled feature flags.
112+
return [], []
108113

109114
selectors_iter = iter(selectors)
110115
first_selector = next(selectors_iter)
@@ -117,18 +122,24 @@ def _normalize_feature_flag_selectors(
117122
)
118123

119124
if is_feature_flag_selector:
125+
feature_flag_selectors = cast(List[FeatureFlagSelector], selectors)
120126
kv_selectors = [
121-
SettingSelector(key_filter=select.name_filter, label_filter=select.label_filter, tag_filters=select.tag_filters)
122-
for select in selectors
127+
SettingSelector(
128+
key_filter=select.name_filter, label_filter=select.label_filter, tag_filters=select.tag_filters
129+
)
130+
for select in feature_flag_selectors
123131
]
124132
# FeatureFlagSelector has no snapshot_name, so every selector is used for enhanced feature flags.
125-
enhanced_selectors = list(selectors)
133+
enhanced_selectors = list(feature_flag_selectors)
126134
return kv_selectors, enhanced_selectors
127135

128-
kv_selectors = list(selectors)
136+
setting_selectors = cast(List[SettingSelector], selectors)
137+
kv_selectors = list(setting_selectors)
129138
enhanced_selectors = [
130-
FeatureFlagSelector(name_filter=select.key_filter, label_filter=select.label_filter, tag_filters=select.tag_filters)
131-
for select in selectors
139+
FeatureFlagSelector(
140+
name_filter=select.key_filter, label_filter=select.label_filter, tag_filters=select.tag_filters
141+
)
142+
for select in setting_selectors
132143
if select.snapshot_name is None
133144
]
134145
return kv_selectors, enhanced_selectors
@@ -273,7 +284,7 @@ def _update_ff_telemetry_metadata_common( # pylint: disable=too-many-positional
273284
feature_flag_reference += f"?label={label}"
274285

275286
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference
276-
allocation_id = self._generate_allocation_id(feature_flag_value, reference_path_segment)
287+
allocation_id = self._generate_allocation_id(feature_flag_value)
277288
if allocation_id:
278289
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ALLOCATION_ID_KEY] = allocation_id
279290

@@ -287,14 +298,12 @@ def _update_ff_telemetry_metadata_common( # pylint: disable=too-many-positional
287298
self._tracing_context.update_max_variants(len(variants))
288299

289300
@staticmethod
290-
def _generate_allocation_id(feature_flag_value: Dict[str, JSON], reference_path_segment: str) -> Optional[str]:
301+
def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str]:
291302
"""
292303
Generates an allocation ID for the specified feature.
293304
seed=123abc\ndefault_when_enabled=Control\npercentiles=0,Control,20;20,Test,100\nvariants=Control,standard;Test,special # pylint:disable=line-too-long
294305
295306
:param Dict[str, JSON] feature_flag_value: The feature to generate an allocation ID for.
296-
:param str reference_path_segment: The path segment identifying which source the feature flag was loaded
297-
from, e.g. "kv" for key-value based feature flags or "ff" for enhanced feature flags.
298307
:rtype: str
299308
:return: The allocation ID.
300309
"""
@@ -356,13 +365,9 @@ def _generate_allocation_id(feature_flag_value: Dict[str, JSON], reference_path_
356365

357366
for v in sorted_variants:
358367
allocation_id += f"{base64.b64encode(v.get('name', '').encode()).decode()},"
359-
# Key-value based feature flags store the variant value under "configuration_value". Enhanced
360-
# feature flags store it under "value" instead.
361-
if reference_path_segment == FEATURE_FLAG_KV_REFERENCE_SEGMENT:
362-
value_key = "configuration_value"
363-
else:
364-
value_key = "value"
365-
allocation_id += f"{json.dumps(v.get(value_key, ''), separators=(',', ':'), sort_keys=True)}"
368+
allocation_id += (
369+
f"{json.dumps(v.get('configuration_value', ''), separators=(',', ':'), sort_keys=True)}"
370+
)
366371
allocation_id += ";"
367372
if sorted_variants:
368373
allocation_id = allocation_id[:-1]
@@ -490,20 +495,20 @@ def _process_and_merge_feature_flags(
490495
feature_flags: Optional[List[FeatureFlagConfigurationSetting]],
491496
enhanced_feature_flags: Optional[List[FeatureFlag]] = None,
492497
) -> Dict[str, Any]:
493-
if feature_flags or enhanced_feature_flags:
498+
if feature_flags or enhanced_feature_flags is not None:
494499
# Reset feature flag usage
495500
self._tracing_context.reset_feature_filter_usage()
496501

497502
if feature_flags:
498503
self._processed_kv_feature_flags = [self._process_kv_feature_flag(ff) for ff in feature_flags]
499504

500-
if enhanced_feature_flags:
505+
if enhanced_feature_flags is not None:
501506
self._processed_enhanced_feature_flags = [
502507
self._process_enhanced_feature_flag(ff) for ff in enhanced_feature_flags
503508
]
504509
self._tracing_context.uses_enhanced_feature_flags = bool(enhanced_feature_flags)
505510

506-
if feature_flags or enhanced_feature_flags:
511+
if feature_flags or enhanced_feature_flags is not None:
507512
processed_feature_flags = self._merge_feature_flags(
508513
self._processed_kv_feature_flags, self._processed_enhanced_feature_flags
509514
)
@@ -534,9 +539,13 @@ def _merge_feature_flags(
534539
merged: Dict[str, Dict[str, Any]] = {}
535540
for ff in kv_feature_flags:
536541
identifier = ff.get(FEATURE_FLAG_ID_FIELD)
542+
if identifier is None:
543+
continue
537544
merged[identifier] = ff
538545
for ff in enhanced_feature_flags:
539546
identifier = ff.get(FEATURE_FLAG_ID_FIELD)
547+
if identifier is None:
548+
continue
540549
merged[identifier] = ff
541550
return list(merged.values())
542551

@@ -554,7 +563,7 @@ def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str,
554563
"""
555564
Convert an enhanced feature flag, loaded from the enhanced feature flag endpoint, into a dictionary that
556565
matches the feature management library's schema.
557-
Ref: https://github.com/microsoft/FeatureManagement/blob/main/Schema/FeatureFlag.v2.0.0.schema.json
566+
Ref: https://github.com/microsoft/FeatureManagement/blob/main/Schema/FeatureFlag.v2.0.0.schema.json
558567
559568
:param feature_flag: The enhanced feature flag.
560569
:type feature_flag: ~azure.appconfiguration.FeatureFlag
@@ -588,7 +597,7 @@ def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str,
588597
feature_flag_value["variants"] = [
589598
{
590599
"name": variant.name,
591-
"value": variant.value,
600+
"configuration_value": variant.value,
592601
"content_type": variant.content_type,
593602
"status_override": variant.status_override,
594603
}
@@ -605,8 +614,8 @@ def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str,
605614
allocation_value["percentile"] = [
606615
{
607616
"variant": percentile.variant,
608-
"percentile_from": percentile.percentile_from,
609-
"percentile_to": percentile.percentile_to,
617+
"from": percentile.percentile_from,
618+
"to": percentile.percentile_to,
610619
}
611620
for percentile in feature_flag.allocation.percentile
612621
]

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
ALLOCATION_ID_KEY = "AllocationId"
1616
ETAG_KEY = "ETag"
1717

18-
# Identifier field required by the feature management library's schema for every feature flag entry.
18+
# Identifier field required by the feature management library's schema for every feature flag entry.
1919
FEATURE_FLAG_ID_FIELD = "id"
2020
# Path segment used to build the feature flag reference URL for feature flags loaded from the key-value store.
2121
FEATURE_FLAG_KV_REFERENCE_SEGMENT = "kv"

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_client_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ async def check_enhanced_feature_flag_etags(
347347
self, feature_flag_selectors: List[FeatureFlagSelector], page_etags: List[List[str]], **kwargs
348348
) -> bool:
349349
"""
350-
Checks if any enhanced feature flag page has changed using page etags.
350+
Checks if any enhanced feature flag page has changed using page etags.
351351
352352
:param feature_flag_selectors: List of feature flag selectors for feature flags
353353
:type feature_flag_selectors: List[FeatureFlagSelector]

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_load.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
overload,
1414
List,
1515
Tuple,
16+
Union,
1617
)
1718
from azure.core.credentials_async import AsyncTokenCredential
1819
from .._constants import (
1920
DEFAULT_STARTUP_TIMEOUT,
2021
)
21-
from .._models import AzureAppConfigurationKeyVaultOptions, SettingSelector
22+
from .._models import AzureAppConfigurationKeyVaultOptions, FeatureFlagSelector, SettingSelector
2223
from .._utils import (
2324
delay_failure,
2425
process_load_parameters,
@@ -49,7 +50,7 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only
4950
on_refresh_success: Optional[Callable] = None,
5051
on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] = None,
5152
feature_flag_enabled: bool = False,
52-
feature_flag_selectors: Optional[List[SettingSelector]] = None,
53+
feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = None,
5354
feature_flag_refresh_enabled: bool = False,
5455
startup_timeout: int = DEFAULT_STARTUP_TIMEOUT,
5556
**kwargs,
@@ -87,9 +88,11 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only
8788
:paramtype on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]]
8889
:keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False.
8990
:paramtype feature_flag_enabled: bool
90-
:keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all
91-
feature flags without a label.
92-
:paramtype feature_flag_selectors: List[SettingSelector]
91+
:keyword feature_flag_selectors: Optional list of selectors to filter feature flags. Either a list of
92+
~azure.appconfiguration.provider.SettingSelector or a list of
93+
~azure.appconfiguration.provider.FeatureFlagSelector (the two types cannot be mixed in the same list).
94+
By default will load all feature flags without a label.
95+
:paramtype feature_flag_selectors: Union[List[SettingSelector], List[FeatureFlagSelector]]
9396
:keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is
9497
False.
9598
:paramtype feature_flag_refresh_enabled: bool
@@ -124,7 +127,7 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only
124127
on_refresh_success: Optional[Callable] = None,
125128
on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] = None,
126129
feature_flag_enabled: bool = False,
127-
feature_flag_selectors: Optional[List[SettingSelector]] = None,
130+
feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = None,
128131
feature_flag_refresh_enabled: bool = False,
129132
startup_timeout: int = DEFAULT_STARTUP_TIMEOUT,
130133
**kwargs,
@@ -164,9 +167,11 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only
164167
:paramtype on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]]
165168
:keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False.
166169
:paramtype feature_flag_enabled: bool
167-
:keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all
168-
feature flags without a label.
169-
:paramtype feature_flag_selectors: List[SettingSelector]
170+
:keyword feature_flag_selectors: Optional list of selectors to filter feature flags. Either a list of
171+
~azure.appconfiguration.provider.SettingSelector or a list of
172+
~azure.appconfiguration.provider.FeatureFlagSelector (the two types cannot be mixed in the same list).
173+
By default will load all feature flags without a label.
174+
:paramtype feature_flag_selectors: Union[List[SettingSelector], List[FeatureFlagSelector]]
170175
:keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is
171176
False.
172177
:paramtype feature_flag_refresh_enabled: bool

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,7 @@ async def _attempt_refresh(
168168
# own page-level etag state, since they are a separate resource type with a separate
169169
# change-detection mechanism.
170170
if not self._enhanced_feature_flag_etags or await client.check_enhanced_feature_flag_etags(
171-
self._enhanced_feature_flag_selectors, self._enhanced_feature_flag_etags, headers=headers,
172-
**kwargs
171+
self._enhanced_feature_flag_selectors, self._enhanced_feature_flag_etags, headers=headers, **kwargs
173172
):
174173
enhanced_feature_flags, enhanced_feature_flag_etags = await client.load_enhanced_feature_flags(
175174
self._enhanced_feature_flag_selectors, headers=headers, **kwargs

sdk/appconfiguration/azure-appconfiguration-provider/setup.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,13 @@
6161
"Programming Language :: Python",
6262
"Programming Language :: Python :: 3 :: Only",
6363
"Programming Language :: Python :: 3",
64-
"Programming Language :: Python :: 3.7",
65-
"Programming Language :: Python :: 3.8",
66-
"Programming Language :: Python :: 3.9",
6764
"Programming Language :: Python :: 3.10",
6865
"Programming Language :: Python :: 3.11",
6966
"License :: OSI Approved :: MIT License",
7067
],
7168
zip_safe=False,
7269
packages=find_packages(exclude=exclude_packages),
73-
python_requires=">=3.6",
70+
python_requires=">=3.10",
7471
install_requires=[
7572
"azure-core>=1.31.0",
7673
"azure-appconfiguration>=1.10.0b1",

0 commit comments

Comments
 (0)