Skip to content

Commit 61a6d33

Browse files
authored
App Config Provider - Tag filters (#41148)
* Adding tag filter * Base test + sp fix * more tests * Update CHANGELOG.md * review comments * Delete test_tag_filters.py * Update test_provider.py * Added tests and filter checks * Fixing tag filter validation * Update assets.json * fix doc * Review comments * null tag test * Update assets.json * Review comments * Update testcase.py * Update _models.py * Update _models.py * Update _models.py * Update _models.py * Update testcase.py
1 parent 26cf736 commit 61a6d33

16 files changed

+483
-50
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,4 @@ component-detection-pip-report.json
171171
**/.project
172172
**/.pydevproject
173173
**/.settings
174+
.github/prompts/copilot-instructions.md

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

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

55
### Features Added
66

7+
- Added `tag_filters` in `SettingSelector` to filter settings by tags.
8+
79
### Breaking Changes
810

911
### Bugs Fixed

sdk/appconfiguration/azure-appconfiguration-provider/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/appconfiguration/azure-appconfiguration-provider",
5-
"Tag": "python/appconfiguration/azure-appconfiguration-provider_6f35ca6dc7"
5+
"Tag": "python/appconfiguration/azure-appconfiguration-provider_b91db477af"
66
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
AzureWebAppEnvironmentVariable,
3434
ContainerAppEnvironmentVariable,
3535
KubernetesEnvironmentVariable,
36-
EMPTY_LABEL,
36+
NULL_CHAR,
3737
CUSTOM_FILTER_KEY,
3838
PERCENTAGE_FILTER_KEY,
3939
TIME_WINDOW_FILTER_KEY,
@@ -166,7 +166,7 @@ def _build_sentinel(setting: Union[str, Tuple[str, str]]) -> Tuple[str, str]:
166166
key, label = setting # type:ignore
167167
except IndexError:
168168
key = setting
169-
label = EMPTY_LABEL
169+
label = NULL_CHAR
170170
if "*" in key or "*" in label:
171171
raise ValueError("Wildcard key or label filters are not supported for refresh.")
172172
return key, label
@@ -263,7 +263,7 @@ def __init__(self, **kwargs: Any) -> None:
263263
self._origin_endpoint = kwargs.get("endpoint", None)
264264
self._dict: Dict[str, Any] = {}
265265
self._selects: List[SettingSelector] = kwargs.pop(
266-
"selects", [SettingSelector(key_filter="*", label_filter=EMPTY_LABEL)]
266+
"selects", [SettingSelector(key_filter="*", label_filter=NULL_CHAR)]
267267
)
268268

269269
trim_prefixes: List[str] = kwargs.pop("trim_prefixes", [])

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def load_configuration_settings(
141141
sentinel_keys = kwargs.pop("sentinel_keys", refresh_on)
142142
for select in selects:
143143
configurations = self._client.list_configuration_settings(
144-
key_filter=select.key_filter, label_filter=select.label_filter, **kwargs
144+
key_filter=select.key_filter, label_filter=select.label_filter, tags_filter=select.tag_filters, **kwargs
145145
)
146146
for config in configurations:
147147
if isinstance(config, FeatureFlagConfigurationSetting):
@@ -175,7 +175,10 @@ def load_feature_flags(
175175
filters_used: Dict[str, bool] = {}
176176
for select in feature_flag_selectors:
177177
feature_flags = self._client.list_configuration_settings(
178-
key_filter=FEATURE_FLAG_PREFIX + select.key_filter, label_filter=select.label_filter, **kwargs
178+
key_filter=FEATURE_FLAG_PREFIX + select.key_filter,
179+
label_filter=select.label_filter,
180+
tags_filter=select.tag_filters,
181+
**kwargs
179182
)
180183
for feature_flag in feature_flags:
181184
if not isinstance(feature_flag, FeatureFlagConfigurationSetting):

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
FEATURE_FLAG_KEY = "feature_flags"
99
FEATURE_FLAG_PREFIX = ".appconfig.featureflag/"
1010

11-
EMPTY_LABEL = "\0"
11+
NULL_CHAR = "\0"
1212

1313
REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE = "AZURE_APP_CONFIGURATION_TRACING_DISABLED"
1414
AzureFunctionEnvironmentVariable = "FUNCTIONS_EXTENSION_VERSION"

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

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
# Licensed under the MIT License. See License.txt in the project root for
44
# license information.
55
# -------------------------------------------------------------------------
6-
from typing import Optional, Callable, TYPE_CHECKING, Union, Awaitable, Mapping, Any, NamedTuple
7-
from ._constants import EMPTY_LABEL
6+
from typing import Optional, Callable, TYPE_CHECKING, Union, Awaitable, Mapping, Any, NamedTuple, List
7+
from ._constants import NULL_CHAR
88

99
if TYPE_CHECKING:
1010
from azure.core.credentials import TokenCredential
@@ -17,7 +17,7 @@ def __init__(
1717
*,
1818
credential: Optional[Union["TokenCredential", "AsyncTokenCredential"]] = None,
1919
client_configs: Optional[Mapping[str, Mapping[str, Any]]] = None,
20-
secret_resolver: Optional[Union[Callable[[str], str], Callable[[str], Awaitable[str]]]] = None
20+
secret_resolver: Optional[Union[Callable[[str], str], Callable[[str], Awaitable[str]]]] = None,
2121
):
2222
"""
2323
Options for connecting to Key Vault.
@@ -43,18 +43,35 @@ class SettingSelector:
4343
"""
4444
Selects a set of configuration settings from Azure App Configuration.
4545
46-
:keyword key_filter: A filter to select configuration settings based on their keys.
46+
:keyword key_filter:A filter to select configuration settings and feature flags based on their keys.
4747
:type key_filter: str
48-
:keyword label_filter: A filter to select configuration settings based on their labels. Default is value is
49-
EMPTY_LABEL i.e. (No Label) as seen in the portal.
48+
:keyword label_filter: A filter to select configuration settings and feature flags based on their labels. Default
49+
is value is \0 i.e. (No Label) as seen in the portal.
5050
:type label_filter: Optional[str]
51+
:keyword tag_filters: A filter to select configuration settings and feature flags based on their tags. This is a
52+
list of strings that will be used to match tags on the configuration settings. Reserved characters (\\*, \\, ,)
53+
must be escaped with backslash if they are part of the value. Tag filters must follow the format
54+
"tagName=tagValue", for empty values use "tagName=" and for null values use "tagName=\\0".
55+
:type tag_filters: Optional[List[str]]
5156
"""
5257

53-
def __init__(self, *, key_filter: str, label_filter: Optional[str] = EMPTY_LABEL):
58+
def __init__(
59+
self, *, key_filter: str, label_filter: Optional[str] = NULL_CHAR, tag_filters: Optional[List[str]] = None
60+
):
61+
if tag_filters is not None:
62+
if not isinstance(tag_filters, list):
63+
raise TypeError("tag_filters must be a list of strings.")
64+
for tag in tag_filters:
65+
if not tag:
66+
raise ValueError("Tag filter cannot be an empty string or None.")
67+
if not isinstance(tag, str) or "=" not in tag or tag.startswith("="):
68+
raise ValueError("Tag filter " + tag + ' does not follow the format "tagName=tagValue".')
69+
5470
self.key_filter = key_filter
5571
self.label_filter = label_filter
72+
self.tag_filters = tag_filters
5673

5774

5875
class WatchKey(NamedTuple):
5976
key: str
60-
label: str = EMPTY_LABEL
77+
label: str = NULL_CHAR

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ async def load_configuration_settings(
143143
sentinel_keys = kwargs.pop("sentinel_keys", refresh_on)
144144
for select in selects:
145145
configurations = self._client.list_configuration_settings(
146-
key_filter=select.key_filter, label_filter=select.label_filter, **kwargs
146+
key_filter=select.key_filter, label_filter=select.label_filter, tags_filter=select.tag_filters, **kwargs
147147
)
148148
async for config in configurations:
149149
if isinstance(config, FeatureFlagConfigurationSetting):
@@ -177,7 +177,10 @@ async def load_feature_flags(
177177
filters_used: Dict[str, bool] = {}
178178
for select in feature_flag_selectors:
179179
feature_flags = self._client.list_configuration_settings(
180-
key_filter=FEATURE_FLAG_PREFIX + select.key_filter, label_filter=select.label_filter, **kwargs
180+
key_filter=FEATURE_FLAG_PREFIX + select.key_filter,
181+
label_filter=select.label_filter,
182+
tags_filter=select.tag_filters,
183+
**kwargs
181184
)
182185
async for feature_flag in feature_flags:
183186
if not isinstance(feature_flag, FeatureFlagConfigurationSetting):

sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ async def create_aad_client(
2525
key_vault_options=None,
2626
on_refresh_success=None,
2727
feature_flag_enabled=False,
28+
feature_flag_selectors=[SettingSelector(key_filter="*", label_filter="\0")],
2829
feature_flag_refresh_enabled=False,
2930
):
3031
cred = self.get_credential(AzureAppConfigurationClient, is_async=True)
@@ -50,6 +51,7 @@ async def create_aad_client(
5051
keyvault_credential=keyvault_cred,
5152
on_refresh_success=on_refresh_success,
5253
feature_flag_enabled=feature_flag_enabled,
54+
feature_flag_selectors=feature_flag_selectors,
5355
feature_flag_refresh_enabled=feature_flag_refresh_enabled,
5456
)
5557
if key_vault_options:
@@ -66,6 +68,7 @@ async def create_aad_client(
6668
key_vault_options=key_vault_options,
6769
on_refresh_success=on_refresh_success,
6870
feature_flag_enabled=feature_flag_enabled,
71+
feature_flag_selectors=feature_flag_selectors,
6972
feature_flag_refresh_enabled=feature_flag_refresh_enabled,
7073
)
7174
return await load(
@@ -79,6 +82,7 @@ async def create_aad_client(
7982
secret_resolver=secret_resolver,
8083
on_refresh_success=on_refresh_success,
8184
feature_flag_enabled=feature_flag_enabled,
85+
feature_flag_selectors=feature_flag_selectors,
8286
feature_flag_refresh_enabled=feature_flag_refresh_enabled,
8387
)
8488

@@ -94,6 +98,7 @@ async def create_client(
9498
key_vault_options=None,
9599
on_refresh_success=None,
96100
feature_flag_enabled=False,
101+
feature_flag_selectors=[SettingSelector(key_filter="*", label_filter="\0")],
97102
feature_flag_refresh_enabled=False,
98103
):
99104
client = AzureAppConfigurationClient.from_connection_string(appconfiguration_connection_string)
@@ -110,6 +115,7 @@ async def create_client(
110115
keyvault_credential=self.get_credential(AzureAppConfigurationClient, is_async=True),
111116
on_refresh_success=on_refresh_success,
112117
feature_flag_enabled=feature_flag_enabled,
118+
feature_flag_selectors=feature_flag_selectors,
113119
feature_flag_refresh_enabled=feature_flag_refresh_enabled,
114120
)
115121
if key_vault_options:
@@ -127,6 +133,7 @@ async def create_client(
127133
key_vault_options=key_vault_options,
128134
on_refresh_success=on_refresh_success,
129135
feature_flag_enabled=feature_flag_enabled,
136+
feature_flag_selectors=feature_flag_selectors,
130137
feature_flag_refresh_enabled=feature_flag_refresh_enabled,
131138
)
132139
return await load(
@@ -139,6 +146,7 @@ async def create_client(
139146
secret_resolver=secret_resolver,
140147
on_refresh_success=on_refresh_success,
141148
feature_flag_enabled=feature_flag_enabled,
149+
feature_flag_selectors=feature_flag_selectors,
142150
feature_flag_refresh_enabled=feature_flag_refresh_enabled,
143151
)
144152

sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ async def test_provider_secret_resolver(self, appconfiguration_connection_string
8888
async with await self.create_client(
8989
appconfiguration_connection_string, selects=selects, secret_resolver=secret_resolver
9090
) as client:
91-
assert client["secret"] == "Reslover Value"
91+
assert client["secret"] == "Resolver Value"
9292

9393
# method: provider_selectors
9494
@app_config_decorator_async
@@ -115,7 +115,7 @@ async def test_provider_secret_resolver_options(self, appconfiguration_connectio
115115
async with await self.create_client(
116116
appconfiguration_connection_string, selects=selects, key_vault_options=key_vault_options
117117
) as client:
118-
assert client["secret"] == "Reslover Value"
118+
assert client["secret"] == "Resolver Value"
119119

120120
@app_config_decorator_async
121121
@recorded_by_proxy_async
@@ -218,6 +218,22 @@ async def test_process_key_value_content_type(self, appconfiguration_connection_
218218
)
219219
assert headers["Correlation-Context"] == "RequestType=fake-request,Features=AI+AICC"
220220

221+
@app_config_decorator_async
222+
@recorded_by_proxy_async
223+
async def test_provider_tag_filters(self, appconfiguration_connection_string, appconfiguration_keyvault_secret_url):
224+
selects = {SettingSelector(key_filter="*", tag_filters=["a=b"])}
225+
async with await self.create_client(
226+
appconfiguration_connection_string,
227+
selects=selects,
228+
feature_flag_enabled=True,
229+
feature_flag_selectors={SettingSelector(key_filter="*", tag_filters=["a=b"])},
230+
keyvault_secret_url=appconfiguration_keyvault_secret_url,
231+
) as client:
232+
assert "tagged_config" in client
233+
assert FEATURE_MANAGEMENT_KEY in client
234+
assert has_feature_flag(client, "TaggedFeatureFlag")
235+
assert "message" not in client
236+
221237

222238
async def secret_resolver(secret_id):
223-
return "Reslover Value"
239+
return "Resolver Value"

0 commit comments

Comments
 (0)