From ce483f137790189774cd123de10582511d66cf09 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 22 Jun 2026 14:00:17 -0700 Subject: [PATCH 01/25] client bugfix --- .../azure/appconfiguration/_azure_appconfiguration_client.py | 3 ++- .../aio/_azure_appconfiguration_client_async.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py index 89d0ebcf9151..56087293f9eb 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py @@ -83,7 +83,8 @@ def __init__(self, base_url: str, credential: TokenCredential, **kwargs: Any) -> audience = get_audience(base_url) # Ensure all scopes end with /.default and strip any trailing slashes before adding suffix - kwargs["credential_scopes"] = [audience + DEFAULT_SCOPE_SUFFIX] + audience_scope = audience.rstrip('/') + '/' + DEFAULT_SCOPE_SUFFIX + kwargs["credential_scopes"] = [audience_scope] if isinstance(credential, AzureKeyCredential): id_credential = kwargs.pop("id_credential") diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py index 66040aa9f80a..257be2407c39 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py @@ -87,7 +87,8 @@ def __init__(self, base_url: str, credential: AsyncTokenCredential, **kwargs: An audience = get_audience(base_url) # Ensure all scopes end with /.default and strip any trailing slashes before adding suffix - kwargs["credential_scopes"] = [audience + DEFAULT_SCOPE_SUFFIX] + audience_scope = audience.rstrip('/') + '/' + DEFAULT_SCOPE_SUFFIX + kwargs["credential_scopes"] = [audience_scope] if isinstance(credential, AzureKeyCredential): id_credential = kwargs.pop("id_credential") From b919e61faa17d176273becbe0abb3b2ea6ca25ae Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 22 Jun 2026 16:28:49 -0700 Subject: [PATCH 02/25] Auto gen, exposed endpoints and basic tests --- .../azure-appconfiguration/_metadata.json | 4 +- .../apiview-properties.json | 30 +- .../azure/appconfiguration/__init__.py | 18 + .../_azure_appconfiguration_client.py | 262 +- .../appconfiguration/_generated/_client.py | 7 +- .../_generated/_configuration.py | 9 +- .../_generated/_operations/_operations.py | 2259 ++++++++++++++--- .../_generated/aio/_client.py | 7 +- .../_generated/aio/_configuration.py | 9 +- .../_generated/aio/_operations/_operations.py | 1106 +++++++- .../_generated/models/__init__.py | 24 + .../_generated/models/_enums.py | 47 + .../_generated/models/_models.py | 401 ++- .../azure/appconfiguration/_models.py | 826 ++++++ .../_azure_appconfiguration_client_async.py | 209 +- .../azure-appconfiguration/pyproject.toml | 2 +- .../tests/asynctestcase.py | 3 + .../tests/test_feature_flags.py | 217 ++ .../tests/test_feature_flags_async.py | 227 ++ .../azure-appconfiguration/tests/testcase.py | 3 + .../azure-appconfiguration/tsp-location.yaml | 2 +- 21 files changed, 5256 insertions(+), 416 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py create mode 100644 sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py diff --git a/sdk/appconfiguration/azure-appconfiguration/_metadata.json b/sdk/appconfiguration/azure-appconfiguration/_metadata.json index 798d1d9d00af..d180245e023e 100644 --- a/sdk/appconfiguration/azure-appconfiguration/_metadata.json +++ b/sdk/appconfiguration/azure-appconfiguration/_metadata.json @@ -1,6 +1,6 @@ { - "apiVersion": "2026-04-01", + "apiVersion": "2026-05-01-preview", "apiVersions": { - "AzureAppConfiguration": "2026-04-01" + "AzureAppConfiguration": "2026-05-01-preview" } } \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json b/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json index 19913b81c9df..543733d8cab7 100644 --- a/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json +++ b/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json @@ -2,14 +2,26 @@ "CrossLanguagePackageId": "AzureAppConfiguration", "CrossLanguageDefinitionId": { "azure.appconfiguration.models.Error": "AzureAppConfiguration.Error", + "azure.appconfiguration.models.FeatureFlag": "AzureAppConfiguration.FeatureFlag", + "azure.appconfiguration.models.FeatureFlagAllocation": "AzureAppConfiguration.FeatureFlagAllocation", + "azure.appconfiguration.models.FeatureFlagConditions": "AzureAppConfiguration.FeatureFlagConditions", + "azure.appconfiguration.models.FeatureFlagFilter": "AzureAppConfiguration.FeatureFlagFilter", + "azure.appconfiguration.models.FeatureFlagTelemetryConfiguration": "AzureAppConfiguration.FeatureFlagTelemetryConfiguration", + "azure.appconfiguration.models.FeatureFlagVariantDefinition": "AzureAppConfiguration.FeatureFlagVariantDefinition", + "azure.appconfiguration.models.GroupAllocation": "AzureAppConfiguration.GroupAllocation", "azure.appconfiguration.models.Key": "AzureAppConfiguration.Key", "azure.appconfiguration.models.KeyValue": "AzureAppConfiguration.KeyValue", "azure.appconfiguration.models.KeyValueFilter": "AzureAppConfiguration.KeyValueFilter", "azure.appconfiguration.models.Label": "AzureAppConfiguration.Label", "azure.appconfiguration.models.OperationDetails": "AzureAppConfiguration.OperationDetails", + "azure.appconfiguration.models.PercentileAllocation": "AzureAppConfiguration.PercentileAllocation", "azure.appconfiguration.models.Snapshot": "AzureAppConfiguration.Snapshot", "azure.appconfiguration.models.SnapshotUpdateParameters": "AzureAppConfiguration.SnapshotUpdateParameters", + "azure.appconfiguration.models.UserAllocation": "AzureAppConfiguration.UserAllocation", "azure.appconfiguration.models.ConfigurationSettingFields": "AzureAppConfiguration.KeyValueFields", + "azure.appconfiguration.models.RequirementType": "AzureAppConfiguration.RequirementType", + "azure.appconfiguration.models.StatusOverride": "AzureAppConfiguration.StatusOverride", + "azure.appconfiguration.models.FeatureFlagFields": "AzureAppConfiguration.FeatureFlagFields", "azure.appconfiguration.models.SnapshotStatus": "AzureAppConfiguration.SnapshotStatus", "azure.appconfiguration.models.SnapshotComposition": "AzureAppConfiguration.CompositionType", "azure.appconfiguration.models.SnapshotFields": "AzureAppConfiguration.SnapshotFields", @@ -29,6 +41,22 @@ "azure.appconfiguration.aio.AzureAppConfigurationClient.delete_key_value": "AzureAppConfiguration.deleteKeyValue", "azure.appconfiguration.AzureAppConfigurationClient.check_key_value": "AzureAppConfiguration.checkKeyValue", "azure.appconfiguration.aio.AzureAppConfigurationClient.check_key_value": "AzureAppConfiguration.checkKeyValue", + "azure.appconfiguration.AzureAppConfigurationClient.get_feature_flags": "AzureAppConfiguration.getFeatureFlags", + "azure.appconfiguration.aio.AzureAppConfigurationClient.get_feature_flags": "AzureAppConfiguration.getFeatureFlags", + "azure.appconfiguration.AzureAppConfigurationClient.check_feature_flags": "AzureAppConfiguration.checkFeatureFlags", + "azure.appconfiguration.aio.AzureAppConfigurationClient.check_feature_flags": "AzureAppConfiguration.checkFeatureFlags", + "azure.appconfiguration.AzureAppConfigurationClient.get_feature_flag": "AzureAppConfiguration.getFeatureFlag", + "azure.appconfiguration.aio.AzureAppConfigurationClient.get_feature_flag": "AzureAppConfiguration.getFeatureFlag", + "azure.appconfiguration.AzureAppConfigurationClient.check_feature_flag": "AzureAppConfiguration.checkFeatureFlag", + "azure.appconfiguration.aio.AzureAppConfigurationClient.check_feature_flag": "AzureAppConfiguration.checkFeatureFlag", + "azure.appconfiguration.AzureAppConfigurationClient.put_feature_flag": "AzureAppConfiguration.putFeatureFlag", + "azure.appconfiguration.aio.AzureAppConfigurationClient.put_feature_flag": "AzureAppConfiguration.putFeatureFlag", + "azure.appconfiguration.AzureAppConfigurationClient.delete_feature_flag": "AzureAppConfiguration.deleteFeatureFlag", + "azure.appconfiguration.aio.AzureAppConfigurationClient.delete_feature_flag": "AzureAppConfiguration.deleteFeatureFlag", + "azure.appconfiguration.AzureAppConfigurationClient.get_feature_flag_revisions": "AzureAppConfiguration.getFeatureFlagRevisions", + "azure.appconfiguration.aio.AzureAppConfigurationClient.get_feature_flag_revisions": "AzureAppConfiguration.getFeatureFlagRevisions", + "azure.appconfiguration.AzureAppConfigurationClient.check_feature_flag_revisions": "AzureAppConfiguration.checkFeatureFlagRevisions", + "azure.appconfiguration.aio.AzureAppConfigurationClient.check_feature_flag_revisions": "AzureAppConfiguration.checkFeatureFlagRevisions", "azure.appconfiguration.AzureAppConfigurationClient.get_snapshots": "AzureAppConfiguration.getSnapshots", "azure.appconfiguration.aio.AzureAppConfigurationClient.get_snapshots": "AzureAppConfiguration.getSnapshots", "azure.appconfiguration.AzureAppConfigurationClient.check_snapshots": "AzureAppConfiguration.checkSnapshots", @@ -54,5 +82,5 @@ "azure.appconfiguration.AzureAppConfigurationClient.check_revisions": "AzureAppConfiguration.checkRevisions", "azure.appconfiguration.aio.AzureAppConfigurationClient.check_revisions": "AzureAppConfiguration.checkRevisions" }, - "CrossLanguageVersion": "a2a80f4f2e80" + "CrossLanguageVersion": "9fdd341c223f" } \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py index 7c8ac904dccb..ebc1cb0eee04 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py @@ -20,6 +20,15 @@ ConfigurationSettingLabel, ConfigurationSettingPaged, AsyncConfigurationSettingPaged, + FeatureFlag, + FeatureFlagConditions, + FeatureFlagFilter, + FeatureFlagVariantDefinition, + FeatureFlagAllocation, + FeatureFlagTelemetryConfiguration, + PercentileAllocation, + UserAllocation, + GroupAllocation, ) from ._generated.models import ( SnapshotStatus, @@ -48,6 +57,15 @@ "ConfigurationSettingLabel", "ConfigurationSettingPaged", "AsyncConfigurationSettingPaged", + "FeatureFlag", + "FeatureFlagConditions", + "FeatureFlagFilter", + "FeatureFlagVariantDefinition", + "FeatureFlagAllocation", + "FeatureFlagTelemetryConfiguration", + "PercentileAllocation", + "UserAllocation", + "GroupAllocation", "FILTER_PERCENTAGE", "FILTER_TARGETING", "FILTER_TIME_WINDOW", diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py index 56087293f9eb..575c9c4c5b14 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------------------------------------------ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for @@ -12,7 +13,7 @@ from azure.core.pipeline.policies import BearerTokenCredentialPolicy from azure.core.polling import LROPoller from azure.core.tracing.decorator import distributed_trace -from azure.core.exceptions import ResourceNotModifiedError +from azure.core.exceptions import ResourceNotFoundError, ResourceNotModifiedError from azure.core.rest import HttpRequest, HttpResponse from ._azure_appconfiguration_error import ResourceReadOnlyError from ._azure_appconfiguration_requests import AppConfigRequestsCredentialsPolicy @@ -33,6 +34,8 @@ ConfigurationSettingsFilter, ConfigurationSnapshot, ConfigurationSettingLabel, + FeatureFlagConfigurationSetting, + FeatureFlag, ) from ._audience import get_audience, DEFAULT_SCOPE_SUFFIX from ._utils import ( @@ -832,6 +835,263 @@ def list_snapshots( **kwargs, ) + @distributed_trace + def list_feature_flags( + self, + name_filter: Optional[str] = None, + label_filter: Optional[str] = None, + *, + tags_filter: Optional[List[str]] = None, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> ItemPaged["FeatureFlag"]: + """ + Find the FeatureFlag objects, optionally filtered by name, label, tags and accept_datetime. + + :param name_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end + of the filter. Default is `None`. + :type name_filter: str or None + :param label_filter: Filter results based on their labels. Default is `None`. + :type label_filter: str or None + :keyword tags_filter: Filter results based on their tags. Default is `None`. + :paramtype tags_filter: list[str] or None + :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: An iterator of :class:`~azure.appconfiguration.FeatureFlag` + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.FeatureFlag] + :raises: :class:`~azure.core.exceptions.HttpResponseError` + + Example + + .. code-block:: python + + # List feature flags + feature_flags = client.list_feature_flags() + for flag in feature_flags: + print(flag.name, flag.enabled) + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + + # Call the generated method and wrap results to convert to SDK FeatureFlag + def convert_to_sdk(objs): + return [FeatureFlag._from_generated(obj) for obj in objs] + + return self._impl.get_feature_flags( # type: ignore[return-value] + name=name_filter, + label=label_filter, + accept_datetime=accept_datetime, + tags=tags_filter, + cls=convert_to_sdk, + **kwargs + ) + + @distributed_trace + def get_feature_flag( + self, + feature_id: str, + label: Optional[str] = None, + *, + etag: Optional[str] = None, + match_condition: MatchConditions = MatchConditions.Unconditionally, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> Union[None, "FeatureFlag"]: + """Get a FeatureFlag from Azure App Configuration service + + :param feature_id: The feature flag name/id to retrieve + :type feature_id: str + :param label: Label used to identify the FeatureFlag. Default is `None`. + :type label: str or None + :keyword etag: Check if the FeatureFlag is changed. Set None to skip checking etag + :paramtype etag: str or None + :keyword match_condition: The match condition to use upon the etag + :paramtype match_condition: ~azure.core.MatchConditions + :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: The matched FeatureFlag object + :rtype: ~azure.appconfiguration.FeatureFlag or None + :raises: :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError`, \ + :class:`~azure.core.exceptions.ResourceNotFoundError`, \ + :class:`~azure.core.exceptions.ResourceModifiedError`, \ + :class:`~azure.core.exceptions.ResourceExistsError` + + Example + + .. code-block:: python + + feature_flag = client.get_feature_flag(feature_id="MyFeatureFlag") + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + try: + generated_feature_flag = self._impl.get_feature_flag( + name=feature_id, + label=label, + accept_datetime=accept_datetime, + etag=etag, + match_condition=match_condition, + **kwargs, + ) + return FeatureFlag._from_generated(generated_feature_flag) + except (ResourceNotFoundError, ResourceNotModifiedError): + return None + + @distributed_trace + def set_feature_flag( + self, + feature_flag: "FeatureFlag", + match_condition: MatchConditions = MatchConditions.Unconditionally, + *, + etag: Optional[str] = None, + **kwargs: Any, + ) -> "FeatureFlag": + """Add or update a FeatureFlag. + If the feature flag identified by feature_id and label does not exist, this is a create. + Otherwise this is an update. + + :param feature_flag: The FeatureFlag to be added (if not exists) \ + or updated (if exists) to the service + :type feature_flag: ~azure.appconfiguration.FeatureFlag + :param match_condition: The match condition to use upon the etag + :type match_condition: ~azure.core.MatchConditions + :keyword etag: Check if the FeatureFlag is changed. \ + Will use the value from param feature_flag if not set. + :paramtype etag: str or None + :return: The FeatureFlag returned from the service + :rtype: ~azure.appconfiguration.FeatureFlag + :raises: :class:`~azure.appconfiguration.ResourceReadOnlyError`, \ + :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError`, \ + :class:`~azure.core.exceptions.ResourceModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotFoundError`, \ + :class:`~azure.core.exceptions.ResourceExistsError` + + Example + + .. code-block:: python + + feature_flag = FeatureFlag( + name="MyFeatureFlag", + enabled=True + ) + returned_feature_flag = client.set_feature_flag(feature_flag) + """ + error_map: Dict[int, Any] = {409: ResourceReadOnlyError} + generated_feature_flag = feature_flag._to_generated() + generated_result = self._impl.put_feature_flag( + entity=generated_feature_flag, + name=feature_flag.name, + label=feature_flag.label, + etag=etag or feature_flag.etag, + match_condition=match_condition, + error_map=error_map, + **kwargs, + ) + return FeatureFlag._from_generated(generated_result) + + @distributed_trace + def delete_feature_flag( + self, + feature_id: str, + label: Optional[str] = None, + *, + etag: Optional[str] = None, + match_condition: MatchConditions = MatchConditions.Unconditionally, + **kwargs: Any, + ) -> Union[None, "FeatureFlag"]: + """Delete a FeatureFlag if it exists + + :param feature_id: The feature flag name/id to delete + :type feature_id: str + :param label: Label used to identify the FeatureFlag. Default is `None`. + :type label: str or None + :keyword etag: Check if the FeatureFlag is changed. Set None to skip checking etag + :paramtype etag: str or None + :keyword match_condition: The match condition to use upon the etag + :paramtype match_condition: ~azure.core.MatchConditions + :return: The deleted FeatureFlag returned from the service, or None if it doesn't exist. + :rtype: ~azure.appconfiguration.FeatureFlag or None + :raises: :class:`~azure.appconfiguration.ResourceReadOnlyError`, \ + :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError`, \ + :class:`~azure.core.exceptions.ResourceModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotFoundError`, \ + :class:`~azure.core.exceptions.ResourceExistsError` + + Example + + .. code-block:: python + + deleted_feature_flag = client.delete_feature_flag(feature_id="MyFeatureFlag") + """ + error_map: Dict[int, Any] = {409: ResourceReadOnlyError} + generated_feature_flag = self._impl.delete_feature_flag( + name=feature_id, + label=label, + etag=etag, + match_condition=match_condition, + error_map=error_map, + **kwargs, + ) + if generated_feature_flag: + return FeatureFlag._from_generated(generated_feature_flag) + return None + + @distributed_trace + def list_feature_flag_revisions( + self, + feature_id_filter: Optional[str] = None, + label_filter: Optional[str] = None, + *, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> ItemPaged["FeatureFlag"]: + """ + Find the FeatureFlag revision history, optionally filtered by feature_id and label. + + :param feature_id_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end + of the filter. Default is `None`. + :type feature_id_filter: str or None + :param label_filter: Filter results based on their labels. Default is `None`. + :type label_filter: str or None + :keyword accept_datetime: Retrieve FeatureFlag revisions that existed at this datetime + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: An iterator of :class:`~azure.appconfiguration.FeatureFlag` + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.FeatureFlag] + :raises: :class:`~azure.core.exceptions.HttpResponseError` + + Example + + .. code-block:: python + + # List feature flag revisions + revisions = client.list_feature_flag_revisions(feature_id_filter="MyFeatureFlag") + for revision in revisions: + print(revision.name, revision.etag) + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + + # Build kwargs for the underlying call + impl_kwargs = dict(kwargs) + if accept_datetime is not None: + impl_kwargs["accept_datetime"] = accept_datetime + + # Call the generated method and wrap results to convert to SDK FeatureFlag + def convert_to_sdk(objs): + return [FeatureFlag._from_generated(obj) for obj in objs] + + return self._impl.get_feature_flag_revisions( # type: ignore[return-value] + name=feature_id_filter, + label=label_filter, + cls=convert_to_sdk, + **impl_kwargs + ) + def update_sync_token(self, token: str) -> None: """Add a sync token to the internal list of tokens. diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_client.py index 04845befa73e..0b4aec99b87f 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_client.py @@ -37,9 +37,10 @@ class AzureAppConfigurationClient(_AzureAppConfigurationClientOperationsMixin): credential type or a token credential type. Required. :type credential: ~azure.core.credentials.AzureKeyCredential or ~azure.core.credentials.TokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2026-04-01" - and None. Default value is None. If not set, the operation's default API version will be used. - Note that overriding this default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-05-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str :keyword int polling_interval: Default waiting time between two polls for LRO operations if no Retry-After header is present. diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_configuration.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_configuration.py index b18987b4ec05..7f25eeb72269 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_configuration.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_configuration.py @@ -29,14 +29,15 @@ class AzureAppConfigurationClientConfiguration: # pylint: disable=too-many-inst credential type or a token credential type. Required. :type credential: ~azure.core.credentials.AzureKeyCredential or ~azure.core.credentials.TokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2026-04-01" - and None. Default value is None. If not set, the operation's default API version will be used. - Note that overriding this default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-05-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ def __init__(self, endpoint: str, credential: Union[AzureKeyCredential, "TokenCredential"], **kwargs: Any) -> None: - api_version: str = kwargs.pop("api_version", "2026-04-01") + api_version: str = kwargs.pop("api_version", "2026-05-01-preview") if endpoint is None: raise ValueError("Parameter 'endpoint' must not be None.") diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py index 21f24d2d26c6..681786c8937c 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py @@ -58,7 +58,7 @@ def build_azure_app_configuration_get_keys_request( # pylint: disable=name-too- _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.keyset+json, application/problem+json") # Construct URL @@ -92,7 +92,7 @@ def build_azure_app_configuration_check_keys_request( # pylint: disable=name-to _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL _url = "/keys" @@ -129,7 +129,7 @@ def build_azure_app_configuration_get_key_values_request( # pylint: disable=nam _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kvset+json, application/problem+json") # Construct URL @@ -183,7 +183,7 @@ def build_azure_app_configuration_check_key_values_request( # pylint: disable=n _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL _url = "/kv" @@ -232,7 +232,7 @@ def build_azure_app_configuration_get_key_value_request( # pylint: disable=name _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") # Construct URL @@ -281,7 +281,7 @@ def build_azure_app_configuration_put_key_value_request( # pylint: disable=name _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") # Construct URL @@ -325,7 +325,7 @@ def build_azure_app_configuration_delete_key_value_request( # pylint: disable=n _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") # Construct URL @@ -370,7 +370,7 @@ def build_azure_app_configuration_check_key_value_request( # pylint: disable=na _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL _url = "/kv/{key}" path_format_arguments = { @@ -403,70 +403,116 @@ def build_azure_app_configuration_check_key_value_request( # pylint: disable=na return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_get_snapshots_request( # pylint: disable=name-too-long +def build_azure_app_configuration_get_feature_flags_request( # pylint: disable=name-too-long *, name: Optional[str] = None, - after: Optional[str] = None, - select: Optional[list[Union[str, _models.SnapshotFields]]] = None, - status: Optional[list[Union[str, _models.SnapshotStatus]]] = None, + label: Optional[str] = None, sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.snapshotset+json, application/problem+json") + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop( + "Accept", + 'application/json;profile="https://azconfig.io/mime-profiles/ffset";charset=utf-8, application/problem+json', + ) # Construct URL - _url = "/snapshots" + _url = "/ff" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") if name is not None: _params["name"] = _SERIALIZER.query("name", name, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") if after is not None: _params["After"] = _SERIALIZER.query("after", after, "str") if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") - if status is not None: - _params["status"] = _SERIALIZER.query("status", status, "[str]", div=",") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_check_snapshots_request( # pylint: disable=name-too-long - *, sync_token: Optional[str] = None, after: Optional[str] = None, **kwargs: Any +def build_azure_app_configuration_check_feature_flags_request( # pylint: disable=name-too-long + *, + name: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL - _url = "/snapshots" + _url = "/ff" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if name is not None: + _params["name"] = _SERIALIZER.query("name", name, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") if after is not None: _params["After"] = _SERIALIZER.query("after", after, "str") + if select is not None: + _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_get_snapshot_request( # pylint: disable=name-too-long +def build_azure_app_configuration_get_feature_flag_request( # pylint: disable=name-too-long name: str, *, - select: Optional[list[Union[str, _models.SnapshotFields]]] = None, + label: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any @@ -474,11 +520,14 @@ def build_azure_app_configuration_get_snapshot_request( # pylint: disable=name- _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.snapshot+json, application/problem+json") + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop( + "Accept", + 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8, application/problem+json', + ) # Construct URL - _url = "/snapshots/{name}" + _url = "/ff/{name}" path_format_arguments = { "name": _SERIALIZER.url("name", name, "str"), } @@ -487,12 +536,18 @@ def build_azure_app_configuration_get_snapshot_request( # pylint: disable=name- # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") if_match = prep_if_match(etag, match_condition) if if_match is not None: @@ -504,40 +559,75 @@ def build_azure_app_configuration_get_snapshot_request( # pylint: disable=name- return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_get_operation_details_request( # pylint: disable=name-too-long - *, snapshot: str, **kwargs: Any +def build_azure_app_configuration_check_feature_flag_request( # pylint: disable=name-too-long + name: str, + *, + label: Optional[str] = None, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) - accept = _headers.pop("Accept", "application/json") - + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL - _url = "/operations" + _url = "/ff/{name}" + path_format_arguments = { + "name": _SERIALIZER.url("name", name, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - _params["snapshot"] = _SERIALIZER.query("snapshot", snapshot, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") + if select is not None: + _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers - _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_create_snapshot_request( # pylint: disable=name-too-long - name: str, *, sync_token: Optional[str] = None, **kwargs: Any +def build_azure_app_configuration_put_feature_flag_request( # pylint: disable=name-too-long + name: str, + *, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.snapshot+json, application/problem+json") + content_type: str = kwargs.pop("content_type") + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop( + "Accept", + 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8, application/problem+json', + ) # Construct URL - _url = "/snapshots/{name}" + _url = "/ff/{name}" path_format_arguments = { "name": _SERIALIZER.url("name", name, "str"), } @@ -546,20 +636,28 @@ def build_azure_app_configuration_create_snapshot_request( # pylint: disable=na # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") # Construct headers - if content_type is not None: - _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") + _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") return HttpRequest(method="PUT", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_update_snapshot_request( # pylint: disable=name-too-long +def build_azure_app_configuration_delete_feature_flag_request( # pylint: disable=name-too-long name: str, *, + label: Optional[str] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, @@ -568,12 +666,14 @@ def build_azure_app_configuration_update_snapshot_request( # pylint: disable=na _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.snapshot+json, application/problem+json") + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop( + "Accept", + 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8, application/problem+json', + ) # Construct URL - _url = "/snapshots/{name}" + _url = "/ff/{name}" path_format_arguments = { "name": _SERIALIZER.url("name", name, "str"), } @@ -582,10 +682,10 @@ def build_azure_app_configuration_update_snapshot_request( # pylint: disable=na # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") # Construct headers - if content_type is not None: - _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") @@ -594,14 +694,18 @@ def build_azure_app_configuration_update_snapshot_request( # pylint: disable=na _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") if_none_match = prep_if_none_match(etag, match_condition) if if_none_match is not None: - _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + _headers["if-none-match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="PATCH", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="DELETE", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_check_snapshot_request( # pylint: disable=name-too-long - name: str, +def build_azure_app_configuration_get_feature_flag_revisions_request( # pylint: disable=name-too-long *, + name: Optional[str] = None, + label: Optional[str] = None, + after: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, @@ -610,21 +714,32 @@ def build_azure_app_configuration_check_snapshot_request( # pylint: disable=nam _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) - # Construct URL - _url = "/snapshots/{name}" - path_format_arguments = { - "name": _SERIALIZER.url("name", name, "str"), - } + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop( + "Accept", + 'application/json;profile="https://azconfig.io/mime-profiles/ffset";charset=utf-8, application/problem+json', + ) - _url: str = _url.format(**path_format_arguments) # type: ignore + # Construct URL + _url = "/ff-revisions" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if name is not None: + _params["name"] = _SERIALIZER.query("name", name, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") + if after is not None: + _params["After"] = _SERIALIZER.query("after", after, "str") + if select is not None: + _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") if_match = prep_if_match(etag, match_condition) if if_match is not None: _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") @@ -632,61 +747,60 @@ def build_azure_app_configuration_check_snapshot_request( # pylint: disable=nam if if_none_match is not None: _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_get_labels_request( # pylint: disable=name-too-long +def build_azure_app_configuration_check_feature_flag_revisions_request( # pylint: disable=name-too-long *, name: Optional[str] = None, - sync_token: Optional[str] = None, + label: Optional[str] = None, after: Optional[str] = None, - accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.LabelFields]]] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.labelset+json, application/problem+json") - + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL - _url = "/labels" + _url = "/ff-revisions" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") if name is not None: _params["name"] = _SERIALIZER.query("name", name, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") if after is not None: _params["After"] = _SERIALIZER.query("after", after, "str") if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers - if sync_token is not None: - _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") - if accept_datetime is not None: - _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") - _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") - return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_check_labels_request( # pylint: disable=name-too-long +def build_azure_app_configuration_get_snapshots_request( # pylint: disable=name-too-long *, name: Optional[str] = None, - sync_token: Optional[str] = None, after: Optional[str] = None, - accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.LabelFields]]] = None, + select: Optional[list[Union[str, _models.SnapshotFields]]] = None, + status: Optional[list[Union[str, _models.SnapshotStatus]]] = None, + sync_token: Optional[str] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.snapshotset+json, application/problem+json") + # Construct URL - _url = "/labels" + _url = "/snapshots" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") @@ -696,62 +810,43 @@ def build_azure_app_configuration_check_labels_request( # pylint: disable=name- _params["After"] = _SERIALIZER.query("after", after, "str") if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if status is not None: + _params["status"] = _SERIALIZER.query("status", status, "[str]", div=",") # Construct headers if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") - if accept_datetime is not None: - _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") - return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_put_lock_request( # pylint: disable=name-too-long - key: str, - *, - label: Optional[str] = None, - sync_token: Optional[str] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any +def build_azure_app_configuration_check_snapshots_request( # pylint: disable=name-too-long + *, sync_token: Optional[str] = None, after: Optional[str] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") - + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL - _url = "/locks/{key}" - path_format_arguments = { - "key": _SERIALIZER.url("key", key, "str"), - } - - _url: str = _url.format(**path_format_arguments) # type: ignore + _url = "/snapshots" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if label is not None: - _params["label"] = _SERIALIZER.query("label", label, "str") + if after is not None: + _params["After"] = _SERIALIZER.query("after", after, "str") # Construct headers if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") - _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") - if_match = prep_if_match(etag, match_condition) - if if_match is not None: - _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") - if_none_match = prep_if_none_match(etag, match_condition) - if if_none_match is not None: - _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="PUT", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_delete_lock_request( # pylint: disable=name-too-long - key: str, +def build_azure_app_configuration_get_snapshot_request( # pylint: disable=name-too-long + name: str, *, - label: Optional[str] = None, + select: Optional[list[Union[str, _models.SnapshotFields]]] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, @@ -760,21 +855,21 @@ def build_azure_app_configuration_delete_lock_request( # pylint: disable=name-t _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.snapshot+json, application/problem+json") # Construct URL - _url = "/locks/{key}" + _url = "/snapshots/{name}" path_format_arguments = { - "key": _SERIALIZER.url("key", key, "str"), + "name": _SERIALIZER.url("name", name, "str"), } _url: str = _url.format(**path_format_arguments) # type: ignore # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if label is not None: - _params["label"] = _SERIALIZER.query("label", label, "str") + if select is not None: + _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") # Construct headers if sync_token is not None: @@ -787,41 +882,169 @@ def build_azure_app_configuration_delete_lock_request( # pylint: disable=name-t if if_none_match is not None: _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="DELETE", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_get_revisions_request( # pylint: disable=name-too-long +def build_azure_app_configuration_get_operation_details_request( # pylint: disable=name-too-long + *, snapshot: str, **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/operations" + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + _params["snapshot"] = _SERIALIZER.query("snapshot", snapshot, "str") + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_azure_app_configuration_create_snapshot_request( # pylint: disable=name-too-long + name: str, *, sync_token: Optional[str] = None, **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.snapshot+json, application/problem+json") + + # Construct URL + _url = "/snapshots/{name}" + path_format_arguments = { + "name": _SERIALIZER.url("name", name, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="PUT", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_azure_app_configuration_update_snapshot_request( # pylint: disable=name-too-long + name: str, *, - key: Optional[str] = None, - label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.snapshot+json, application/problem+json") + + # Construct URL + _url = "/snapshots/{name}" + path_format_arguments = { + "name": _SERIALIZER.url("name", name, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + + return HttpRequest(method="PATCH", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_azure_app_configuration_check_snapshot_request( # pylint: disable=name-too-long + name: str, + *, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + # Construct URL + _url = "/snapshots/{name}" + path_format_arguments = { + "name": _SERIALIZER.url("name", name, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + # Construct headers + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + + return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_azure_app_configuration_get_labels_request( # pylint: disable=name-too-long + *, + name: Optional[str] = None, sync_token: Optional[str] = None, after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - tags: Optional[list[str]] = None, + select: Optional[list[Union[str, _models.LabelFields]]] = None, + resource_type: Optional[str] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kvset+json, application/problem+json") + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.labelset+json, application/problem+json") # Construct URL - _url = "/revisions" + _url = "/labels" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if key is not None: - _params["key"] = _SERIALIZER.query("key", key, "str") - if label is not None: - _params["label"] = _SERIALIZER.query("label", label, "str") + if name is not None: + _params["name"] = _SERIALIZER.query("name", name, "str") if after is not None: _params["After"] = _SERIALIZER.query("after", after, "str") if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") - if tags is not None: - _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] + if resource_type is not None: + _params["resource-type"] = _SERIALIZER.query("resource_type", resource_type, "str") # Construct headers if sync_token is not None: @@ -833,36 +1056,33 @@ def build_azure_app_configuration_get_revisions_request( # pylint: disable=name return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_check_revisions_request( # pylint: disable=name-too-long +def build_azure_app_configuration_check_labels_request( # pylint: disable=name-too-long *, - key: Optional[str] = None, - label: Optional[str] = None, + name: Optional[str] = None, sync_token: Optional[str] = None, after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - tags: Optional[list[str]] = None, + select: Optional[list[Union[str, _models.LabelFields]]] = None, + resource_type: Optional[str] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-04-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL - _url = "/revisions" + _url = "/labels" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if key is not None: - _params["key"] = _SERIALIZER.query("key", key, "str") - if label is not None: - _params["label"] = _SERIALIZER.query("label", label, "str") + if name is not None: + _params["name"] = _SERIALIZER.query("name", name, "str") if after is not None: _params["After"] = _SERIALIZER.query("after", after, "str") if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") - if tags is not None: - _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] + if resource_type is not None: + _params["resource-type"] = _SERIALIZER.query("resource_type", resource_type, "str") # Construct headers if sync_token is not None: @@ -873,65 +1093,232 @@ def build_azure_app_configuration_check_revisions_request( # pylint: disable=na return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -class _AzureAppConfigurationClientOperationsMixin( # pylint: disable=too-many-public-methods - ClientMixinABC[PipelineClient[HttpRequest, HttpResponse], AzureAppConfigurationClientConfiguration] -): +def build_azure_app_configuration_put_lock_request( # pylint: disable=name-too-long + key: str, + *, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - @distributed_trace - def get_keys( - self, - *, - name: Optional[str] = None, - after: Optional[str] = None, - sync_token: Optional[str] = None, - accept_datetime: Optional[str] = None, - **kwargs: Any - ) -> ItemPaged["_models.Key"]: - """Gets a list of keys. + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") - Gets a list of keys. + # Construct URL + _url = "/locks/{key}" + path_format_arguments = { + "key": _SERIALIZER.url("key", key, "str"), + } - :keyword name: A filter for the name of the returned keys. Default value is None. - :paramtype name: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str - :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is - None. - :paramtype sync_token: str - :keyword accept_datetime: Requests the server to respond with the state of the resource at the - specified - time. Default value is None. - :paramtype accept_datetime: str - :return: An iterator like instance of Key - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.Key] - :raises ~azure.core.exceptions.HttpResponseError: - """ - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} + _url: str = _url.format(**path_format_arguments) # type: ignore - cls: ClsType[list[_models.Key]] = kwargs.pop("cls", None) + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) + # Construct headers + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - def prepare_request(next_link=None): - if not next_link: + return HttpRequest(method="PUT", url=_url, params=_params, headers=_headers, **kwargs) - _request = build_azure_app_configuration_get_keys_request( - name=name, - after=after, - sync_token=sync_token, - accept_datetime=accept_datetime, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) + +def build_azure_app_configuration_delete_lock_request( # pylint: disable=name-too-long + key: str, + *, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") + + # Construct URL + _url = "/locks/{key}" + path_format_arguments = { + "key": _SERIALIZER.url("key", key, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") + + # Construct headers + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + + return HttpRequest(method="DELETE", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_azure_app_configuration_get_revisions_request( # pylint: disable=name-too-long + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + tags: Optional[list[str]] = None, + **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kvset+json, application/problem+json") + + # Construct URL + _url = "/revisions" + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if key is not None: + _params["key"] = _SERIALIZER.query("key", key, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") + if after is not None: + _params["After"] = _SERIALIZER.query("after", after, "str") + if select is not None: + _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] + + # Construct headers + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_azure_app_configuration_check_revisions_request( # pylint: disable=name-too-long + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + tags: Optional[list[str]] = None, + **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + # Construct URL + _url = "/revisions" + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if key is not None: + _params["key"] = _SERIALIZER.query("key", key, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") + if after is not None: + _params["After"] = _SERIALIZER.query("after", after, "str") + if select is not None: + _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] + + # Construct headers + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + + return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) + + +class _AzureAppConfigurationClientOperationsMixin( # pylint: disable=too-many-public-methods + ClientMixinABC[PipelineClient[HttpRequest, HttpResponse], AzureAppConfigurationClientConfiguration] +): + + @distributed_trace + def get_keys( + self, + *, + name: Optional[str] = None, + after: Optional[str] = None, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + **kwargs: Any + ) -> ItemPaged["_models.Key"]: + """Gets a list of keys. + + Gets a list of keys. + + :keyword name: A filter for the name of the returned keys. Default value is None. + :paramtype name: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :return: An iterator like instance of Key + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.Key] + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[list[_models.Key]] = kwargs.pop("cls", None) + + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + def prepare_request(next_link=None): + if not next_link: + + _request = build_azure_app_configuration_get_keys_request( + name=name, + after=after, + sync_token=sync_token, + accept_datetime=accept_datetime, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) path_format_arguments = { "endpoint": self._serialize.url( "self._config.endpoint", self._config.endpoint, "str", skip_quote=True @@ -970,50 +1357,800 @@ def extract_data(pipeline_response): list[_models.Key], deserialized.get("items", []), ) - if cls: - list_of_elem = cls(list_of_elem) # type: ignore - return deserialized.get("@nextLink") or None, iter(list_of_elem) + if cls: + list_of_elem = cls(list_of_elem) # type: ignore + return deserialized.get("@nextLink") or None, iter(list_of_elem) + + def get_next(next_link=None): + _request = prepare_request(next_link) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + return pipeline_response + + return ItemPaged(get_next, extract_data) + + @distributed_trace + def check_keys( + self, + *, + name: Optional[str] = None, + after: Optional[str] = None, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + **kwargs: Any + ) -> bool: + """Requests the headers and status of the given resource. + + Requests the headers and status of the given resource. + + :keyword name: A filter for the name of the returned keys. Default value is None. + :paramtype name: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :return: bool + :rtype: bool + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_check_keys_request( + name=name, + after=after, + sync_token=sync_token, + accept_datetime=accept_datetime, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + + if cls: + return cls(pipeline_response, None, response_headers) # type: ignore + return 200 <= response.status_code <= 299 + + @distributed_trace + def get_key_values( + self, + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> ItemPaged["_models.KeyValue"]: + """Gets a list of key-values. + + Gets a list of key-values. + + :keyword key: A filter used to match keys. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype key: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of + the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. + :paramtype snapshot: str + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: An iterator like instance of KeyValue + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.KeyValue] + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[list[_models.KeyValue]] = kwargs.pop("cls", None) + + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + def prepare_request(next_link=None): + if not next_link: + + _request = build_azure_app_configuration_get_key_values_request( + key=key, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + snapshot=snapshot, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + else: + # make call to next link with the client's api-version + _parsed_next_link = urllib.parse.urlparse(next_link) + _next_request_params = case_insensitive_dict( + { + key: [urllib.parse.quote(v) for v in value] + for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() + } + ) + _next_request_params["api-version"] = self._config.api_version + _request = HttpRequest( + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + return _request + + def extract_data(pipeline_response): + deserialized = pipeline_response.http_response.json() + list_of_elem = _deserialize( + list[_models.KeyValue], + deserialized.get("items", []), + ) + if cls: + list_of_elem = cls(list_of_elem) # type: ignore + return deserialized.get("@nextLink") or None, iter(list_of_elem) + + def get_next(next_link=None): + _request = prepare_request(next_link) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + return pipeline_response + + return ItemPaged(get_next, extract_data) + + @distributed_trace + def check_key_values( + self, + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> bool: + """Requests the headers and status of the given resource. + + Requests the headers and status of the given resource. + + :keyword key: A filter used to match keys. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype key: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of + the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. + :paramtype snapshot: str + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: bool + :rtype: bool + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_check_key_values_request( + key=key, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + snapshot=snapshot, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + + if cls: + return cls(pipeline_response, None, response_headers) # type: ignore + return 200 <= response.status_code <= 299 + + @distributed_trace + @api_version_validation( + params_added_on={"2024-09-01": ["tags"]}, + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], + ) + def get_key_value( + self, + key: str, + *, + label: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: + """Gets a single key-value. + + Gets a single key-value. + + :param key: The key of the key-value. Required. + :type key: str + :keyword label: The label of the key-value to retrieve. Default value is None. + :paramtype label: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: KeyValue. The KeyValue is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.KeyValue + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_get_key_value_request( + key=key, + label=label, + select=select, + sync_token=sync_token, + accept_datetime=accept_datetime, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["x-ms-request-id"] = self._deserialize("str", response.headers.get("x-ms-request-id")) + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.KeyValue, response.json()) + + if cls: + return cls(pipeline_response, deserialized, response_headers) # type: ignore + + return deserialized # type: ignore + + @overload + def _put_key_value( + self, + key: str, + entity: Optional[_models.KeyValue] = None, + *, + content_type: str, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: ... + @overload + def _put_key_value( + self, + key: str, + entity: Optional[JSON] = None, + *, + content_type: str, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: ... + @overload + def _put_key_value( + self, + key: str, + entity: Optional[IO[bytes]] = None, + *, + content_type: str, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: ... + + @distributed_trace + def _put_key_value( + self, + key: str, + entity: Optional[Union[_models.KeyValue, JSON, IO[bytes]]] = None, + *, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: + """Creates a key-value. + + Creates a key-value. + + :param key: The key of the key-value to create. Required. + :type key: str + :param entity: The key-value to create. Is one of the following types: KeyValue, JSON, + IO[bytes] Default value is None. + :type entity: ~azure.appconfiguration._generated.models.KeyValue or JSON or IO[bytes] + :keyword label: The label of the key-value to create. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: KeyValue. The KeyValue is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.KeyValue + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + content_type = content_type if entity else None + cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" if entity else None + _content = None + if isinstance(entity, (IOBase, bytes)): + _content = entity + else: + if entity is not None: + _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + else: + _content = None + + _request = build_azure_app_configuration_put_key_value_request( + key=key, + label=label, + sync_token=sync_token, + etag=etag, + match_condition=match_condition, + content_type=content_type, + api_version=self._config.api_version, + content=_content, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.KeyValue, response.json()) + + if cls: + return cls(pipeline_response, deserialized, response_headers) # type: ignore + + return deserialized # type: ignore + + @distributed_trace + def delete_key_value( + self, + key: str, + *, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> Optional[_models.KeyValue]: + """Deletes a key-value. + + Deletes a key-value. + + :param key: The key of the key-value to delete. Required. + :type key: str + :keyword label: The label of the key-value to delete. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: KeyValue or None. The KeyValue is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.KeyValue or None + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[Optional[_models.KeyValue]] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_delete_key_value_request( + key=key, + label=label, + sync_token=sync_token, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 204]: + if _stream: + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) - def get_next(next_link=None): - _request = prepare_request(next_link) + deserialized = None + response_headers = {} + if response.status_code == 200: + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) - _stream = False - pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - response = pipeline_response.http_response + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.KeyValue, response.json()) - if response.status_code not in [200]: - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.Error, - response, - ) - raise HttpResponseError(response=response, model=error) + if response.status_code == 204: + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) - return pipeline_response + if cls: + return cls(pipeline_response, deserialized, response_headers) # type: ignore - return ItemPaged(get_next, extract_data) + return deserialized # type: ignore @distributed_trace - def check_keys( + @api_version_validation( + params_added_on={"2024-09-01": ["tags"]}, + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], + ) + def check_key_value( self, + key: str, *, - name: Optional[str] = None, - after: Optional[str] = None, + label: Optional[str] = None, sync_token: Optional[str] = None, accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> bool: """Requests the headers and status of the given resource. Requests the headers and status of the given resource. - :keyword name: A filter for the name of the returned keys. Default value is None. - :paramtype name: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str + :param key: The key of the key-value to retrieve. Required. + :type key: str + :keyword label: The label of the key-value to retrieve. Default value is None. + :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str @@ -1021,6 +2158,18 @@ def check_keys( specified time. Default value is None. :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions :return: bool :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: @@ -1031,6 +2180,12 @@ def check_keys( 409: ResourceExistsError, 304: ResourceNotModifiedError, } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) _headers = kwargs.pop("headers", {}) or {} @@ -1038,11 +2193,15 @@ def check_keys( cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_keys_request( - name=name, - after=after, + _request = build_azure_app_configuration_check_key_value_request( + key=key, + label=label, sync_token=sync_token, accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, api_version=self._config.api_version, headers=_headers, params=_params, @@ -1069,35 +2228,52 @@ def check_keys( response_headers = {} response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) if cls: return cls(pipeline_response, None, response_headers) # type: ignore return 200 <= response.status_code <= 299 @distributed_trace - def get_key_values( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "after", + "accept_datetime", + "select", + "tags", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def get_feature_flags( self, *, - key: Optional[str] = None, + name: Optional[str] = None, label: Optional[str] = None, sync_token: Optional[str] = None, after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - snapshot: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> ItemPaged["_models.KeyValue"]: - """Gets a list of key-values. + ) -> ItemPaged["_models.FeatureFlag"]: + """Gets a list of feature flags. - Gets a list of key-values. + Gets a list of feature flags. - :keyword key: A filter used to match keys. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype key: str + :keyword name: A filter used to match feature flag names. Default value is None. + :paramtype name: str :keyword label: A filter used to match labels. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -1114,10 +2290,7 @@ def get_key_values( :paramtype accept_datetime: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] - :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of - the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. - :paramtype snapshot: str + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -1127,14 +2300,14 @@ def get_key_values( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: An iterator like instance of KeyValue - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.KeyValue] + :return: An iterator like instance of FeatureFlag + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.FeatureFlag] :raises ~azure.core.exceptions.HttpResponseError: """ _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[list[_models.KeyValue]] = kwargs.pop("cls", None) + cls: ClsType[list[_models.FeatureFlag]] = kwargs.pop("cls", None) error_map: MutableMapping = { 401: ClientAuthenticationError, @@ -1153,14 +2326,13 @@ def get_key_values( def prepare_request(next_link=None): if not next_link: - _request = build_azure_app_configuration_get_key_values_request( - key=key, + _request = build_azure_app_configuration_get_feature_flags_request( + name=name, label=label, sync_token=sync_token, after=after, accept_datetime=accept_datetime, select=select, - snapshot=snapshot, tags=tags, etag=etag, match_condition=match_condition, @@ -1203,7 +2375,7 @@ def prepare_request(next_link=None): def extract_data(pipeline_response): deserialized = pipeline_response.http_response.json() list_of_elem = _deserialize( - list[_models.KeyValue], + list[_models.FeatureFlag], deserialized.get("items", []), ) if cls: @@ -1232,16 +2404,34 @@ def get_next(next_link=None): return ItemPaged(get_next, extract_data) @distributed_trace - def check_key_values( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "after", + "accept_datetime", + "select", + "tags", + "client_request_id", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def check_feature_flags( self, *, - key: Optional[str] = None, + name: Optional[str] = None, label: Optional[str] = None, sync_token: Optional[str] = None, after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - snapshot: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, @@ -1251,10 +2441,8 @@ def check_key_values( Requests the headers and status of the given resource. - :keyword key: A filter used to match keys. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype key: str + :keyword name: A filter used to match feature flag names. Default value is None. + :paramtype name: str :keyword label: A filter used to match labels. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -1271,10 +2459,7 @@ def check_key_values( :paramtype accept_datetime: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] - :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of - the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. - :paramtype snapshot: str + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -1307,14 +2492,13 @@ def check_key_values( cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_key_values_request( - key=key, + _request = build_azure_app_configuration_check_feature_flags_request( + name=name, label=label, sync_token=sync_token, after=after, accept_datetime=accept_datetime, select=select, - snapshot=snapshot, tags=tags, etag=etag, match_condition=match_condition, @@ -1352,33 +2536,48 @@ def check_key_values( @distributed_trace @api_version_validation( - params_added_on={"2024-09-01": ["tags"]}, - api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01"], + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "select", + "sync_token", + "accept_datetime", + "client_request_id", + "tags", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], ) - def get_key_value( + def get_feature_flag( self, - key: str, + name: str, *, label: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, sync_token: Optional[str] = None, accept_datetime: Optional[str] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.KeyValue: - """Gets a single key-value. + ) -> _models.FeatureFlag: + """Gets a single feature flag. - Gets a single key-value. + Gets a single feature flag. - :param key: The key of the key-value. Required. - :type key: str - :keyword label: The label of the key-value to retrieve. Default value is None. + :param name: The name of the feature flag. Required. + :type name: str + :keyword label: The label of the feature flag to retrieve. Default value is None. :paramtype label: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str @@ -1395,8 +2594,8 @@ def get_key_value( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: KeyValue. The KeyValue is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.KeyValue + :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -1416,10 +2615,10 @@ def get_key_value( _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) + cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_get_key_value_request( - key=key, + _request = build_azure_app_configuration_get_feature_flag_request( + name=name, label=label, select=select, sync_token=sync_token, @@ -1466,75 +2665,174 @@ def get_key_value( if _stream: deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.KeyValue, response.json()) + deserialized = _deserialize(_models.FeatureFlag, response.json()) if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore return deserialized # type: ignore - @overload - def _put_key_value( - self, - key: str, - entity: Optional[_models.KeyValue] = None, - *, - content_type: str, - label: Optional[str] = None, - sync_token: Optional[str] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> _models.KeyValue: ... - @overload - def _put_key_value( - self, - key: str, - entity: Optional[JSON] = None, - *, - content_type: str, - label: Optional[str] = None, - sync_token: Optional[str] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> _models.KeyValue: ... - @overload - def _put_key_value( + @distributed_trace + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "accept_datetime", + "select", + "tags", + "client_request_id", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def check_feature_flag( self, - key: str, - entity: Optional[IO[bytes]] = None, + name: str, *, - content_type: str, label: Optional[str] = None, sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.KeyValue: ... + ) -> bool: + """Requests the headers and status of the given resource. + + Requests the headers and status of the given resource. + + :param name: The name of the feature flag to retrieve. Required. + :type name: str + :keyword label: The label of the feature flag to retrieve. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: bool + :rtype: bool + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_check_feature_flag_request( + name=name, + label=label, + sync_token=sync_token, + accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + + if cls: + return cls(pipeline_response, None, response_headers) # type: ignore + return 200 <= response.status_code <= 299 @distributed_trace - def _put_key_value( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "content_type", + "name", + "label", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def put_feature_flag( self, - key: str, - entity: Optional[Union[_models.KeyValue, JSON, IO[bytes]]] = None, + name: str, + entity: Optional[_models.FeatureFlag] = None, *, label: Optional[str] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.KeyValue: - """Creates a key-value. + ) -> _models.FeatureFlag: + """Creates a feature flag. - Creates a key-value. + Creates a feature flag. - :param key: The key of the key-value to create. Required. - :type key: str - :param entity: The key-value to create. Is one of the following types: KeyValue, JSON, - IO[bytes] Default value is None. - :type entity: ~azure.appconfiguration._generated.models.KeyValue or JSON or IO[bytes] - :keyword label: The label of the key-value to create. Default value is None. + :param name: The name of the feature flag to create. Required. + :type name: str + :param entity: The feature flag to create. Default value is None. + :type entity: ~azure.appconfiguration._generated.models.FeatureFlag + :keyword label: The label of the feature flag to create. Default value is None. :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. @@ -1544,8 +2842,8 @@ def _put_key_value( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: KeyValue. The KeyValue is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.KeyValue + :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -1565,22 +2863,22 @@ def _put_key_value( _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = kwargs.pop("params", {}) or {} - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + content_type: Optional[str] = kwargs.pop( + "content_type", + _headers.pop( + "Content-Type", 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8' + ), + ) content_type = content_type if entity else None - cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) + cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) - content_type = content_type or "application/json" if entity else None - _content = None - if isinstance(entity, (IOBase, bytes)): - _content = entity + if entity is not None: + _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore else: - if entity is not None: - _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - else: - _content = None + _content = None - _request = build_azure_app_configuration_put_key_value_request( - key=key, + _request = build_azure_app_configuration_put_feature_flag_request( + name=name, label=label, sync_token=sync_token, etag=etag, @@ -1625,7 +2923,7 @@ def _put_key_value( if _stream: deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.KeyValue, response.json()) + deserialized = _deserialize(_models.FeatureFlag, response.json()) if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore @@ -1633,23 +2931,39 @@ def _put_key_value( return deserialized # type: ignore @distributed_trace - def delete_key_value( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def delete_feature_flag( self, - key: str, + name: str, *, label: Optional[str] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> Optional[_models.KeyValue]: - """Deletes a key-value. + ) -> Optional[_models.FeatureFlag]: + """Deletes a feature flag. - Deletes a key-value. + Deletes a feature flag. - :param key: The key of the key-value to delete. Required. - :type key: str - :keyword label: The label of the key-value to delete. Default value is None. + :param name: The name of the feature flag to delete. Required. + :type name: str + :keyword label: The label of the feature flag to delete. Default value is None. :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. @@ -1659,8 +2973,8 @@ def delete_key_value( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: KeyValue or None. The KeyValue is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.KeyValue or None + :return: FeatureFlag or None. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag or None :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -1680,10 +2994,10 @@ def delete_key_value( _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[Optional[_models.KeyValue]] = kwargs.pop("cls", None) + cls: ClsType[Optional[_models.FeatureFlag]] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_delete_key_value_request( - key=key, + _request = build_azure_app_configuration_delete_feature_flag_request( + name=name, label=label, sync_token=sync_token, etag=etag, @@ -1728,7 +3042,7 @@ def delete_key_value( if _stream: deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.KeyValue, response.json()) + deserialized = _deserialize(_models.FeatureFlag, response.json()) if response.status_code == 204: response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) @@ -1740,53 +3054,74 @@ def delete_key_value( @distributed_trace @api_version_validation( - params_added_on={"2024-09-01": ["tags"]}, - api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01"], + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "after", + "select", + "tags", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], ) - def check_key_value( + def get_feature_flag_revisions( self, - key: str, *, + name: Optional[str] = None, label: Optional[str] = None, - sync_token: Optional[str] = None, - accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + after: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, + sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> bool: - """Requests the headers and status of the given resource. + ) -> ItemPaged["_models.FeatureFlag"]: + """Gets a list of feature flag revisions. - Requests the headers and status of the given resource. + Gets a list of feature flag revisions. - :param key: The key of the key-value to retrieve. Required. - :type key: str - :keyword label: The label of the key-value to retrieve. Default value is None. + :keyword name: A filter used to match names. Default value is None. + :paramtype name: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. :paramtype label: str - :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is - None. - :paramtype sync_token: str - :keyword accept_datetime: Requests the server to respond with the state of the resource at the - specified - time. Default value is None. - :paramtype accept_datetime: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. :paramtype tags: list[str] + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is None. :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: bool - :rtype: bool + :return: An iterator like instance of FeatureFlag + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.FeatureFlag] :raises ~azure.core.exceptions.HttpResponseError: """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[list[_models.FeatureFlag]] = kwargs.pop("cls", None) + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, @@ -1801,20 +3136,146 @@ def check_key_value( error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) + def prepare_request(next_link=None): + if not next_link: + + _request = build_azure_app_configuration_get_feature_flag_revisions_request( + name=name, + label=label, + after=after, + select=select, + tags=tags, + sync_token=sync_token, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + else: + # make call to next link with the client's api-version + _parsed_next_link = urllib.parse.urlparse(next_link) + _next_request_params = case_insensitive_dict( + { + key: [urllib.parse.quote(v) for v in value] + for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() + } + ) + _next_request_params["api-version"] = self._config.api_version + _request = HttpRequest( + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + return _request + + def extract_data(pipeline_response): + deserialized = pipeline_response.http_response.json() + list_of_elem = _deserialize( + list[_models.FeatureFlag], + deserialized.get("items", []), + ) + if cls: + list_of_elem = cls(list_of_elem) # type: ignore + return deserialized.get("@nextLink") or None, iter(list_of_elem) + + def get_next(next_link=None): + _request = prepare_request(next_link) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + return pipeline_response + + return ItemPaged(get_next, extract_data) + + @distributed_trace + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": ["api_version", "name", "label", "after", "select", "tags", "client_request_id"] + }, + api_versions_list=["2026-05-01-preview"], + ) + def check_feature_flag_revisions( + self, + *, + name: Optional[str] = None, + label: Optional[str] = None, + after: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + **kwargs: Any + ) -> bool: + """Requests the headers and status of the given resource. + + Requests the headers and status of the given resource. + + :keyword name: A filter used to match names. Default value is None. + :paramtype name: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. + :paramtype label: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. + :paramtype tags: list[str] + :return: bool + :rtype: bool + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_key_value_request( - key=key, + _request = build_azure_app_configuration_check_feature_flag_revisions_request( + name=name, label=label, - sync_token=sync_token, - accept_datetime=accept_datetime, + after=after, select=select, tags=tags, - etag=etag, - match_condition=match_condition, api_version=self._config.api_version, headers=_headers, params=_params, @@ -2676,6 +4137,10 @@ def check_snapshot( return 200 <= response.status_code <= 299 @distributed_trace + @api_version_validation( + params_added_on={"2026-05-01-preview": ["resource_type"]}, + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], + ) def get_labels( self, *, @@ -2684,6 +4149,7 @@ def get_labels( after: Optional[str] = None, accept_datetime: Optional[str] = None, select: Optional[list[Union[str, _models.LabelFields]]] = None, + resource_type: Optional[str] = None, **kwargs: Any ) -> ItemPaged["_models.Label"]: """Gets a list of labels. @@ -2705,6 +4171,9 @@ def get_labels( :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. :paramtype select: list[str or ~azure.appconfiguration.models.LabelFields] + :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted + values: 'kv' for key-value labels, 'ff' for feature flag labels. Default value is None. + :paramtype resource_type: str :return: An iterator like instance of Label :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.Label] :raises ~azure.core.exceptions.HttpResponseError: @@ -2731,6 +4200,7 @@ def prepare_request(next_link=None): after=after, accept_datetime=accept_datetime, select=select, + resource_type=resource_type, api_version=self._config.api_version, headers=_headers, params=_params, @@ -2799,6 +4269,10 @@ def get_next(next_link=None): return ItemPaged(get_next, extract_data) @distributed_trace + @api_version_validation( + params_added_on={"2026-05-01-preview": ["resource_type"]}, + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], + ) def check_labels( self, *, @@ -2807,6 +4281,7 @@ def check_labels( after: Optional[str] = None, accept_datetime: Optional[str] = None, select: Optional[list[Union[str, _models.LabelFields]]] = None, + resource_type: Optional[str] = None, **kwargs: Any ) -> bool: """Requests the headers and status of the given resource. @@ -2828,6 +4303,9 @@ def check_labels( :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. :paramtype select: list[str or ~azure.appconfiguration.models.LabelFields] + :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted + values: 'kv' for key-value labels, 'ff' for feature flag labels. Default value is None. + :paramtype resource_type: str :return: bool :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: @@ -2851,6 +4329,7 @@ def check_labels( after=after, accept_datetime=accept_datetime, select=select, + resource_type=resource_type, api_version=self._config.api_version, headers=_headers, params=_params, diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_client.py index 08136942332c..f24f87ec8a9b 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_client.py @@ -37,9 +37,10 @@ class AzureAppConfigurationClient(_AzureAppConfigurationClientOperationsMixin): credential type or a token credential type. Required. :type credential: ~azure.core.credentials.AzureKeyCredential or ~azure.core.credentials_async.AsyncTokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2026-04-01" - and None. Default value is None. If not set, the operation's default API version will be used. - Note that overriding this default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-05-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str :keyword int polling_interval: Default waiting time between two polls for LRO operations if no Retry-After header is present. diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_configuration.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_configuration.py index 8d883e6ff8e0..831a184df162 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_configuration.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_configuration.py @@ -29,16 +29,17 @@ class AzureAppConfigurationClientConfiguration: # pylint: disable=too-many-inst credential type or a token credential type. Required. :type credential: ~azure.core.credentials.AzureKeyCredential or ~azure.core.credentials_async.AsyncTokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2026-04-01" - and None. Default value is None. If not set, the operation's default API version will be used. - Note that overriding this default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-05-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ def __init__( self, endpoint: str, credential: Union[AzureKeyCredential, "AsyncTokenCredential"], **kwargs: Any ) -> None: - api_version: str = kwargs.pop("api_version", "2026-04-01") + api_version: str = kwargs.pop("api_version", "2026-05-01-preview") if endpoint is None: raise ValueError("Parameter 'endpoint' must not be None.") diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_operations/_operations.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_operations/_operations.py index cf4aca4f9514..9aee4704a00c 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_operations/_operations.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_operations/_operations.py @@ -35,6 +35,9 @@ from ... import models as _models from ..._operations._operations import ( + build_azure_app_configuration_check_feature_flag_request, + build_azure_app_configuration_check_feature_flag_revisions_request, + build_azure_app_configuration_check_feature_flags_request, build_azure_app_configuration_check_key_value_request, build_azure_app_configuration_check_key_values_request, build_azure_app_configuration_check_keys_request, @@ -43,8 +46,12 @@ build_azure_app_configuration_check_snapshot_request, build_azure_app_configuration_check_snapshots_request, build_azure_app_configuration_create_snapshot_request, + build_azure_app_configuration_delete_feature_flag_request, build_azure_app_configuration_delete_key_value_request, build_azure_app_configuration_delete_lock_request, + build_azure_app_configuration_get_feature_flag_request, + build_azure_app_configuration_get_feature_flag_revisions_request, + build_azure_app_configuration_get_feature_flags_request, build_azure_app_configuration_get_key_value_request, build_azure_app_configuration_get_key_values_request, build_azure_app_configuration_get_keys_request, @@ -53,6 +60,7 @@ build_azure_app_configuration_get_revisions_request, build_azure_app_configuration_get_snapshot_request, build_azure_app_configuration_get_snapshots_request, + build_azure_app_configuration_put_feature_flag_request, build_azure_app_configuration_put_key_value_request, build_azure_app_configuration_put_lock_request, build_azure_app_configuration_update_snapshot_request, @@ -548,7 +556,7 @@ async def check_key_values( @distributed_trace_async @api_version_validation( params_added_on={"2024-09-01": ["tags"]}, - api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01"], + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], ) async def get_key_value( self, @@ -936,7 +944,7 @@ async def delete_key_value( @distributed_trace_async @api_version_validation( params_added_on={"2024-09-01": ["tags"]}, - api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01"], + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], ) async def check_key_value( self, @@ -1042,6 +1050,1082 @@ async def check_key_value( return cls(pipeline_response, None, response_headers) # type: ignore return 200 <= response.status_code <= 299 + @distributed_trace + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "after", + "accept_datetime", + "select", + "tags", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def get_feature_flags( + self, + *, + name: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> AsyncItemPaged["_models.FeatureFlag"]: + """Gets a list of feature flags. + + Gets a list of feature flags. + + :keyword name: A filter used to match feature flag names. Default value is None. + :paramtype name: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: An iterator like instance of FeatureFlag + :rtype: + ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.FeatureFlag] + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[list[_models.FeatureFlag]] = kwargs.pop("cls", None) + + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + def prepare_request(next_link=None): + if not next_link: + + _request = build_azure_app_configuration_get_feature_flags_request( + name=name, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + else: + # make call to next link with the client's api-version + _parsed_next_link = urllib.parse.urlparse(next_link) + _next_request_params = case_insensitive_dict( + { + key: [urllib.parse.quote(v) for v in value] + for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() + } + ) + _next_request_params["api-version"] = self._config.api_version + _request = HttpRequest( + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + return _request + + async def extract_data(pipeline_response): + deserialized = pipeline_response.http_response.json() + list_of_elem = _deserialize( + list[_models.FeatureFlag], + deserialized.get("items", []), + ) + if cls: + list_of_elem = cls(list_of_elem) # type: ignore + return deserialized.get("@nextLink") or None, AsyncList(list_of_elem) + + async def get_next(next_link=None): + _request = prepare_request(next_link) + + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + return pipeline_response + + return AsyncItemPaged(get_next, extract_data) + + @distributed_trace_async + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "after", + "accept_datetime", + "select", + "tags", + "client_request_id", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def check_feature_flags( + self, + *, + name: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> bool: + """Requests the headers and status of the given resource. + + Requests the headers and status of the given resource. + + :keyword name: A filter used to match feature flag names. Default value is None. + :paramtype name: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: bool + :rtype: bool + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_check_feature_flags_request( + name=name, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + + if cls: + return cls(pipeline_response, None, response_headers) # type: ignore + return 200 <= response.status_code <= 299 + + @distributed_trace_async + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "select", + "sync_token", + "accept_datetime", + "client_request_id", + "tags", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def get_feature_flag( + self, + name: str, + *, + label: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.FeatureFlag: + """Gets a single feature flag. + + Gets a single feature flag. + + :param name: The name of the feature flag. Required. + :type name: str + :keyword label: The label of the feature flag to retrieve. Default value is None. + :paramtype label: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_get_feature_flag_request( + name=name, + label=label, + select=select, + sync_token=sync_token, + accept_datetime=accept_datetime, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["x-ms-request-id"] = self._deserialize("str", response.headers.get("x-ms-request-id")) + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.FeatureFlag, response.json()) + + if cls: + return cls(pipeline_response, deserialized, response_headers) # type: ignore + + return deserialized # type: ignore + + @distributed_trace_async + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "accept_datetime", + "select", + "tags", + "client_request_id", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def check_feature_flag( + self, + name: str, + *, + label: Optional[str] = None, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> bool: + """Requests the headers and status of the given resource. + + Requests the headers and status of the given resource. + + :param name: The name of the feature flag to retrieve. Required. + :type name: str + :keyword label: The label of the feature flag to retrieve. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: bool + :rtype: bool + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_check_feature_flag_request( + name=name, + label=label, + sync_token=sync_token, + accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + + if cls: + return cls(pipeline_response, None, response_headers) # type: ignore + return 200 <= response.status_code <= 299 + + @distributed_trace_async + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "content_type", + "name", + "label", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def put_feature_flag( + self, + name: str, + entity: Optional[_models.FeatureFlag] = None, + *, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.FeatureFlag: + """Creates a feature flag. + + Creates a feature flag. + + :param name: The name of the feature flag to create. Required. + :type name: str + :param entity: The feature flag to create. Default value is None. + :type entity: ~azure.appconfiguration._generated.models.FeatureFlag + :keyword label: The label of the feature flag to create. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", + _headers.pop( + "Content-Type", 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8' + ), + ) + content_type = content_type if entity else None + cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) + + if entity is not None: + _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + else: + _content = None + + _request = build_azure_app_configuration_put_feature_flag_request( + name=name, + label=label, + sync_token=sync_token, + etag=etag, + match_condition=match_condition, + content_type=content_type, + api_version=self._config.api_version, + content=_content, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.FeatureFlag, response.json()) + + if cls: + return cls(pipeline_response, deserialized, response_headers) # type: ignore + + return deserialized # type: ignore + + @distributed_trace_async + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def delete_feature_flag( + self, + name: str, + *, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> Optional[_models.FeatureFlag]: + """Deletes a feature flag. + + Deletes a feature flag. + + :param name: The name of the feature flag to delete. Required. + :type name: str + :keyword label: The label of the feature flag to delete. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: FeatureFlag or None. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag or None + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[Optional[_models.FeatureFlag]] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_delete_feature_flag_request( + name=name, + label=label, + sync_token=sync_token, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 204]: + if _stream: + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + deserialized = None + response_headers = {} + if response.status_code == 200: + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.FeatureFlag, response.json()) + + if response.status_code == 204: + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + + if cls: + return cls(pipeline_response, deserialized, response_headers) # type: ignore + + return deserialized # type: ignore + + @distributed_trace + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "after", + "select", + "tags", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def get_feature_flag_revisions( + self, + *, + name: Optional[str] = None, + label: Optional[str] = None, + after: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> AsyncItemPaged["_models.FeatureFlag"]: + """Gets a list of feature flag revisions. + + Gets a list of feature flag revisions. + + :keyword name: A filter used to match names. Default value is None. + :paramtype name: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. + :paramtype label: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. + :paramtype tags: list[str] + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: An iterator like instance of FeatureFlag + :rtype: + ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.FeatureFlag] + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[list[_models.FeatureFlag]] = kwargs.pop("cls", None) + + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + def prepare_request(next_link=None): + if not next_link: + + _request = build_azure_app_configuration_get_feature_flag_revisions_request( + name=name, + label=label, + after=after, + select=select, + tags=tags, + sync_token=sync_token, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + else: + # make call to next link with the client's api-version + _parsed_next_link = urllib.parse.urlparse(next_link) + _next_request_params = case_insensitive_dict( + { + key: [urllib.parse.quote(v) for v in value] + for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() + } + ) + _next_request_params["api-version"] = self._config.api_version + _request = HttpRequest( + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + return _request + + async def extract_data(pipeline_response): + deserialized = pipeline_response.http_response.json() + list_of_elem = _deserialize( + list[_models.FeatureFlag], + deserialized.get("items", []), + ) + if cls: + list_of_elem = cls(list_of_elem) # type: ignore + return deserialized.get("@nextLink") or None, AsyncList(list_of_elem) + + async def get_next(next_link=None): + _request = prepare_request(next_link) + + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + return pipeline_response + + return AsyncItemPaged(get_next, extract_data) + + @distributed_trace_async + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": ["api_version", "name", "label", "after", "select", "tags", "client_request_id"] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def check_feature_flag_revisions( + self, + *, + name: Optional[str] = None, + label: Optional[str] = None, + after: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + **kwargs: Any + ) -> bool: + """Requests the headers and status of the given resource. + + Requests the headers and status of the given resource. + + :keyword name: A filter used to match names. Default value is None. + :paramtype name: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. + :paramtype label: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. + :paramtype tags: list[str] + :return: bool + :rtype: bool + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_check_feature_flag_revisions_request( + name=name, + label=label, + after=after, + select=select, + tags=tags, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + + if cls: + return cls(pipeline_response, None, response_headers) # type: ignore + return 200 <= response.status_code <= 299 + @distributed_trace def get_snapshots( self, @@ -1875,6 +2959,10 @@ async def check_snapshot( return 200 <= response.status_code <= 299 @distributed_trace + @api_version_validation( + params_added_on={"2026-05-01-preview": ["resource_type"]}, + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], + ) def get_labels( self, *, @@ -1883,6 +2971,7 @@ def get_labels( after: Optional[str] = None, accept_datetime: Optional[str] = None, select: Optional[list[Union[str, _models.LabelFields]]] = None, + resource_type: Optional[str] = None, **kwargs: Any ) -> AsyncItemPaged["_models.Label"]: """Gets a list of labels. @@ -1904,6 +2993,9 @@ def get_labels( :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. :paramtype select: list[str or ~azure.appconfiguration.models.LabelFields] + :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted + values: 'kv' for key-value labels, 'ff' for feature flag labels. Default value is None. + :paramtype resource_type: str :return: An iterator like instance of Label :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.Label] @@ -1931,6 +3023,7 @@ def prepare_request(next_link=None): after=after, accept_datetime=accept_datetime, select=select, + resource_type=resource_type, api_version=self._config.api_version, headers=_headers, params=_params, @@ -1999,6 +3092,10 @@ async def get_next(next_link=None): return AsyncItemPaged(get_next, extract_data) @distributed_trace_async + @api_version_validation( + params_added_on={"2026-05-01-preview": ["resource_type"]}, + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], + ) async def check_labels( self, *, @@ -2007,6 +3104,7 @@ async def check_labels( after: Optional[str] = None, accept_datetime: Optional[str] = None, select: Optional[list[Union[str, _models.LabelFields]]] = None, + resource_type: Optional[str] = None, **kwargs: Any ) -> bool: """Requests the headers and status of the given resource. @@ -2028,6 +3126,9 @@ async def check_labels( :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. :paramtype select: list[str or ~azure.appconfiguration.models.LabelFields] + :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted + values: 'kv' for key-value labels, 'ff' for feature flag labels. Default value is None. + :paramtype resource_type: str :return: bool :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: @@ -2051,6 +3152,7 @@ async def check_labels( after=after, accept_datetime=accept_datetime, select=select, + resource_type=resource_type, api_version=self._config.api_version, headers=_headers, params=_params, diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/__init__.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/__init__.py index a6a4cb4accbc..f0cfc287cf36 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/__init__.py @@ -15,22 +15,34 @@ from ._models import ( # type: ignore Error, + FeatureFlag, + FeatureFlagAllocation, + FeatureFlagConditions, + FeatureFlagFilter, + FeatureFlagTelemetryConfiguration, + FeatureFlagVariantDefinition, + GroupAllocation, Key, KeyValue, KeyValueFilter, Label, OperationDetails, + PercentileAllocation, Snapshot, SnapshotUpdateParameters, + UserAllocation, ) from ._enums import ( # type: ignore ConfigurationSettingFields, + FeatureFlagFields, LabelFields, OperationState, + RequirementType, SnapshotComposition, SnapshotFields, SnapshotStatus, + StatusOverride, ) from ._patch import __all__ as _patch_all from ._patch import * @@ -38,19 +50,31 @@ __all__ = [ "Error", + "FeatureFlag", + "FeatureFlagAllocation", + "FeatureFlagConditions", + "FeatureFlagFilter", + "FeatureFlagTelemetryConfiguration", + "FeatureFlagVariantDefinition", + "GroupAllocation", "Key", "KeyValue", "KeyValueFilter", "Label", "OperationDetails", + "PercentileAllocation", "Snapshot", "SnapshotUpdateParameters", + "UserAllocation", "ConfigurationSettingFields", + "FeatureFlagFields", "LabelFields", "OperationState", + "RequirementType", "SnapshotComposition", "SnapshotFields", "SnapshotStatus", + "StatusOverride", ] __all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore _patch_sdk() diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_enums.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_enums.py index dfc9dba30aa6..41ecb75c126d 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_enums.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_enums.py @@ -33,6 +33,33 @@ class ConfigurationSettingFields(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Etag field.""" +class FeatureFlagFields(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Feature Flag fields.""" + + NAME = "name" + """Name field.""" + ENABLED = "enabled" + """Enabled field.""" + LABEL = "label" + """Label field.""" + DESCRIPTION = "description" + """Description field.""" + CONDITIONS = "conditions" + """Conditions field.""" + VARIANTS = "variants" + """Variants field.""" + ALLOCATION = "allocation" + """Allocation field.""" + TELEMETRY = "telemetry" + """Telemetry field.""" + TAGS = "tags" + """Tags field.""" + LAST_MODIFIED = "last_modified" + """Last modified field.""" + ETAG = "etag" + """Etag field.""" + + class LabelFields(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Label fields.""" @@ -55,6 +82,15 @@ class OperationState(str, Enum, metaclass=CaseInsensitiveEnumMeta): """The operation has been canceled by the user.""" +class RequirementType(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Requirement Type.""" + + ANY = "Any" + """Any.""" + ALL = "All" + """All.""" + + class SnapshotComposition(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Composition types.""" @@ -104,3 +140,14 @@ class SnapshotStatus(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Archived.""" FAILED = "failed" """Failed.""" + + +class StatusOverride(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Status Override.""" + + NONE = "None" + """None.""" + ENABLED = "Enabled" + """Enabled.""" + DISABLED = "Disabled" + """Disabled.""" diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py index d51d26bb5742..20a4a799a924 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py @@ -66,6 +66,331 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) +class FeatureFlag(_Model): + """A feature flag. + + :ivar name: The name of the feature flag. Required. + :vartype name: str + :ivar enabled: The enabled state of the feature flag. + :vartype enabled: bool + :ivar label: The label the feature flag belongs to. + :vartype label: str + :ivar description: The description of the feature flag. + :vartype description: str + :ivar conditions: The conditions of the feature flag. + :vartype conditions: ~azure.appconfiguration._generated.models.FeatureFlagConditions + :ivar variants: The variants of the feature flag. + :vartype variants: list[~azure.appconfiguration._generated.models.FeatureFlagVariantDefinition] + :ivar allocation: The allocation of the feature flag. + :vartype allocation: ~azure.appconfiguration._generated.models.FeatureFlagAllocation + :ivar telemetry: The telemetry settings of the feature flag. + :vartype telemetry: ~azure.appconfiguration._generated.models.FeatureFlagTelemetryConfiguration + :ivar tags: The tags of the feature flag. + :vartype tags: dict[str, str] + :ivar last_modified: A date representing the last time the feature flag was modified. + :vartype last_modified: ~datetime.datetime + :ivar etag: A value representing the current state of the resource. + :vartype etag: str + """ + + name: str = rest_field(visibility=["read"]) + """The name of the feature flag. Required.""" + enabled: Optional[bool] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The enabled state of the feature flag.""" + label: Optional[str] = rest_field(visibility=["read"]) + """The label the feature flag belongs to.""" + description: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The description of the feature flag.""" + conditions: Optional["_models.FeatureFlagConditions"] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """The conditions of the feature flag.""" + variants: Optional[list["_models.FeatureFlagVariantDefinition"]] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """The variants of the feature flag.""" + allocation: Optional["_models.FeatureFlagAllocation"] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """The allocation of the feature flag.""" + telemetry: Optional["_models.FeatureFlagTelemetryConfiguration"] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """The telemetry settings of the feature flag.""" + tags: Optional[dict[str, str]] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The tags of the feature flag.""" + last_modified: Optional[datetime.datetime] = rest_field(visibility=["read"], format="rfc3339") + """A date representing the last time the feature flag was modified.""" + etag: Optional[str] = rest_field(visibility=["read"]) + """A value representing the current state of the resource.""" + + @overload + def __init__( + self, + *, + enabled: Optional[bool] = None, + description: Optional[str] = None, + conditions: Optional["_models.FeatureFlagConditions"] = None, + variants: Optional[list["_models.FeatureFlagVariantDefinition"]] = None, + allocation: Optional["_models.FeatureFlagAllocation"] = None, + telemetry: Optional["_models.FeatureFlagTelemetryConfiguration"] = None, + tags: Optional[dict[str, str]] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + +class FeatureFlagAllocation(_Model): + """Defines how to allocate variants based on context. + + :ivar default_when_disabled: The default variant to use when disabled. + :vartype default_when_disabled: str + :ivar default_when_enabled: The default variant to use when enabled but not allocated. + :vartype default_when_enabled: str + :ivar percentile: Allocates percentiles to variants. + :vartype percentile: list[~azure.appconfiguration._generated.models.PercentileAllocation] + :ivar user: Allocates users to variants. + :vartype user: list[~azure.appconfiguration._generated.models.UserAllocation] + :ivar group: Allocates groups to variants. + :vartype group: list[~azure.appconfiguration._generated.models.GroupAllocation] + :ivar seed: The seed used for random allocation. + :vartype seed: str + """ + + default_when_disabled: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The default variant to use when disabled.""" + default_when_enabled: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The default variant to use when enabled but not allocated.""" + percentile: Optional[list["_models.PercentileAllocation"]] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """Allocates percentiles to variants.""" + user: Optional[list["_models.UserAllocation"]] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """Allocates users to variants.""" + group: Optional[list["_models.GroupAllocation"]] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """Allocates groups to variants.""" + seed: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The seed used for random allocation.""" + + @overload + def __init__( + self, + *, + default_when_disabled: Optional[str] = None, + default_when_enabled: Optional[str] = None, + percentile: Optional[list["_models.PercentileAllocation"]] = None, + user: Optional[list["_models.UserAllocation"]] = None, + group: Optional[list["_models.GroupAllocation"]] = None, + seed: Optional[str] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + +class FeatureFlagConditions(_Model): + """The conditions that must be met for the feature flag to be enabled. + + :ivar requirement_type: The requirement type for the conditions. Known values are: "Any" and + "All". + :vartype requirement_type: str or ~azure.appconfiguration.models.RequirementType + :ivar filters: The filters that will conditionally enable or disable the flag. + :vartype filters: list[~azure.appconfiguration._generated.models.FeatureFlagFilter] + """ + + requirement_type: Optional[Union[str, "_models.RequirementType"]] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """The requirement type for the conditions. Known values are: \"Any\" and \"All\".""" + filters: Optional[list["_models.FeatureFlagFilter"]] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """The filters that will conditionally enable or disable the flag.""" + + @overload + def __init__( + self, + *, + requirement_type: Optional[Union[str, "_models.RequirementType"]] = None, + filters: Optional[list["_models.FeatureFlagFilter"]] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + +class FeatureFlagFilter(_Model): + """Feature Flag Filter object. + + :ivar name: The name of the filter. Required. + :vartype name: str + :ivar parameters: The parameters used by the filter. + :vartype parameters: dict[str, str] + """ + + name: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The name of the filter. Required.""" + parameters: Optional[dict[str, str]] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The parameters used by the filter.""" + + @overload + def __init__( + self, + *, + name: str, + parameters: Optional[dict[str, str]] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + +class FeatureFlagTelemetryConfiguration(_Model): + """Feature Flag Telemetry object. + + :ivar enabled: The enabled state of the telemetry. Required. + :vartype enabled: bool + :ivar metadata: The metadata to include on outbound telemetry. + :vartype metadata: dict[str, str] + """ + + enabled: bool = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The enabled state of the telemetry. Required.""" + metadata: Optional[dict[str, str]] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The metadata to include on outbound telemetry.""" + + @overload + def __init__( + self, + *, + enabled: bool, + metadata: Optional[dict[str, str]] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + +class FeatureFlagVariantDefinition(_Model): + """Feature Flag Variants object. + + :ivar name: The name of the variant. Required. + :vartype name: str + :ivar value: The value of the variant. + :vartype value: str + :ivar content_type: The content type of the value stored within the key-value. + :vartype content_type: str + :ivar status_override: Determines if the variant should override the status of the flag. Known + values are: "None", "Enabled", and "Disabled". + :vartype status_override: str or ~azure.appconfiguration.models.StatusOverride + """ + + name: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The name of the variant. Required.""" + value: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The value of the variant.""" + content_type: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The content type of the value stored within the key-value.""" + status_override: Optional[Union[str, "_models.StatusOverride"]] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """Determines if the variant should override the status of the flag. Known values are: \"None\", + \"Enabled\", and \"Disabled\".""" + + @overload + def __init__( + self, + *, + name: str, + value: Optional[str] = None, + content_type: Optional[str] = None, + status_override: Optional[Union[str, "_models.StatusOverride"]] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + +class GroupAllocation(_Model): + """Feature Flag GroupAllocation object. + + :ivar variant: The variant to allocate these percentiles to. Required. + :vartype variant: str + :ivar groups: The groups to get this variant. Required. + :vartype groups: list[str] + """ + + variant: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The variant to allocate these percentiles to. Required.""" + groups: list[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The groups to get this variant. Required.""" + + @overload + def __init__( + self, + *, + variant: str, + groups: list[str], + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + class Key(_Model): """Keys serve as identifiers for key-values and are used to store and retrieve corresponding values. @@ -103,7 +428,7 @@ class KeyValue(_Model): key: str = rest_field(visibility=["read"]) """The key of the key-value. Required.""" - label: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + label: Optional[str] = rest_field(visibility=["read"]) """The label the key-value belongs to.""" content_type: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) """The content type of the value stored within the key-value.""" @@ -126,7 +451,6 @@ class KeyValue(_Model): def __init__( self, *, - label: Optional[str] = None, content_type: Optional[str] = None, value: Optional[str] = None, last_modified: Optional[datetime.datetime] = None, @@ -188,7 +512,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: class Label(_Model): - """Labels are used to group key-values. + """Labels are used to group key values or feature flags. :ivar name: The name of the label. :vartype name: str @@ -258,6 +582,44 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) +class PercentileAllocation(_Model): + """Feature Flag PercentileAllocation object. + + :ivar variant: The variant to allocate these percentiles to. Required. + :vartype variant: str + :ivar from_property: The lower bounds for this percentile allocation. Required. + :vartype from_property: int + :ivar to: The upper bounds for this percentile allocation. Required. + :vartype to: int + """ + + variant: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The variant to allocate these percentiles to. Required.""" + from_property: int = rest_field(name="from", visibility=["read", "create", "update", "delete", "query"]) + """The lower bounds for this percentile allocation. Required.""" + to: int = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The upper bounds for this percentile allocation. Required.""" + + @overload + def __init__( + self, + *, + variant: str, + from_property: int, + to: int, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + class Snapshot(_Model): """A snapshot is a named, immutable subset of an App Configuration store's key-values. @@ -379,3 +741,36 @@ def __init__(self, mapping: Mapping[str, Any]) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) + + +class UserAllocation(_Model): + """Feature Flag UserAllocation object. + + :ivar variant: The variant to allocate these percentiles to. Required. + :vartype variant: str + :ivar users: The users to get this variant. Required. + :vartype users: list[str] + """ + + variant: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The variant to allocate these percentiles to. Required.""" + users: list[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The users to get this variant. Required.""" + + @overload + def __init__( + self, + *, + variant: str, + users: list[str], + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py index 19d50919c47c..ab4bbb36da39 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py @@ -19,6 +19,15 @@ Snapshot as GeneratedConfigurationSnapshot, SnapshotStatus, SnapshotComposition, + FeatureFlag as _GeneratedFeatureFlag, + FeatureFlagAllocation as _GeneratedFeatureFlagAllocation, + FeatureFlagConditions as _GeneratedFeatureFlagConditions, + FeatureFlagFilter as _GeneratedFeatureFlagFilter, + FeatureFlagTelemetryConfiguration as _GeneratedFeatureFlagTelemetryConfiguration, + FeatureFlagVariantDefinition as _GeneratedFeatureFlagVariantDefinition, + GroupAllocation as _GeneratedGroupAllocation, + PercentileAllocation as _GeneratedPercentileAllocation, + UserAllocation as _GeneratedUserAllocation, ) from ._generated._utils.model_base import _deserialize @@ -287,6 +296,54 @@ def _to_generated(self) -> KeyValue: etag=self.etag, ) + @classmethod + def _from_feature_flag(cls, feature_flag: _GeneratedFeatureFlag) -> "FeatureFlagConfigurationSetting": + """Create a FeatureFlagConfigurationSetting from a native FeatureFlag object. + + :param feature_flag: The native FeatureFlag object from the Feature Flag endpoint + :type feature_flag: ~azure.appconfiguration._generated.models.FeatureFlag + :return: A FeatureFlagConfigurationSetting + :rtype: ~azure.appconfiguration.FeatureFlagConfigurationSetting + """ + filters = None + if feature_flag.conditions and feature_flag.conditions.get("client_filters"): + filters = feature_flag.conditions["client_filters"] + + return cls( + feature_id=feature_flag.name, # type: ignore + key=cls._key_prefix + feature_flag.name, + label=feature_flag.label, + content_type=cls._feature_flag_content_type, + last_modified=feature_flag.last_modified, + tags=feature_flag.tags or {}, + read_only=False, # FeatureFlag doesn't have a read_only field + etag=feature_flag.etag, + enabled=feature_flag.enabled or False, + filters=filters, + display_name=None, + description=feature_flag.description, + ) + + def _to_feature_flag(self) -> _GeneratedFeatureFlag: + """Convert this FeatureFlagConfigurationSetting to a native FeatureFlag object. + + :return: A native FeatureFlag object for the Feature Flag endpoint + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag + """ + conditions = None + if self.filters: + conditions = {"client_filters": self.filters} + + return _GeneratedFeatureFlag( + name=self.feature_id, + enabled=self.enabled, + label=self.label, + description=self.description, + conditions=conditions, + tags=self.tags, + etag=self.etag, + ) + class SecretReferenceConfigurationSetting(ConfigurationSetting): """A configuration value that references a configuration setting secret.""" @@ -404,6 +461,548 @@ def _to_generated(self) -> KeyValue: ) +class FeatureFlagFilter: + """A filter that conditionally enables or disables a feature flag. + + :ivar name: The name of the filter. Required. + :vartype name: str + :ivar parameters: The parameters used by the filter. + :vartype parameters: dict[str, Any] or None + """ + + name: str + """The name of the filter. Required.""" + parameters: Optional[Dict[str, Any]] + """The parameters used by the filter.""" + + def __init__(self, *, name: str, parameters: Optional[Dict[str, Any]] = None) -> None: + """ + :keyword name: The name of the filter. Required. + :paramtype name: str + :keyword parameters: The parameters used by the filter. + :paramtype parameters: dict[str, Any] or None + """ + self.name = name + self.parameters = parameters + + @classmethod + def _from_generated(cls, generated: _GeneratedFeatureFlagFilter) -> "FeatureFlagFilter": + return cls(name=generated.name, parameters=generated.parameters) + + def _to_generated(self) -> _GeneratedFeatureFlagFilter: + return _GeneratedFeatureFlagFilter(name=self.name, parameters=self.parameters) + + +class FeatureFlagConditions: + """The conditions that must be met for a feature flag to be enabled. + + :ivar requirement_type: The requirement type for the conditions. Known values are: "Any" and + "All". + :vartype requirement_type: str or None + :ivar client_filters: The filters that will conditionally enable or disable the flag. + :vartype client_filters: list[~azure.appconfiguration.FeatureFlagFilter] or None + """ + + requirement_type: Optional[str] + """The requirement type for the conditions. Known values are: "Any" and "All".""" + client_filters: Optional[List[FeatureFlagFilter]] + """The filters that will conditionally enable or disable the flag.""" + + def __init__( + self, + *, + requirement_type: Optional[str] = None, + client_filters: Optional[List[FeatureFlagFilter]] = None, + ) -> None: + """ + :keyword requirement_type: The requirement type for the conditions. Known values are: "Any" + and "All". + :paramtype requirement_type: str or None + :keyword client_filters: The filters that will conditionally enable or disable the flag. + :paramtype client_filters: list[~azure.appconfiguration.FeatureFlagFilter] or None + """ + self.requirement_type = requirement_type + self.client_filters = client_filters + + @classmethod + def _from_generated(cls, generated: _GeneratedFeatureFlagConditions) -> "FeatureFlagConditions": + return cls( + requirement_type=generated.requirement_type, + client_filters=( + [FeatureFlagFilter._from_generated(f) for f in generated.filters] + if generated.filters is not None + else None + ), + ) + + def _to_generated(self) -> _GeneratedFeatureFlagConditions: + return _GeneratedFeatureFlagConditions( + requirement_type=self.requirement_type, + filters=( + [f._to_generated() for f in self.client_filters] if self.client_filters is not None else None + ), + ) + + +class FeatureFlagVariantDefinition: + """A variant of a feature flag. + + :ivar name: The name of the variant. Required. + :vartype name: str + :ivar value: The value of the variant. + :vartype value: str or None + :ivar content_type: The content type of the value stored within the key-value. + :vartype content_type: str or None + :ivar status_override: Determines if the variant should override the status of the flag. Known + values are: "None", "Enabled", and "Disabled". + :vartype status_override: str or None + """ + + name: str + """The name of the variant. Required.""" + value: Optional[str] + """The value of the variant.""" + content_type: Optional[str] + """The content type of the value stored within the key-value.""" + status_override: Optional[str] + """Determines if the variant should override the status of the flag.""" + + def __init__( + self, + *, + name: str, + value: Optional[str] = None, + content_type: Optional[str] = None, + status_override: Optional[str] = None, + ) -> None: + """ + :keyword name: The name of the variant. Required. + :paramtype name: str + :keyword value: The value of the variant. + :paramtype value: str or None + :keyword content_type: The content type of the value stored within the key-value. + :paramtype content_type: str or None + :keyword status_override: Determines if the variant should override the status of the flag. + Known values are: "None", "Enabled", and "Disabled". + :paramtype status_override: str or None + """ + self.name = name + self.value = value + self.content_type = content_type + self.status_override = status_override + + @classmethod + def _from_generated( + cls, generated: _GeneratedFeatureFlagVariantDefinition + ) -> "FeatureFlagVariantDefinition": + return cls( + name=generated.name, + value=generated.value, + content_type=generated.content_type, + status_override=generated.status_override, + ) + + def _to_generated(self) -> _GeneratedFeatureFlagVariantDefinition: + return _GeneratedFeatureFlagVariantDefinition( + name=self.name, + value=self.value, + content_type=self.content_type, + status_override=self.status_override, + ) + + +class PercentileAllocation: + """Allocates a percentile range of users to a variant. + + :ivar variant: The variant to allocate these percentiles to. Required. + :vartype variant: str + :ivar percentile_from: The lower bounds for this percentile allocation. Required. + :vartype percentile_from: int + :ivar percentile_to: The upper bounds for this percentile allocation. Required. + :vartype percentile_to: int + """ + + variant: str + """The variant to allocate these percentiles to. Required.""" + percentile_from: int + """The lower bounds for this percentile allocation. Required.""" + percentile_to: int + """The upper bounds for this percentile allocation. Required.""" + + def __init__(self, *, variant: str, percentile_from: int, percentile_to: int) -> None: + """ + :keyword variant: The variant to allocate these percentiles to. Required. + :paramtype variant: str + :keyword percentile_from: The lower bounds for this percentile allocation. Required. + :paramtype percentile_from: int + :keyword percentile_to: The upper bounds for this percentile allocation. Required. + :paramtype percentile_to: int + """ + self.variant = variant + self.percentile_from = percentile_from + self.percentile_to = percentile_to + + @classmethod + def _from_generated(cls, generated: _GeneratedPercentileAllocation) -> "PercentileAllocation": + return cls( + variant=generated.variant, + percentile_from=generated.from_property, + percentile_to=generated.to, + ) + + def _to_generated(self) -> _GeneratedPercentileAllocation: + return _GeneratedPercentileAllocation( + variant=self.variant, + from_property=self.percentile_from, + to=self.percentile_to, + ) + + +class UserAllocation: + """Allocates specific users to a variant. + + :ivar variant: The variant to allocate these users to. Required. + :vartype variant: str + :ivar users: The users to get this variant. Required. + :vartype users: list[str] + """ + + variant: str + """The variant to allocate these users to. Required.""" + users: List[str] + """The users to get this variant. Required.""" + + def __init__(self, *, variant: str, users: List[str]) -> None: + """ + :keyword variant: The variant to allocate these users to. Required. + :paramtype variant: str + :keyword users: The users to get this variant. Required. + :paramtype users: list[str] + """ + self.variant = variant + self.users = users + + @classmethod + def _from_generated(cls, generated: _GeneratedUserAllocation) -> "UserAllocation": + return cls(variant=generated.variant, users=generated.users) + + def _to_generated(self) -> _GeneratedUserAllocation: + return _GeneratedUserAllocation(variant=self.variant, users=self.users) + + +class GroupAllocation: + """Allocates specific groups to a variant. + + :ivar variant: The variant to allocate these groups to. Required. + :vartype variant: str + :ivar groups: The groups to get this variant. Required. + :vartype groups: list[str] + """ + + variant: str + """The variant to allocate these groups to. Required.""" + groups: List[str] + """The groups to get this variant. Required.""" + + def __init__(self, *, variant: str, groups: List[str]) -> None: + """ + :keyword variant: The variant to allocate these groups to. Required. + :paramtype variant: str + :keyword groups: The groups to get this variant. Required. + :paramtype groups: list[str] + """ + self.variant = variant + self.groups = groups + + @classmethod + def _from_generated(cls, generated: _GeneratedGroupAllocation) -> "GroupAllocation": + return cls(variant=generated.variant, groups=generated.groups) + + def _to_generated(self) -> _GeneratedGroupAllocation: + return _GeneratedGroupAllocation(variant=self.variant, groups=self.groups) + + +class FeatureFlagAllocation: + """Defines how to allocate variants based on context. + + :ivar default_when_disabled: The default variant to use when disabled. + :vartype default_when_disabled: str or None + :ivar default_when_enabled: The default variant to use when enabled but not allocated. + :vartype default_when_enabled: str or None + :ivar percentile: Allocates percentiles to variants. + :vartype percentile: list[~azure.appconfiguration.PercentileAllocation] or None + :ivar user: Allocates users to variants. + :vartype user: list[~azure.appconfiguration.UserAllocation] or None + :ivar group: Allocates groups to variants. + :vartype group: list[~azure.appconfiguration.GroupAllocation] or None + :ivar seed: The seed used for random allocation. + :vartype seed: str or None + """ + + default_when_disabled: Optional[str] + """The default variant to use when disabled.""" + default_when_enabled: Optional[str] + """The default variant to use when enabled but not allocated.""" + percentile: Optional[List[PercentileAllocation]] + """Allocates percentiles to variants.""" + user: Optional[List[UserAllocation]] + """Allocates users to variants.""" + group: Optional[List[GroupAllocation]] + """Allocates groups to variants.""" + seed: Optional[str] + """The seed used for random allocation.""" + + def __init__( + self, + *, + default_when_disabled: Optional[str] = None, + default_when_enabled: Optional[str] = None, + percentile: Optional[List[PercentileAllocation]] = None, + user: Optional[List[UserAllocation]] = None, + group: Optional[List[GroupAllocation]] = None, + seed: Optional[str] = None, + ) -> None: + """ + :keyword default_when_disabled: The default variant to use when disabled. + :paramtype default_when_disabled: str or None + :keyword default_when_enabled: The default variant to use when enabled but not allocated. + :paramtype default_when_enabled: str or None + :keyword percentile: Allocates percentiles to variants. + :paramtype percentile: list[~azure.appconfiguration.PercentileAllocation] or None + :keyword user: Allocates users to variants. + :paramtype user: list[~azure.appconfiguration.UserAllocation] or None + :keyword group: Allocates groups to variants. + :paramtype group: list[~azure.appconfiguration.GroupAllocation] or None + :keyword seed: The seed used for random allocation. + :paramtype seed: str or None + """ + self.default_when_disabled = default_when_disabled + self.default_when_enabled = default_when_enabled + self.percentile = percentile + self.user = user + self.group = group + self.seed = seed + + @classmethod + def _from_generated(cls, generated: _GeneratedFeatureFlagAllocation) -> "FeatureFlagAllocation": + return cls( + default_when_disabled=generated.default_when_disabled, + default_when_enabled=generated.default_when_enabled, + percentile=( + [PercentileAllocation._from_generated(p) for p in generated.percentile] + if generated.percentile is not None + else None + ), + user=( + [UserAllocation._from_generated(u) for u in generated.user] + if generated.user is not None + else None + ), + group=( + [GroupAllocation._from_generated(g) for g in generated.group] + if generated.group is not None + else None + ), + seed=generated.seed, + ) + + def _to_generated(self) -> _GeneratedFeatureFlagAllocation: + return _GeneratedFeatureFlagAllocation( + default_when_disabled=self.default_when_disabled, + default_when_enabled=self.default_when_enabled, + percentile=( + [p._to_generated() for p in self.percentile] if self.percentile is not None else None + ), + user=[u._to_generated() for u in self.user] if self.user is not None else None, + group=[g._to_generated() for g in self.group] if self.group is not None else None, + seed=self.seed, + ) + + +class FeatureFlagTelemetryConfiguration: + """The telemetry configuration of a feature flag. + + :ivar enabled: The enabled state of the telemetry. Required. + :vartype enabled: bool + :ivar metadata: The metadata to include on outbound telemetry. + :vartype metadata: dict[str, str] or None + """ + + enabled: bool + """The enabled state of the telemetry. Required.""" + metadata: Optional[Dict[str, str]] + """The metadata to include on outbound telemetry.""" + + def __init__(self, *, enabled: bool, metadata: Optional[Dict[str, str]] = None) -> None: + """ + :keyword enabled: The enabled state of the telemetry. Required. + :paramtype enabled: bool + :keyword metadata: The metadata to include on outbound telemetry. + :paramtype metadata: dict[str, str] or None + """ + self.enabled = enabled + self.metadata = metadata + + @classmethod + def _from_generated( + cls, generated: _GeneratedFeatureFlagTelemetryConfiguration + ) -> "FeatureFlagTelemetryConfiguration": + return cls(enabled=generated.enabled, metadata=generated.metadata) + + def _to_generated(self) -> _GeneratedFeatureFlagTelemetryConfiguration: + return _GeneratedFeatureFlagTelemetryConfiguration(enabled=self.enabled, metadata=self.metadata) + + +class FeatureFlag(Model): + """A feature flag used with the dedicated feature flag endpoints. + + This model represents a feature flag and is used exclusively with the + feature flag-specific API endpoints (set_feature_flag, get_feature_flag, etc). + """ + + name: str + """The name of the feature flag.""" + enabled: Optional[bool] + """The enabled state of the feature flag.""" + label: Optional[str] + """The label the feature flag belongs to.""" + description: Optional[str] + """The description of the feature flag.""" + conditions: Optional[FeatureFlagConditions] + """The conditions of the feature flag.""" + variants: Optional[List[FeatureFlagVariantDefinition]] + """The variants of the feature flag.""" + allocation: Optional[FeatureFlagAllocation] + """The allocation of the feature flag.""" + telemetry: Optional[FeatureFlagTelemetryConfiguration] + """The telemetry settings of the feature flag.""" + tags: Optional[Dict[str, str]] + """The tags of the feature flag.""" + last_modified: Optional[datetime] + """A date representing the last time the feature flag was modified.""" + etag: Optional[str] + """A value representing the current state of the resource.""" + + _attribute_map = { + "name": {"key": "name", "type": "str"}, + "enabled": {"key": "enabled", "type": "bool"}, + "label": {"key": "label", "type": "str"}, + "description": {"key": "description", "type": "str"}, + "conditions": {"key": "conditions", "type": "object"}, + "variants": {"key": "variants", "type": "[object]"}, + "allocation": {"key": "allocation", "type": "object"}, + "telemetry": {"key": "telemetry", "type": "object"}, + "tags": {"key": "tags", "type": "{str}"}, + "last_modified": {"key": "last_modified", "type": "iso-8601"}, + "etag": {"key": "etag", "type": "str"}, + } + + def __init__( + self, + name: str, + *, + enabled: Optional[bool] = None, + label: Optional[str] = None, + description: Optional[str] = None, + conditions: Optional[FeatureFlagConditions] = None, + variants: Optional[List[FeatureFlagVariantDefinition]] = None, + allocation: Optional[FeatureFlagAllocation] = None, + telemetry: Optional[FeatureFlagTelemetryConfiguration] = None, + tags: Optional[Dict[str, str]] = None, + **kwargs: Any, + ) -> None: + """ + :param name: The name of the feature flag. + :type name: str + :keyword enabled: The enabled state of the feature flag. Default is False. + :paramtype enabled: bool or None + :keyword label: The label the feature flag belongs to. + :paramtype label: str or None + :keyword description: The description of the feature flag. + :paramtype description: str or None + :keyword conditions: The conditions of the feature flag. + :paramtype conditions: ~azure.appconfiguration.FeatureFlagConditions or None + :keyword variants: The variants of the feature flag. + :paramtype variants: list[~azure.appconfiguration.FeatureFlagVariantDefinition] or None + :keyword allocation: The allocation of the feature flag. + :paramtype allocation: ~azure.appconfiguration.FeatureFlagAllocation or None + :keyword telemetry: The telemetry settings of the feature flag. + :paramtype telemetry: ~azure.appconfiguration.FeatureFlagTelemetryConfiguration or None + :keyword tags: The tags of the feature flag. + :paramtype tags: dict[str, str] or None + """ + self.name = name + self.enabled = enabled if enabled is not None else False + self.label = label + self.description = description + self.conditions = conditions + self.variants = variants + self.allocation = allocation + self.telemetry = telemetry + self.tags = tags or {} + self.last_modified = kwargs.get("last_modified", None) + self.etag = kwargs.get("etag", None) + + @classmethod + def _from_generated(cls, generated: _GeneratedFeatureFlag) -> "FeatureFlag": + """Create an SDK FeatureFlag from a generated FeatureFlag object. + + :param generated: The generated FeatureFlag object + :type generated: ~azure.appconfiguration._generated.models.FeatureFlag + :return: An SDK FeatureFlag + :rtype: ~azure.appconfiguration.FeatureFlag + """ + return cls( + name=generated.name, + enabled=generated.enabled, + label=generated.label, + description=generated.description, + conditions=( + FeatureFlagConditions._from_generated(generated.conditions) + if generated.conditions is not None + else None + ), + variants=( + [FeatureFlagVariantDefinition._from_generated(v) for v in generated.variants] + if generated.variants is not None + else None + ), + allocation=( + FeatureFlagAllocation._from_generated(generated.allocation) + if generated.allocation is not None + else None + ), + telemetry=( + FeatureFlagTelemetryConfiguration._from_generated(generated.telemetry) + if generated.telemetry is not None + else None + ), + tags=generated.tags, + last_modified=generated.last_modified, + etag=generated.etag, + ) + + def _to_generated(self) -> _GeneratedFeatureFlag: + """Convert this SDK FeatureFlag to a generated FeatureFlag object. + + :return: A generated FeatureFlag + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag + """ + return _GeneratedFeatureFlag( + name=self.name, + enabled=self.enabled, + label=self.label, + description=self.description, + conditions=self.conditions._to_generated() if self.conditions is not None else None, + variants=( + [v._to_generated() for v in self.variants] if self.variants is not None else None + ), + allocation=self.allocation._to_generated() if self.allocation is not None else None, + telemetry=self.telemetry._to_generated() if self.telemetry is not None else None, + tags=self.tags, + ) + + class ConfigurationSettingsFilter: """Enables filtering of configuration settings.""" @@ -601,6 +1200,178 @@ def _return_deserialized_and_headers(_, deserialized, response_headers): return deserialized, response_headers +class FeatureFlagConfigurationSettingPropertiesPagedBase: # pylint:disable=too-many-instance-attributes + """Base class for iterable of FeatureFlagConfigurationSetting properties.""" + + etag: str + """The current etag""" + _etags: List[str] + """The etag expected for the pages.""" + _command: Callable + """The command to get the next page""" + _key: Optional[str] + """The key filter for the items""" + _label: Optional[str] + """The label filter for the items""" + _accept_datetime: Optional[str] + """The accept datetime for the items""" + _select: Optional[List[Union[str, Any]]] + """The select fields for the items""" + _tags: Optional[List[str]] + """The tags filter for the items""" + _snapshot: Optional[str] + """The snapshot name for the items""" + _match_condition: Optional[MatchConditions] + """The match condition""" + _kwargs: Dict[str, Any] + """The keyword arguments""" + + def __init__(self, command: Callable, **kwargs: Any): + self._command = command + self._key = kwargs.pop("key", None) + self._label = kwargs.pop("label", None) + self._accept_datetime = kwargs.pop("accept_datetime", None) + self._select = kwargs.pop("select", None) + self._tags = kwargs.pop("tags", None) + self._snapshot = kwargs.pop("snapshot", None) + self._match_condition = kwargs.pop("match_condition", None) + self._kwargs = kwargs + self._etags: List[str] = [] + + def _next_etag(self) -> Optional[str]: + if self._etags: + etag = self._etags.pop(0) + return etag + return None + + def _extract_data_cb_base(self, get_next_return): + etags, data = get_next_return + self.etag = etags + return data + + def _page_iterator_class(self, **kwargs: Any) -> PageIterator: + raise NotImplementedError() + + +class FeatureFlagConfigurationSettingPropertiesPaged( + FeatureFlagConfigurationSettingPropertiesPagedBase, PageIterator +): # pylint:disable=too-many-instance-attributes + """An iterable of FeatureFlagConfigurationSetting properties.""" + + def __init__(self, command: Callable, **kwargs: Any): + super().__init__(command, **kwargs) + PageIterator.__init__( + self, + self._get_next_cb, + self._extract_data_cb, + continuation_token=kwargs.get("continuation_token"), + ) + + def _get_next_cb(self, continuation_token, **kwargs): + etag = self._next_etag() + return self._command( + name=self._key, + label=self._label, + accept_datetime=self._accept_datetime, + select=self._select, + tags=self._tags, + etag=etag, + match_condition=self._match_condition, + continuation_token=continuation_token, + cls=kwargs.pop("cls", None) or _return_deserialized_and_headers, + ) + + def _extract_data_cb(self, get_next_return): + return self._extract_data_cb_base(get_next_return) + + def __next__(self) -> Iterator[ReturnType]: + """Get the next page in the iterator. + + :returns: An iterator of objects in the next page. + :rtype: iterator[ReturnType] + :raises StopIteration: If there are no more pages to return. + :raises AzureError: If the request fails. + """ + # Is the exact same method as `PageIterator`, excluding the if statement before the return. + if self.continuation_token is None and self._did_a_call_already: + raise StopIteration("End of paging") + try: + self._response = self._get_next(self.continuation_token) + except AzureError as error: + if not error.continuation_token: + error.continuation_token = self.continuation_token + raise + + self._did_a_call_already = True + + self.continuation_token, self._current_page = self._extract_data(self._response) + + # App Config's addition to skip empty pages + if self._current_page is None: + # We skip over pages that are empty, change from mach conditions + return self.__next__() + return iter(self._current_page) + + +class FeatureFlagConfigurationSettingPropertiesPagedAsync( + FeatureFlagConfigurationSettingPropertiesPagedBase, AsyncPageIterator +): # pylint:disable=too-many-instance-attributes + """An async iterable of FeatureFlagConfigurationSetting properties.""" + + def __init__(self, command: Callable, **kwargs: Any): + super().__init__(command, **kwargs) + AsyncPageIterator.__init__( + self, + self._get_next_cb, + self._extract_data_cb, + continuation_token=kwargs.get("continuation_token"), + ) + + def _get_next_cb(self, continuation_token, **kwargs): + etag = self._next_etag() + return self._command( + name=self._key, + label=self._label, + accept_datetime=self._accept_datetime, + select=self._select, + tags=self._tags, + etag=etag, + match_condition=self._match_condition, + continuation_token=continuation_token, + cls=kwargs.pop("cls", None) or _return_deserialized_and_headers, + ) + + def _extract_data_cb(self, get_next_return): + return self._extract_data_cb_base(get_next_return) + + async def __anext__(self): + """Get the next page in the async iterator. + + :returns: An async iterator of objects in the next page. + :rtype: async_iterator[ReturnType] + :raises StopAsyncIteration: If there are no more pages to return. + :raises AzureError: If the request fails. + """ + if self.continuation_token is None and self._did_a_call_already: + raise StopAsyncIteration("End of paging") # pylint: disable=raise-missing-from + try: + self._response = await self._get_next(self.continuation_token) + except AzureError as error: + if not error.continuation_token: + error.continuation_token = self.continuation_token + raise + + self._did_a_call_already = True + + self.continuation_token, self._current_page = self._extract_data(self._response) + + # App Config's addition to skip empty pages + if self._current_page is None: + # We skip over pages that are empty, change from mach conditions + return await self.__anext__() + return self._current_page + + class ConfigurationSettingPropertiesPagedBase: # pylint:disable=too-many-instance-attributes """Base class for iterable of ConfigurationSetting properties.""" @@ -884,3 +1655,58 @@ def by_page(self, continuation_token: Optional[str] = None, *, match_conditions: self._kwargs["etags"] = match_conditions self._kwargs["match_condition"] = MatchConditions.IfModified return self._page_iterator_class(continuation_token=continuation_token, *self._args, **self._kwargs) + + +class FeatureFlagConfigurationSettingPaged(ItemPaged["FeatureFlagConfigurationSetting"]): + """ + An iterable of FeatureFlagConfigurationSettings that supports etag-based change detection. + + This class extends ItemPaged to provide efficient monitoring of feature flag configuration changes + by using ETags. When used with the `match_conditions` parameter in `by_page()`, + it only returns pages that have changed since the provided ETags were collected. + """ + + def by_page(self, continuation_token: Optional[str] = None, *, match_conditions: Optional[List[str]] = None) -> Any: + """Get an iterator of pages of objects, instead of an iterator of objects. + + :param str continuation_token: + An opaque continuation token. This value can be retrieved from the + continuation_token field of a previous generator object. If specified, + this generator will begin returning results from this point. + :keyword match_conditions: A list of etags to check for changes. If provided, the iterator will + check each page against the corresponding etag and only return pages that have changed. + :paramtype match_conditions: list[str] or None + :returns: An iterator of pages (themselves iterator of objects) + :rtype: iterator[iterator[ReturnType]] + """ + if "match_conditions" not in self._kwargs and match_conditions: + self._kwargs["etags"] = match_conditions + self._kwargs["match_condition"] = MatchConditions.IfModified + return self._page_iterator_class(continuation_token=continuation_token, *self._args, **self._kwargs) + + +class AsyncFeatureFlagConfigurationSettingPaged(AsyncItemPaged["FeatureFlagConfigurationSetting"]): + """ + An async iterable of FeatureFlagConfigurationSettings that supports etag-based change detection. + + This class provides asynchronous iteration over feature flag configuration settings, with optional support for + etag-based change detection. + """ + + def by_page(self, continuation_token: Optional[str] = None, *, match_conditions: Optional[List[str]] = None) -> Any: + """Get an async iterator of pages of objects, instead of an iterator of objects. + + :param str continuation_token: + An opaque continuation token. This value can be retrieved from the + continuation_token field of a previous generator object. If specified, + this generator will begin returning results from this point. + :keyword match_conditions: A list of etags to check for changes. If provided, the iterator will + check each page against the corresponding etag and only return pages that have changed. + :paramtype match_conditions: list[str] or None + :returns: An async iterator of pages (themselves iterator of objects) + :rtype: AsyncIterator[AsyncIterator[ReturnType]] + """ + if "match_conditions" not in self._kwargs and match_conditions: + self._kwargs["etags"] = match_conditions + self._kwargs["match_condition"] = MatchConditions.IfModified + return self._page_iterator_class(continuation_token=continuation_token, *self._args, **self._kwargs) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py index 257be2407c39..48455c82d299 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------------------------------------------ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for @@ -14,7 +15,7 @@ from azure.core.polling import AsyncLROPoller from azure.core.tracing.decorator import distributed_trace from azure.core.tracing.decorator_async import distributed_trace_async -from azure.core.exceptions import ResourceNotModifiedError +from azure.core.exceptions import ResourceNotFoundError, ResourceNotModifiedError from azure.core.rest import AsyncHttpResponse, HttpRequest from ._sync_token_async import AsyncSyncTokenPolicy from .._azure_appconfiguration_error import ResourceReadOnlyError @@ -36,6 +37,7 @@ ConfigurationSettingsFilter, ConfigurationSnapshot, ConfigurationSettingLabel, + FeatureFlag, ) from .._audience import get_audience, DEFAULT_SCOPE_SUFFIX from .._utils import ( @@ -845,6 +847,211 @@ def list_snapshots( **kwargs, ) + @distributed_trace + def list_feature_flags( + self, + name_filter: Optional[str] = None, + label_filter: Optional[str] = None, + *, + tags_filter: Optional[List[str]] = None, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> AsyncConfigurationSettingPaged: + """ + Find the FeatureFlag objects, optionally filtered by name, label, tags and accept_datetime. + + :param name_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end + of the filter. Default is `None`. + :type name_filter: str or None + :param label_filter: Filter results based on their labels. Default is `None`. + :type label_filter: str or None + :keyword tags_filter: Filter results based on their tags. Default is `None`. + :paramtype tags_filter: list[str] or None + :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: An async iterator of :class:`~azure.appconfiguration.FeatureFlag` + :rtype: ~azure.core.paging.AsyncItemPaged[~azure.appconfiguration.FeatureFlag] + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + + # Call the generated method and wrap results to convert to SDK FeatureFlag + def convert_to_sdk(objs): + return [FeatureFlag._from_generated(obj) for obj in objs] + + return self._impl.get_feature_flags( # type: ignore[return-value] + name=name_filter, + label=label_filter, + accept_datetime=accept_datetime, + tags=tags_filter, + cls=convert_to_sdk, + **kwargs + ) + + @distributed_trace_async + async def get_feature_flag( + self, + feature_id: str, + label: Optional[str] = None, + *, + etag: Optional[str] = None, + match_condition: MatchConditions = MatchConditions.Unconditionally, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> Optional["FeatureFlag"]: + """ + Get a feature flag from the service, identified by `feature_id` and optionally `label`. + + :param feature_id: The feature flag identifier. + :type feature_id: str + :param label: The label of the feature flag. Defaults to None. + :type label: str or None + :keyword etag: Check if the feature flag is changed. Set None to skip checking etag. + :paramtype etag: str or None + :keyword match_condition: The match condition to use upon the etag. + :paramtype match_condition: ~azure.core.MatchConditions + :keyword accept_datetime: Retrieve the FeatureFlag that existed at this datetime. + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: The FeatureFlag if found; None otherwise. + :rtype: ~azure.appconfiguration.FeatureFlag or None + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + + try: + generated_feature_flag = await self._impl.get_feature_flag( + name=feature_id, + label=label, + etag=etag, + match_condition=match_condition, + accept_datetime=accept_datetime, + **kwargs, + ) + return FeatureFlag._from_generated(generated_feature_flag) + except (ResourceNotFoundError, ResourceNotModifiedError): + return None + + @distributed_trace_async + async def set_feature_flag( + self, + feature_flag: "FeatureFlag", + match_condition: MatchConditions = MatchConditions.Unconditionally, + etag: Optional[str] = None, + **kwargs: Any, + ) -> "FeatureFlag": + """ + Create or update a feature flag via the dedicated feature flag endpoint. + + :param feature_flag: A FeatureFlag object. + :type feature_flag: ~azure.appconfiguration.FeatureFlag + :keyword match_condition: The match condition to use upon the etag. + :paramtype match_condition: ~azure.core.MatchConditions + :keyword etag: The etag of the feature flag. If provided, the feature flag will be updated only if the etag matches. + :paramtype etag: str or None + :return: The updated FeatureFlag. + :rtype: ~azure.appconfiguration.FeatureFlag + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + error_map: Dict[int, Any] = {409: ResourceReadOnlyError} + + if etag is None: + etag = feature_flag.etag + + generated_feature_flag = feature_flag._to_generated() + result = await self._impl.put_feature_flag( + name=feature_flag.name, + entity=generated_feature_flag, + label=feature_flag.label, + etag=etag, + match_condition=match_condition, + error_map=error_map, + **kwargs, + ) + + return FeatureFlag._from_generated(result) + + @distributed_trace_async + async def delete_feature_flag( + self, + feature_id: str, + label: Optional[str] = None, + *, + etag: Optional[str] = None, + match_condition: MatchConditions = MatchConditions.Unconditionally, + **kwargs: Any, + ) -> Optional["FeatureFlag"]: + """ + Delete a feature flag from the service. + + :param feature_id: The feature flag identifier. + :type feature_id: str + :param label: The label of the feature flag. Defaults to None. + :type label: str or None + :keyword etag: Check if the feature flag is changed. Set None to skip checking etag. + :paramtype etag: str or None + :keyword match_condition: The match condition to use upon the etag. + :paramtype match_condition: ~azure.core.MatchConditions + :return: The deleted FeatureFlag, or None if not found. + :rtype: ~azure.appconfiguration.FeatureFlag or None + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + error_map: Dict[int, Any] = {409: ResourceReadOnlyError} + generated_feature_flag = await self._impl.delete_feature_flag( + name=feature_id, + label=label, + etag=etag, + match_condition=match_condition, + error_map=error_map, + **kwargs, + ) + if generated_feature_flag: + return FeatureFlag._from_generated(generated_feature_flag) + return None + + @distributed_trace + def list_feature_flag_revisions( + self, + feature_id_filter: Optional[str] = None, + label_filter: Optional[str] = None, + *, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> AsyncConfigurationSettingPaged: + """ + Find the FeatureFlag revision history, optionally filtered by feature_id and label. + + :param feature_id_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end + of the filter. Default is `None`. + :type feature_id_filter: str or None + :param label_filter: Filter results based on their labels. Default is `None`. + :type label_filter: str or None + :keyword accept_datetime: Retrieve FeatureFlag revisions that existed at this datetime + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: An async iterator of :class:`~azure.appconfiguration.FeatureFlag` + :rtype: ~azure.core.paging.AsyncItemPaged[~azure.appconfiguration.FeatureFlag] + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + + # Build kwargs for the underlying call + impl_kwargs = dict(kwargs) + if accept_datetime is not None: + impl_kwargs["accept_datetime"] = accept_datetime + + # Call the generated method and wrap results to convert to SDK FeatureFlag + def convert_to_sdk(objs): + return [FeatureFlag._from_generated(obj) for obj in objs] + + return self._impl.get_feature_flag_revisions( # type: ignore[return-value] + name=feature_id_filter, + label=label_filter, + cls=convert_to_sdk, + **impl_kwargs + ) + async def update_sync_token(self, token: str) -> None: """Add a sync token to the internal list of tokens. diff --git a/sdk/appconfiguration/azure-appconfiguration/pyproject.toml b/sdk/appconfiguration/azure-appconfiguration/pyproject.toml index 1d937239979e..80ee29b5867c 100644 --- a/sdk/appconfiguration/azure-appconfiguration/pyproject.toml +++ b/sdk/appconfiguration/azure-appconfiguration/pyproject.toml @@ -17,7 +17,7 @@ authors = [ description = "Microsoft Corporation Azure App Configuration Data Client Library for Python" license = "MIT" classifiers = [ - "Development Status :: 5 - Production/Stable", + "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py index 0da4192b0626..d3fa647c818b 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py @@ -4,6 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +import os from typing import List from testcase import AppConfigTestCase from azure.appconfiguration.aio import AzureAppConfigurationClient @@ -14,6 +15,8 @@ class AsyncAppConfigTestCase(AppConfigTestCase): def create_client(self, appconfiguration_endpoint_string, audience=None): cred = self.get_credential(AzureAppConfigurationClient, is_async=True) + if audience is None: + audience = os.environ.get("APPCONFIGURATION_AUDIENCE") return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, audience=audience) async def convert_to_list(self, items: AsyncItemPaged) -> List: diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py new file mode 100644 index 000000000000..90d7d15cebff --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py @@ -0,0 +1,217 @@ +# pylint: disable=too-many-lines +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import functools +from consts import ( + APPCONFIGURATION_ENDPOINT_STRING, + APPCONFIGURATION_CONNECTION_STRING, +) +from devtools_testutils import EnvironmentVariableLoader, recorded_by_proxy, set_custom_default_matcher +from testcase import AppConfigTestCase +from azure.appconfiguration import ( + AzureAppConfigurationClient, + FeatureFlag, +) + +AppConfigPreparer = functools.partial( + EnvironmentVariableLoader, + "appconfiguration", + appconfiguration_endpoint_string=APPCONFIGURATION_ENDPOINT_STRING, +) + + +class TestFeatureFlagEndpoint(AppConfigTestCase): + """Tests for the new dedicated feature flag endpoint methods""" + + @AppConfigPreparer() + @recorded_by_proxy + def test_list_feature_flags(self, appconfiguration_endpoint_string): + """Test listing feature flags using the dedicated feature flag endpoint.""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + # Create some feature flags + feature_flag1 = FeatureFlag(name="feature1", enabled=True) + feature_flag2 = FeatureFlag(name="feature2", enabled=False) + + client.set_feature_flag(feature_flag1) + client.set_feature_flag(feature_flag2) + + # List all feature flags + flags = list(client.list_feature_flags()) + assert len(flags) >= 2 + assert any(f.name == "feature1" for f in flags) + assert any(f.name == "feature2" for f in flags) + + # Clean up + client.delete_feature_flag("feature1", label=feature_flag1.label) + client.delete_feature_flag("feature2", label=feature_flag2.label) + + @AppConfigPreparer() + @recorded_by_proxy + def test_list_feature_flags_with_name_filter(self, appconfiguration_endpoint_string): + """Test listing feature flags with name filter.""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + # Create feature flags + feature_flag1 = FeatureFlag(name="myfeature_alpha", enabled=True) + feature_flag2 = FeatureFlag(name="myfeature_beta", enabled=False) + feature_flag3 = FeatureFlag(name="otherfeature", enabled=True) + + client.set_feature_flag(feature_flag1) + client.set_feature_flag(feature_flag2) + client.set_feature_flag(feature_flag3) + + # List with name filter + flags = list(client.list_feature_flags(name_filter="myfeature*")) + assert len(flags) >= 2 + assert all("myfeature" in f.name for f in flags) + + # Clean up + client.delete_feature_flag("myfeature_alpha", label=feature_flag1.label) + client.delete_feature_flag("myfeature_beta", label=feature_flag2.label) + client.delete_feature_flag("otherfeature", label=feature_flag3.label) + + @AppConfigPreparer() + @recorded_by_proxy + def test_get_feature_flag(self, appconfiguration_endpoint_string): + """Test getting a specific feature flag.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Create a feature flag + feature_flag = FeatureFlag(name="test_feature_get", enabled=True) + created = client.set_feature_flag(feature_flag) + + # Get the feature flag using the new endpoint method + retrieved = client.get_feature_flag("test_feature_get", label=created.label) + assert retrieved is not None + assert retrieved.name == "test_feature_get" + assert retrieved.enabled == True + + # Clean up + client.delete_feature_flag("test_feature_get", label=created.label) + + @AppConfigPreparer() + @recorded_by_proxy + def test_get_feature_flag_not_found(self, appconfiguration_endpoint_string): + """Test getting a non-existent feature flag.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Try to get a non-existent feature flag + result = client.get_feature_flag("nonexistent_feature", label=None) + assert result is None + + @AppConfigPreparer() + @recorded_by_proxy + def test_set_feature_flag(self, appconfiguration_endpoint_string): + """Test setting a feature flag via the dedicated endpoint.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Create a feature flag + feature_flag = FeatureFlag(name="test_feature_set", enabled=True) + set_flag = client.set_feature_flag(feature_flag) + + assert set_flag is not None + assert set_flag.enabled == True + assert set_flag.name == "test_feature_set" + + # Clean up + client.delete_feature_flag("test_feature_set", label=set_flag.label) + + @AppConfigPreparer() + @recorded_by_proxy + def test_set_feature_flag_update(self, appconfiguration_endpoint_string): + """Test updating an existing feature flag.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Create and then update a feature flag + feature_flag = FeatureFlag(name="test_feature_update", enabled=True) + created = client.set_feature_flag(feature_flag) + + # Update it + created.enabled = False + updated = client.set_feature_flag(created) + + assert updated.enabled == False + assert updated.etag != created.etag + + # Clean up + client.delete_feature_flag("test_feature_update", label=updated.label) + + @AppConfigPreparer() + @recorded_by_proxy + def test_delete_feature_flag(self, appconfiguration_endpoint_string): + """Test deleting a feature flag.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Create a feature flag + feature_flag = FeatureFlag(name="test_feature_delete", enabled=True) + created = client.set_feature_flag(feature_flag) + + # Delete it using the endpoint method + client.delete_feature_flag("test_feature_delete", label=created.label) + + # Verify it's deleted + result = client.get_feature_flag("test_feature_delete", label=created.label) + assert result is None + + @AppConfigPreparer() + @recorded_by_proxy + def test_list_feature_flag_revisions(self, appconfiguration_endpoint_string): + """Test listing feature flag revisions.""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + # Create and update a feature flag to create revisions + feature_flag = FeatureFlag(name="test_feature_revisions", enabled=True) + created = client.set_feature_flag(feature_flag) + + # Update to create another revision + created.enabled = False + updated = client.set_feature_flag(created) + + # List revisions + revisions = list(client.list_feature_flag_revisions(feature_id_filter="test_feature_revisions")) + assert len(revisions) >= 1 # At least one revision + assert all("test_feature_revisions" in r.name for r in revisions) + + # Clean up + client.delete_feature_flag("test_feature_revisions", label=updated.label) + + @AppConfigPreparer() + @recorded_by_proxy + def test_feature_flag_with_label(self, appconfiguration_endpoint_string): + """Test feature flag operations with labels.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Create feature flags with labels + feature_flag_prod = FeatureFlag( + name="feature_with_label", + enabled=True, + label="prod" + ) + feature_flag_staging = FeatureFlag( + name="feature_with_label", + enabled=False, + label="staging" + ) + + client.set_feature_flag(feature_flag_prod) + client.set_feature_flag(feature_flag_staging) + + # Get specific labeled version + prod_flag = client.get_feature_flag("feature_with_label", label="prod") + assert prod_flag is not None + assert prod_flag.enabled == True + + staging_flag = client.get_feature_flag("feature_with_label", label="staging") + assert staging_flag is not None + assert staging_flag.enabled == False + + # Clean up + client.delete_feature_flag("feature_with_label", label="prod") + client.delete_feature_flag("feature_with_label", label="staging") diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py new file mode 100644 index 000000000000..2ce2ee31c8e5 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py @@ -0,0 +1,227 @@ +# pylint: disable=too-many-lines +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import functools +from consts import ( + APPCONFIGURATION_ENDPOINT_STRING, + APPCONFIGURATION_CONNECTION_STRING, +) +from devtools_testutils import EnvironmentVariableLoader, set_custom_default_matcher +from devtools_testutils.aio import recorded_by_proxy_async +from asynctestcase import AsyncAppConfigTestCase +from azure.appconfiguration import ( + FeatureFlag, +) +from azure.appconfiguration.aio import AzureAppConfigurationClient + +AppConfigPreparer = functools.partial( + EnvironmentVariableLoader, + "appconfiguration", + appconfiguration_endpoint_string=APPCONFIGURATION_ENDPOINT_STRING, +) + + +class TestFeatureFlagEndpointAsync(AsyncAppConfigTestCase): + """Async tests for the new dedicated feature flag endpoint methods""" + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_list_feature_flags(self, appconfiguration_endpoint_string): + """Test listing feature flags using the dedicated feature flag endpoint.""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + # Create some feature flags + feature_flag1 = FeatureFlag(name="feature1", enabled=True) + feature_flag2 = FeatureFlag(name="feature2", enabled=False) + + await client.set_feature_flag(feature_flag1) + await client.set_feature_flag(feature_flag2) + + # List all feature flags + flags = [f async for f in client.list_feature_flags()] + assert len(flags) >= 2 + assert any(f.name == "feature1" for f in flags) + assert any(f.name == "feature2" for f in flags) + + # Clean up + await client.delete_feature_flag("feature1", label=feature_flag1.label) + await client.delete_feature_flag("feature2", label=feature_flag2.label) + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_list_feature_flags_with_name_filter(self, appconfiguration_endpoint_string): + """Test listing feature flags with name filter.""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + # Create feature flags + feature_flag1 = FeatureFlag(name="myfeature_alpha", enabled=True) + feature_flag2 = FeatureFlag(name="myfeature_beta", enabled=False) + feature_flag3 = FeatureFlag(name="otherfeature", enabled=True) + + await client.set_feature_flag(feature_flag1) + await client.set_feature_flag(feature_flag2) + await client.set_feature_flag(feature_flag3) + + # List with name filter + flags = [f async for f in client.list_feature_flags(name_filter="myfeature*")] + assert len(flags) >= 2 + assert all("myfeature" in f.name for f in flags) + + # Clean up + await client.delete_feature_flag("myfeature_alpha", label=feature_flag1.label) + await client.delete_feature_flag("myfeature_beta", label=feature_flag2.label) + await client.delete_feature_flag("otherfeature", label=feature_flag3.label) + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_get_feature_flag(self, appconfiguration_endpoint_string): + """Test getting a specific feature flag.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Create a feature flag + feature_flag = FeatureFlag(name="test_feature_get", enabled=True) + created = await client.set_feature_flag(feature_flag) + + # Get the feature flag using the new endpoint method + retrieved = await client.get_feature_flag("test_feature_get", label=created.label) + assert retrieved is not None + assert retrieved.name == "test_feature_get" + assert retrieved.enabled == True + + # Clean up + await client.delete_feature_flag("test_feature_get", label=created.label) + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_get_feature_flag_not_found(self, appconfiguration_endpoint_string): + """Test getting a non-existent feature flag.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Try to get a non-existent feature flag + result = await client.get_feature_flag("nonexistent_feature", label=None) + assert result is None + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_set_feature_flag(self, appconfiguration_endpoint_string): + """Test setting a feature flag via the dedicated endpoint.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Create a feature flag + feature_flag = FeatureFlag(name="test_feature_set", enabled=True) + set_flag = await client.set_feature_flag(feature_flag) + + assert set_flag is not None + assert set_flag.enabled == True + assert set_flag.name == "test_feature_set" + + # Clean up + await client.delete_feature_flag("test_feature_set", label=set_flag.label) + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_set_feature_flag_update(self, appconfiguration_endpoint_string): + """Test updating an existing feature flag.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Create and then update a feature flag + feature_flag = FeatureFlag(name="test_feature_update", enabled=True) + created = await client.set_feature_flag(feature_flag) + + # Update it + created.enabled = False + updated = await client.set_feature_flag(created) + + assert updated.enabled == False + assert updated.etag != created.etag + + # Clean up + await client.delete_feature_flag("test_feature_update", label=updated.label) + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_delete_feature_flag(self, appconfiguration_endpoint_string): + """Test deleting a feature flag.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Create a feature flag + feature_flag = FeatureFlag(name="test_feature_delete", enabled=True) + created = await client.set_feature_flag(feature_flag) + + # Delete it using the endpoint method + await client.delete_feature_flag("test_feature_delete", label=created.label) + + # Verify it's deleted + result = await client.get_feature_flag("test_feature_delete", label=created.label) + assert result is None + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_list_feature_flag_revisions(self, appconfiguration_endpoint_string): + """Test listing feature flag revisions.""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + # Create and update a feature flag to create revisions + feature_flag = FeatureFlag(name="test_feature_revisions", enabled=True) + created = await client.set_feature_flag(feature_flag) + + # Update to create another revision + created.enabled = False + updated = await client.set_feature_flag(created) + + # List revisions + revisions = [r async for r in client.list_feature_flag_revisions(feature_id_filter="test_feature_revisions")] + assert len(revisions) >= 1 # At least one revision + assert all("test_feature_revisions" in r.name for r in revisions) + + # Clean up + await client.delete_feature_flag("test_feature_revisions", label=updated.label) + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_feature_flag_with_label(self, appconfiguration_endpoint_string): + """Test feature flag operations with labels.""" + client = self.create_client(appconfiguration_endpoint_string) + + # Create feature flags with labels + feature_flag_prod = FeatureFlag( + name="feature_with_label", + enabled=True, + label="prod" + ) + feature_flag_staging = FeatureFlag( + name="feature_with_label", + enabled=False, + label="staging" + ) + + await client.set_feature_flag(feature_flag_prod) + await client.set_feature_flag(feature_flag_staging) + + # Get specific labeled version + prod_flag = await client.get_feature_flag("feature_with_label", label="prod") + assert prod_flag is not None + assert prod_flag.enabled == True + + staging_flag = await client.get_feature_flag("feature_with_label", label="staging") + assert staging_flag is not None + assert staging_flag.enabled == False + + # Clean up + await client.delete_feature_flag("feature_with_label", label="prod") + await client.delete_feature_flag("feature_with_label", label="staging") + await client.close() diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py index 2ca8681adfad..c4f0d8b0cbe3 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py @@ -4,6 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +import os from consts import ( KEY, LABEL, @@ -25,6 +26,8 @@ class AppConfigTestCase(AzureRecordedTestCase): def create_client(self, appconfiguration_endpoint_string, audience=None): cred = self.get_credential(AzureAppConfigurationClient) + if audience is None: + audience = os.environ.get("APPCONFIGURATION_AUDIENCE") return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, audience=audience) def create_config_setting(self): diff --git a/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml b/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml index 5e79ad4ad55e..d90f05e6b8e3 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml +++ b/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml @@ -1,4 +1,4 @@ -commit: 609f05e331fd1b5b71ed2c476f0d356d25053c9a +commit: ee5f3d39891991feaae672a9ae28b984627d9d16 repo: Azure/azure-rest-api-specs directory: specification/appconfiguration/data-plane/AppConfiguration additionalDirectories: \ No newline at end of file From 01b095fd910068ad4b2e2d08cb455eb0a7d9ca94 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 22 Jun 2026 17:09:24 -0700 Subject: [PATCH 03/25] Updating tests, added samples --- .../azure-appconfiguration/samples/README.md | 3 + .../samples/feature_flag_sample.py | 127 +++++++++++++ .../samples/feature_flag_sample_async.py | 131 +++++++++++++ .../tests/asynctestcase.py | 6 +- .../test_audience_error_handling_live.py | 4 +- ...test_audience_error_handling_live_async.py | 4 +- .../test_azure_appconfiguration_client.py | 5 +- ...est_azure_appconfiguration_client_async.py | 5 +- .../tests/test_feature_flags.py | 175 +++++++++++++++++ .../tests/test_feature_flags_async.py | 179 ++++++++++++++++++ .../azure-appconfiguration/tests/testcase.py | 8 +- 11 files changed, 634 insertions(+), 13 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py create mode 100644 sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/README.md b/sdk/appconfiguration/azure-appconfiguration/samples/README.md index c24271cbba91..731d3eb8da4a 100644 --- a/sdk/appconfiguration/azure-appconfiguration/samples/README.md +++ b/sdk/appconfiguration/azure-appconfiguration/samples/README.md @@ -40,6 +40,7 @@ pip install azure-appconfiguration | [list_revision_sample.py][list_revision_sample] / [list_revision_sample_async.py][list_revision_sample_async] | demos how to get configuration setting revision history | | [sync_token_sample.py][sync_token_sample] / [sync_token_sample_async.py][sync_token_sample_async] | demos how to update sync_token for an AzureAppConfigurationClient | | [snapshot_sample.py][snapshot_sample] / [snapshot_sample_async.py][snapshot_sample_async] | demos how to create/retrieve/archive/recover/list configuration settings snapshot and list configuration settings of a snapshot | +| [feature_flag_sample.py][feature_flag_sample] / [feature_flag_sample_async.py][feature_flag_sample_async] | demos how to set/get/list/delete feature flags using the dedicated feature flag endpoint, including the full FeatureFlag model (conditions, variants, allocation, telemetry) | | [send_request_sample.py][send_request_sample] / [send_request_sample_async.py][send_request_sample_async] | demos how to make custom HTTP requests through a client pipeline | @@ -63,5 +64,7 @@ pip install azure-appconfiguration [sync_token_sample_async]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/sync_token_sample_async.py [snapshot_sample]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/snapshot_sample.py [snapshot_sample_async]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/snapshot_sample_async.py +[feature_flag_sample]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py +[feature_flag_sample_async]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py [send_request_sample]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/send_request_sample.py [send_request_sample_async]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/send_request_sample_async.py diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py new file mode 100644 index 000000000000..0cf0c72ca340 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py @@ -0,0 +1,127 @@ +# coding: utf-8 + +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +""" +FILE: feature_flag_sample.py + +DESCRIPTION: + This sample demos how to set, get, list, and delete feature flags using the + dedicated feature flag endpoint methods synchronously. It also shows how to build + a fully populated FeatureFlag with conditions, variants, allocation, and telemetry. + +USAGE: python feature_flag_sample.py + + Set the environment variables with your own values before running the sample: + 1) APPCONFIGURATION_ENDPOINT_STRING: Endpoint URL used to access the Azure App Configuration. +""" + +import os +from azure.identity import DefaultAzureCredential +from azure.appconfiguration import ( + AzureAppConfigurationClient, + FeatureFlag, + FeatureFlagConditions, + FeatureFlagFilter, + FeatureFlagVariantDefinition, + FeatureFlagAllocation, + FeatureFlagTelemetryConfiguration, + PercentileAllocation, + UserAllocation, + GroupAllocation, +) + + +def main(): + endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"] + credential = DefaultAzureCredential() + client = AzureAppConfigurationClient(base_url=endpoint, credential=credential) + + print("Set a simple feature flag") + # [START set_feature_flag] + flag = FeatureFlag(name="SampleFeature", enabled=True, description="A simple on/off feature flag") + created = client.set_feature_flag(flag) + # [END set_feature_flag] + print(f" name={created.name}, enabled={created.enabled}") + print("") + + print("Get a feature flag") + # [START get_feature_flag] + retrieved = client.get_feature_flag("SampleFeature") + # [END get_feature_flag] + if retrieved is not None: + print(f" name={retrieved.name}, enabled={retrieved.enabled}, description={retrieved.description}") + print("") + + print("Set a fully populated feature flag (conditions, variants, allocation, telemetry, tags)") + rich_flag = FeatureFlag( + name="RichFeature", + enabled=True, + description="A feature flag using every part of the model", + # Conditions gate the feature with client filters. + conditions=FeatureFlagConditions( + requirement_type="All", + client_filters=[ + FeatureFlagFilter(name="Microsoft.TimeWindow", parameters={"Start": "Mon, 01 Jan 2024 00:00:00 GMT"}), + FeatureFlagFilter(name="Microsoft.Percentage", parameters={"Value": 50}), + ], + ), + # Variants describe the possible values the feature can resolve to. + variants=[ + FeatureFlagVariantDefinition(name="Large", value="large", status_override="Enabled"), + FeatureFlagVariantDefinition(name="Small", value="small"), + ], + # Allocation controls how users/groups/percentiles map to variants. + allocation=FeatureFlagAllocation( + default_when_enabled="Large", + default_when_disabled="Small", + seed="sample-seed", + percentile=[ + PercentileAllocation(variant="Large", percentile_from=0, percentile_to=50), + PercentileAllocation(variant="Small", percentile_from=50, percentile_to=100), + ], + user=[UserAllocation(variant="Large", users=["alice", "bob"])], + group=[GroupAllocation(variant="Small", groups=["beta-testers"])], + ), + # Telemetry configuration and free-form metadata/tags. + telemetry=FeatureFlagTelemetryConfiguration(enabled=True, metadata={"owner": "team-a"}), + tags={"env": "prod", "team": "core"}, + ) + client.set_feature_flag(rich_flag) + fetched = client.get_feature_flag("RichFeature") + if fetched is not None: + print(f" name={fetched.name}") + print(f" requirement_type={fetched.conditions.requirement_type}") + print(f" variants={[v.name for v in fetched.variants]}") + print(f" default_when_enabled={fetched.allocation.default_when_enabled}") + print(f" telemetry_enabled={fetched.telemetry.enabled}") + print(f" tags={fetched.tags}") + print("") + + print("List feature flags") + # [START list_feature_flags] + for f in client.list_feature_flags(): + print(f" {f.name}: enabled={f.enabled}") + # [END list_feature_flags] + print("") + + print("List revisions of a feature flag") + # [START list_feature_flag_revisions] + for revision in client.list_feature_flag_revisions(feature_id_filter="RichFeature"): + print(f" {revision.name}: last_modified={revision.last_modified}") + # [END list_feature_flag_revisions] + print("") + + print("Delete feature flags") + # [START delete_feature_flag] + client.delete_feature_flag("SampleFeature") + client.delete_feature_flag("RichFeature") + # [END delete_feature_flag] + + +if __name__ == "__main__": + main() diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py new file mode 100644 index 000000000000..494122378322 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py @@ -0,0 +1,131 @@ +# coding: utf-8 + +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +""" +FILE: feature_flag_sample_async.py + +DESCRIPTION: + This sample demos how to set, get, list, and delete feature flags using the + dedicated feature flag endpoint methods asynchronously. It also shows how to build + a fully populated FeatureFlag with conditions, variants, allocation, and telemetry. + +USAGE: python feature_flag_sample_async.py + + Set the environment variables with your own values before running the sample: + 1) APPCONFIGURATION_ENDPOINT_STRING: Endpoint URL used to access the Azure App Configuration. +""" + +import asyncio +import os +from azure.identity.aio import DefaultAzureCredential +from azure.appconfiguration.aio import AzureAppConfigurationClient +from azure.appconfiguration import ( + FeatureFlag, + FeatureFlagConditions, + FeatureFlagFilter, + FeatureFlagVariantDefinition, + FeatureFlagAllocation, + FeatureFlagTelemetryConfiguration, + PercentileAllocation, + UserAllocation, + GroupAllocation, +) + + +async def main(): + endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"] + credential = DefaultAzureCredential() + client = AzureAppConfigurationClient(base_url=endpoint, credential=credential) + + print("Set a simple feature flag") + # [START set_feature_flag_async] + flag = FeatureFlag(name="SampleFeature", enabled=True, description="A simple on/off feature flag") + created = await client.set_feature_flag(flag) + # [END set_feature_flag_async] + print(f" name={created.name}, enabled={created.enabled}") + print("") + + print("Get a feature flag") + # [START get_feature_flag_async] + retrieved = await client.get_feature_flag("SampleFeature") + # [END get_feature_flag_async] + if retrieved is not None: + print(f" name={retrieved.name}, enabled={retrieved.enabled}, description={retrieved.description}") + print("") + + print("Set a fully populated feature flag (conditions, variants, allocation, telemetry, tags)") + rich_flag = FeatureFlag( + name="RichFeature", + enabled=True, + description="A feature flag using every part of the model", + # Conditions gate the feature with client filters. + conditions=FeatureFlagConditions( + requirement_type="All", + client_filters=[ + FeatureFlagFilter(name="Microsoft.TimeWindow", parameters={"Start": "Mon, 01 Jan 2024 00:00:00 GMT"}), + FeatureFlagFilter(name="Microsoft.Percentage", parameters={"Value": 50}), + ], + ), + # Variants describe the possible values the feature can resolve to. + variants=[ + FeatureFlagVariantDefinition(name="Large", value="large", status_override="Enabled"), + FeatureFlagVariantDefinition(name="Small", value="small"), + ], + # Allocation controls how users/groups/percentiles map to variants. + allocation=FeatureFlagAllocation( + default_when_enabled="Large", + default_when_disabled="Small", + seed="sample-seed", + percentile=[ + PercentileAllocation(variant="Large", percentile_from=0, percentile_to=50), + PercentileAllocation(variant="Small", percentile_from=50, percentile_to=100), + ], + user=[UserAllocation(variant="Large", users=["alice", "bob"])], + group=[GroupAllocation(variant="Small", groups=["beta-testers"])], + ), + # Telemetry configuration and free-form metadata/tags. + telemetry=FeatureFlagTelemetryConfiguration(enabled=True, metadata={"owner": "team-a"}), + tags={"env": "prod", "team": "core"}, + ) + await client.set_feature_flag(rich_flag) + fetched = await client.get_feature_flag("RichFeature") + if fetched is not None: + print(f" name={fetched.name}") + print(f" requirement_type={fetched.conditions.requirement_type}") + print(f" variants={[v.name for v in fetched.variants]}") + print(f" default_when_enabled={fetched.allocation.default_when_enabled}") + print(f" telemetry_enabled={fetched.telemetry.enabled}") + print(f" tags={fetched.tags}") + print("") + + print("List feature flags") + # [START list_feature_flags_async] + async for f in client.list_feature_flags(): + print(f" {f.name}: enabled={f.enabled}") + # [END list_feature_flags_async] + print("") + + print("List revisions of a feature flag") + # [START list_feature_flag_revisions_async] + async for revision in client.list_feature_flag_revisions(feature_id_filter="RichFeature"): + print(f" {revision.name}: last_modified={revision.last_modified}") + # [END list_feature_flag_revisions_async] + print("") + + print("Delete feature flags") + # [START delete_feature_flag_async] + await client.delete_feature_flag("SampleFeature") + await client.delete_feature_flag("RichFeature") + # [END delete_feature_flag_async] + + await client.close() + await credential.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py index d3fa647c818b..715b959562d4 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py @@ -6,16 +6,16 @@ # -------------------------------------------------------------------------- import os from typing import List -from testcase import AppConfigTestCase +from testcase import AppConfigTestCase, _AUDIENCE_UNSET from azure.appconfiguration.aio import AzureAppConfigurationClient from azure.core.async_paging import AsyncItemPaged # pylint: disable=invalid-overridden-method class AsyncAppConfigTestCase(AppConfigTestCase): - def create_client(self, appconfiguration_endpoint_string, audience=None): + def create_client(self, appconfiguration_endpoint_string, audience=_AUDIENCE_UNSET): cred = self.get_credential(AzureAppConfigurationClient, is_async=True) - if audience is None: + if audience is _AUDIENCE_UNSET: audience = os.environ.get("APPCONFIGURATION_AUDIENCE") return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, audience=audience) diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_audience_error_handling_live.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_audience_error_handling_live.py index 6e511153af10..898e1d6af870 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_audience_error_handling_live.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_audience_error_handling_live.py @@ -34,8 +34,8 @@ class TestAudienceErrorHandlingLive(AppConfigTestCase): @recorded_by_proxy def test_client_has_audience_policy_with_no_audience(self, appconfiguration_endpoint_string): """Test that client created without audience has policy with has_audience=False.""" - # Create client without audience - client = self.create_client(appconfiguration_endpoint_string) + # Create client without audience (explicit None bypasses the env-var fallback) + client = self.create_client(appconfiguration_endpoint_string, audience=None) # Check that audience error handling policy is in the pipeline policies = client._impl._client._pipeline._impl_policies diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_audience_error_handling_live_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_audience_error_handling_live_async.py index 16a81b668234..074bee242c07 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_audience_error_handling_live_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_audience_error_handling_live_async.py @@ -35,8 +35,8 @@ class TestAudienceErrorHandlingLiveAsync(AsyncAppConfigTestCase): @recorded_by_proxy_async async def test_async_client_has_audience_policy_with_no_audience(self, appconfiguration_endpoint_string): """Test that async client created without audience has policy with has_audience=False.""" - # Create client without audience - client = self.create_client(appconfiguration_endpoint_string) + # Create client without audience (explicit None bypasses the env-var fallback) + client = self.create_client(appconfiguration_endpoint_string, audience=None) # Check that audience error handling policy is in the pipeline policies = client._impl._client._pipeline._impl_policies diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py index b3e48edb4285..959978ac1534 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py @@ -466,8 +466,9 @@ def test_list_revisions_with_tags_filter(self, appconfiguration_endpoint_string) self.set_up(appconfiguration_endpoint_string) items = list(self.client.list_revisions(tags_filter=["tag1=value1"])) assert len(items) >= 1 - assert all(x.key == KEY for x in items) - assert all(x.label == LABEL for x in items) + # list_revisions returns full revision history, which on a shared live store may include + # revisions of other keys that previously carried this tag. Verify the filter itself instead. + assert all(x.tags.get("tag1") == "value1" for x in items) self.tear_down() @AppConfigPreparer() diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py index 92dda975b3a9..7ac4c2724811 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py @@ -476,8 +476,9 @@ async def test_list_revisions_with_tags_filter(self, appconfiguration_endpoint_s await self.set_up(appconfiguration_endpoint_string) items = await self.convert_to_list(self.client.list_revisions(tags_filter=["tag1=value1"])) assert len(items) >= 1 - assert all(x.key == KEY for x in items) - assert all(x.label == LABEL for x in items) + # list_revisions returns full revision history, which on a shared live store may include + # revisions of other keys that previously carried this tag. Verify the filter itself instead. + assert all(x.tags.get("tag1") == "value1" for x in items) await self.tear_down() @AppConfigPreparer() diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py index 90d7d15cebff..e985612b6a5b 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py @@ -14,6 +14,14 @@ from azure.appconfiguration import ( AzureAppConfigurationClient, FeatureFlag, + FeatureFlagConditions, + FeatureFlagFilter, + FeatureFlagVariantDefinition, + FeatureFlagAllocation, + FeatureFlagTelemetryConfiguration, + PercentileAllocation, + UserAllocation, + GroupAllocation, ) AppConfigPreparer = functools.partial( @@ -215,3 +223,170 @@ def test_feature_flag_with_label(self, appconfiguration_endpoint_string): # Clean up client.delete_feature_flag("feature_with_label", label="prod") client.delete_feature_flag("feature_with_label", label="staging") + + @AppConfigPreparer() + @recorded_by_proxy + def test_feature_flag_with_conditions(self, appconfiguration_endpoint_string): + """Test a feature flag that uses conditions with client filters.""" + client = self.create_client(appconfiguration_endpoint_string) + + feature_flag = FeatureFlag( + name="test_feature_conditions", + enabled=True, + description="A feature flag gated by client filters", + conditions=FeatureFlagConditions( + requirement_type="All", + client_filters=[ + FeatureFlagFilter(name="Microsoft.TimeWindow", parameters={"Start": "Mon, 01 Jan 2024 00:00:00 GMT"}), + FeatureFlagFilter(name="Microsoft.Percentage", parameters={"Value": 50}), + ], + ), + ) + created = client.set_feature_flag(feature_flag) + + retrieved = client.get_feature_flag("test_feature_conditions", label=created.label) + assert retrieved is not None + assert retrieved.description == "A feature flag gated by client filters" + assert retrieved.conditions is not None + assert retrieved.conditions.requirement_type == "All" + assert retrieved.conditions.client_filters is not None + assert len(retrieved.conditions.client_filters) == 2 + filter_names = {f.name for f in retrieved.conditions.client_filters} + assert "Microsoft.TimeWindow" in filter_names + assert "Microsoft.Percentage" in filter_names + + client.delete_feature_flag("test_feature_conditions", label=created.label) + + @AppConfigPreparer() + @recorded_by_proxy + def test_feature_flag_with_variants_and_allocation(self, appconfiguration_endpoint_string): + """Test a feature flag with variants and a full allocation (percentile/user/group).""" + client = self.create_client(appconfiguration_endpoint_string) + + feature_flag = FeatureFlag( + name="test_feature_variants", + enabled=True, + variants=[ + FeatureFlagVariantDefinition(name="On", value="true", status_override="Enabled"), + FeatureFlagVariantDefinition(name="Off", value="false", status_override="Disabled"), + ], + allocation=FeatureFlagAllocation( + default_when_enabled="On", + default_when_disabled="Off", + seed="test-seed", + percentile=[ + PercentileAllocation(variant="On", percentile_from=0, percentile_to=50), + PercentileAllocation(variant="Off", percentile_from=50, percentile_to=100), + ], + user=[UserAllocation(variant="On", users=["alice", "bob"])], + group=[GroupAllocation(variant="Off", groups=["beta-testers"])], + ), + ) + created = client.set_feature_flag(feature_flag) + + retrieved = client.get_feature_flag("test_feature_variants", label=created.label) + assert retrieved is not None + assert retrieved.variants is not None + assert len(retrieved.variants) == 2 + variant_names = {v.name for v in retrieved.variants} + assert variant_names == {"On", "Off"} + + assert retrieved.allocation is not None + assert retrieved.allocation.default_when_enabled == "On" + assert retrieved.allocation.default_when_disabled == "Off" + assert retrieved.allocation.seed == "test-seed" + assert retrieved.allocation.percentile is not None + assert len(retrieved.allocation.percentile) == 2 + assert retrieved.allocation.percentile[0].percentile_from == 0 + assert retrieved.allocation.percentile[0].percentile_to == 50 + assert retrieved.allocation.user is not None + assert retrieved.allocation.user[0].users == ["alice", "bob"] + assert retrieved.allocation.group is not None + assert retrieved.allocation.group[0].groups == ["beta-testers"] + + client.delete_feature_flag("test_feature_variants", label=created.label) + + @AppConfigPreparer() + @recorded_by_proxy + def test_feature_flag_with_telemetry_and_tags(self, appconfiguration_endpoint_string): + """Test a feature flag with telemetry configuration and tags.""" + client = self.create_client(appconfiguration_endpoint_string) + + feature_flag = FeatureFlag( + name="test_feature_telemetry", + enabled=True, + telemetry=FeatureFlagTelemetryConfiguration( + enabled=True, + metadata={"owner": "team-a", "tier": "premium"}, + ), + tags={"env": "test", "team": "core"}, + ) + created = client.set_feature_flag(feature_flag) + + retrieved = client.get_feature_flag("test_feature_telemetry", label=created.label) + assert retrieved is not None + assert retrieved.telemetry is not None + assert retrieved.telemetry.enabled == True + assert retrieved.telemetry.metadata == {"owner": "team-a", "tier": "premium"} + assert retrieved.tags == {"env": "test", "team": "core"} + + client.delete_feature_flag("test_feature_telemetry", label=created.label) + + @AppConfigPreparer() + @recorded_by_proxy + def test_feature_flag_full_model_roundtrip(self, appconfiguration_endpoint_string): + """Test setting and retrieving a feature flag that populates every field of the model.""" + client = self.create_client(appconfiguration_endpoint_string) + + feature_flag = FeatureFlag( + name="test_feature_full", + enabled=True, + label="prod", + description="A fully populated feature flag", + conditions=FeatureFlagConditions( + requirement_type="Any", + client_filters=[ + FeatureFlagFilter(name="Microsoft.Targeting", parameters={"Audience": {"DefaultRolloutPercentage": 25}}), + ], + ), + variants=[ + FeatureFlagVariantDefinition( + name="Large", value="large", content_type="text/plain", status_override="Enabled" + ), + FeatureFlagVariantDefinition(name="Small", value="small"), + ], + allocation=FeatureFlagAllocation( + default_when_enabled="Large", + default_when_disabled="Small", + seed="full-seed", + percentile=[PercentileAllocation(variant="Large", percentile_from=0, percentile_to=100)], + user=[UserAllocation(variant="Large", users=["carol"])], + group=[GroupAllocation(variant="Small", groups=["internal"])], + ), + telemetry=FeatureFlagTelemetryConfiguration(enabled=True, metadata={"source": "test"}), + tags={"env": "prod", "critical": "true"}, + ) + created = client.set_feature_flag(feature_flag) + + retrieved = client.get_feature_flag("test_feature_full", label="prod") + assert retrieved is not None + assert retrieved.name == "test_feature_full" + assert retrieved.enabled == True + assert retrieved.label == "prod" + assert retrieved.description == "A fully populated feature flag" + assert retrieved.conditions is not None + assert retrieved.conditions.requirement_type == "Any" + assert retrieved.conditions.client_filters is not None + assert retrieved.conditions.client_filters[0].name == "Microsoft.Targeting" + assert retrieved.variants is not None + assert len(retrieved.variants) == 2 + assert retrieved.variants[0].content_type == "text/plain" + assert retrieved.allocation is not None + assert retrieved.allocation.default_when_enabled == "Large" + assert retrieved.telemetry is not None + assert retrieved.telemetry.metadata == {"source": "test"} + assert retrieved.tags == {"env": "prod", "critical": "true"} + assert retrieved.etag is not None + assert retrieved.last_modified is not None + + client.delete_feature_flag("test_feature_full", label="prod") diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py index 2ce2ee31c8e5..6c444e1d3e57 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py @@ -14,6 +14,14 @@ from asynctestcase import AsyncAppConfigTestCase from azure.appconfiguration import ( FeatureFlag, + FeatureFlagConditions, + FeatureFlagFilter, + FeatureFlagVariantDefinition, + FeatureFlagAllocation, + FeatureFlagTelemetryConfiguration, + PercentileAllocation, + UserAllocation, + GroupAllocation, ) from azure.appconfiguration.aio import AzureAppConfigurationClient @@ -225,3 +233,174 @@ async def test_feature_flag_with_label(self, appconfiguration_endpoint_string): await client.delete_feature_flag("feature_with_label", label="prod") await client.delete_feature_flag("feature_with_label", label="staging") await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_feature_flag_with_conditions(self, appconfiguration_endpoint_string): + """Test a feature flag that uses conditions with client filters.""" + client = self.create_client(appconfiguration_endpoint_string) + + feature_flag = FeatureFlag( + name="test_feature_conditions", + enabled=True, + description="A feature flag gated by client filters", + conditions=FeatureFlagConditions( + requirement_type="All", + client_filters=[ + FeatureFlagFilter(name="Microsoft.TimeWindow", parameters={"Start": "Mon, 01 Jan 2024 00:00:00 GMT"}), + FeatureFlagFilter(name="Microsoft.Percentage", parameters={"Value": 50}), + ], + ), + ) + created = await client.set_feature_flag(feature_flag) + + retrieved = await client.get_feature_flag("test_feature_conditions", label=created.label) + assert retrieved is not None + assert retrieved.description == "A feature flag gated by client filters" + assert retrieved.conditions is not None + assert retrieved.conditions.requirement_type == "All" + assert retrieved.conditions.client_filters is not None + assert len(retrieved.conditions.client_filters) == 2 + filter_names = {f.name for f in retrieved.conditions.client_filters} + assert "Microsoft.TimeWindow" in filter_names + assert "Microsoft.Percentage" in filter_names + + await client.delete_feature_flag("test_feature_conditions", label=created.label) + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_feature_flag_with_variants_and_allocation(self, appconfiguration_endpoint_string): + """Test a feature flag with variants and a full allocation (percentile/user/group).""" + client = self.create_client(appconfiguration_endpoint_string) + + feature_flag = FeatureFlag( + name="test_feature_variants", + enabled=True, + variants=[ + FeatureFlagVariantDefinition(name="On", value="true", status_override="Enabled"), + FeatureFlagVariantDefinition(name="Off", value="false", status_override="Disabled"), + ], + allocation=FeatureFlagAllocation( + default_when_enabled="On", + default_when_disabled="Off", + seed="test-seed", + percentile=[ + PercentileAllocation(variant="On", percentile_from=0, percentile_to=50), + PercentileAllocation(variant="Off", percentile_from=50, percentile_to=100), + ], + user=[UserAllocation(variant="On", users=["alice", "bob"])], + group=[GroupAllocation(variant="Off", groups=["beta-testers"])], + ), + ) + created = await client.set_feature_flag(feature_flag) + + retrieved = await client.get_feature_flag("test_feature_variants", label=created.label) + assert retrieved is not None + assert retrieved.variants is not None + assert len(retrieved.variants) == 2 + variant_names = {v.name for v in retrieved.variants} + assert variant_names == {"On", "Off"} + + assert retrieved.allocation is not None + assert retrieved.allocation.default_when_enabled == "On" + assert retrieved.allocation.default_when_disabled == "Off" + assert retrieved.allocation.seed == "test-seed" + assert retrieved.allocation.percentile is not None + assert len(retrieved.allocation.percentile) == 2 + assert retrieved.allocation.percentile[0].percentile_from == 0 + assert retrieved.allocation.percentile[0].percentile_to == 50 + assert retrieved.allocation.user is not None + assert retrieved.allocation.user[0].users == ["alice", "bob"] + assert retrieved.allocation.group is not None + assert retrieved.allocation.group[0].groups == ["beta-testers"] + + await client.delete_feature_flag("test_feature_variants", label=created.label) + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_feature_flag_with_telemetry_and_tags(self, appconfiguration_endpoint_string): + """Test a feature flag with telemetry configuration and tags.""" + client = self.create_client(appconfiguration_endpoint_string) + + feature_flag = FeatureFlag( + name="test_feature_telemetry", + enabled=True, + telemetry=FeatureFlagTelemetryConfiguration( + enabled=True, + metadata={"owner": "team-a", "tier": "premium"}, + ), + tags={"env": "test", "team": "core"}, + ) + created = await client.set_feature_flag(feature_flag) + + retrieved = await client.get_feature_flag("test_feature_telemetry", label=created.label) + assert retrieved is not None + assert retrieved.telemetry is not None + assert retrieved.telemetry.enabled == True + assert retrieved.telemetry.metadata == {"owner": "team-a", "tier": "premium"} + assert retrieved.tags == {"env": "test", "team": "core"} + + await client.delete_feature_flag("test_feature_telemetry", label=created.label) + await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_feature_flag_full_model_roundtrip(self, appconfiguration_endpoint_string): + """Test setting and retrieving a feature flag that populates every field of the model.""" + client = self.create_client(appconfiguration_endpoint_string) + + feature_flag = FeatureFlag( + name="test_feature_full", + enabled=True, + label="prod", + description="A fully populated feature flag", + conditions=FeatureFlagConditions( + requirement_type="Any", + client_filters=[ + FeatureFlagFilter(name="Microsoft.Targeting", parameters={"Audience": {"DefaultRolloutPercentage": 25}}), + ], + ), + variants=[ + FeatureFlagVariantDefinition( + name="Large", value="large", content_type="text/plain", status_override="Enabled" + ), + FeatureFlagVariantDefinition(name="Small", value="small"), + ], + allocation=FeatureFlagAllocation( + default_when_enabled="Large", + default_when_disabled="Small", + seed="full-seed", + percentile=[PercentileAllocation(variant="Large", percentile_from=0, percentile_to=100)], + user=[UserAllocation(variant="Large", users=["carol"])], + group=[GroupAllocation(variant="Small", groups=["internal"])], + ), + telemetry=FeatureFlagTelemetryConfiguration(enabled=True, metadata={"source": "test"}), + tags={"env": "prod", "critical": "true"}, + ) + created = await client.set_feature_flag(feature_flag) + + retrieved = await client.get_feature_flag("test_feature_full", label="prod") + assert retrieved is not None + assert retrieved.name == "test_feature_full" + assert retrieved.enabled == True + assert retrieved.label == "prod" + assert retrieved.description == "A fully populated feature flag" + assert retrieved.conditions is not None + assert retrieved.conditions.requirement_type == "Any" + assert retrieved.conditions.client_filters is not None + assert retrieved.conditions.client_filters[0].name == "Microsoft.Targeting" + assert retrieved.variants is not None + assert len(retrieved.variants) == 2 + assert retrieved.variants[0].content_type == "text/plain" + assert retrieved.allocation is not None + assert retrieved.allocation.default_when_enabled == "Large" + assert retrieved.telemetry is not None + assert retrieved.telemetry.metadata == {"source": "test"} + assert retrieved.tags == {"env": "prod", "critical": "true"} + assert retrieved.etag is not None + assert retrieved.last_modified is not None + + await client.delete_feature_flag("test_feature_full", label="prod") + await client.close() diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py index c4f0d8b0cbe3..5be5a818c4a9 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py @@ -20,13 +20,17 @@ ConfigurationSnapshot, ) +# Sentinel distinguishing "argument omitted" (apply env-var fallback) from an +# explicit ``audience=None`` (force no audience). +_AUDIENCE_UNSET = object() + class AppConfigTestCase(AzureRecordedTestCase): client = None - def create_client(self, appconfiguration_endpoint_string, audience=None): + def create_client(self, appconfiguration_endpoint_string, audience=_AUDIENCE_UNSET): cred = self.get_credential(AzureAppConfigurationClient) - if audience is None: + if audience is _AUDIENCE_UNSET: audience = os.environ.get("APPCONFIGURATION_AUDIENCE") return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, audience=audience) From d762028b4c6ef2d15a46f2d82ba55a302d645631 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 23 Jun 2026 09:08:10 -0700 Subject: [PATCH 04/25] Update README.md --- .../azure-appconfiguration/README.md | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/sdk/appconfiguration/azure-appconfiguration/README.md b/sdk/appconfiguration/azure-appconfiguration/README.md index fa289e2cf95d..824708114fef 100644 --- a/sdk/appconfiguration/azure-appconfiguration/README.md +++ b/sdk/appconfiguration/azure-appconfiguration/README.md @@ -168,6 +168,20 @@ etag : str Azure App Configuration allows users to create a point-in-time snapshot of their configuration store, providing them with the ability to treat settings as one consistent version. This feature enables applications to hold a consistent view of configuration, ensuring that there are no version mismatches to individual settings due to reading as updates were made. Snapshots are immutable, ensuring that configuration can confidently be rolled back to a last-known-good configuration in the event of a problem. +### Feature Flag + +A Feature Flag is a special kind of configuration that turns application functionality on or off at runtime. Azure App Configuration exposes a dedicated feature flag endpoint that works with the strongly typed `FeatureFlag` model rather than raw configuration settings. + +A `FeatureFlag` can be as simple as a name with an `enabled` flag, or it can carry a rich model: + +* `conditions` / `client_filters` - gate the feature with filters such as time windows, percentages, or targeting. +* `variants` - the possible values the feature can resolve to. +* `allocation` - how percentiles, users, and groups map to variants. +* `telemetry` - telemetry configuration and free-form metadata. +* `tags` - user-defined tags for organizing feature flags. + +The feature flag endpoint methods are `set_feature_flag`, `get_feature_flag`, `list_feature_flags`, `list_feature_flag_revisions`, and `delete_feature_flag`. + ## Examples The following sections provide several code snippets covering some of the most common Configuration Service tasks, including: @@ -182,6 +196,10 @@ The following sections provide several code snippets covering some of the most c * [Recover a Snapshot](#recover-a-snapshot) * [List Snapshots](#list-snapshots) * [List Configuration Settings of a Snapshot](#list-configuration-settings-of-a-snapshot) +* [Set a Feature Flag](#set-a-feature-flag) +* [Get a Feature Flag](#get-a-feature-flag) +* [List Feature Flags](#list-feature-flags) +* [Delete a Feature Flag](#delete-a-feature-flag) * [Async APIs](#async-apis) ### Create a Configuration Setting @@ -379,6 +397,51 @@ for config_setting in client.list_configuration_settings(snapshot_name=snapshot_ +### Set a Feature Flag + +Create or update a feature flag through the dedicated feature flag endpoint. A feature flag can be a simple on/off toggle or carry the full model (conditions, variants, allocation, telemetry, tags). + + + +```python +flag = FeatureFlag(name="SampleFeature", enabled=True, description="A simple on/off feature flag") +created = client.set_feature_flag(flag) +``` + + + +### Get a Feature Flag + + + +```python +retrieved = client.get_feature_flag("SampleFeature") +``` + + + +### List Feature Flags + + + +```python +for f in client.list_feature_flags(): + print(f" {f.name}: enabled={f.enabled}") +``` + + + +### Delete a Feature Flag + + + +```python +client.delete_feature_flag("SampleFeature") +client.delete_feature_flag("RichFeature") +``` + + + ### Async APIs Async client is supported. @@ -436,6 +499,7 @@ Several App Configuration client library samples are available to you in this Gi - [Read revision history](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/list_revision_sample.py) / [Async version](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/list_revision_sample_async.py) - [Get a setting if changed](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/conditional_operation_sample.py) / [Async version](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/conditional_operation_sample_async.py) - [Create, retrieve and update status of a configuration settings snapshot](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/snapshot_sample.py) / [Async version](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/snapshot_sample_async.py) +- [Set, get, list and delete feature flags (including the full FeatureFlag model)](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py) / [Async version](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py) - [Send custom HTTP requests](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/send_request_sample.py) / [Async version](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/send_request_sample_async.py) - [Update AzureAppConfigurationClient sync_token](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/sync_token_sample.py) / [Async version](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/samples/sync_token_sample_async.py) From 5c9807bb6e5dc253a1b586c4c1505952ad724589 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 23 Jun 2026 10:46:40 -0700 Subject: [PATCH 05/25] Labels endpoint --- .../azure-appconfiguration/README.md | 18 +++++++++++++ .../_azure_appconfiguration_client.py | 5 ++++ .../_azure_appconfiguration_client_async.py | 5 ++++ .../samples/list_labels_sample.py | 12 +++++++++ .../samples/list_labels_sample_async.py | 10 +++++++ .../test_azure_appconfiguration_client.py | 26 ++++++++++++++++++ ...est_azure_appconfiguration_client_async.py | 27 +++++++++++++++++++ 7 files changed, 103 insertions(+) diff --git a/sdk/appconfiguration/azure-appconfiguration/README.md b/sdk/appconfiguration/azure-appconfiguration/README.md index 824708114fef..6d7e28c6aa8b 100644 --- a/sdk/appconfiguration/azure-appconfiguration/README.md +++ b/sdk/appconfiguration/azure-appconfiguration/README.md @@ -331,6 +331,24 @@ for config_setting in config_settings: +You can also filter labels by the type of resource they belong to with `resource_type`, using `"kv"` for key-value settings or `"ff"` for feature flags. + + + +```python +print("List labels used by key-value settings only") +config_settings = client.list_labels(resource_type="kv") +for config_setting in config_settings: + print(config_setting) + +print("List labels used by feature flags only") +config_settings = client.list_labels(resource_type="ff") +for config_setting in config_settings: + print(config_setting) +``` + + + ### Create a Snapshot diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py index 575c9c4c5b14..820d0eb7fed7 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py @@ -640,6 +640,7 @@ def list_labels( after: Optional[str] = None, accept_datetime: Optional[Union[datetime, str]] = None, fields: Optional[List[Union[str, LabelFields]]] = None, + resource_type: Optional[str] = None, **kwargs: Any, ) -> ItemPaged[ConfigurationSettingLabel]: """Gets a list of labels. @@ -657,6 +658,9 @@ def list_labels( :keyword fields: Specify which fields to include in the results. If not specified, will include all fields. Available fields see :class:`~azure.appconfiguration.LabelFields`. :paramtype fields: list[str] or list[~azure.appconfiguration.LabelFields] or None + :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted values: + 'kv' for key-value labels, 'ff' for feature flag labels. Default is `None`. + :paramtype resource_type: str or None :return: An iterator of labels. :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.ConfigurationSettingLabel] :raises: :class:`~azure.core.exceptions.HttpResponseError` @@ -668,6 +672,7 @@ def list_labels( after=after, accept_datetime=accept_datetime, select=fields, + resource_type=resource_type, cls=lambda objs: [ConfigurationSettingLabel(name=x.name) for x in objs], **kwargs, ) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py index 48455c82d299..a39e0ed20141 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py @@ -652,6 +652,7 @@ def list_labels( after: Optional[str] = None, accept_datetime: Optional[Union[datetime, str]] = None, fields: Optional[List[Union[str, LabelFields]]] = None, + resource_type: Optional[str] = None, **kwargs: Any, ) -> AsyncItemPaged[ConfigurationSettingLabel]: """Gets a list of labels. @@ -669,6 +670,9 @@ def list_labels( :keyword fields: Specify which fields to include in the results. If not specified, will include all fields. Available fields see :class:`~azure.appconfiguration.LabelFields`. :paramtype fields: list[str] or list[~azure.appconfiguration.LabelFields] or None + :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted values: + 'kv' for key-value labels, 'ff' for feature flag labels. Default is `None`. + :paramtype resource_type: str or None :return: An async iterator of labels. :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration.ConfigurationSettingLabel] :raises: :class:`~azure.core.exceptions.HttpResponseError` @@ -680,6 +684,7 @@ def list_labels( after=after, accept_datetime=accept_datetime, select=fields, + resource_type=resource_type, cls=lambda objs: [ConfigurationSettingLabel(name=x.name) for x in objs], **kwargs, ) diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/list_labels_sample.py b/sdk/appconfiguration/azure-appconfiguration/samples/list_labels_sample.py index e50b9b5c1501..491bec958636 100644 --- a/sdk/appconfiguration/azure-appconfiguration/samples/list_labels_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration/samples/list_labels_sample.py @@ -54,6 +54,18 @@ def main(): print(config_setting) # [END list_labels] + # [START list_labels_by_resource_type] + print("List labels used by key-value settings only") + config_settings = client.list_labels(resource_type="kv") + for config_setting in config_settings: + print(config_setting) + + print("List labels used by feature flags only") + config_settings = client.list_labels(resource_type="ff") + for config_setting in config_settings: + print(config_setting) + # [END list_labels_by_resource_type] + client.delete_configuration_setting(key="MyKey1", label="my label1") client.delete_configuration_setting(key="MyKey2", label="my label2") diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/list_labels_sample_async.py b/sdk/appconfiguration/azure-appconfiguration/samples/list_labels_sample_async.py index ef3334ac1719..0d68ffda6d01 100644 --- a/sdk/appconfiguration/azure-appconfiguration/samples/list_labels_sample_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/samples/list_labels_sample_async.py @@ -54,6 +54,16 @@ async def main(): async for config_setting in config_settings: print(config_setting) + print("List labels used by key-value settings only") + config_settings = client.list_labels(resource_type="kv") + async for config_setting in config_settings: + print(config_setting) + + print("List labels used by feature flags only") + config_settings = client.list_labels(resource_type="ff") + async for config_setting in config_settings: + print(config_setting) + await client.delete_configuration_setting(key="MyKey1", label="my label1") await client.delete_configuration_setting(key="MyKey2", label="my label2") diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py index 959978ac1534..212855c42304 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py @@ -41,6 +41,7 @@ ConfigurationSettingsFilter, SecretReferenceConfigurationSetting, FeatureFlagConfigurationSetting, + FeatureFlag, FILTER_PERCENTAGE, FILTER_TARGETING, FILTER_TIME_WINDOW, @@ -1289,6 +1290,31 @@ def test_list_labels(self, appconfiguration_endpoint_string): rep = self.client.list_labels() assert len(list(rep)) == 0 + @AppConfigPreparer() + @recorded_by_proxy + def test_list_labels_resource_type(self, appconfiguration_endpoint_string): + self.set_up(appconfiguration_endpoint_string) + # set_up creates key-value settings labeled LABEL. Add a dedicated feature flag + # (written through the feature-flag endpoint) with a distinct label so we can + # verify resource_type segregates key-value labels ('kv') from feature-flag + # labels ('ff'). + ff_label = "ff_resource_type_" + LABEL + feature_flag = FeatureFlag(name="resource_type_feature", enabled=True, label=ff_label) + self.client.set_feature_flag(feature_flag) + try: + # resource_type="kv" returns labels used by key-value settings only. + kv_labels = {item.name for item in self.client.list_labels(resource_type="kv")} + assert LABEL in kv_labels + assert ff_label not in kv_labels + + # resource_type="ff" returns labels used by feature flags only. + ff_labels = {item.name for item in self.client.list_labels(resource_type="ff")} + assert ff_label in ff_labels + assert LABEL not in ff_labels + finally: + self.client.delete_feature_flag("resource_type_feature", label=ff_label) + self.tear_down() + class TestAppConfigurationClientUnitTest: def test_type_error(self): diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py index 7ac4c2724811..0fbb5e997cd8 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py @@ -41,6 +41,7 @@ ConfigurationSettingsFilter, SecretReferenceConfigurationSetting, FeatureFlagConfigurationSetting, + FeatureFlag, FILTER_PERCENTAGE, FILTER_TARGETING, FILTER_TIME_WINDOW, @@ -1336,6 +1337,32 @@ async def test_list_labels(self, appconfiguration_endpoint_string): assert len(list(rep)) == 0 await self.client.close() + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_list_labels_resource_type(self, appconfiguration_endpoint_string): + await self.set_up(appconfiguration_endpoint_string) + # set_up creates key-value settings labeled LABEL. Add a dedicated feature flag + # (written through the feature-flag endpoint) with a distinct label so we can + # verify resource_type segregates key-value labels ('kv') from feature-flag + # labels ('ff'). + ff_label = "ff_resource_type_" + LABEL + feature_flag = FeatureFlag(name="resource_type_feature", enabled=True, label=ff_label) + await self.client.set_feature_flag(feature_flag) + try: + # resource_type="kv" returns labels used by key-value settings only. + kv_labels = {item.name for item in await self.convert_to_list(self.client.list_labels(resource_type="kv"))} + assert LABEL in kv_labels + assert ff_label not in kv_labels + + # resource_type="ff" returns labels used by feature flags only. + ff_labels = {item.name for item in await self.convert_to_list(self.client.list_labels(resource_type="ff"))} + assert ff_label in ff_labels + assert LABEL not in ff_labels + finally: + await self.client.delete_feature_flag("resource_type_feature", label=ff_label) + await self.tear_down() + await self.client.close() + class TestAppConfigurationClientUnitTest: def test_type_error(self): From 61b1c99d67c82d7732e1439bf4b5c61842ab8e41 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 23 Jun 2026 14:11:40 -0700 Subject: [PATCH 06/25] fixes --- .../apiview-properties.json | 2 +- .../azure-appconfiguration/assets.json | 2 +- .../_azure_appconfiguration_client.py | 11 +-- .../_generated/_operations/_operations.py | 4 +- .../azure/appconfiguration/_models.py | 49 +++++------ .../_azure_appconfiguration_client_async.py | 11 +-- .../tests/test_feature_flags.py | 81 +++++++++---------- .../tests/test_feature_flags_async.py | 81 +++++++++---------- .../azure-appconfiguration/tsp-location.yaml | 2 +- 9 files changed, 107 insertions(+), 136 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json b/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json index 543733d8cab7..6b109c7bf209 100644 --- a/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json +++ b/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json @@ -82,5 +82,5 @@ "azure.appconfiguration.AzureAppConfigurationClient.check_revisions": "AzureAppConfiguration.checkRevisions", "azure.appconfiguration.aio.AzureAppConfigurationClient.check_revisions": "AzureAppConfiguration.checkRevisions" }, - "CrossLanguageVersion": "9fdd341c223f" + "CrossLanguageVersion": "f60baf2bc06c" } \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration/assets.json b/sdk/appconfiguration/azure-appconfiguration/assets.json index 6f560b9fdef3..eec3394a0f36 100644 --- a/sdk/appconfiguration/azure-appconfiguration/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration", - "Tag": "python/appconfiguration/azure-appconfiguration_13a99205dc" + "Tag": "python/appconfiguration/azure-appconfiguration_cd1c4e2888" } diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py index 820d0eb7fed7..0b600e9b59cd 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py @@ -1,4 +1,4 @@ -# pylint: disable=line-too-long,useless-suppression +# pylint: disable=line-too-long,useless-suppression,too-many-lines # ------------------------------------------------------------------------ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for @@ -86,7 +86,7 @@ def __init__(self, base_url: str, credential: TokenCredential, **kwargs: Any) -> audience = get_audience(base_url) # Ensure all scopes end with /.default and strip any trailing slashes before adding suffix - audience_scope = audience.rstrip('/') + '/' + DEFAULT_SCOPE_SUFFIX + audience_scope = audience.rstrip("/") + "/" + DEFAULT_SCOPE_SUFFIX kwargs["credential_scopes"] = [audience_scope] if isinstance(credential, AzureKeyCredential): @@ -888,7 +888,7 @@ def convert_to_sdk(objs): accept_datetime=accept_datetime, tags=tags_filter, cls=convert_to_sdk, - **kwargs + **kwargs, ) @distributed_trace @@ -1091,10 +1091,7 @@ def convert_to_sdk(objs): return [FeatureFlag._from_generated(obj) for obj in objs] return self._impl.get_feature_flag_revisions( # type: ignore[return-value] - name=feature_id_filter, - label=label_filter, - cls=convert_to_sdk, - **impl_kwargs + name=feature_id_filter, label=label_filter, cls=convert_to_sdk, **impl_kwargs ) def update_sync_token(self, token: str) -> None: diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py index 681786c8937c..51005a80882d 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py @@ -1044,7 +1044,7 @@ def build_azure_app_configuration_get_labels_request( # pylint: disable=name-to if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") if resource_type is not None: - _params["resource-type"] = _SERIALIZER.query("resource_type", resource_type, "str") + _params["resourceType"] = _SERIALIZER.query("resource_type", resource_type, "str") # Construct headers if sync_token is not None: @@ -1082,7 +1082,7 @@ def build_azure_app_configuration_check_labels_request( # pylint: disable=name- if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") if resource_type is not None: - _params["resource-type"] = _SERIALIZER.query("resource_type", resource_type, "str") + _params["resourceType"] = _SERIALIZER.query("resource_type", resource_type, "str") # Construct headers if sync_token is not None: diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py index ab4bbb36da39..49276884ed52 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -299,7 +300,7 @@ def _to_generated(self) -> KeyValue: @classmethod def _from_feature_flag(cls, feature_flag: _GeneratedFeatureFlag) -> "FeatureFlagConfigurationSetting": """Create a FeatureFlagConfigurationSetting from a native FeatureFlag object. - + :param feature_flag: The native FeatureFlag object from the Feature Flag endpoint :type feature_flag: ~azure.appconfiguration._generated.models.FeatureFlag :return: A FeatureFlagConfigurationSetting @@ -308,7 +309,7 @@ def _from_feature_flag(cls, feature_flag: _GeneratedFeatureFlag) -> "FeatureFlag filters = None if feature_flag.conditions and feature_flag.conditions.get("client_filters"): filters = feature_flag.conditions["client_filters"] - + return cls( feature_id=feature_flag.name, # type: ignore key=cls._key_prefix + feature_flag.name, @@ -326,14 +327,14 @@ def _from_feature_flag(cls, feature_flag: _GeneratedFeatureFlag) -> "FeatureFlag def _to_feature_flag(self) -> _GeneratedFeatureFlag: """Convert this FeatureFlagConfigurationSetting to a native FeatureFlag object. - + :return: A native FeatureFlag object for the Feature Flag endpoint :rtype: ~azure.appconfiguration._generated.models.FeatureFlag """ conditions = None if self.filters: conditions = {"client_filters": self.filters} - + return _GeneratedFeatureFlag( name=self.feature_id, enabled=self.enabled, @@ -467,20 +468,20 @@ class FeatureFlagFilter: :ivar name: The name of the filter. Required. :vartype name: str :ivar parameters: The parameters used by the filter. - :vartype parameters: dict[str, Any] or None + :vartype parameters: dict[str, str] or None """ name: str """The name of the filter. Required.""" - parameters: Optional[Dict[str, Any]] + parameters: Optional[Dict[str, str]] """The parameters used by the filter.""" - def __init__(self, *, name: str, parameters: Optional[Dict[str, Any]] = None) -> None: + def __init__(self, *, name: str, parameters: Optional[Dict[str, str]] = None) -> None: """ :keyword name: The name of the filter. Required. :paramtype name: str :keyword parameters: The parameters used by the filter. - :paramtype parameters: dict[str, Any] or None + :paramtype parameters: dict[str, str] or None """ self.name = name self.parameters = parameters @@ -538,9 +539,7 @@ def _from_generated(cls, generated: _GeneratedFeatureFlagConditions) -> "Feature def _to_generated(self) -> _GeneratedFeatureFlagConditions: return _GeneratedFeatureFlagConditions( requirement_type=self.requirement_type, - filters=( - [f._to_generated() for f in self.client_filters] if self.client_filters is not None else None - ), + filters=([f._to_generated() for f in self.client_filters] if self.client_filters is not None else None), ) @@ -592,9 +591,7 @@ def __init__( self.status_override = status_override @classmethod - def _from_generated( - cls, generated: _GeneratedFeatureFlagVariantDefinition - ) -> "FeatureFlagVariantDefinition": + def _from_generated(cls, generated: _GeneratedFeatureFlagVariantDefinition) -> "FeatureFlagVariantDefinition": return cls( name=generated.name, value=generated.value, @@ -793,15 +790,9 @@ def _from_generated(cls, generated: _GeneratedFeatureFlagAllocation) -> "Feature if generated.percentile is not None else None ), - user=( - [UserAllocation._from_generated(u) for u in generated.user] - if generated.user is not None - else None - ), + user=([UserAllocation._from_generated(u) for u in generated.user] if generated.user is not None else None), group=( - [GroupAllocation._from_generated(g) for g in generated.group] - if generated.group is not None - else None + [GroupAllocation._from_generated(g) for g in generated.group] if generated.group is not None else None ), seed=generated.seed, ) @@ -810,9 +801,7 @@ def _to_generated(self) -> _GeneratedFeatureFlagAllocation: return _GeneratedFeatureFlagAllocation( default_when_disabled=self.default_when_disabled, default_when_enabled=self.default_when_enabled, - percentile=( - [p._to_generated() for p in self.percentile] if self.percentile is not None else None - ), + percentile=([p._to_generated() for p in self.percentile] if self.percentile is not None else None), user=[u._to_generated() for u in self.user] if self.user is not None else None, group=[g._to_generated() for g in self.group] if self.group is not None else None, seed=self.seed, @@ -855,7 +844,7 @@ def _to_generated(self) -> _GeneratedFeatureFlagTelemetryConfiguration: class FeatureFlag(Model): """A feature flag used with the dedicated feature flag endpoints. - + This model represents a feature flag and is used exclusively with the feature flag-specific API endpoints (set_feature_flag, get_feature_flag, etc). """ @@ -946,7 +935,7 @@ def __init__( @classmethod def _from_generated(cls, generated: _GeneratedFeatureFlag) -> "FeatureFlag": """Create an SDK FeatureFlag from a generated FeatureFlag object. - + :param generated: The generated FeatureFlag object :type generated: ~azure.appconfiguration._generated.models.FeatureFlag :return: An SDK FeatureFlag @@ -984,7 +973,7 @@ def _from_generated(cls, generated: _GeneratedFeatureFlag) -> "FeatureFlag": def _to_generated(self) -> _GeneratedFeatureFlag: """Convert this SDK FeatureFlag to a generated FeatureFlag object. - + :return: A generated FeatureFlag :rtype: ~azure.appconfiguration._generated.models.FeatureFlag """ @@ -994,9 +983,7 @@ def _to_generated(self) -> _GeneratedFeatureFlag: label=self.label, description=self.description, conditions=self.conditions._to_generated() if self.conditions is not None else None, - variants=( - [v._to_generated() for v in self.variants] if self.variants is not None else None - ), + variants=([v._to_generated() for v in self.variants] if self.variants is not None else None), allocation=self.allocation._to_generated() if self.allocation is not None else None, telemetry=self.telemetry._to_generated() if self.telemetry is not None else None, tags=self.tags, diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py index a39e0ed20141..1f89066d7fec 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py @@ -1,4 +1,4 @@ -# pylint: disable=line-too-long,useless-suppression +# pylint: disable=line-too-long,useless-suppression,too-many-lines # ------------------------------------------------------------------------ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for @@ -89,7 +89,7 @@ def __init__(self, base_url: str, credential: AsyncTokenCredential, **kwargs: An audience = get_audience(base_url) # Ensure all scopes end with /.default and strip any trailing slashes before adding suffix - audience_scope = audience.rstrip('/') + '/' + DEFAULT_SCOPE_SUFFIX + audience_scope = audience.rstrip("/") + "/" + DEFAULT_SCOPE_SUFFIX kwargs["credential_scopes"] = [audience_scope] if isinstance(credential, AzureKeyCredential): @@ -891,7 +891,7 @@ def convert_to_sdk(objs): accept_datetime=accept_datetime, tags=tags_filter, cls=convert_to_sdk, - **kwargs + **kwargs, ) @distributed_trace_async @@ -1051,10 +1051,7 @@ def convert_to_sdk(objs): return [FeatureFlag._from_generated(obj) for obj in objs] return self._impl.get_feature_flag_revisions( # type: ignore[return-value] - name=feature_id_filter, - label=label_filter, - cls=convert_to_sdk, - **impl_kwargs + name=feature_id_filter, label=label_filter, cls=convert_to_sdk, **impl_kwargs ) async def update_sync_token(self, token: str) -> None: diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py index e985612b6a5b..cdb6de10a6e4 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py @@ -40,20 +40,20 @@ def test_list_feature_flags(self, appconfiguration_endpoint_string): """Test listing feature flags using the dedicated feature flag endpoint.""" set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") client = self.create_client(appconfiguration_endpoint_string) - + # Create some feature flags feature_flag1 = FeatureFlag(name="feature1", enabled=True) feature_flag2 = FeatureFlag(name="feature2", enabled=False) - + client.set_feature_flag(feature_flag1) client.set_feature_flag(feature_flag2) - + # List all feature flags flags = list(client.list_feature_flags()) assert len(flags) >= 2 assert any(f.name == "feature1" for f in flags) assert any(f.name == "feature2" for f in flags) - + # Clean up client.delete_feature_flag("feature1", label=feature_flag1.label) client.delete_feature_flag("feature2", label=feature_flag2.label) @@ -64,21 +64,21 @@ def test_list_feature_flags_with_name_filter(self, appconfiguration_endpoint_str """Test listing feature flags with name filter.""" set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") client = self.create_client(appconfiguration_endpoint_string) - + # Create feature flags feature_flag1 = FeatureFlag(name="myfeature_alpha", enabled=True) feature_flag2 = FeatureFlag(name="myfeature_beta", enabled=False) feature_flag3 = FeatureFlag(name="otherfeature", enabled=True) - + client.set_feature_flag(feature_flag1) client.set_feature_flag(feature_flag2) client.set_feature_flag(feature_flag3) - + # List with name filter flags = list(client.list_feature_flags(name_filter="myfeature*")) assert len(flags) >= 2 assert all("myfeature" in f.name for f in flags) - + # Clean up client.delete_feature_flag("myfeature_alpha", label=feature_flag1.label) client.delete_feature_flag("myfeature_beta", label=feature_flag2.label) @@ -89,17 +89,17 @@ def test_list_feature_flags_with_name_filter(self, appconfiguration_endpoint_str def test_get_feature_flag(self, appconfiguration_endpoint_string): """Test getting a specific feature flag.""" client = self.create_client(appconfiguration_endpoint_string) - + # Create a feature flag feature_flag = FeatureFlag(name="test_feature_get", enabled=True) created = client.set_feature_flag(feature_flag) - + # Get the feature flag using the new endpoint method retrieved = client.get_feature_flag("test_feature_get", label=created.label) assert retrieved is not None assert retrieved.name == "test_feature_get" assert retrieved.enabled == True - + # Clean up client.delete_feature_flag("test_feature_get", label=created.label) @@ -108,7 +108,7 @@ def test_get_feature_flag(self, appconfiguration_endpoint_string): def test_get_feature_flag_not_found(self, appconfiguration_endpoint_string): """Test getting a non-existent feature flag.""" client = self.create_client(appconfiguration_endpoint_string) - + # Try to get a non-existent feature flag result = client.get_feature_flag("nonexistent_feature", label=None) assert result is None @@ -118,15 +118,15 @@ def test_get_feature_flag_not_found(self, appconfiguration_endpoint_string): def test_set_feature_flag(self, appconfiguration_endpoint_string): """Test setting a feature flag via the dedicated endpoint.""" client = self.create_client(appconfiguration_endpoint_string) - + # Create a feature flag feature_flag = FeatureFlag(name="test_feature_set", enabled=True) set_flag = client.set_feature_flag(feature_flag) - + assert set_flag is not None assert set_flag.enabled == True assert set_flag.name == "test_feature_set" - + # Clean up client.delete_feature_flag("test_feature_set", label=set_flag.label) @@ -135,18 +135,18 @@ def test_set_feature_flag(self, appconfiguration_endpoint_string): def test_set_feature_flag_update(self, appconfiguration_endpoint_string): """Test updating an existing feature flag.""" client = self.create_client(appconfiguration_endpoint_string) - + # Create and then update a feature flag feature_flag = FeatureFlag(name="test_feature_update", enabled=True) created = client.set_feature_flag(feature_flag) - + # Update it created.enabled = False updated = client.set_feature_flag(created) - + assert updated.enabled == False assert updated.etag != created.etag - + # Clean up client.delete_feature_flag("test_feature_update", label=updated.label) @@ -155,14 +155,14 @@ def test_set_feature_flag_update(self, appconfiguration_endpoint_string): def test_delete_feature_flag(self, appconfiguration_endpoint_string): """Test deleting a feature flag.""" client = self.create_client(appconfiguration_endpoint_string) - + # Create a feature flag feature_flag = FeatureFlag(name="test_feature_delete", enabled=True) created = client.set_feature_flag(feature_flag) - + # Delete it using the endpoint method client.delete_feature_flag("test_feature_delete", label=created.label) - + # Verify it's deleted result = client.get_feature_flag("test_feature_delete", label=created.label) assert result is None @@ -173,20 +173,20 @@ def test_list_feature_flag_revisions(self, appconfiguration_endpoint_string): """Test listing feature flag revisions.""" set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") client = self.create_client(appconfiguration_endpoint_string) - + # Create and update a feature flag to create revisions feature_flag = FeatureFlag(name="test_feature_revisions", enabled=True) created = client.set_feature_flag(feature_flag) - + # Update to create another revision created.enabled = False updated = client.set_feature_flag(created) - + # List revisions revisions = list(client.list_feature_flag_revisions(feature_id_filter="test_feature_revisions")) assert len(revisions) >= 1 # At least one revision assert all("test_feature_revisions" in r.name for r in revisions) - + # Clean up client.delete_feature_flag("test_feature_revisions", label=updated.label) @@ -195,31 +195,23 @@ def test_list_feature_flag_revisions(self, appconfiguration_endpoint_string): def test_feature_flag_with_label(self, appconfiguration_endpoint_string): """Test feature flag operations with labels.""" client = self.create_client(appconfiguration_endpoint_string) - + # Create feature flags with labels - feature_flag_prod = FeatureFlag( - name="feature_with_label", - enabled=True, - label="prod" - ) - feature_flag_staging = FeatureFlag( - name="feature_with_label", - enabled=False, - label="staging" - ) - + feature_flag_prod = FeatureFlag(name="feature_with_label", enabled=True, label="prod") + feature_flag_staging = FeatureFlag(name="feature_with_label", enabled=False, label="staging") + client.set_feature_flag(feature_flag_prod) client.set_feature_flag(feature_flag_staging) - + # Get specific labeled version prod_flag = client.get_feature_flag("feature_with_label", label="prod") assert prod_flag is not None assert prod_flag.enabled == True - + staging_flag = client.get_feature_flag("feature_with_label", label="staging") assert staging_flag is not None assert staging_flag.enabled == False - + # Clean up client.delete_feature_flag("feature_with_label", label="prod") client.delete_feature_flag("feature_with_label", label="staging") @@ -237,7 +229,9 @@ def test_feature_flag_with_conditions(self, appconfiguration_endpoint_string): conditions=FeatureFlagConditions( requirement_type="All", client_filters=[ - FeatureFlagFilter(name="Microsoft.TimeWindow", parameters={"Start": "Mon, 01 Jan 2024 00:00:00 GMT"}), + FeatureFlagFilter( + name="Microsoft.TimeWindow", parameters={"Start": "Mon, 01 Jan 2024 00:00:00 GMT"} + ), FeatureFlagFilter(name="Microsoft.Percentage", parameters={"Value": 50}), ], ), @@ -346,7 +340,8 @@ def test_feature_flag_full_model_roundtrip(self, appconfiguration_endpoint_strin conditions=FeatureFlagConditions( requirement_type="Any", client_filters=[ - FeatureFlagFilter(name="Microsoft.Targeting", parameters={"Audience": {"DefaultRolloutPercentage": 25}}), + # Filter parameters are string-valued per the API spec (Record). + FeatureFlagFilter(name="Microsoft.Targeting", parameters={"Audience": "all-users"}), ], ), variants=[ diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py index 6c444e1d3e57..ce9323e5d4e8 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py @@ -41,20 +41,20 @@ async def test_list_feature_flags(self, appconfiguration_endpoint_string): """Test listing feature flags using the dedicated feature flag endpoint.""" set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") client = self.create_client(appconfiguration_endpoint_string) - + # Create some feature flags feature_flag1 = FeatureFlag(name="feature1", enabled=True) feature_flag2 = FeatureFlag(name="feature2", enabled=False) - + await client.set_feature_flag(feature_flag1) await client.set_feature_flag(feature_flag2) - + # List all feature flags flags = [f async for f in client.list_feature_flags()] assert len(flags) >= 2 assert any(f.name == "feature1" for f in flags) assert any(f.name == "feature2" for f in flags) - + # Clean up await client.delete_feature_flag("feature1", label=feature_flag1.label) await client.delete_feature_flag("feature2", label=feature_flag2.label) @@ -66,21 +66,21 @@ async def test_list_feature_flags_with_name_filter(self, appconfiguration_endpoi """Test listing feature flags with name filter.""" set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") client = self.create_client(appconfiguration_endpoint_string) - + # Create feature flags feature_flag1 = FeatureFlag(name="myfeature_alpha", enabled=True) feature_flag2 = FeatureFlag(name="myfeature_beta", enabled=False) feature_flag3 = FeatureFlag(name="otherfeature", enabled=True) - + await client.set_feature_flag(feature_flag1) await client.set_feature_flag(feature_flag2) await client.set_feature_flag(feature_flag3) - + # List with name filter flags = [f async for f in client.list_feature_flags(name_filter="myfeature*")] assert len(flags) >= 2 assert all("myfeature" in f.name for f in flags) - + # Clean up await client.delete_feature_flag("myfeature_alpha", label=feature_flag1.label) await client.delete_feature_flag("myfeature_beta", label=feature_flag2.label) @@ -92,17 +92,17 @@ async def test_list_feature_flags_with_name_filter(self, appconfiguration_endpoi async def test_get_feature_flag(self, appconfiguration_endpoint_string): """Test getting a specific feature flag.""" client = self.create_client(appconfiguration_endpoint_string) - + # Create a feature flag feature_flag = FeatureFlag(name="test_feature_get", enabled=True) created = await client.set_feature_flag(feature_flag) - + # Get the feature flag using the new endpoint method retrieved = await client.get_feature_flag("test_feature_get", label=created.label) assert retrieved is not None assert retrieved.name == "test_feature_get" assert retrieved.enabled == True - + # Clean up await client.delete_feature_flag("test_feature_get", label=created.label) await client.close() @@ -112,7 +112,7 @@ async def test_get_feature_flag(self, appconfiguration_endpoint_string): async def test_get_feature_flag_not_found(self, appconfiguration_endpoint_string): """Test getting a non-existent feature flag.""" client = self.create_client(appconfiguration_endpoint_string) - + # Try to get a non-existent feature flag result = await client.get_feature_flag("nonexistent_feature", label=None) assert result is None @@ -123,15 +123,15 @@ async def test_get_feature_flag_not_found(self, appconfiguration_endpoint_string async def test_set_feature_flag(self, appconfiguration_endpoint_string): """Test setting a feature flag via the dedicated endpoint.""" client = self.create_client(appconfiguration_endpoint_string) - + # Create a feature flag feature_flag = FeatureFlag(name="test_feature_set", enabled=True) set_flag = await client.set_feature_flag(feature_flag) - + assert set_flag is not None assert set_flag.enabled == True assert set_flag.name == "test_feature_set" - + # Clean up await client.delete_feature_flag("test_feature_set", label=set_flag.label) await client.close() @@ -141,18 +141,18 @@ async def test_set_feature_flag(self, appconfiguration_endpoint_string): async def test_set_feature_flag_update(self, appconfiguration_endpoint_string): """Test updating an existing feature flag.""" client = self.create_client(appconfiguration_endpoint_string) - + # Create and then update a feature flag feature_flag = FeatureFlag(name="test_feature_update", enabled=True) created = await client.set_feature_flag(feature_flag) - + # Update it created.enabled = False updated = await client.set_feature_flag(created) - + assert updated.enabled == False assert updated.etag != created.etag - + # Clean up await client.delete_feature_flag("test_feature_update", label=updated.label) await client.close() @@ -162,14 +162,14 @@ async def test_set_feature_flag_update(self, appconfiguration_endpoint_string): async def test_delete_feature_flag(self, appconfiguration_endpoint_string): """Test deleting a feature flag.""" client = self.create_client(appconfiguration_endpoint_string) - + # Create a feature flag feature_flag = FeatureFlag(name="test_feature_delete", enabled=True) created = await client.set_feature_flag(feature_flag) - + # Delete it using the endpoint method await client.delete_feature_flag("test_feature_delete", label=created.label) - + # Verify it's deleted result = await client.get_feature_flag("test_feature_delete", label=created.label) assert result is None @@ -181,20 +181,20 @@ async def test_list_feature_flag_revisions(self, appconfiguration_endpoint_strin """Test listing feature flag revisions.""" set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") client = self.create_client(appconfiguration_endpoint_string) - + # Create and update a feature flag to create revisions feature_flag = FeatureFlag(name="test_feature_revisions", enabled=True) created = await client.set_feature_flag(feature_flag) - + # Update to create another revision created.enabled = False updated = await client.set_feature_flag(created) - + # List revisions revisions = [r async for r in client.list_feature_flag_revisions(feature_id_filter="test_feature_revisions")] assert len(revisions) >= 1 # At least one revision assert all("test_feature_revisions" in r.name for r in revisions) - + # Clean up await client.delete_feature_flag("test_feature_revisions", label=updated.label) await client.close() @@ -204,31 +204,23 @@ async def test_list_feature_flag_revisions(self, appconfiguration_endpoint_strin async def test_feature_flag_with_label(self, appconfiguration_endpoint_string): """Test feature flag operations with labels.""" client = self.create_client(appconfiguration_endpoint_string) - + # Create feature flags with labels - feature_flag_prod = FeatureFlag( - name="feature_with_label", - enabled=True, - label="prod" - ) - feature_flag_staging = FeatureFlag( - name="feature_with_label", - enabled=False, - label="staging" - ) - + feature_flag_prod = FeatureFlag(name="feature_with_label", enabled=True, label="prod") + feature_flag_staging = FeatureFlag(name="feature_with_label", enabled=False, label="staging") + await client.set_feature_flag(feature_flag_prod) await client.set_feature_flag(feature_flag_staging) - + # Get specific labeled version prod_flag = await client.get_feature_flag("feature_with_label", label="prod") assert prod_flag is not None assert prod_flag.enabled == True - + staging_flag = await client.get_feature_flag("feature_with_label", label="staging") assert staging_flag is not None assert staging_flag.enabled == False - + # Clean up await client.delete_feature_flag("feature_with_label", label="prod") await client.delete_feature_flag("feature_with_label", label="staging") @@ -247,7 +239,9 @@ async def test_feature_flag_with_conditions(self, appconfiguration_endpoint_stri conditions=FeatureFlagConditions( requirement_type="All", client_filters=[ - FeatureFlagFilter(name="Microsoft.TimeWindow", parameters={"Start": "Mon, 01 Jan 2024 00:00:00 GMT"}), + FeatureFlagFilter( + name="Microsoft.TimeWindow", parameters={"Start": "Mon, 01 Jan 2024 00:00:00 GMT"} + ), FeatureFlagFilter(name="Microsoft.Percentage", parameters={"Value": 50}), ], ), @@ -359,7 +353,8 @@ async def test_feature_flag_full_model_roundtrip(self, appconfiguration_endpoint conditions=FeatureFlagConditions( requirement_type="Any", client_filters=[ - FeatureFlagFilter(name="Microsoft.Targeting", parameters={"Audience": {"DefaultRolloutPercentage": 25}}), + # Filter parameters are string-valued per the API spec (Record). + FeatureFlagFilter(name="Microsoft.Targeting", parameters={"Audience": "all-users"}), ], ), variants=[ diff --git a/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml b/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml index d90f05e6b8e3..955185feced9 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml +++ b/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml @@ -1,4 +1,4 @@ -commit: ee5f3d39891991feaae672a9ae28b984627d9d16 repo: Azure/azure-rest-api-specs +commit: 6e7e0ec0847f966d243c5a7dd25b706018744d4a directory: specification/appconfiguration/data-plane/AppConfiguration additionalDirectories: \ No newline at end of file From bff7b4c0017aa1a275c1ff091e13bd3199fed01b Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 24 Jun 2026 11:44:46 -0700 Subject: [PATCH 07/25] pylint fixes --- .../_azure_appconfiguration_client.py | 5 +- .../azure/appconfiguration/_models.py | 236 +----------------- .../_azure_appconfiguration_client_async.py | 37 ++- 3 files changed, 35 insertions(+), 243 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py index 0b600e9b59cd..c1075ceab3dd 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py @@ -34,7 +34,6 @@ ConfigurationSettingsFilter, ConfigurationSnapshot, ConfigurationSettingLabel, - FeatureFlagConfigurationSetting, FeatureFlag, ) from ._audience import get_audience, DEFAULT_SCOPE_SUFFIX @@ -48,7 +47,7 @@ from ._audience_error_handling_policy import AudienceErrorHandlingPolicy -class AzureAppConfigurationClient: +class AzureAppConfigurationClient: # pylint: disable=too-many-public-methods """Represents a client that calls restful API of Azure App Configuration service. :param str base_url: Base url of the service. @@ -998,7 +997,7 @@ def set_feature_flag( return FeatureFlag._from_generated(generated_result) @distributed_trace - def delete_feature_flag( + def delete_feature_flag( # pylint:disable=delete-operation-wrong-return-type self, feature_id: str, label: Optional[str] = None, diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py index 49276884ed52..09f0738a8bfb 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py @@ -527,6 +527,7 @@ def __init__( @classmethod def _from_generated(cls, generated: _GeneratedFeatureFlagConditions) -> "FeatureFlagConditions": + # pylint:disable=protected-access return cls( requirement_type=generated.requirement_type, client_filters=( @@ -537,6 +538,7 @@ def _from_generated(cls, generated: _GeneratedFeatureFlagConditions) -> "Feature ) def _to_generated(self) -> _GeneratedFeatureFlagConditions: + # pylint:disable=protected-access return _GeneratedFeatureFlagConditions( requirement_type=self.requirement_type, filters=([f._to_generated() for f in self.client_filters] if self.client_filters is not None else None), @@ -782,6 +784,7 @@ def __init__( @classmethod def _from_generated(cls, generated: _GeneratedFeatureFlagAllocation) -> "FeatureFlagAllocation": + # pylint:disable=protected-access return cls( default_when_disabled=generated.default_when_disabled, default_when_enabled=generated.default_when_enabled, @@ -798,6 +801,7 @@ def _from_generated(cls, generated: _GeneratedFeatureFlagAllocation) -> "Feature ) def _to_generated(self) -> _GeneratedFeatureFlagAllocation: + # pylint:disable=protected-access return _GeneratedFeatureFlagAllocation( default_when_disabled=self.default_when_disabled, default_when_enabled=self.default_when_enabled, @@ -842,7 +846,7 @@ def _to_generated(self) -> _GeneratedFeatureFlagTelemetryConfiguration: return _GeneratedFeatureFlagTelemetryConfiguration(enabled=self.enabled, metadata=self.metadata) -class FeatureFlag(Model): +class FeatureFlag(Model): # pylint: disable=too-many-instance-attributes """A feature flag used with the dedicated feature flag endpoints. This model represents a feature flag and is used exclusively with the @@ -920,6 +924,7 @@ def __init__( :keyword tags: The tags of the feature flag. :paramtype tags: dict[str, str] or None """ + super().__init__(**kwargs) self.name = name self.enabled = enabled if enabled is not None else False self.label = label @@ -941,6 +946,7 @@ def _from_generated(cls, generated: _GeneratedFeatureFlag) -> "FeatureFlag": :return: An SDK FeatureFlag :rtype: ~azure.appconfiguration.FeatureFlag """ + # pylint:disable=protected-access return cls( name=generated.name, enabled=generated.enabled, @@ -977,6 +983,7 @@ def _to_generated(self) -> _GeneratedFeatureFlag: :return: A generated FeatureFlag :rtype: ~azure.appconfiguration._generated.models.FeatureFlag """ + # pylint:disable=protected-access return _GeneratedFeatureFlag( name=self.name, enabled=self.enabled, @@ -1187,178 +1194,6 @@ def _return_deserialized_and_headers(_, deserialized, response_headers): return deserialized, response_headers -class FeatureFlagConfigurationSettingPropertiesPagedBase: # pylint:disable=too-many-instance-attributes - """Base class for iterable of FeatureFlagConfigurationSetting properties.""" - - etag: str - """The current etag""" - _etags: List[str] - """The etag expected for the pages.""" - _command: Callable - """The command to get the next page""" - _key: Optional[str] - """The key filter for the items""" - _label: Optional[str] - """The label filter for the items""" - _accept_datetime: Optional[str] - """The accept datetime for the items""" - _select: Optional[List[Union[str, Any]]] - """The select fields for the items""" - _tags: Optional[List[str]] - """The tags filter for the items""" - _snapshot: Optional[str] - """The snapshot name for the items""" - _match_condition: Optional[MatchConditions] - """The match condition""" - _kwargs: Dict[str, Any] - """The keyword arguments""" - - def __init__(self, command: Callable, **kwargs: Any): - self._command = command - self._key = kwargs.pop("key", None) - self._label = kwargs.pop("label", None) - self._accept_datetime = kwargs.pop("accept_datetime", None) - self._select = kwargs.pop("select", None) - self._tags = kwargs.pop("tags", None) - self._snapshot = kwargs.pop("snapshot", None) - self._match_condition = kwargs.pop("match_condition", None) - self._kwargs = kwargs - self._etags: List[str] = [] - - def _next_etag(self) -> Optional[str]: - if self._etags: - etag = self._etags.pop(0) - return etag - return None - - def _extract_data_cb_base(self, get_next_return): - etags, data = get_next_return - self.etag = etags - return data - - def _page_iterator_class(self, **kwargs: Any) -> PageIterator: - raise NotImplementedError() - - -class FeatureFlagConfigurationSettingPropertiesPaged( - FeatureFlagConfigurationSettingPropertiesPagedBase, PageIterator -): # pylint:disable=too-many-instance-attributes - """An iterable of FeatureFlagConfigurationSetting properties.""" - - def __init__(self, command: Callable, **kwargs: Any): - super().__init__(command, **kwargs) - PageIterator.__init__( - self, - self._get_next_cb, - self._extract_data_cb, - continuation_token=kwargs.get("continuation_token"), - ) - - def _get_next_cb(self, continuation_token, **kwargs): - etag = self._next_etag() - return self._command( - name=self._key, - label=self._label, - accept_datetime=self._accept_datetime, - select=self._select, - tags=self._tags, - etag=etag, - match_condition=self._match_condition, - continuation_token=continuation_token, - cls=kwargs.pop("cls", None) or _return_deserialized_and_headers, - ) - - def _extract_data_cb(self, get_next_return): - return self._extract_data_cb_base(get_next_return) - - def __next__(self) -> Iterator[ReturnType]: - """Get the next page in the iterator. - - :returns: An iterator of objects in the next page. - :rtype: iterator[ReturnType] - :raises StopIteration: If there are no more pages to return. - :raises AzureError: If the request fails. - """ - # Is the exact same method as `PageIterator`, excluding the if statement before the return. - if self.continuation_token is None and self._did_a_call_already: - raise StopIteration("End of paging") - try: - self._response = self._get_next(self.continuation_token) - except AzureError as error: - if not error.continuation_token: - error.continuation_token = self.continuation_token - raise - - self._did_a_call_already = True - - self.continuation_token, self._current_page = self._extract_data(self._response) - - # App Config's addition to skip empty pages - if self._current_page is None: - # We skip over pages that are empty, change from mach conditions - return self.__next__() - return iter(self._current_page) - - -class FeatureFlagConfigurationSettingPropertiesPagedAsync( - FeatureFlagConfigurationSettingPropertiesPagedBase, AsyncPageIterator -): # pylint:disable=too-many-instance-attributes - """An async iterable of FeatureFlagConfigurationSetting properties.""" - - def __init__(self, command: Callable, **kwargs: Any): - super().__init__(command, **kwargs) - AsyncPageIterator.__init__( - self, - self._get_next_cb, - self._extract_data_cb, - continuation_token=kwargs.get("continuation_token"), - ) - - def _get_next_cb(self, continuation_token, **kwargs): - etag = self._next_etag() - return self._command( - name=self._key, - label=self._label, - accept_datetime=self._accept_datetime, - select=self._select, - tags=self._tags, - etag=etag, - match_condition=self._match_condition, - continuation_token=continuation_token, - cls=kwargs.pop("cls", None) or _return_deserialized_and_headers, - ) - - def _extract_data_cb(self, get_next_return): - return self._extract_data_cb_base(get_next_return) - - async def __anext__(self): - """Get the next page in the async iterator. - - :returns: An async iterator of objects in the next page. - :rtype: async_iterator[ReturnType] - :raises StopAsyncIteration: If there are no more pages to return. - :raises AzureError: If the request fails. - """ - if self.continuation_token is None and self._did_a_call_already: - raise StopAsyncIteration("End of paging") # pylint: disable=raise-missing-from - try: - self._response = await self._get_next(self.continuation_token) - except AzureError as error: - if not error.continuation_token: - error.continuation_token = self.continuation_token - raise - - self._did_a_call_already = True - - self.continuation_token, self._current_page = self._extract_data(self._response) - - # App Config's addition to skip empty pages - if self._current_page is None: - # We skip over pages that are empty, change from mach conditions - return await self.__anext__() - return self._current_page - - class ConfigurationSettingPropertiesPagedBase: # pylint:disable=too-many-instance-attributes """Base class for iterable of ConfigurationSetting properties.""" @@ -1642,58 +1477,3 @@ def by_page(self, continuation_token: Optional[str] = None, *, match_conditions: self._kwargs["etags"] = match_conditions self._kwargs["match_condition"] = MatchConditions.IfModified return self._page_iterator_class(continuation_token=continuation_token, *self._args, **self._kwargs) - - -class FeatureFlagConfigurationSettingPaged(ItemPaged["FeatureFlagConfigurationSetting"]): - """ - An iterable of FeatureFlagConfigurationSettings that supports etag-based change detection. - - This class extends ItemPaged to provide efficient monitoring of feature flag configuration changes - by using ETags. When used with the `match_conditions` parameter in `by_page()`, - it only returns pages that have changed since the provided ETags were collected. - """ - - def by_page(self, continuation_token: Optional[str] = None, *, match_conditions: Optional[List[str]] = None) -> Any: - """Get an iterator of pages of objects, instead of an iterator of objects. - - :param str continuation_token: - An opaque continuation token. This value can be retrieved from the - continuation_token field of a previous generator object. If specified, - this generator will begin returning results from this point. - :keyword match_conditions: A list of etags to check for changes. If provided, the iterator will - check each page against the corresponding etag and only return pages that have changed. - :paramtype match_conditions: list[str] or None - :returns: An iterator of pages (themselves iterator of objects) - :rtype: iterator[iterator[ReturnType]] - """ - if "match_conditions" not in self._kwargs and match_conditions: - self._kwargs["etags"] = match_conditions - self._kwargs["match_condition"] = MatchConditions.IfModified - return self._page_iterator_class(continuation_token=continuation_token, *self._args, **self._kwargs) - - -class AsyncFeatureFlagConfigurationSettingPaged(AsyncItemPaged["FeatureFlagConfigurationSetting"]): - """ - An async iterable of FeatureFlagConfigurationSettings that supports etag-based change detection. - - This class provides asynchronous iteration over feature flag configuration settings, with optional support for - etag-based change detection. - """ - - def by_page(self, continuation_token: Optional[str] = None, *, match_conditions: Optional[List[str]] = None) -> Any: - """Get an async iterator of pages of objects, instead of an iterator of objects. - - :param str continuation_token: - An opaque continuation token. This value can be retrieved from the - continuation_token field of a previous generator object. If specified, - this generator will begin returning results from this point. - :keyword match_conditions: A list of etags to check for changes. If provided, the iterator will - check each page against the corresponding etag and only return pages that have changed. - :paramtype match_conditions: list[str] or None - :returns: An async iterator of pages (themselves iterator of objects) - :rtype: AsyncIterator[AsyncIterator[ReturnType]] - """ - if "match_conditions" not in self._kwargs and match_conditions: - self._kwargs["etags"] = match_conditions - self._kwargs["match_condition"] = MatchConditions.IfModified - return self._page_iterator_class(continuation_token=continuation_token, *self._args, **self._kwargs) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py index 1f89066d7fec..0c55900b31fa 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py @@ -48,7 +48,7 @@ from .._audience_error_handling_policy import AudienceErrorHandlingPolicy -class AzureAppConfigurationClient: +class AzureAppConfigurationClient: # pylint: disable=too-many-public-methods """Represents a client that calls restful API of Azure App Configuration service. :param str base_url: Base url of the service. @@ -943,6 +943,7 @@ async def set_feature_flag( self, feature_flag: "FeatureFlag", match_condition: MatchConditions = MatchConditions.Unconditionally, + *, etag: Optional[str] = None, **kwargs: Any, ) -> "FeatureFlag": @@ -951,8 +952,8 @@ async def set_feature_flag( :param feature_flag: A FeatureFlag object. :type feature_flag: ~azure.appconfiguration.FeatureFlag - :keyword match_condition: The match condition to use upon the etag. - :paramtype match_condition: ~azure.core.MatchConditions + :param match_condition: The match condition to use upon the etag. + :type match_condition: ~azure.core.MatchConditions :keyword etag: The etag of the feature flag. If provided, the feature flag will be updated only if the etag matches. :paramtype etag: str or None :return: The updated FeatureFlag. @@ -986,21 +987,33 @@ async def delete_feature_flag( etag: Optional[str] = None, match_condition: MatchConditions = MatchConditions.Unconditionally, **kwargs: Any, - ) -> Optional["FeatureFlag"]: - """ - Delete a feature flag from the service. + ) -> Union[None, "FeatureFlag"]: + """Delete a FeatureFlag if it exists - :param feature_id: The feature flag identifier. + :param feature_id: The feature flag name/id to delete :type feature_id: str - :param label: The label of the feature flag. Defaults to None. + :param label: Label used to identify the FeatureFlag. Default is `None`. :type label: str or None - :keyword etag: Check if the feature flag is changed. Set None to skip checking etag. + :keyword etag: Check if the FeatureFlag is changed. Set None to skip checking etag :paramtype etag: str or None - :keyword match_condition: The match condition to use upon the etag. + :keyword match_condition: The match condition to use upon the etag :paramtype match_condition: ~azure.core.MatchConditions - :return: The deleted FeatureFlag, or None if not found. + :return: The deleted FeatureFlag returned from the service, or None if it doesn't exist. :rtype: ~azure.appconfiguration.FeatureFlag or None - :raises: :class:`~azure.core.exceptions.HttpResponseError` + :raises: :class:`~azure.appconfiguration.ResourceReadOnlyError`, \ + :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError`, \ + :class:`~azure.core.exceptions.ResourceModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotFoundError`, \ + :class:`~azure.core.exceptions.ResourceExistsError` + + Example + + .. code-block:: python + + # in async function + deleted_feature_flag = await async_client.delete_feature_flag(feature_id="MyFeatureFlag") """ error_map: Dict[int, Any] = {409: ResourceReadOnlyError} generated_feature_flag = await self._impl.delete_feature_flag( From 34a120a588fb9c4d8c0f052e80ea619201d08388 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 24 Jun 2026 12:56:10 -0700 Subject: [PATCH 08/25] mypy fixes --- .../azure/appconfiguration/_models.py | 50 ------------------- .../_azure_appconfiguration_client_async.py | 4 +- .../samples/feature_flag_sample.py | 14 ++++-- .../samples/feature_flag_sample_async.py | 14 ++++-- 4 files changed, 20 insertions(+), 62 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py index 09f0738a8bfb..0a2c8584e841 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py @@ -297,54 +297,6 @@ def _to_generated(self) -> KeyValue: etag=self.etag, ) - @classmethod - def _from_feature_flag(cls, feature_flag: _GeneratedFeatureFlag) -> "FeatureFlagConfigurationSetting": - """Create a FeatureFlagConfigurationSetting from a native FeatureFlag object. - - :param feature_flag: The native FeatureFlag object from the Feature Flag endpoint - :type feature_flag: ~azure.appconfiguration._generated.models.FeatureFlag - :return: A FeatureFlagConfigurationSetting - :rtype: ~azure.appconfiguration.FeatureFlagConfigurationSetting - """ - filters = None - if feature_flag.conditions and feature_flag.conditions.get("client_filters"): - filters = feature_flag.conditions["client_filters"] - - return cls( - feature_id=feature_flag.name, # type: ignore - key=cls._key_prefix + feature_flag.name, - label=feature_flag.label, - content_type=cls._feature_flag_content_type, - last_modified=feature_flag.last_modified, - tags=feature_flag.tags or {}, - read_only=False, # FeatureFlag doesn't have a read_only field - etag=feature_flag.etag, - enabled=feature_flag.enabled or False, - filters=filters, - display_name=None, - description=feature_flag.description, - ) - - def _to_feature_flag(self) -> _GeneratedFeatureFlag: - """Convert this FeatureFlagConfigurationSetting to a native FeatureFlag object. - - :return: A native FeatureFlag object for the Feature Flag endpoint - :rtype: ~azure.appconfiguration._generated.models.FeatureFlag - """ - conditions = None - if self.filters: - conditions = {"client_filters": self.filters} - - return _GeneratedFeatureFlag( - name=self.feature_id, - enabled=self.enabled, - label=self.label, - description=self.description, - conditions=conditions, - tags=self.tags, - etag=self.etag, - ) - class SecretReferenceConfigurationSetting(ConfigurationSetting): """A configuration value that references a configuration setting secret.""" @@ -985,9 +937,7 @@ def _to_generated(self) -> _GeneratedFeatureFlag: """ # pylint:disable=protected-access return _GeneratedFeatureFlag( - name=self.name, enabled=self.enabled, - label=self.label, description=self.description, conditions=self.conditions._to_generated() if self.conditions is not None else None, variants=([v._to_generated() for v in self.variants] if self.variants is not None else None), diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py index 0c55900b31fa..55b4d615b7d6 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py @@ -861,7 +861,7 @@ def list_feature_flags( tags_filter: Optional[List[str]] = None, accept_datetime: Optional[Union[datetime, str]] = None, **kwargs: Any, - ) -> AsyncConfigurationSettingPaged: + ) -> AsyncItemPaged["FeatureFlag"]: """ Find the FeatureFlag objects, optionally filtered by name, label, tags and accept_datetime. @@ -1036,7 +1036,7 @@ def list_feature_flag_revisions( *, accept_datetime: Optional[Union[datetime, str]] = None, **kwargs: Any, - ) -> AsyncConfigurationSettingPaged: + ) -> AsyncItemPaged["FeatureFlag"]: """ Find the FeatureFlag revision history, optionally filtered by feature_id and label. diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py index 0cf0c72ca340..5967db91a58e 100644 --- a/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py @@ -67,7 +67,7 @@ def main(): requirement_type="All", client_filters=[ FeatureFlagFilter(name="Microsoft.TimeWindow", parameters={"Start": "Mon, 01 Jan 2024 00:00:00 GMT"}), - FeatureFlagFilter(name="Microsoft.Percentage", parameters={"Value": 50}), + FeatureFlagFilter(name="Microsoft.Percentage", parameters={"Value": "50"}), ], ), # Variants describe the possible values the feature can resolve to. @@ -95,10 +95,14 @@ def main(): fetched = client.get_feature_flag("RichFeature") if fetched is not None: print(f" name={fetched.name}") - print(f" requirement_type={fetched.conditions.requirement_type}") - print(f" variants={[v.name for v in fetched.variants]}") - print(f" default_when_enabled={fetched.allocation.default_when_enabled}") - print(f" telemetry_enabled={fetched.telemetry.enabled}") + if fetched.conditions is not None: + print(f" requirement_type={fetched.conditions.requirement_type}") + if fetched.variants is not None: + print(f" variants={[v.name for v in fetched.variants]}") + if fetched.allocation is not None: + print(f" default_when_enabled={fetched.allocation.default_when_enabled}") + if fetched.telemetry is not None: + print(f" telemetry_enabled={fetched.telemetry.enabled}") print(f" tags={fetched.tags}") print("") diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py index 494122378322..528db08e3b95 100644 --- a/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py @@ -68,7 +68,7 @@ async def main(): requirement_type="All", client_filters=[ FeatureFlagFilter(name="Microsoft.TimeWindow", parameters={"Start": "Mon, 01 Jan 2024 00:00:00 GMT"}), - FeatureFlagFilter(name="Microsoft.Percentage", parameters={"Value": 50}), + FeatureFlagFilter(name="Microsoft.Percentage", parameters={"Value": "50"}), ], ), # Variants describe the possible values the feature can resolve to. @@ -96,10 +96,14 @@ async def main(): fetched = await client.get_feature_flag("RichFeature") if fetched is not None: print(f" name={fetched.name}") - print(f" requirement_type={fetched.conditions.requirement_type}") - print(f" variants={[v.name for v in fetched.variants]}") - print(f" default_when_enabled={fetched.allocation.default_when_enabled}") - print(f" telemetry_enabled={fetched.telemetry.enabled}") + if fetched.conditions is not None: + print(f" requirement_type={fetched.conditions.requirement_type}") + if fetched.variants is not None: + print(f" variants={[v.name for v in fetched.variants]}") + if fetched.allocation is not None: + print(f" default_when_enabled={fetched.allocation.default_when_enabled}") + if fetched.telemetry is not None: + print(f" telemetry_enabled={fetched.telemetry.enabled}") print(f" tags={fetched.tags}") print("") From fc722eaf890da01de2c3c9fed818a9bd998f94cb Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 2 Jul 2026 12:58:15 -0700 Subject: [PATCH 09/25] Split Clients --- .../azure-appconfiguration/CHANGELOG.md | 7 +- .../apiview-properties.json | 36 +- .../azure-appconfiguration/assets.json | 2 +- .../azure/appconfiguration/__init__.py | 2 + .../_azure_appconfiguration_client.py | 257 +- .../appconfiguration/_feature_flag_client.py | 489 ++++ .../appconfiguration/_generated/_client.py | 7 +- .../_generated/_utils/model_base.py | 35 +- .../_generated/_utils/serialization.py | 109 + .../appconfiguration/_generated/_version.py | 2 +- .../_generated/aio/_client.py | 7 +- .../operations}/__init__.py | 5 +- .../_operations.py | 1791 ++++++------- .../_generated/aio/operations/_patch.py | 360 +++ .../_generated/models/_models.py | 12 +- .../_operations => operations}/__init__.py | 5 +- .../_operations.py | 2254 +++++++++-------- .../_generated/operations/_patch.py | 367 +++ .../azure/appconfiguration/_models.py | 14 +- .../azure/appconfiguration/_version.py | 2 +- .../azure/appconfiguration/aio/__init__.py | 3 +- .../_azure_appconfiguration_client_async.py | 218 +- .../aio/_feature_flag_client_async.py | 453 ++++ .../samples/feature_flag_sample.py | 4 +- .../samples/feature_flag_sample_async.py | 4 +- .../tests/asynctestcase.py | 8 +- .../azure-appconfiguration/tests/conftest.py | 4 +- .../test_azure_appconfiguration_client.py | 5 +- ...est_azure_appconfiguration_client_async.py | 6 +- .../tests/test_feature_flags.py | 25 + .../tests/test_feature_flags_async.py | 27 + .../azure-appconfiguration/tests/testcase.py | 7 + .../azure-appconfiguration/tsp-location.yaml | 4 +- 33 files changed, 4003 insertions(+), 2528 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py rename sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/{_operations => aio/operations}/__init__.py (88%) rename sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/{_operations => operations}/_operations.py (98%) create mode 100644 sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_patch.py rename sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/{aio/_operations => operations}/__init__.py (88%) rename sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/{_operations => operations}/_operations.py (98%) create mode 100644 sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_patch.py create mode 100644 sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py diff --git a/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md index 5fdd9bb73454..58bf0004f581 100644 --- a/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md @@ -1,9 +1,14 @@ # Release History -## 1.9.1 (Unreleased) +## 1.10.0b1 (Unreleased) ### Features Added +- Added `FeatureFlagClient` (sync and async) for managing feature flags through a dedicated endpoint (requires API version `2026-05-01-preview` or later): + - Exposes `set_feature_flag()`, `get_feature_flag()`, `list_feature_flags()`, `delete_feature_flag()`, `add_feature_flag()`, `list_feature_flag_revisions()`, and `list_labels()`. + - `FeatureFlagClient` is available from `azure.appconfiguration` (sync) and `azure.appconfiguration.aio` (async). + - Added the `FeatureFlag` model along with `FeatureFlagConditions`, `FeatureFlagFilter`, `FeatureFlagVariantDefinition`, `FeatureFlagAllocation`, `FeatureFlagTelemetryConfiguration`, `PercentileAllocation`, `UserAllocation`, and `GroupAllocation`. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json b/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json index 6b109c7bf209..9db5a25a42ad 100644 --- a/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json +++ b/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json @@ -18,15 +18,31 @@ "azure.appconfiguration.models.Snapshot": "AzureAppConfiguration.Snapshot", "azure.appconfiguration.models.SnapshotUpdateParameters": "AzureAppConfiguration.SnapshotUpdateParameters", "azure.appconfiguration.models.UserAllocation": "AzureAppConfiguration.UserAllocation", - "azure.appconfiguration.models.ConfigurationSettingFields": "AzureAppConfiguration.KeyValueFields", "azure.appconfiguration.models.RequirementType": "AzureAppConfiguration.RequirementType", "azure.appconfiguration.models.StatusOverride": "AzureAppConfiguration.StatusOverride", "azure.appconfiguration.models.FeatureFlagFields": "AzureAppConfiguration.FeatureFlagFields", + "azure.appconfiguration.models.ConfigurationSettingFields": "AzureAppConfiguration.KeyValueFields", "azure.appconfiguration.models.SnapshotStatus": "AzureAppConfiguration.SnapshotStatus", "azure.appconfiguration.models.SnapshotComposition": "AzureAppConfiguration.CompositionType", "azure.appconfiguration.models.SnapshotFields": "AzureAppConfiguration.SnapshotFields", "azure.appconfiguration.models.OperationState": "Azure.Core.Foundations.OperationState", "azure.appconfiguration.models.LabelFields": "AzureAppConfiguration.LabelFields", + "azure.appconfiguration.operations.FeatureFlagClientOperations.get_feature_flags": "AzureAppConfiguration.getFeatureFlags", + "azure.appconfiguration.aio.operations.FeatureFlagClientOperations.get_feature_flags": "AzureAppConfiguration.getFeatureFlags", + "azure.appconfiguration.operations.FeatureFlagClientOperations.check_feature_flags": "AzureAppConfiguration.checkFeatureFlags", + "azure.appconfiguration.aio.operations.FeatureFlagClientOperations.check_feature_flags": "AzureAppConfiguration.checkFeatureFlags", + "azure.appconfiguration.operations.FeatureFlagClientOperations.get_feature_flag": "AzureAppConfiguration.getFeatureFlag", + "azure.appconfiguration.aio.operations.FeatureFlagClientOperations.get_feature_flag": "AzureAppConfiguration.getFeatureFlag", + "azure.appconfiguration.operations.FeatureFlagClientOperations.check_feature_flag": "AzureAppConfiguration.checkFeatureFlag", + "azure.appconfiguration.aio.operations.FeatureFlagClientOperations.check_feature_flag": "AzureAppConfiguration.checkFeatureFlag", + "azure.appconfiguration.operations.FeatureFlagClientOperations.put_feature_flag": "AzureAppConfiguration.putFeatureFlag", + "azure.appconfiguration.aio.operations.FeatureFlagClientOperations.put_feature_flag": "AzureAppConfiguration.putFeatureFlag", + "azure.appconfiguration.operations.FeatureFlagClientOperations.delete_feature_flag": "AzureAppConfiguration.deleteFeatureFlag", + "azure.appconfiguration.aio.operations.FeatureFlagClientOperations.delete_feature_flag": "AzureAppConfiguration.deleteFeatureFlag", + "azure.appconfiguration.operations.FeatureFlagClientOperations.get_feature_flag_revisions": "AzureAppConfiguration.getFeatureFlagRevisions", + "azure.appconfiguration.aio.operations.FeatureFlagClientOperations.get_feature_flag_revisions": "AzureAppConfiguration.getFeatureFlagRevisions", + "azure.appconfiguration.operations.FeatureFlagClientOperations.check_feature_flag_revisions": "AzureAppConfiguration.checkFeatureFlagRevisions", + "azure.appconfiguration.aio.operations.FeatureFlagClientOperations.check_feature_flag_revisions": "AzureAppConfiguration.checkFeatureFlagRevisions", "azure.appconfiguration.AzureAppConfigurationClient.get_keys": "AzureAppConfiguration.getKeys", "azure.appconfiguration.aio.AzureAppConfigurationClient.get_keys": "AzureAppConfiguration.getKeys", "azure.appconfiguration.AzureAppConfigurationClient.check_keys": "AzureAppConfiguration.checkKeys", @@ -41,22 +57,6 @@ "azure.appconfiguration.aio.AzureAppConfigurationClient.delete_key_value": "AzureAppConfiguration.deleteKeyValue", "azure.appconfiguration.AzureAppConfigurationClient.check_key_value": "AzureAppConfiguration.checkKeyValue", "azure.appconfiguration.aio.AzureAppConfigurationClient.check_key_value": "AzureAppConfiguration.checkKeyValue", - "azure.appconfiguration.AzureAppConfigurationClient.get_feature_flags": "AzureAppConfiguration.getFeatureFlags", - "azure.appconfiguration.aio.AzureAppConfigurationClient.get_feature_flags": "AzureAppConfiguration.getFeatureFlags", - "azure.appconfiguration.AzureAppConfigurationClient.check_feature_flags": "AzureAppConfiguration.checkFeatureFlags", - "azure.appconfiguration.aio.AzureAppConfigurationClient.check_feature_flags": "AzureAppConfiguration.checkFeatureFlags", - "azure.appconfiguration.AzureAppConfigurationClient.get_feature_flag": "AzureAppConfiguration.getFeatureFlag", - "azure.appconfiguration.aio.AzureAppConfigurationClient.get_feature_flag": "AzureAppConfiguration.getFeatureFlag", - "azure.appconfiguration.AzureAppConfigurationClient.check_feature_flag": "AzureAppConfiguration.checkFeatureFlag", - "azure.appconfiguration.aio.AzureAppConfigurationClient.check_feature_flag": "AzureAppConfiguration.checkFeatureFlag", - "azure.appconfiguration.AzureAppConfigurationClient.put_feature_flag": "AzureAppConfiguration.putFeatureFlag", - "azure.appconfiguration.aio.AzureAppConfigurationClient.put_feature_flag": "AzureAppConfiguration.putFeatureFlag", - "azure.appconfiguration.AzureAppConfigurationClient.delete_feature_flag": "AzureAppConfiguration.deleteFeatureFlag", - "azure.appconfiguration.aio.AzureAppConfigurationClient.delete_feature_flag": "AzureAppConfiguration.deleteFeatureFlag", - "azure.appconfiguration.AzureAppConfigurationClient.get_feature_flag_revisions": "AzureAppConfiguration.getFeatureFlagRevisions", - "azure.appconfiguration.aio.AzureAppConfigurationClient.get_feature_flag_revisions": "AzureAppConfiguration.getFeatureFlagRevisions", - "azure.appconfiguration.AzureAppConfigurationClient.check_feature_flag_revisions": "AzureAppConfiguration.checkFeatureFlagRevisions", - "azure.appconfiguration.aio.AzureAppConfigurationClient.check_feature_flag_revisions": "AzureAppConfiguration.checkFeatureFlagRevisions", "azure.appconfiguration.AzureAppConfigurationClient.get_snapshots": "AzureAppConfiguration.getSnapshots", "azure.appconfiguration.aio.AzureAppConfigurationClient.get_snapshots": "AzureAppConfiguration.getSnapshots", "azure.appconfiguration.AzureAppConfigurationClient.check_snapshots": "AzureAppConfiguration.checkSnapshots", @@ -82,5 +82,5 @@ "azure.appconfiguration.AzureAppConfigurationClient.check_revisions": "AzureAppConfiguration.checkRevisions", "azure.appconfiguration.aio.AzureAppConfigurationClient.check_revisions": "AzureAppConfiguration.checkRevisions" }, - "CrossLanguageVersion": "f60baf2bc06c" + "CrossLanguageVersion": "9848bf3c406d" } \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration/assets.json b/sdk/appconfiguration/azure-appconfiguration/assets.json index eec3394a0f36..a5f9617ef358 100644 --- a/sdk/appconfiguration/azure-appconfiguration/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration", - "Tag": "python/appconfiguration/azure-appconfiguration_cd1c4e2888" + "Tag": "python/appconfiguration/azure-appconfiguration_f51152e2e2" } diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py index ebc1cb0eee04..09f5f0956d89 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py @@ -10,6 +10,7 @@ # -------------------------------------------------------------------------- from ._azure_appconfiguration_client import AzureAppConfigurationClient +from ._feature_flag_client import FeatureFlagClient from ._constants import FILTER_PERCENTAGE, FILTER_TARGETING, FILTER_TIME_WINDOW from ._models import ( ConfigurationSetting, @@ -43,6 +44,7 @@ __version__ = VERSION __all__ = [ "AzureAppConfigurationClient", + "FeatureFlagClient", "ConfigurationSetting", "ResourceReadOnlyError", "FeatureFlagConfigurationSetting", diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py index c1075ceab3dd..5a072d5864c3 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py @@ -13,7 +13,7 @@ from azure.core.pipeline.policies import BearerTokenCredentialPolicy from azure.core.polling import LROPoller from azure.core.tracing.decorator import distributed_trace -from azure.core.exceptions import ResourceNotFoundError, ResourceNotModifiedError +from azure.core.exceptions import ResourceNotModifiedError from azure.core.rest import HttpRequest, HttpResponse from ._azure_appconfiguration_error import ResourceReadOnlyError from ._azure_appconfiguration_requests import AppConfigRequestsCredentialsPolicy @@ -34,7 +34,6 @@ ConfigurationSettingsFilter, ConfigurationSnapshot, ConfigurationSettingLabel, - FeatureFlag, ) from ._audience import get_audience, DEFAULT_SCOPE_SUFFIX from ._utils import ( @@ -839,260 +838,6 @@ def list_snapshots( **kwargs, ) - @distributed_trace - def list_feature_flags( - self, - name_filter: Optional[str] = None, - label_filter: Optional[str] = None, - *, - tags_filter: Optional[List[str]] = None, - accept_datetime: Optional[Union[datetime, str]] = None, - **kwargs: Any, - ) -> ItemPaged["FeatureFlag"]: - """ - Find the FeatureFlag objects, optionally filtered by name, label, tags and accept_datetime. - - :param name_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end - of the filter. Default is `None`. - :type name_filter: str or None - :param label_filter: Filter results based on their labels. Default is `None`. - :type label_filter: str or None - :keyword tags_filter: Filter results based on their tags. Default is `None`. - :paramtype tags_filter: list[str] or None - :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime - :paramtype accept_datetime: ~datetime.datetime or str or None - :return: An iterator of :class:`~azure.appconfiguration.FeatureFlag` - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.FeatureFlag] - :raises: :class:`~azure.core.exceptions.HttpResponseError` - - Example - - .. code-block:: python - - # List feature flags - feature_flags = client.list_feature_flags() - for flag in feature_flags: - print(flag.name, flag.enabled) - """ - if isinstance(accept_datetime, datetime): - accept_datetime = str(accept_datetime) - - # Call the generated method and wrap results to convert to SDK FeatureFlag - def convert_to_sdk(objs): - return [FeatureFlag._from_generated(obj) for obj in objs] - - return self._impl.get_feature_flags( # type: ignore[return-value] - name=name_filter, - label=label_filter, - accept_datetime=accept_datetime, - tags=tags_filter, - cls=convert_to_sdk, - **kwargs, - ) - - @distributed_trace - def get_feature_flag( - self, - feature_id: str, - label: Optional[str] = None, - *, - etag: Optional[str] = None, - match_condition: MatchConditions = MatchConditions.Unconditionally, - accept_datetime: Optional[Union[datetime, str]] = None, - **kwargs: Any, - ) -> Union[None, "FeatureFlag"]: - """Get a FeatureFlag from Azure App Configuration service - - :param feature_id: The feature flag name/id to retrieve - :type feature_id: str - :param label: Label used to identify the FeatureFlag. Default is `None`. - :type label: str or None - :keyword etag: Check if the FeatureFlag is changed. Set None to skip checking etag - :paramtype etag: str or None - :keyword match_condition: The match condition to use upon the etag - :paramtype match_condition: ~azure.core.MatchConditions - :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime - :paramtype accept_datetime: ~datetime.datetime or str or None - :return: The matched FeatureFlag object - :rtype: ~azure.appconfiguration.FeatureFlag or None - :raises: :class:`~azure.core.exceptions.HttpResponseError`, \ - :class:`~azure.core.exceptions.ClientAuthenticationError`, \ - :class:`~azure.core.exceptions.ResourceNotFoundError`, \ - :class:`~azure.core.exceptions.ResourceModifiedError`, \ - :class:`~azure.core.exceptions.ResourceExistsError` - - Example - - .. code-block:: python - - feature_flag = client.get_feature_flag(feature_id="MyFeatureFlag") - """ - if isinstance(accept_datetime, datetime): - accept_datetime = str(accept_datetime) - try: - generated_feature_flag = self._impl.get_feature_flag( - name=feature_id, - label=label, - accept_datetime=accept_datetime, - etag=etag, - match_condition=match_condition, - **kwargs, - ) - return FeatureFlag._from_generated(generated_feature_flag) - except (ResourceNotFoundError, ResourceNotModifiedError): - return None - - @distributed_trace - def set_feature_flag( - self, - feature_flag: "FeatureFlag", - match_condition: MatchConditions = MatchConditions.Unconditionally, - *, - etag: Optional[str] = None, - **kwargs: Any, - ) -> "FeatureFlag": - """Add or update a FeatureFlag. - If the feature flag identified by feature_id and label does not exist, this is a create. - Otherwise this is an update. - - :param feature_flag: The FeatureFlag to be added (if not exists) \ - or updated (if exists) to the service - :type feature_flag: ~azure.appconfiguration.FeatureFlag - :param match_condition: The match condition to use upon the etag - :type match_condition: ~azure.core.MatchConditions - :keyword etag: Check if the FeatureFlag is changed. \ - Will use the value from param feature_flag if not set. - :paramtype etag: str or None - :return: The FeatureFlag returned from the service - :rtype: ~azure.appconfiguration.FeatureFlag - :raises: :class:`~azure.appconfiguration.ResourceReadOnlyError`, \ - :class:`~azure.core.exceptions.HttpResponseError`, \ - :class:`~azure.core.exceptions.ClientAuthenticationError`, \ - :class:`~azure.core.exceptions.ResourceModifiedError`, \ - :class:`~azure.core.exceptions.ResourceNotModifiedError`, \ - :class:`~azure.core.exceptions.ResourceNotFoundError`, \ - :class:`~azure.core.exceptions.ResourceExistsError` - - Example - - .. code-block:: python - - feature_flag = FeatureFlag( - name="MyFeatureFlag", - enabled=True - ) - returned_feature_flag = client.set_feature_flag(feature_flag) - """ - error_map: Dict[int, Any] = {409: ResourceReadOnlyError} - generated_feature_flag = feature_flag._to_generated() - generated_result = self._impl.put_feature_flag( - entity=generated_feature_flag, - name=feature_flag.name, - label=feature_flag.label, - etag=etag or feature_flag.etag, - match_condition=match_condition, - error_map=error_map, - **kwargs, - ) - return FeatureFlag._from_generated(generated_result) - - @distributed_trace - def delete_feature_flag( # pylint:disable=delete-operation-wrong-return-type - self, - feature_id: str, - label: Optional[str] = None, - *, - etag: Optional[str] = None, - match_condition: MatchConditions = MatchConditions.Unconditionally, - **kwargs: Any, - ) -> Union[None, "FeatureFlag"]: - """Delete a FeatureFlag if it exists - - :param feature_id: The feature flag name/id to delete - :type feature_id: str - :param label: Label used to identify the FeatureFlag. Default is `None`. - :type label: str or None - :keyword etag: Check if the FeatureFlag is changed. Set None to skip checking etag - :paramtype etag: str or None - :keyword match_condition: The match condition to use upon the etag - :paramtype match_condition: ~azure.core.MatchConditions - :return: The deleted FeatureFlag returned from the service, or None if it doesn't exist. - :rtype: ~azure.appconfiguration.FeatureFlag or None - :raises: :class:`~azure.appconfiguration.ResourceReadOnlyError`, \ - :class:`~azure.core.exceptions.HttpResponseError`, \ - :class:`~azure.core.exceptions.ClientAuthenticationError`, \ - :class:`~azure.core.exceptions.ResourceModifiedError`, \ - :class:`~azure.core.exceptions.ResourceNotModifiedError`, \ - :class:`~azure.core.exceptions.ResourceNotFoundError`, \ - :class:`~azure.core.exceptions.ResourceExistsError` - - Example - - .. code-block:: python - - deleted_feature_flag = client.delete_feature_flag(feature_id="MyFeatureFlag") - """ - error_map: Dict[int, Any] = {409: ResourceReadOnlyError} - generated_feature_flag = self._impl.delete_feature_flag( - name=feature_id, - label=label, - etag=etag, - match_condition=match_condition, - error_map=error_map, - **kwargs, - ) - if generated_feature_flag: - return FeatureFlag._from_generated(generated_feature_flag) - return None - - @distributed_trace - def list_feature_flag_revisions( - self, - feature_id_filter: Optional[str] = None, - label_filter: Optional[str] = None, - *, - accept_datetime: Optional[Union[datetime, str]] = None, - **kwargs: Any, - ) -> ItemPaged["FeatureFlag"]: - """ - Find the FeatureFlag revision history, optionally filtered by feature_id and label. - - :param feature_id_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end - of the filter. Default is `None`. - :type feature_id_filter: str or None - :param label_filter: Filter results based on their labels. Default is `None`. - :type label_filter: str or None - :keyword accept_datetime: Retrieve FeatureFlag revisions that existed at this datetime - :paramtype accept_datetime: ~datetime.datetime or str or None - :return: An iterator of :class:`~azure.appconfiguration.FeatureFlag` - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.FeatureFlag] - :raises: :class:`~azure.core.exceptions.HttpResponseError` - - Example - - .. code-block:: python - - # List feature flag revisions - revisions = client.list_feature_flag_revisions(feature_id_filter="MyFeatureFlag") - for revision in revisions: - print(revision.name, revision.etag) - """ - if isinstance(accept_datetime, datetime): - accept_datetime = str(accept_datetime) - - # Build kwargs for the underlying call - impl_kwargs = dict(kwargs) - if accept_datetime is not None: - impl_kwargs["accept_datetime"] = accept_datetime - - # Call the generated method and wrap results to convert to SDK FeatureFlag - def convert_to_sdk(objs): - return [FeatureFlag._from_generated(obj) for obj in objs] - - return self._impl.get_feature_flag_revisions( # type: ignore[return-value] - name=feature_id_filter, label=label_filter, cls=convert_to_sdk, **impl_kwargs - ) - def update_sync_token(self, token: str) -> None: """Add a sync token to the internal list of tokens. diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py new file mode 100644 index 000000000000..7ff1e11e95db --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py @@ -0,0 +1,489 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +from datetime import datetime +from typing import Any, Dict, List, Optional, Union +from azure.core import MatchConditions +from azure.core.paging import ItemPaged +from azure.core.credentials import TokenCredential, AzureKeyCredential +from azure.core.pipeline.policies import BearerTokenCredentialPolicy +from azure.core.tracing.decorator import distributed_trace +from azure.core.exceptions import ResourceNotFoundError, ResourceNotModifiedError +from azure.core.rest import HttpRequest, HttpResponse +from ._azure_appconfiguration_error import ResourceReadOnlyError +from ._azure_appconfiguration_requests import AppConfigRequestsCredentialsPolicy +from ._query_param_policy import QueryParamPolicy +from ._generated import AzureAppConfigurationClient as AzureAppConfigurationClientGenerated +from ._generated.models import LabelFields +from ._models import FeatureFlag, ConfigurationSettingLabel +from ._audience import get_audience, DEFAULT_SCOPE_SUFFIX +from ._utils import parse_connection_string +from ._sync_token import SyncTokenPolicy +from ._audience_error_handling_policy import AudienceErrorHandlingPolicy + + +class FeatureFlagClient: + """Represents a client that manages feature flags in the Azure App Configuration service. + + :param str base_url: Base url of the service. + :param credential: An object which can provide secrets for the app configuration service + :type credential: ~azure.core.credentials.TokenCredential + :keyword api_version: Api Version. Default value is "2023-11-01". Note that overriding this default + value may result in unsupported behavior. + :paramtype api_version: str + :keyword audience: The audience to use for authentication with Microsoft Entra. Defaults to the public Azure App + Configuration audience. See the supported audience list at https://aka.ms/appconfig/client-token-audience + :paramtype audience: str + + """ + + # pylint:disable=protected-access + def __init__(self, base_url: str, credential: TokenCredential, **kwargs: Any) -> None: + try: + if not base_url.lower().startswith("http"): + base_url = f"https://{base_url}" + except AttributeError as exc: + raise ValueError("Base URL must be a string.") from exc + + if not credential: + raise ValueError("Missing credential") + + self._sync_token_policy = SyncTokenPolicy() + self._query_param_policy = QueryParamPolicy() + + audience = kwargs.pop("audience", None) + + audience_policy = AudienceErrorHandlingPolicy(bool(audience)) + per_call_policies = [self._query_param_policy, self._sync_token_policy, audience_policy] + + if audience is None: + audience = get_audience(base_url) + + # Ensure all scopes end with /.default and strip any trailing slashes before adding suffix + audience_scope = audience.rstrip("/") + "/" + DEFAULT_SCOPE_SUFFIX + kwargs["credential_scopes"] = [audience_scope] + + if isinstance(credential, AzureKeyCredential): + id_credential = kwargs.pop("id_credential") + kwargs.update( + { + "authentication_policy": AppConfigRequestsCredentialsPolicy(credential, base_url, id_credential), + } + ) + elif isinstance(credential, TokenCredential): + kwargs.update( + { + "authentication_policy": BearerTokenCredentialPolicy( + credential, *kwargs["credential_scopes"], **kwargs + ), + } + ) + else: + raise TypeError( + f"Unsupported credential: {type(credential)}. Use an instance of token credential from azure.identity" + ) + # mypy doesn't compare the credential type hint with the API surface in patch.py + self._impl = AzureAppConfigurationClientGenerated( + base_url, credential, per_call_policies=per_call_policies, **kwargs # type: ignore[arg-type] + ) + + @classmethod + def from_connection_string(cls, connection_string: str, **kwargs: Any) -> "FeatureFlagClient": + """Create FeatureFlagClient from a Connection String. + + :param str connection_string: Connection String + (one of the access keys of the Azure App Configuration resource) + used to access the Azure App Configuration. + :return: A FeatureFlagClient authenticated with the connection string + :rtype: ~azure.appconfiguration.FeatureFlagClient + + Example + + .. code-block:: python + + from azure.appconfiguration import FeatureFlagClient + + connection_str = "" + client = FeatureFlagClient.from_connection_string(connection_str) + """ + endpoint, id_credential, secret = parse_connection_string(connection_string) + # AzureKeyCredential type is for internal use, it's not exposed in public API. + return cls( + credential=AzureKeyCredential(secret), # type: ignore[arg-type] + base_url=endpoint, + id_credential=id_credential, + **kwargs, + ) + + @distributed_trace + def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: Any) -> HttpResponse: + """Runs a network request using the client's existing pipeline. + + The request URL can be relative to the vault URL. The service API version used for the request is the same as + the client's unless otherwise specified. This method does not raise if the response is an error; to raise an + exception, call `raise_for_status()` on the returned response object. For more information about how to send + custom requests with this method, see https://aka.ms/azsdk/dpcodegen/python/send_request. + + :param request: The network request you want to make. + :type request: ~azure.core.rest.HttpRequest + :keyword bool stream: Whether the response payload will be streamed. Defaults to False. + :return: The response of your network call. Does not do error handling on your response. + :rtype: ~azure.core.rest.HttpResponse + """ + return self._impl.send_request(request, stream=stream, **kwargs) + + @distributed_trace + def list_feature_flags( + self, + name_filter: Optional[str] = None, + label_filter: Optional[str] = None, + *, + tags_filter: Optional[List[str]] = None, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> ItemPaged["FeatureFlag"]: + """ + Find the FeatureFlag objects, optionally filtered by name, label, tags and accept_datetime. + + :param name_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end + of the filter. Default is `None`. + :type name_filter: str or None + :param label_filter: Filter results based on their labels. Default is `None`. + :type label_filter: str or None + :keyword tags_filter: Filter results based on their tags. Default is `None`. + :paramtype tags_filter: list[str] or None + :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: An iterator of :class:`~azure.appconfiguration.FeatureFlag` + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.FeatureFlag] + :raises: :class:`~azure.core.exceptions.HttpResponseError` + + Example + + .. code-block:: python + + # List feature flags + feature_flags = client.list_feature_flags() + for flag in feature_flags: + print(flag.name, flag.enabled) + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + + # Call the generated method and wrap results to convert to SDK FeatureFlag + def convert_to_sdk(objs): + return [FeatureFlag._from_generated(obj) for obj in objs] # pylint:disable=protected-access + + return self._impl.feature_flag_client.get_feature_flags( # type: ignore[return-value] + name=name_filter, + label=label_filter, + accept_datetime=accept_datetime, + tags=tags_filter, + cls=convert_to_sdk, + **kwargs, + ) + + @distributed_trace + def get_feature_flag( + self, + feature_id: str, + label: Optional[str] = None, + *, + etag: Optional[str] = None, + match_condition: MatchConditions = MatchConditions.Unconditionally, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> Union[None, "FeatureFlag"]: + """Get a FeatureFlag from Azure App Configuration service + + :param feature_id: The feature flag name/id to retrieve + :type feature_id: str + :param label: Label used to identify the FeatureFlag. Default is `None`. + :type label: str or None + :keyword etag: Check if the FeatureFlag is changed. Set None to skip checking etag + :paramtype etag: str or None + :keyword match_condition: The match condition to use upon the etag + :paramtype match_condition: ~azure.core.MatchConditions + :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: The matched FeatureFlag object + :rtype: ~azure.appconfiguration.FeatureFlag or None + :raises: :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError`, \ + :class:`~azure.core.exceptions.ResourceNotFoundError`, \ + :class:`~azure.core.exceptions.ResourceModifiedError`, \ + :class:`~azure.core.exceptions.ResourceExistsError` + + Example + + .. code-block:: python + + feature_flag = client.get_feature_flag(feature_id="MyFeatureFlag") + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + try: + generated_feature_flag = self._impl.feature_flag_client.get_feature_flag( + name=feature_id, + label=label, + accept_datetime=accept_datetime, + etag=etag, + match_condition=match_condition, + **kwargs, + ) + return FeatureFlag._from_generated(generated_feature_flag) + except (ResourceNotFoundError, ResourceNotModifiedError): + return None + + @distributed_trace + def add_feature_flag(self, feature_flag: "FeatureFlag", **kwargs: Any) -> "FeatureFlag": + """Add a FeatureFlag instance into the Azure App Configuration service. + + :param feature_flag: The FeatureFlag object to be added + :type feature_flag: ~azure.appconfiguration.FeatureFlag + :return: The FeatureFlag object returned from the App Configuration service + :rtype: ~azure.appconfiguration.FeatureFlag + :raises: :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError`, \ + :class:`~azure.core.exceptions.ResourceExistsError` + + Example + + .. code-block:: python + + feature_flag = FeatureFlag( + name="MyFeatureFlag", + enabled=True + ) + added_feature_flag = client.add_feature_flag(feature_flag) + """ + generated_feature_flag = feature_flag._to_generated() + generated_result = self._impl.feature_flag_client.put_feature_flag( + entity=generated_feature_flag, + name=feature_flag.name, + label=feature_flag.label, + match_condition=MatchConditions.IfMissing, + **kwargs, + ) + return FeatureFlag._from_generated(generated_result) + + @distributed_trace + def set_feature_flag( + self, + feature_flag: "FeatureFlag", + match_condition: MatchConditions = MatchConditions.Unconditionally, + *, + etag: Optional[str] = None, + **kwargs: Any, + ) -> "FeatureFlag": + """Add or update a FeatureFlag. + If the feature flag identified by feature_id and label does not exist, this is a create. + Otherwise this is an update. + + :param feature_flag: The FeatureFlag to be added (if not exists) \ + or updated (if exists) to the service + :type feature_flag: ~azure.appconfiguration.FeatureFlag + :param match_condition: The match condition to use upon the etag + :type match_condition: ~azure.core.MatchConditions + :keyword etag: Check if the FeatureFlag is changed. \ + Will use the value from param feature_flag if not set. + :paramtype etag: str or None + :return: The FeatureFlag returned from the service + :rtype: ~azure.appconfiguration.FeatureFlag + :raises: :class:`~azure.appconfiguration.ResourceReadOnlyError`, \ + :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError`, \ + :class:`~azure.core.exceptions.ResourceModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotFoundError`, \ + :class:`~azure.core.exceptions.ResourceExistsError` + + Example + + .. code-block:: python + + feature_flag = FeatureFlag( + name="MyFeatureFlag", + enabled=True + ) + returned_feature_flag = client.set_feature_flag(feature_flag) + """ + error_map: Dict[int, Any] = {409: ResourceReadOnlyError} + generated_feature_flag = feature_flag._to_generated() + generated_result = self._impl.feature_flag_client.put_feature_flag( + entity=generated_feature_flag, + name=feature_flag.name, + label=feature_flag.label, + etag=etag or feature_flag.etag, + match_condition=match_condition, + error_map=error_map, + **kwargs, + ) + return FeatureFlag._from_generated(generated_result) + + @distributed_trace + def delete_feature_flag( # pylint:disable=delete-operation-wrong-return-type + self, + feature_id: str, + label: Optional[str] = None, + *, + etag: Optional[str] = None, + match_condition: MatchConditions = MatchConditions.Unconditionally, + **kwargs: Any, + ) -> Union[None, "FeatureFlag"]: + """Delete a FeatureFlag if it exists + + :param feature_id: The feature flag name/id to delete + :type feature_id: str + :param label: Label used to identify the FeatureFlag. Default is `None`. + :type label: str or None + :keyword etag: Check if the FeatureFlag is changed. Set None to skip checking etag + :paramtype etag: str or None + :keyword match_condition: The match condition to use upon the etag + :paramtype match_condition: ~azure.core.MatchConditions + :return: The deleted FeatureFlag returned from the service, or None if it doesn't exist. + :rtype: ~azure.appconfiguration.FeatureFlag or None + :raises: :class:`~azure.appconfiguration.ResourceReadOnlyError`, \ + :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError`, \ + :class:`~azure.core.exceptions.ResourceModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotFoundError`, \ + :class:`~azure.core.exceptions.ResourceExistsError` + + Example + + .. code-block:: python + + deleted_feature_flag = client.delete_feature_flag(feature_id="MyFeatureFlag") + """ + error_map: Dict[int, Any] = {409: ResourceReadOnlyError} + generated_feature_flag = self._impl.feature_flag_client.delete_feature_flag( + name=feature_id, + label=label, + etag=etag, + match_condition=match_condition, + error_map=error_map, + **kwargs, + ) + if generated_feature_flag: + return FeatureFlag._from_generated(generated_feature_flag) + return None + + @distributed_trace + def list_feature_flag_revisions( + self, + feature_id_filter: Optional[str] = None, + label_filter: Optional[str] = None, + *, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> ItemPaged["FeatureFlag"]: + """ + Find the FeatureFlag revision history, optionally filtered by feature_id and label. + + :param feature_id_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end + of the filter. Default is `None`. + :type feature_id_filter: str or None + :param label_filter: Filter results based on their labels. Default is `None`. + :type label_filter: str or None + :keyword accept_datetime: Retrieve FeatureFlag revisions that existed at this datetime + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: An iterator of :class:`~azure.appconfiguration.FeatureFlag` + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.FeatureFlag] + :raises: :class:`~azure.core.exceptions.HttpResponseError` + + Example + + .. code-block:: python + + # List feature flag revisions + revisions = client.list_feature_flag_revisions(feature_id_filter="MyFeatureFlag") + for revision in revisions: + print(revision.name, revision.etag) + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + + # Build kwargs for the underlying call + impl_kwargs = dict(kwargs) + if accept_datetime is not None: + impl_kwargs["accept_datetime"] = accept_datetime + + # Call the generated method and wrap results to convert to SDK FeatureFlag + def convert_to_sdk(objs): + return [FeatureFlag._from_generated(obj) for obj in objs] # pylint:disable=protected-access + + return self._impl.feature_flag_client.get_feature_flag_revisions( # type: ignore[return-value] + name=feature_id_filter, label=label_filter, cls=convert_to_sdk, **impl_kwargs + ) + + @distributed_trace + def list_labels( + self, + *, + name: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[Union[datetime, str]] = None, + fields: Optional[List[Union[str, LabelFields]]] = None, + resource_type: Optional[str] = None, + **kwargs: Any, + ) -> ItemPaged[ConfigurationSettingLabel]: + """Gets a list of labels. + + :keyword name: A filter for the name of the returned labels. '*' can be used as wildcard + in the beginning or end of the filter. For more information about supported filters, see + https://learn.microsoft.com/azure/azure-app-configuration/rest-api-labels?pivots=v23-11#supported-filters. + :paramtype name: str or None + :keyword after: Instructs the server to return elements that appear after the element referred to + by the specified token. + :paramtype after: str or None + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified time. + :paramtype accept_datetime: ~datetime.datetime or str or None + :keyword fields: Specify which fields to include in the results. If not specified, will include all fields. + Available fields see :class:`~azure.appconfiguration.LabelFields`. + :paramtype fields: list[str] or list[~azure.appconfiguration.LabelFields] or None + :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted values: + 'kv' for key-value labels, 'ff' for feature flag labels. Default is `None`. + :paramtype resource_type: str or None + :return: An iterator of labels. + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.ConfigurationSettingLabel] + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + return self._impl.get_labels( # type: ignore[return-value] + name=name, + after=after, + accept_datetime=accept_datetime, + select=fields, + resource_type=resource_type, + cls=lambda objs: [ConfigurationSettingLabel(name=x.name) for x in objs], + **kwargs, + ) + + def update_sync_token(self, token: str) -> None: + """Add a sync token to the internal list of tokens. + + :param str token: The sync token to be added to the internal list of tokens + """ + if not self._sync_token_policy: + raise AttributeError( + "Client has no sync token policy, possibly because it was not provided during instantiation." + ) + self._sync_token_policy.add_token(token) + + def close(self) -> None: + """Close all connections made by the client""" + self._impl._client.close() + + def __enter__(self) -> "FeatureFlagClient": + self._impl.__enter__() + return self + + def __exit__(self, *args: Any) -> None: + self._impl.__exit__(*args) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_client.py index 0b4aec99b87f..227516cba44d 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_client.py @@ -16,8 +16,8 @@ from azure.core.rest import HttpRequest, HttpResponse from ._configuration import AzureAppConfigurationClientConfiguration -from ._operations import _AzureAppConfigurationClientOperationsMixin from ._utils.serialization import Deserializer, Serializer +from .operations import FeatureFlagClientOperations, _AzureAppConfigurationClientOperationsMixin if sys.version_info >= (3, 11): from typing import Self @@ -31,6 +31,8 @@ class AzureAppConfigurationClient(_AzureAppConfigurationClientOperationsMixin): """Azure App Configuration REST API. + :ivar feature_flag_client: FeatureFlagClientOperations operations + :vartype feature_flag_client: azure.appconfiguration.operations.FeatureFlagClientOperations :param endpoint: Required. :type endpoint: str :param credential: Credential used to authenticate requests to the service. Is either a key @@ -72,6 +74,9 @@ def __init__(self, endpoint: str, credential: Union[AzureKeyCredential, "TokenCr self._serialize = Serializer() self._deserialize = Deserializer() self._serialize.client_side_validation = False + self.feature_flag_client = FeatureFlagClientOperations( + self._client, self._config, self._serialize, self._deserialize + ) def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: Any) -> HttpResponse: """Runs the network request through the client's chained policies. diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_utils/model_base.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_utils/model_base.py index d725c55906d3..b93f5120d517 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_utils/model_base.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_utils/model_base.py @@ -109,6 +109,29 @@ def _serialize_bytes(o, format: typing.Optional[str] = None) -> str: return encoded +def _serialize_duration(td: timedelta, format: typing.Optional[str] = None): + """Serialize a timedelta to its wire representation. + + For the ``seconds``/``milliseconds`` encodings the value is converted to a + numeric value, otherwise it falls back to an ISO 8601 duration string. + + :param timedelta td: The timedelta to serialize. + :param str format: The duration encoding format. + :rtype: int or float or str + :return: serialized duration + """ + seconds = td.total_seconds() + if format == "duration-seconds-int": + return int(seconds) + if format == "duration-seconds-float": + return seconds + if format == "duration-milliseconds-int": + return int(seconds * 1000) + if format == "duration-milliseconds-float": + return seconds * 1000 + return _timedelta_as_isostr(td) + + def _serialize_datetime(o, format: typing.Optional[str] = None): if hasattr(o, "year") and hasattr(o, "hour"): if format == "rfc7231": @@ -301,6 +324,12 @@ def _deserialize_duration(attr): return isodate.parse_duration(attr) +def _deserialize_duration_numeric(attr, unit): + if isinstance(attr, timedelta): + return attr + return timedelta(**{unit: float(attr)}) + + def _deserialize_decimal(attr): if isinstance(attr, decimal.Decimal): return attr @@ -330,6 +359,10 @@ def _deserialize_int_as_str(attr): "unix-timestamp": _deserialize_datetime_unix_timestamp, "base64": _deserialize_bytes, "base64url": _deserialize_bytes_base64, + "duration-seconds-int": functools.partial(_deserialize_duration_numeric, unit="seconds"), + "duration-seconds-float": functools.partial(_deserialize_duration_numeric, unit="seconds"), + "duration-milliseconds-int": functools.partial(_deserialize_duration_numeric, unit="milliseconds"), + "duration-milliseconds-float": functools.partial(_deserialize_duration_numeric, unit="milliseconds"), } @@ -564,7 +597,7 @@ def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-m pass # Last, try datetime.timedelta try: - return _timedelta_as_isostr(o) + return _serialize_duration(o, format) except AttributeError: # This will be raised when it hits value.total_seconds in the method above pass diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_utils/serialization.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_utils/serialization.py index a088671e9c51..75906e2eb77f 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_utils/serialization.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_utils/serialization.py @@ -520,6 +520,10 @@ def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: "rfc-1123": Serializer.serialize_rfc, "unix-time": Serializer.serialize_unix, "duration": Serializer.serialize_duration, + "duration-seconds-int": Serializer.serialize_duration_seconds_int, + "duration-seconds-float": Serializer.serialize_duration_seconds_float, + "duration-milliseconds-int": Serializer.serialize_duration_milliseconds_int, + "duration-milliseconds-float": Serializer.serialize_duration_milliseconds_float, "date": Serializer.serialize_date, "time": Serializer.serialize_time, "decimal": Serializer.serialize_decimal, @@ -1109,6 +1113,61 @@ def serialize_duration(attr, **kwargs): # pylint: disable=unused-argument attr = isodate.parse_duration(attr) return isodate.duration_isoformat(attr) + @staticmethod + def _serialize_duration_numeric(attr, scale, as_int): + """Serialize a TimeDelta into a numeric value scaled to the wire unit. + + :param TimeDelta attr: Object to be serialized. + :param int scale: Multiplier applied to total seconds (1 for seconds, 1000 for milliseconds). + :param bool as_int: Whether to truncate the result to an int. + :rtype: int or float + :return: serialized duration + """ + if isinstance(attr, str): + attr = isodate.parse_duration(attr) + value = attr.total_seconds() * scale if isinstance(attr, datetime.timedelta) else attr + return int(value) if as_int else float(value) + + @staticmethod + def serialize_duration_seconds_int(attr, **kwargs): # pylint: disable=unused-argument + """Serialize TimeDelta object into an integer number of seconds. + + :param TimeDelta attr: Object to be serialized. + :rtype: int + :return: serialized duration + """ + return Serializer._serialize_duration_numeric(attr, 1, True) + + @staticmethod + def serialize_duration_seconds_float(attr, **kwargs): # pylint: disable=unused-argument + """Serialize TimeDelta object into a floating point number of seconds. + + :param TimeDelta attr: Object to be serialized. + :rtype: float + :return: serialized duration + """ + return Serializer._serialize_duration_numeric(attr, 1, False) + + @staticmethod + def serialize_duration_milliseconds_int(attr, **kwargs): # pylint: disable=unused-argument + """Serialize TimeDelta object into an integer number of milliseconds. + + :param TimeDelta attr: Object to be serialized. + :rtype: int + :return: serialized duration + """ + return Serializer._serialize_duration_numeric(attr, 1000, True) + + @staticmethod + def serialize_duration_milliseconds_float(attr, **kwargs): # pylint: disable=unused-argument + """Serialize TimeDelta object into a floating point number of milliseconds. + + :param TimeDelta attr: Object to be serialized. + :rtype: float + :return: serialized duration + """ + return Serializer._serialize_duration_numeric(attr, 1000, False) + @staticmethod def serialize_rfc(attr, **kwargs): # pylint: disable=unused-argument """Serialize Datetime object into RFC-1123 formatted string. @@ -1381,6 +1440,10 @@ def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: "rfc-1123": Deserializer.deserialize_rfc, "unix-time": Deserializer.deserialize_unix, "duration": Deserializer.deserialize_duration, + "duration-seconds-int": Deserializer.deserialize_duration_seconds, + "duration-seconds-float": Deserializer.deserialize_duration_seconds, + "duration-milliseconds-int": Deserializer.deserialize_duration_milliseconds, + "duration-milliseconds-float": Deserializer.deserialize_duration_milliseconds, "date": Deserializer.deserialize_date, "time": Deserializer.deserialize_time, "decimal": Deserializer.deserialize_decimal, @@ -1393,6 +1456,10 @@ def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: } self.deserialize_expected_types = { "duration": (isodate.Duration, datetime.timedelta), + "duration-seconds-int": (isodate.Duration, datetime.timedelta), + "duration-seconds-float": (isodate.Duration, datetime.timedelta), + "duration-milliseconds-int": (isodate.Duration, datetime.timedelta), + "duration-milliseconds-float": (isodate.Duration, datetime.timedelta), "iso-8601": (datetime.datetime), } self.dependencies: dict[str, type] = dict(classes) if classes else {} @@ -1954,6 +2021,48 @@ def deserialize_duration(attr): raise DeserializationError(msg) from err return duration + @staticmethod + def _deserialize_duration_numeric(attr, unit): + """Deserialize a numeric duration value into a TimeDelta object. + + :param float attr: response value to be deserialized. + :param str unit: The wire unit, used as the ``timedelta`` keyword + (``"seconds"`` or ``"milliseconds"``). + :return: Deserialized duration + :rtype: TimeDelta + :raises DeserializationError: if value is invalid. + """ + if isinstance(attr, ET.Element): + attr = attr.text + try: + duration = datetime.timedelta(**{unit: float(attr)}) # type: ignore + except (ValueError, OverflowError, TypeError) as err: + msg = "Cannot deserialize duration object." + raise DeserializationError(msg) from err + return duration + + @staticmethod + def deserialize_duration_seconds(attr): + """Deserialize a numeric number of seconds into a TimeDelta object. + + :param float attr: response value to be deserialized. + :return: Deserialized duration + :rtype: TimeDelta + :raises DeserializationError: if value is invalid. + """ + return Deserializer._deserialize_duration_numeric(attr, "seconds") + + @staticmethod + def deserialize_duration_milliseconds(attr): + """Deserialize a numeric number of milliseconds into a TimeDelta object. + + :param float attr: response value to be deserialized. + :return: Deserialized duration + :rtype: TimeDelta + :raises DeserializationError: if value is invalid. + """ + return Deserializer._deserialize_duration_numeric(attr, "milliseconds") + @staticmethod def deserialize_date(attr): """Deserialize ISO-8601 formatted string into Date object. diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_version.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_version.py index 97f7a0416f1e..be71c81bd282 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_version.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "1.9.0" +VERSION = "1.0.0b1" diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_client.py index f24f87ec8a9b..0375435c6d7d 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_client.py @@ -17,7 +17,7 @@ from .._utils.serialization import Deserializer, Serializer from ._configuration import AzureAppConfigurationClientConfiguration -from ._operations import _AzureAppConfigurationClientOperationsMixin +from .operations import FeatureFlagClientOperations, _AzureAppConfigurationClientOperationsMixin if sys.version_info >= (3, 11): from typing import Self @@ -31,6 +31,8 @@ class AzureAppConfigurationClient(_AzureAppConfigurationClientOperationsMixin): """Azure App Configuration REST API. + :ivar feature_flag_client: FeatureFlagClientOperations operations + :vartype feature_flag_client: azure.appconfiguration.aio.operations.FeatureFlagClientOperations :param endpoint: Required. :type endpoint: str :param credential: Credential used to authenticate requests to the service. Is either a key @@ -74,6 +76,9 @@ def __init__( self._serialize = Serializer() self._deserialize = Deserializer() self._serialize.client_side_validation = False + self.feature_flag_client = FeatureFlagClientOperations( + self._client, self._config, self._serialize, self._deserialize + ) def send_request( self, request: HttpRequest, *, stream: bool = False, **kwargs: Any diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/__init__.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/__init__.py similarity index 88% rename from sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/__init__.py rename to sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/__init__.py index 91bafc83f689..478d88f8496d 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/__init__.py @@ -12,12 +12,15 @@ if TYPE_CHECKING: from ._patch import * # pylint: disable=unused-wildcard-import +from ._operations import FeatureFlagClientOperations # type: ignore from ._operations import _AzureAppConfigurationClientOperationsMixin # type: ignore # pylint: disable=unused-import from ._patch import __all__ as _patch_all from ._patch import * from ._patch import patch_sdk as _patch_sdk -__all__ = [] +__all__ = [ + "FeatureFlagClientOperations", +] __all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore _patch_sdk() diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_operations/_operations.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_operations.py similarity index 98% rename from sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_operations/_operations.py rename to sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_operations.py index 9aee4704a00c..fd4dfcaf5d52 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_operations/_operations.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_operations.py @@ -34,10 +34,11 @@ from azure.core.utils import case_insensitive_dict from ... import models as _models -from ..._operations._operations import ( - build_azure_app_configuration_check_feature_flag_request, - build_azure_app_configuration_check_feature_flag_revisions_request, - build_azure_app_configuration_check_feature_flags_request, +from ..._utils.model_base import SdkJSONEncoder, _deserialize, _failsafe_deserialize +from ..._utils.serialization import Deserializer, Serializer +from ..._utils.utils import ClientMixinABC +from ..._validation import api_version_validation +from ...operations._operations import ( build_azure_app_configuration_check_key_value_request, build_azure_app_configuration_check_key_values_request, build_azure_app_configuration_check_keys_request, @@ -46,12 +47,8 @@ build_azure_app_configuration_check_snapshot_request, build_azure_app_configuration_check_snapshots_request, build_azure_app_configuration_create_snapshot_request, - build_azure_app_configuration_delete_feature_flag_request, build_azure_app_configuration_delete_key_value_request, build_azure_app_configuration_delete_lock_request, - build_azure_app_configuration_get_feature_flag_request, - build_azure_app_configuration_get_feature_flag_revisions_request, - build_azure_app_configuration_get_feature_flags_request, build_azure_app_configuration_get_key_value_request, build_azure_app_configuration_get_key_values_request, build_azure_app_configuration_get_keys_request, @@ -60,59 +57,119 @@ build_azure_app_configuration_get_revisions_request, build_azure_app_configuration_get_snapshot_request, build_azure_app_configuration_get_snapshots_request, - build_azure_app_configuration_put_feature_flag_request, build_azure_app_configuration_put_key_value_request, build_azure_app_configuration_put_lock_request, build_azure_app_configuration_update_snapshot_request, + build_feature_flag_client_check_feature_flag_request, + build_feature_flag_client_check_feature_flag_revisions_request, + build_feature_flag_client_check_feature_flags_request, + build_feature_flag_client_delete_feature_flag_request, + build_feature_flag_client_get_feature_flag_request, + build_feature_flag_client_get_feature_flag_revisions_request, + build_feature_flag_client_get_feature_flags_request, + build_feature_flag_client_put_feature_flag_request, ) -from ..._utils.model_base import SdkJSONEncoder, _deserialize, _failsafe_deserialize -from ..._utils.utils import ClientMixinABC -from ..._validation import api_version_validation from .._configuration import AzureAppConfigurationClientConfiguration -JSON = MutableMapping[str, Any] T = TypeVar("T") ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, dict[str, Any]], Any]] +JSON = MutableMapping[str, Any] -class _AzureAppConfigurationClientOperationsMixin( # pylint: disable=too-many-public-methods - ClientMixinABC[AsyncPipelineClient[HttpRequest, AsyncHttpResponse], AzureAppConfigurationClientConfiguration] -): +class FeatureFlagClientOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~azure.appconfiguration.aio.AzureAppConfigurationClient`'s + :attr:`feature_flag_client` attribute. + """ + + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client: AsyncPipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") + self._config: AzureAppConfigurationClientConfiguration = ( + input_args.pop(0) if input_args else kwargs.pop("config") + ) + self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") @distributed_trace - def get_keys( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "after", + "accept_datetime", + "select", + "tags", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def get_feature_flags( self, *, name: Optional[str] = None, - after: Optional[str] = None, + label: Optional[str] = None, sync_token: Optional[str] = None, + after: Optional[str] = None, accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> AsyncItemPaged["_models.Key"]: - """Gets a list of keys. + ) -> AsyncItemPaged["_models.FeatureFlag"]: + """Gets a list of feature flags. - Gets a list of keys. + Gets a list of feature flags. - :keyword name: A filter for the name of the returned keys. Default value is None. + :keyword name: A filter used to match feature flag names. Default value is None. :paramtype name: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str :keyword accept_datetime: Requests the server to respond with the state of the resource at the specified time. Default value is None. :paramtype accept_datetime: str - :return: An iterator like instance of Key - :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.Key] + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: An iterator like instance of FeatureFlag + :rtype: + ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.FeatureFlag] :raises ~azure.core.exceptions.HttpResponseError: """ _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[list[_models.Key]] = kwargs.pop("cls", None) + cls: ClsType[list[_models.FeatureFlag]] = kwargs.pop("cls", None) error_map: MutableMapping = { 401: ClientAuthenticationError, @@ -120,16 +177,27 @@ def get_keys( 409: ResourceExistsError, 304: ResourceNotModifiedError, } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) def prepare_request(next_link=None): if not next_link: - _request = build_azure_app_configuration_get_keys_request( + _request = build_feature_flag_client_get_feature_flags_request( name=name, - after=after, + label=label, sync_token=sync_token, + after=after, accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, api_version=self._config.api_version, headers=_headers, params=_params, @@ -169,7 +237,7 @@ def prepare_request(next_link=None): async def extract_data(pipeline_response): deserialized = pipeline_response.http_response.json() list_of_elem = _deserialize( - list[_models.Key], + list[_models.FeatureFlag], deserialized.get("items", []), ) if cls: @@ -180,7 +248,7 @@ async def get_next(next_link=None): _request = prepare_request(next_link) _stream = False - pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs ) response = pipeline_response.http_response @@ -198,31 +266,71 @@ async def get_next(next_link=None): return AsyncItemPaged(get_next, extract_data) @distributed_trace_async - async def check_keys( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "after", + "accept_datetime", + "select", + "tags", + "client_request_id", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def check_feature_flags( self, *, name: Optional[str] = None, - after: Optional[str] = None, + label: Optional[str] = None, sync_token: Optional[str] = None, + after: Optional[str] = None, accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> bool: """Requests the headers and status of the given resource. Requests the headers and status of the given resource. - :keyword name: A filter for the name of the returned keys. Default value is None. + :keyword name: A filter used to match feature flag names. Default value is None. :paramtype name: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str :keyword accept_datetime: Requests the server to respond with the state of the resource at the specified time. Default value is None. :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions :return: bool :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: @@ -233,6 +341,12 @@ async def check_keys( 409: ResourceExistsError, 304: ResourceNotModifiedError, } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) _headers = kwargs.pop("headers", {}) or {} @@ -240,11 +354,16 @@ async def check_keys( cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_keys_request( + _request = build_feature_flag_client_check_feature_flags_request( name=name, - after=after, + label=label, sync_token=sync_token, + after=after, accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, api_version=self._config.api_version, headers=_headers, params=_params, @@ -255,7 +374,7 @@ async def check_keys( _request.url = self._client.format_url(_request.url, **path_format_arguments) _stream = False - pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs ) @@ -271,55 +390,63 @@ async def check_keys( response_headers = {} response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) if cls: return cls(pipeline_response, None, response_headers) # type: ignore return 200 <= response.status_code <= 299 - @distributed_trace - def get_key_values( + @distributed_trace_async + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "select", + "sync_token", + "accept_datetime", + "client_request_id", + "tags", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def get_feature_flag( self, + name: str, *, - key: Optional[str] = None, label: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, sync_token: Optional[str] = None, - after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - snapshot: Optional[str] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> AsyncItemPaged["_models.KeyValue"]: - """Gets a list of key-values. + ) -> _models.FeatureFlag: + """Gets a single feature flag. - Gets a list of key-values. + Gets a single feature flag. - :keyword key: A filter used to match keys. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype key: str - :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. + :param name: The name of the feature flag. Required. + :type name: str + :keyword label: The label of the feature flag to retrieve. Default value is None. :paramtype label: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str :keyword accept_datetime: Requests the server to respond with the state of the resource at the specified time. Default value is None. :paramtype accept_datetime: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] - :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of - the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. - :paramtype snapshot: str :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -329,16 +456,10 @@ def get_key_values( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: An iterator like instance of KeyValue - :rtype: - ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.KeyValue] + :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag :raises ~azure.core.exceptions.HttpResponseError: """ - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[list[_models.KeyValue]] = kwargs.pop("cls", None) - error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, @@ -353,98 +474,93 @@ def get_key_values( error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) - def prepare_request(next_link=None): - if not next_link: + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} - _request = build_azure_app_configuration_get_key_values_request( - key=key, - label=label, - sync_token=sync_token, - after=after, - accept_datetime=accept_datetime, - select=select, - snapshot=snapshot, - tags=tags, - etag=etag, - match_condition=match_condition, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url( - "self._config.endpoint", self._config.endpoint, "str", skip_quote=True - ), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - else: - # make call to next link with the client's api-version - _parsed_next_link = urllib.parse.urlparse(next_link) - _next_request_params = case_insensitive_dict( - { - key: [urllib.parse.quote(v) for v in value] - for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() - } - ) - _next_request_params["api-version"] = self._config.api_version - _request = HttpRequest( - "GET", - urllib.parse.urljoin(next_link, _parsed_next_link.path), - headers=_headers, - params=_next_request_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url( - "self._config.endpoint", self._config.endpoint, "str", skip_quote=True - ), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) - return _request + _request = build_feature_flag_client_get_feature_flag_request( + name=name, + label=label, + select=select, + sync_token=sync_token, + accept_datetime=accept_datetime, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) - async def extract_data(pipeline_response): - deserialized = pipeline_response.http_response.json() - list_of_elem = _deserialize( - list[_models.KeyValue], - deserialized.get("items", []), - ) - if cls: - list_of_elem = cls(list_of_elem) # type: ignore - return deserialized.get("@nextLink") or None, AsyncList(list_of_elem) + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) - async def get_next(next_link=None): - _request = prepare_request(next_link) + response = pipeline_response.http_response - _stream = False - pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access - _request, stream=_stream, **kwargs + if response.status_code not in [200]: + if _stream: + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, ) - response = pipeline_response.http_response + raise HttpResponseError(response=response, model=error) - if response.status_code not in [200]: - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.Error, - response, - ) - raise HttpResponseError(response=response, model=error) + response_headers = {} + response_headers["x-ms-request-id"] = self._deserialize("str", response.headers.get("x-ms-request-id")) + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) - return pipeline_response + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.FeatureFlag, response.json()) - return AsyncItemPaged(get_next, extract_data) + if cls: + return cls(pipeline_response, deserialized, response_headers) # type: ignore + + return deserialized # type: ignore @distributed_trace_async - async def check_key_values( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "accept_datetime", + "select", + "tags", + "client_request_id", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def check_feature_flag( self, + name: str, *, - key: Optional[str] = None, label: Optional[str] = None, sync_token: Optional[str] = None, - after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - snapshot: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, @@ -454,30 +570,20 @@ async def check_key_values( Requests the headers and status of the given resource. - :keyword key: A filter used to match keys. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype key: str - :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. + :param name: The name of the feature flag to retrieve. Required. + :type name: str + :keyword label: The label of the feature flag to retrieve. Default value is None. :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str :keyword accept_datetime: Requests the server to respond with the state of the resource at the specified time. Default value is None. :paramtype accept_datetime: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] - :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of - the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. - :paramtype snapshot: str + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -510,14 +616,12 @@ async def check_key_values( cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_key_values_request( - key=key, + _request = build_feature_flag_client_check_feature_flag_request( + name=name, label=label, sync_token=sync_token, - after=after, accept_datetime=accept_datetime, select=select, - snapshot=snapshot, tags=tags, etag=etag, match_condition=match_condition, @@ -531,7 +635,7 @@ async def check_key_values( _request.url = self._client.format_url(_request.url, **path_format_arguments) _stream = False - pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs ) @@ -555,51 +659,53 @@ async def check_key_values( @distributed_trace_async @api_version_validation( - params_added_on={"2024-09-01": ["tags"]}, - api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "content_type", + "name", + "label", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], ) - async def get_key_value( + async def put_feature_flag( self, - key: str, + name: str, + entity: Optional[_models.FeatureFlag] = None, *, label: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, sync_token: Optional[str] = None, - accept_datetime: Optional[str] = None, - tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.KeyValue: - """Gets a single key-value. + ) -> _models.FeatureFlag: + """Creates a feature flag. - Gets a single key-value. + Creates a feature flag. - :param key: The key of the key-value. Required. - :type key: str - :keyword label: The label of the key-value to retrieve. Default value is None. + :param name: The name of the feature flag to create. Required. + :type name: str + :param entity: The feature flag to create. Default value is None. + :type entity: ~azure.appconfiguration._generated.models.FeatureFlag + :keyword label: The label of the feature flag to create. Default value is None. :paramtype label: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str - :keyword accept_datetime: Requests the server to respond with the state of the resource at the - specified - time. Default value is None. - :paramtype accept_datetime: str - :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype tags: list[str] :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is None. :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: KeyValue. The KeyValue is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.KeyValue + :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -616,21 +722,32 @@ async def get_key_value( error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) - _headers = kwargs.pop("headers", {}) or {} + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = kwargs.pop("params", {}) or {} - cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) + content_type: Optional[str] = kwargs.pop( + "content_type", + _headers.pop( + "Content-Type", 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8' + ), + ) + content_type = content_type if entity else None + cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_get_key_value_request( - key=key, + if entity is not None: + _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + else: + _content = None + + _request = build_feature_flag_client_put_feature_flag_request( + name=name, label=label, - select=select, sync_token=sync_token, - accept_datetime=accept_datetime, - tags=tags, etag=etag, match_condition=match_condition, + content_type=content_type, api_version=self._config.api_version, + content=_content, headers=_headers, params=_params, ) @@ -641,7 +758,7 @@ async def get_key_value( _decompress = kwargs.pop("decompress", True) _stream = kwargs.pop("stream", False) - pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs ) @@ -661,7 +778,6 @@ async def get_key_value( raise HttpResponseError(response=response, model=error) response_headers = {} - response_headers["x-ms-request-id"] = self._deserialize("str", response.headers.get("x-ms-request-id")) response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) @@ -669,75 +785,47 @@ async def get_key_value( if _stream: deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.KeyValue, response.json()) + deserialized = _deserialize(_models.FeatureFlag, response.json()) if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore return deserialized # type: ignore - @overload - async def _put_key_value( + @distributed_trace_async + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def delete_feature_flag( self, - key: str, - entity: Optional[_models.KeyValue] = None, + name: str, *, - content_type: str, label: Optional[str] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.KeyValue: ... - @overload - async def _put_key_value( - self, - key: str, - entity: Optional[JSON] = None, - *, - content_type: str, - label: Optional[str] = None, - sync_token: Optional[str] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> _models.KeyValue: ... - @overload - async def _put_key_value( - self, - key: str, - entity: Optional[IO[bytes]] = None, - *, - content_type: str, - label: Optional[str] = None, - sync_token: Optional[str] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> _models.KeyValue: ... - - @distributed_trace_async - async def _put_key_value( - self, - key: str, - entity: Optional[Union[_models.KeyValue, JSON, IO[bytes]]] = None, - *, - label: Optional[str] = None, - sync_token: Optional[str] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> _models.KeyValue: - """Creates a key-value. + ) -> Optional[_models.FeatureFlag]: + """Deletes a feature flag. - Creates a key-value. + Deletes a feature flag. - :param key: The key of the key-value to create. Required. - :type key: str - :param entity: The key-value to create. Is one of the following types: KeyValue, JSON, - IO[bytes] Default value is None. - :type entity: ~azure.appconfiguration._generated.models.KeyValue or JSON or IO[bytes] - :keyword label: The label of the key-value to create. Default value is None. + :param name: The name of the feature flag to delete. Required. + :type name: str + :keyword label: The label of the feature flag to delete. Default value is None. :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. @@ -747,8 +835,8 @@ async def _put_key_value( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: KeyValue. The KeyValue is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.KeyValue + :return: FeatureFlag or None. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag or None :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -765,32 +853,18 @@ async def _put_key_value( error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - content_type = content_type if entity else None - cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) - - content_type = content_type or "application/json" if entity else None - _content = None - if isinstance(entity, (IOBase, bytes)): - _content = entity - else: - if entity is not None: - _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - else: - _content = None + cls: ClsType[Optional[_models.FeatureFlag]] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_put_key_value_request( - key=key, + _request = build_feature_flag_client_delete_feature_flag_request( + name=name, label=label, sync_token=sync_token, etag=etag, match_condition=match_condition, - content_type=content_type, api_version=self._config.api_version, - content=_content, headers=_headers, params=_params, ) @@ -801,13 +875,13 @@ async def _put_key_value( _decompress = kwargs.pop("decompress", True) _stream = kwargs.pop("stream", False) - pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs ) response = pipeline_response.http_response - if response.status_code not in [200]: + if response.status_code not in [200, 204]: if _stream: try: await response.read() # Load the body in memory and close the socket @@ -820,40 +894,79 @@ async def _put_key_value( ) raise HttpResponseError(response=response, model=error) + deserialized = None response_headers = {} - response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) - response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) - response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) + if response.status_code == 200: + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.KeyValue, response.json()) + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.FeatureFlag, response.json()) + + if response.status_code == 204: + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore return deserialized # type: ignore - @distributed_trace_async - async def delete_key_value( + @distributed_trace + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "after", + "select", + "tags", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def get_feature_flag_revisions( self, - key: str, *, + name: Optional[str] = None, label: Optional[str] = None, + after: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> Optional[_models.KeyValue]: - """Deletes a key-value. + ) -> AsyncItemPaged["_models.FeatureFlag"]: + """Gets a list of feature flag revisions. - Deletes a key-value. + Gets a list of feature flag revisions. - :param key: The key of the key-value to delete. Required. - :type key: str - :keyword label: The label of the key-value to delete. Default value is None. + :keyword name: A filter used to match names. Default value is None. + :paramtype name: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. :paramtype label: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. + :paramtype tags: list[str] :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str @@ -862,10 +975,16 @@ async def delete_key_value( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: KeyValue or None. The KeyValue is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.KeyValue or None + :return: An iterator like instance of FeatureFlag + :rtype: + ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.FeatureFlag] :raises ~azure.core.exceptions.HttpResponseError: """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[list[_models.FeatureFlag]] = kwargs.pop("cls", None) + error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, @@ -880,17 +999,146 @@ async def delete_key_value( error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) + def prepare_request(next_link=None): + if not next_link: + + _request = build_feature_flag_client_get_feature_flag_revisions_request( + name=name, + label=label, + after=after, + select=select, + tags=tags, + sync_token=sync_token, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + else: + # make call to next link with the client's api-version + _parsed_next_link = urllib.parse.urlparse(next_link) + _next_request_params = case_insensitive_dict( + { + key: [urllib.parse.quote(v) for v in value] + for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() + } + ) + _next_request_params["api-version"] = self._config.api_version + _request = HttpRequest( + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + return _request + + async def extract_data(pipeline_response): + deserialized = pipeline_response.http_response.json() + list_of_elem = _deserialize( + list[_models.FeatureFlag], + deserialized.get("items", []), + ) + if cls: + list_of_elem = cls(list_of_elem) # type: ignore + return deserialized.get("@nextLink") or None, AsyncList(list_of_elem) + + async def get_next(next_link=None): + _request = prepare_request(next_link) + + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + return pipeline_response + + return AsyncItemPaged(get_next, extract_data) + + @distributed_trace_async + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": ["api_version", "name", "label", "after", "select", "tags", "client_request_id"] + }, + api_versions_list=["2026-05-01-preview"], + ) + async def check_feature_flag_revisions( + self, + *, + name: Optional[str] = None, + label: Optional[str] = None, + after: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + **kwargs: Any + ) -> bool: + """Requests the headers and status of the given resource. + + Requests the headers and status of the given resource. + + :keyword name: A filter used to match names. Default value is None. + :paramtype name: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. + :paramtype label: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. + :paramtype tags: list[str] + :return: bool + :rtype: bool + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[Optional[_models.KeyValue]] = kwargs.pop("cls", None) + cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_delete_key_value_request( - key=key, + _request = build_feature_flag_client_check_feature_flag_revisions_request( + name=name, label=label, - sync_token=sync_token, - etag=etag, - match_condition=match_condition, + after=after, + select=select, + tags=tags, api_version=self._config.api_version, headers=_headers, params=_params, @@ -900,20 +1148,14 @@ async def delete_key_value( } _request.url = self._client.format_url(_request.url, **path_format_arguments) - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) - pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs ) response = pipeline_response.http_response - if response.status_code not in [200, 204]: - if _stream: - try: - await response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass + if response.status_code not in [200]: map_error(status_code=response.status_code, response=response, error_map=error_map) error = _failsafe_deserialize( _models.Error, @@ -921,52 +1163,156 @@ async def delete_key_value( ) raise HttpResponseError(response=response, model=error) - deserialized = None response_headers = {} - if response.status_code == 200: - response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) - response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) - response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + + if cls: + return cls(pipeline_response, None, response_headers) # type: ignore + return 200 <= response.status_code <= 299 + + +class _AzureAppConfigurationClientOperationsMixin( # pylint: disable=too-many-public-methods + ClientMixinABC[AsyncPipelineClient[HttpRequest, AsyncHttpResponse], AzureAppConfigurationClientConfiguration] +): + + @distributed_trace + def get_keys( + self, + *, + name: Optional[str] = None, + after: Optional[str] = None, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + **kwargs: Any + ) -> AsyncItemPaged["_models.Key"]: + """Gets a list of keys. + + Gets a list of keys. + + :keyword name: A filter for the name of the returned keys. Default value is None. + :paramtype name: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :return: An iterator like instance of Key + :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.Key] + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[list[_models.Key]] = kwargs.pop("cls", None) + + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + def prepare_request(next_link=None): + if not next_link: + + _request = build_azure_app_configuration_get_keys_request( + name=name, + after=after, + sync_token=sync_token, + accept_datetime=accept_datetime, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.KeyValue, response.json()) + # make call to next link with the client's api-version + _parsed_next_link = urllib.parse.urlparse(next_link) + _next_request_params = case_insensitive_dict( + { + key: [urllib.parse.quote(v) for v in value] + for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() + } + ) + _next_request_params["api-version"] = self._config.api_version + _request = HttpRequest( + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) - if response.status_code == 204: - response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + return _request - if cls: - return cls(pipeline_response, deserialized, response_headers) # type: ignore + async def extract_data(pipeline_response): + deserialized = pipeline_response.http_response.json() + list_of_elem = _deserialize( + list[_models.Key], + deserialized.get("items", []), + ) + if cls: + list_of_elem = cls(list_of_elem) # type: ignore + return deserialized.get("@nextLink") or None, AsyncList(list_of_elem) - return deserialized # type: ignore + async def get_next(next_link=None): + _request = prepare_request(next_link) + + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + return pipeline_response + + return AsyncItemPaged(get_next, extract_data) @distributed_trace_async - @api_version_validation( - params_added_on={"2024-09-01": ["tags"]}, - api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], - ) - async def check_key_value( + async def check_keys( self, - key: str, *, - label: Optional[str] = None, + name: Optional[str] = None, + after: Optional[str] = None, sync_token: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - tags: Optional[list[str]] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> bool: """Requests the headers and status of the given resource. Requests the headers and status of the given resource. - :param key: The key of the key-value to retrieve. Required. - :type key: str - :keyword label: The label of the key-value to retrieve. Default value is None. - :paramtype label: str + :keyword name: A filter for the name of the returned keys. Default value is None. + :paramtype name: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str @@ -974,18 +1320,6 @@ async def check_key_value( specified time. Default value is None. :paramtype accept_datetime: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] - :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype tags: list[str] - :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is - None. - :paramtype etag: str - :keyword match_condition: The match condition to use upon the etag. Default value is None. - :paramtype match_condition: ~azure.core.MatchConditions :return: bool :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: @@ -996,12 +1330,6 @@ async def check_key_value( 409: ResourceExistsError, 304: ResourceNotModifiedError, } - if match_condition == MatchConditions.IfNotModified: - error_map[412] = ResourceModifiedError - elif match_condition == MatchConditions.IfPresent: - error_map[412] = ResourceNotFoundError - elif match_condition == MatchConditions.IfMissing: - error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) _headers = kwargs.pop("headers", {}) or {} @@ -1009,15 +1337,11 @@ async def check_key_value( cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_key_value_request( - key=key, - label=label, + _request = build_azure_app_configuration_check_keys_request( + name=name, + after=after, sync_token=sync_token, accept_datetime=accept_datetime, - select=select, - tags=tags, - etag=etag, - match_condition=match_condition, api_version=self._config.api_version, headers=_headers, params=_params, @@ -1044,52 +1368,35 @@ async def check_key_value( response_headers = {} response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) - response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) if cls: return cls(pipeline_response, None, response_headers) # type: ignore return 200 <= response.status_code <= 299 @distributed_trace - @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "sync_token", - "after", - "accept_datetime", - "select", - "tags", - "accept", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], - ) - def get_feature_flags( + def get_key_values( self, *, - name: Optional[str] = None, + key: Optional[str] = None, label: Optional[str] = None, sync_token: Optional[str] = None, after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> AsyncItemPaged["_models.FeatureFlag"]: - """Gets a list of feature flags. + ) -> AsyncItemPaged["_models.KeyValue"]: + """Gets a list of key-values. - Gets a list of feature flags. + Gets a list of key-values. - :keyword name: A filter used to match feature flag names. Default value is None. - :paramtype name: str + :keyword key: A filter used to match keys. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype key: str :keyword label: A filter used to match labels. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -1106,7 +1413,10 @@ def get_feature_flags( :paramtype accept_datetime: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of + the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. + :paramtype snapshot: str :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -1116,15 +1426,15 @@ def get_feature_flags( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: An iterator like instance of FeatureFlag + :return: An iterator like instance of KeyValue :rtype: - ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.FeatureFlag] + ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.KeyValue] :raises ~azure.core.exceptions.HttpResponseError: """ _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[list[_models.FeatureFlag]] = kwargs.pop("cls", None) + cls: ClsType[list[_models.KeyValue]] = kwargs.pop("cls", None) error_map: MutableMapping = { 401: ClientAuthenticationError, @@ -1143,13 +1453,14 @@ def get_feature_flags( def prepare_request(next_link=None): if not next_link: - _request = build_azure_app_configuration_get_feature_flags_request( - name=name, + _request = build_azure_app_configuration_get_key_values_request( + key=key, label=label, sync_token=sync_token, after=after, accept_datetime=accept_datetime, select=select, + snapshot=snapshot, tags=tags, etag=etag, match_condition=match_condition, @@ -1192,7 +1503,7 @@ def prepare_request(next_link=None): async def extract_data(pipeline_response): deserialized = pipeline_response.http_response.json() list_of_elem = _deserialize( - list[_models.FeatureFlag], + list[_models.KeyValue], deserialized.get("items", []), ) if cls: @@ -1212,196 +1523,58 @@ async def get_next(next_link=None): map_error(status_code=response.status_code, response=response, error_map=error_map) error = _failsafe_deserialize( _models.Error, - response, - ) - raise HttpResponseError(response=response, model=error) - - return pipeline_response - - return AsyncItemPaged(get_next, extract_data) - - @distributed_trace_async - @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "sync_token", - "after", - "accept_datetime", - "select", - "tags", - "client_request_id", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], - ) - async def check_feature_flags( - self, - *, - name: Optional[str] = None, - label: Optional[str] = None, - sync_token: Optional[str] = None, - after: Optional[str] = None, - accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, - tags: Optional[list[str]] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> bool: - """Requests the headers and status of the given resource. - - Requests the headers and status of the given resource. - - :keyword name: A filter used to match feature flag names. Default value is None. - :paramtype name: str - :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype label: str - :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is - None. - :paramtype sync_token: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str - :keyword accept_datetime: Requests the server to respond with the state of the resource at the - specified - time. Default value is None. - :paramtype accept_datetime: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] - :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype tags: list[str] - :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is - None. - :paramtype etag: str - :keyword match_condition: The match condition to use upon the etag. Default value is None. - :paramtype match_condition: ~azure.core.MatchConditions - :return: bool - :rtype: bool - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - if match_condition == MatchConditions.IfNotModified: - error_map[412] = ResourceModifiedError - elif match_condition == MatchConditions.IfPresent: - error_map[412] = ResourceNotFoundError - elif match_condition == MatchConditions.IfMissing: - error_map[412] = ResourceExistsError - error_map.update(kwargs.pop("error_map", {}) or {}) - - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[None] = kwargs.pop("cls", None) - - _request = build_azure_app_configuration_check_feature_flags_request( - name=name, - label=label, - sync_token=sync_token, - after=after, - accept_datetime=accept_datetime, - select=select, - tags=tags, - etag=etag, - match_condition=match_condition, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _stream = False - pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - - response = pipeline_response.http_response - - if response.status_code not in [200]: - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.Error, - response, - ) - raise HttpResponseError(response=response, model=error) - - response_headers = {} - response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) - response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) - - if cls: - return cls(pipeline_response, None, response_headers) # type: ignore - return 200 <= response.status_code <= 299 - - @distributed_trace_async - @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "select", - "sync_token", - "accept_datetime", - "client_request_id", - "tags", - "accept", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], - ) - async def get_feature_flag( + response, + ) + raise HttpResponseError(response=response, model=error) + + return pipeline_response + + return AsyncItemPaged(get_next, extract_data) + + @distributed_trace_async + async def check_key_values( self, - name: str, *, + key: Optional[str] = None, label: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, sync_token: Optional[str] = None, + after: Optional[str] = None, accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.FeatureFlag: - """Gets a single feature flag. + ) -> bool: + """Requests the headers and status of the given resource. - Gets a single feature flag. + Requests the headers and status of the given resource. - :param name: The name of the feature flag. Required. - :type name: str - :keyword label: The label of the feature flag to retrieve. Default value is None. + :keyword key: A filter used to match keys. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype key: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. :paramtype label: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str :keyword accept_datetime: Requests the server to respond with the state of the resource at the specified time. Default value is None. :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of + the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. + :paramtype snapshot: str :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -1411,8 +1584,8 @@ async def get_feature_flag( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.FeatureFlag + :return: bool + :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -1432,14 +1605,16 @@ async def get_feature_flag( _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) + cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_get_feature_flag_request( - name=name, + _request = build_azure_app_configuration_check_key_values_request( + key=key, label=label, - select=select, sync_token=sync_token, + after=after, accept_datetime=accept_datetime, + select=select, + snapshot=snapshot, tags=tags, etag=etag, match_condition=match_condition, @@ -1452,8 +1627,7 @@ async def get_feature_flag( } _request.url = self._client.format_url(_request.url, **path_format_arguments) - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) + _stream = False pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access _request, stream=_stream, **kwargs ) @@ -1461,11 +1635,6 @@ async def get_feature_flag( response = pipeline_response.http_response if response.status_code not in [200]: - if _stream: - try: - await response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass map_error(status_code=response.status_code, response=response, error_map=error_map) error = _failsafe_deserialize( _models.Error, @@ -1474,61 +1643,42 @@ async def get_feature_flag( raise HttpResponseError(response=response, model=error) response_headers = {} - response_headers["x-ms-request-id"] = self._deserialize("str", response.headers.get("x-ms-request-id")) response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) - response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) - - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.FeatureFlag, response.json()) if cls: - return cls(pipeline_response, deserialized, response_headers) # type: ignore - - return deserialized # type: ignore + return cls(pipeline_response, None, response_headers) # type: ignore + return 200 <= response.status_code <= 299 @distributed_trace_async @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "sync_token", - "accept_datetime", - "select", - "tags", - "client_request_id", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], + params_added_on={"2024-09-01": ["tags"]}, + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], ) - async def check_feature_flag( + async def get_key_value( self, - name: str, + key: str, *, label: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, sync_token: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> bool: - """Requests the headers and status of the given resource. + ) -> _models.KeyValue: + """Gets a single key-value. - Requests the headers and status of the given resource. + Gets a single key-value. - :param name: The name of the feature flag to retrieve. Required. - :type name: str - :keyword label: The label of the feature flag to retrieve. Default value is None. + :param key: The key of the key-value. Required. + :type key: str + :keyword label: The label of the key-value to retrieve. Default value is None. :paramtype label: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str @@ -1536,9 +1686,6 @@ async def check_feature_flag( specified time. Default value is None. :paramtype accept_datetime: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -1548,8 +1695,8 @@ async def check_feature_flag( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: bool - :rtype: bool + :return: KeyValue. The KeyValue is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.KeyValue :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -1569,14 +1716,14 @@ async def check_feature_flag( _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[None] = kwargs.pop("cls", None) + cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_feature_flag_request( - name=name, + _request = build_azure_app_configuration_get_key_value_request( + key=key, label=label, + select=select, sync_token=sync_token, accept_datetime=accept_datetime, - select=select, tags=tags, etag=etag, match_condition=match_condition, @@ -1589,7 +1736,8 @@ async def check_feature_flag( } _request.url = self._client.format_url(_request.url, **path_format_arguments) - _stream = False + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access _request, stream=_stream, **kwargs ) @@ -1597,6 +1745,11 @@ async def check_feature_flag( response = pipeline_response.http_response if response.status_code not in [200]: + if _stream: + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass map_error(status_code=response.status_code, response=response, error_map=error_map) error = _failsafe_deserialize( _models.Error, @@ -1605,51 +1758,83 @@ async def check_feature_flag( raise HttpResponseError(response=response, model=error) response_headers = {} + response_headers["x-ms-request-id"] = self._deserialize("str", response.headers.get("x-ms-request-id")) response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.KeyValue, response.json()) if cls: - return cls(pipeline_response, None, response_headers) # type: ignore - return 200 <= response.status_code <= 299 + return cls(pipeline_response, deserialized, response_headers) # type: ignore + + return deserialized # type: ignore + + @overload + async def _put_key_value( + self, + key: str, + entity: Optional[_models.KeyValue] = None, + *, + content_type: str, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: ... + @overload + async def _put_key_value( + self, + key: str, + entity: Optional[JSON] = None, + *, + content_type: str, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: ... + @overload + async def _put_key_value( + self, + key: str, + entity: Optional[IO[bytes]] = None, + *, + content_type: str, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: ... @distributed_trace_async - @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "content_type", - "name", - "label", - "sync_token", - "client_request_id", - "accept", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], - ) - async def put_feature_flag( + async def _put_key_value( self, - name: str, - entity: Optional[_models.FeatureFlag] = None, + key: str, + entity: Optional[Union[_models.KeyValue, JSON, IO[bytes]]] = None, *, label: Optional[str] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.FeatureFlag: - """Creates a feature flag. + ) -> _models.KeyValue: + """Creates a key-value. - Creates a feature flag. + Creates a key-value. - :param name: The name of the feature flag to create. Required. - :type name: str - :param entity: The feature flag to create. Default value is None. - :type entity: ~azure.appconfiguration._generated.models.FeatureFlag - :keyword label: The label of the feature flag to create. Default value is None. + :param key: The key of the key-value to create. Required. + :type key: str + :param entity: The key-value to create. Is one of the following types: KeyValue, JSON, + IO[bytes] Default value is None. + :type entity: ~azure.appconfiguration._generated.models.KeyValue or JSON or IO[bytes] + :keyword label: The label of the key-value to create. Default value is None. :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. @@ -1659,8 +1844,8 @@ async def put_feature_flag( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.FeatureFlag + :return: KeyValue. The KeyValue is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.KeyValue :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -1680,22 +1865,22 @@ async def put_feature_flag( _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = kwargs.pop("params", {}) or {} - content_type: Optional[str] = kwargs.pop( - "content_type", - _headers.pop( - "Content-Type", 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8' - ), - ) + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) content_type = content_type if entity else None - cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) + cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) - if entity is not None: - _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + content_type = content_type or "application/json" if entity else None + _content = None + if isinstance(entity, (IOBase, bytes)): + _content = entity else: - _content = None + if entity is not None: + _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + else: + _content = None - _request = build_azure_app_configuration_put_feature_flag_request( - name=name, + _request = build_azure_app_configuration_put_key_value_request( + key=key, label=label, sync_token=sync_token, etag=etag, @@ -1740,7 +1925,7 @@ async def put_feature_flag( if _stream: deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.FeatureFlag, response.json()) + deserialized = _deserialize(_models.KeyValue, response.json()) if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore @@ -1748,39 +1933,23 @@ async def put_feature_flag( return deserialized # type: ignore @distributed_trace_async - @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "sync_token", - "client_request_id", - "accept", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], - ) - async def delete_feature_flag( + async def delete_key_value( self, - name: str, + key: str, *, label: Optional[str] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> Optional[_models.FeatureFlag]: - """Deletes a feature flag. + ) -> Optional[_models.KeyValue]: + """Deletes a key-value. - Deletes a feature flag. + Deletes a key-value. - :param name: The name of the feature flag to delete. Required. - :type name: str - :keyword label: The label of the feature flag to delete. Default value is None. + :param key: The key of the key-value to delete. Required. + :type key: str + :keyword label: The label of the key-value to delete. Default value is None. :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. @@ -1790,8 +1959,8 @@ async def delete_feature_flag( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: FeatureFlag or None. The FeatureFlag is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.FeatureFlag or None + :return: KeyValue or None. The KeyValue is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.KeyValue or None :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -1811,10 +1980,10 @@ async def delete_feature_flag( _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[Optional[_models.FeatureFlag]] = kwargs.pop("cls", None) + cls: ClsType[Optional[_models.KeyValue]] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_delete_feature_flag_request( - name=name, + _request = build_azure_app_configuration_delete_key_value_request( + key=key, label=label, sync_token=sync_token, etag=etag, @@ -1859,7 +2028,7 @@ async def delete_feature_flag( if _stream: deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.FeatureFlag, response.json()) + deserialized = _deserialize(_models.KeyValue, response.json()) if response.status_code == 204: response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) @@ -1869,77 +2038,55 @@ async def delete_feature_flag( return deserialized # type: ignore - @distributed_trace + @distributed_trace_async @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "after", - "select", - "tags", - "sync_token", - "client_request_id", - "accept", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], + params_added_on={"2024-09-01": ["tags"]}, + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], ) - def get_feature_flag_revisions( + async def check_key_value( self, + key: str, *, - name: Optional[str] = None, label: Optional[str] = None, - after: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, - tags: Optional[list[str]] = None, sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> AsyncItemPaged["_models.FeatureFlag"]: - """Gets a list of feature flag revisions. + ) -> bool: + """Requests the headers and status of the given resource. - Gets a list of feature flag revisions. + Requests the headers and status of the given resource. - :keyword name: A filter used to match names. Default value is None. - :paramtype name: str - :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/restapirevisions - `_. Default value is None. + :param key: The key of the key-value to retrieve. Required. + :type key: str + :keyword label: The label of the key-value to retrieve. Default value is None. :paramtype label: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/restapirevisions - `_. Default value is None. + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. :paramtype tags: list[str] - :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is - None. - :paramtype sync_token: str :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is None. :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: An iterator like instance of FeatureFlag - :rtype: - ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration._generated.models.FeatureFlag] + :return: bool + :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: """ - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[list[_models.FeatureFlag]] = kwargs.pop("cls", None) - error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, @@ -1954,146 +2101,20 @@ def get_feature_flag_revisions( error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) - def prepare_request(next_link=None): - if not next_link: - - _request = build_azure_app_configuration_get_feature_flag_revisions_request( - name=name, - label=label, - after=after, - select=select, - tags=tags, - sync_token=sync_token, - etag=etag, - match_condition=match_condition, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url( - "self._config.endpoint", self._config.endpoint, "str", skip_quote=True - ), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - else: - # make call to next link with the client's api-version - _parsed_next_link = urllib.parse.urlparse(next_link) - _next_request_params = case_insensitive_dict( - { - key: [urllib.parse.quote(v) for v in value] - for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() - } - ) - _next_request_params["api-version"] = self._config.api_version - _request = HttpRequest( - "GET", - urllib.parse.urljoin(next_link, _parsed_next_link.path), - headers=_headers, - params=_next_request_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url( - "self._config.endpoint", self._config.endpoint, "str", skip_quote=True - ), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - return _request - - async def extract_data(pipeline_response): - deserialized = pipeline_response.http_response.json() - list_of_elem = _deserialize( - list[_models.FeatureFlag], - deserialized.get("items", []), - ) - if cls: - list_of_elem = cls(list_of_elem) # type: ignore - return deserialized.get("@nextLink") or None, AsyncList(list_of_elem) - - async def get_next(next_link=None): - _request = prepare_request(next_link) - - _stream = False - pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - response = pipeline_response.http_response - - if response.status_code not in [200]: - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.Error, - response, - ) - raise HttpResponseError(response=response, model=error) - - return pipeline_response - - return AsyncItemPaged(get_next, extract_data) - - @distributed_trace_async - @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": ["api_version", "name", "label", "after", "select", "tags", "client_request_id"] - }, - api_versions_list=["2026-05-01-preview"], - ) - async def check_feature_flag_revisions( - self, - *, - name: Optional[str] = None, - label: Optional[str] = None, - after: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, - tags: Optional[list[str]] = None, - **kwargs: Any - ) -> bool: - """Requests the headers and status of the given resource. - - Requests the headers and status of the given resource. - - :keyword name: A filter used to match names. Default value is None. - :paramtype name: str - :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/restapirevisions - `_. Default value is None. - :paramtype label: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] - :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/restapirevisions - `_. Default value is None. - :paramtype tags: list[str] - :return: bool - :rtype: bool - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) - _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_feature_flag_revisions_request( - name=name, + _request = build_azure_app_configuration_check_key_value_request( + key=key, label=label, - after=after, + sync_token=sync_token, + accept_datetime=accept_datetime, select=select, tags=tags, + etag=etag, + match_condition=match_condition, api_version=self._config.api_version, headers=_headers, params=_params, diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_patch.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_patch.py new file mode 100644 index 000000000000..2a91a5fbce41 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_patch.py @@ -0,0 +1,360 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Customize generated code here. + +Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize +""" + +import json +import urllib.parse +from typing import Any, List, Optional, Union, MutableMapping, Type +from azure.core import MatchConditions +from azure.core.exceptions import ( + ClientAuthenticationError, + HttpResponseError, + ResourceExistsError, + ResourceNotFoundError, + ResourceNotModifiedError, + ResourceModifiedError, + map_error, +) +from azure.core.rest import HttpRequest +from azure.core.tracing.decorator_async import distributed_trace_async +from azure.core.utils import case_insensitive_dict +from ._operations import ( + _AzureAppConfigurationClientOperationsMixin as AzureAppConfigClientOpGenerated, + ClsType, + build_azure_app_configuration_get_key_values_request, + build_azure_app_configuration_check_key_values_request, +) +from ...operations._operations import prep_if_match, prep_if_none_match +from ... import models as _models +from ..._utils.model_base import _deserialize +from ..._utils.serialization import Serializer + +_SERIALIZER = Serializer() +_SERIALIZER.client_side_validation = False + + +class _AzureAppConfigurationClientOperationsMixin(AzureAppConfigClientOpGenerated): + def _build_kv_error_map( + self, + match_condition: Optional[MatchConditions], + ) -> MutableMapping[int, Type[HttpResponseError]]: + """Build an error map for key-value operations.""" + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 401: ClientAuthenticationError, + 403: HttpResponseError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + return error_map + + def _prepare_continuation_request( + self, + http_method: str, + continuation_token: str, + *, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + headers: Optional[dict] = None, + ) -> HttpRequest: + """Build a continuation request from a pagination token.""" + _headers = headers or {} + + _parsed_next_link = urllib.parse.urlparse(continuation_token) + _next_request_params = case_insensitive_dict( + { + key: [urllib.parse.quote(v) for v in value] + for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() + } + ) + _next_request_params["api-version"] = self._config.api_version + + _next_headers = dict(_headers) + accept = _headers.pop("Accept", None) + if sync_token is not None: + _next_headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _next_headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + if accept is not None: + _next_headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _next_headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _next_headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + _request = HttpRequest( + http_method, + urllib.parse.urljoin(continuation_token, _parsed_next_link.path), + params=_next_request_params, + headers=_next_headers, + ) + return self._format_request(_request) + + def _format_request(self, _request: HttpRequest) -> HttpRequest: + """Apply endpoint formatting to a request URL.""" + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + return _request + + @distributed_trace_async + async def get_key_values_in_one_page( + self, + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[List[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, + tags: Optional[List[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + continuation_token: Optional[str] = None, + **kwargs: Any, + ) -> dict: + """Gets a list of key-values in one page. + + Gets a list of key-values in one page. + + :keyword key: A filter used to match keys. Syntax reference: + https://aka.ms/azconfig/docs/keyvaluefiltering. Default value is None. + :paramtype key: str + :keyword label: A filter used to match labels. Syntax reference: + https://aka.ms/azconfig/docs/keyvaluefiltering. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.KeyValueFields] + :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of + the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. + :paramtype snapshot: str + :keyword tags: A filter used to query by tags. Syntax reference: + https://aka.ms/azconfig/docs/keyvaluefiltering. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :param str continuation_token: An opaque continuation token. + :return: A dict containing ``items`` (list of key-values) and ``@nextLink`` (pagination URL or None). + :rtype: dict + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[List[_models.KeyValue]] = kwargs.pop("cls", None) + + error_map = self._build_kv_error_map(match_condition) + error_map.update(kwargs.pop("error_map", {}) or {}) + + if continuation_token: + _request = self._prepare_continuation_request( + "GET", + continuation_token, + sync_token=sync_token, + accept_datetime=accept_datetime, + etag=etag, + match_condition=match_condition, + headers=_headers, + ) + else: + _request = build_azure_app_configuration_get_key_values_request( + key=key, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + snapshot=snapshot, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + _request = self._format_request(_request) + + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=False, **kwargs + ) + response = pipeline_response.http_response + + valid_status_codes = [200] + if etag is not None and match_condition is not None: + valid_status_codes.append(304) + + if response.status_code not in valid_status_codes: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _deserialize(_models.Error, response.json()) + raise HttpResponseError(response=response, model=error) + + response_headers = response.headers + result = json.loads("{}") + if response.status_code != 304: + result = pipeline_response.http_response.json() + else: + unparsed_link = pipeline_response.http_response.headers.get("Link") + next_link = None + if unparsed_link: + next_link = unparsed_link[1 : unparsed_link.index(">")] + result["@nextLink"] = next_link + + if cls: + return cls(pipeline_response, result, response_headers) + + return result + + @distributed_trace_async + async def check_key_values_in_one_page( + self, + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[List[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, + tags: Optional[List[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + continuation_token: Optional[str] = None, + **kwargs: Any, + ) -> dict: + """Checks key-values using HEAD request, returning only headers without the response body. + + :keyword key: A filter used to match keys. Default value is None. + :paramtype key: str + :keyword label: A filter used to match labels. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword snapshot: A filter used get key-values for a snapshot. Default value is None. + :paramtype snapshot: str + :keyword tags: A filter used to query by tags. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :param str continuation_token: An opaque continuation token. + :return: A dict containing ``items`` (empty list) and ``@nextLink`` (pagination URL or None). + :rtype: dict + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[List[_models.KeyValue]] = kwargs.pop("cls", None) + + error_map = self._build_kv_error_map(match_condition) + error_map.update(kwargs.pop("error_map", {}) or {}) + + if continuation_token: + _request = self._prepare_continuation_request( + "HEAD", + continuation_token, + sync_token=sync_token, + accept_datetime=accept_datetime, + etag=etag, + match_condition=match_condition, + headers=_headers, + ) + else: + _request = build_azure_app_configuration_check_key_values_request( + key=key, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + snapshot=snapshot, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + _request = self._format_request(_request) + + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=False, **kwargs + ) + response = pipeline_response.http_response + + valid_status_codes = [200] + if etag is not None and match_condition == MatchConditions.IfModified: + valid_status_codes.append(304) + + if response.status_code not in valid_status_codes: + map_error(status_code=response.status_code, response=response, error_map=error_map) + raise HttpResponseError(response=response) + + response_headers = response.headers + result = {} + unparsed_link = response_headers.get("Link") + next_link = None + if unparsed_link: + next_link = unparsed_link[1 : unparsed_link.index(">")] + result["@nextLink"] = next_link + if response.status_code != 304: + result["items"] = [] + + if cls: + return cls(pipeline_response, result, response_headers) + + return result + + +__all__: List[str] = [ + "_AzureAppConfigurationClientOperationsMixin" +] # Add all objects you want publicly available to users at this package level + + +def patch_sdk(): + """Do not remove from this file. + + `patch_sdk` is a last resort escape hatch that allows you to do customizations + you can't accomplish using the techniques described in + https://aka.ms/azsdk/python/dpcodegen/python/customize + """ diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py index 20a4a799a924..dce821e15388 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py @@ -588,16 +588,16 @@ class PercentileAllocation(_Model): :ivar variant: The variant to allocate these percentiles to. Required. :vartype variant: str :ivar from_property: The lower bounds for this percentile allocation. Required. - :vartype from_property: int + :vartype from_property: float :ivar to: The upper bounds for this percentile allocation. Required. - :vartype to: int + :vartype to: float """ variant: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) """The variant to allocate these percentiles to. Required.""" - from_property: int = rest_field(name="from", visibility=["read", "create", "update", "delete", "query"]) + from_property: float = rest_field(name="from", visibility=["read", "create", "update", "delete", "query"]) """The lower bounds for this percentile allocation. Required.""" - to: int = rest_field(visibility=["read", "create", "update", "delete", "query"]) + to: float = rest_field(visibility=["read", "create", "update", "delete", "query"]) """The upper bounds for this percentile allocation. Required.""" @overload @@ -605,8 +605,8 @@ def __init__( self, *, variant: str, - from_property: int, - to: int, + from_property: float, + to: float, ) -> None: ... @overload diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_operations/__init__.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/__init__.py similarity index 88% rename from sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_operations/__init__.py rename to sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/__init__.py index 91bafc83f689..478d88f8496d 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/_operations/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/__init__.py @@ -12,12 +12,15 @@ if TYPE_CHECKING: from ._patch import * # pylint: disable=unused-wildcard-import +from ._operations import FeatureFlagClientOperations # type: ignore from ._operations import _AzureAppConfigurationClientOperationsMixin # type: ignore # pylint: disable=unused-import from ._patch import __all__ as _patch_all from ._patch import * from ._patch import patch_sdk as _patch_sdk -__all__ = [] +__all__ = [ + "FeatureFlagClientOperations", +] __all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore _patch_sdk() diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_operations.py similarity index 98% rename from sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py rename to sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_operations.py index 51005a80882d..af728123431c 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_operations.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_operations.py @@ -35,92 +35,26 @@ from .. import models as _models from .._configuration import AzureAppConfigurationClientConfiguration from .._utils.model_base import SdkJSONEncoder, _deserialize, _failsafe_deserialize -from .._utils.serialization import Serializer +from .._utils.serialization import Deserializer, Serializer from .._utils.utils import ClientMixinABC, prep_if_match, prep_if_none_match from .._validation import api_version_validation -JSON = MutableMapping[str, Any] T = TypeVar("T") ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, dict[str, Any]], Any]] +JSON = MutableMapping[str, Any] _SERIALIZER = Serializer() _SERIALIZER.client_side_validation = False -def build_azure_app_configuration_get_keys_request( # pylint: disable=name-too-long - *, - name: Optional[str] = None, - after: Optional[str] = None, - sync_token: Optional[str] = None, - accept_datetime: Optional[str] = None, - **kwargs: Any -) -> HttpRequest: - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) - _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.keyset+json, application/problem+json") - - # Construct URL - _url = "/keys" - - # Construct parameters - _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if name is not None: - _params["name"] = _SERIALIZER.query("name", name, "str") - if after is not None: - _params["After"] = _SERIALIZER.query("after", after, "str") - - # Construct headers - if sync_token is not None: - _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") - if accept_datetime is not None: - _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") - _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") - - return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) - - -def build_azure_app_configuration_check_keys_request( # pylint: disable=name-too-long +def build_feature_flag_client_get_feature_flags_request( # pylint: disable=name-too-long *, name: Optional[str] = None, - after: Optional[str] = None, - sync_token: Optional[str] = None, - accept_datetime: Optional[str] = None, - **kwargs: Any -) -> HttpRequest: - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) - _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) - # Construct URL - _url = "/keys" - - # Construct parameters - _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if name is not None: - _params["name"] = _SERIALIZER.query("name", name, "str") - if after is not None: - _params["After"] = _SERIALIZER.query("after", after, "str") - - # Construct headers - if sync_token is not None: - _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") - if accept_datetime is not None: - _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") - - return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) - - -def build_azure_app_configuration_get_key_values_request( # pylint: disable=name-too-long - *, - key: Optional[str] = None, label: Optional[str] = None, sync_token: Optional[str] = None, after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - snapshot: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, @@ -130,23 +64,24 @@ def build_azure_app_configuration_get_key_values_request( # pylint: disable=nam _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kvset+json, application/problem+json") + accept = _headers.pop( + "Accept", + 'application/json;profile="https://azconfig.io/mime-profiles/ffset";charset=utf-8, application/problem+json', + ) # Construct URL - _url = "/kv" + _url = "/ff" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if key is not None: - _params["key"] = _SERIALIZER.query("key", key, "str") + if name is not None: + _params["name"] = _SERIALIZER.query("name", name, "str") if label is not None: _params["label"] = _SERIALIZER.query("label", label, "str") if after is not None: _params["After"] = _SERIALIZER.query("after", after, "str") if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") - if snapshot is not None: - _params["snapshot"] = _SERIALIZER.query("snapshot", snapshot, "str") if tags is not None: _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] @@ -166,15 +101,14 @@ def build_azure_app_configuration_get_key_values_request( # pylint: disable=nam return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_check_key_values_request( # pylint: disable=name-too-long +def build_feature_flag_client_check_feature_flags_request( # pylint: disable=name-too-long *, - key: Optional[str] = None, + name: Optional[str] = None, label: Optional[str] = None, sync_token: Optional[str] = None, after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - snapshot: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, @@ -185,20 +119,18 @@ def build_azure_app_configuration_check_key_values_request( # pylint: disable=n api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL - _url = "/kv" + _url = "/ff" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if key is not None: - _params["key"] = _SERIALIZER.query("key", key, "str") + if name is not None: + _params["name"] = _SERIALIZER.query("name", name, "str") if label is not None: _params["label"] = _SERIALIZER.query("label", label, "str") if after is not None: _params["After"] = _SERIALIZER.query("after", after, "str") if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") - if snapshot is not None: - _params["snapshot"] = _SERIALIZER.query("snapshot", snapshot, "str") if tags is not None: _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] @@ -217,11 +149,11 @@ def build_azure_app_configuration_check_key_values_request( # pylint: disable=n return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_get_key_value_request( # pylint: disable=name-too-long - key: str, +def build_feature_flag_client_get_feature_flag_request( # pylint: disable=name-too-long + name: str, *, label: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, sync_token: Optional[str] = None, accept_datetime: Optional[str] = None, tags: Optional[list[str]] = None, @@ -233,12 +165,15 @@ def build_azure_app_configuration_get_key_value_request( # pylint: disable=name _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") + accept = _headers.pop( + "Accept", + 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8, application/problem+json', + ) # Construct URL - _url = "/kv/{key}" + _url = "/ff/{name}" path_format_arguments = { - "key": _SERIALIZER.url("key", key, "str"), + "name": _SERIALIZER.url("name", name, "str"), } _url: str = _url.format(**path_format_arguments) # type: ignore @@ -268,11 +203,14 @@ def build_azure_app_configuration_get_key_value_request( # pylint: disable=name return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_put_key_value_request( # pylint: disable=name-too-long - key: str, +def build_feature_flag_client_check_feature_flag_request( # pylint: disable=name-too-long + name: str, *, label: Optional[str] = None, sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any @@ -280,14 +218,11 @@ def build_azure_app_configuration_put_key_value_request( # pylint: disable=name _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") - # Construct URL - _url = "/kv/{key}" + _url = "/ff/{name}" path_format_arguments = { - "key": _SERIALIZER.url("key", key, "str"), + "name": _SERIALIZER.url("name", name, "str"), } _url: str = _url.format(**path_format_arguments) # type: ignore @@ -296,13 +231,16 @@ def build_azure_app_configuration_put_key_value_request( # pylint: disable=name _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") if label is not None: _params["label"] = _SERIALIZER.query("label", label, "str") + if select is not None: + _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers - if content_type is not None: - _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") - _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") if_match = prep_if_match(etag, match_condition) if if_match is not None: _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") @@ -310,11 +248,11 @@ def build_azure_app_configuration_put_key_value_request( # pylint: disable=name if if_none_match is not None: _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="PUT", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_delete_key_value_request( # pylint: disable=name-too-long - key: str, +def build_feature_flag_client_put_feature_flag_request( # pylint: disable=name-too-long + name: str, *, label: Optional[str] = None, sync_token: Optional[str] = None, @@ -325,13 +263,17 @@ def build_azure_app_configuration_delete_key_value_request( # pylint: disable=n _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + content_type: str = kwargs.pop("content_type") api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) - accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") + accept = _headers.pop( + "Accept", + 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8, application/problem+json', + ) # Construct URL - _url = "/kv/{key}" + _url = "/ff/{name}" path_format_arguments = { - "key": _SERIALIZER.url("key", key, "str"), + "name": _SERIALIZER.url("name", name, "str"), } _url: str = _url.format(**path_format_arguments) # type: ignore @@ -342,6 +284,7 @@ def build_azure_app_configuration_delete_key_value_request( # pylint: disable=n _params["label"] = _SERIALIZER.query("label", label, "str") # Construct headers + _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") @@ -350,19 +293,16 @@ def build_azure_app_configuration_delete_key_value_request( # pylint: disable=n _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") if_none_match = prep_if_none_match(etag, match_condition) if if_none_match is not None: - _headers["if-none-match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="DELETE", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="PUT", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_check_key_value_request( # pylint: disable=name-too-long - key: str, +def build_feature_flag_client_delete_feature_flag_request( # pylint: disable=name-too-long + name: str, *, label: Optional[str] = None, sync_token: Optional[str] = None, - accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any @@ -371,10 +311,15 @@ def build_azure_app_configuration_check_key_value_request( # pylint: disable=na _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop( + "Accept", + 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8, application/problem+json', + ) + # Construct URL - _url = "/kv/{key}" + _url = "/ff/{name}" path_format_arguments = { - "key": _SERIALIZER.url("key", key, "str"), + "name": _SERIALIZER.url("name", name, "str"), } _url: str = _url.format(**path_format_arguments) # type: ignore @@ -383,35 +328,29 @@ def build_azure_app_configuration_check_key_value_request( # pylint: disable=na _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") if label is not None: _params["label"] = _SERIALIZER.query("label", label, "str") - if select is not None: - _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") - if tags is not None: - _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") - if accept_datetime is not None: - _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") if_match = prep_if_match(etag, match_condition) if if_match is not None: _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") if_none_match = prep_if_none_match(etag, match_condition) if if_none_match is not None: - _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + _headers["if-none-match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="DELETE", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_get_feature_flags_request( # pylint: disable=name-too-long +def build_feature_flag_client_get_feature_flag_revisions_request( # pylint: disable=name-too-long *, name: Optional[str] = None, label: Optional[str] = None, - sync_token: Optional[str] = None, after: Optional[str] = None, - accept_datetime: Optional[str] = None, select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, + sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any @@ -426,7 +365,7 @@ def build_azure_app_configuration_get_feature_flags_request( # pylint: disable= ) # Construct URL - _url = "/ff" + _url = "/ff-revisions" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") @@ -444,8 +383,6 @@ def build_azure_app_configuration_get_feature_flags_request( # pylint: disable= # Construct headers if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") - if accept_datetime is not None: - _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") if_match = prep_if_match(etag, match_condition) if if_match is not None: @@ -457,17 +394,13 @@ def build_azure_app_configuration_get_feature_flags_request( # pylint: disable= return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_check_feature_flags_request( # pylint: disable=name-too-long +def build_feature_flag_client_check_feature_flag_revisions_request( # pylint: disable=name-too-long *, name: Optional[str] = None, label: Optional[str] = None, - sync_token: Optional[str] = None, after: Optional[str] = None, - accept_datetime: Optional[str] = None, select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) @@ -475,7 +408,7 @@ def build_azure_app_configuration_check_feature_flags_request( # pylint: disabl api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL - _url = "/ff" + _url = "/ff-revisions" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") @@ -491,57 +424,33 @@ def build_azure_app_configuration_check_feature_flags_request( # pylint: disabl _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers - if sync_token is not None: - _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") - if accept_datetime is not None: - _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") - if_match = prep_if_match(etag, match_condition) - if if_match is not None: - _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") - if_none_match = prep_if_none_match(etag, match_condition) - if if_none_match is not None: - _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_get_feature_flag_request( # pylint: disable=name-too-long - name: str, +def build_azure_app_configuration_get_keys_request( # pylint: disable=name-too-long *, - label: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + name: Optional[str] = None, + after: Optional[str] = None, sync_token: Optional[str] = None, accept_datetime: Optional[str] = None, - tags: Optional[list[str]] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) - accept = _headers.pop( - "Accept", - 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8, application/problem+json', - ) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.keyset+json, application/problem+json") # Construct URL - _url = "/ff/{name}" - path_format_arguments = { - "name": _SERIALIZER.url("name", name, "str"), - } - - _url: str = _url.format(**path_format_arguments) # type: ignore + _url = "/keys" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if label is not None: - _params["label"] = _SERIALIZER.query("label", label, "str") - if select is not None: - _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") - if tags is not None: - _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] + if name is not None: + _params["name"] = _SERIALIZER.query("name", name, "str") + if after is not None: + _params["After"] = _SERIALIZER.query("after", after, "str") # Construct headers if sync_token is not None: @@ -549,26 +458,16 @@ def build_azure_app_configuration_get_feature_flag_request( # pylint: disable=n if accept_datetime is not None: _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") - if_match = prep_if_match(etag, match_condition) - if if_match is not None: - _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") - if_none_match = prep_if_none_match(etag, match_condition) - if if_none_match is not None: - _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_check_feature_flag_request( # pylint: disable=name-too-long - name: str, +def build_azure_app_configuration_check_keys_request( # pylint: disable=name-too-long *, - label: Optional[str] = None, + name: Optional[str] = None, + after: Optional[str] = None, sync_token: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, - tags: Optional[list[str]] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) @@ -576,19 +475,111 @@ def build_azure_app_configuration_check_feature_flag_request( # pylint: disable api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL - _url = "/ff/{name}" - path_format_arguments = { - "name": _SERIALIZER.url("name", name, "str"), - } - - _url: str = _url.format(**path_format_arguments) # type: ignore + _url = "/keys" # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if name is not None: + _params["name"] = _SERIALIZER.query("name", name, "str") + if after is not None: + _params["After"] = _SERIALIZER.query("after", after, "str") + + # Construct headers + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + + return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_azure_app_configuration_get_key_values_request( # pylint: disable=name-too-long + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kvset+json, application/problem+json") + + # Construct URL + _url = "/kv" + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if key is not None: + _params["key"] = _SERIALIZER.query("key", key, "str") + if label is not None: + _params["label"] = _SERIALIZER.query("label", label, "str") + if after is not None: + _params["After"] = _SERIALIZER.query("after", after, "str") + if select is not None: + _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if snapshot is not None: + _params["snapshot"] = _SERIALIZER.query("snapshot", snapshot, "str") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] + + # Construct headers + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + + return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_azure_app_configuration_check_key_values_request( # pylint: disable=name-too-long + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) + # Construct URL + _url = "/kv" + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + if key is not None: + _params["key"] = _SERIALIZER.query("key", key, "str") if label is not None: _params["label"] = _SERIALIZER.query("label", label, "str") + if after is not None: + _params["After"] = _SERIALIZER.query("after", after, "str") if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if snapshot is not None: + _params["snapshot"] = _SERIALIZER.query("snapshot", snapshot, "str") if tags is not None: _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] @@ -607,11 +598,14 @@ def build_azure_app_configuration_check_feature_flag_request( # pylint: disable return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_put_feature_flag_request( # pylint: disable=name-too-long - name: str, +def build_azure_app_configuration_get_key_value_request( # pylint: disable=name-too-long + key: str, *, label: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any @@ -619,17 +613,13 @@ def build_azure_app_configuration_put_feature_flag_request( # pylint: disable=n _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - content_type: str = kwargs.pop("content_type") api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) - accept = _headers.pop( - "Accept", - 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8, application/problem+json', - ) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") # Construct URL - _url = "/ff/{name}" + _url = "/kv/{key}" path_format_arguments = { - "name": _SERIALIZER.url("name", name, "str"), + "key": _SERIALIZER.url("key", key, "str"), } _url: str = _url.format(**path_format_arguments) # type: ignore @@ -638,11 +628,16 @@ def build_azure_app_configuration_put_feature_flag_request( # pylint: disable=n _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") if label is not None: _params["label"] = _SERIALIZER.query("label", label, "str") + if select is not None: + _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") + if tags is not None: + _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers - _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") if_match = prep_if_match(etag, match_condition) if if_match is not None: @@ -651,11 +646,11 @@ def build_azure_app_configuration_put_feature_flag_request( # pylint: disable=n if if_none_match is not None: _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="PUT", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_delete_feature_flag_request( # pylint: disable=name-too-long - name: str, +def build_azure_app_configuration_put_key_value_request( # pylint: disable=name-too-long + key: str, *, label: Optional[str] = None, sync_token: Optional[str] = None, @@ -666,16 +661,14 @@ def build_azure_app_configuration_delete_feature_flag_request( # pylint: disabl _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) - accept = _headers.pop( - "Accept", - 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8, application/problem+json', - ) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") # Construct URL - _url = "/ff/{name}" + _url = "/kv/{key}" path_format_arguments = { - "name": _SERIALIZER.url("name", name, "str"), + "key": _SERIALIZER.url("key", key, "str"), } _url: str = _url.format(**path_format_arguments) # type: ignore @@ -686,6 +679,8 @@ def build_azure_app_configuration_delete_feature_flag_request( # pylint: disabl _params["label"] = _SERIALIZER.query("label", label, "str") # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") if sync_token is not None: _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") @@ -694,18 +689,15 @@ def build_azure_app_configuration_delete_feature_flag_request( # pylint: disabl _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") if_none_match = prep_if_none_match(etag, match_condition) if if_none_match is not None: - _headers["if-none-match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="DELETE", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="PUT", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_get_feature_flag_revisions_request( # pylint: disable=name-too-long +def build_azure_app_configuration_delete_key_value_request( # pylint: disable=name-too-long + key: str, *, - name: Optional[str] = None, label: Optional[str] = None, - after: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, - tags: Optional[list[str]] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, @@ -715,26 +707,20 @@ def build_azure_app_configuration_get_feature_flag_revisions_request( # pylint: _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) - accept = _headers.pop( - "Accept", - 'application/json;profile="https://azconfig.io/mime-profiles/ffset";charset=utf-8, application/problem+json', - ) + accept = _headers.pop("Accept", "application/vnd.microsoft.appconfig.kv+json, application/problem+json") # Construct URL - _url = "/ff-revisions" + _url = "/kv/{key}" + path_format_arguments = { + "key": _SERIALIZER.url("key", key, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if name is not None: - _params["name"] = _SERIALIZER.query("name", name, "str") if label is not None: _params["label"] = _SERIALIZER.query("label", label, "str") - if after is not None: - _params["After"] = _SERIALIZER.query("after", after, "str") - if select is not None: - _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") - if tags is not None: - _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers if sync_token is not None: @@ -745,18 +731,21 @@ def build_azure_app_configuration_get_feature_flag_revisions_request( # pylint: _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") if_none_match = prep_if_none_match(etag, match_condition) if if_none_match is not None: - _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + _headers["if-none-match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") - return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) + return HttpRequest(method="DELETE", url=_url, params=_params, headers=_headers, **kwargs) -def build_azure_app_configuration_check_feature_flag_revisions_request( # pylint: disable=name-too-long +def build_azure_app_configuration_check_key_value_request( # pylint: disable=name-too-long + key: str, *, - name: Optional[str] = None, label: Optional[str] = None, - after: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) @@ -764,22 +753,33 @@ def build_azure_app_configuration_check_feature_flag_revisions_request( # pylin api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-05-01-preview")) # Construct URL - _url = "/ff-revisions" + _url = "/kv/{key}" + path_format_arguments = { + "key": _SERIALIZER.url("key", key, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore # Construct parameters _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") - if name is not None: - _params["name"] = _SERIALIZER.query("name", name, "str") if label is not None: _params["label"] = _SERIALIZER.query("label", label, "str") - if after is not None: - _params["After"] = _SERIALIZER.query("after", after, "str") if select is not None: _params["$Select"] = _SERIALIZER.query("select", select, "[str]", div=",") if tags is not None: _params["tags"] = [_SERIALIZER.query("tags", q, "str") if q is not None else "" for q in tags] # Construct headers + if sync_token is not None: + _headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) @@ -1260,44 +1260,99 @@ def build_azure_app_configuration_check_revisions_request( # pylint: disable=na return HttpRequest(method="HEAD", url=_url, params=_params, headers=_headers, **kwargs) -class _AzureAppConfigurationClientOperationsMixin( # pylint: disable=too-many-public-methods - ClientMixinABC[PipelineClient[HttpRequest, HttpResponse], AzureAppConfigurationClientConfiguration] -): +class FeatureFlagClientOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~azure.appconfiguration.AzureAppConfigurationClient`'s + :attr:`feature_flag_client` attribute. + """ + + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client: PipelineClient = input_args.pop(0) if input_args else kwargs.pop("client") + self._config: AzureAppConfigurationClientConfiguration = ( + input_args.pop(0) if input_args else kwargs.pop("config") + ) + self._serialize: Serializer = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize: Deserializer = input_args.pop(0) if input_args else kwargs.pop("deserializer") @distributed_trace - def get_keys( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "after", + "accept_datetime", + "select", + "tags", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def get_feature_flags( self, *, name: Optional[str] = None, - after: Optional[str] = None, + label: Optional[str] = None, sync_token: Optional[str] = None, + after: Optional[str] = None, accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> ItemPaged["_models.Key"]: - """Gets a list of keys. + ) -> ItemPaged["_models.FeatureFlag"]: + """Gets a list of feature flags. - Gets a list of keys. + Gets a list of feature flags. - :keyword name: A filter for the name of the returned keys. Default value is None. + :keyword name: A filter used to match feature flag names. Default value is None. :paramtype name: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str :keyword accept_datetime: Requests the server to respond with the state of the resource at the specified time. Default value is None. :paramtype accept_datetime: str - :return: An iterator like instance of Key - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.Key] + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: An iterator like instance of FeatureFlag + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.FeatureFlag] :raises ~azure.core.exceptions.HttpResponseError: """ _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[list[_models.Key]] = kwargs.pop("cls", None) + cls: ClsType[list[_models.FeatureFlag]] = kwargs.pop("cls", None) error_map: MutableMapping = { 401: ClientAuthenticationError, @@ -1305,16 +1360,27 @@ def get_keys( 409: ResourceExistsError, 304: ResourceNotModifiedError, } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) def prepare_request(next_link=None): if not next_link: - _request = build_azure_app_configuration_get_keys_request( + _request = build_feature_flag_client_get_feature_flags_request( name=name, - after=after, + label=label, sync_token=sync_token, + after=after, accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, api_version=self._config.api_version, headers=_headers, params=_params, @@ -1354,7 +1420,7 @@ def prepare_request(next_link=None): def extract_data(pipeline_response): deserialized = pipeline_response.http_response.json() list_of_elem = _deserialize( - list[_models.Key], + list[_models.FeatureFlag], deserialized.get("items", []), ) if cls: @@ -1383,31 +1449,71 @@ def get_next(next_link=None): return ItemPaged(get_next, extract_data) @distributed_trace - def check_keys( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "after", + "accept_datetime", + "select", + "tags", + "client_request_id", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def check_feature_flags( self, *, name: Optional[str] = None, - after: Optional[str] = None, + label: Optional[str] = None, sync_token: Optional[str] = None, + after: Optional[str] = None, accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> bool: """Requests the headers and status of the given resource. Requests the headers and status of the given resource. - :keyword name: A filter for the name of the returned keys. Default value is None. + :keyword name: A filter used to match feature flag names. Default value is None. :paramtype name: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str :keyword accept_datetime: Requests the server to respond with the state of the resource at the specified time. Default value is None. :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions :return: bool :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: @@ -1418,6 +1524,12 @@ def check_keys( 409: ResourceExistsError, 304: ResourceNotModifiedError, } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) _headers = kwargs.pop("headers", {}) or {} @@ -1425,11 +1537,16 @@ def check_keys( cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_keys_request( + _request = build_feature_flag_client_check_feature_flags_request( name=name, - after=after, + label=label, sync_token=sync_token, + after=after, accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, api_version=self._config.api_version, headers=_headers, params=_params, @@ -1456,55 +1573,63 @@ def check_keys( response_headers = {} response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) if cls: return cls(pipeline_response, None, response_headers) # type: ignore return 200 <= response.status_code <= 299 @distributed_trace - def get_key_values( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "select", + "sync_token", + "accept_datetime", + "client_request_id", + "tags", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def get_feature_flag( self, + name: str, *, - key: Optional[str] = None, label: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, sync_token: Optional[str] = None, - after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - snapshot: Optional[str] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> ItemPaged["_models.KeyValue"]: - """Gets a list of key-values. + ) -> _models.FeatureFlag: + """Gets a single feature flag. - Gets a list of key-values. + Gets a single feature flag. - :keyword key: A filter used to match keys. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype key: str - :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. + :param name: The name of the feature flag. Required. + :type name: str + :keyword label: The label of the feature flag to retrieve. Default value is None. :paramtype label: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str :keyword accept_datetime: Requests the server to respond with the state of the resource at the specified time. Default value is None. :paramtype accept_datetime: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] - :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of - the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. - :paramtype snapshot: str :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -1514,15 +1639,10 @@ def get_key_values( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: An iterator like instance of KeyValue - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.KeyValue] + :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag :raises ~azure.core.exceptions.HttpResponseError: """ - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[list[_models.KeyValue]] = kwargs.pop("cls", None) - error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, @@ -1537,98 +1657,93 @@ def get_key_values( error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) - def prepare_request(next_link=None): - if not next_link: + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} - _request = build_azure_app_configuration_get_key_values_request( - key=key, - label=label, - sync_token=sync_token, - after=after, - accept_datetime=accept_datetime, - select=select, - snapshot=snapshot, - tags=tags, - etag=etag, - match_condition=match_condition, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url( - "self._config.endpoint", self._config.endpoint, "str", skip_quote=True - ), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) - else: - # make call to next link with the client's api-version - _parsed_next_link = urllib.parse.urlparse(next_link) - _next_request_params = case_insensitive_dict( - { - key: [urllib.parse.quote(v) for v in value] - for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() - } - ) - _next_request_params["api-version"] = self._config.api_version - _request = HttpRequest( - "GET", - urllib.parse.urljoin(next_link, _parsed_next_link.path), - headers=_headers, - params=_next_request_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url( - "self._config.endpoint", self._config.endpoint, "str", skip_quote=True - ), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) + _request = build_feature_flag_client_get_feature_flag_request( + name=name, + label=label, + select=select, + sync_token=sync_token, + accept_datetime=accept_datetime, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) - return _request + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) - def extract_data(pipeline_response): - deserialized = pipeline_response.http_response.json() - list_of_elem = _deserialize( - list[_models.KeyValue], - deserialized.get("items", []), + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, ) - if cls: - list_of_elem = cls(list_of_elem) # type: ignore - return deserialized.get("@nextLink") or None, iter(list_of_elem) + raise HttpResponseError(response=response, model=error) - def get_next(next_link=None): - _request = prepare_request(next_link) - - _stream = False - pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - response = pipeline_response.http_response + response_headers = {} + response_headers["x-ms-request-id"] = self._deserialize("str", response.headers.get("x-ms-request-id")) + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) - if response.status_code not in [200]: - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.Error, - response, - ) - raise HttpResponseError(response=response, model=error) + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.FeatureFlag, response.json()) - return pipeline_response + if cls: + return cls(pipeline_response, deserialized, response_headers) # type: ignore - return ItemPaged(get_next, extract_data) + return deserialized # type: ignore @distributed_trace - def check_key_values( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "accept_datetime", + "select", + "tags", + "client_request_id", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def check_feature_flag( self, + name: str, *, - key: Optional[str] = None, label: Optional[str] = None, sync_token: Optional[str] = None, - after: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - snapshot: Optional[str] = None, + select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, @@ -1638,30 +1753,20 @@ def check_key_values( Requests the headers and status of the given resource. - :keyword key: A filter used to match keys. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype key: str - :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. + :param name: The name of the feature flag to retrieve. Required. + :type name: str + :keyword label: The label of the feature flag to retrieve. Default value is None. :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str :keyword accept_datetime: Requests the server to respond with the state of the resource at the specified time. Default value is None. :paramtype accept_datetime: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] - :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of - the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. - :paramtype snapshot: str + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -1694,14 +1799,12 @@ def check_key_values( cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_key_values_request( - key=key, + _request = build_feature_flag_client_check_feature_flag_request( + name=name, label=label, sync_token=sync_token, - after=after, accept_datetime=accept_datetime, select=select, - snapshot=snapshot, tags=tags, etag=etag, match_condition=match_condition, @@ -1739,51 +1842,53 @@ def check_key_values( @distributed_trace @api_version_validation( - params_added_on={"2024-09-01": ["tags"]}, - api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "content_type", + "name", + "label", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], ) - def get_key_value( + def put_feature_flag( self, - key: str, + name: str, + entity: Optional[_models.FeatureFlag] = None, *, label: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, sync_token: Optional[str] = None, - accept_datetime: Optional[str] = None, - tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.KeyValue: - """Gets a single key-value. + ) -> _models.FeatureFlag: + """Creates a feature flag. - Gets a single key-value. + Creates a feature flag. - :param key: The key of the key-value. Required. - :type key: str - :keyword label: The label of the key-value to retrieve. Default value is None. + :param name: The name of the feature flag to create. Required. + :type name: str + :param entity: The feature flag to create. Default value is None. + :type entity: ~azure.appconfiguration._generated.models.FeatureFlag + :keyword label: The label of the feature flag to create. Default value is None. :paramtype label: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str - :keyword accept_datetime: Requests the server to respond with the state of the resource at the - specified - time. Default value is None. - :paramtype accept_datetime: str - :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype tags: list[str] :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is None. :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: KeyValue. The KeyValue is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.KeyValue + :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -1800,21 +1905,32 @@ def get_key_value( error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) - _headers = kwargs.pop("headers", {}) or {} + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = kwargs.pop("params", {}) or {} - cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) + content_type: Optional[str] = kwargs.pop( + "content_type", + _headers.pop( + "Content-Type", 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8' + ), + ) + content_type = content_type if entity else None + cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_get_key_value_request( - key=key, + if entity is not None: + _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + else: + _content = None + + _request = build_feature_flag_client_put_feature_flag_request( + name=name, label=label, - select=select, sync_token=sync_token, - accept_datetime=accept_datetime, - tags=tags, etag=etag, match_condition=match_condition, + content_type=content_type, api_version=self._config.api_version, + content=_content, headers=_headers, params=_params, ) @@ -1845,7 +1961,6 @@ def get_key_value( raise HttpResponseError(response=response, model=error) response_headers = {} - response_headers["x-ms-request-id"] = self._deserialize("str", response.headers.get("x-ms-request-id")) response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) @@ -1853,75 +1968,47 @@ def get_key_value( if _stream: deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.KeyValue, response.json()) + deserialized = _deserialize(_models.FeatureFlag, response.json()) if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore return deserialized # type: ignore - @overload - def _put_key_value( - self, - key: str, - entity: Optional[_models.KeyValue] = None, - *, - content_type: str, - label: Optional[str] = None, - sync_token: Optional[str] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> _models.KeyValue: ... - @overload - def _put_key_value( - self, - key: str, - entity: Optional[JSON] = None, - *, - content_type: str, - label: Optional[str] = None, - sync_token: Optional[str] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> _models.KeyValue: ... - @overload - def _put_key_value( - self, - key: str, - entity: Optional[IO[bytes]] = None, - *, - content_type: str, - label: Optional[str] = None, - sync_token: Optional[str] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> _models.KeyValue: ... - @distributed_trace - def _put_key_value( + @api_version_validation( + method_added_on="2026-05-01-preview", + params_added_on={ + "2026-05-01-preview": [ + "api_version", + "name", + "label", + "sync_token", + "client_request_id", + "accept", + "etag", + "match_condition", + ] + }, + api_versions_list=["2026-05-01-preview"], + ) + def delete_feature_flag( self, - key: str, - entity: Optional[Union[_models.KeyValue, JSON, IO[bytes]]] = None, + name: str, *, label: Optional[str] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.KeyValue: - """Creates a key-value. + ) -> Optional[_models.FeatureFlag]: + """Deletes a feature flag. - Creates a key-value. + Deletes a feature flag. - :param key: The key of the key-value to create. Required. - :type key: str - :param entity: The key-value to create. Is one of the following types: KeyValue, JSON, - IO[bytes] Default value is None. - :type entity: ~azure.appconfiguration._generated.models.KeyValue or JSON or IO[bytes] - :keyword label: The label of the key-value to create. Default value is None. + :param name: The name of the feature flag to delete. Required. + :type name: str + :keyword label: The label of the feature flag to delete. Default value is None. :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. @@ -1931,8 +2018,8 @@ def _put_key_value( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: KeyValue. The KeyValue is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.KeyValue + :return: FeatureFlag or None. The FeatureFlag is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.FeatureFlag or None :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -1949,128 +2036,13 @@ def _put_key_value( error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) - _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - content_type = content_type if entity else None - cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) - - content_type = content_type or "application/json" if entity else None - _content = None - if isinstance(entity, (IOBase, bytes)): - _content = entity - else: - if entity is not None: - _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore - else: - _content = None + cls: ClsType[Optional[_models.FeatureFlag]] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_put_key_value_request( - key=key, - label=label, - sync_token=sync_token, - etag=etag, - match_condition=match_condition, - content_type=content_type, - api_version=self._config.api_version, - content=_content, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) - pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - - response = pipeline_response.http_response - - if response.status_code not in [200]: - if _stream: - try: - response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.Error, - response, - ) - raise HttpResponseError(response=response, model=error) - - response_headers = {} - response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) - response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) - response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) - - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.KeyValue, response.json()) - - if cls: - return cls(pipeline_response, deserialized, response_headers) # type: ignore - - return deserialized # type: ignore - - @distributed_trace - def delete_key_value( - self, - key: str, - *, - label: Optional[str] = None, - sync_token: Optional[str] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> Optional[_models.KeyValue]: - """Deletes a key-value. - - Deletes a key-value. - - :param key: The key of the key-value to delete. Required. - :type key: str - :keyword label: The label of the key-value to delete. Default value is None. - :paramtype label: str - :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is - None. - :paramtype sync_token: str - :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is - None. - :paramtype etag: str - :keyword match_condition: The match condition to use upon the etag. Default value is None. - :paramtype match_condition: ~azure.core.MatchConditions - :return: KeyValue or None. The KeyValue is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.KeyValue or None - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - if match_condition == MatchConditions.IfNotModified: - error_map[412] = ResourceModifiedError - elif match_condition == MatchConditions.IfPresent: - error_map[412] = ResourceNotFoundError - elif match_condition == MatchConditions.IfMissing: - error_map[412] = ResourceExistsError - error_map.update(kwargs.pop("error_map", {}) or {}) - - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[Optional[_models.KeyValue]] = kwargs.pop("cls", None) - - _request = build_azure_app_configuration_delete_key_value_request( - key=key, + _request = build_feature_flag_client_delete_feature_flag_request( + name=name, label=label, sync_token=sync_token, etag=etag, @@ -2115,7 +2087,7 @@ def delete_key_value( if _stream: deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.KeyValue, response.json()) + deserialized = _deserialize(_models.FeatureFlag, response.json()) if response.status_code == 204: response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) @@ -2125,115 +2097,6 @@ def delete_key_value( return deserialized # type: ignore - @distributed_trace - @api_version_validation( - params_added_on={"2024-09-01": ["tags"]}, - api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], - ) - def check_key_value( - self, - key: str, - *, - label: Optional[str] = None, - sync_token: Optional[str] = None, - accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, - tags: Optional[list[str]] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, - **kwargs: Any - ) -> bool: - """Requests the headers and status of the given resource. - - Requests the headers and status of the given resource. - - :param key: The key of the key-value to retrieve. Required. - :type key: str - :keyword label: The label of the key-value to retrieve. Default value is None. - :paramtype label: str - :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is - None. - :paramtype sync_token: str - :keyword accept_datetime: Requests the server to respond with the state of the resource at the - specified - time. Default value is None. - :paramtype accept_datetime: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] - :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. - :paramtype tags: list[str] - :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is - None. - :paramtype etag: str - :keyword match_condition: The match condition to use upon the etag. Default value is None. - :paramtype match_condition: ~azure.core.MatchConditions - :return: bool - :rtype: bool - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - if match_condition == MatchConditions.IfNotModified: - error_map[412] = ResourceModifiedError - elif match_condition == MatchConditions.IfPresent: - error_map[412] = ResourceNotFoundError - elif match_condition == MatchConditions.IfMissing: - error_map[412] = ResourceExistsError - error_map.update(kwargs.pop("error_map", {}) or {}) - - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[None] = kwargs.pop("cls", None) - - _request = build_azure_app_configuration_check_key_value_request( - key=key, - label=label, - sync_token=sync_token, - accept_datetime=accept_datetime, - select=select, - tags=tags, - etag=etag, - match_condition=match_condition, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - _stream = False - pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - - response = pipeline_response.http_response - - if response.status_code not in [200]: - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.Error, - response, - ) - raise HttpResponseError(response=response, model=error) - - response_headers = {} - response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) - response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) - - if cls: - return cls(pipeline_response, None, response_headers) # type: ignore - return 200 <= response.status_code <= 299 - @distributed_trace @api_version_validation( method_added_on="2026-05-01-preview", @@ -2242,11 +2105,11 @@ def check_key_value( "api_version", "name", "label", - "sync_token", "after", - "accept_datetime", "select", "tags", + "sync_token", + "client_request_id", "accept", "etag", "match_condition", @@ -2254,47 +2117,42 @@ def check_key_value( }, api_versions_list=["2026-05-01-preview"], ) - def get_feature_flags( + def get_feature_flag_revisions( self, *, name: Optional[str] = None, label: Optional[str] = None, - sync_token: Optional[str] = None, after: Optional[str] = None, - accept_datetime: Optional[str] = None, select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, + sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> ItemPaged["_models.FeatureFlag"]: - """Gets a list of feature flags. + """Gets a list of feature flag revisions. - Gets a list of feature flags. + Gets a list of feature flag revisions. - :keyword name: A filter used to match feature flag names. Default value is None. + :keyword name: A filter used to match names. Default value is None. :paramtype name: str :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. :paramtype label: str - :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is - None. - :paramtype sync_token: str :keyword after: Instructs the server to return elements that appear after the element referred to by the specified token. Default value is None. :paramtype after: str - :keyword accept_datetime: Requests the server to respond with the state of the resource at the - specified - time. Default value is None. - :paramtype accept_datetime: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. :paramtype tags: list[str] + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is None. :paramtype etag: str @@ -2326,14 +2184,13 @@ def get_feature_flags( def prepare_request(next_link=None): if not next_link: - _request = build_azure_app_configuration_get_feature_flags_request( + _request = build_feature_flag_client_get_feature_flag_revisions_request( name=name, label=label, - sync_token=sync_token, after=after, - accept_datetime=accept_datetime, select=select, tags=tags, + sync_token=sync_token, etag=etag, match_condition=match_condition, api_version=self._config.api_version, @@ -2407,68 +2264,40 @@ def get_next(next_link=None): @api_version_validation( method_added_on="2026-05-01-preview", params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "sync_token", - "after", - "accept_datetime", - "select", - "tags", - "client_request_id", - "etag", - "match_condition", - ] + "2026-05-01-preview": ["api_version", "name", "label", "after", "select", "tags", "client_request_id"] }, api_versions_list=["2026-05-01-preview"], ) - def check_feature_flags( + def check_feature_flag_revisions( self, *, name: Optional[str] = None, label: Optional[str] = None, - sync_token: Optional[str] = None, after: Optional[str] = None, - accept_datetime: Optional[str] = None, select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, - etag: Optional[str] = None, - match_condition: Optional[MatchConditions] = None, **kwargs: Any ) -> bool: """Requests the headers and status of the given resource. Requests the headers and status of the given resource. - :keyword name: A filter used to match feature flag names. Default value is None. + :keyword name: A filter used to match names. Default value is None. :paramtype name: str :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. :paramtype label: str - :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is - None. - :paramtype sync_token: str :keyword after: Instructs the server to return elements that appear after the element referred to by the specified token. Default value is None. :paramtype after: str - :keyword accept_datetime: Requests the server to respond with the state of the resource at the - specified - time. Default value is None. - :paramtype accept_datetime: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/keyvaluefiltering - `_. Default value is None. + `https://aka.ms/azconfig/docs/restapirevisions + `_. Default value is None. :paramtype tags: list[str] - :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is - None. - :paramtype etag: str - :keyword match_condition: The match condition to use upon the etag. Default value is None. - :paramtype match_condition: ~azure.core.MatchConditions :return: bool :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: @@ -2479,12 +2308,6 @@ def check_feature_flags( 409: ResourceExistsError, 304: ResourceNotModifiedError, } - if match_condition == MatchConditions.IfNotModified: - error_map[412] = ResourceModifiedError - elif match_condition == MatchConditions.IfPresent: - error_map[412] = ResourceNotFoundError - elif match_condition == MatchConditions.IfMissing: - error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) _headers = kwargs.pop("headers", {}) or {} @@ -2492,16 +2315,12 @@ def check_feature_flags( cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_feature_flags_request( + _request = build_feature_flag_client_check_feature_flag_revisions_request( name=name, label=label, - sync_token=sync_token, after=after, - accept_datetime=accept_datetime, select=select, tags=tags, - etag=etag, - match_condition=match_condition, api_version=self._config.api_version, headers=_headers, params=_params, @@ -2534,57 +2353,409 @@ def check_feature_flags( return cls(pipeline_response, None, response_headers) # type: ignore return 200 <= response.status_code <= 299 + +class _AzureAppConfigurationClientOperationsMixin( # pylint: disable=too-many-public-methods + ClientMixinABC[PipelineClient[HttpRequest, HttpResponse], AzureAppConfigurationClientConfiguration] +): + @distributed_trace - @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "select", - "sync_token", - "accept_datetime", - "client_request_id", - "tags", - "accept", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], - ) - def get_feature_flag( + def get_keys( + self, + *, + name: Optional[str] = None, + after: Optional[str] = None, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + **kwargs: Any + ) -> ItemPaged["_models.Key"]: + """Gets a list of keys. + + Gets a list of keys. + + :keyword name: A filter for the name of the returned keys. Default value is None. + :paramtype name: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :return: An iterator like instance of Key + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.Key] + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[list[_models.Key]] = kwargs.pop("cls", None) + + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + def prepare_request(next_link=None): + if not next_link: + + _request = build_azure_app_configuration_get_keys_request( + name=name, + after=after, + sync_token=sync_token, + accept_datetime=accept_datetime, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + else: + # make call to next link with the client's api-version + _parsed_next_link = urllib.parse.urlparse(next_link) + _next_request_params = case_insensitive_dict( + { + key: [urllib.parse.quote(v) for v in value] + for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() + } + ) + _next_request_params["api-version"] = self._config.api_version + _request = HttpRequest( + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + return _request + + def extract_data(pipeline_response): + deserialized = pipeline_response.http_response.json() + list_of_elem = _deserialize( + list[_models.Key], + deserialized.get("items", []), + ) + if cls: + list_of_elem = cls(list_of_elem) # type: ignore + return deserialized.get("@nextLink") or None, iter(list_of_elem) + + def get_next(next_link=None): + _request = prepare_request(next_link) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + return pipeline_response + + return ItemPaged(get_next, extract_data) + + @distributed_trace + def check_keys( + self, + *, + name: Optional[str] = None, + after: Optional[str] = None, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + **kwargs: Any + ) -> bool: + """Requests the headers and status of the given resource. + + Requests the headers and status of the given resource. + + :keyword name: A filter for the name of the returned keys. Default value is None. + :paramtype name: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :return: bool + :rtype: bool + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_azure_app_configuration_check_keys_request( + name=name, + after=after, + sync_token=sync_token, + accept_datetime=accept_datetime, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + response_headers = {} + response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) + + if cls: + return cls(pipeline_response, None, response_headers) # type: ignore + return 200 <= response.status_code <= 299 + + @distributed_trace + def get_key_values( + self, + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, + tags: Optional[list[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> ItemPaged["_models.KeyValue"]: + """Gets a list of key-values. + + Gets a list of key-values. + + :keyword key: A filter used to match keys. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype key: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of + the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. + :paramtype snapshot: str + :keyword tags: A filter used to query by tags. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :return: An iterator like instance of KeyValue + :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.KeyValue] + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[list[_models.KeyValue]] = kwargs.pop("cls", None) + + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + error_map.update(kwargs.pop("error_map", {}) or {}) + + def prepare_request(next_link=None): + if not next_link: + + _request = build_azure_app_configuration_get_key_values_request( + key=key, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + snapshot=snapshot, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + else: + # make call to next link with the client's api-version + _parsed_next_link = urllib.parse.urlparse(next_link) + _next_request_params = case_insensitive_dict( + { + key: [urllib.parse.quote(v) for v in value] + for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() + } + ) + _next_request_params["api-version"] = self._config.api_version + _request = HttpRequest( + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url( + "self._config.endpoint", self._config.endpoint, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + return _request + + def extract_data(pipeline_response): + deserialized = pipeline_response.http_response.json() + list_of_elem = _deserialize( + list[_models.KeyValue], + deserialized.get("items", []), + ) + if cls: + list_of_elem = cls(list_of_elem) # type: ignore + return deserialized.get("@nextLink") or None, iter(list_of_elem) + + def get_next(next_link=None): + _request = prepare_request(next_link) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.Error, + response, + ) + raise HttpResponseError(response=response, model=error) + + return pipeline_response + + return ItemPaged(get_next, extract_data) + + @distributed_trace + def check_key_values( self, - name: str, *, + key: Optional[str] = None, label: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, sync_token: Optional[str] = None, + after: Optional[str] = None, accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.FeatureFlag: - """Gets a single feature flag. + ) -> bool: + """Requests the headers and status of the given resource. - Gets a single feature flag. + Requests the headers and status of the given resource. - :param name: The name of the feature flag. Required. - :type name: str - :keyword label: The label of the feature flag to retrieve. Default value is None. + :keyword key: A filter used to match keys. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. + :paramtype key: str + :keyword label: A filter used to match labels. Syntax reference: + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. :paramtype label: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str :keyword accept_datetime: Requests the server to respond with the state of the resource at the specified time. Default value is None. :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of + the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. + :paramtype snapshot: str :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -2594,8 +2765,8 @@ def get_feature_flag( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.FeatureFlag + :return: bool + :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -2615,14 +2786,16 @@ def get_feature_flag( _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) + cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_get_feature_flag_request( - name=name, + _request = build_azure_app_configuration_check_key_values_request( + key=key, label=label, - select=select, sync_token=sync_token, + after=after, accept_datetime=accept_datetime, + select=select, + snapshot=snapshot, tags=tags, etag=etag, match_condition=match_condition, @@ -2635,8 +2808,7 @@ def get_feature_flag( } _request.url = self._client.format_url(_request.url, **path_format_arguments) - _decompress = kwargs.pop("decompress", True) - _stream = kwargs.pop("stream", False) + _stream = False pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs ) @@ -2644,11 +2816,6 @@ def get_feature_flag( response = pipeline_response.http_response if response.status_code not in [200]: - if _stream: - try: - response.read() # Load the body in memory and close the socket - except (StreamConsumedError, StreamClosedError): - pass map_error(status_code=response.status_code, response=response, error_map=error_map) error = _failsafe_deserialize( _models.Error, @@ -2657,61 +2824,42 @@ def get_feature_flag( raise HttpResponseError(response=response, model=error) response_headers = {} - response_headers["x-ms-request-id"] = self._deserialize("str", response.headers.get("x-ms-request-id")) response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) - response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) - - if _stream: - deserialized = response.iter_bytes() if _decompress else response.iter_raw() - else: - deserialized = _deserialize(_models.FeatureFlag, response.json()) if cls: - return cls(pipeline_response, deserialized, response_headers) # type: ignore - - return deserialized # type: ignore + return cls(pipeline_response, None, response_headers) # type: ignore + return 200 <= response.status_code <= 299 @distributed_trace @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "sync_token", - "accept_datetime", - "select", - "tags", - "client_request_id", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], + params_added_on={"2024-09-01": ["tags"]}, + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], ) - def check_feature_flag( + def get_key_value( self, - name: str, + key: str, *, label: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, sync_token: Optional[str] = None, accept_datetime: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> bool: - """Requests the headers and status of the given resource. + ) -> _models.KeyValue: + """Gets a single key-value. - Requests the headers and status of the given resource. + Gets a single key-value. - :param name: The name of the feature flag to retrieve. Required. - :type name: str - :keyword label: The label of the feature flag to retrieve. Default value is None. + :param key: The key of the key-value. Required. + :type key: str + :keyword label: The label of the key-value to retrieve. Default value is None. :paramtype label: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. :paramtype sync_token: str @@ -2719,9 +2867,6 @@ def check_feature_flag( specified time. Default value is None. :paramtype accept_datetime: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] :keyword tags: A filter used to query by tags. Syntax reference: `https://aka.ms/azconfig/docs/keyvaluefiltering `_. Default value is None. @@ -2731,8 +2876,8 @@ def check_feature_flag( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: bool - :rtype: bool + :return: KeyValue. The KeyValue is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.KeyValue :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -2752,14 +2897,14 @@ def check_feature_flag( _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[None] = kwargs.pop("cls", None) + cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_feature_flag_request( - name=name, + _request = build_azure_app_configuration_get_key_value_request( + key=key, label=label, + select=select, sync_token=sync_token, accept_datetime=accept_datetime, - select=select, tags=tags, etag=etag, match_condition=match_condition, @@ -2772,7 +2917,8 @@ def check_feature_flag( } _request.url = self._client.format_url(_request.url, **path_format_arguments) - _stream = False + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs ) @@ -2780,6 +2926,11 @@ def check_feature_flag( response = pipeline_response.http_response if response.status_code not in [200]: + if _stream: + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass map_error(status_code=response.status_code, response=response, error_map=error_map) error = _failsafe_deserialize( _models.Error, @@ -2788,51 +2939,83 @@ def check_feature_flag( raise HttpResponseError(response=response, model=error) response_headers = {} + response_headers["x-ms-request-id"] = self._deserialize("str", response.headers.get("x-ms-request-id")) response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag")) + response_headers["Content-Type"] = self._deserialize("str", response.headers.get("Content-Type")) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.KeyValue, response.json()) if cls: - return cls(pipeline_response, None, response_headers) # type: ignore - return 200 <= response.status_code <= 299 + return cls(pipeline_response, deserialized, response_headers) # type: ignore + + return deserialized # type: ignore + + @overload + def _put_key_value( + self, + key: str, + entity: Optional[_models.KeyValue] = None, + *, + content_type: str, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: ... + @overload + def _put_key_value( + self, + key: str, + entity: Optional[JSON] = None, + *, + content_type: str, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: ... + @overload + def _put_key_value( + self, + key: str, + entity: Optional[IO[bytes]] = None, + *, + content_type: str, + label: Optional[str] = None, + sync_token: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + **kwargs: Any + ) -> _models.KeyValue: ... @distributed_trace - @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "content_type", - "name", - "label", - "sync_token", - "client_request_id", - "accept", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], - ) - def put_feature_flag( + def _put_key_value( self, - name: str, - entity: Optional[_models.FeatureFlag] = None, + key: str, + entity: Optional[Union[_models.KeyValue, JSON, IO[bytes]]] = None, *, label: Optional[str] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> _models.FeatureFlag: - """Creates a feature flag. + ) -> _models.KeyValue: + """Creates a key-value. - Creates a feature flag. + Creates a key-value. - :param name: The name of the feature flag to create. Required. - :type name: str - :param entity: The feature flag to create. Default value is None. - :type entity: ~azure.appconfiguration._generated.models.FeatureFlag - :keyword label: The label of the feature flag to create. Default value is None. + :param key: The key of the key-value to create. Required. + :type key: str + :param entity: The key-value to create. Is one of the following types: KeyValue, JSON, + IO[bytes] Default value is None. + :type entity: ~azure.appconfiguration._generated.models.KeyValue or JSON or IO[bytes] + :keyword label: The label of the key-value to create. Default value is None. :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. @@ -2842,8 +3025,8 @@ def put_feature_flag( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: FeatureFlag. The FeatureFlag is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.FeatureFlag + :return: KeyValue. The KeyValue is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.KeyValue :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -2863,22 +3046,22 @@ def put_feature_flag( _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = kwargs.pop("params", {}) or {} - content_type: Optional[str] = kwargs.pop( - "content_type", - _headers.pop( - "Content-Type", 'application/json;profile="https://azconfig.io/mime-profiles/ff";charset=utf-8' - ), - ) + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) content_type = content_type if entity else None - cls: ClsType[_models.FeatureFlag] = kwargs.pop("cls", None) + cls: ClsType[_models.KeyValue] = kwargs.pop("cls", None) - if entity is not None: - _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + content_type = content_type or "application/json" if entity else None + _content = None + if isinstance(entity, (IOBase, bytes)): + _content = entity else: - _content = None + if entity is not None: + _content = json.dumps(entity, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + else: + _content = None - _request = build_azure_app_configuration_put_feature_flag_request( - name=name, + _request = build_azure_app_configuration_put_key_value_request( + key=key, label=label, sync_token=sync_token, etag=etag, @@ -2923,7 +3106,7 @@ def put_feature_flag( if _stream: deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.FeatureFlag, response.json()) + deserialized = _deserialize(_models.KeyValue, response.json()) if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore @@ -2931,39 +3114,23 @@ def put_feature_flag( return deserialized # type: ignore @distributed_trace - @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "sync_token", - "client_request_id", - "accept", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], - ) - def delete_feature_flag( + def delete_key_value( self, - name: str, + key: str, *, label: Optional[str] = None, sync_token: Optional[str] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> Optional[_models.FeatureFlag]: - """Deletes a feature flag. + ) -> Optional[_models.KeyValue]: + """Deletes a key-value. - Deletes a feature flag. + Deletes a key-value. - :param name: The name of the feature flag to delete. Required. - :type name: str - :keyword label: The label of the feature flag to delete. Default value is None. + :param key: The key of the key-value to delete. Required. + :type key: str + :keyword label: The label of the key-value to delete. Default value is None. :paramtype label: str :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. @@ -2973,8 +3140,8 @@ def delete_feature_flag( :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: FeatureFlag or None. The FeatureFlag is compatible with MutableMapping - :rtype: ~azure.appconfiguration._generated.models.FeatureFlag or None + :return: KeyValue or None. The KeyValue is compatible with MutableMapping + :rtype: ~azure.appconfiguration._generated.models.KeyValue or None :raises ~azure.core.exceptions.HttpResponseError: """ error_map: MutableMapping = { @@ -2994,10 +3161,10 @@ def delete_feature_flag( _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} - cls: ClsType[Optional[_models.FeatureFlag]] = kwargs.pop("cls", None) + cls: ClsType[Optional[_models.KeyValue]] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_delete_feature_flag_request( - name=name, + _request = build_azure_app_configuration_delete_key_value_request( + key=key, label=label, sync_token=sync_token, etag=etag, @@ -3042,7 +3209,7 @@ def delete_feature_flag( if _stream: deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: - deserialized = _deserialize(_models.FeatureFlag, response.json()) + deserialized = _deserialize(_models.KeyValue, response.json()) if response.status_code == 204: response_headers["Sync-Token"] = self._deserialize("str", response.headers.get("Sync-Token")) @@ -3054,74 +3221,53 @@ def delete_feature_flag( @distributed_trace @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": [ - "api_version", - "name", - "label", - "after", - "select", - "tags", - "sync_token", - "client_request_id", - "accept", - "etag", - "match_condition", - ] - }, - api_versions_list=["2026-05-01-preview"], + params_added_on={"2024-09-01": ["tags"]}, + api_versions_list=["2023-11-01", "2024-09-01", "2026-04-01", "2026-05-01-preview"], ) - def get_feature_flag_revisions( + def check_key_value( self, + key: str, *, - name: Optional[str] = None, label: Optional[str] = None, - after: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, - tags: Optional[list[str]] = None, sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[list[Union[str, _models.ConfigurationSettingFields]]] = None, + tags: Optional[list[str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, **kwargs: Any - ) -> ItemPaged["_models.FeatureFlag"]: - """Gets a list of feature flag revisions. + ) -> bool: + """Requests the headers and status of the given resource. - Gets a list of feature flag revisions. + Requests the headers and status of the given resource. - :keyword name: A filter used to match names. Default value is None. - :paramtype name: str - :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/restapirevisions - `_. Default value is None. + :param key: The key of the key-value to retrieve. Required. + :type key: str + :keyword label: The label of the key-value to retrieve. Default value is None. :paramtype label: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/restapirevisions - `_. Default value is None. + `https://aka.ms/azconfig/docs/keyvaluefiltering + `_. Default value is None. :paramtype tags: list[str] - :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is - None. - :paramtype sync_token: str :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is None. :paramtype etag: str :keyword match_condition: The match condition to use upon the etag. Default value is None. :paramtype match_condition: ~azure.core.MatchConditions - :return: An iterator like instance of FeatureFlag - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration._generated.models.FeatureFlag] + :return: bool + :rtype: bool :raises ~azure.core.exceptions.HttpResponseError: """ - _headers = kwargs.pop("headers", {}) or {} - _params = kwargs.pop("params", {}) or {} - - cls: ClsType[list[_models.FeatureFlag]] = kwargs.pop("cls", None) - error_map: MutableMapping = { 401: ClientAuthenticationError, 404: ResourceNotFoundError, @@ -3136,146 +3282,20 @@ def get_feature_flag_revisions( error_map[412] = ResourceExistsError error_map.update(kwargs.pop("error_map", {}) or {}) - def prepare_request(next_link=None): - if not next_link: - - _request = build_azure_app_configuration_get_feature_flag_revisions_request( - name=name, - label=label, - after=after, - select=select, - tags=tags, - sync_token=sync_token, - etag=etag, - match_condition=match_condition, - api_version=self._config.api_version, - headers=_headers, - params=_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url( - "self._config.endpoint", self._config.endpoint, "str", skip_quote=True - ), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - else: - # make call to next link with the client's api-version - _parsed_next_link = urllib.parse.urlparse(next_link) - _next_request_params = case_insensitive_dict( - { - key: [urllib.parse.quote(v) for v in value] - for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() - } - ) - _next_request_params["api-version"] = self._config.api_version - _request = HttpRequest( - "GET", - urllib.parse.urljoin(next_link, _parsed_next_link.path), - headers=_headers, - params=_next_request_params, - ) - path_format_arguments = { - "endpoint": self._serialize.url( - "self._config.endpoint", self._config.endpoint, "str", skip_quote=True - ), - } - _request.url = self._client.format_url(_request.url, **path_format_arguments) - - return _request - - def extract_data(pipeline_response): - deserialized = pipeline_response.http_response.json() - list_of_elem = _deserialize( - list[_models.FeatureFlag], - deserialized.get("items", []), - ) - if cls: - list_of_elem = cls(list_of_elem) # type: ignore - return deserialized.get("@nextLink") or None, iter(list_of_elem) - - def get_next(next_link=None): - _request = prepare_request(next_link) - - _stream = False - pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access - _request, stream=_stream, **kwargs - ) - response = pipeline_response.http_response - - if response.status_code not in [200]: - map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize( - _models.Error, - response, - ) - raise HttpResponseError(response=response, model=error) - - return pipeline_response - - return ItemPaged(get_next, extract_data) - - @distributed_trace - @api_version_validation( - method_added_on="2026-05-01-preview", - params_added_on={ - "2026-05-01-preview": ["api_version", "name", "label", "after", "select", "tags", "client_request_id"] - }, - api_versions_list=["2026-05-01-preview"], - ) - def check_feature_flag_revisions( - self, - *, - name: Optional[str] = None, - label: Optional[str] = None, - after: Optional[str] = None, - select: Optional[list[Union[str, _models.FeatureFlagFields]]] = None, - tags: Optional[list[str]] = None, - **kwargs: Any - ) -> bool: - """Requests the headers and status of the given resource. - - Requests the headers and status of the given resource. - - :keyword name: A filter used to match names. Default value is None. - :paramtype name: str - :keyword label: A filter used to match labels. Syntax reference: - `https://aka.ms/azconfig/docs/restapirevisions - `_. Default value is None. - :paramtype label: str - :keyword after: Instructs the server to return elements that appear after the element referred - to by the specified token. Default value is None. - :paramtype after: str - :keyword select: Used to select what fields are present in the returned resource(s). Default - value is None. - :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] - :keyword tags: A filter used to query by tags. Syntax reference: - `https://aka.ms/azconfig/docs/restapirevisions - `_. Default value is None. - :paramtype tags: list[str] - :return: bool - :rtype: bool - :raises ~azure.core.exceptions.HttpResponseError: - """ - error_map: MutableMapping = { - 401: ClientAuthenticationError, - 404: ResourceNotFoundError, - 409: ResourceExistsError, - 304: ResourceNotModifiedError, - } - error_map.update(kwargs.pop("error_map", {}) or {}) - _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} cls: ClsType[None] = kwargs.pop("cls", None) - _request = build_azure_app_configuration_check_feature_flag_revisions_request( - name=name, + _request = build_azure_app_configuration_check_key_value_request( + key=key, label=label, - after=after, + sync_token=sync_token, + accept_datetime=accept_datetime, select=select, tags=tags, + etag=etag, + match_condition=match_condition, api_version=self._config.api_version, headers=_headers, params=_params, diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_patch.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_patch.py new file mode 100644 index 000000000000..e5fd5ebe5f28 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_patch.py @@ -0,0 +1,367 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + + +"""Customize generated code here. + +Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize +""" + +import json +import urllib.parse +from typing import Any, List, Optional, Union, MutableMapping, Type +from azure.core import MatchConditions +from azure.core.exceptions import ( + ClientAuthenticationError, + HttpResponseError, + ResourceExistsError, + ResourceNotFoundError, + ResourceNotModifiedError, + ResourceModifiedError, + map_error, +) +from azure.core.pipeline import PipelineResponse +from azure.core.rest import HttpRequest +from azure.core.tracing.decorator import distributed_trace +from azure.core.utils import case_insensitive_dict +from ._operations import ( + _AzureAppConfigurationClientOperationsMixin as AzureAppConfigClientOpGenerated, + ClsType, + build_azure_app_configuration_get_key_values_request, + build_azure_app_configuration_check_key_values_request, + prep_if_match, + prep_if_none_match, +) +from .. import models as _models +from .._utils.model_base import _deserialize +from .._utils.serialization import Serializer + +_SERIALIZER = Serializer() +_SERIALIZER.client_side_validation = False + + +class _AzureAppConfigurationClientOperationsMixin(AzureAppConfigClientOpGenerated): + def _build_kv_error_map( + self, + match_condition: Optional[MatchConditions], + ) -> MutableMapping[int, Type[HttpResponseError]]: + """Build an error map for key-value operations.""" + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 401: ClientAuthenticationError, + 403: HttpResponseError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + if match_condition == MatchConditions.IfNotModified: + error_map[412] = ResourceModifiedError + elif match_condition == MatchConditions.IfPresent: + error_map[412] = ResourceNotFoundError + elif match_condition == MatchConditions.IfMissing: + error_map[412] = ResourceExistsError + return error_map + + def _prepare_continuation_request( + self, + http_method: str, + continuation_token: str, + *, + sync_token: Optional[str] = None, + accept_datetime: Optional[str] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + headers: Optional[dict] = None, + ) -> HttpRequest: + """Build a continuation request from a pagination token.""" + _headers = headers or {} + + _parsed_next_link = urllib.parse.urlparse(continuation_token) + _next_request_params = case_insensitive_dict( + { + key: [urllib.parse.quote(v) for v in value] + for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items() + } + ) + _next_request_params["api-version"] = self._config.api_version + + _next_headers = dict(_headers) + accept = _headers.pop("Accept", None) + if sync_token is not None: + _next_headers["Sync-Token"] = _SERIALIZER.header("sync_token", sync_token, "str") + if accept_datetime is not None: + _next_headers["Accept-Datetime"] = _SERIALIZER.header("accept_datetime", accept_datetime, "str") + if accept is not None: + _next_headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + if_match = prep_if_match(etag, match_condition) + if if_match is not None: + _next_headers["If-Match"] = _SERIALIZER.header("if_match", if_match, "str") + if_none_match = prep_if_none_match(etag, match_condition) + if if_none_match is not None: + _next_headers["If-None-Match"] = _SERIALIZER.header("if_none_match", if_none_match, "str") + _request = HttpRequest( + http_method, + urllib.parse.urljoin(continuation_token, _parsed_next_link.path), + params=_next_request_params, + headers=_next_headers, + ) + return self._format_request(_request) + + def _format_request(self, _request: HttpRequest) -> HttpRequest: + """Apply endpoint formatting to a request URL.""" + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + return _request + + @distributed_trace + def get_key_values_in_one_page( + self, + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[List[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, + tags: Optional[List[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + continuation_token: Optional[str] = None, + **kwargs: Any, + ) -> dict: + """Gets a list of key-values in one page. + + Gets a list of key-values in one page. + + :keyword key: A filter used to match keys. Syntax reference: + https://aka.ms/azconfig/docs/keyvaluefiltering. Default value is None. + :paramtype key: str + :keyword label: A filter used to match labels. Syntax reference: + https://aka.ms/azconfig/docs/keyvaluefiltering. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified + time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword snapshot: A filter used get key-values for a snapshot. The value should be the name of + the snapshot. Not valid when used with 'key' and 'label' filters. Default value is None. + :paramtype snapshot: str + :keyword tags: A filter used to query by tags. Syntax reference: + https://aka.ms/azconfig/docs/keyvaluefiltering. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :param str continuation_token: An opaque continuation token. + :return: A dict containing ``items`` (list of key-values) and ``@nextLink`` (pagination URL or None). + :rtype: dict + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[List[_models.KeyValue]] = kwargs.pop("cls", None) + + error_map = self._build_kv_error_map(match_condition) + error_map.update(kwargs.pop("error_map", {}) or {}) + + if continuation_token: + _request = self._prepare_continuation_request( + "GET", + continuation_token, + sync_token=sync_token, + accept_datetime=accept_datetime, + etag=etag, + match_condition=match_condition, + headers=_headers, + ) + else: + _request = build_azure_app_configuration_get_key_values_request( + key=key, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + snapshot=snapshot, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + _request = self._format_request(_request) + + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=False, **kwargs + ) + response = pipeline_response.http_response + + valid_status_codes = [200] + if etag is not None and match_condition is MatchConditions.IfModified: + valid_status_codes.append(304) + + if response.status_code not in valid_status_codes: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = None + try: + error = _deserialize(_models.Error, response.json()) + except (ValueError, KeyError): + pass + raise HttpResponseError(response=response, model=error) + + response_headers = response.headers + result = json.loads("{}") + if response.status_code != 304: + result = pipeline_response.http_response.json() + else: + unparsed_link = pipeline_response.http_response.headers.get("Link") + next_link = None + if unparsed_link: + next_link = unparsed_link[1 : unparsed_link.index(">")] + result["@nextLink"] = next_link + + if cls: + return cls(pipeline_response, result, response_headers) + + return result + + @distributed_trace + def check_key_values_in_one_page( + self, + *, + key: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[List[Union[str, _models.ConfigurationSettingFields]]] = None, + snapshot: Optional[str] = None, + tags: Optional[List[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + continuation_token: Optional[str] = None, + **kwargs: Any, + ) -> dict: + """Checks key-values using HEAD request, returning only headers without the response body. + + :keyword key: A filter used to match keys. Default value is None. + :paramtype key: str + :keyword label: A filter used to match labels. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.ConfigurationSettingFields] + :keyword snapshot: A filter used get key-values for a snapshot. Default value is None. + :paramtype snapshot: str + :keyword tags: A filter used to query by tags. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :param str continuation_token: An opaque continuation token. + :return: A dict containing ``items`` (empty list) and ``@nextLink`` (pagination URL or None). + :rtype: dict + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[List[_models.KeyValue]] = kwargs.pop("cls", None) + + error_map = self._build_kv_error_map(match_condition) + error_map.update(kwargs.pop("error_map", {}) or {}) + + if continuation_token: + _request = self._prepare_continuation_request( + "HEAD", + continuation_token, + sync_token=sync_token, + accept_datetime=accept_datetime, + etag=etag, + match_condition=match_condition, + headers=_headers, + ) + else: + _request = build_azure_app_configuration_check_key_values_request( + key=key, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + snapshot=snapshot, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + _request = self._format_request(_request) + + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=False, **kwargs + ) + response = pipeline_response.http_response + + valid_status_codes = [200] + if etag is not None and match_condition == MatchConditions.IfModified: + valid_status_codes.append(304) + + if response.status_code not in valid_status_codes: + map_error(status_code=response.status_code, response=response, error_map=error_map) + raise HttpResponseError(response=response) + + response_headers = response.headers + result = {} + unparsed_link = response_headers.get("Link") + next_link = None + if unparsed_link: + next_link = unparsed_link[1 : unparsed_link.index(">")] + result["@nextLink"] = next_link + if response.status_code != 304: + result["items"] = [] + + if cls: + return cls(pipeline_response, result, response_headers) + + return result + + +__all__: List[str] = [ + "_AzureAppConfigurationClientOperationsMixin" +] # Add all objects you want publicly available to users at this package level + + +def patch_sdk(): + """Do not remove from this file. + + `patch_sdk` is a last resort escape hatch that allows you to do customizations + you can't accomplish using the techniques described in + https://aka.ms/azsdk/python/dpcodegen/python/customize + """ diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py index 0a2c8584e841..f86937c0d13b 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py @@ -568,26 +568,26 @@ class PercentileAllocation: :ivar variant: The variant to allocate these percentiles to. Required. :vartype variant: str :ivar percentile_from: The lower bounds for this percentile allocation. Required. - :vartype percentile_from: int + :vartype percentile_from: float :ivar percentile_to: The upper bounds for this percentile allocation. Required. - :vartype percentile_to: int + :vartype percentile_to: float """ variant: str """The variant to allocate these percentiles to. Required.""" - percentile_from: int + percentile_from: float """The lower bounds for this percentile allocation. Required.""" - percentile_to: int + percentile_to: float """The upper bounds for this percentile allocation. Required.""" - def __init__(self, *, variant: str, percentile_from: int, percentile_to: int) -> None: + def __init__(self, *, variant: str, percentile_from: float, percentile_to: float) -> None: """ :keyword variant: The variant to allocate these percentiles to. Required. :paramtype variant: str :keyword percentile_from: The lower bounds for this percentile allocation. Required. - :paramtype percentile_from: int + :paramtype percentile_from: float :keyword percentile_to: The upper bounds for this percentile allocation. Required. - :paramtype percentile_to: int + :paramtype percentile_to: float """ self.variant = variant self.percentile_from = percentile_from diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_version.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_version.py index 8b963ff4f81a..d9b0b51e4597 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_version.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "1.9.1" +VERSION = "1.10.0b1" diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/__init__.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/__init__.py index 243f403a5362..fd3322f1402d 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/__init__.py @@ -10,5 +10,6 @@ # -------------------------------------------------------------------------- from ._azure_appconfiguration_client_async import AzureAppConfigurationClient +from ._feature_flag_client_async import FeatureFlagClient -__all__ = ["AzureAppConfigurationClient"] +__all__ = ["AzureAppConfigurationClient", "FeatureFlagClient"] diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py index 55b4d615b7d6..662f2768e255 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py @@ -15,7 +15,7 @@ from azure.core.polling import AsyncLROPoller from azure.core.tracing.decorator import distributed_trace from azure.core.tracing.decorator_async import distributed_trace_async -from azure.core.exceptions import ResourceNotFoundError, ResourceNotModifiedError +from azure.core.exceptions import ResourceNotModifiedError from azure.core.rest import AsyncHttpResponse, HttpRequest from ._sync_token_async import AsyncSyncTokenPolicy from .._azure_appconfiguration_error import ResourceReadOnlyError @@ -37,7 +37,6 @@ ConfigurationSettingsFilter, ConfigurationSnapshot, ConfigurationSettingLabel, - FeatureFlag, ) from .._audience import get_audience, DEFAULT_SCOPE_SUFFIX from .._utils import ( @@ -852,221 +851,6 @@ def list_snapshots( **kwargs, ) - @distributed_trace - def list_feature_flags( - self, - name_filter: Optional[str] = None, - label_filter: Optional[str] = None, - *, - tags_filter: Optional[List[str]] = None, - accept_datetime: Optional[Union[datetime, str]] = None, - **kwargs: Any, - ) -> AsyncItemPaged["FeatureFlag"]: - """ - Find the FeatureFlag objects, optionally filtered by name, label, tags and accept_datetime. - - :param name_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end - of the filter. Default is `None`. - :type name_filter: str or None - :param label_filter: Filter results based on their labels. Default is `None`. - :type label_filter: str or None - :keyword tags_filter: Filter results based on their tags. Default is `None`. - :paramtype tags_filter: list[str] or None - :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime - :paramtype accept_datetime: ~datetime.datetime or str or None - :return: An async iterator of :class:`~azure.appconfiguration.FeatureFlag` - :rtype: ~azure.core.paging.AsyncItemPaged[~azure.appconfiguration.FeatureFlag] - :raises: :class:`~azure.core.exceptions.HttpResponseError` - """ - if isinstance(accept_datetime, datetime): - accept_datetime = str(accept_datetime) - - # Call the generated method and wrap results to convert to SDK FeatureFlag - def convert_to_sdk(objs): - return [FeatureFlag._from_generated(obj) for obj in objs] - - return self._impl.get_feature_flags( # type: ignore[return-value] - name=name_filter, - label=label_filter, - accept_datetime=accept_datetime, - tags=tags_filter, - cls=convert_to_sdk, - **kwargs, - ) - - @distributed_trace_async - async def get_feature_flag( - self, - feature_id: str, - label: Optional[str] = None, - *, - etag: Optional[str] = None, - match_condition: MatchConditions = MatchConditions.Unconditionally, - accept_datetime: Optional[Union[datetime, str]] = None, - **kwargs: Any, - ) -> Optional["FeatureFlag"]: - """ - Get a feature flag from the service, identified by `feature_id` and optionally `label`. - - :param feature_id: The feature flag identifier. - :type feature_id: str - :param label: The label of the feature flag. Defaults to None. - :type label: str or None - :keyword etag: Check if the feature flag is changed. Set None to skip checking etag. - :paramtype etag: str or None - :keyword match_condition: The match condition to use upon the etag. - :paramtype match_condition: ~azure.core.MatchConditions - :keyword accept_datetime: Retrieve the FeatureFlag that existed at this datetime. - :paramtype accept_datetime: ~datetime.datetime or str or None - :return: The FeatureFlag if found; None otherwise. - :rtype: ~azure.appconfiguration.FeatureFlag or None - :raises: :class:`~azure.core.exceptions.HttpResponseError` - """ - if isinstance(accept_datetime, datetime): - accept_datetime = str(accept_datetime) - - try: - generated_feature_flag = await self._impl.get_feature_flag( - name=feature_id, - label=label, - etag=etag, - match_condition=match_condition, - accept_datetime=accept_datetime, - **kwargs, - ) - return FeatureFlag._from_generated(generated_feature_flag) - except (ResourceNotFoundError, ResourceNotModifiedError): - return None - - @distributed_trace_async - async def set_feature_flag( - self, - feature_flag: "FeatureFlag", - match_condition: MatchConditions = MatchConditions.Unconditionally, - *, - etag: Optional[str] = None, - **kwargs: Any, - ) -> "FeatureFlag": - """ - Create or update a feature flag via the dedicated feature flag endpoint. - - :param feature_flag: A FeatureFlag object. - :type feature_flag: ~azure.appconfiguration.FeatureFlag - :param match_condition: The match condition to use upon the etag. - :type match_condition: ~azure.core.MatchConditions - :keyword etag: The etag of the feature flag. If provided, the feature flag will be updated only if the etag matches. - :paramtype etag: str or None - :return: The updated FeatureFlag. - :rtype: ~azure.appconfiguration.FeatureFlag - :raises: :class:`~azure.core.exceptions.HttpResponseError` - """ - error_map: Dict[int, Any] = {409: ResourceReadOnlyError} - - if etag is None: - etag = feature_flag.etag - - generated_feature_flag = feature_flag._to_generated() - result = await self._impl.put_feature_flag( - name=feature_flag.name, - entity=generated_feature_flag, - label=feature_flag.label, - etag=etag, - match_condition=match_condition, - error_map=error_map, - **kwargs, - ) - - return FeatureFlag._from_generated(result) - - @distributed_trace_async - async def delete_feature_flag( - self, - feature_id: str, - label: Optional[str] = None, - *, - etag: Optional[str] = None, - match_condition: MatchConditions = MatchConditions.Unconditionally, - **kwargs: Any, - ) -> Union[None, "FeatureFlag"]: - """Delete a FeatureFlag if it exists - - :param feature_id: The feature flag name/id to delete - :type feature_id: str - :param label: Label used to identify the FeatureFlag. Default is `None`. - :type label: str or None - :keyword etag: Check if the FeatureFlag is changed. Set None to skip checking etag - :paramtype etag: str or None - :keyword match_condition: The match condition to use upon the etag - :paramtype match_condition: ~azure.core.MatchConditions - :return: The deleted FeatureFlag returned from the service, or None if it doesn't exist. - :rtype: ~azure.appconfiguration.FeatureFlag or None - :raises: :class:`~azure.appconfiguration.ResourceReadOnlyError`, \ - :class:`~azure.core.exceptions.HttpResponseError`, \ - :class:`~azure.core.exceptions.ClientAuthenticationError`, \ - :class:`~azure.core.exceptions.ResourceModifiedError`, \ - :class:`~azure.core.exceptions.ResourceNotModifiedError`, \ - :class:`~azure.core.exceptions.ResourceNotFoundError`, \ - :class:`~azure.core.exceptions.ResourceExistsError` - - Example - - .. code-block:: python - - # in async function - deleted_feature_flag = await async_client.delete_feature_flag(feature_id="MyFeatureFlag") - """ - error_map: Dict[int, Any] = {409: ResourceReadOnlyError} - generated_feature_flag = await self._impl.delete_feature_flag( - name=feature_id, - label=label, - etag=etag, - match_condition=match_condition, - error_map=error_map, - **kwargs, - ) - if generated_feature_flag: - return FeatureFlag._from_generated(generated_feature_flag) - return None - - @distributed_trace - def list_feature_flag_revisions( - self, - feature_id_filter: Optional[str] = None, - label_filter: Optional[str] = None, - *, - accept_datetime: Optional[Union[datetime, str]] = None, - **kwargs: Any, - ) -> AsyncItemPaged["FeatureFlag"]: - """ - Find the FeatureFlag revision history, optionally filtered by feature_id and label. - - :param feature_id_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end - of the filter. Default is `None`. - :type feature_id_filter: str or None - :param label_filter: Filter results based on their labels. Default is `None`. - :type label_filter: str or None - :keyword accept_datetime: Retrieve FeatureFlag revisions that existed at this datetime - :paramtype accept_datetime: ~datetime.datetime or str or None - :return: An async iterator of :class:`~azure.appconfiguration.FeatureFlag` - :rtype: ~azure.core.paging.AsyncItemPaged[~azure.appconfiguration.FeatureFlag] - :raises: :class:`~azure.core.exceptions.HttpResponseError` - """ - if isinstance(accept_datetime, datetime): - accept_datetime = str(accept_datetime) - - # Build kwargs for the underlying call - impl_kwargs = dict(kwargs) - if accept_datetime is not None: - impl_kwargs["accept_datetime"] = accept_datetime - - # Call the generated method and wrap results to convert to SDK FeatureFlag - def convert_to_sdk(objs): - return [FeatureFlag._from_generated(obj) for obj in objs] - - return self._impl.get_feature_flag_revisions( # type: ignore[return-value] - name=feature_id_filter, label=label_filter, cls=convert_to_sdk, **impl_kwargs - ) - async def update_sync_token(self, token: str) -> None: """Add a sync token to the internal list of tokens. diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py new file mode 100644 index 000000000000..6398ae005c58 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py @@ -0,0 +1,453 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +from datetime import datetime +from typing import Any, Dict, List, Optional, Union +from azure.core import MatchConditions +from azure.core.async_paging import AsyncItemPaged +from azure.core.credentials import AzureKeyCredential +from azure.core.credentials_async import AsyncTokenCredential +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy +from azure.core.tracing.decorator import distributed_trace +from azure.core.tracing.decorator_async import distributed_trace_async +from azure.core.exceptions import ResourceNotFoundError, ResourceNotModifiedError +from azure.core.rest import AsyncHttpResponse, HttpRequest +from ._sync_token_async import AsyncSyncTokenPolicy +from .._azure_appconfiguration_error import ResourceReadOnlyError +from .._azure_appconfiguration_requests import AppConfigRequestsCredentialsPolicy +from .._query_param_policy import QueryParamPolicy +from .._generated.aio import AzureAppConfigurationClient as AzureAppConfigurationClientGenerated +from .._generated.models import LabelFields +from .._models import FeatureFlag, ConfigurationSettingLabel +from .._audience import get_audience, DEFAULT_SCOPE_SUFFIX +from .._utils import parse_connection_string +from .._audience_error_handling_policy import AudienceErrorHandlingPolicy + + +class FeatureFlagClient: + """Represents a client that manages feature flags in the Azure App Configuration service. + + :param str base_url: Base url of the service. + :param credential: An object which can provide secrets for the app configuration service + :type credential: ~azure.core.credentials_async.AsyncTokenCredential + :keyword api_version: Api Version. Default value is "2023-11-01". Note that overriding this default + value may result in unsupported behavior. + :paramtype api_version: str + :keyword audience: The audience to use for authentication with Microsoft Entra. Defaults to the public Azure App + Configuration audience. See the supported audience list at https://aka.ms/appconfig/client-token-audience + :paramtype audience: str + + + This is the async version of :class:`~azure.appconfiguration.FeatureFlagClient` + + """ + + # pylint:disable=protected-access + def __init__(self, base_url: str, credential: AsyncTokenCredential, **kwargs: Any) -> None: + try: + if not base_url.lower().startswith("http"): + base_url = f"https://{base_url}" + except AttributeError as exc: + raise ValueError("Base URL must be a string.") from exc + + if not credential: + raise ValueError("Missing credential") + + self._sync_token_policy = AsyncSyncTokenPolicy() + self._query_param_policy = QueryParamPolicy() + + audience = kwargs.pop("audience", None) + + audience_policy = AudienceErrorHandlingPolicy(bool(audience)) + per_call_policies = [self._query_param_policy, self._sync_token_policy, audience_policy] + + if audience is None: + audience = get_audience(base_url) + + # Ensure all scopes end with /.default and strip any trailing slashes before adding suffix + audience_scope = audience.rstrip("/") + "/" + DEFAULT_SCOPE_SUFFIX + kwargs["credential_scopes"] = [audience_scope] + + if isinstance(credential, AzureKeyCredential): + id_credential = kwargs.pop("id_credential") + kwargs.update( + { + "authentication_policy": AppConfigRequestsCredentialsPolicy(credential, base_url, id_credential), + } + ) + elif hasattr(credential, "get_token"): # AsyncFakeCredential is not an instance of AsyncTokenCredential + kwargs.update( + { + "authentication_policy": AsyncBearerTokenCredentialPolicy( + credential, *kwargs["credential_scopes"], **kwargs + ), + } + ) + else: + raise TypeError( + f"Unsupported credential: {type(credential)}. Use an instance of token credential from azure.identity" + ) + # mypy doesn't compare the credential type hint with the API surface in patch.py + self._impl = AzureAppConfigurationClientGenerated( + base_url, credential, per_call_policies=per_call_policies, **kwargs # type: ignore[arg-type] + ) + + @classmethod + def from_connection_string(cls, connection_string: str, **kwargs: Any) -> "FeatureFlagClient": + """Create FeatureFlagClient from a Connection String. + This is the async version of :class:`~azure.appconfiguration.FeatureFlagClient` + + :param str connection_string: Connection String + (one of the access keys of the Azure App Configuration resource) + used to access the Azure App Configuration. + :return: A FeatureFlagClient authenticated with the connection string + :rtype: ~azure.appconfiguration.aio.FeatureFlagClient + + Example + + .. code-block:: python + + from azure.appconfiguration.aio import FeatureFlagClient + + connection_str = "" + async_client = FeatureFlagClient.from_connection_string(connection_str) + """ + endpoint, id_credential, secret = parse_connection_string(connection_string) + # AzureKeyCredential type is for internal use, it's not exposed in public API. + return cls( + credential=AzureKeyCredential(secret), # type: ignore[arg-type] + base_url=endpoint, + id_credential=id_credential, + **kwargs, + ) + + @distributed_trace_async + async def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: Any) -> AsyncHttpResponse: + """Runs a network request using the client's existing pipeline. + + The request URL can be relative to the vault URL. The service API version used for the request is the same as + the client's unless otherwise specified. This method does not raise if the response is an error; to raise an + exception, call `raise_for_status()` on the returned response object. For more information about how to send + custom requests with this method, see https://aka.ms/azsdk/dpcodegen/python/send_request. + + :param request: The network request you want to make. + :type request: ~azure.core.rest.HttpRequest + :keyword bool stream: Whether the response payload will be streamed. Defaults to False. + :return: The response of your network call. Does not do error handling on your response. + :rtype: ~azure.core.rest.AsyncHttpResponse + """ + return await self._impl.send_request(request, stream=stream, **kwargs) + + @distributed_trace + def list_feature_flags( + self, + name_filter: Optional[str] = None, + label_filter: Optional[str] = None, + *, + tags_filter: Optional[List[str]] = None, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> AsyncItemPaged["FeatureFlag"]: + """ + Find the FeatureFlag objects, optionally filtered by name, label, tags and accept_datetime. + + :param name_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end + of the filter. Default is `None`. + :type name_filter: str or None + :param label_filter: Filter results based on their labels. Default is `None`. + :type label_filter: str or None + :keyword tags_filter: Filter results based on their tags. Default is `None`. + :paramtype tags_filter: list[str] or None + :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: An async iterator of :class:`~azure.appconfiguration.FeatureFlag` + :rtype: ~azure.core.paging.AsyncItemPaged[~azure.appconfiguration.FeatureFlag] + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + + # Call the generated method and wrap results to convert to SDK FeatureFlag + def convert_to_sdk(objs): + return [FeatureFlag._from_generated(obj) for obj in objs] # pylint:disable=protected-access + + return self._impl.feature_flag_client.get_feature_flags( # type: ignore[return-value] + name=name_filter, + label=label_filter, + accept_datetime=accept_datetime, + tags=tags_filter, + cls=convert_to_sdk, + **kwargs, + ) + + @distributed_trace_async + async def get_feature_flag( + self, + feature_id: str, + label: Optional[str] = None, + *, + etag: Optional[str] = None, + match_condition: MatchConditions = MatchConditions.Unconditionally, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> Optional["FeatureFlag"]: + """ + Get a feature flag from the service, identified by `feature_id` and optionally `label`. + + :param feature_id: The feature flag identifier. + :type feature_id: str + :param label: The label of the feature flag. Defaults to None. + :type label: str or None + :keyword etag: Check if the feature flag is changed. Set None to skip checking etag. + :paramtype etag: str or None + :keyword match_condition: The match condition to use upon the etag. + :paramtype match_condition: ~azure.core.MatchConditions + :keyword accept_datetime: Retrieve the FeatureFlag that existed at this datetime. + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: The FeatureFlag if found; None otherwise. + :rtype: ~azure.appconfiguration.FeatureFlag or None + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + + try: + generated_feature_flag = await self._impl.feature_flag_client.get_feature_flag( + name=feature_id, + label=label, + etag=etag, + match_condition=match_condition, + accept_datetime=accept_datetime, + **kwargs, + ) + return FeatureFlag._from_generated(generated_feature_flag) + except (ResourceNotFoundError, ResourceNotModifiedError): + return None + + @distributed_trace_async + async def add_feature_flag(self, feature_flag: "FeatureFlag", **kwargs: Any) -> "FeatureFlag": + """Add a FeatureFlag instance into the Azure App Configuration service. + + :param feature_flag: The FeatureFlag object to be added + :type feature_flag: ~azure.appconfiguration.FeatureFlag + :return: The FeatureFlag object returned from the App Configuration service + :rtype: ~azure.appconfiguration.FeatureFlag + :raises: :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError`, \ + :class:`~azure.core.exceptions.ResourceExistsError` + + Example + + .. code-block:: python + + feature_flag = FeatureFlag( + name="MyFeatureFlag", + enabled=True + ) + added_feature_flag = await async_client.add_feature_flag(feature_flag) + """ + generated_feature_flag = feature_flag._to_generated() + generated_result = await self._impl.feature_flag_client.put_feature_flag( + entity=generated_feature_flag, + name=feature_flag.name, + label=feature_flag.label, + match_condition=MatchConditions.IfMissing, + **kwargs, + ) + return FeatureFlag._from_generated(generated_result) + + @distributed_trace_async + async def set_feature_flag( + self, + feature_flag: "FeatureFlag", + match_condition: MatchConditions = MatchConditions.Unconditionally, + *, + etag: Optional[str] = None, + **kwargs: Any, + ) -> "FeatureFlag": + """ + Create or update a feature flag via the dedicated feature flag endpoint. + + :param feature_flag: A FeatureFlag object. + :type feature_flag: ~azure.appconfiguration.FeatureFlag + :param match_condition: The match condition to use upon the etag. + :type match_condition: ~azure.core.MatchConditions + :keyword etag: The etag of the feature flag. If provided, the feature flag will be updated only if the etag matches. + :paramtype etag: str or None + :return: The updated FeatureFlag. + :rtype: ~azure.appconfiguration.FeatureFlag + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + error_map: Dict[int, Any] = {409: ResourceReadOnlyError} + + if etag is None: + etag = feature_flag.etag + + generated_feature_flag = feature_flag._to_generated() + result = await self._impl.feature_flag_client.put_feature_flag( + name=feature_flag.name, + entity=generated_feature_flag, + label=feature_flag.label, + etag=etag, + match_condition=match_condition, + error_map=error_map, + **kwargs, + ) + + return FeatureFlag._from_generated(result) + + @distributed_trace_async + async def delete_feature_flag( + self, + feature_id: str, + label: Optional[str] = None, + *, + etag: Optional[str] = None, + match_condition: MatchConditions = MatchConditions.Unconditionally, + **kwargs: Any, + ) -> Union[None, "FeatureFlag"]: + """Delete a FeatureFlag if it exists + + :param feature_id: The feature flag name/id to delete + :type feature_id: str + :param label: Label used to identify the FeatureFlag. Default is `None`. + :type label: str or None + :keyword etag: Check if the FeatureFlag is changed. Set None to skip checking etag + :paramtype etag: str or None + :keyword match_condition: The match condition to use upon the etag + :paramtype match_condition: ~azure.core.MatchConditions + :return: The deleted FeatureFlag returned from the service, or None if it doesn't exist. + :rtype: ~azure.appconfiguration.FeatureFlag or None + :raises: :class:`~azure.appconfiguration.ResourceReadOnlyError`, \ + :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError`, \ + :class:`~azure.core.exceptions.ResourceModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotModifiedError`, \ + :class:`~azure.core.exceptions.ResourceNotFoundError`, \ + :class:`~azure.core.exceptions.ResourceExistsError` + + Example + + .. code-block:: python + + # in async function + deleted_feature_flag = await async_client.delete_feature_flag(feature_id="MyFeatureFlag") + """ + error_map: Dict[int, Any] = {409: ResourceReadOnlyError} + generated_feature_flag = await self._impl.feature_flag_client.delete_feature_flag( + name=feature_id, + label=label, + etag=etag, + match_condition=match_condition, + error_map=error_map, + **kwargs, + ) + if generated_feature_flag: + return FeatureFlag._from_generated(generated_feature_flag) + return None + + @distributed_trace + def list_feature_flag_revisions( + self, + feature_id_filter: Optional[str] = None, + label_filter: Optional[str] = None, + *, + accept_datetime: Optional[Union[datetime, str]] = None, + **kwargs: Any, + ) -> AsyncItemPaged["FeatureFlag"]: + """ + Find the FeatureFlag revision history, optionally filtered by feature_id and label. + + :param feature_id_filter: Filter results based on their feature flag names. '*' can be used as wildcard at the end + of the filter. Default is `None`. + :type feature_id_filter: str or None + :param label_filter: Filter results based on their labels. Default is `None`. + :type label_filter: str or None + :keyword accept_datetime: Retrieve FeatureFlag revisions that existed at this datetime + :paramtype accept_datetime: ~datetime.datetime or str or None + :return: An async iterator of :class:`~azure.appconfiguration.FeatureFlag` + :rtype: ~azure.core.paging.AsyncItemPaged[~azure.appconfiguration.FeatureFlag] + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + + # Build kwargs for the underlying call + impl_kwargs = dict(kwargs) + if accept_datetime is not None: + impl_kwargs["accept_datetime"] = accept_datetime + + # Call the generated method and wrap results to convert to SDK FeatureFlag + def convert_to_sdk(objs): + return [FeatureFlag._from_generated(obj) for obj in objs] # pylint:disable=protected-access + + return self._impl.feature_flag_client.get_feature_flag_revisions( # type: ignore[return-value] + name=feature_id_filter, label=label_filter, cls=convert_to_sdk, **impl_kwargs + ) + + @distributed_trace + def list_labels( + self, + *, + name: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[Union[datetime, str]] = None, + fields: Optional[List[Union[str, LabelFields]]] = None, + resource_type: Optional[str] = None, + **kwargs: Any, + ) -> AsyncItemPaged[ConfigurationSettingLabel]: + """Gets a list of labels. + + :keyword name: A filter for the name of the returned labels. '*' can be used as wildcard + in the beginning or end of the filter. For more information about supported filters, see + https://learn.microsoft.com/azure/azure-app-configuration/rest-api-labels?pivots=v23-11#supported-filters. + :paramtype name: str or None + :keyword after: Instructs the server to return elements that appear after the element referred to + by the specified token. + :paramtype after: str or None + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified time. + :paramtype accept_datetime: ~datetime.datetime or str or None + :keyword fields: Specify which fields to include in the results. If not specified, will include all fields. + Available fields see :class:`~azure.appconfiguration.LabelFields`. + :paramtype fields: list[str] or list[~azure.appconfiguration.LabelFields] or None + :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted values: + 'kv' for key-value labels, 'ff' for feature flag labels. Default is `None`. + :paramtype resource_type: str or None + :return: An async iterator of labels. + :rtype: ~azure.core.paging.AsyncItemPaged[~azure.appconfiguration.ConfigurationSettingLabel] + :raises: :class:`~azure.core.exceptions.HttpResponseError` + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + return self._impl.get_labels( # type: ignore[return-value] + name=name, + after=after, + accept_datetime=accept_datetime, + select=fields, + resource_type=resource_type, + cls=lambda objs: [ConfigurationSettingLabel(name=x.name) for x in objs], + **kwargs, + ) + + async def update_sync_token(self, token: str) -> None: + """Add a sync token to the internal list of tokens. + + :param str token: The sync token to be added to the internal list of tokens + """ + + await self._sync_token_policy.add_token(token) + + async def close(self) -> None: + """Close all connections made by the client""" + await self._impl._client.close() + + async def __aenter__(self) -> "FeatureFlagClient": + await self._impl.__aenter__() + return self + + async def __aexit__(self, *args: Any) -> None: + await self._impl.__aexit__(*args) diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py index 5967db91a58e..f0ecdc05fdbf 100644 --- a/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample.py @@ -23,7 +23,7 @@ import os from azure.identity import DefaultAzureCredential from azure.appconfiguration import ( - AzureAppConfigurationClient, + FeatureFlagClient, FeatureFlag, FeatureFlagConditions, FeatureFlagFilter, @@ -39,7 +39,7 @@ def main(): endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"] credential = DefaultAzureCredential() - client = AzureAppConfigurationClient(base_url=endpoint, credential=credential) + client = FeatureFlagClient(base_url=endpoint, credential=credential) print("Set a simple feature flag") # [START set_feature_flag] diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py index 528db08e3b95..0d252a857e23 100644 --- a/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/samples/feature_flag_sample_async.py @@ -23,7 +23,7 @@ import asyncio import os from azure.identity.aio import DefaultAzureCredential -from azure.appconfiguration.aio import AzureAppConfigurationClient +from azure.appconfiguration.aio import FeatureFlagClient from azure.appconfiguration import ( FeatureFlag, FeatureFlagConditions, @@ -40,7 +40,7 @@ async def main(): endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"] credential = DefaultAzureCredential() - client = AzureAppConfigurationClient(base_url=endpoint, credential=credential) + client = FeatureFlagClient(base_url=endpoint, credential=credential) print("Set a simple feature flag") # [START set_feature_flag_async] diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py index 715b959562d4..816767fb9ab2 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/asynctestcase.py @@ -7,7 +7,7 @@ import os from typing import List from testcase import AppConfigTestCase, _AUDIENCE_UNSET -from azure.appconfiguration.aio import AzureAppConfigurationClient +from azure.appconfiguration.aio import AzureAppConfigurationClient, FeatureFlagClient from azure.core.async_paging import AsyncItemPaged @@ -19,6 +19,12 @@ def create_client(self, appconfiguration_endpoint_string, audience=_AUDIENCE_UNS audience = os.environ.get("APPCONFIGURATION_AUDIENCE") return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, audience=audience) + def create_feature_flag_client(self, appconfiguration_endpoint_string, audience=_AUDIENCE_UNSET): + cred = self.get_credential(FeatureFlagClient, is_async=True) + if audience is _AUDIENCE_UNSET: + audience = os.environ.get("APPCONFIGURATION_AUDIENCE") + return FeatureFlagClient(appconfiguration_endpoint_string, cred, audience=audience) + async def convert_to_list(self, items: AsyncItemPaged) -> List: list = [] async for item in items: diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/conftest.py b/sdk/appconfiguration/azure-appconfiguration/tests/conftest.py index d0dcda865be9..1a048f3fa831 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/conftest.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/conftest.py @@ -40,8 +40,10 @@ def add_sanitizers(test_proxy, patch_sleep, patch_async_sleep): add_general_regex_sanitizer(value="00000000-0000-0000-0000-000000000000", regex=tenant_id) # Remove the following sanitizers since certain fields are needed in tests and are non-sensitive: + # - AZSDK3424: $..to (feature flag percentile allocation bounds) + # - AZSDK3425: $..from (feature flag percentile allocation bounds) # - AZSDK3447: $.key # - AZSDK3490: $..etag # - AZSDK3493: $..name # - AZSDK4001: host name -> Sanitized - remove_batch_sanitizers(["AZSDK3447", "AZSDK3490", "AZSDK3493", "AZSDK4001"]) + remove_batch_sanitizers(["AZSDK3424", "AZSDK3425", "AZSDK3447", "AZSDK3490", "AZSDK3493", "AZSDK4001"]) diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py index 212855c42304..07c738ea6c4c 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py @@ -1299,8 +1299,9 @@ def test_list_labels_resource_type(self, appconfiguration_endpoint_string): # verify resource_type segregates key-value labels ('kv') from feature-flag # labels ('ff'). ff_label = "ff_resource_type_" + LABEL + ff_client = self.create_feature_flag_client(appconfiguration_endpoint_string) feature_flag = FeatureFlag(name="resource_type_feature", enabled=True, label=ff_label) - self.client.set_feature_flag(feature_flag) + ff_client.set_feature_flag(feature_flag) try: # resource_type="kv" returns labels used by key-value settings only. kv_labels = {item.name for item in self.client.list_labels(resource_type="kv")} @@ -1312,7 +1313,7 @@ def test_list_labels_resource_type(self, appconfiguration_endpoint_string): assert ff_label in ff_labels assert LABEL not in ff_labels finally: - self.client.delete_feature_flag("resource_type_feature", label=ff_label) + ff_client.delete_feature_flag("resource_type_feature", label=ff_label) self.tear_down() diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py index 0fbb5e997cd8..b4ed73dfeebe 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py @@ -1346,8 +1346,9 @@ async def test_list_labels_resource_type(self, appconfiguration_endpoint_string) # verify resource_type segregates key-value labels ('kv') from feature-flag # labels ('ff'). ff_label = "ff_resource_type_" + LABEL + ff_client = self.create_feature_flag_client(appconfiguration_endpoint_string) feature_flag = FeatureFlag(name="resource_type_feature", enabled=True, label=ff_label) - await self.client.set_feature_flag(feature_flag) + await ff_client.set_feature_flag(feature_flag) try: # resource_type="kv" returns labels used by key-value settings only. kv_labels = {item.name for item in await self.convert_to_list(self.client.list_labels(resource_type="kv"))} @@ -1359,7 +1360,8 @@ async def test_list_labels_resource_type(self, appconfiguration_endpoint_string) assert ff_label in ff_labels assert LABEL not in ff_labels finally: - await self.client.delete_feature_flag("resource_type_feature", label=ff_label) + await ff_client.delete_feature_flag("resource_type_feature", label=ff_label) + await ff_client.close() await self.tear_down() await self.client.close() diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py index cdb6de10a6e4..726697a8c89a 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py @@ -34,6 +34,9 @@ class TestFeatureFlagEndpoint(AppConfigTestCase): """Tests for the new dedicated feature flag endpoint methods""" + def create_client(self, *args, **kwargs): + return self.create_feature_flag_client(*args, **kwargs) + @AppConfigPreparer() @recorded_by_proxy def test_list_feature_flags(self, appconfiguration_endpoint_string): @@ -385,3 +388,25 @@ def test_feature_flag_full_model_roundtrip(self, appconfiguration_endpoint_strin assert retrieved.last_modified is not None client.delete_feature_flag("test_feature_full", label="prod") + + @AppConfigPreparer() + @recorded_by_proxy + def test_list_labels(self, appconfiguration_endpoint_string): + """Test listing feature flag labels using the dedicated feature flag endpoint.""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + ff_label = "test_ff_label" + feature_flag = FeatureFlag(name="test_feature_labels", enabled=True, label=ff_label) + client.set_feature_flag(feature_flag) + try: + # resource_type="ff" returns labels used by feature flags. + ff_labels = {item.name for item in client.list_labels(resource_type="ff")} + assert ff_label in ff_labels + + # name filter narrows the results to the matching label. + filtered = list(client.list_labels(name=ff_label, resource_type="ff")) + assert len(filtered) == 1 + assert filtered[0].name == ff_label + finally: + client.delete_feature_flag("test_feature_labels", label=ff_label) diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py index ce9323e5d4e8..1daed1364e0e 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py @@ -35,6 +35,9 @@ class TestFeatureFlagEndpointAsync(AsyncAppConfigTestCase): """Async tests for the new dedicated feature flag endpoint methods""" + def create_client(self, *args, **kwargs): + return self.create_feature_flag_client(*args, **kwargs) + @AppConfigPreparer() @recorded_by_proxy_async async def test_list_feature_flags(self, appconfiguration_endpoint_string): @@ -399,3 +402,27 @@ async def test_feature_flag_full_model_roundtrip(self, appconfiguration_endpoint await client.delete_feature_flag("test_feature_full", label="prod") await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_list_labels(self, appconfiguration_endpoint_string): + """Test listing feature flag labels using the dedicated feature flag endpoint.""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + ff_label = "test_ff_label" + feature_flag = FeatureFlag(name="test_feature_labels", enabled=True, label=ff_label) + await client.set_feature_flag(feature_flag) + try: + # resource_type="ff" returns labels used by feature flags. + labels = await self.convert_to_list(client.list_labels(resource_type="ff")) + ff_labels = {item.name for item in labels} + assert ff_label in ff_labels + + # name filter narrows the results to the matching label. + filtered = await self.convert_to_list(client.list_labels(name=ff_label, resource_type="ff")) + assert len(filtered) == 1 + assert filtered[0].name == ff_label + finally: + await client.delete_feature_flag("test_feature_labels", label=ff_label) + await client.close() diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py index 5be5a818c4a9..9b15654cf643 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/testcase.py @@ -14,6 +14,7 @@ from devtools_testutils import AzureRecordedTestCase from azure.appconfiguration import ( AzureAppConfigurationClient, + FeatureFlagClient, ConfigurationSetting, FeatureFlagConfigurationSetting, SecretReferenceConfigurationSetting, @@ -34,6 +35,12 @@ def create_client(self, appconfiguration_endpoint_string, audience=_AUDIENCE_UNS audience = os.environ.get("APPCONFIGURATION_AUDIENCE") return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, audience=audience) + def create_feature_flag_client(self, appconfiguration_endpoint_string, audience=_AUDIENCE_UNSET): + cred = self.get_credential(FeatureFlagClient) + if audience is _AUDIENCE_UNSET: + audience = os.environ.get("APPCONFIGURATION_AUDIENCE") + return FeatureFlagClient(appconfiguration_endpoint_string, cred, audience=audience) + def create_config_setting(self): return ConfigurationSetting( key=KEY, diff --git a/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml b/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml index 955185feced9..2fb18b2d92cc 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml +++ b/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml @@ -1,4 +1,4 @@ -repo: Azure/azure-rest-api-specs -commit: 6e7e0ec0847f966d243c5a7dd25b706018744d4a +repo: mrm9084/azure-rest-api-specs +commit: 94454db0c0edc07120be22b8fc77e9b898c158a1 directory: specification/appconfiguration/data-plane/AppConfiguration additionalDirectories: \ No newline at end of file From ed03cafbe55f5323977902d8cbb82b5307a5f6cf Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 7 Jul 2026 11:00:49 -0700 Subject: [PATCH 10/25] pipeline fixes --- .../azure-appconfiguration/assets.json | 2 +- .../tests/test_feature_flags.py | 20 +++++++++---------- .../tests/test_feature_flags_async.py | 20 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration/assets.json b/sdk/appconfiguration/azure-appconfiguration/assets.json index a5f9617ef358..093fabe9a5cf 100644 --- a/sdk/appconfiguration/azure-appconfiguration/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration", - "Tag": "python/appconfiguration/azure-appconfiguration_f51152e2e2" + "Tag": "python/appconfiguration/azure-appconfiguration_f3a785af7a" } diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py index 726697a8c89a..2799ce7d58ba 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py @@ -69,23 +69,23 @@ def test_list_feature_flags_with_name_filter(self, appconfiguration_endpoint_str client = self.create_client(appconfiguration_endpoint_string) # Create feature flags - feature_flag1 = FeatureFlag(name="myfeature_alpha", enabled=True) - feature_flag2 = FeatureFlag(name="myfeature_beta", enabled=False) - feature_flag3 = FeatureFlag(name="otherfeature", enabled=True) + feature_flag1 = FeatureFlag(name="my_feature_alpha", enabled=True) + feature_flag2 = FeatureFlag(name="my_feature_beta", enabled=False) + feature_flag3 = FeatureFlag(name="other_feature", enabled=True) client.set_feature_flag(feature_flag1) client.set_feature_flag(feature_flag2) client.set_feature_flag(feature_flag3) # List with name filter - flags = list(client.list_feature_flags(name_filter="myfeature*")) + flags = list(client.list_feature_flags(name_filter="my_feature*")) assert len(flags) >= 2 - assert all("myfeature" in f.name for f in flags) + assert all("my_feature" in f.name for f in flags) # Clean up - client.delete_feature_flag("myfeature_alpha", label=feature_flag1.label) - client.delete_feature_flag("myfeature_beta", label=feature_flag2.label) - client.delete_feature_flag("otherfeature", label=feature_flag3.label) + client.delete_feature_flag("my_feature_alpha", label=feature_flag1.label) + client.delete_feature_flag("my_feature_beta", label=feature_flag2.label) + client.delete_feature_flag("other_feature", label=feature_flag3.label) @AppConfigPreparer() @recorded_by_proxy @@ -361,7 +361,7 @@ def test_feature_flag_full_model_roundtrip(self, appconfiguration_endpoint_strin user=[UserAllocation(variant="Large", users=["carol"])], group=[GroupAllocation(variant="Small", groups=["internal"])], ), - telemetry=FeatureFlagTelemetryConfiguration(enabled=True, metadata={"source": "test"}), + telemetry=FeatureFlagTelemetryConfiguration(enabled=True, metadata={"origin": "test"}), tags={"env": "prod", "critical": "true"}, ) created = client.set_feature_flag(feature_flag) @@ -382,7 +382,7 @@ def test_feature_flag_full_model_roundtrip(self, appconfiguration_endpoint_strin assert retrieved.allocation is not None assert retrieved.allocation.default_when_enabled == "Large" assert retrieved.telemetry is not None - assert retrieved.telemetry.metadata == {"source": "test"} + assert retrieved.telemetry.metadata == {"origin": "test"} assert retrieved.tags == {"env": "prod", "critical": "true"} assert retrieved.etag is not None assert retrieved.last_modified is not None diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py index 1daed1364e0e..72ea6fb3c7b7 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py @@ -71,23 +71,23 @@ async def test_list_feature_flags_with_name_filter(self, appconfiguration_endpoi client = self.create_client(appconfiguration_endpoint_string) # Create feature flags - feature_flag1 = FeatureFlag(name="myfeature_alpha", enabled=True) - feature_flag2 = FeatureFlag(name="myfeature_beta", enabled=False) - feature_flag3 = FeatureFlag(name="otherfeature", enabled=True) + feature_flag1 = FeatureFlag(name="my_feature_alpha", enabled=True) + feature_flag2 = FeatureFlag(name="my_feature_beta", enabled=False) + feature_flag3 = FeatureFlag(name="other_feature", enabled=True) await client.set_feature_flag(feature_flag1) await client.set_feature_flag(feature_flag2) await client.set_feature_flag(feature_flag3) # List with name filter - flags = [f async for f in client.list_feature_flags(name_filter="myfeature*")] + flags = [f async for f in client.list_feature_flags(name_filter="my_feature*")] assert len(flags) >= 2 - assert all("myfeature" in f.name for f in flags) + assert all("my_feature" in f.name for f in flags) # Clean up - await client.delete_feature_flag("myfeature_alpha", label=feature_flag1.label) - await client.delete_feature_flag("myfeature_beta", label=feature_flag2.label) - await client.delete_feature_flag("otherfeature", label=feature_flag3.label) + await client.delete_feature_flag("my_feature_alpha", label=feature_flag1.label) + await client.delete_feature_flag("my_feature_beta", label=feature_flag2.label) + await client.delete_feature_flag("other_feature", label=feature_flag3.label) await client.close() @AppConfigPreparer() @@ -374,7 +374,7 @@ async def test_feature_flag_full_model_roundtrip(self, appconfiguration_endpoint user=[UserAllocation(variant="Large", users=["carol"])], group=[GroupAllocation(variant="Small", groups=["internal"])], ), - telemetry=FeatureFlagTelemetryConfiguration(enabled=True, metadata={"source": "test"}), + telemetry=FeatureFlagTelemetryConfiguration(enabled=True, metadata={"origin": "test"}), tags={"env": "prod", "critical": "true"}, ) created = await client.set_feature_flag(feature_flag) @@ -395,7 +395,7 @@ async def test_feature_flag_full_model_roundtrip(self, appconfiguration_endpoint assert retrieved.allocation is not None assert retrieved.allocation.default_when_enabled == "Large" assert retrieved.telemetry is not None - assert retrieved.telemetry.metadata == {"source": "test"} + assert retrieved.telemetry.metadata == {"origin": "test"} assert retrieved.tags == {"env": "prod", "critical": "true"} assert retrieved.etag is not None assert retrieved.last_modified is not None From 444cab1b8f89a2ea9383c32ac65e2103079a877a Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 15 Jul 2026 13:03:37 -0700 Subject: [PATCH 11/25] Updating label usage --- .../azure-appconfiguration/CHANGELOG.md | 3 +++ .../azure-appconfiguration/apiview-properties.json | 2 +- .../azure-appconfiguration/assets.json | 2 +- .../_azure_appconfiguration_client.py | 10 +++------- .../azure/appconfiguration/_feature_flag_client.py | 8 ++------ .../appconfiguration/_generated/models/_models.py | 13 +++++++------ .../aio/_azure_appconfiguration_client_async.py | 10 +++------- .../aio/_feature_flag_client_async.py | 8 ++------ .../tests/test_azure_appconfiguration_client.py | 13 +++++++------ .../test_azure_appconfiguration_client_async.py | 13 +++++++------ .../tests/test_feature_flags.py | 6 +++--- .../tests/test_feature_flags_async.py | 6 +++--- .../azure-appconfiguration/tsp-location.yaml | 2 +- 13 files changed, 43 insertions(+), 53 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md index 58bf0004f581..a64060ce2b26 100644 --- a/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md @@ -8,6 +8,7 @@ - Exposes `set_feature_flag()`, `get_feature_flag()`, `list_feature_flags()`, `delete_feature_flag()`, `add_feature_flag()`, `list_feature_flag_revisions()`, and `list_labels()`. - `FeatureFlagClient` is available from `azure.appconfiguration` (sync) and `azure.appconfiguration.aio` (async). - Added the `FeatureFlag` model along with `FeatureFlagConditions`, `FeatureFlagFilter`, `FeatureFlagVariantDefinition`, `FeatureFlagAllocation`, `FeatureFlagTelemetryConfiguration`, `PercentileAllocation`, `UserAllocation`, and `GroupAllocation`. + - `FeatureFlagClient.list_labels()` always lists feature-flag labels, while `AzureAppConfigurationClient.list_labels()` always lists key-value labels. ### Breaking Changes @@ -15,6 +16,8 @@ ### Other Changes +- Updated the default service API version to `2026-05-01-preview`. + ## 1.9.0 (2026-06-18) ### Features Added diff --git a/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json b/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json index 9db5a25a42ad..cce98f7247e9 100644 --- a/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json +++ b/sdk/appconfiguration/azure-appconfiguration/apiview-properties.json @@ -82,5 +82,5 @@ "azure.appconfiguration.AzureAppConfigurationClient.check_revisions": "AzureAppConfiguration.checkRevisions", "azure.appconfiguration.aio.AzureAppConfigurationClient.check_revisions": "AzureAppConfiguration.checkRevisions" }, - "CrossLanguageVersion": "9848bf3c406d" + "CrossLanguageVersion": "596dfe19d55d" } \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration/assets.json b/sdk/appconfiguration/azure-appconfiguration/assets.json index 093fabe9a5cf..8fac311ad3f1 100644 --- a/sdk/appconfiguration/azure-appconfiguration/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration", - "Tag": "python/appconfiguration/azure-appconfiguration_f3a785af7a" + "Tag": "python/appconfiguration/azure-appconfiguration_10cccad6ed" } diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py index 5a072d5864c3..bc97fba45c9e 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py @@ -1,4 +1,4 @@ -# pylint: disable=line-too-long,useless-suppression,too-many-lines +# pylint: disable=line-too-long,useless-suppression # ------------------------------------------------------------------------ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for @@ -46,7 +46,7 @@ from ._audience_error_handling_policy import AudienceErrorHandlingPolicy -class AzureAppConfigurationClient: # pylint: disable=too-many-public-methods +class AzureAppConfigurationClient: """Represents a client that calls restful API of Azure App Configuration service. :param str base_url: Base url of the service. @@ -638,7 +638,6 @@ def list_labels( after: Optional[str] = None, accept_datetime: Optional[Union[datetime, str]] = None, fields: Optional[List[Union[str, LabelFields]]] = None, - resource_type: Optional[str] = None, **kwargs: Any, ) -> ItemPaged[ConfigurationSettingLabel]: """Gets a list of labels. @@ -656,9 +655,6 @@ def list_labels( :keyword fields: Specify which fields to include in the results. If not specified, will include all fields. Available fields see :class:`~azure.appconfiguration.LabelFields`. :paramtype fields: list[str] or list[~azure.appconfiguration.LabelFields] or None - :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted values: - 'kv' for key-value labels, 'ff' for feature flag labels. Default is `None`. - :paramtype resource_type: str or None :return: An iterator of labels. :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.ConfigurationSettingLabel] :raises: :class:`~azure.core.exceptions.HttpResponseError` @@ -670,7 +666,7 @@ def list_labels( after=after, accept_datetime=accept_datetime, select=fields, - resource_type=resource_type, + resource_type="kv", cls=lambda objs: [ConfigurationSettingLabel(name=x.name) for x in objs], **kwargs, ) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py index 7ff1e11e95db..168544f88a7c 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py @@ -429,10 +429,9 @@ def list_labels( after: Optional[str] = None, accept_datetime: Optional[Union[datetime, str]] = None, fields: Optional[List[Union[str, LabelFields]]] = None, - resource_type: Optional[str] = None, **kwargs: Any, ) -> ItemPaged[ConfigurationSettingLabel]: - """Gets a list of labels. + """Gets a list of feature flag labels. :keyword name: A filter for the name of the returned labels. '*' can be used as wildcard in the beginning or end of the filter. For more information about supported filters, see @@ -447,9 +446,6 @@ def list_labels( :keyword fields: Specify which fields to include in the results. If not specified, will include all fields. Available fields see :class:`~azure.appconfiguration.LabelFields`. :paramtype fields: list[str] or list[~azure.appconfiguration.LabelFields] or None - :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted values: - 'kv' for key-value labels, 'ff' for feature flag labels. Default is `None`. - :paramtype resource_type: str or None :return: An iterator of labels. :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.ConfigurationSettingLabel] :raises: :class:`~azure.core.exceptions.HttpResponseError` @@ -461,7 +457,7 @@ def list_labels( after=after, accept_datetime=accept_datetime, select=fields, - resource_type=resource_type, + resource_type="ff", cls=lambda objs: [ConfigurationSettingLabel(name=x.name) for x in objs], **kwargs, ) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py index dce821e15388..a3cd60392ff6 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/models/_models.py @@ -71,7 +71,7 @@ class FeatureFlag(_Model): :ivar name: The name of the feature flag. Required. :vartype name: str - :ivar enabled: The enabled state of the feature flag. + :ivar enabled: The enabled state of the feature flag. Required. :vartype enabled: bool :ivar label: The label the feature flag belongs to. :vartype label: str @@ -95,8 +95,8 @@ class FeatureFlag(_Model): name: str = rest_field(visibility=["read"]) """The name of the feature flag. Required.""" - enabled: Optional[bool] = rest_field(visibility=["read", "create", "update", "delete", "query"]) - """The enabled state of the feature flag.""" + enabled: bool = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The enabled state of the feature flag. Required.""" label: Optional[str] = rest_field(visibility=["read"]) """The label the feature flag belongs to.""" description: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) @@ -128,7 +128,7 @@ class FeatureFlag(_Model): def __init__( self, *, - enabled: Optional[bool] = None, + enabled: bool, description: Optional[str] = None, conditions: Optional["_models.FeatureFlagConditions"] = None, variants: Optional[list["_models.FeatureFlagVariantDefinition"]] = None, @@ -428,7 +428,7 @@ class KeyValue(_Model): key: str = rest_field(visibility=["read"]) """The key of the key-value. Required.""" - label: Optional[str] = rest_field(visibility=["read"]) + label: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) """The label the key-value belongs to.""" content_type: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) """The content type of the value stored within the key-value.""" @@ -451,6 +451,7 @@ class KeyValue(_Model): def __init__( self, *, + label: Optional[str] = None, content_type: Optional[str] = None, value: Optional[str] = None, last_modified: Optional[datetime.datetime] = None, @@ -512,7 +513,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: class Label(_Model): - """Labels are used to group key values or feature flags. + """Labels are used to group key-values. :ivar name: The name of the label. :vartype name: str diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py index 662f2768e255..b317c974c299 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py @@ -1,4 +1,4 @@ -# pylint: disable=line-too-long,useless-suppression,too-many-lines +# pylint: disable=line-too-long,useless-suppression # ------------------------------------------------------------------------ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for @@ -47,7 +47,7 @@ from .._audience_error_handling_policy import AudienceErrorHandlingPolicy -class AzureAppConfigurationClient: # pylint: disable=too-many-public-methods +class AzureAppConfigurationClient: """Represents a client that calls restful API of Azure App Configuration service. :param str base_url: Base url of the service. @@ -651,7 +651,6 @@ def list_labels( after: Optional[str] = None, accept_datetime: Optional[Union[datetime, str]] = None, fields: Optional[List[Union[str, LabelFields]]] = None, - resource_type: Optional[str] = None, **kwargs: Any, ) -> AsyncItemPaged[ConfigurationSettingLabel]: """Gets a list of labels. @@ -669,9 +668,6 @@ def list_labels( :keyword fields: Specify which fields to include in the results. If not specified, will include all fields. Available fields see :class:`~azure.appconfiguration.LabelFields`. :paramtype fields: list[str] or list[~azure.appconfiguration.LabelFields] or None - :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted values: - 'kv' for key-value labels, 'ff' for feature flag labels. Default is `None`. - :paramtype resource_type: str or None :return: An async iterator of labels. :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration.ConfigurationSettingLabel] :raises: :class:`~azure.core.exceptions.HttpResponseError` @@ -683,7 +679,7 @@ def list_labels( after=after, accept_datetime=accept_datetime, select=fields, - resource_type=resource_type, + resource_type="kv", cls=lambda objs: [ConfigurationSettingLabel(name=x.name) for x in objs], **kwargs, ) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py index 6398ae005c58..692dcaf1e157 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py @@ -396,10 +396,9 @@ def list_labels( after: Optional[str] = None, accept_datetime: Optional[Union[datetime, str]] = None, fields: Optional[List[Union[str, LabelFields]]] = None, - resource_type: Optional[str] = None, **kwargs: Any, ) -> AsyncItemPaged[ConfigurationSettingLabel]: - """Gets a list of labels. + """Gets a list of feature flag labels. :keyword name: A filter for the name of the returned labels. '*' can be used as wildcard in the beginning or end of the filter. For more information about supported filters, see @@ -414,9 +413,6 @@ def list_labels( :keyword fields: Specify which fields to include in the results. If not specified, will include all fields. Available fields see :class:`~azure.appconfiguration.LabelFields`. :paramtype fields: list[str] or list[~azure.appconfiguration.LabelFields] or None - :keyword resource_type: A filter used to indicate the resource type of the labels. Accepted values: - 'kv' for key-value labels, 'ff' for feature flag labels. Default is `None`. - :paramtype resource_type: str or None :return: An async iterator of labels. :rtype: ~azure.core.paging.AsyncItemPaged[~azure.appconfiguration.ConfigurationSettingLabel] :raises: :class:`~azure.core.exceptions.HttpResponseError` @@ -428,7 +424,7 @@ def list_labels( after=after, accept_datetime=accept_datetime, select=fields, - resource_type=resource_type, + resource_type="ff", cls=lambda objs: [ConfigurationSettingLabel(name=x.name) for x in objs], **kwargs, ) diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py index 07c738ea6c4c..226ea49eae06 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py @@ -1296,20 +1296,21 @@ def test_list_labels_resource_type(self, appconfiguration_endpoint_string): self.set_up(appconfiguration_endpoint_string) # set_up creates key-value settings labeled LABEL. Add a dedicated feature flag # (written through the feature-flag endpoint) with a distinct label so we can - # verify resource_type segregates key-value labels ('kv') from feature-flag - # labels ('ff'). + # verify each client only returns labels for its own resource type: the + # configuration client always lists key-value labels ('kv') and the feature + # flag client always lists feature-flag labels ('ff'). ff_label = "ff_resource_type_" + LABEL ff_client = self.create_feature_flag_client(appconfiguration_endpoint_string) feature_flag = FeatureFlag(name="resource_type_feature", enabled=True, label=ff_label) ff_client.set_feature_flag(feature_flag) try: - # resource_type="kv" returns labels used by key-value settings only. - kv_labels = {item.name for item in self.client.list_labels(resource_type="kv")} + # The configuration client only lists key-value labels. + kv_labels = {item.name for item in self.client.list_labels()} assert LABEL in kv_labels assert ff_label not in kv_labels - # resource_type="ff" returns labels used by feature flags only. - ff_labels = {item.name for item in self.client.list_labels(resource_type="ff")} + # The feature flag client only lists feature-flag labels. + ff_labels = {item.name for item in ff_client.list_labels()} assert ff_label in ff_labels assert LABEL not in ff_labels finally: diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py index b4ed73dfeebe..fe292f79342e 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client_async.py @@ -1343,20 +1343,21 @@ async def test_list_labels_resource_type(self, appconfiguration_endpoint_string) await self.set_up(appconfiguration_endpoint_string) # set_up creates key-value settings labeled LABEL. Add a dedicated feature flag # (written through the feature-flag endpoint) with a distinct label so we can - # verify resource_type segregates key-value labels ('kv') from feature-flag - # labels ('ff'). + # verify each client only returns labels for its own resource type: the + # configuration client always lists key-value labels ('kv') and the feature + # flag client always lists feature-flag labels ('ff'). ff_label = "ff_resource_type_" + LABEL ff_client = self.create_feature_flag_client(appconfiguration_endpoint_string) feature_flag = FeatureFlag(name="resource_type_feature", enabled=True, label=ff_label) await ff_client.set_feature_flag(feature_flag) try: - # resource_type="kv" returns labels used by key-value settings only. - kv_labels = {item.name for item in await self.convert_to_list(self.client.list_labels(resource_type="kv"))} + # The configuration client only lists key-value labels. + kv_labels = {item.name for item in await self.convert_to_list(self.client.list_labels())} assert LABEL in kv_labels assert ff_label not in kv_labels - # resource_type="ff" returns labels used by feature flags only. - ff_labels = {item.name for item in await self.convert_to_list(self.client.list_labels(resource_type="ff"))} + # The feature flag client only lists feature-flag labels. + ff_labels = {item.name for item in await self.convert_to_list(ff_client.list_labels())} assert ff_label in ff_labels assert LABEL not in ff_labels finally: diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py index 2799ce7d58ba..2816f332a1f1 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py @@ -400,12 +400,12 @@ def test_list_labels(self, appconfiguration_endpoint_string): feature_flag = FeatureFlag(name="test_feature_labels", enabled=True, label=ff_label) client.set_feature_flag(feature_flag) try: - # resource_type="ff" returns labels used by feature flags. - ff_labels = {item.name for item in client.list_labels(resource_type="ff")} + # The feature flag endpoint only lists feature-flag labels. + ff_labels = {item.name for item in client.list_labels()} assert ff_label in ff_labels # name filter narrows the results to the matching label. - filtered = list(client.list_labels(name=ff_label, resource_type="ff")) + filtered = list(client.list_labels(name=ff_label)) assert len(filtered) == 1 assert filtered[0].name == ff_label finally: diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py index 72ea6fb3c7b7..d1202c983c14 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py @@ -414,13 +414,13 @@ async def test_list_labels(self, appconfiguration_endpoint_string): feature_flag = FeatureFlag(name="test_feature_labels", enabled=True, label=ff_label) await client.set_feature_flag(feature_flag) try: - # resource_type="ff" returns labels used by feature flags. - labels = await self.convert_to_list(client.list_labels(resource_type="ff")) + # The feature flag endpoint only lists feature-flag labels. + labels = await self.convert_to_list(client.list_labels()) ff_labels = {item.name for item in labels} assert ff_label in ff_labels # name filter narrows the results to the matching label. - filtered = await self.convert_to_list(client.list_labels(name=ff_label, resource_type="ff")) + filtered = await self.convert_to_list(client.list_labels(name=ff_label)) assert len(filtered) == 1 assert filtered[0].name == ff_label finally: diff --git a/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml b/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml index 2fb18b2d92cc..803ac6d88412 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml +++ b/sdk/appconfiguration/azure-appconfiguration/tsp-location.yaml @@ -1,4 +1,4 @@ repo: mrm9084/azure-rest-api-specs -commit: 94454db0c0edc07120be22b8fc77e9b898c158a1 +commit: 7cef3270dc4f2791d56817b08a8011338fe49bbb directory: specification/appconfiguration/data-plane/AppConfiguration additionalDirectories: \ No newline at end of file From e2ba71cf3170ee7f4c9d8d93fb53f33bf9494a98 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 16 Jul 2026 15:52:46 -0700 Subject: [PATCH 12/25] Update _models.py --- .../azure-appconfiguration/azure/appconfiguration/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py index f86937c0d13b..a07edb91cf34 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py @@ -807,7 +807,7 @@ class FeatureFlag(Model): # pylint: disable=too-many-instance-attributes name: str """The name of the feature flag.""" - enabled: Optional[bool] + enabled: bool """The enabled state of the feature flag.""" label: Optional[str] """The label the feature flag belongs to.""" From 509976f00868606b2b5043bdf2f40d5781d62a75 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 20 Jul 2026 16:17:23 -0700 Subject: [PATCH 13/25] review comments --- .../aio/_feature_flag_client_async.py | 6 ++++++ .../tests/test_feature_flags.py | 20 ++++++++++++++++++ .../tests/test_feature_flags_async.py | 21 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py index 692dcaf1e157..46a3ac5371e6 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py @@ -210,6 +210,12 @@ async def get_feature_flag( :return: The FeatureFlag if found; None otherwise. :rtype: ~azure.appconfiguration.FeatureFlag or None :raises: :class:`~azure.core.exceptions.HttpResponseError` + + Example + + .. code-block:: python + + feature_flag = await async_client.get_feature_flag(feature_id="MyFeatureFlag") """ if isinstance(accept_datetime, datetime): accept_datetime = str(accept_datetime) diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py index 2816f332a1f1..78b0b4423ce8 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py @@ -408,5 +408,25 @@ def test_list_labels(self, appconfiguration_endpoint_string): filtered = list(client.list_labels(name=ff_label)) assert len(filtered) == 1 assert filtered[0].name == ff_label + + # Negative case: a name filter that matches no label returns no results. + no_match = list(client.list_labels(name="nonexistent_ff_label")) + assert no_match == [] finally: client.delete_feature_flag("test_feature_labels", label=ff_label) + + @AppConfigPreparer() + @recorded_by_proxy + def test_get_feature_flag_wrong_label(self, appconfiguration_endpoint_string): + """Negative test: getting a feature flag with a label it doesn't have returns None.""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + feature_flag = FeatureFlag(name="test_feature_wrong_label", enabled=True, label="real_label") + client.set_feature_flag(feature_flag) + try: + # The flag exists, but not under this label. + result = client.get_feature_flag("test_feature_wrong_label", label="wrong_label") + assert result is None + finally: + client.delete_feature_flag("test_feature_wrong_label", label="real_label") diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py index d1202c983c14..6a611e38da74 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py @@ -423,6 +423,27 @@ async def test_list_labels(self, appconfiguration_endpoint_string): filtered = await self.convert_to_list(client.list_labels(name=ff_label)) assert len(filtered) == 1 assert filtered[0].name == ff_label + + # Negative case: a name filter that matches no label returns no results. + no_match = await self.convert_to_list(client.list_labels(name="nonexistent_ff_label")) + assert no_match == [] finally: await client.delete_feature_flag("test_feature_labels", label=ff_label) await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_get_feature_flag_wrong_label(self, appconfiguration_endpoint_string): + """Negative test: getting a feature flag with a label it doesn't have returns None.""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + feature_flag = FeatureFlag(name="test_feature_wrong_label", enabled=True, label="real_label") + await client.set_feature_flag(feature_flag) + try: + # The flag exists, but not under this label. + result = await client.get_feature_flag("test_feature_wrong_label", label="wrong_label") + assert result is None + finally: + await client.delete_feature_flag("test_feature_wrong_label", label="real_label") + await client.close() From f534f6b9ef97a743f813c1d0d8c19cf2c363bd0e Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 21 Jul 2026 10:32:26 -0700 Subject: [PATCH 14/25] PageBasedEtagFF --- .../azure/appconfiguration/__init__.py | 4 + .../appconfiguration/_feature_flag_client.py | 28 +- .../_generated/aio/operations/_patch.py | 114 +++++++ .../_generated/operations/_patch.py | 118 ++++++++ .../azure/appconfiguration/_models.py | 282 ++++++++++++++++++ .../aio/_feature_flag_client_async.py | 42 ++- 6 files changed, 568 insertions(+), 20 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py index 09f5f0956d89..c287712eb76d 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py @@ -27,6 +27,8 @@ FeatureFlagVariantDefinition, FeatureFlagAllocation, FeatureFlagTelemetryConfiguration, + FeatureFlagPaged, + AsyncFeatureFlagPaged, PercentileAllocation, UserAllocation, GroupAllocation, @@ -65,6 +67,8 @@ "FeatureFlagVariantDefinition", "FeatureFlagAllocation", "FeatureFlagTelemetryConfiguration", + "FeatureFlagPaged", + "AsyncFeatureFlagPaged", "PercentileAllocation", "UserAllocation", "GroupAllocation", diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py index 168544f88a7c..d1d74044c1e8 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_feature_flag_client.py @@ -5,6 +5,7 @@ # license information. # ------------------------------------------------------------------------- from datetime import datetime +import functools from typing import Any, Dict, List, Optional, Union from azure.core import MatchConditions from azure.core.paging import ItemPaged @@ -18,7 +19,7 @@ from ._query_param_policy import QueryParamPolicy from ._generated import AzureAppConfigurationClient as AzureAppConfigurationClientGenerated from ._generated.models import LabelFields -from ._models import FeatureFlag, ConfigurationSettingLabel +from ._models import FeatureFlag, ConfigurationSettingLabel, FeatureFlagPaged, FeatureFlagPropertiesPaged from ._audience import get_audience, DEFAULT_SCOPE_SUFFIX from ._utils import parse_connection_string from ._sync_token import SyncTokenPolicy @@ -144,7 +145,7 @@ def list_feature_flags( tags_filter: Optional[List[str]] = None, accept_datetime: Optional[Union[datetime, str]] = None, **kwargs: Any, - ) -> ItemPaged["FeatureFlag"]: + ) -> FeatureFlagPaged: """ Find the FeatureFlag objects, optionally filtered by name, label, tags and accept_datetime. @@ -158,7 +159,7 @@ def list_feature_flags( :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime :paramtype accept_datetime: ~datetime.datetime or str or None :return: An iterator of :class:`~azure.appconfiguration.FeatureFlag` - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.FeatureFlag] + :rtype: ~azure.appconfiguration.FeatureFlagPaged :raises: :class:`~azure.core.exceptions.HttpResponseError` Example @@ -169,21 +170,28 @@ def list_feature_flags( feature_flags = client.list_feature_flags() for flag in feature_flags: print(flag.name, flag.enabled) + + # Detect changes across pages using etags + feature_flags = client.list_feature_flags(name_filter="sample_*") + match_conditions = [page.etag for page in feature_flags.by_page()] + + feature_flags = client.list_feature_flags(name_filter="sample_*") + for page in feature_flags.by_page(match_conditions=match_conditions): + # Only pages that changed are returned + for flag in page: + print(flag.name, flag.enabled) """ if isinstance(accept_datetime, datetime): accept_datetime = str(accept_datetime) - # Call the generated method and wrap results to convert to SDK FeatureFlag - def convert_to_sdk(objs): - return [FeatureFlag._from_generated(obj) for obj in objs] # pylint:disable=protected-access - - return self._impl.feature_flag_client.get_feature_flags( # type: ignore[return-value] + command = functools.partial(self._impl.get_feature_flags_in_one_page, **kwargs) # type: ignore[attr-defined] # pylint: disable=no-member + return FeatureFlagPaged( + command, name=name_filter, label=label_filter, accept_datetime=accept_datetime, tags=tags_filter, - cls=convert_to_sdk, - **kwargs, + page_iterator_class=FeatureFlagPropertiesPaged, ) @distributed_trace diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_patch.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_patch.py index 2a91a5fbce41..087b6f7812a4 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_patch.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/aio/operations/_patch.py @@ -29,6 +29,7 @@ ClsType, build_azure_app_configuration_get_key_values_request, build_azure_app_configuration_check_key_values_request, + build_feature_flag_client_get_feature_flags_request, ) from ...operations._operations import prep_if_match, prep_if_none_match from ... import models as _models @@ -345,6 +346,119 @@ async def check_key_values_in_one_page( return result + @distributed_trace_async + async def get_feature_flags_in_one_page( + self, + *, + name: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[List[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[List[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + continuation_token: Optional[str] = None, + **kwargs: Any, + ) -> dict: + """Gets a list of feature flags in one page. + + Gets a list of feature flags in one page. + + :keyword name: A filter used to match feature flag names. Default value is None. + :paramtype name: str + :keyword label: A filter used to match labels. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :param str continuation_token: An opaque continuation token. + :return: A dict containing ``items`` (list of feature flags) and ``@nextLink`` (pagination URL or None). + :rtype: dict + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[List[_models.FeatureFlag]] = kwargs.pop("cls", None) + + error_map = self._build_kv_error_map(match_condition) + error_map.update(kwargs.pop("error_map", {}) or {}) + + if continuation_token: + _request = self._prepare_continuation_request( + "GET", + continuation_token, + sync_token=sync_token, + accept_datetime=accept_datetime, + etag=etag, + match_condition=match_condition, + headers=_headers, + ) + else: + _request = build_feature_flag_client_get_feature_flags_request( + name=name, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + _request = self._format_request(_request) + + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=False, **kwargs + ) + response = pipeline_response.http_response + + valid_status_codes = [200] + if etag is not None and match_condition is MatchConditions.IfModified: + valid_status_codes.append(304) + + if response.status_code not in valid_status_codes: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _deserialize(_models.Error, response.json()) + raise HttpResponseError(response=response, model=error) + + response_headers = response.headers + result = json.loads("{}") + if response.status_code != 304: + result = pipeline_response.http_response.json() + else: + unparsed_link = pipeline_response.http_response.headers.get("Link") + next_link = None + if unparsed_link: + next_link = unparsed_link[1 : unparsed_link.index(">")] + result["@nextLink"] = next_link + + if cls: + return cls(pipeline_response, result, response_headers) + + return result + __all__: List[str] = [ "_AzureAppConfigurationClientOperationsMixin" diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_patch.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_patch.py index e5fd5ebe5f28..aac39692aed3 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_patch.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/operations/_patch.py @@ -31,6 +31,7 @@ ClsType, build_azure_app_configuration_get_key_values_request, build_azure_app_configuration_check_key_values_request, + build_feature_flag_client_get_feature_flags_request, prep_if_match, prep_if_none_match, ) @@ -352,6 +353,123 @@ def check_key_values_in_one_page( return result + @distributed_trace + def get_feature_flags_in_one_page( + self, + *, + name: Optional[str] = None, + label: Optional[str] = None, + sync_token: Optional[str] = None, + after: Optional[str] = None, + accept_datetime: Optional[str] = None, + select: Optional[List[Union[str, _models.FeatureFlagFields]]] = None, + tags: Optional[List[str]] = None, + etag: Optional[str] = None, + match_condition: Optional[MatchConditions] = None, + continuation_token: Optional[str] = None, + **kwargs: Any, + ) -> dict: + """Gets a list of feature flags in one page. + + Gets a list of feature flags in one page. + + :keyword name: A filter used to match feature flag names. Default value is None. + :paramtype name: str + :keyword label: A filter used to match labels. Default value is None. + :paramtype label: str + :keyword sync_token: Used to guarantee real-time consistency between requests. Default value is + None. + :paramtype sync_token: str + :keyword after: Instructs the server to return elements that appear after the element referred + to by the specified token. Default value is None. + :paramtype after: str + :keyword accept_datetime: Requests the server to respond with the state of the resource at the + specified time. Default value is None. + :paramtype accept_datetime: str + :keyword select: Used to select what fields are present in the returned resource(s). Default + value is None. + :paramtype select: list[str or ~azure.appconfiguration.models.FeatureFlagFields] + :keyword tags: A filter used to query by tags. Default value is None. + :paramtype tags: list[str] + :keyword etag: check if resource is changed. Set None to skip checking etag. Default value is + None. + :paramtype etag: str + :keyword match_condition: The match condition to use upon the etag. Default value is None. + :paramtype match_condition: ~azure.core.MatchConditions + :param str continuation_token: An opaque continuation token. + :return: A dict containing ``items`` (list of feature flags) and ``@nextLink`` (pagination URL or None). + :rtype: dict + :raises ~azure.core.exceptions.HttpResponseError: + """ + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[List[_models.FeatureFlag]] = kwargs.pop("cls", None) + + error_map = self._build_kv_error_map(match_condition) + error_map.update(kwargs.pop("error_map", {}) or {}) + + if continuation_token: + _request = self._prepare_continuation_request( + "GET", + continuation_token, + sync_token=sync_token, + accept_datetime=accept_datetime, + etag=etag, + match_condition=match_condition, + headers=_headers, + ) + else: + _request = build_feature_flag_client_get_feature_flags_request( + name=name, + label=label, + sync_token=sync_token, + after=after, + accept_datetime=accept_datetime, + select=select, + tags=tags, + etag=etag, + match_condition=match_condition, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + _request = self._format_request(_request) + + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=False, **kwargs + ) + response = pipeline_response.http_response + + valid_status_codes = [200] + if etag is not None and match_condition is MatchConditions.IfModified: + valid_status_codes.append(304) + + if response.status_code not in valid_status_codes: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = None + try: + error = _deserialize(_models.Error, response.json()) + except (ValueError, KeyError): + pass + raise HttpResponseError(response=response, model=error) + + response_headers = response.headers + result = json.loads("{}") + if response.status_code != 304: + result = pipeline_response.http_response.json() + else: + unparsed_link = pipeline_response.http_response.headers.get("Link") + next_link = None + if unparsed_link: + next_link = unparsed_link[1 : unparsed_link.index(">")] + result["@nextLink"] = next_link + + if cls: + return cls(pipeline_response, result, response_headers) + + return result + __all__: List[str] = [ "_AzureAppConfigurationClientOperationsMixin" diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py index a07edb91cf34..cfbd1761970b 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py @@ -1427,3 +1427,285 @@ def by_page(self, continuation_token: Optional[str] = None, *, match_conditions: self._kwargs["etags"] = match_conditions self._kwargs["match_condition"] = MatchConditions.IfModified return self._page_iterator_class(continuation_token=continuation_token, *self._args, **self._kwargs) + + +class FeatureFlagPropertiesPagedBase: # pylint:disable=too-many-instance-attributes + """Base class for iterable of FeatureFlag properties.""" + + etag: str + """The current etag""" + _etags: List[str] + """The etag expected for the pages.""" + _current_etag: int = 0 + """Current index in the etags list.""" + + def __init__(self, command: Callable, **kwargs: Any): + """Initialize common attributes for paged feature flags. + + :param command: The command to execute for pagination. + :type command: Callable + """ + self._command = command + self._name = kwargs.get("name") + self._label = kwargs.get("label") + self._accept_datetime = kwargs.get("accept_datetime") + self._select = kwargs.get("select") + self._tags = kwargs.get("tags") + self._etags: List[str] = kwargs.get("etags", []) + self._current_etag = 0 + self._match_condition = kwargs.get("match_condition") + self._deserializer = lambda objs: [ + FeatureFlag._from_generated(x) for x in objs # pylint:disable=protected-access + ] + + def _next_etag(self) -> Optional[str]: + """Get the next etag from the list and increment the current position. + + :return: The next etag if available, otherwise None. + :rtype: str or None + """ + if not self._etags or self._current_etag >= len(self._etags): + return None + etag = self._etags[self._current_etag] + self._current_etag += 1 + return etag + + def _extract_data_cb_base(self, get_next_return) -> tuple: + """Extract pagination data from the response. + + :param get_next_return: Tuple of (deserialized response, response headers) + :type get_next_return: tuple + :return: Tuple of (next_link, page iterator or None) + :rtype: tuple + """ + deserialized, response_headers = get_next_return + + # Set etag from response headers, or fall back to expected etag if available + self.etag = response_headers.get("ETag") + if self._etags and self._current_etag > 0: + # There was a 304 Not Modified response, we need to set the etag + self.etag = response_headers.get("ETag", self._etags[self._current_etag - 1]) + + next_link = deserialized.get("@nextLink") + + if "items" in deserialized: + list_of_elem = _deserialize(List[_GeneratedFeatureFlag], deserialized["items"]) + return next_link, iter(self._deserializer(list_of_elem)) + + # No items found in the response, skipping the page + return next_link, None + + +class FeatureFlagPropertiesPaged( + FeatureFlagPropertiesPagedBase, PageIterator +): # pylint:disable=too-many-instance-attributes + """An iterable of FeatureFlag properties.""" + + def __init__(self, command: Callable, **kwargs: Any): + super().__init__(command, **kwargs) + PageIterator.__init__( + self, + self._get_next_cb, + self._extract_data_cb, + continuation_token=kwargs.get("continuation_token"), + ) + + def _get_next_cb(self, continuation_token, **kwargs): + etag = self._next_etag() + return self._command( + name=self._name, + label=self._label, + accept_datetime=self._accept_datetime, + select=self._select, + tags=self._tags, + etag=etag, + match_condition=self._match_condition, + continuation_token=continuation_token, + cls=kwargs.pop("cls", None) or _return_deserialized_and_headers, + ) + + def _extract_data_cb(self, get_next_return): + return self._extract_data_cb_base(get_next_return) + + def __next__(self) -> Iterator[ReturnType]: + """Get the next page in the iterator. + + :returns: An iterator of objects in the next page. + :rtype: iterator[ReturnType] + :raises StopIteration: If there are no more pages to return. + :raises AzureError: If the request fails. + """ + # Is the exact same method as `PageIterator`, excluding the if statement before the return. + if self.continuation_token is None and self._did_a_call_already: + raise StopIteration("End of paging") + try: + self._response = self._get_next(self.continuation_token) + except AzureError as error: + if not error.continuation_token: + error.continuation_token = self.continuation_token + raise + + self._did_a_call_already = True + + self.continuation_token, self._current_page = self._extract_data(self._response) + + # App Config's addition to skip empty pages + if self._current_page is None: + # We skip over pages that are empty, change from mach conditions + return self.__next__() + return iter(self._current_page) + + +class FeatureFlagPropertiesPagedAsync( + FeatureFlagPropertiesPagedBase, AsyncPageIterator +): # pylint:disable=too-many-instance-attributes + """An iterable of FeatureFlag properties.""" + + def __init__(self, command: Callable, **kwargs: Any): + FeatureFlagPropertiesPagedBase.__init__(self, command, **kwargs) + AsyncPageIterator.__init__( + self, + self._get_next_cb, + self._extract_data_cb, + continuation_token=kwargs.get("continuation_token"), + ) + + async def _get_next_cb(self, continuation_token, **kwargs): + etag = self._next_etag() + return await self._command( + name=self._name, + label=self._label, + accept_datetime=self._accept_datetime, + select=self._select, + tags=self._tags, + etag=etag, + match_condition=self._match_condition, + continuation_token=continuation_token, + cls=kwargs.pop("cls", None) or _return_deserialized_and_headers, + ) + + async def _extract_data_cb(self, get_next_return): + return self._extract_data_cb_base(get_next_return) + + async def __anext__(self) -> AsyncIterator[ReturnType]: + """Get the next page in the iterator. + + :returns: An iterator of objects in the next page. + :rtype: iterator[ReturnType] + :raises StopIteration: If there are no more pages to return. + :raises AzureError: If the request fails. + """ + # Is the exact same method as `PageIterator`, excluding the if statement before the return. + if self.continuation_token is None and self._did_a_call_already: + raise StopAsyncIteration("End of paging") + try: + self._response = await self._get_next(self.continuation_token) + except AzureError as error: + if not error.continuation_token: + error.continuation_token = self.continuation_token + raise + + self._did_a_call_already = True + + self.continuation_token, self._current_page = await self._extract_data(self._response) + + # App Config's addition to skip empty pages + if self._current_page is None: + # We skip over pages that are empty, change from mach conditions + return await self.__anext__() + + # If current_page was a sync list, wrap it async-like + if isinstance(self._current_page, collections.abc.Iterable): + self._current_page = AsyncList(self._current_page) + + return self._current_page + + +class FeatureFlagPaged(ItemPaged[FeatureFlag]): + """ + An iterable of FeatureFlags that supports etag-based change detection. + + This class extends ItemPaged to provide efficient monitoring of feature flag changes + by using ETags. When used with the `match_conditions` parameter in `by_page()`, + it only returns pages that have changed since the provided ETags were collected. + + Example: + + .. code-block:: python + + # Get initial page ETags + flags = client.list_feature_flags(name_filter="sample_*") + match_conditions = [page.etag for page in flags.by_page()] + + # Later, check for changes - only changed pages are returned + flags = client.list_feature_flags(name_filter="sample_*") + for page in flags.by_page(match_conditions=match_conditions): + # Process only changed pages + pass + """ + + def by_page(self, continuation_token: Optional[str] = None, *, match_conditions: Optional[List[str]] = None) -> Any: + """Get an iterator of pages of objects, instead of an iterator of objects. + + :param str continuation_token: + An opaque continuation token. This value can be retrieved from the + continuation_token field of a previous generator object. If specified, + this generator will begin returning results from this point. + :keyword match_conditions: A list of etags to check for changes. If provided, the iterator will + check each page against the corresponding etag and only return pages that have changed. + :paramtype match_conditions: list[str] or None + :returns: An iterator of pages (themselves iterator of objects) + :rtype: iterator[iterator[ReturnType]] + """ + if "match_conditions" not in self._kwargs and match_conditions: + self._kwargs["etags"] = match_conditions + self._kwargs["match_condition"] = MatchConditions.IfModified + return self._page_iterator_class(continuation_token=continuation_token, *self._args, **self._kwargs) + + +class AsyncFeatureFlagPaged(AsyncItemPaged[FeatureFlag]): + """ + An async iterable of FeatureFlags that supports etag-based change detection. + + This class provides asynchronous iteration over feature flags, with optional support for + etag-based change detection. By supplying a list of etags via the `match_conditions` parameter to + the `by_page` method, you can efficiently detect and retrieve only those pages that have changed + since your last retrieval. + + Example: + + .. code-block:: python + + async for flag in AsyncFeatureFlagPaged(...): + # Process each feature flag asynchronously + print(flag) + + # To iterate by page and use etag-based change detection: + etags = ["etag1", "etag2", "etag3"] + async for page in paged.by_page(match_conditions=etags): + async for flag in page: + print(flag) + + When `match_conditions` is provided, each page is checked against the corresponding etag. + If the page has not changed (HTTP 304), it is skipped. If the page has changed (HTTP 200), + the new page is returned. This allows efficient polling for changes without retrieving + unchanged data. + """ + + def by_page(self, continuation_token: Optional[str] = None, *, match_conditions: Optional[List[str]] = None) -> Any: + """Get an async iterator of pages of objects, instead of an iterator of objects. + + :param str continuation_token: + An opaque continuation token. This value can be retrieved from the + continuation_token field of a previous generator object. If specified, + this generator will begin returning results from this point. + :keyword match_conditions: A list of etags to check for changes. If provided, the iterator will + check each page against the corresponding etag and only return pages that have changed. + :paramtype match_conditions: list[str] or None + :returns: An async iterator of pages (themselves iterator of objects) + :rtype: AsyncIterator[AsyncIterator[ReturnType]] + """ + if "match_conditions" not in self._kwargs and match_conditions: + self._kwargs["etags"] = match_conditions + self._kwargs["match_condition"] = MatchConditions.IfModified + return self._page_iterator_class(continuation_token=continuation_token, *self._args, **self._kwargs) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py index 46a3ac5371e6..9ad28e4a7a92 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_feature_flag_client_async.py @@ -5,6 +5,7 @@ # license information. # ------------------------------------------------------------------------- from datetime import datetime +import functools from typing import Any, Dict, List, Optional, Union from azure.core import MatchConditions from azure.core.async_paging import AsyncItemPaged @@ -21,7 +22,12 @@ from .._query_param_policy import QueryParamPolicy from .._generated.aio import AzureAppConfigurationClient as AzureAppConfigurationClientGenerated from .._generated.models import LabelFields -from .._models import FeatureFlag, ConfigurationSettingLabel +from .._models import ( + FeatureFlag, + ConfigurationSettingLabel, + AsyncFeatureFlagPaged, + FeatureFlagPropertiesPagedAsync, +) from .._audience import get_audience, DEFAULT_SCOPE_SUFFIX from .._utils import parse_connection_string from .._audience_error_handling_policy import AudienceErrorHandlingPolicy @@ -150,7 +156,7 @@ def list_feature_flags( tags_filter: Optional[List[str]] = None, accept_datetime: Optional[Union[datetime, str]] = None, **kwargs: Any, - ) -> AsyncItemPaged["FeatureFlag"]: + ) -> AsyncFeatureFlagPaged: """ Find the FeatureFlag objects, optionally filtered by name, label, tags and accept_datetime. @@ -164,23 +170,39 @@ def list_feature_flags( :keyword accept_datetime: Retrieve FeatureFlag that existed at this datetime :paramtype accept_datetime: ~datetime.datetime or str or None :return: An async iterator of :class:`~azure.appconfiguration.FeatureFlag` - :rtype: ~azure.core.paging.AsyncItemPaged[~azure.appconfiguration.FeatureFlag] + :rtype: ~azure.appconfiguration.AsyncFeatureFlagPaged :raises: :class:`~azure.core.exceptions.HttpResponseError` + + Example + + .. code-block:: python + + # List feature flags + feature_flags = async_client.list_feature_flags() + async for flag in feature_flags: + print(flag.name, flag.enabled) + + # Detect changes across pages using etags + feature_flags = async_client.list_feature_flags(name_filter="sample_*") + match_conditions = [page.etag async for page in feature_flags.by_page()] + + feature_flags = async_client.list_feature_flags(name_filter="sample_*") + async for page in feature_flags.by_page(match_conditions=match_conditions): + # Only pages that changed are returned + async for flag in page: + print(flag.name, flag.enabled) """ if isinstance(accept_datetime, datetime): accept_datetime = str(accept_datetime) - # Call the generated method and wrap results to convert to SDK FeatureFlag - def convert_to_sdk(objs): - return [FeatureFlag._from_generated(obj) for obj in objs] # pylint:disable=protected-access - - return self._impl.feature_flag_client.get_feature_flags( # type: ignore[return-value] + command = functools.partial(self._impl.get_feature_flags_in_one_page, **kwargs) # type: ignore[attr-defined] # pylint: disable=no-member + return AsyncFeatureFlagPaged( + command, name=name_filter, label=label_filter, accept_datetime=accept_datetime, tags=tags_filter, - cls=convert_to_sdk, - **kwargs, + page_iterator_class=FeatureFlagPropertiesPagedAsync, ) @distributed_trace_async From 16737c7dadf3bc0eee542257413b661007d1e2e3 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 21 Jul 2026 11:11:38 -0700 Subject: [PATCH 15/25] page based etag ff samples and tests --- .../azure-appconfiguration/assets.json | 2 +- .../list_feature_flags_by_page_sample.py | 86 +++++++++++++++++++ .../tests/test_feature_flags.py | 54 ++++++++++++ .../tests/test_feature_flags_async.py | 53 ++++++++++++ 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration/samples/list_feature_flags_by_page_sample.py diff --git a/sdk/appconfiguration/azure-appconfiguration/assets.json b/sdk/appconfiguration/azure-appconfiguration/assets.json index 8fac311ad3f1..7308e89122e8 100644 --- a/sdk/appconfiguration/azure-appconfiguration/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration", - "Tag": "python/appconfiguration/azure-appconfiguration_10cccad6ed" + "Tag": "python/appconfiguration/azure-appconfiguration_1f78b37d37" } diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/list_feature_flags_by_page_sample.py b/sdk/appconfiguration/azure-appconfiguration/samples/list_feature_flags_by_page_sample.py new file mode 100644 index 000000000000..6b8f20318d0a --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/samples/list_feature_flags_by_page_sample.py @@ -0,0 +1,86 @@ +# coding: utf-8 + +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +""" +FILE: list_feature_flags_by_page_sample.py + +DESCRIPTION: + This sample demos how to detect feature flag changes using etag-based change detection. + By collecting page ETags from list_feature_flags and passing them to by_page via the + match_conditions parameter, unchanged pages are skipped (HTTP 304), making it efficient + to detect whether feature flags have changed. + +USAGE: python list_feature_flags_by_page_sample.py + + Set the environment variables with your own values before running the sample: + 1) APPCONFIGURATION_ENDPOINT_STRING: Endpoint URL used to access the Azure App Configuration. +""" + +import os +from azure.appconfiguration import FeatureFlagClient, FeatureFlag +from azure.identity import DefaultAzureCredential + + +def main(): + ENDPOINT = os.environ["APPCONFIGURATION_ENDPOINT_STRING"] + credential = DefaultAzureCredential() + + # Create a feature flag client + client = FeatureFlagClient(base_url=ENDPOINT, credential=credential) + + # Set up sample feature flags + client.set_feature_flag(FeatureFlag(name="ListFeature1", enabled=True, tags={"env": "dev"})) + client.set_feature_flag(FeatureFlag(name="ListFeature2", enabled=False, tags={"env": "dev"})) + + # [START list_feature_flags] + # Use list_feature_flags to get page ETags. + # These ETags identify the current state of each page of feature flags. + print("Collecting feature flag page ETags...") + items = client.list_feature_flags(name_filter="ListFeature*") + iterator = items.by_page() + etags = [] + for _ in iterator: + print(f" Page ETag: {iterator.etag}") + etags.append(iterator.etag) + + # Later, use the collected ETags to check if any pages have changed. + # Pages that haven't changed will be skipped (HTTP 304), so only changed pages are returned. + print("\nChecking for changes using ETags...") + items = client.list_feature_flags(name_filter="ListFeature*") + has_changes = False + iterator = items.by_page(match_conditions=etags) + for _ in iterator: + has_changes = True + print(f" Page changed! New ETag: {iterator.etag}") + + if not has_changes: + print(" No changes detected.") + + # Now modify a feature flag and check again + print("\nModifying a feature flag...") + client.set_feature_flag(FeatureFlag(name="ListFeature1", enabled=False, tags={"env": "dev"})) + + print("Checking for changes after modification...") + items = client.list_feature_flags(name_filter="ListFeature*") + has_changes = False + iterator = items.by_page(match_conditions=etags) + for _ in iterator: + has_changes = True + print(f" Page changed! New ETag: {iterator.etag}") + + if not has_changes: + print(" No changes detected.") + # [END list_feature_flags] + + # Clean up + client.delete_feature_flag("ListFeature1") + client.delete_feature_flag("ListFeature2") + + +if __name__ == "__main__": + main() diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py index 78b0b4423ce8..43b33b523d1a 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags.py @@ -430,3 +430,57 @@ def test_get_feature_flag_wrong_label(self, appconfiguration_endpoint_string): assert result is None finally: client.delete_feature_flag("test_feature_wrong_label", label="real_label") + + @AppConfigPreparer() + @recorded_by_proxy + def test_monitor_feature_flags_by_page_etag(self, appconfiguration_endpoint_string): + """Test etag-based change detection across feature flag pages via by_page(match_conditions=...).""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + # prepare 200 feature flags -> multiple pages (100 feature flags per page) + for i in range(200): + client.set_feature_flag( + FeatureFlag(name=f"monitor_ff_{str(i)}", enabled=True, label=f"monitor_label_{str(i)}") + ) + + # collect current page etags + match_conditions = [] + items = client.list_feature_flags(name_filter="monitor_ff_*", label_filter="monitor_label_*") + iterator = items.by_page() + for _ in iterator: + match_conditions.append(iterator.etag) + assert len(match_conditions) >= 2 + + # monitor without changes - unchanged pages are skipped (HTTP 304), so no pages are yielded + items = client.list_feature_flags(name_filter="monitor_ff_*", label_filter="monitor_label_*") + iterator = items.by_page(match_conditions=match_conditions) + changed_pages = list(iterator) + assert len(changed_pages) == 0 + + # modify a feature flag that lives on the first page + client.set_feature_flag(FeatureFlag(name="monitor_ff_0", enabled=False, label="monitor_label_0")) + + # monitor with the old etags - only the changed page is yielded + items = client.list_feature_flags(name_filter="monitor_ff_*", label_filter="monitor_label_*") + iterator = items.by_page(match_conditions=match_conditions) + changed_pages = list(iterator) + assert len(changed_pages) >= 1 + + # collect fresh etags; the first page etag changed while the page count is unchanged + new_match_conditions = [] + items = client.list_feature_flags(name_filter="monitor_ff_*", label_filter="monitor_label_*") + iterator = items.by_page() + for _ in iterator: + new_match_conditions.append(iterator.etag) + assert new_match_conditions[0] != match_conditions[0] + assert len(new_match_conditions) == len(match_conditions) + + # monitoring with the fresh etags yields no changed pages + items = client.list_feature_flags(name_filter="monitor_ff_*", label_filter="monitor_label_*") + iterator = items.by_page(match_conditions=new_match_conditions) + assert len(list(iterator)) == 0 + + # clean up + for i in range(200): + client.delete_feature_flag(f"monitor_ff_{str(i)}", label=f"monitor_label_{str(i)}") diff --git a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py index 6a611e38da74..edced9203575 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_feature_flags_async.py @@ -447,3 +447,56 @@ async def test_get_feature_flag_wrong_label(self, appconfiguration_endpoint_stri finally: await client.delete_feature_flag("test_feature_wrong_label", label="real_label") await client.close() + + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_monitor_feature_flags_by_page_etag(self, appconfiguration_endpoint_string): + """Test etag-based change detection across feature flag pages via by_page(match_conditions=...).""" + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + client = self.create_client(appconfiguration_endpoint_string) + + # prepare 200 feature flags -> multiple pages (100 feature flags per page) + for i in range(200): + await client.set_feature_flag( + FeatureFlag(name=f"monitor_ff_{str(i)}", enabled=True, label=f"monitor_label_{str(i)}") + ) + + # collect current page etags + match_conditions = [] + items = client.list_feature_flags(name_filter="monitor_ff_*", label_filter="monitor_label_*") + iterator = items.by_page() + async for _ in iterator: + match_conditions.append(iterator.etag) + assert len(match_conditions) >= 2 + + # monitor without changes - unchanged pages are skipped (HTTP 304), so no pages are yielded + items = client.list_feature_flags(name_filter="monitor_ff_*", label_filter="monitor_label_*") + changed_pages = [page async for page in items.by_page(match_conditions=match_conditions)] + assert len(changed_pages) == 0 + + # modify a feature flag that lives on the first page + await client.set_feature_flag(FeatureFlag(name="monitor_ff_0", enabled=False, label="monitor_label_0")) + + # monitor with the old etags - only the changed page is yielded + items = client.list_feature_flags(name_filter="monitor_ff_*", label_filter="monitor_label_*") + changed_pages = [page async for page in items.by_page(match_conditions=match_conditions)] + assert len(changed_pages) >= 1 + + # collect fresh etags; the first page etag changed while the page count is unchanged + new_match_conditions = [] + items = client.list_feature_flags(name_filter="monitor_ff_*", label_filter="monitor_label_*") + iterator = items.by_page() + async for _ in iterator: + new_match_conditions.append(iterator.etag) + assert new_match_conditions[0] != match_conditions[0] + assert len(new_match_conditions) == len(match_conditions) + + # monitoring with the fresh etags yields no changed pages + items = client.list_feature_flags(name_filter="monitor_ff_*", label_filter="monitor_label_*") + remaining = [page async for page in items.by_page(match_conditions=new_match_conditions)] + assert len(remaining) == 0 + + # clean up + for i in range(200): + await client.delete_feature_flag(f"monitor_ff_{str(i)}", label=f"monitor_label_{str(i)}") + await client.close() From 2907d04e316093855db6041968c4a6bdcd0c739d Mon Sep 17 00:00:00 2001 From: Yuan Qu Date: Wed, 22 Jul 2026 14:02:55 -0700 Subject: [PATCH 16/25] Load feature flags from the dedicated feature flag resource endpoint in the provider Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CHANGELOG.md | 5 +- .../azure-appconfiguration-provider/README.md | 93 ++++++++ .../assets.json | 2 +- .../_azureappconfigurationprovider.py | 32 ++- .../_azureappconfigurationproviderbase.py | 212 ++++++++++++++++- .../provider/_client_manager.py | 98 ++++++++ .../appconfiguration/provider/_constants.py | 9 + .../provider/_request_tracing_context.py | 29 ++- .../appconfiguration/provider/_version.py | 2 +- .../provider/aio/_async_client_manager.py | 101 +++++++- .../_azureappconfigurationproviderasync.py | 32 ++- .../samples/README.md | 2 + .../async_feature_flag_resource_sample.py | 80 +++++++ .../samples/feature_flag_resource_sample.py | 65 ++++++ .../azure-appconfiguration-provider/setup.py | 2 +- ...t_async_provider_feature_flag_resources.py | 153 ++++++++++++ .../tests/asynctestcase.py | 24 +- .../test_azureappconfigurationproviderbase.py | 218 ++++++++++++++++++ .../test_configuration_client_manager.py | 176 ++++++++++++++ .../test_provider_feature_flag_resources.py | 150 ++++++++++++ .../tests/testcase.py | 40 +++- 21 files changed, 1496 insertions(+), 29 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/samples/async_feature_flag_resource_sample.py create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/samples/feature_flag_resource_sample.py create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider_feature_flag_resources.py create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_resources.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md index 54481723b4d3..5d36e017dc8a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md @@ -1,9 +1,11 @@ # 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 @@ -11,6 +13,7 @@ ### 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) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/README.md index 8500a5be6a22..7780b20300a2 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/README.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/README.md @@ -377,6 +377,48 @@ config = load( +### Loading Feature Flags as Resources + +Feature flags can also be created using the dedicated feature flag resource endpoint (via `FeatureFlagClient`/`FeatureFlag` in `azure-appconfiguration`), instead of as classic key-value configuration settings. The provider loads both kinds side by side into the same `feature_management.feature_flags` list, with feature flag resources 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`. + + + +```python +from azure.appconfiguration.provider import load + +# Feature flags loaded from the feature flag resource 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"] +resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") +print(resource_beta["enabled"]) +``` + + + +The same `SettingSelector` used to filter key-value based feature flags also filters feature flag resources, by name, label, or tags. Note that selectors with a `snapshot_name` are not currently supported by the feature flag resource endpoint and are skipped when loading feature flag resources. + + + +```python +from azure.appconfiguration.provider import load, SettingSelector + +# The same SettingSelector used to filter key-value based feature flags also filters feature flag +# resources, by name/label/tags. +config = load( + endpoint=endpoint, + credential=credential, + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="Resource*")], + **kwargs, +) +feature_flags = config["feature_management"]["feature_flags"] +resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") +print(resource_beta["enabled"]) +``` + + + ## 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. @@ -469,6 +511,57 @@ 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) + +The tests for this package are under the `tests/` directory and are split into two categories: + +* **Unit tests** (e.g. `tests/test_azureappconfigurationproviderbase.py`, `tests/test_configuration_client_manager.py`) — exercise internal logic in isolation using mocked clients. These do not require any App Configuration store, network access, or environment variables, and can be run at any time with no setup. +* **Integration tests** (e.g. `tests/test_provider.py`, `tests/test_provider_feature_flag_resources.py`, and their `tests/aio/` async equivalents) — exercise the provider end-to-end against an Azure App Configuration store. These tests are built on [`devtools_testutils`](https://github.com/Azure/azure-sdk-for-python/tree/main/eng/tools/azure-sdk-tools/devtools_testutils) and each test method is decorated with `@recorded_by_proxy` / `@recorded_by_proxy_async`, which route the test's HTTP traffic through the [test proxy](https://github.com/Azure/azure-sdk-tools/tree/main/tools/test-proxy) tool. + +### Live tests vs. recorded (playback) tests + +Whether an integration test makes a real network call or replays a recording is controlled entirely by the `AZURE_TEST_RUN_LIVE` environment variable, not by anything in this package's code: + +* `AZURE_TEST_RUN_LIVE=true` — Tests run in **live/record mode**. The test proxy forwards requests to the real endpoint configured via your environment variables (see below), and (unless `AZURE_SKIP_LIVE_RECORDING=true` is also set) records the request/response pairs as new recording files for use in future playback runs. +* `AZURE_TEST_RUN_LIVE` unset or `false` (the default, and what CI uses) — Tests run in **playback mode**. The test proxy replays the existing recordings instead of contacting the real service, so **no network calls are made** and no live App Configuration store is required. + +Recordings themselves are not stored directly in this repository — they live in the separate [`Azure/azure-sdk-assets`](https://github.com/Azure/azure-sdk-assets) repo, and this package's `assets.json` file pins the exact recordings revision (`Tag`) that CI uses. If you add or change integration tests, you need to generate new recordings and publish them: + +1. Run the affected tests with `AZURE_TEST_RUN_LIVE=true` (and without `AZURE_SKIP_LIVE_RECORDING`) so the test proxy records real interactions to local recording files. +2. From the repo root, push the new/updated recordings to the assets repo: + + ```bash + dotnet tool run test-proxy push -a sdk/appconfiguration/azure-appconfiguration-provider/assets.json + ``` + + This uploads the changed recordings and updates the `Tag` field in `assets.json`. +3. Commit the updated `assets.json` as part of your PR — this is what allows CI (which always runs in playback mode) to pick up the new recordings. + +Only re-record tests you added or intentionally changed; unrelated existing recordings don't need to be regenerated. + +### Environment variables for local testing + +To run the integration tests locally in live mode, create a `.env` file at the repository root (it is automatically loaded by `devtools_testutils`) with the following variables: + +``` +AZURE_TEST_RUN_LIVE=true +APPCONFIGURATION_CONNECTION_STRING= +APPCONFIGURATION_ENDPOINT_STRING=.azconfig.io> +APPCONFIGURATION_KEY_VAULT_REFERENCE= +APPCONFIGURATION_KEY_VAULT_REFERENCE2= +APPCONFIGURATION_KEYVAULT_SECRET_URL= +APPCONFIGURATION_KEYVAULT_SECRET_URL2= +``` + +Notes: + +* For key vault URI, you can create a secret in Azure Key Vault service. The key vault URI is the *Secret Identifier*, without the final version number. For example, if the secret identifier is `https://some_secret.vault.azure.net/secrets/fake-secret/30d8830ec5ed4a428d311292a826f452`, the key vault URI should be `https://some_secret.vault.azure.net/secrets/fake-secret/`. +* Authentication for Entra ID-based tests relies on your local Azure CLI login (`az login`); make sure you're signed in to the subscription that contains your App Configuration store. +* Add `AZURE_SKIP_LIVE_RECORDING=true` if you want to run tests live against the real store without generating/overwriting recording files (useful for a quick sanity check). +* Omit `AZURE_TEST_RUN_LIVE` (or set it to `false`) to run the same tests in playback mode against existing recordings — this does not require any of the App Configuration environment variables above. + ## Next steps Check out our Django and Flask examples to see how to use the provider in a web application. diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index b9e5f6a80d69..ab4ac3e8fb87 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -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" } 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..6161477e40b2 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -17,6 +17,7 @@ ) from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module ConfigurationSetting, + FeatureFlag, FeatureFlagConfigurationSetting, SecretReferenceConfigurationSetting, ) @@ -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 + feature_flag_resources: Optional[List[FeatureFlag]] = None # Timer needs to be reset even if no refresh happened if time had passed configuration_refresh_attempted = False @@ -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]] = [] + feature_flag_resource_etags: List[List[str]] = [] try: if self._refresh_enabled and not self._watched_settings and self._refresh_timer.needs_refresh(): configuration_refresh_attempted = True @@ -148,6 +151,16 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f self._feature_flag_selectors, headers=headers, **kwargs ) + # Feature flag resources 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._feature_flag_resource_etags or client.check_feature_flag_resource_etags( + self._feature_flag_selectors, self._feature_flag_resource_etags, headers=headers, **kwargs + ): + feature_flag_resources, feature_flag_resource_etags = client.load_feature_flag_resources( + self._feature_flag_selectors, headers=headers, **kwargs + ) + # Default to existing settings if no refresh occurred processed_settings = self._dict @@ -157,7 +170,9 @@ 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_feature_flags( + processed_settings, processed_feature_flags, feature_flags, feature_flag_resources + ) self._dict = processed_settings if settings_refreshed: self._page_etags = page_etags @@ -165,12 +180,14 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f self._watched_settings.update(updated_watched_settings) if feature_flags is not None: self._feature_flag_page_etags = feature_flag_page_etags + if feature_flag_resources is not None: + self._feature_flag_resource_etags = feature_flag_resource_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 feature_flag_resources) and self._on_refresh_success: self._on_refresh_success() except AzureError as e: logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint) @@ -278,6 +295,7 @@ 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]] = [] + feature_flag_resource_etags: List[List[str]] = [] if self._feature_flag_enabled: feature_flags: List[FeatureFlagConfigurationSetting] feature_flags, feature_flag_page_etags = client.load_feature_flags( @@ -285,7 +303,14 @@ def _try_initialize(self, startup_exceptions: List[Exception], **kwargs: Any) -> headers=headers, **kwargs, ) - processed_settings = self._process_feature_flags(processed_settings, [], feature_flags) + feature_flag_resources, feature_flag_resource_etags = client.load_feature_flag_resources( + self._feature_flag_selectors, + headers=headers, + **kwargs, + ) + processed_settings = self._process_feature_flags( + processed_settings, [], feature_flags, feature_flag_resources + ) for (key, label), etag in self._watched_settings.items(): if not etag: try: @@ -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._feature_flag_resource_etags = feature_flag_resource_etags return True except AzureError as e: logger.warning("Failed to load configurations from endpoint %s.\n %s", client.endpoint, e.message) 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..d2cc796cdd5c 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -25,6 +25,7 @@ from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module ConfigurationSetting, FeatureFlagConfigurationSetting, + FeatureFlag, ) from ._models import SettingSelector from ._constants import ( @@ -38,6 +39,10 @@ APP_CONFIG_AICC_MIME_PROFILE, FEATURE_MANAGEMENT_KEY, FEATURE_FLAG_KEY, + FEATURE_FLAG_ID_FIELD, + FEATURE_FLAG_NAME_FIELD, + FEATURE_FLAG_KV_REFERENCE_SEGMENT, + FEATURE_FLAG_RESOURCE_REFERENCE_SEGMENT, ) from ._refresh_timer import _RefreshTimer from ._request_tracing_context import _RequestTracingContext @@ -115,6 +120,16 @@ def __init__(self, **kwargs: Any) -> None: self._refresh_enabled = refresh_enabled self._page_etags: List[List[str]] = [] self._feature_flag_page_etags: List[List[str]] = [] + # Per-selector collection ETags for feature flags loaded from the feature flag resource endpoint. This is + # independent of the key-value based feature_flag_page_etags, since the resource endpoint is a separate + # resource type with its own change-detection mechanism. + self._feature_flag_resource_etags: List[List[str]] = [] + # Feature flags are loaded from two independent sources: the classic key-value store, and the newer + # dedicated feature flag resource endpoint. Each source's processed output is cached separately so that a + # refresh of one source does not require re-processing or discarding the other source's data. The two are + # merged (resource-based feature flags take precedence on identifier collision) whenever either changes. + self._processed_kv_feature_flags: List[Dict[str, Any]] = [] + self._processed_resource_feature_flags: List[Dict[str, Any]] = [] self._tracing_context = _RequestTracingContext(kwargs.pop("load_balancing_enabled", False)) self._update_lock = Lock() self._refresh_lock = Lock() @@ -132,7 +147,7 @@ def _update_ff_telemetry_metadata( self, endpoint: str, feature_flag: FeatureFlagConfigurationSetting, feature_flag_value: Dict ): """ - Add telemetry metadata to feature flag values. + Add telemetry metadata to feature flag values loaded from the classic key-value store. :param endpoint: The App Configuration endpoint URL. :type endpoint: str @@ -141,6 +156,61 @@ def _update_ff_telemetry_metadata( :param feature_flag_value: The feature flag value dictionary to update. :type feature_flag_value: Dict[str, Any] """ + self._update_ff_telemetry_metadata_common( + endpoint, + feature_flag.key, + feature_flag.label, + feature_flag.etag, + feature_flag_value, + FEATURE_FLAG_KV_REFERENCE_SEGMENT, + ) + + def _update_ff_resource_telemetry_metadata(self, endpoint: str, feature_flag: FeatureFlag, feature_flag_value: Dict): + """ + Add telemetry metadata to feature flag values loaded from the feature flag resource endpoint. + + :param endpoint: The App Configuration endpoint URL. + :type endpoint: str + :param feature_flag: The feature flag resource. + :type feature_flag: ~azure.appconfiguration.FeatureFlag + :param feature_flag_value: The feature flag value dictionary to update. + :type feature_flag_value: Dict[str, Any] + """ + self._update_ff_telemetry_metadata_common( + endpoint, + feature_flag.name, + feature_flag.label, + feature_flag.etag, + feature_flag_value, + FEATURE_FLAG_RESOURCE_REFERENCE_SEGMENT, + ) + + def _update_ff_telemetry_metadata_common( # pylint: disable=too-many-positional-arguments + self, + endpoint: str, + identifier: str, + label: Optional[str], + etag: Optional[str], + feature_flag_value: Dict, + reference_path_segment: str, + ): + """ + Add telemetry metadata to a feature flag value dictionary, regardless of which endpoint it was loaded from. + + :param endpoint: The App Configuration endpoint URL. + :type endpoint: str + :param identifier: The identifier of the feature flag (key for key-value based, name for resource-based). + :type identifier: str + :param label: The label of the feature flag. + :type label: Optional[str] + :param etag: The etag of the feature flag. + :type etag: Optional[str] + :param feature_flag_value: The feature flag value dictionary to update. + :type feature_flag_value: Dict[str, Any] + :param reference_path_segment: The path segment to use when building the feature flag reference URL, e.g. + "kv" for key-value based feature flags or "ff" for resource-based feature flags. + :type reference_path_segment: str + """ if TELEMETRY_KEY not in feature_flag_value: # Initialize telemetry dictionary if not present feature_flag_value[TELEMETRY_KEY] = {} @@ -148,15 +218,15 @@ def _update_ff_telemetry_metadata( # Update telemetry metadata for application insights/logging in feature management if METADATA_KEY not in feature_flag_value[TELEMETRY_KEY]: feature_flag_value[TELEMETRY_KEY][METADATA_KEY] = {} - feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ETAG_KEY] = feature_flag.etag + feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ETAG_KEY] = etag if feature_flag_value[TELEMETRY_KEY].get("enabled"): self._tracing_context.uses_telemetry = True if not endpoint.endswith("/"): endpoint += "/" - feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" - if feature_flag.label and not feature_flag.label.isspace(): - feature_flag_reference += f"?label={feature_flag.label}" + feature_flag_reference = f"{endpoint}{reference_path_segment}/{identifier}" + if label and not label.isspace(): + feature_flag_reference += f"?label={label}" feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference allocation_id = self._generate_allocation_id(feature_flag_value) @@ -240,10 +310,14 @@ def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str for v in sorted_variants: allocation_id += f"{base64.b64encode(v.get('name', '').encode()).decode()}," + # Key-value based feature flags store the variant value under "configuration_value". Feature + # flags loaded from the feature flag resource endpoint store it under "value" instead. if "configuration_value" in v: allocation_id += ( f"{json.dumps(v.get('configuration_value', ''), separators=(',', ':'), sort_keys=True)}" ) + elif "value" in v: + allocation_id += f"{json.dumps(v.get('value', ''), separators=(',', ':'), sort_keys=True)}" allocation_id += ";" if sorted_variants: allocation_id = allocation_id[:-1] @@ -369,17 +443,56 @@ def _process_feature_flags( processed_settings: Dict[str, Any], processed_feature_flags: List[Dict[str, Any]], feature_flags: Optional[List[FeatureFlagConfigurationSetting]], + feature_flag_resources: Optional[List[FeatureFlag]] = None, ) -> Dict[str, Any]: - if feature_flags: + if feature_flags or feature_flag_resources: # Reset feature flag usage self._tracing_context.reset_feature_filter_usage() - processed_feature_flags = [self._process_feature_flag(ff) for ff in feature_flags] + + if feature_flags: + self._processed_kv_feature_flags = [self._process_feature_flag(ff) for ff in feature_flags] + + if feature_flag_resources: + self._processed_resource_feature_flags = [ + self._process_feature_flag_resource(ff) for ff in feature_flag_resources + ] + + if feature_flags or feature_flag_resources: + processed_feature_flags = self._merge_feature_flags( + self._processed_kv_feature_flags, self._processed_resource_feature_flags + ) if self._feature_flag_enabled: processed_settings[FEATURE_MANAGEMENT_KEY] = {} processed_settings[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = processed_feature_flags return processed_settings + @staticmethod + def _merge_feature_flags( + kv_feature_flags: List[Dict[str, Any]], resource_feature_flags: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + Merge feature flags loaded from the classic key-value store with feature flags loaded from the feature + flag resource endpoint. Feature flags are matched by their identifier (``id`` for key-value based feature + flags, ``name`` for resource-based feature flags). When both sources contain a feature flag with the same + identifier, the resource-based feature flag takes precedence. + + :param kv_feature_flags: The feature flags loaded from the classic key-value store. + :type kv_feature_flags: List[Dict[str, Any]] + :param resource_feature_flags: The feature flags loaded from the feature flag resource endpoint. + :type resource_feature_flags: List[Dict[str, Any]] + :return: The merged list of feature flags. + :rtype: List[Dict[str, Any]] + """ + merged: Dict[str, Dict[str, Any]] = {} + for ff in kv_feature_flags: + identifier = ff.get(FEATURE_FLAG_ID_FIELD) + merged[identifier] = ff + for ff in resource_feature_flags: + identifier = ff.get(FEATURE_FLAG_NAME_FIELD) + merged[identifier] = ff + return list(merged.values()) + def _process_feature_flag(self, feature_flag: FeatureFlagConfigurationSetting) -> Dict[str, Any]: try: feature_flag_value = json.loads(feature_flag.value) @@ -390,6 +503,91 @@ def _process_feature_flag(self, feature_flag: FeatureFlagConfigurationSetting) - # Feature flag value is not a valid JSON return {} + def _process_feature_flag_resource(self, feature_flag: FeatureFlag) -> Dict[str, Any]: + """ + Convert a feature flag resource, loaded from the feature flag resource endpoint, into a dictionary using + the feature flag resource's native field names. + + :param feature_flag: The feature flag resource. + :type feature_flag: ~azure.appconfiguration.FeatureFlag + :return: The feature flag as a dictionary. + :rtype: Dict[str, Any] + """ + feature_flag_value: Dict[str, Any] = { + FEATURE_FLAG_NAME_FIELD: feature_flag.name, + "enabled": feature_flag.enabled, + } + if feature_flag.label and not feature_flag.label.isspace(): + feature_flag_value["label"] = feature_flag.label + if feature_flag.description: + feature_flag_value["description"] = feature_flag.description + + filter_names: List[Optional[str]] = [] + if feature_flag.conditions: + conditions_value: Dict[str, Any] = {} + if feature_flag.conditions.requirement_type: + conditions_value["requirement_type"] = feature_flag.conditions.requirement_type + if feature_flag.conditions.client_filters: + conditions_value["client_filters"] = [ + {"name": client_filter.name, "parameters": client_filter.parameters} + for client_filter in feature_flag.conditions.client_filters + ] + filter_names = [client_filter.name for client_filter in feature_flag.conditions.client_filters] + if conditions_value: + feature_flag_value["conditions"] = conditions_value + + if feature_flag.variants: + feature_flag_value["variants"] = [ + { + "name": variant.name, + "value": variant.value, + "content_type": variant.content_type, + "status_override": variant.status_override, + } + for variant in feature_flag.variants + ] + + if feature_flag.allocation: + allocation_value: Dict[str, Any] = {} + if feature_flag.allocation.default_when_disabled: + allocation_value["default_when_disabled"] = feature_flag.allocation.default_when_disabled + if feature_flag.allocation.default_when_enabled: + allocation_value["default_when_enabled"] = feature_flag.allocation.default_when_enabled + if feature_flag.allocation.percentile: + allocation_value["percentile"] = [ + { + "variant": percentile.variant, + "percentile_from": percentile.percentile_from, + "percentile_to": percentile.percentile_to, + } + for percentile in feature_flag.allocation.percentile + ] + if feature_flag.allocation.user: + allocation_value["user"] = [ + {"variant": user.variant, "users": user.users} for user in feature_flag.allocation.user + ] + if feature_flag.allocation.group: + allocation_value["group"] = [ + {"variant": group.variant, "groups": group.groups} for group in feature_flag.allocation.group + ] + if feature_flag.allocation.seed: + allocation_value["seed"] = feature_flag.allocation.seed + if allocation_value: + feature_flag_value["allocation"] = allocation_value + + if feature_flag.telemetry: + feature_flag_value["telemetry"] = { + "enabled": feature_flag.telemetry.enabled, + "metadata": dict(feature_flag.telemetry.metadata) if feature_flag.telemetry.metadata else {}, + } + + if feature_flag.tags: + feature_flag_value["tags"] = dict(feature_flag.tags) + + self._update_ff_resource_telemetry_metadata(self._origin_endpoint, feature_flag, feature_flag_value) + self._tracing_context.update_feature_filter_telemetry_by_names(filter_names) + return feature_flag_value + def _update_watched_settings( self, configuration_settings: List[ConfigurationSetting] ) -> Dict[Tuple[str, str], Optional[str]]: 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..056e375ab7de 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 @@ -17,6 +17,8 @@ ConfigurationSetting, AzureAppConfigurationClient, FeatureFlagConfigurationSetting, + FeatureFlag, + FeatureFlagClient, SnapshotComposition, ) from ._client_manager_base import ( @@ -35,6 +37,7 @@ @dataclass class _ConfigurationClientWrapper(_ConfigurationClientWrapperBase): _client: AzureAppConfigurationClient + _feature_flag_client: Optional[FeatureFlagClient] = None backoff_end_time: float = 0 failed_attempts: int = 0 LOGGER = getLogger(__name__) @@ -71,6 +74,14 @@ def from_credential( retry_backoff_max=retry_backoff_max, **kwargs, ), + FeatureFlagClient( + endpoint, + credential, + user_agent=user_agent, + retry_total=retry_total, + retry_backoff_max=retry_backoff_max, + **kwargs, + ), ) @classmethod @@ -98,6 +109,13 @@ def from_connection_string( retry_backoff_max=retry_backoff_max, **kwargs, ), + FeatureFlagClient.from_connection_string( + connection_string, + user_agent=user_agent, + retry_total=retry_total, + retry_backoff_max=retry_backoff_max, + **kwargs, + ), ) def _check_configuration_setting( @@ -282,6 +300,80 @@ def check_feature_flag_page_etags( return True return False + @distributed_trace + def load_feature_flag_resources( + self, feature_flag_selectors: List[SettingSelector], **kwargs + ) -> Tuple[List[FeatureFlag], List[List[str]]]: + """ + Loads feature flags from the feature flag resource endpoint using page-based iteration, collecting page + etags for each selector. Selectors with a ``snapshot_name`` are currently not supported by the feature flag + resource endpoint and are skipped. + + :param feature_flag_selectors: List of setting selectors to filter feature flags + :type feature_flag_selectors: List[SettingSelector] + :return: A tuple of (feature_flags, page_etags_per_selector) + :rtype: Tuple[List[~azure.appconfiguration.FeatureFlag], List[List[str]]] + """ + loaded_feature_flags: List[FeatureFlag] = [] + page_etags: List[List[str]] = [] + # Needs to be removed unknown keyword argument for the feature flag client + kwargs.pop("sentinel_keys", None) + if self._feature_flag_client is None: + return loaded_feature_flags, [[] for _ in feature_flag_selectors] + for select in feature_flag_selectors: + selector_etags: List[str] = [] + if select.snapshot_name is not None: + # Snapshots are not supported by the feature flag resource endpoint as of now + page_etags.append(selector_etags) + continue + feature_flags = self._feature_flag_client.list_feature_flags( + name_filter=select.key_filter, + label_filter=select.label_filter, + tags_filter=select.tag_filters, + **kwargs, + ) + iterator = feature_flags.by_page() + for page in iterator: + loaded_feature_flags.extend(page) + selector_etags.append(iterator.etag) + page_etags.append(selector_etags) + return loaded_feature_flags, page_etags + + @distributed_trace + def check_feature_flag_resource_etags( + self, feature_flag_selectors: List[SettingSelector], page_etags: List[List[str]], **kwargs + ) -> bool: + """ + Checks if any feature flag resource page has changed using page etags. + + :param feature_flag_selectors: List of setting selectors for feature flags + :type feature_flag_selectors: List[SettingSelector] + :param page_etags: The page etags from the last load, one list per selector + :type page_etags: List[List[str]] + :return: True if any page has changed, False otherwise + :rtype: bool + """ + if self._feature_flag_client is None: + return False + for i, select in enumerate(feature_flag_selectors): + if select.snapshot_name is not None: + # Snapshots are not supported by the feature flag resource endpoint + continue + if i >= len(page_etags): + # Missing or stale etag state should trigger a refresh instead of failing. + return True + selector_etags = page_etags[i] + feature_flags = self._feature_flag_client.list_feature_flags( + name_filter=select.key_filter, + label_filter=select.label_filter, + tags_filter=select.tag_filters, + **kwargs, + ) + for _ in feature_flags.by_page(match_conditions=selector_etags): + # If any page is returned, it means that page has changed + return True + return False + @distributed_trace def get_updated_watched_settings( self, watched_settings: Mapping[Tuple[str, str], Optional[str]], headers: Dict[str, str], **kwargs @@ -362,13 +454,19 @@ def close(self) -> None: Closes the connection to Azure App Configuration. """ self._client.close() + if self._feature_flag_client is not None: + self._feature_flag_client.close() def __enter__(self): self._client.__enter__() + if self._feature_flag_client is not None: + self._feature_flag_client.__enter__() return self def __exit__(self, *args): self._client.__exit__(*args) + if self._feature_flag_client is not None: + self._feature_flag_client.__exit__(*args) def resolve_snapshot_reference(self, setting: ConfigurationSetting, **kwargs) -> List[ConfigurationSetting]: """ 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..fc916cebff3a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -15,6 +15,15 @@ ALLOCATION_ID_KEY = "AllocationId" ETAG_KEY = "ETag" +# Identifier field used by feature flags loaded from the classic key-value store. +FEATURE_FLAG_ID_FIELD = "id" +# Identifier field used by feature flags loaded from the dedicated feature flag resource endpoint. +FEATURE_FLAG_NAME_FIELD = "name" +# Path segment used to build the feature flag reference URL for feature flags loaded from the key-value store. +FEATURE_FLAG_KV_REFERENCE_SEGMENT = "kv" +# Path segment used to build the feature flag reference URL for feature flags loaded from the resource endpoint. +FEATURE_FLAG_RESOURCE_REFERENCE_SEGMENT = "ff" + # ------------------------------------------------------------------------ # Environment Variable Constants # ------------------------------------------------------------------------ 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..330a719613a6 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 @@ -243,15 +243,26 @@ def update_feature_filter_telemetry(self, feature_flag) -> None: # 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: - self.feature_filter_usage[PERCENTAGE_FILTER_KEY] = True - elif 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: - self.feature_filter_usage[TARGETING_FILTER_KEY] = True - else: - self.feature_filter_usage[CUSTOM_FILTER_KEY] = True + self.update_feature_filter_telemetry_by_names(filter.get("name") for filter in feature_flag.filters) + + def update_feature_filter_telemetry_by_names(self, filter_names) -> None: + """ + Track feature filter usage for App Configuration telemetry, given the filter names directly. Used for feature + flags that don't expose their filters as dictionaries, e.g. feature flags loaded from the feature flag + resource endpoint. + + :param filter_names: The names of the filters used by a feature flag. + :type filter_names: Iterable[Optional[str]] + """ + for name in filter_names: + if name in PERCENTAGE_FILTER_NAMES: + self.feature_filter_usage[PERCENTAGE_FILTER_KEY] = True + elif name in TIME_WINDOW_FILTER_NAMES: + self.feature_filter_usage[TIME_WINDOW_FILTER_KEY] = True + elif name in TARGETING_FILTER_NAMES: + self.feature_filter_usage[TARGETING_FILTER_KEY] = True + else: + self.feature_filter_usage[CUSTOM_FILTER_KEY] = True def reset_feature_filter_usage(self) -> None: """Reset the feature filter usage tracking.""" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py index 41676d00c483..0f4ca7972c61 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py @@ -4,4 +4,4 @@ # license information. # ------------------------------------------------------------------------- -VERSION = "2.5.1" +VERSION = "2.6.0b1" 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..c2bde9f89586 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 @@ -15,9 +15,10 @@ from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module ConfigurationSetting, FeatureFlagConfigurationSetting, + FeatureFlag, SnapshotComposition, ) -from azure.appconfiguration.aio import AzureAppConfigurationClient +from azure.appconfiguration.aio import AzureAppConfigurationClient, FeatureFlagClient from .._client_manager_base import ( _ConfigurationClientWrapperBase, ConfigurationClientManagerBase, @@ -37,6 +38,7 @@ @dataclass class _AsyncConfigurationClientWrapper(_ConfigurationClientWrapperBase): _client: AzureAppConfigurationClient + _feature_flag_client: Optional[FeatureFlagClient] = None backoff_end_time: float = 0 failed_attempts: int = 0 LOGGER = getLogger(__name__) @@ -73,6 +75,14 @@ def from_credential( retry_backoff_max=retry_backoff_max, **kwargs, ), + FeatureFlagClient( + endpoint, + credential, + user_agent=user_agent, + retry_total=retry_total, + retry_backoff_max=retry_backoff_max, + **kwargs, + ), ) @classmethod @@ -100,6 +110,13 @@ def from_connection_string( retry_backoff_max=retry_backoff_max, **kwargs, ), + FeatureFlagClient.from_connection_string( + connection_string, + user_agent=user_agent, + retry_total=retry_total, + retry_backoff_max=retry_backoff_max, + **kwargs, + ), ) async def _check_configuration_setting( @@ -284,6 +301,82 @@ async def check_feature_flag_page_etags( return True return False + @distributed_trace + @distributed_trace + async def load_feature_flag_resources( + self, feature_flag_selectors: List[SettingSelector], **kwargs + ) -> Tuple[List[FeatureFlag], List[List[str]]]: + """ + Loads feature flags from the feature flag resource endpoint using page-based iteration, collecting page + etags for each selector. Selectors with a ``snapshot_name`` are not supported by the feature flag resource + endpoint and are skipped. + + :param feature_flag_selectors: List of setting selectors to filter feature flags + :type feature_flag_selectors: List[SettingSelector] + :return: A tuple of (feature_flags, page_etags_per_selector) + :rtype: Tuple[List[~azure.appconfiguration.FeatureFlag], List[List[str]]] + """ + loaded_feature_flags: List[FeatureFlag] = [] + page_etags: List[List[str]] = [] + # Needs to be removed unknown keyword argument for the feature flag client + kwargs.pop("sentinel_keys", None) + if self._feature_flag_client is None: + return loaded_feature_flags, [[] for _ in feature_flag_selectors] + for select in feature_flag_selectors: + selector_etags: List[str] = [] + if select.snapshot_name is not None: + # Snapshots are not supported by the feature flag resource endpoint + page_etags.append(selector_etags) + continue + feature_flags = self._feature_flag_client.list_feature_flags( + name_filter=select.key_filter, + label_filter=select.label_filter, + tags_filter=select.tag_filters, + **kwargs, + ) + iterator = feature_flags.by_page() + async for page in iterator: + async for ff in page: + loaded_feature_flags.append(ff) + selector_etags.append(iterator.etag) # type: ignore[attr-defined] + page_etags.append(selector_etags) + return loaded_feature_flags, page_etags + + @distributed_trace + async def check_feature_flag_resource_etags( + self, feature_flag_selectors: List[SettingSelector], page_etags: List[List[str]], **kwargs + ) -> bool: + """ + Checks if any feature flag resource page has changed using page etags. + + :param feature_flag_selectors: List of setting selectors for feature flags + :type feature_flag_selectors: List[SettingSelector] + :param page_etags: The page etags from the last load, one list per selector + :type page_etags: List[List[str]] + :return: True if any page has changed, False otherwise + :rtype: bool + """ + if self._feature_flag_client is None: + return False + for i, select in enumerate(feature_flag_selectors): + if select.snapshot_name is not None: + # Snapshots are not supported by the feature flag resource endpoint + continue + if i >= len(page_etags): + # Missing or stale etag state should trigger a refresh instead of failing. + return True + selector_etags = page_etags[i] + feature_flags = self._feature_flag_client.list_feature_flags( + name_filter=select.key_filter, + label_filter=select.label_filter, + tags_filter=select.tag_filters, + **kwargs, + ) + async for _ in feature_flags.by_page(match_conditions=selector_etags): # type: ignore[call-arg] + # If any page is returned, it means that page has changed + return True + return False + @distributed_trace async def get_updated_watched_settings( self, watched_settings: Mapping[Tuple[str, str], Optional[str]], headers: Dict[str, str], **kwargs @@ -364,13 +457,19 @@ async def close(self) -> None: Closes the connection to Azure App Configuration. """ await self._client.close() + if self._feature_flag_client is not None: + await self._feature_flag_client.close() async def __aenter__(self): await self._client.__aenter__() + if self._feature_flag_client is not None: + await self._feature_flag_client.__aenter__() return self async def __aexit__(self, *args): await self._client.__aexit__(*args) + if self._feature_flag_client is not None: + await self._feature_flag_client.__aexit__(*args) async def resolve_snapshot_reference(self, setting: ConfigurationSetting, **kwargs) -> List[ConfigurationSetting]: """ 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..819102ad63ea 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 @@ -19,6 +19,7 @@ ) from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module ConfigurationSetting, + FeatureFlag, FeatureFlagConfigurationSetting, SecretReferenceConfigurationSetting, ) @@ -120,6 +121,7 @@ async def _attempt_refresh( ) configuration_settings: List[ConfigurationSetting] = [] feature_flags: Optional[List[FeatureFlagConfigurationSetting]] = None + feature_flag_resources: Optional[List[FeatureFlag]] = None # Timer needs to be reset even if no refresh happened if time had passed configuration_refresh_attempted = False @@ -128,6 +130,7 @@ async def _attempt_refresh( existing_feature_flag_usage = self._tracing_context.feature_filter_usage.copy() page_etags: List[List[str]] = [] feature_flag_page_etags: List[List[str]] = [] + feature_flag_resource_etags: List[List[str]] = [] try: if self._refresh_enabled and not self._watched_settings and self._refresh_timer.needs_refresh(): configuration_refresh_attempted = True @@ -160,6 +163,16 @@ async def _attempt_refresh( feature_flags, feature_flag_page_etags = await client.load_feature_flags( self._feature_flag_selectors, headers=headers, **kwargs ) + + # Feature flag resources 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._feature_flag_resource_etags or await client.check_feature_flag_resource_etags( + self._feature_flag_selectors, self._feature_flag_resource_etags, headers=headers, **kwargs + ): + feature_flag_resources, feature_flag_resource_etags = await client.load_feature_flag_resources( + self._feature_flag_selectors, headers=headers, **kwargs + ) # Default to existing settings if no refresh occurred processed_settings = self._dict @@ -169,7 +182,9 @@ async def _attempt_refresh( # Configuration Settings have been refreshed processed_settings = await self._process_configurations(configuration_settings, client) - processed_settings = self._process_feature_flags(processed_settings, processed_feature_flags, feature_flags) + processed_settings = self._process_feature_flags( + processed_settings, processed_feature_flags, feature_flags, feature_flag_resources + ) self._dict = processed_settings if settings_refreshed: self._page_etags = page_etags @@ -177,12 +192,14 @@ async def _attempt_refresh( self._watched_settings.update(updated_watched_settings) if feature_flags is not None: self._feature_flag_page_etags = feature_flag_page_etags + if feature_flag_resources is not None: + self._feature_flag_resource_etags = feature_flag_resource_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 feature_flag_resources) and self._on_refresh_success: self._on_refresh_success() except AzureError as e: logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint) @@ -290,6 +307,7 @@ async def _try_initialize(self, startup_exceptions: List[Exception], **kwargs: A processed_settings = await self._process_configurations(configuration_settings, client) feature_flag_page_etags: List[List[str]] = [] + feature_flag_resource_etags: List[List[str]] = [] if self._feature_flag_enabled: feature_flags: List[FeatureFlagConfigurationSetting] feature_flags, feature_flag_page_etags = await client.load_feature_flags( @@ -297,7 +315,14 @@ async def _try_initialize(self, startup_exceptions: List[Exception], **kwargs: A headers=headers, **kwargs, ) - processed_settings = self._process_feature_flags(processed_settings, [], feature_flags) + feature_flag_resources, feature_flag_resource_etags = await client.load_feature_flag_resources( + self._feature_flag_selectors, + headers=headers, + **kwargs, + ) + processed_settings = self._process_feature_flags( + processed_settings, [], feature_flags, feature_flag_resources + ) for (key, label), etag in self._watched_settings.items(): if not etag: try: @@ -324,6 +349,7 @@ async def _try_initialize(self, startup_exceptions: List[Exception], **kwargs: A self._dict = processed_settings self._page_etags = page_etags self._feature_flag_page_etags = feature_flag_page_etags + self._feature_flag_resource_etags = feature_flag_resource_etags return True except AzureError as e: logger.warning("Failed to load configurations from endpoint %s.\n %s", client.endpoint, e.message) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md index 90b21ea17f95..e15d0f6b1d6e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md @@ -49,6 +49,8 @@ pip install azure.appconfiguration.provider | entra_id_sample.py | demos connecting to app configuration with Entra ID | | connection_string_sample.py | demos connecting to app configuration with a Connection String | | key_vault_reference_sample.py | demos resolving key vault references with App Configuration | +| feature_flag_resource_sample.py | demos loading feature flags created via the dedicated feature flag resource endpoint | +| async_feature_flag_resource_sample.py | async version of feature_flag_resource_sample.py | ## Next steps diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_feature_flag_resource_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_feature_flag_resource_sample.py new file mode 100644 index 000000000000..2b11ecf1ba1f --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_feature_flag_resource_sample.py @@ -0,0 +1,80 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +""" +FILE: async_feature_flag_resource_sample.py +DESCRIPTION: + This sample demonstrates loading feature flags that were created using the dedicated feature flag + resource endpoint (via ``FeatureFlagClient``/``FeatureFlag``), as opposed to the classic key-value + based feature flags stored as configuration settings. The provider loads both kinds of feature + flags side by side into the same ``feature_management.feature_flags`` list, so no additional + ``load()`` options are required to opt in. This is the async version of feature_flag_resource_sample.py. +USAGE: python async_feature_flag_resource_sample.py + Set the environment variable APPCONFIGURATION_ENDPOINT_STRING with your App Configuration + connection endpoint before running the sample. +""" +import os +import asyncio +from sample_utilities import get_authority, get_credential, get_client_modifications +from azure.appconfiguration.aio import FeatureFlagClient +from azure.appconfiguration import FeatureFlag +from azure.appconfiguration.provider.aio import load +from azure.appconfiguration.provider import SettingSelector + + +async def main(): + endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"] + authority = get_authority(endpoint) + credential = get_credential(authority, is_async=True) + kwargs = get_client_modifications() + + # Creating a feature flag using the dedicated feature flag resource endpoint. This is a separate + # resource type from the classic key-value based feature flags, and is managed via FeatureFlagClient + # instead of AzureAppConfigurationClient. + feature_flag_client = FeatureFlagClient(endpoint, credential, **kwargs) + await feature_flag_client.set_feature_flag(FeatureFlag(name="ResourceBeta", enabled=True)) + + try: + # [START feature_flag_resource_loading_async] + from azure.appconfiguration.provider.aio import load + + # Feature flags loaded from the feature flag resource endpoint are merged into the same + # feature_management.feature_flags list as key-value based feature flags. + config = await load(endpoint=endpoint, credential=credential, feature_flag_enabled=True, **kwargs) + feature_flags = config["feature_management"]["feature_flags"] + resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") + print(resource_beta["enabled"]) + + await config.close() + # [END feature_flag_resource_loading_async] + + # [START feature_flag_resource_selector_async] + from azure.appconfiguration.provider.aio import load + from azure.appconfiguration.provider import SettingSelector + + # The same SettingSelector used to filter key-value based feature flags also filters feature flag + # resources, by name/label/tags. + config = await load( + endpoint=endpoint, + credential=credential, + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="Resource*")], + **kwargs, + ) + feature_flags = config["feature_management"]["feature_flags"] + resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") + print(resource_beta["enabled"]) + + await config.close() + # [END feature_flag_resource_selector_async] + finally: + # Cleaning up the feature flag resource created for this sample. + await feature_flag_client.delete_feature_flag("ResourceBeta") + await feature_flag_client.close() + await credential.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/feature_flag_resource_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/feature_flag_resource_sample.py new file mode 100644 index 000000000000..0545d534f1a6 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/feature_flag_resource_sample.py @@ -0,0 +1,65 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +""" +FILE: feature_flag_resource_sample.py +DESCRIPTION: + This sample demonstrates loading feature flags that were created using the dedicated feature flag + resource endpoint (via ``FeatureFlagClient``/``FeatureFlag``), as opposed to the classic key-value + based feature flags stored as configuration settings. The provider loads both kinds of feature + flags side by side into the same ``feature_management.feature_flags`` list, so no additional + ``load()`` options are required to opt in. +USAGE: python feature_flag_resource_sample.py + Set the environment variable APPCONFIGURATION_ENDPOINT_STRING with your App Configuration + connection endpoint before running the sample. +""" +import os +from sample_utilities import get_authority, get_credential, get_client_modifications +from azure.appconfiguration import FeatureFlag, FeatureFlagClient +from azure.appconfiguration.provider import load, SettingSelector + +endpoint = os.environ.get("APPCONFIGURATION_ENDPOINT_STRING") +authority = get_authority(endpoint) +credential = get_credential(authority) +kwargs = get_client_modifications() + +# Creating a feature flag using the dedicated feature flag resource endpoint. This is a separate resource +# type from the classic key-value based feature flags, and is managed via FeatureFlagClient instead of +# AzureAppConfigurationClient. +feature_flag_client = FeatureFlagClient(endpoint, credential, **kwargs) +feature_flag_client.set_feature_flag(FeatureFlag(name="ResourceBeta", enabled=True)) + +try: + # [START feature_flag_resource_loading] + from azure.appconfiguration.provider import load + + # Feature flags loaded from the feature flag resource 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"] + resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") + print(resource_beta["enabled"]) + # [END feature_flag_resource_loading] + + # [START feature_flag_resource_selector] + from azure.appconfiguration.provider import load, SettingSelector + + # The same SettingSelector used to filter key-value based feature flags also filters feature flag + # resources, by name/label/tags. + config = load( + endpoint=endpoint, + credential=credential, + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="Resource*")], + **kwargs, + ) + feature_flags = config["feature_management"]["feature_flags"] + resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") + print(resource_beta["enabled"]) + # [END feature_flag_resource_selector] +finally: + # Cleaning up the feature flag resource created for this sample. + feature_flag_client.delete_feature_flag("ResourceBeta") + feature_flag_client.close() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/setup.py b/sdk/appconfiguration/azure-appconfiguration-provider/setup.py index d756d6d66783..495014bbd097 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/setup.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/setup.py @@ -73,7 +73,7 @@ python_requires=">=3.6", install_requires=[ "azure-core>=1.31.0", - "azure-appconfiguration>=1.8.0", + "azure-appconfiguration>=1.10.0b1", "azure-keyvault-secrets>=4.3.0", "dnspython>=2.6.1", ], diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider_feature_flag_resources.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider_feature_flag_resources.py new file mode 100644 index 000000000000..538eddbcb1f7 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider_feature_flag_resources.py @@ -0,0 +1,153 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +""" +Tests for loading feature flags from the dedicated feature flag resource endpoint +(``FeatureFlagClient``/``FeatureFlag``), as opposed to the classic key-value based +``FeatureFlagConfigurationSetting`` stored via ``AzureAppConfigurationClient`` (async version). +""" +import functools +from devtools_testutils import EnvironmentVariableLoader +from devtools_testutils.aio import recorded_by_proxy_async +from testcase import has_feature_flag, get_feature_flag +from asynctestcase import AppConfigTestCase +from test_constants import APPCONFIGURATION_ENDPOINT_STRING, FEATURE_MANAGEMENT_KEY +from azure.appconfiguration import FeatureFlag, FeatureFlagConfigurationSetting +from azure.appconfiguration.provider import SettingSelector +from azure.appconfiguration.provider._constants import NULL_CHAR + +AppConfigProviderPreparer = functools.partial( + EnvironmentVariableLoader, + "appconfiguration", + appconfiguration_endpoint_string=APPCONFIGURATION_ENDPOINT_STRING, +) + + +class TestAppConfigurationProviderFeatureFlagResources(AppConfigTestCase): + """Tests for the provider loading feature flags from the dedicated feature flag resource endpoint (async).""" + + # method: load + @AppConfigProviderPreparer() + @recorded_by_proxy_async + async def test_load_feature_flag_resource(self, appconfiguration_endpoint_string): + """A feature flag created via the feature flag resource endpoint should be loaded by the provider.""" + feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + feature_flag = FeatureFlag(name="ResourceOnlyFeature", enabled=True) + await feature_flag_client.set_feature_flag(feature_flag) + + try: + async with await self.create_client( + endpoint=appconfiguration_endpoint_string, + selects=[], + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="ResourceOnlyFeature")], + ) as client: + assert FEATURE_MANAGEMENT_KEY in client + assert has_feature_flag(client, "ResourceOnlyFeature", enabled=True) + finally: + await feature_flag_client.delete_feature_flag("ResourceOnlyFeature") + await feature_flag_client.close() + + # method: load + @AppConfigProviderPreparer() + @recorded_by_proxy_async + async def test_load_feature_flag_resource_disabled(self, appconfiguration_endpoint_string): + """A disabled feature flag resource should be loaded with enabled set to False.""" + feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + feature_flag = FeatureFlag(name="ResourceDisabledFeature", enabled=False) + await feature_flag_client.set_feature_flag(feature_flag) + + try: + async with await self.create_client( + endpoint=appconfiguration_endpoint_string, + selects=[], + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="ResourceDisabledFeature")], + ) as client: + assert has_feature_flag(client, "ResourceDisabledFeature", enabled=False) + finally: + await feature_flag_client.delete_feature_flag("ResourceDisabledFeature") + await feature_flag_client.close() + + # method: load + @AppConfigProviderPreparer() + @recorded_by_proxy_async + async def test_load_feature_flag_resource_with_label(self, appconfiguration_endpoint_string): + """A feature flag resource with a label should be loaded when the label filter matches.""" + feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + feature_flag = FeatureFlag(name="ResourceLabeledFeature", enabled=True, label="test_label") + await feature_flag_client.set_feature_flag(feature_flag) + + try: + async with await self.create_client( + endpoint=appconfiguration_endpoint_string, + selects=[], + feature_flag_enabled=True, + feature_flag_selectors=[ + SettingSelector(key_filter="ResourceLabeledFeature", label_filter="test_label") + ], + ) as client: + assert has_feature_flag(client, "ResourceLabeledFeature", enabled=True) + finally: + await feature_flag_client.delete_feature_flag("ResourceLabeledFeature", label="test_label") + await feature_flag_client.close() + + # method: load + @AppConfigProviderPreparer() + @recorded_by_proxy_async + async def test_feature_flag_resource_selector_filters_by_name(self, appconfiguration_endpoint_string): + """The feature_flag_selectors key_filter should scope which feature flag resources are loaded.""" + feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + included_flag = FeatureFlag(name="IncludedResourceFeature", enabled=True) + excluded_flag = FeatureFlag(name="ExcludedResourceFeature", enabled=True) + await feature_flag_client.set_feature_flag(included_flag) + await feature_flag_client.set_feature_flag(excluded_flag) + + try: + async with await self.create_client( + endpoint=appconfiguration_endpoint_string, + selects=[], + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="Included*")], + ) as client: + assert has_feature_flag(client, "IncludedResourceFeature", enabled=True) + assert not has_feature_flag(client, "ExcludedResourceFeature") + finally: + await feature_flag_client.delete_feature_flag("IncludedResourceFeature") + await feature_flag_client.delete_feature_flag("ExcludedResourceFeature") + await feature_flag_client.close() + + # method: load + @AppConfigProviderPreparer() + @recorded_by_proxy_async + async def test_feature_flag_resource_overrides_key_value(self, appconfiguration_endpoint_string): + """A feature flag resource should take precedence over a key-value based feature flag with the + same identifier when both are loaded.""" + appconfig_client = self.create_appconfig_client(appconfiguration_endpoint_string) + feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + + kv_feature_flag = FeatureFlagConfigurationSetting(feature_id="OverlapFeature", enabled=False, label=NULL_CHAR) + await appconfig_client.set_configuration_setting(kv_feature_flag) + resource_feature_flag = FeatureFlag(name="OverlapFeature", enabled=True) + await feature_flag_client.set_feature_flag(resource_feature_flag) + + try: + async with await self.create_client( + endpoint=appconfiguration_endpoint_string, + selects=[], + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="OverlapFeature")], + ) as client: + # The resource-based feature flag (enabled=True) should win over the key-value based one + # (enabled=False) since they share the same identifier. + assert has_feature_flag(client, "OverlapFeature", enabled=True) + feature_flag = get_feature_flag(client, "OverlapFeature") + assert feature_flag is not None + assert "name" in feature_flag + finally: + await appconfig_client.delete_configuration_setting(key=kv_feature_flag.key, label=kv_feature_flag.label) + await feature_flag_client.delete_feature_flag("OverlapFeature") + await appconfig_client.close() + await feature_flag_client.close() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py index a433b81ce018..9a84248e5c4b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py @@ -6,7 +6,7 @@ # -------------------------------------------------------------------------- from devtools_testutils import AzureRecordedTestCase from testcase import get_configs -from azure.appconfiguration.aio import AzureAppConfigurationClient +from azure.appconfiguration.aio import AzureAppConfigurationClient, FeatureFlagClient from azure.appconfiguration.provider import AzureAppConfigurationKeyVaultOptions from azure.appconfiguration.provider.aio import load @@ -35,6 +35,10 @@ def create_appconfig_client(self, appconfiguration_endpoint_string): cred = self.get_credential(AzureAppConfigurationClient, is_async=True) return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") + def create_feature_flag_client(self, appconfiguration_endpoint_string): + cred = self.get_credential(FeatureFlagClient, is_async=True) + return FeatureFlagClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") + async def setup_configs(client, keyvault_secret_url, keyvault_secret_url2): async with client: @@ -82,6 +86,24 @@ async def set_test_settings_async(client, settings): await client.set_configuration_setting(setting) +async def cleanup_feature_flag_resources_async(feature_flag_client, feature_flags): + """ + Delete feature flag resources created via the dedicated feature flag resource endpoint (async version). + + :param feature_flag_client: The async FeatureFlagClient to use for cleanup. + :param feature_flags: List of FeatureFlag objects (or (name, label) tuples) to delete. + """ + for feature_flag in feature_flags: + if isinstance(feature_flag, tuple): + name, label = feature_flag + else: + name, label = feature_flag.name, feature_flag.label + try: + await feature_flag_client.delete_feature_flag(name, label=label) + except Exception: # pylint: disable=broad-except + pass + + async def create_snapshot_async(client, snapshot_name, key_filters, composition_type=None, retention_period=3600): """ Create a snapshot in Azure App Configuration and verify it was created successfully (async version). diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 5073f2afad31..c4801f992526 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -11,6 +11,17 @@ from typing import Dict, Any from azure.appconfiguration import FeatureFlagConfigurationSetting +from azure.appconfiguration import ( + FeatureFlag, + FeatureFlagAllocation, + FeatureFlagConditions, + FeatureFlagFilter, + FeatureFlagTelemetryConfiguration, + FeatureFlagVariantDefinition, + GroupAllocation, + PercentileAllocation, + UserAllocation, +) from azure.appconfiguration.provider._azureappconfigurationproviderbase import ( is_json_content_type, _build_watched_setting, @@ -398,3 +409,210 @@ def test_generate_allocation_id_truly_empty(self): result = AzureAppConfigurationProviderBase._generate_allocation_id(feature_flag_value) # This should return None because allocated_variants is empty and no seed self.assertIsNone(result) + + +class TestProcessFeatureFlagResource(unittest.TestCase): + """Test processing of feature flags loaded from the dedicated feature flag resource endpoint.""" + + def setUp(self): + self.provider = AzureAppConfigurationProviderBase(endpoint="https://test.azconfig.io") + + def test_process_feature_flag_resource_minimal(self): + """Test processing a minimal feature flag resource.""" + feature_flag = FeatureFlag(name="MyFeature", enabled=True) + + result = self.provider._process_feature_flag_resource(feature_flag) + + self.assertEqual(result["name"], "MyFeature") + self.assertTrue(result["enabled"]) + self.assertNotIn("label", result) + self.assertNotIn("description", result) + self.assertNotIn("conditions", result) + self.assertNotIn("variants", result) + self.assertNotIn("allocation", result) + self.assertNotIn("tags", result) + # Telemetry metadata (ETag) is always attached during processing, even without an explicit + # telemetry configuration on the feature flag resource. + self.assertIn("telemetry", result) + self.assertNotIn("enabled", result["telemetry"]) + + def test_process_feature_flag_resource_with_label_and_description(self): + """Test processing a feature flag resource with label and description.""" + feature_flag = FeatureFlag(name="MyFeature", enabled=False, label="prod", description="A test feature") + + result = self.provider._process_feature_flag_resource(feature_flag) + + self.assertEqual(result["name"], "MyFeature") + self.assertFalse(result["enabled"]) + self.assertEqual(result["label"], "prod") + self.assertEqual(result["description"], "A test feature") + + def test_process_feature_flag_resource_whitespace_label_omitted(self): + """Test that a whitespace-only label is not included in the processed output.""" + feature_flag = FeatureFlag(name="MyFeature", enabled=True, label=" ") + + result = self.provider._process_feature_flag_resource(feature_flag) + + self.assertNotIn("label", result) + + def test_process_feature_flag_resource_with_conditions(self): + """Test processing a feature flag resource with conditions/client filters.""" + feature_flag = FeatureFlag( + name="MyFeature", + enabled=True, + conditions=FeatureFlagConditions( + requirement_type="All", + client_filters=[FeatureFlagFilter(name="Percentage", parameters={"Value": "50"})], + ), + ) + + result = self.provider._process_feature_flag_resource(feature_flag) + + self.assertEqual(result["conditions"]["requirement_type"], "All") + self.assertEqual(len(result["conditions"]["client_filters"]), 1) + self.assertEqual(result["conditions"]["client_filters"][0]["name"], "Percentage") + self.assertEqual(result["conditions"]["client_filters"][0]["parameters"], {"Value": "50"}) + + def test_process_feature_flag_resource_with_variants_and_allocation(self): + """Test processing a feature flag resource with variants and allocation.""" + feature_flag = FeatureFlag( + name="MyFeature", + enabled=True, + variants=[ + FeatureFlagVariantDefinition(name="Control", value={"key": "control_value"}), + FeatureFlagVariantDefinition(name="Test", value={"key": "test_value"}, content_type="application/json"), + ], + allocation=FeatureFlagAllocation( + default_when_disabled="Control", + default_when_enabled="Test", + percentile=[PercentileAllocation(variant="Control", percentile_from=0, percentile_to=50)], + user=[UserAllocation(variant="Test", users=["user1"])], + group=[GroupAllocation(variant="Test", groups=["group1"])], + seed="1234", + ), + ) + + result = self.provider._process_feature_flag_resource(feature_flag) + + self.assertEqual(len(result["variants"]), 2) + self.assertEqual(result["variants"][0]["name"], "Control") + self.assertEqual(result["variants"][0]["value"], {"key": "control_value"}) + self.assertEqual(result["variants"][1]["content_type"], "application/json") + + allocation = result["allocation"] + self.assertEqual(allocation["default_when_disabled"], "Control") + self.assertEqual(allocation["default_when_enabled"], "Test") + self.assertEqual(allocation["percentile"], [{"variant": "Control", "percentile_from": 0, "percentile_to": 50}]) + self.assertEqual(allocation["user"], [{"variant": "Test", "users": ["user1"]}]) + self.assertEqual(allocation["group"], [{"variant": "Test", "groups": ["group1"]}]) + self.assertEqual(allocation["seed"], "1234") + + def test_process_feature_flag_resource_with_telemetry_and_tags(self): + """Test processing a feature flag resource with telemetry settings and tags.""" + feature_flag = FeatureFlag( + name="MyFeature", + enabled=True, + telemetry=FeatureFlagTelemetryConfiguration(enabled=True, metadata={"custom": "value"}), + tags={"team": "infra"}, + ) + + result = self.provider._process_feature_flag_resource(feature_flag) + + # Telemetry metadata gets ETag/FeatureFlagReference metadata appended by + # _update_ff_resource_telemetry_metadata as part of processing. + self.assertTrue(result["telemetry"]["enabled"]) + self.assertEqual(result["telemetry"]["metadata"]["custom"], "value") + self.assertEqual(result["tags"], {"team": "infra"}) + + def test_process_feature_flag_resource_updates_telemetry_metadata(self): + """Test that processing a feature flag resource adds ETag/FeatureFlagReference telemetry metadata.""" + feature_flag = FeatureFlag( + name="MyFeature", + enabled=True, + label="prod", + telemetry=FeatureFlagTelemetryConfiguration(enabled=True), + ) + feature_flag.etag = "resource_etag" + + result = self.provider._process_feature_flag_resource(feature_flag) + + metadata = result["telemetry"][METADATA_KEY] + self.assertEqual(metadata[ETAG_KEY], "resource_etag") + self.assertIn(FEATURE_FLAG_REFERENCE_KEY, metadata) + # The resource-based feature flag reference uses the "ff" path segment, not "kv". + self.assertIn("/ff/MyFeature", metadata[FEATURE_FLAG_REFERENCE_KEY]) + self.assertIn("?label=prod", metadata[FEATURE_FLAG_REFERENCE_KEY]) + + +class TestUpdateFfResourceTelemetryMetadata(unittest.TestCase): + """Test the _update_ff_resource_telemetry_metadata method.""" + + def setUp(self): + self.provider = AzureAppConfigurationProviderBase(endpoint="https://test.azconfig.io") + + def test_update_ff_resource_telemetry_metadata(self): + """Test resource-based feature flag telemetry processing uses the 'ff' reference segment.""" + feature_flag = FeatureFlag(name="test_feature", enabled=True, label="test_label") + feature_flag.etag = "test_etag" + + feature_flag_value: Dict[str, Any] = {TELEMETRY_KEY: {"enabled": True}} + endpoint = "https://test.azconfig.io" + + self.provider._update_ff_resource_telemetry_metadata(endpoint, feature_flag, feature_flag_value) + + metadata = feature_flag_value[TELEMETRY_KEY][METADATA_KEY] + self.assertEqual(metadata[ETAG_KEY], "test_etag") + self.assertIn(FEATURE_FLAG_REFERENCE_KEY, metadata) + self.assertIn("/ff/test_feature", metadata[FEATURE_FLAG_REFERENCE_KEY]) + self.assertIn("?label=test_label", metadata[FEATURE_FLAG_REFERENCE_KEY]) + + +class TestMergeFeatureFlags(unittest.TestCase): + """Test the _merge_feature_flags static method.""" + + def test_merge_no_overlap(self): + """Test merging when there is no identifier overlap between the two sources.""" + kv_flags = [{"id": "KvFeature", "enabled": True}] + resource_flags = [{"name": "ResourceFeature", "enabled": False}] + + merged = AzureAppConfigurationProviderBase._merge_feature_flags(kv_flags, resource_flags) + + self.assertEqual(len(merged), 2) + self.assertIn({"id": "KvFeature", "enabled": True}, merged) + self.assertIn({"name": "ResourceFeature", "enabled": False}, merged) + + def test_merge_resource_takes_precedence_on_collision(self): + """Test that a resource-based feature flag overrides a key-value one with the same identifier.""" + kv_flags = [{"id": "SharedFeature", "enabled": False, "source": "kv"}] + resource_flags = [{"name": "SharedFeature", "enabled": True, "source": "resource"}] + + merged = AzureAppConfigurationProviderBase._merge_feature_flags(kv_flags, resource_flags) + + self.assertEqual(len(merged), 1) + self.assertEqual(merged[0]["source"], "resource") + self.assertTrue(merged[0]["enabled"]) + + def test_merge_empty_lists(self): + """Test merging two empty lists returns an empty list.""" + merged = AzureAppConfigurationProviderBase._merge_feature_flags([], []) + self.assertEqual(merged, []) + + def test_merge_only_kv_flags(self): + """Test merging when only key-value based feature flags are present.""" + kv_flags = [{"id": "Feature1", "enabled": True}, {"id": "Feature2", "enabled": False}] + + merged = AzureAppConfigurationProviderBase._merge_feature_flags(kv_flags, []) + + self.assertEqual(len(merged), 2) + + def test_merge_only_resource_flags(self): + """Test merging when only resource-based feature flags are present.""" + resource_flags = [{"name": "Feature1", "enabled": True}, {"name": "Feature2", "enabled": False}] + + merged = AzureAppConfigurationProviderBase._merge_feature_flags([], resource_flags) + + self.assertEqual(len(merged), 2) + + +if __name__ == "__main__": + unittest.main() 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..85d13144c746 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 @@ -27,6 +27,22 @@ def __init__(self, endpoint, connection_string, credential, retry_total, retry_b self.retry_backoff = retry_backoff +class _FakePagedIterator: + """Mimics an ItemPaged page iterator, exposing a mutable ``etag`` reflecting the last-yielded page.""" + + def __init__(self, pages): + self._pages = iter(pages) + self.etag = None + + def __iter__(self): + return self + + def __next__(self): + page, etag = next(self._pages) + self.etag = etag + return page + + @pytest.mark.usefixtures("caplog") class TestConfigurationClientManager(unittest.TestCase): @@ -371,3 +387,163 @@ def test_check_page_etags_keys_first_then_snapshot(): mock_client.list_configuration_settings.assert_called_once_with( key_filter="app/*", label_filter="\0", tags_filter=None ) + + +def test_load_feature_flag_resources_no_feature_flag_client(): + """When no feature flag client is configured, no service calls are made.""" + mock_client = Mock() + wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client) + + selects = [SettingSelector(key_filter="app/*"), SettingSelector(key_filter="other/*")] + + feature_flags, page_etags = wrapper.load_feature_flag_resources(selects) + + assert feature_flags == [] + assert page_etags == [[], []] + + +def test_load_feature_flag_resources_skips_snapshot_selectors(): + """Selectors with a snapshot_name are not supported by the feature flag resource endpoint and are skipped.""" + mock_client = Mock() + mock_feature_flag_client = Mock() + wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) + + selects = [ + SettingSelector(snapshot_name="my-snapshot"), + SettingSelector(key_filter="app/*"), + ] + + flag1 = Mock(name="flag1") + mock_response = Mock() + mock_response.by_page.return_value = _FakePagedIterator([([flag1], "etag1")]) + mock_feature_flag_client.list_feature_flags.return_value = mock_response + + feature_flags, page_etags = wrapper.load_feature_flag_resources(selects) + + assert feature_flags == [flag1] + assert page_etags == [[], ["etag1"]] + # Only the non-snapshot selector should trigger a service call + mock_feature_flag_client.list_feature_flags.assert_called_once_with( + name_filter="app/*", label_filter="\0", tags_filter=None + ) + + +def test_load_feature_flag_resources_multiple_pages(): + """Multiple pages should be aggregated and each page's etag collected.""" + mock_client = Mock() + mock_feature_flag_client = Mock() + wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) + + selects = [SettingSelector(key_filter="app/*")] + + flag1 = Mock(name="flag1") + flag2 = Mock(name="flag2") + + class FakeIterator: + """Mimics an ItemPaged iterator, exposing a mutable ``etag`` reflecting the last-yielded page.""" + + def __init__(self, pages): + self._pages = iter(pages) + self.etag = None + + def __iter__(self): + return self + + def __next__(self): + page, etag = next(self._pages) + self.etag = etag + return page + + mock_response = Mock() + mock_response.by_page.return_value = FakeIterator([([flag1], "etag1"), ([flag2], "etag2")]) + mock_feature_flag_client.list_feature_flags.return_value = mock_response + + feature_flags, page_etags = wrapper.load_feature_flag_resources(selects) + + assert feature_flags == [flag1, flag2] + assert page_etags == [["etag1", "etag2"]] + + +def test_check_feature_flag_resource_etags_no_feature_flag_client(): + """When no feature flag client is configured, no changes are reported.""" + mock_client = Mock() + wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client) + + selects = [SettingSelector(key_filter="app/*")] + + result = wrapper.check_feature_flag_resource_etags(selects, [["etag1"]]) + + assert result is False + + +def test_check_feature_flag_resource_etags_no_change(): + """When the returned pages are empty, no changes are reported.""" + mock_client = Mock() + mock_feature_flag_client = Mock() + wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) + + selects = [SettingSelector(key_filter="app/*")] + page_etags = [["etag1"]] + + mock_response = Mock() + mock_response.by_page.return_value = iter([]) + mock_feature_flag_client.list_feature_flags.return_value = mock_response + + result = wrapper.check_feature_flag_resource_etags(selects, page_etags) + + assert result is False + mock_feature_flag_client.list_feature_flags.assert_called_once_with( + name_filter="app/*", label_filter="\0", tags_filter=None + ) + mock_response.by_page.assert_called_once_with(match_conditions=["etag1"]) + + +def test_check_feature_flag_resource_etags_change_detected(): + """When a page is returned, a change should be reported.""" + mock_client = Mock() + mock_feature_flag_client = Mock() + wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) + + selects = [SettingSelector(key_filter="app/*")] + page_etags = [["etag1"]] + + mock_response = Mock() + mock_response.by_page.return_value = iter([[Mock()]]) + mock_feature_flag_client.list_feature_flags.return_value = mock_response + + result = wrapper.check_feature_flag_resource_etags(selects, page_etags) + + assert result is True + + +def test_check_feature_flag_resource_etags_skips_snapshot_selectors(): + """Selectors with a snapshot_name are not supported and should be skipped without a service call.""" + mock_client = Mock() + mock_feature_flag_client = Mock() + wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) + + selects = [SettingSelector(snapshot_name="my-snapshot")] + page_etags = [[]] + + result = wrapper.check_feature_flag_resource_etags(selects, page_etags) + + assert result is False + mock_feature_flag_client.list_feature_flags.assert_not_called() + + +def test_check_feature_flag_resource_etags_missing_page_etags_triggers_refresh(): + """Missing etag state for a selector should trigger a refresh instead of failing.""" + mock_client = Mock() + mock_feature_flag_client = Mock() + wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) + + selects = [SettingSelector(key_filter="app/*"), SettingSelector(key_filter="other/*")] + # Only one entry provided for two selectors; the first selector's page hasn't changed. + mock_response = Mock() + mock_response.by_page.return_value = iter([]) + mock_feature_flag_client.list_feature_flags.return_value = mock_response + page_etags = [["etag1"]] + + result = wrapper.check_feature_flag_resource_etags(selects, page_etags) + + assert result is True diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_resources.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_resources.py new file mode 100644 index 000000000000..e9b82ec7457c --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_resources.py @@ -0,0 +1,150 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +""" +Tests for loading feature flags from the dedicated feature flag resource endpoint +(``FeatureFlagClient``/``FeatureFlag``), as opposed to the classic key-value based +``FeatureFlagConfigurationSetting`` stored via ``AzureAppConfigurationClient``. +""" +import functools +from devtools_testutils import EnvironmentVariableLoader, recorded_by_proxy +from testcase import AppConfigTestCase, has_feature_flag, get_feature_flag +from test_constants import APPCONFIGURATION_ENDPOINT_STRING, FEATURE_MANAGEMENT_KEY +from azure.appconfiguration import FeatureFlag, FeatureFlagConfigurationSetting +from azure.appconfiguration.provider import SettingSelector +from azure.appconfiguration.provider._constants import NULL_CHAR + +AppConfigProviderPreparer = functools.partial( + EnvironmentVariableLoader, + "appconfiguration", + appconfiguration_endpoint_string=APPCONFIGURATION_ENDPOINT_STRING, +) + + +class TestAppConfigurationProviderFeatureFlagResources(AppConfigTestCase): + """Tests for the provider loading feature flags from the dedicated feature flag resource endpoint.""" + + # method: load + @AppConfigProviderPreparer() + @recorded_by_proxy + def test_load_feature_flag_resource(self, appconfiguration_endpoint_string): + """A feature flag created via the feature flag resource endpoint should be loaded by the provider.""" + feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + feature_flag = FeatureFlag(name="ResourceOnlyFeature", enabled=True) + feature_flag_client.set_feature_flag(feature_flag) + + try: + client = self.create_client( + endpoint=appconfiguration_endpoint_string, + selects=[], + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="ResourceOnlyFeature")], + ) + + assert FEATURE_MANAGEMENT_KEY in client + assert has_feature_flag(client, "ResourceOnlyFeature", enabled=True) + finally: + feature_flag_client.delete_feature_flag("ResourceOnlyFeature") + + # method: load + @AppConfigProviderPreparer() + @recorded_by_proxy + def test_load_feature_flag_resource_disabled(self, appconfiguration_endpoint_string): + """A disabled feature flag resource should be loaded with enabled set to False.""" + feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + feature_flag = FeatureFlag(name="ResourceDisabledFeature", enabled=False) + feature_flag_client.set_feature_flag(feature_flag) + + try: + client = self.create_client( + endpoint=appconfiguration_endpoint_string, + selects=[], + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="ResourceDisabledFeature")], + ) + + assert has_feature_flag(client, "ResourceDisabledFeature", enabled=False) + finally: + feature_flag_client.delete_feature_flag("ResourceDisabledFeature") + + # method: load + @AppConfigProviderPreparer() + @recorded_by_proxy + def test_load_feature_flag_resource_with_label(self, appconfiguration_endpoint_string): + """A feature flag resource with a label should be loaded when the label filter matches.""" + feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + feature_flag = FeatureFlag(name="ResourceLabeledFeature", enabled=True, label="test_label") + feature_flag_client.set_feature_flag(feature_flag) + + try: + client = self.create_client( + endpoint=appconfiguration_endpoint_string, + selects=[], + feature_flag_enabled=True, + feature_flag_selectors=[ + SettingSelector(key_filter="ResourceLabeledFeature", label_filter="test_label") + ], + ) + + assert has_feature_flag(client, "ResourceLabeledFeature", enabled=True) + finally: + feature_flag_client.delete_feature_flag("ResourceLabeledFeature", label="test_label") + + # method: load + @AppConfigProviderPreparer() + @recorded_by_proxy + def test_feature_flag_resource_selector_filters_by_name(self, appconfiguration_endpoint_string): + """The feature_flag_selectors key_filter should scope which feature flag resources are loaded.""" + feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + included_flag = FeatureFlag(name="IncludedResourceFeature", enabled=True) + excluded_flag = FeatureFlag(name="ExcludedResourceFeature", enabled=True) + feature_flag_client.set_feature_flag(included_flag) + feature_flag_client.set_feature_flag(excluded_flag) + + try: + client = self.create_client( + endpoint=appconfiguration_endpoint_string, + selects=[], + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="Included*")], + ) + + assert has_feature_flag(client, "IncludedResourceFeature", enabled=True) + assert not has_feature_flag(client, "ExcludedResourceFeature") + finally: + feature_flag_client.delete_feature_flag("IncludedResourceFeature") + feature_flag_client.delete_feature_flag("ExcludedResourceFeature") + + # method: load + @AppConfigProviderPreparer() + @recorded_by_proxy + def test_feature_flag_resource_overrides_key_value(self, appconfiguration_endpoint_string): + """A feature flag resource should take precedence over a key-value based feature flag with the + same identifier when both are loaded.""" + appconfig_client = self.create_appconfig_client(appconfiguration_endpoint_string) + feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + + kv_feature_flag = FeatureFlagConfigurationSetting(feature_id="OverlapFeature", enabled=False, label=NULL_CHAR) + appconfig_client.set_configuration_setting(kv_feature_flag) + resource_feature_flag = FeatureFlag(name="OverlapFeature", enabled=True) + feature_flag_client.set_feature_flag(resource_feature_flag) + + try: + client = self.create_client( + endpoint=appconfiguration_endpoint_string, + selects=[], + feature_flag_enabled=True, + feature_flag_selectors=[SettingSelector(key_filter="OverlapFeature")], + ) + + # The resource-based feature flag (enabled=True) should win over the key-value based one + # (enabled=False) since they share the same identifier. + assert has_feature_flag(client, "OverlapFeature", enabled=True) + feature_flag = get_feature_flag(client, "OverlapFeature") + assert feature_flag is not None + assert "name" in feature_flag + finally: + appconfig_client.delete_configuration_setting(key=kv_feature_flag.key, label=kv_feature_flag.label) + feature_flag_client.delete_feature_flag("OverlapFeature") diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py index 99074621b628..a2cdedb6310e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py @@ -11,6 +11,8 @@ AzureAppConfigurationClient, ConfigurationSetting, ConfigurationSettingsFilter, + FeatureFlag, + FeatureFlagClient, FeatureFlagConfigurationSetting, SecretReferenceConfigurationSetting, SnapshotComposition, @@ -44,6 +46,10 @@ def create_appconfig_client(self, appconfiguration_endpoint_string): cred = self.get_credential(AzureAppConfigurationClient) return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") + def create_feature_flag_client(self, appconfiguration_endpoint_string): + cred = self.get_credential(FeatureFlagClient) + return FeatureFlagClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") + def setup_configs(client, keyvault_secret_url, keyvault_secret_url2): """Set up all test configs and create snapshots. Returns (snapshot_name, ff_snapshot_name).""" @@ -164,6 +170,38 @@ def create_feature_flag_config_setting(key, label, enabled, tags=None): return FeatureFlagConfigurationSetting(feature_id=key, label=label, enabled=enabled, tags=tags) +def create_feature_flag_resource(name, enabled, label=None, **kwargs): + """ + Create a FeatureFlag resource object for use with the dedicated feature flag resource endpoint + (``FeatureFlagClient``), as opposed to the classic key-value based ``FeatureFlagConfigurationSetting``. + + :param name: The name/identifier of the feature flag. + :param enabled: Whether the feature flag is enabled. + :param label: The label of the feature flag. + :return: A FeatureFlag resource object. + :rtype: ~azure.appconfiguration.FeatureFlag + """ + return FeatureFlag(name=name, enabled=enabled, label=label, **kwargs) + + +def cleanup_feature_flag_resources(feature_flag_client, feature_flags): + """ + Delete feature flag resources created via the dedicated feature flag resource endpoint. + + :param feature_flag_client: The FeatureFlagClient to use for cleanup. + :param feature_flags: List of FeatureFlag objects (or (name, label) tuples) to delete. + """ + for feature_flag in feature_flags: + if isinstance(feature_flag, tuple): + name, label = feature_flag + else: + name, label = feature_flag.name, feature_flag.label + try: + feature_flag_client.delete_feature_flag(name, label=label) + except Exception: # pylint: disable=broad-except + pass + + def cleanup_test_resources( client, settings=None, @@ -245,7 +283,7 @@ def create_snapshot(client, snapshot_name, key_filters, composition_type=None, r def get_feature_flag(client, feature_id): for feature_flag in client[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY]: - if feature_flag["id"] == feature_id: + if feature_flag.get("id", feature_flag.get("name")) == feature_id: return feature_flag return None From 0e66fcaae8e9a73d47e4807f88359c56268642ac Mon Sep 17 00:00:00 2001 From: Yuan Qu Date: Wed, 29 Jul 2026 11:48:27 -0700 Subject: [PATCH 17/25] Rename to enhanced feature flag terminology, fix id/name schema bug, and address review comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure-appconfiguration-provider/README.md | 75 ++-------- .../_azureappconfigurationprovider.py | 38 +++--- .../_azureappconfigurationproviderbase.py | 116 ++++++++-------- .../provider/_client_manager.py | 88 ++++++------ .../appconfiguration/provider/_constants.py | 11 +- .../provider/aio/_async_client_manager.py | 97 ++++++------- .../_azureappconfigurationproviderasync.py | 39 +++--- .../samples/README.md | 4 +- ... => async_enhanced_feature_flag_sample.py} | 44 +++--- ...ple.py => enhanced_feature_flag_sample.py} | 42 +++--- .../tests/README.md | 50 +++++++ ..._async_provider_enhanced_feature_flags.py} | 46 +++---- .../tests/asynctestcase.py | 6 +- .../test_azureappconfigurationproviderbase.py | 129 ++++++++++-------- .../test_configuration_client_manager.py | 62 +++++---- ...> test_provider_enhanced_feature_flags.py} | 46 +++---- .../tests/testcase.py | 14 +- 17 files changed, 471 insertions(+), 436 deletions(-) rename sdk/appconfiguration/azure-appconfiguration-provider/samples/{async_feature_flag_resource_sample.py => async_enhanced_feature_flag_sample.py} (66%) rename sdk/appconfiguration/azure-appconfiguration-provider/samples/{feature_flag_resource_sample.py => enhanced_feature_flag_sample.py} (60%) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/tests/README.md rename sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/{test_async_provider_feature_flag_resources.py => test_async_provider_enhanced_feature_flags.py} (77%) rename sdk/appconfiguration/azure-appconfiguration-provider/tests/{test_provider_feature_flag_resources.py => test_provider_enhanced_feature_flags.py} (76%) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/README.md index 7780b20300a2..31ae14598326 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/README.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/README.md @@ -377,44 +377,44 @@ config = load( -### Loading Feature Flags as Resources +### Loading Enhanced Feature Flags -Feature flags can also be created using the dedicated feature flag resource endpoint (via `FeatureFlagClient`/`FeatureFlag` in `azure-appconfiguration`), instead of as classic key-value configuration settings. The provider loads both kinds side by side into the same `feature_management.feature_flags` list, with feature flag resources 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`. +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`. - + ```python from azure.appconfiguration.provider import load -# Feature flags loaded from the feature flag resource endpoint are merged into the same +# 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"] -resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") -print(resource_beta["enabled"]) +enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("name") == "EnhancedFeatureBeta") +print(enhanced_flag_beta["enabled"]) ``` -The same `SettingSelector` used to filter key-value based feature flags also filters feature flag resources, by name, label, or tags. Note that selectors with a `snapshot_name` are not currently supported by the feature flag resource endpoint and are skipped when loading feature flag resources. +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. - + ```python from azure.appconfiguration.provider import load, SettingSelector -# The same SettingSelector used to filter key-value based feature flags also filters feature flag -# resources, by name/label/tags. +# 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="Resource*")], + feature_flag_selectors=[SettingSelector(key_filter="Enhanced*")], **kwargs, ) feature_flags = config["feature_management"]["feature_flags"] -resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") -print(resource_beta["enabled"]) +enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("name") == "EnhancedFeatureBeta") +print(enhanced_flag_beta["enabled"]) ``` @@ -511,56 +511,11 @@ 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 +## Testing (This content is for `azure-appconfiguration-provider` package developer only) -The tests for this package are under the `tests/` directory and are split into two categories: - -* **Unit tests** (e.g. `tests/test_azureappconfigurationproviderbase.py`, `tests/test_configuration_client_manager.py`) — exercise internal logic in isolation using mocked clients. These do not require any App Configuration store, network access, or environment variables, and can be run at any time with no setup. -* **Integration tests** (e.g. `tests/test_provider.py`, `tests/test_provider_feature_flag_resources.py`, and their `tests/aio/` async equivalents) — exercise the provider end-to-end against an Azure App Configuration store. These tests are built on [`devtools_testutils`](https://github.com/Azure/azure-sdk-for-python/tree/main/eng/tools/azure-sdk-tools/devtools_testutils) and each test method is decorated with `@recorded_by_proxy` / `@recorded_by_proxy_async`, which route the test's HTTP traffic through the [test proxy](https://github.com/Azure/azure-sdk-tools/tree/main/tools/test-proxy) tool. - -### Live tests vs. recorded (playback) tests - -Whether an integration test makes a real network call or replays a recording is controlled entirely by the `AZURE_TEST_RUN_LIVE` environment variable, not by anything in this package's code: - -* `AZURE_TEST_RUN_LIVE=true` — Tests run in **live/record mode**. The test proxy forwards requests to the real endpoint configured via your environment variables (see below), and (unless `AZURE_SKIP_LIVE_RECORDING=true` is also set) records the request/response pairs as new recording files for use in future playback runs. -* `AZURE_TEST_RUN_LIVE` unset or `false` (the default, and what CI uses) — Tests run in **playback mode**. The test proxy replays the existing recordings instead of contacting the real service, so **no network calls are made** and no live App Configuration store is required. - -Recordings themselves are not stored directly in this repository — they live in the separate [`Azure/azure-sdk-assets`](https://github.com/Azure/azure-sdk-assets) repo, and this package's `assets.json` file pins the exact recordings revision (`Tag`) that CI uses. If you add or change integration tests, you need to generate new recordings and publish them: - -1. Run the affected tests with `AZURE_TEST_RUN_LIVE=true` (and without `AZURE_SKIP_LIVE_RECORDING`) so the test proxy records real interactions to local recording files. -2. From the repo root, push the new/updated recordings to the assets repo: - - ```bash - dotnet tool run test-proxy push -a sdk/appconfiguration/azure-appconfiguration-provider/assets.json - ``` - - This uploads the changed recordings and updates the `Tag` field in `assets.json`. -3. Commit the updated `assets.json` as part of your PR — this is what allows CI (which always runs in playback mode) to pick up the new recordings. - -Only re-record tests you added or intentionally changed; unrelated existing recordings don't need to be regenerated. - -### Environment variables for local testing - -To run the integration tests locally in live mode, create a `.env` file at the repository root (it is automatically loaded by `devtools_testutils`) with the following variables: - -``` -AZURE_TEST_RUN_LIVE=true -APPCONFIGURATION_CONNECTION_STRING= -APPCONFIGURATION_ENDPOINT_STRING=.azconfig.io> -APPCONFIGURATION_KEY_VAULT_REFERENCE= -APPCONFIGURATION_KEY_VAULT_REFERENCE2= -APPCONFIGURATION_KEYVAULT_SECRET_URL= -APPCONFIGURATION_KEYVAULT_SECRET_URL2= -``` - -Notes: - -* For key vault URI, you can create a secret in Azure Key Vault service. The key vault URI is the *Secret Identifier*, without the final version number. For example, if the secret identifier is `https://some_secret.vault.azure.net/secrets/fake-secret/30d8830ec5ed4a428d311292a826f452`, the key vault URI should be `https://some_secret.vault.azure.net/secrets/fake-secret/`. -* Authentication for Entra ID-based tests relies on your local Azure CLI login (`az login`); make sure you're signed in to the subscription that contains your App Configuration store. -* Add `AZURE_SKIP_LIVE_RECORDING=true` if you want to run tests live against the real store without generating/overwriting recording files (useful for a quick sanity check). -* Omit `AZURE_TEST_RUN_LIVE` (or set it to `false`) to run the same tests in playback mode against existing recordings — this does not require any of the App Configuration environment variables above. +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 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 6161477e40b2..d6c01d6ddad8 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -108,7 +108,7 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f ) configuration_settings: List[ConfigurationSetting] = [] feature_flags: Optional[List[FeatureFlagConfigurationSetting]] = None - feature_flag_resources: Optional[List[FeatureFlag]] = 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 @@ -117,7 +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]] = [] - feature_flag_resource_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 @@ -151,14 +151,14 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f self._feature_flag_selectors, headers=headers, **kwargs ) - # Feature flag resources are loaded independently of the key-value based feature flags, using their + # 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._feature_flag_resource_etags or client.check_feature_flag_resource_etags( - self._feature_flag_selectors, self._feature_flag_resource_etags, headers=headers, **kwargs + 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 ): - feature_flag_resources, feature_flag_resource_etags = client.load_feature_flag_resources( - self._feature_flag_selectors, 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 @@ -170,8 +170,8 @@ 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, feature_flag_resources + 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: @@ -180,14 +180,14 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f self._watched_settings.update(updated_watched_settings) if feature_flags is not None: self._feature_flag_page_etags = feature_flag_page_etags - if feature_flag_resources is not None: - self._feature_flag_resource_etags = feature_flag_resource_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 or feature_flag_resources) 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) @@ -295,7 +295,7 @@ 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]] = [] - feature_flag_resource_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( @@ -303,13 +303,13 @@ def _try_initialize(self, startup_exceptions: List[Exception], **kwargs: Any) -> headers=headers, **kwargs, ) - feature_flag_resources, feature_flag_resource_etags = client.load_feature_flag_resources( - self._feature_flag_selectors, + enhanced_feature_flags, enhanced_feature_flag_etags = client.load_enhanced_feature_flags( + self._enhanced_feature_flag_selectors, headers=headers, **kwargs, ) - processed_settings = self._process_feature_flags( - processed_settings, [], feature_flags, feature_flag_resources + 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: @@ -335,7 +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._feature_flag_resource_etags = feature_flag_resource_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) @@ -401,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) 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 d2cc796cdd5c..b6bf97f28e2b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -40,9 +40,8 @@ FEATURE_MANAGEMENT_KEY, FEATURE_FLAG_KEY, FEATURE_FLAG_ID_FIELD, - FEATURE_FLAG_NAME_FIELD, FEATURE_FLAG_KV_REFERENCE_SEGMENT, - FEATURE_FLAG_RESOURCE_REFERENCE_SEGMENT, + ENHANCED_FEATURE_FLAG_REFERENCE_SEGMENT, ) from ._refresh_timer import _RefreshTimer from ._request_tracing_context import _RequestTracingContext @@ -110,26 +109,23 @@ def __init__(self, **kwargs: Any) -> None: self._feature_flag_selectors = kwargs.pop("feature_flag_selectors", None) if self._feature_flag_selectors is None: self._feature_flag_selectors = [SettingSelector(key_filter="*")] + # The enhanced feature flag currently does not support snapshots, so selectors with a snapshot_name are + # filtered out. + self._enhanced_feature_flag_selectors = [ + select for select in self._feature_flag_selectors if select.snapshot_name is None + ] self._feature_flag_refresh_timer: _RefreshTimer = _RefreshTimer(**kwargs) self._feature_flag_refresh_enabled = kwargs.pop("feature_flag_refresh_enabled", False) refresh_enabled = kwargs.pop("refresh_enabled", None) if refresh_enabled is None and len(refresh_on) > 0: # If refresh_enabled is not explicitly set, enable refresh if there are settings to refresh on - # This make sure we don't break existing users. refresh_enabled = True self._refresh_enabled = refresh_enabled self._page_etags: List[List[str]] = [] self._feature_flag_page_etags: List[List[str]] = [] - # Per-selector collection ETags for feature flags loaded from the feature flag resource endpoint. This is - # independent of the key-value based feature_flag_page_etags, since the resource endpoint is a separate - # resource type with its own change-detection mechanism. - self._feature_flag_resource_etags: List[List[str]] = [] - # Feature flags are loaded from two independent sources: the classic key-value store, and the newer - # dedicated feature flag resource endpoint. Each source's processed output is cached separately so that a - # refresh of one source does not require re-processing or discarding the other source's data. The two are - # merged (resource-based feature flags take precedence on identifier collision) whenever either changes. + self._enhanced_feature_flag_etags: List[List[str]] = [] self._processed_kv_feature_flags: List[Dict[str, Any]] = [] - self._processed_resource_feature_flags: List[Dict[str, Any]] = [] + self._processed_enhanced_feature_flags: List[Dict[str, Any]] = [] self._tracing_context = _RequestTracingContext(kwargs.pop("load_balancing_enabled", False)) self._update_lock = Lock() self._refresh_lock = Lock() @@ -147,7 +143,7 @@ def _update_ff_telemetry_metadata( self, endpoint: str, feature_flag: FeatureFlagConfigurationSetting, feature_flag_value: Dict ): """ - Add telemetry metadata to feature flag values loaded from the classic key-value store. + Add telemetry metadata to feature flag values loaded from the key-value store. :param endpoint: The App Configuration endpoint URL. :type endpoint: str @@ -165,13 +161,15 @@ def _update_ff_telemetry_metadata( FEATURE_FLAG_KV_REFERENCE_SEGMENT, ) - def _update_ff_resource_telemetry_metadata(self, endpoint: str, feature_flag: FeatureFlag, feature_flag_value: Dict): + def _update_enhanced_feature_flag_telemetry_metadata( + self, endpoint: str, feature_flag: FeatureFlag, feature_flag_value: Dict + ): """ - Add telemetry metadata to feature flag values loaded from the feature flag resource endpoint. + Add telemetry metadata to enhanced feature flag values loaded from the enhanced feature flag endpoint. :param endpoint: The App Configuration endpoint URL. :type endpoint: str - :param feature_flag: The feature flag resource. + :param feature_flag: The enhanced feature flag. :type feature_flag: ~azure.appconfiguration.FeatureFlag :param feature_flag_value: The feature flag value dictionary to update. :type feature_flag_value: Dict[str, Any] @@ -182,7 +180,7 @@ def _update_ff_resource_telemetry_metadata(self, endpoint: str, feature_flag: Fe feature_flag.label, feature_flag.etag, feature_flag_value, - FEATURE_FLAG_RESOURCE_REFERENCE_SEGMENT, + ENHANCED_FEATURE_FLAG_REFERENCE_SEGMENT, ) def _update_ff_telemetry_metadata_common( # pylint: disable=too-many-positional-arguments @@ -199,7 +197,8 @@ def _update_ff_telemetry_metadata_common( # pylint: disable=too-many-positional :param endpoint: The App Configuration endpoint URL. :type endpoint: str - :param identifier: The identifier of the feature flag (key for key-value based, name for resource-based). + :param identifier: The identifier of the feature flag (key for key-value based, name for enhanced feature + flags). :type identifier: str :param label: The label of the feature flag. :type label: Optional[str] @@ -208,7 +207,7 @@ def _update_ff_telemetry_metadata_common( # pylint: disable=too-many-positional :param feature_flag_value: The feature flag value dictionary to update. :type feature_flag_value: Dict[str, Any] :param reference_path_segment: The path segment to use when building the feature flag reference URL, e.g. - "kv" for key-value based feature flags or "ff" for resource-based feature flags. + "kv" for key-value based feature flags or "ff" for enhanced feature flags. :type reference_path_segment: str """ if TELEMETRY_KEY not in feature_flag_value: @@ -225,11 +224,11 @@ def _update_ff_telemetry_metadata_common( # pylint: disable=too-many-positional if not endpoint.endswith("/"): endpoint += "/" feature_flag_reference = f"{endpoint}{reference_path_segment}/{identifier}" - if label and not label.isspace(): + if label: feature_flag_reference += f"?label={label}" feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference - allocation_id = self._generate_allocation_id(feature_flag_value) + allocation_id = self._generate_allocation_id(feature_flag_value, reference_path_segment) if allocation_id: feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ALLOCATION_ID_KEY] = allocation_id @@ -243,12 +242,14 @@ def _update_ff_telemetry_metadata_common( # pylint: disable=too-many-positional self._tracing_context.update_max_variants(len(variants)) @staticmethod - def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str]: + def _generate_allocation_id(feature_flag_value: Dict[str, JSON], reference_path_segment: str) -> Optional[str]: """ Generates an allocation ID for the specified feature. 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. + :param str reference_path_segment: The path segment identifying which source the feature flag was loaded + from, e.g. "kv" for key-value based feature flags or "ff" for enhanced feature flags. :rtype: str :return: The allocation ID. """ @@ -310,14 +311,13 @@ def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str for v in sorted_variants: allocation_id += f"{base64.b64encode(v.get('name', '').encode()).decode()}," - # Key-value based feature flags store the variant value under "configuration_value". Feature - # flags loaded from the feature flag resource endpoint store it under "value" instead. - if "configuration_value" in v: - allocation_id += ( - f"{json.dumps(v.get('configuration_value', ''), separators=(',', ':'), sort_keys=True)}" - ) - elif "value" in v: - allocation_id += f"{json.dumps(v.get('value', ''), separators=(',', ':'), sort_keys=True)}" + # Key-value based feature flags store the variant value under "configuration_value". Enhanced + # feature flags store it under "value" instead. + if reference_path_segment == FEATURE_FLAG_KV_REFERENCE_SEGMENT: + value_key = "configuration_value" + else: + value_key = "value" + allocation_id += f"{json.dumps(v.get(value_key, ''), separators=(',', ':'), sort_keys=True)}" allocation_id += ";" if sorted_variants: allocation_id = allocation_id[:-1] @@ -438,28 +438,28 @@ def _process_key_value_base(self, config: ConfigurationSetting) -> Union[str, Di return config.value return config.value - def _process_feature_flags( + def _process_and_merge_feature_flags( self, processed_settings: Dict[str, Any], processed_feature_flags: List[Dict[str, Any]], feature_flags: Optional[List[FeatureFlagConfigurationSetting]], - feature_flag_resources: Optional[List[FeatureFlag]] = None, + enhanced_feature_flags: Optional[List[FeatureFlag]] = None, ) -> Dict[str, Any]: - if feature_flags or feature_flag_resources: + if feature_flags or enhanced_feature_flags: # Reset feature flag usage self._tracing_context.reset_feature_filter_usage() if feature_flags: - self._processed_kv_feature_flags = [self._process_feature_flag(ff) for ff in feature_flags] + self._processed_kv_feature_flags = [self._process_kv_feature_flag(ff) for ff in feature_flags] - if feature_flag_resources: - self._processed_resource_feature_flags = [ - self._process_feature_flag_resource(ff) for ff in feature_flag_resources + if enhanced_feature_flags: + self._processed_enhanced_feature_flags = [ + self._process_enhanced_feature_flag(ff) for ff in enhanced_feature_flags ] - if feature_flags or feature_flag_resources: + if feature_flags or enhanced_feature_flags: processed_feature_flags = self._merge_feature_flags( - self._processed_kv_feature_flags, self._processed_resource_feature_flags + self._processed_kv_feature_flags, self._processed_enhanced_feature_flags ) if self._feature_flag_enabled: @@ -469,18 +469,19 @@ def _process_feature_flags( @staticmethod def _merge_feature_flags( - kv_feature_flags: List[Dict[str, Any]], resource_feature_flags: List[Dict[str, Any]] + kv_feature_flags: List[Dict[str, Any]], enhanced_feature_flags: List[Dict[str, Any]] ) -> List[Dict[str, Any]]: """ - Merge feature flags loaded from the classic key-value store with feature flags loaded from the feature - flag resource endpoint. Feature flags are matched by their identifier (``id`` for key-value based feature - flags, ``name`` for resource-based feature flags). When both sources contain a feature flag with the same - identifier, the resource-based feature flag takes precedence. + Merge feature flags loaded from the key-value store with enhanced feature flags loaded from the + enhanced feature flag endpoint. Both sources populate the ``id`` field using the feature management + library's schema (for enhanced feature flags, the enhanced feature flag's name is used as the ``id``). + Feature flags are matched by their ``id`` field. When both sources contain a feature flag with the + same identifier, the enhanced feature flag takes precedence. - :param kv_feature_flags: The feature flags loaded from the classic key-value store. + :param kv_feature_flags: The feature flags loaded from the key-value store. :type kv_feature_flags: List[Dict[str, Any]] - :param resource_feature_flags: The feature flags loaded from the feature flag resource endpoint. - :type resource_feature_flags: List[Dict[str, Any]] + :param enhanced_feature_flags: The enhanced feature flags loaded from the enhanced feature flag endpoint. + :type enhanced_feature_flags: List[Dict[str, Any]] :return: The merged list of feature flags. :rtype: List[Dict[str, Any]] """ @@ -488,12 +489,12 @@ def _merge_feature_flags( for ff in kv_feature_flags: identifier = ff.get(FEATURE_FLAG_ID_FIELD) merged[identifier] = ff - for ff in resource_feature_flags: - identifier = ff.get(FEATURE_FLAG_NAME_FIELD) + for ff in enhanced_feature_flags: + identifier = ff.get(FEATURE_FLAG_ID_FIELD) merged[identifier] = ff return list(merged.values()) - def _process_feature_flag(self, feature_flag: FeatureFlagConfigurationSetting) -> Dict[str, Any]: + def _process_kv_feature_flag(self, feature_flag: FeatureFlagConfigurationSetting) -> Dict[str, Any]: try: feature_flag_value = json.loads(feature_flag.value) self._update_ff_telemetry_metadata(self._origin_endpoint, feature_flag, feature_flag_value) @@ -503,21 +504,22 @@ def _process_feature_flag(self, feature_flag: FeatureFlagConfigurationSetting) - # Feature flag value is not a valid JSON return {} - def _process_feature_flag_resource(self, feature_flag: FeatureFlag) -> Dict[str, Any]: + def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str, Any]: """ - Convert a feature flag resource, loaded from the feature flag resource endpoint, into a dictionary using - the feature flag resource's native field names. + Convert an enhanced feature flag, loaded from the enhanced feature flag endpoint, into a dictionary that + matches the feature management library's schema. + Ref: https://github.com/microsoft/FeatureManagement/blob/main/Schema/FeatureFlag.v2.0.0.schema.json - :param feature_flag: The feature flag resource. + :param feature_flag: The enhanced feature flag. :type feature_flag: ~azure.appconfiguration.FeatureFlag :return: The feature flag as a dictionary. :rtype: Dict[str, Any] """ feature_flag_value: Dict[str, Any] = { - FEATURE_FLAG_NAME_FIELD: feature_flag.name, + FEATURE_FLAG_ID_FIELD: feature_flag.name, "enabled": feature_flag.enabled, } - if feature_flag.label and not feature_flag.label.isspace(): + if feature_flag.label: feature_flag_value["label"] = feature_flag.label if feature_flag.description: feature_flag_value["description"] = feature_flag.description @@ -584,7 +586,7 @@ def _process_feature_flag_resource(self, feature_flag: FeatureFlag) -> Dict[str, if feature_flag.tags: feature_flag_value["tags"] = dict(feature_flag.tags) - self._update_ff_resource_telemetry_metadata(self._origin_endpoint, feature_flag, feature_flag_value) + self._update_enhanced_feature_flag_telemetry_metadata(self._origin_endpoint, feature_flag, feature_flag_value) self._tracing_context.update_feature_filter_telemetry_by_names(filter_names) return feature_flag_value 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 056e375ab7de..184ac5a3afc3 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 @@ -37,7 +37,7 @@ @dataclass class _ConfigurationClientWrapper(_ConfigurationClientWrapperBase): _client: AzureAppConfigurationClient - _feature_flag_client: Optional[FeatureFlagClient] = None + _enhanced_feature_flag_client: Optional[FeatureFlagClient] = None backoff_end_time: float = 0 failed_attempts: int = 0 LOGGER = getLogger(__name__) @@ -64,6 +64,7 @@ def from_credential( :return: A new instance of the _ConfigurationClientWrapper class :rtype: _ConfigurationClientWrapper """ + feature_flag_enabled = kwargs.pop("feature_flag_enabled", False) return cls( endpoint, AzureAppConfigurationClient( @@ -74,13 +75,17 @@ def from_credential( retry_backoff_max=retry_backoff_max, **kwargs, ), - FeatureFlagClient( - endpoint, - credential, - user_agent=user_agent, - retry_total=retry_total, - retry_backoff_max=retry_backoff_max, - **kwargs, + ( + FeatureFlagClient( + endpoint, + credential, + user_agent=user_agent, + retry_total=retry_total, + retry_backoff_max=retry_backoff_max, + **kwargs, + ) + if feature_flag_enabled + else None ), ) @@ -100,6 +105,7 @@ def from_connection_string( :return: A new instance of the _ConfigurationClientWrapper class :rtype: _ConfigurationClientWrapper """ + feature_flag_enabled = kwargs.pop("feature_flag_enabled", False) return cls( endpoint, AzureAppConfigurationClient.from_connection_string( @@ -109,12 +115,16 @@ def from_connection_string( retry_backoff_max=retry_backoff_max, **kwargs, ), - FeatureFlagClient.from_connection_string( - connection_string, - user_agent=user_agent, - retry_total=retry_total, - retry_backoff_max=retry_backoff_max, - **kwargs, + ( + FeatureFlagClient.from_connection_string( + connection_string, + user_agent=user_agent, + retry_total=retry_total, + retry_backoff_max=retry_backoff_max, + **kwargs, + ) + if feature_flag_enabled + else None ), ) @@ -236,8 +246,6 @@ def load_feature_flags( """ loaded_feature_flags: List[FeatureFlagConfigurationSetting] = [] page_etags: List[List[str]] = [] - # Needs to be removed unknown keyword argument for list_configuration_settings - kwargs.pop("sentinel_keys", None) for select in feature_flag_selectors: selector_etags: List[str] = [] if select.snapshot_name is not None: @@ -301,32 +309,26 @@ def check_feature_flag_page_etags( return False @distributed_trace - def load_feature_flag_resources( + def load_enhanced_feature_flags( self, feature_flag_selectors: List[SettingSelector], **kwargs ) -> Tuple[List[FeatureFlag], List[List[str]]]: """ - Loads feature flags from the feature flag resource endpoint using page-based iteration, collecting page - etags for each selector. Selectors with a ``snapshot_name`` are currently not supported by the feature flag - resource endpoint and are skipped. + Loads enhanced feature flags from the enhanced feature flag endpoint using page-based iteration. + The enhanced feature flag endpoint currently does not support snapshots. :param feature_flag_selectors: List of setting selectors to filter feature flags :type feature_flag_selectors: List[SettingSelector] - :return: A tuple of (feature_flags, page_etags_per_selector) + :return: A tuple of (feature_flags, page_etags_per_selector), with one page etags entry per selector, in the + same relative order as ``feature_flag_selectors``. :rtype: Tuple[List[~azure.appconfiguration.FeatureFlag], List[List[str]]] """ loaded_feature_flags: List[FeatureFlag] = [] - page_etags: List[List[str]] = [] - # Needs to be removed unknown keyword argument for the feature flag client - kwargs.pop("sentinel_keys", None) - if self._feature_flag_client is None: + if self._enhanced_feature_flag_client is None: return loaded_feature_flags, [[] for _ in feature_flag_selectors] + page_etags: List[List[str]] = [] for select in feature_flag_selectors: selector_etags: List[str] = [] - if select.snapshot_name is not None: - # Snapshots are not supported by the feature flag resource endpoint as of now - page_etags.append(selector_etags) - continue - feature_flags = self._feature_flag_client.list_feature_flags( + feature_flags = self._enhanced_feature_flag_client.list_feature_flags( name_filter=select.key_filter, label_filter=select.label_filter, tags_filter=select.tag_filters, @@ -340,30 +342,28 @@ def load_feature_flag_resources( return loaded_feature_flags, page_etags @distributed_trace - def check_feature_flag_resource_etags( + def check_enhanced_feature_flag_etags( self, feature_flag_selectors: List[SettingSelector], page_etags: List[List[str]], **kwargs ) -> bool: """ - Checks if any feature flag resource page has changed using page etags. + Checks if any enhanced feature flag page has changed using page etags. :param feature_flag_selectors: List of setting selectors for feature flags :type feature_flag_selectors: List[SettingSelector] - :param page_etags: The page etags from the last load, one list per selector + :param page_etags: The page etags from the last load, one entry per selector, in the same relative order as + ``feature_flag_selectors``. :type page_etags: List[List[str]] :return: True if any page has changed, False otherwise :rtype: bool """ - if self._feature_flag_client is None: + if self._enhanced_feature_flag_client is None: return False for i, select in enumerate(feature_flag_selectors): - if select.snapshot_name is not None: - # Snapshots are not supported by the feature flag resource endpoint - continue if i >= len(page_etags): # Missing or stale etag state should trigger a refresh instead of failing. return True selector_etags = page_etags[i] - feature_flags = self._feature_flag_client.list_feature_flags( + feature_flags = self._enhanced_feature_flag_client.list_feature_flags( name_filter=select.key_filter, label_filter=select.label_filter, tags_filter=select.tag_filters, @@ -454,19 +454,19 @@ def close(self) -> None: Closes the connection to Azure App Configuration. """ self._client.close() - if self._feature_flag_client is not None: - self._feature_flag_client.close() + if self._enhanced_feature_flag_client is not None: + self._enhanced_feature_flag_client.close() def __enter__(self): self._client.__enter__() - if self._feature_flag_client is not None: - self._feature_flag_client.__enter__() + if self._enhanced_feature_flag_client is not None: + self._enhanced_feature_flag_client.__enter__() return self def __exit__(self, *args): self._client.__exit__(*args) - if self._feature_flag_client is not None: - self._feature_flag_client.__exit__(*args) + if self._enhanced_feature_flag_client is not None: + self._enhanced_feature_flag_client.__exit__(*args) def resolve_snapshot_reference(self, setting: ConfigurationSetting, **kwargs) -> List[ConfigurationSetting]: """ 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 fc916cebff3a..2ed2e07053f3 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -15,14 +15,15 @@ ALLOCATION_ID_KEY = "AllocationId" ETAG_KEY = "ETag" -# Identifier field used by feature flags loaded from the classic key-value store. +# Identifier field required by the feature management library's schema for every feature flag entry. For +# enhanced feature flags, which do not have their own "id" concept, the enhanced feature flag's name is used +# as the value of this field. FEATURE_FLAG_ID_FIELD = "id" -# Identifier field used by feature flags loaded from the dedicated feature flag resource endpoint. -FEATURE_FLAG_NAME_FIELD = "name" # Path segment used to build the feature flag reference URL for feature flags loaded from the key-value store. FEATURE_FLAG_KV_REFERENCE_SEGMENT = "kv" -# Path segment used to build the feature flag reference URL for feature flags loaded from the resource endpoint. -FEATURE_FLAG_RESOURCE_REFERENCE_SEGMENT = "ff" +# Path segment used to build the feature flag reference URL for enhanced feature flags loaded from the enhanced +# feature flag endpoint. +ENHANCED_FEATURE_FLAG_REFERENCE_SEGMENT = "ff" # ------------------------------------------------------------------------ # Environment Variable Constants 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 c2bde9f89586..fd7c20e5ef0c 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 @@ -38,7 +38,7 @@ @dataclass class _AsyncConfigurationClientWrapper(_ConfigurationClientWrapperBase): _client: AzureAppConfigurationClient - _feature_flag_client: Optional[FeatureFlagClient] = None + _enhanced_feature_flag_client: Optional[FeatureFlagClient] = None backoff_end_time: float = 0 failed_attempts: int = 0 LOGGER = getLogger(__name__) @@ -65,6 +65,7 @@ def from_credential( :return: A new instance of the _AsyncConfigurationClientWrapper class :rtype: _AsyncConfigurationClientWrapper """ + feature_flag_enabled = kwargs.pop("feature_flag_enabled", False) return cls( endpoint, AzureAppConfigurationClient( @@ -75,13 +76,17 @@ def from_credential( retry_backoff_max=retry_backoff_max, **kwargs, ), - FeatureFlagClient( - endpoint, - credential, - user_agent=user_agent, - retry_total=retry_total, - retry_backoff_max=retry_backoff_max, - **kwargs, + ( + FeatureFlagClient( + endpoint, + credential, + user_agent=user_agent, + retry_total=retry_total, + retry_backoff_max=retry_backoff_max, + **kwargs, + ) + if feature_flag_enabled + else None ), ) @@ -101,6 +106,7 @@ def from_connection_string( :return: A new instance of the _AsyncConfigurationClientWrapper class :rtype: _AsyncConfigurationClientWrapper """ + feature_flag_enabled = kwargs.pop("feature_flag_enabled", False) return cls( endpoint, AzureAppConfigurationClient.from_connection_string( @@ -110,12 +116,16 @@ def from_connection_string( retry_backoff_max=retry_backoff_max, **kwargs, ), - FeatureFlagClient.from_connection_string( - connection_string, - user_agent=user_agent, - retry_total=retry_total, - retry_backoff_max=retry_backoff_max, - **kwargs, + ( + FeatureFlagClient.from_connection_string( + connection_string, + user_agent=user_agent, + retry_total=retry_total, + retry_backoff_max=retry_backoff_max, + **kwargs, + ) + if feature_flag_enabled + else None ), ) @@ -189,7 +199,7 @@ async def load_configuration_settings( async for config in page: if not isinstance(config, FeatureFlagConfigurationSetting): configuration_settings.append(config) - selector_etags.append(iterator.etag) # type: ignore[attr-defined] + selector_etags.append(iterator.etag) page_etags.append(selector_etags) return configuration_settings, page_etags @@ -237,8 +247,6 @@ async def load_feature_flags( """ loaded_feature_flags: List[FeatureFlagConfigurationSetting] = [] page_etags: List[List[str]] = [] - # Needs to be removed unknown keyword argument for list_configuration_settings - kwargs.pop("sentinel_keys", None) for select in feature_flag_selectors: selector_etags: List[str] = [] if select.snapshot_name is not None: @@ -264,7 +272,7 @@ async def load_feature_flags( async for ff in page: if isinstance(ff, FeatureFlagConfigurationSetting): loaded_feature_flags.append(ff) - selector_etags.append(iterator.etag) # type: ignore[attr-defined] + selector_etags.append(iterator.etag) page_etags.append(selector_etags) return loaded_feature_flags, page_etags @@ -302,33 +310,28 @@ async def check_feature_flag_page_etags( return False @distributed_trace - @distributed_trace - async def load_feature_flag_resources( + async def load_enhanced_feature_flags( self, feature_flag_selectors: List[SettingSelector], **kwargs ) -> Tuple[List[FeatureFlag], List[List[str]]]: """ - Loads feature flags from the feature flag resource endpoint using page-based iteration, collecting page - etags for each selector. Selectors with a ``snapshot_name`` are not supported by the feature flag resource - endpoint and are skipped. + Loads enhanced feature flags from the enhanced feature flag endpoint using page-based iteration, collecting + page etags for each selector. The enhanced feature flag endpoint currently does not support snapshots, so + ``feature_flag_selectors`` is expected to already be filtered to exclude selectors with a + ``snapshot_name`` (see ``ConfigurationProviderBase._enhanced_feature_flag_selectors``). :param feature_flag_selectors: List of setting selectors to filter feature flags :type feature_flag_selectors: List[SettingSelector] - :return: A tuple of (feature_flags, page_etags_per_selector) + :return: A tuple of (feature_flags, page_etags_per_selector), with one page etags entry per selector, in the + same relative order as ``feature_flag_selectors``. :rtype: Tuple[List[~azure.appconfiguration.FeatureFlag], List[List[str]]] """ loaded_feature_flags: List[FeatureFlag] = [] - page_etags: List[List[str]] = [] - # Needs to be removed unknown keyword argument for the feature flag client - kwargs.pop("sentinel_keys", None) - if self._feature_flag_client is None: + if self._enhanced_feature_flag_client is None: return loaded_feature_flags, [[] for _ in feature_flag_selectors] + page_etags: List[List[str]] = [] for select in feature_flag_selectors: selector_etags: List[str] = [] - if select.snapshot_name is not None: - # Snapshots are not supported by the feature flag resource endpoint - page_etags.append(selector_etags) - continue - feature_flags = self._feature_flag_client.list_feature_flags( + feature_flags = self._enhanced_feature_flag_client.list_feature_flags( name_filter=select.key_filter, label_filter=select.label_filter, tags_filter=select.tag_filters, @@ -338,35 +341,33 @@ async def load_feature_flag_resources( async for page in iterator: async for ff in page: loaded_feature_flags.append(ff) - selector_etags.append(iterator.etag) # type: ignore[attr-defined] + selector_etags.append(iterator.etag) page_etags.append(selector_etags) return loaded_feature_flags, page_etags @distributed_trace - async def check_feature_flag_resource_etags( + async def check_enhanced_feature_flag_etags( self, feature_flag_selectors: List[SettingSelector], page_etags: List[List[str]], **kwargs ) -> bool: """ - Checks if any feature flag resource page has changed using page etags. + Checks if any enhanced feature flag page has changed using page etags. :param feature_flag_selectors: List of setting selectors for feature flags :type feature_flag_selectors: List[SettingSelector] - :param page_etags: The page etags from the last load, one list per selector + :param page_etags: The page etags from the last load, one entry per selector, in the same relative order as + ``feature_flag_selectors``. :type page_etags: List[List[str]] :return: True if any page has changed, False otherwise :rtype: bool """ - if self._feature_flag_client is None: + if self._enhanced_feature_flag_client is None: return False for i, select in enumerate(feature_flag_selectors): - if select.snapshot_name is not None: - # Snapshots are not supported by the feature flag resource endpoint - continue if i >= len(page_etags): # Missing or stale etag state should trigger a refresh instead of failing. return True selector_etags = page_etags[i] - feature_flags = self._feature_flag_client.list_feature_flags( + feature_flags = self._enhanced_feature_flag_client.list_feature_flags( name_filter=select.key_filter, label_filter=select.label_filter, tags_filter=select.tag_filters, @@ -457,19 +458,19 @@ async def close(self) -> None: Closes the connection to Azure App Configuration. """ await self._client.close() - if self._feature_flag_client is not None: - await self._feature_flag_client.close() + if self._enhanced_feature_flag_client is not None: + await self._enhanced_feature_flag_client.close() async def __aenter__(self): await self._client.__aenter__() - if self._feature_flag_client is not None: - await self._feature_flag_client.__aenter__() + if self._enhanced_feature_flag_client is not None: + await self._enhanced_feature_flag_client.__aenter__() return self async def __aexit__(self, *args): await self._client.__aexit__(*args) - if self._feature_flag_client is not None: - await self._feature_flag_client.__aexit__(*args) + if self._enhanced_feature_flag_client is not None: + await self._enhanced_feature_flag_client.__aexit__(*args) async def resolve_snapshot_reference(self, setting: ConfigurationSetting, **kwargs) -> List[ConfigurationSetting]: """ 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 819102ad63ea..b06e5d536a80 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 @@ -121,7 +121,7 @@ async def _attempt_refresh( ) configuration_settings: List[ConfigurationSetting] = [] feature_flags: Optional[List[FeatureFlagConfigurationSetting]] = None - feature_flag_resources: Optional[List[FeatureFlag]] = 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 @@ -130,7 +130,7 @@ async def _attempt_refresh( existing_feature_flag_usage = self._tracing_context.feature_filter_usage.copy() page_etags: List[List[str]] = [] feature_flag_page_etags: List[List[str]] = [] - feature_flag_resource_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 @@ -164,14 +164,15 @@ async def _attempt_refresh( self._feature_flag_selectors, headers=headers, **kwargs ) - # Feature flag resources are loaded independently of the key-value based feature flags, using their + # 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._feature_flag_resource_etags or await client.check_feature_flag_resource_etags( - self._feature_flag_selectors, self._feature_flag_resource_etags, headers=headers, **kwargs + if not self._enhanced_feature_flag_etags or await client.check_enhanced_feature_flag_etags( + self._enhanced_feature_flag_selectors, self._enhanced_feature_flag_etags, headers=headers, + **kwargs ): - feature_flag_resources, feature_flag_resource_etags = await client.load_feature_flag_resources( - self._feature_flag_selectors, headers=headers, **kwargs + enhanced_feature_flags, enhanced_feature_flag_etags = await 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 @@ -182,8 +183,8 @@ async def _attempt_refresh( # Configuration Settings have been refreshed processed_settings = await self._process_configurations(configuration_settings, client) - processed_settings = self._process_feature_flags( - processed_settings, processed_feature_flags, feature_flags, feature_flag_resources + 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: @@ -192,14 +193,14 @@ async def _attempt_refresh( self._watched_settings.update(updated_watched_settings) if feature_flags is not None: self._feature_flag_page_etags = feature_flag_page_etags - if feature_flag_resources is not None: - self._feature_flag_resource_etags = feature_flag_resource_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 or feature_flag_resources) 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) @@ -307,7 +308,7 @@ async def _try_initialize(self, startup_exceptions: List[Exception], **kwargs: A processed_settings = await self._process_configurations(configuration_settings, client) feature_flag_page_etags: List[List[str]] = [] - feature_flag_resource_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 = await client.load_feature_flags( @@ -315,13 +316,13 @@ async def _try_initialize(self, startup_exceptions: List[Exception], **kwargs: A headers=headers, **kwargs, ) - feature_flag_resources, feature_flag_resource_etags = await client.load_feature_flag_resources( - self._feature_flag_selectors, + enhanced_feature_flags, enhanced_feature_flag_etags = await client.load_enhanced_feature_flags( + self._enhanced_feature_flag_selectors, headers=headers, **kwargs, ) - processed_settings = self._process_feature_flags( - processed_settings, [], feature_flags, feature_flag_resources + 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: @@ -349,7 +350,7 @@ async def _try_initialize(self, startup_exceptions: List[Exception], **kwargs: A self._dict = processed_settings self._page_etags = page_etags self._feature_flag_page_etags = feature_flag_page_etags - self._feature_flag_resource_etags = feature_flag_resource_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) @@ -416,7 +417,7 @@ async def _process_configurations( await 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) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md index e15d0f6b1d6e..83138f21b560 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md @@ -49,8 +49,8 @@ pip install azure.appconfiguration.provider | entra_id_sample.py | demos connecting to app configuration with Entra ID | | connection_string_sample.py | demos connecting to app configuration with a Connection String | | key_vault_reference_sample.py | demos resolving key vault references with App Configuration | -| feature_flag_resource_sample.py | demos loading feature flags created via the dedicated feature flag resource endpoint | -| async_feature_flag_resource_sample.py | async version of feature_flag_resource_sample.py | +| enhanced_feature_flag_sample.py | demos loading feature flags created via the dedicated enhanced feature flag endpoint | +| async_enhanced_feature_flag_sample.py | async version of enhanced_feature_flag_sample.py | ## Next steps diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_feature_flag_resource_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py similarity index 66% rename from sdk/appconfiguration/azure-appconfiguration-provider/samples/async_feature_flag_resource_sample.py rename to sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py index 2b11ecf1ba1f..3b61b6275241 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_feature_flag_resource_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py @@ -4,14 +4,14 @@ # license information. # ------------------------------------------------------------------------- """ -FILE: async_feature_flag_resource_sample.py +FILE: async_enhanced_feature_flag_sample.py DESCRIPTION: - This sample demonstrates loading feature flags that were created using the dedicated feature flag - resource endpoint (via ``FeatureFlagClient``/``FeatureFlag``), as opposed to the classic key-value + This sample demonstrates loading feature flags that were created using the dedicated enhanced feature flag + endpoint (via ``FeatureFlagClient``/``FeatureFlag``), as opposed to the key-value based feature flags stored as configuration settings. The provider loads both kinds of feature flags side by side into the same ``feature_management.feature_flags`` list, so no additional - ``load()`` options are required to opt in. This is the async version of feature_flag_resource_sample.py. -USAGE: python async_feature_flag_resource_sample.py + ``load()`` options are required to opt in. This is the async version of enhanced_feature_flag_sample.py. +USAGE: python async_enhanced_feature_flag_sample.py Set the environment variable APPCONFIGURATION_ENDPOINT_STRING with your App Configuration connection endpoint before running the sample. """ @@ -30,48 +30,48 @@ async def main(): credential = get_credential(authority, is_async=True) kwargs = get_client_modifications() - # Creating a feature flag using the dedicated feature flag resource endpoint. This is a separate - # resource type from the classic key-value based feature flags, and is managed via FeatureFlagClient + # Creating a feature flag using the dedicated enhanced feature flag endpoint. This is a separate + # resource type from the key-value based feature flags, and is managed via FeatureFlagClient # instead of AzureAppConfigurationClient. feature_flag_client = FeatureFlagClient(endpoint, credential, **kwargs) - await feature_flag_client.set_feature_flag(FeatureFlag(name="ResourceBeta", enabled=True)) + await feature_flag_client.set_feature_flag(FeatureFlag(name="EnhancedFeatureBeta", enabled=True)) try: - # [START feature_flag_resource_loading_async] + # [START enhanced_feature_flag_loading_async] from azure.appconfiguration.provider.aio import load - # Feature flags loaded from the feature flag resource endpoint are merged into the same + # 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 = await load(endpoint=endpoint, credential=credential, feature_flag_enabled=True, **kwargs) feature_flags = config["feature_management"]["feature_flags"] - resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") - print(resource_beta["enabled"]) + enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("id") == "EnhancedFeatureBeta") + print(enhanced_flag_beta["enabled"]) await config.close() - # [END feature_flag_resource_loading_async] + # [END enhanced_feature_flag_loading_async] - # [START feature_flag_resource_selector_async] + # [START enhanced_feature_flag_selector_async] from azure.appconfiguration.provider.aio import load from azure.appconfiguration.provider import SettingSelector - # The same SettingSelector used to filter key-value based feature flags also filters feature flag - # resources, by name/label/tags. + # The same SettingSelector used to filter key-value based feature flags also filters enhanced feature + # flags, by name/label/tags. config = await load( endpoint=endpoint, credential=credential, feature_flag_enabled=True, - feature_flag_selectors=[SettingSelector(key_filter="Resource*")], + feature_flag_selectors=[SettingSelector(key_filter="Enhanced*")], **kwargs, ) feature_flags = config["feature_management"]["feature_flags"] - resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") - print(resource_beta["enabled"]) + enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("id") == "EnhancedFeatureBeta") + print(enhanced_flag_beta["enabled"]) await config.close() - # [END feature_flag_resource_selector_async] + # [END enhanced_feature_flag_selector_async] finally: - # Cleaning up the feature flag resource created for this sample. - await feature_flag_client.delete_feature_flag("ResourceBeta") + # Cleaning up the enhanced feature flag created for this sample. + await feature_flag_client.delete_feature_flag("EnhancedFeatureBeta") await feature_flag_client.close() await credential.close() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/feature_flag_resource_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py similarity index 60% rename from sdk/appconfiguration/azure-appconfiguration-provider/samples/feature_flag_resource_sample.py rename to sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py index 0545d534f1a6..815a3696d0db 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/feature_flag_resource_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py @@ -4,14 +4,14 @@ # license information. # ------------------------------------------------------------------------- """ -FILE: feature_flag_resource_sample.py +FILE: enhanced_feature_flag_sample.py DESCRIPTION: - This sample demonstrates loading feature flags that were created using the dedicated feature flag - resource endpoint (via ``FeatureFlagClient``/``FeatureFlag``), as opposed to the classic key-value + This sample demonstrates loading feature flags that were created using the dedicated enhanced feature flag + endpoint (via ``FeatureFlagClient``/``FeatureFlag``), as opposed to the key-value based feature flags stored as configuration settings. The provider loads both kinds of feature flags side by side into the same ``feature_management.feature_flags`` list, so no additional ``load()`` options are required to opt in. -USAGE: python feature_flag_resource_sample.py +USAGE: python enhanced_feature_flag_sample.py Set the environment variable APPCONFIGURATION_ENDPOINT_STRING with your App Configuration connection endpoint before running the sample. """ @@ -25,41 +25,41 @@ credential = get_credential(authority) kwargs = get_client_modifications() -# Creating a feature flag using the dedicated feature flag resource endpoint. This is a separate resource -# type from the classic key-value based feature flags, and is managed via FeatureFlagClient instead of +# Creating a feature flag using the dedicated enhanced feature flag endpoint. This is a separate resource +# type from the key-value based feature flags, and is managed via FeatureFlagClient instead of # AzureAppConfigurationClient. feature_flag_client = FeatureFlagClient(endpoint, credential, **kwargs) -feature_flag_client.set_feature_flag(FeatureFlag(name="ResourceBeta", enabled=True)) +feature_flag_client.set_feature_flag(FeatureFlag(name="EnhancedFeatureBeta", enabled=True)) try: - # [START feature_flag_resource_loading] + # [START enhanced_feature_flag_loading] from azure.appconfiguration.provider import load - # Feature flags loaded from the feature flag resource endpoint are merged into the same + # 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"] - resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") - print(resource_beta["enabled"]) - # [END feature_flag_resource_loading] + enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("id") == "EnhancedFeatureBeta") + print(enhanced_flag_beta["enabled"]) + # [END enhanced_feature_flag_loading] - # [START feature_flag_resource_selector] + # [START enhanced_feature_flag_selector] from azure.appconfiguration.provider import load, SettingSelector - # The same SettingSelector used to filter key-value based feature flags also filters feature flag - # resources, by name/label/tags. + # 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="Resource*")], + feature_flag_selectors=[SettingSelector(key_filter="Enhanced*")], **kwargs, ) feature_flags = config["feature_management"]["feature_flags"] - resource_beta = next(flag for flag in feature_flags if flag.get("name") == "ResourceBeta") - print(resource_beta["enabled"]) - # [END feature_flag_resource_selector] + enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("id") == "EnhancedFeatureBeta") + print(enhanced_flag_beta["enabled"]) + # [END enhanced_feature_flag_selector] finally: - # Cleaning up the feature flag resource created for this sample. - feature_flag_client.delete_feature_flag("ResourceBeta") + # Cleaning up the enhanced feature flag created for this sample. + feature_flag_client.delete_feature_flag("EnhancedFeatureBeta") feature_flag_client.close() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/tests/README.md new file mode 100644 index 000000000000..85f257fc1fce --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/README.md @@ -0,0 +1,50 @@ +# Azure App Configuration Python Provider tests + +(This content is for `azure-appconfiguration-provider` package developer only) + +The tests for this package are under the `tests/` directory and are split into two categories: + +* **Unit tests** (e.g. `tests/test_azureappconfigurationproviderbase.py`, `tests/test_configuration_client_manager.py`) — exercise internal logic in isolation using mocked clients. These do not require any App Configuration store, network access, or environment variables, and can be run at any time with no setup. +* **Integration tests** (e.g. `tests/test_provider.py`, `tests/test_provider_enhanced_feature_flags.py`, and their `tests/aio/` async equivalents) — exercise the provider end-to-end against an Azure App Configuration store. These tests are built on [`devtools_testutils`](https://github.com/Azure/azure-sdk-for-python/tree/main/eng/tools/azure-sdk-tools/devtools_testutils) and each test method is decorated with `@recorded_by_proxy` / `@recorded_by_proxy_async`, which route the test's HTTP traffic through the [test proxy](https://github.com/Azure/azure-sdk-tools/tree/main/tools/test-proxy) tool. + +## Live tests vs. recorded (playback) tests + +Whether an integration test makes a real network call or replays a recording is controlled entirely by the `AZURE_TEST_RUN_LIVE` environment variable, not by anything in this package's code: + +* `AZURE_TEST_RUN_LIVE=true` — Tests run in **live/record mode**. The test proxy forwards requests to the real endpoint configured via your environment variables (see below), and (unless `AZURE_SKIP_LIVE_RECORDING=true` is also set) records the request/response pairs as new recording files for use in future playback runs. +* `AZURE_TEST_RUN_LIVE` unset or `false` (the default, and what CI uses) — Tests run in **playback mode**. The test proxy replays the existing recordings instead of contacting the real service, so **no network calls are made** and no live App Configuration store is required. + +Recordings themselves are not stored directly in this repository — they live in the separate [`Azure/azure-sdk-assets`](https://github.com/Azure/azure-sdk-assets) repo, and this package's `assets.json` file pins the exact recordings revision (`Tag`) that CI uses. If you add or change integration tests, you need to generate new recordings and publish them: + +1. Run the affected tests with `AZURE_TEST_RUN_LIVE=true` (and without `AZURE_SKIP_LIVE_RECORDING`) so the test proxy records real interactions to local recording files. +2. From the repo root, push the new/updated recordings to the assets repo: + + ```bash + dotnet tool run test-proxy push -a sdk/appconfiguration/azure-appconfiguration-provider/assets.json + ``` + + This uploads the changed recordings and updates the `Tag` field in `assets.json`. +3. Commit the updated `assets.json` as part of your PR — this is what allows CI (which always runs in playback mode) to pick up the new recordings. + +Only re-record tests you added or intentionally changed; unrelated existing recordings don't need to be regenerated. + +## Environment variables for local testing + +To run the integration tests locally in live mode, create a `.env` file at the repository root (it is automatically loaded by `devtools_testutils`) with the following variables: + +``` +AZURE_TEST_RUN_LIVE=true +APPCONFIGURATION_CONNECTION_STRING= +APPCONFIGURATION_ENDPOINT_STRING=.azconfig.io> +APPCONFIGURATION_KEY_VAULT_REFERENCE= +APPCONFIGURATION_KEY_VAULT_REFERENCE2= +APPCONFIGURATION_KEYVAULT_SECRET_URL= +APPCONFIGURATION_KEYVAULT_SECRET_URL2= +``` + +Notes: + +* For key vault URI, you can create a secret in Azure Key Vault service. The key vault URI is the *Secret Identifier*, without the final version number. For example, if the secret identifier is `https://some_secret.vault.azure.net/secrets/fake-secret/30d8830ec5ed4a428d311292a826f452`, the key vault URI should be `https://some_secret.vault.azure.net/secrets/fake-secret/`. +* Authentication for Entra ID-based tests relies on your local Azure CLI login (`az login`); make sure you're signed in to the subscription that contains your App Configuration store. +* Add `AZURE_SKIP_LIVE_RECORDING=true` if you want to run tests live against the real store without generating/overwriting recording files (useful for a quick sanity check). +* Omit `AZURE_TEST_RUN_LIVE` (or set it to `false`) to run the same tests in playback mode against existing recordings — this does not require any of the App Configuration environment variables above. diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider_feature_flag_resources.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider_enhanced_feature_flags.py similarity index 77% rename from sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider_feature_flag_resources.py rename to sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider_enhanced_feature_flags.py index 538eddbcb1f7..051ad70ad882 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider_feature_flag_resources.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider_enhanced_feature_flags.py @@ -4,8 +4,8 @@ # license information. # -------------------------------------------------------------------------- """ -Tests for loading feature flags from the dedicated feature flag resource endpoint -(``FeatureFlagClient``/``FeatureFlag``), as opposed to the classic key-value based +Tests for loading feature flags from the dedicated enhanced feature flag endpoint +(``FeatureFlagClient``/``FeatureFlag``), as opposed to the key-value based ``FeatureFlagConfigurationSetting`` stored via ``AzureAppConfigurationClient`` (async version). """ import functools @@ -25,15 +25,15 @@ ) -class TestAppConfigurationProviderFeatureFlagResources(AppConfigTestCase): - """Tests for the provider loading feature flags from the dedicated feature flag resource endpoint (async).""" +class TestAppConfigurationProviderEnhancedFeatureFlags(AppConfigTestCase): + """Tests for the provider loading feature flags from the dedicated enhanced feature flag endpoint (async).""" # method: load @AppConfigProviderPreparer() @recorded_by_proxy_async - async def test_load_feature_flag_resource(self, appconfiguration_endpoint_string): - """A feature flag created via the feature flag resource endpoint should be loaded by the provider.""" - feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + async def test_load_enhanced_feature_flag(self, appconfiguration_endpoint_string): + """A feature flag created via the enhanced feature flag endpoint should be loaded by the provider.""" + feature_flag_client = self.create_enhanced_feature_flag_client(appconfiguration_endpoint_string) feature_flag = FeatureFlag(name="ResourceOnlyFeature", enabled=True) await feature_flag_client.set_feature_flag(feature_flag) @@ -53,9 +53,9 @@ async def test_load_feature_flag_resource(self, appconfiguration_endpoint_string # method: load @AppConfigProviderPreparer() @recorded_by_proxy_async - async def test_load_feature_flag_resource_disabled(self, appconfiguration_endpoint_string): - """A disabled feature flag resource should be loaded with enabled set to False.""" - feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + async def test_load_enhanced_feature_flag_disabled(self, appconfiguration_endpoint_string): + """A disabled enhanced feature flag should be loaded with enabled set to False.""" + feature_flag_client = self.create_enhanced_feature_flag_client(appconfiguration_endpoint_string) feature_flag = FeatureFlag(name="ResourceDisabledFeature", enabled=False) await feature_flag_client.set_feature_flag(feature_flag) @@ -74,9 +74,9 @@ async def test_load_feature_flag_resource_disabled(self, appconfiguration_endpoi # method: load @AppConfigProviderPreparer() @recorded_by_proxy_async - async def test_load_feature_flag_resource_with_label(self, appconfiguration_endpoint_string): - """A feature flag resource with a label should be loaded when the label filter matches.""" - feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + async def test_load_enhanced_feature_flag_with_label(self, appconfiguration_endpoint_string): + """An enhanced feature flag with a label should be loaded when the label filter matches.""" + feature_flag_client = self.create_enhanced_feature_flag_client(appconfiguration_endpoint_string) feature_flag = FeatureFlag(name="ResourceLabeledFeature", enabled=True, label="test_label") await feature_flag_client.set_feature_flag(feature_flag) @@ -97,9 +97,9 @@ async def test_load_feature_flag_resource_with_label(self, appconfiguration_endp # method: load @AppConfigProviderPreparer() @recorded_by_proxy_async - async def test_feature_flag_resource_selector_filters_by_name(self, appconfiguration_endpoint_string): - """The feature_flag_selectors key_filter should scope which feature flag resources are loaded.""" - feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + async def test_enhanced_feature_flag_selector_filters_by_name(self, appconfiguration_endpoint_string): + """The feature_flag_selectors key_filter should scope which enhanced feature flags are loaded.""" + feature_flag_client = self.create_enhanced_feature_flag_client(appconfiguration_endpoint_string) included_flag = FeatureFlag(name="IncludedResourceFeature", enabled=True) excluded_flag = FeatureFlag(name="ExcludedResourceFeature", enabled=True) await feature_flag_client.set_feature_flag(included_flag) @@ -122,16 +122,16 @@ async def test_feature_flag_resource_selector_filters_by_name(self, appconfigura # method: load @AppConfigProviderPreparer() @recorded_by_proxy_async - async def test_feature_flag_resource_overrides_key_value(self, appconfiguration_endpoint_string): - """A feature flag resource should take precedence over a key-value based feature flag with the + async def test_enhanced_feature_flag_overrides_key_value(self, appconfiguration_endpoint_string): + """An enhanced feature flag should take precedence over a key-value based feature flag with the same identifier when both are loaded.""" appconfig_client = self.create_appconfig_client(appconfiguration_endpoint_string) - feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + feature_flag_client = self.create_enhanced_feature_flag_client(appconfiguration_endpoint_string) kv_feature_flag = FeatureFlagConfigurationSetting(feature_id="OverlapFeature", enabled=False, label=NULL_CHAR) await appconfig_client.set_configuration_setting(kv_feature_flag) - resource_feature_flag = FeatureFlag(name="OverlapFeature", enabled=True) - await feature_flag_client.set_feature_flag(resource_feature_flag) + enhanced_feature_flag_obj = FeatureFlag(name="OverlapFeature", enabled=True) + await feature_flag_client.set_feature_flag(enhanced_feature_flag_obj) try: async with await self.create_client( @@ -140,12 +140,12 @@ async def test_feature_flag_resource_overrides_key_value(self, appconfiguration_ feature_flag_enabled=True, feature_flag_selectors=[SettingSelector(key_filter="OverlapFeature")], ) as client: - # The resource-based feature flag (enabled=True) should win over the key-value based one + # The enhanced feature flag (enabled=True) should win over the key-value based one # (enabled=False) since they share the same identifier. assert has_feature_flag(client, "OverlapFeature", enabled=True) feature_flag = get_feature_flag(client, "OverlapFeature") assert feature_flag is not None - assert "name" in feature_flag + assert "id" in feature_flag finally: await appconfig_client.delete_configuration_setting(key=kv_feature_flag.key, label=kv_feature_flag.label) await feature_flag_client.delete_feature_flag("OverlapFeature") diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py index 9a84248e5c4b..65d389d35186 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py @@ -35,7 +35,7 @@ def create_appconfig_client(self, appconfiguration_endpoint_string): cred = self.get_credential(AzureAppConfigurationClient, is_async=True) return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") - def create_feature_flag_client(self, appconfiguration_endpoint_string): + def create_enhanced_feature_flag_client(self, appconfiguration_endpoint_string): cred = self.get_credential(FeatureFlagClient, is_async=True) return FeatureFlagClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") @@ -86,9 +86,9 @@ async def set_test_settings_async(client, settings): await client.set_configuration_setting(setting) -async def cleanup_feature_flag_resources_async(feature_flag_client, feature_flags): +async def cleanup_enhanced_feature_flags_async(feature_flag_client, feature_flags): """ - Delete feature flag resources created via the dedicated feature flag resource endpoint (async version). + Delete enhanced feature flags created via the dedicated enhanced feature flag endpoint (async version). :param feature_flag_client: The async FeatureFlagClient to use for cleanup. :param feature_flags: List of FeatureFlag objects (or (name, label) tuples) to delete. diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index c4801f992526..792b02ed2440 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -34,6 +34,7 @@ METADATA_KEY, ETAG_KEY, FEATURE_FLAG_REFERENCE_KEY, + FEATURE_FLAG_KV_REFERENCE_SEGMENT, ) from azure.appconfiguration.provider._refresh_timer import _RefreshTimer @@ -212,6 +213,22 @@ def test_initialization_with_custom_values(self): self.assertTrue(provider._feature_flag_enabled) self.assertEqual(provider._refresh_timer._interval, 60) + def test_enhanced_feature_flag_selectors_excludes_snapshot_selectors(self): + """The enhanced feature flag endpoint doesn't support snapshots, so snapshot-name selectors should be + filtered out once at startup, while the original selector list (used for the key-value store, which does + support snapshots) is left untouched.""" + key_select = SettingSelector(key_filter="app:*") + snapshot_select = SettingSelector(snapshot_name="my-snapshot") + feature_flag_selectors = [snapshot_select, key_select] + + provider = AzureAppConfigurationProviderBase( + endpoint="https://test.azconfig.io", + feature_flag_selectors=feature_flag_selectors, + ) + + self.assertEqual(provider._feature_flag_selectors, feature_flag_selectors) + self.assertEqual(provider._enhanced_feature_flag_selectors, [key_select]) + def test_process_key_name_with_no_prefix(self): """Test key name processing with no matching prefix.""" config = Mock() @@ -357,7 +374,9 @@ def test_update_ff_telemetry_metadata_max_variants(self): def test_generate_allocation_id_no_allocation(self): """Test allocation ID generation with no allocation.""" feature_flag_value: Dict[str, Any] = {"no_allocation": "here"} - result = AzureAppConfigurationProviderBase._generate_allocation_id(feature_flag_value) + result = AzureAppConfigurationProviderBase._generate_allocation_id( + feature_flag_value, FEATURE_FLAG_KV_REFERENCE_SEGMENT + ) self.assertIsNone(result) def test_generate_allocation_id_with_allocation(self): @@ -374,7 +393,9 @@ def test_generate_allocation_id_with_allocation(self): ], } - result = AzureAppConfigurationProviderBase._generate_allocation_id(feature_flag_value) + result = AzureAppConfigurationProviderBase._generate_allocation_id( + feature_flag_value, FEATURE_FLAG_KV_REFERENCE_SEGMENT + ) self.assertIsNotNone(result) self.assertIsInstance(result, str) # Should be a base64 encoded string @@ -393,7 +414,9 @@ def test_generate_allocation_id_no_variants_no_seed(self): "default_when_enabled": "Control" } } - result = AzureAppConfigurationProviderBase._generate_allocation_id(feature_flag_value) + result = AzureAppConfigurationProviderBase._generate_allocation_id( + feature_flag_value, FEATURE_FLAG_KV_REFERENCE_SEGMENT + ) # Since default_when_enabled is provided, allocated_variants won't be empty # so this should return a valid allocation ID self.assertIsNotNone(result) @@ -406,24 +429,26 @@ def test_generate_allocation_id_truly_empty(self): # No seed and no default_when_enabled } } - result = AzureAppConfigurationProviderBase._generate_allocation_id(feature_flag_value) + result = AzureAppConfigurationProviderBase._generate_allocation_id( + feature_flag_value, FEATURE_FLAG_KV_REFERENCE_SEGMENT + ) # This should return None because allocated_variants is empty and no seed self.assertIsNone(result) -class TestProcessFeatureFlagResource(unittest.TestCase): - """Test processing of feature flags loaded from the dedicated feature flag resource endpoint.""" +class TestProcessEnhancedFeatureFlag(unittest.TestCase): + """Test processing of feature flags loaded from the dedicated enhanced feature flag endpoint.""" def setUp(self): self.provider = AzureAppConfigurationProviderBase(endpoint="https://test.azconfig.io") - def test_process_feature_flag_resource_minimal(self): - """Test processing a minimal feature flag resource.""" + def test_process_enhanced_feature_flag_minimal(self): + """Test processing a minimal enhanced feature flag.""" feature_flag = FeatureFlag(name="MyFeature", enabled=True) - result = self.provider._process_feature_flag_resource(feature_flag) + result = self.provider._process_enhanced_feature_flag(feature_flag) - self.assertEqual(result["name"], "MyFeature") + self.assertEqual(result["id"], "MyFeature") self.assertTrue(result["enabled"]) self.assertNotIn("label", result) self.assertNotIn("description", result) @@ -432,31 +457,23 @@ def test_process_feature_flag_resource_minimal(self): self.assertNotIn("allocation", result) self.assertNotIn("tags", result) # Telemetry metadata (ETag) is always attached during processing, even without an explicit - # telemetry configuration on the feature flag resource. + # telemetry configuration on the enhanced feature flag. self.assertIn("telemetry", result) self.assertNotIn("enabled", result["telemetry"]) - def test_process_feature_flag_resource_with_label_and_description(self): - """Test processing a feature flag resource with label and description.""" + def test_process_enhanced_feature_flag_with_label_and_description(self): + """Test processing an enhanced feature flag with label and description.""" feature_flag = FeatureFlag(name="MyFeature", enabled=False, label="prod", description="A test feature") - result = self.provider._process_feature_flag_resource(feature_flag) + result = self.provider._process_enhanced_feature_flag(feature_flag) - self.assertEqual(result["name"], "MyFeature") + self.assertEqual(result["id"], "MyFeature") self.assertFalse(result["enabled"]) self.assertEqual(result["label"], "prod") self.assertEqual(result["description"], "A test feature") - def test_process_feature_flag_resource_whitespace_label_omitted(self): - """Test that a whitespace-only label is not included in the processed output.""" - feature_flag = FeatureFlag(name="MyFeature", enabled=True, label=" ") - - result = self.provider._process_feature_flag_resource(feature_flag) - - self.assertNotIn("label", result) - - def test_process_feature_flag_resource_with_conditions(self): - """Test processing a feature flag resource with conditions/client filters.""" + def test_process_enhanced_feature_flag_with_conditions(self): + """Test processing an enhanced feature flag with conditions/client filters.""" feature_flag = FeatureFlag( name="MyFeature", enabled=True, @@ -466,15 +483,15 @@ def test_process_feature_flag_resource_with_conditions(self): ), ) - result = self.provider._process_feature_flag_resource(feature_flag) + result = self.provider._process_enhanced_feature_flag(feature_flag) self.assertEqual(result["conditions"]["requirement_type"], "All") self.assertEqual(len(result["conditions"]["client_filters"]), 1) self.assertEqual(result["conditions"]["client_filters"][0]["name"], "Percentage") self.assertEqual(result["conditions"]["client_filters"][0]["parameters"], {"Value": "50"}) - def test_process_feature_flag_resource_with_variants_and_allocation(self): - """Test processing a feature flag resource with variants and allocation.""" + def test_process_enhanced_feature_flag_with_variants_and_allocation(self): + """Test processing an enhanced feature flag with variants and allocation.""" feature_flag = FeatureFlag( name="MyFeature", enabled=True, @@ -492,7 +509,7 @@ def test_process_feature_flag_resource_with_variants_and_allocation(self): ), ) - result = self.provider._process_feature_flag_resource(feature_flag) + result = self.provider._process_enhanced_feature_flag(feature_flag) self.assertEqual(len(result["variants"]), 2) self.assertEqual(result["variants"][0]["name"], "Control") @@ -507,8 +524,8 @@ def test_process_feature_flag_resource_with_variants_and_allocation(self): self.assertEqual(allocation["group"], [{"variant": "Test", "groups": ["group1"]}]) self.assertEqual(allocation["seed"], "1234") - def test_process_feature_flag_resource_with_telemetry_and_tags(self): - """Test processing a feature flag resource with telemetry settings and tags.""" + def test_process_enhanced_feature_flag_with_telemetry_and_tags(self): + """Test processing an enhanced feature flag with telemetry settings and tags.""" feature_flag = FeatureFlag( name="MyFeature", enabled=True, @@ -516,49 +533,49 @@ def test_process_feature_flag_resource_with_telemetry_and_tags(self): tags={"team": "infra"}, ) - result = self.provider._process_feature_flag_resource(feature_flag) + result = self.provider._process_enhanced_feature_flag(feature_flag) # Telemetry metadata gets ETag/FeatureFlagReference metadata appended by - # _update_ff_resource_telemetry_metadata as part of processing. + # _update_enhanced_feature_flag_telemetry_metadata as part of processing. self.assertTrue(result["telemetry"]["enabled"]) self.assertEqual(result["telemetry"]["metadata"]["custom"], "value") self.assertEqual(result["tags"], {"team": "infra"}) - def test_process_feature_flag_resource_updates_telemetry_metadata(self): - """Test that processing a feature flag resource adds ETag/FeatureFlagReference telemetry metadata.""" + def test_process_enhanced_feature_flag_updates_telemetry_metadata(self): + """Test that processing an enhanced feature flag adds ETag/FeatureFlagReference telemetry metadata.""" feature_flag = FeatureFlag( name="MyFeature", enabled=True, label="prod", telemetry=FeatureFlagTelemetryConfiguration(enabled=True), ) - feature_flag.etag = "resource_etag" + feature_flag.etag = "enhanced_etag" - result = self.provider._process_feature_flag_resource(feature_flag) + result = self.provider._process_enhanced_feature_flag(feature_flag) metadata = result["telemetry"][METADATA_KEY] - self.assertEqual(metadata[ETAG_KEY], "resource_etag") + self.assertEqual(metadata[ETAG_KEY], "enhanced_etag") self.assertIn(FEATURE_FLAG_REFERENCE_KEY, metadata) - # The resource-based feature flag reference uses the "ff" path segment, not "kv". + # The enhanced feature flag reference uses the "ff" path segment, not "kv". self.assertIn("/ff/MyFeature", metadata[FEATURE_FLAG_REFERENCE_KEY]) self.assertIn("?label=prod", metadata[FEATURE_FLAG_REFERENCE_KEY]) -class TestUpdateFfResourceTelemetryMetadata(unittest.TestCase): - """Test the _update_ff_resource_telemetry_metadata method.""" +class TestUpdateEnhancedFeatureFlagTelemetryMetadata(unittest.TestCase): + """Test the _update_enhanced_feature_flag_telemetry_metadata method.""" def setUp(self): self.provider = AzureAppConfigurationProviderBase(endpoint="https://test.azconfig.io") - def test_update_ff_resource_telemetry_metadata(self): - """Test resource-based feature flag telemetry processing uses the 'ff' reference segment.""" + def test_update_enhanced_feature_flag_telemetry_metadata(self): + """Test enhanced feature flag telemetry processing uses the 'ff' reference segment.""" feature_flag = FeatureFlag(name="test_feature", enabled=True, label="test_label") feature_flag.etag = "test_etag" feature_flag_value: Dict[str, Any] = {TELEMETRY_KEY: {"enabled": True}} endpoint = "https://test.azconfig.io" - self.provider._update_ff_resource_telemetry_metadata(endpoint, feature_flag, feature_flag_value) + self.provider._update_enhanced_feature_flag_telemetry_metadata(endpoint, feature_flag, feature_flag_value) metadata = feature_flag_value[TELEMETRY_KEY][METADATA_KEY] self.assertEqual(metadata[ETAG_KEY], "test_etag") @@ -573,23 +590,23 @@ class TestMergeFeatureFlags(unittest.TestCase): def test_merge_no_overlap(self): """Test merging when there is no identifier overlap between the two sources.""" kv_flags = [{"id": "KvFeature", "enabled": True}] - resource_flags = [{"name": "ResourceFeature", "enabled": False}] + enhanced_flags = [{"id": "EnhancedFeature", "enabled": False}] - merged = AzureAppConfigurationProviderBase._merge_feature_flags(kv_flags, resource_flags) + merged = AzureAppConfigurationProviderBase._merge_feature_flags(kv_flags, enhanced_flags) self.assertEqual(len(merged), 2) self.assertIn({"id": "KvFeature", "enabled": True}, merged) - self.assertIn({"name": "ResourceFeature", "enabled": False}, merged) + self.assertIn({"id": "EnhancedFeature", "enabled": False}, merged) - def test_merge_resource_takes_precedence_on_collision(self): - """Test that a resource-based feature flag overrides a key-value one with the same identifier.""" + def test_merge_enhanced_takes_precedence_on_collision(self): + """Test that an enhanced feature flag overrides a key-value one with the same identifier.""" kv_flags = [{"id": "SharedFeature", "enabled": False, "source": "kv"}] - resource_flags = [{"name": "SharedFeature", "enabled": True, "source": "resource"}] + enhanced_flags = [{"id": "SharedFeature", "enabled": True, "source": "enhanced"}] - merged = AzureAppConfigurationProviderBase._merge_feature_flags(kv_flags, resource_flags) + merged = AzureAppConfigurationProviderBase._merge_feature_flags(kv_flags, enhanced_flags) self.assertEqual(len(merged), 1) - self.assertEqual(merged[0]["source"], "resource") + self.assertEqual(merged[0]["source"], "enhanced") self.assertTrue(merged[0]["enabled"]) def test_merge_empty_lists(self): @@ -605,11 +622,11 @@ def test_merge_only_kv_flags(self): self.assertEqual(len(merged), 2) - def test_merge_only_resource_flags(self): - """Test merging when only resource-based feature flags are present.""" - resource_flags = [{"name": "Feature1", "enabled": True}, {"name": "Feature2", "enabled": False}] + def test_merge_only_enhanced_flags(self): + """Test merging when only enhanced feature flags are present.""" + enhanced_flags = [{"id": "Feature1", "enabled": True}, {"id": "Feature2", "enabled": False}] - merged = AzureAppConfigurationProviderBase._merge_feature_flags([], resource_flags) + merged = AzureAppConfigurationProviderBase._merge_feature_flags([], enhanced_flags) self.assertEqual(len(merged), 2) 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 85d13144c746..4daa4eade5a8 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 @@ -389,46 +389,45 @@ def test_check_page_etags_keys_first_then_snapshot(): ) -def test_load_feature_flag_resources_no_feature_flag_client(): +def test_load_enhanced_feature_flags_no_feature_flag_client(): """When no feature flag client is configured, no service calls are made.""" mock_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client) selects = [SettingSelector(key_filter="app/*"), SettingSelector(key_filter="other/*")] - feature_flags, page_etags = wrapper.load_feature_flag_resources(selects) + feature_flags, page_etags = wrapper.load_enhanced_feature_flags(selects) assert feature_flags == [] assert page_etags == [[], []] -def test_load_feature_flag_resources_skips_snapshot_selectors(): - """Selectors with a snapshot_name are not supported by the feature flag resource endpoint and are skipped.""" +def test_load_enhanced_feature_flags_assumes_pre_filtered_selectors(): + """The enhanced feature flag endpoint does not support snapshots. Filtering out selectors with a + snapshot_name is the caller's responsibility (done once at startup via + ConfigurationProviderBase._enhanced_feature_flag_selectors), so this method should simply process whatever + selectors it is given.""" mock_client = Mock() mock_feature_flag_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) - selects = [ - SettingSelector(snapshot_name="my-snapshot"), - SettingSelector(key_filter="app/*"), - ] + selects = [SettingSelector(key_filter="app/*")] flag1 = Mock(name="flag1") mock_response = Mock() mock_response.by_page.return_value = _FakePagedIterator([([flag1], "etag1")]) mock_feature_flag_client.list_feature_flags.return_value = mock_response - feature_flags, page_etags = wrapper.load_feature_flag_resources(selects) + feature_flags, page_etags = wrapper.load_enhanced_feature_flags(selects) assert feature_flags == [flag1] - assert page_etags == [[], ["etag1"]] - # Only the non-snapshot selector should trigger a service call + assert page_etags == [["etag1"]] mock_feature_flag_client.list_feature_flags.assert_called_once_with( name_filter="app/*", label_filter="\0", tags_filter=None ) -def test_load_feature_flag_resources_multiple_pages(): +def test_load_enhanced_feature_flags_multiple_pages(): """Multiple pages should be aggregated and each page's etag collected.""" mock_client = Mock() mock_feature_flag_client = Mock() @@ -458,25 +457,25 @@ def __next__(self): mock_response.by_page.return_value = FakeIterator([([flag1], "etag1"), ([flag2], "etag2")]) mock_feature_flag_client.list_feature_flags.return_value = mock_response - feature_flags, page_etags = wrapper.load_feature_flag_resources(selects) + feature_flags, page_etags = wrapper.load_enhanced_feature_flags(selects) assert feature_flags == [flag1, flag2] assert page_etags == [["etag1", "etag2"]] -def test_check_feature_flag_resource_etags_no_feature_flag_client(): +def test_check_enhanced_feature_flag_etags_no_feature_flag_client(): """When no feature flag client is configured, no changes are reported.""" mock_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client) selects = [SettingSelector(key_filter="app/*")] - result = wrapper.check_feature_flag_resource_etags(selects, [["etag1"]]) + result = wrapper.check_enhanced_feature_flag_etags(selects, [["etag1"]]) assert result is False -def test_check_feature_flag_resource_etags_no_change(): +def test_check_enhanced_feature_flag_etags_no_change(): """When the returned pages are empty, no changes are reported.""" mock_client = Mock() mock_feature_flag_client = Mock() @@ -489,7 +488,7 @@ def test_check_feature_flag_resource_etags_no_change(): mock_response.by_page.return_value = iter([]) mock_feature_flag_client.list_feature_flags.return_value = mock_response - result = wrapper.check_feature_flag_resource_etags(selects, page_etags) + result = wrapper.check_enhanced_feature_flag_etags(selects, page_etags) assert result is False mock_feature_flag_client.list_feature_flags.assert_called_once_with( @@ -498,7 +497,7 @@ def test_check_feature_flag_resource_etags_no_change(): mock_response.by_page.assert_called_once_with(match_conditions=["etag1"]) -def test_check_feature_flag_resource_etags_change_detected(): +def test_check_enhanced_feature_flag_etags_change_detected(): """When a page is returned, a change should be reported.""" mock_client = Mock() mock_feature_flag_client = Mock() @@ -511,27 +510,36 @@ def test_check_feature_flag_resource_etags_change_detected(): mock_response.by_page.return_value = iter([[Mock()]]) mock_feature_flag_client.list_feature_flags.return_value = mock_response - result = wrapper.check_feature_flag_resource_etags(selects, page_etags) + result = wrapper.check_enhanced_feature_flag_etags(selects, page_etags) assert result is True -def test_check_feature_flag_resource_etags_skips_snapshot_selectors(): - """Selectors with a snapshot_name are not supported and should be skipped without a service call.""" +def test_check_enhanced_feature_flag_etags_assumes_pre_filtered_selectors(): + """The enhanced feature flag endpoint does not support snapshots. Filtering out selectors with a + snapshot_name is the caller's responsibility (done once at startup via + ConfigurationProviderBase._enhanced_feature_flag_selectors), so this method should simply process whatever + selectors it is given.""" mock_client = Mock() mock_feature_flag_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) - selects = [SettingSelector(snapshot_name="my-snapshot")] - page_etags = [[]] + selects = [SettingSelector(key_filter="app/*")] + page_etags = [["etag1"]] - result = wrapper.check_feature_flag_resource_etags(selects, page_etags) + mock_response = Mock() + mock_response.by_page.return_value = iter([]) + mock_feature_flag_client.list_feature_flags.return_value = mock_response + + result = wrapper.check_enhanced_feature_flag_etags(selects, page_etags) assert result is False - mock_feature_flag_client.list_feature_flags.assert_not_called() + mock_feature_flag_client.list_feature_flags.assert_called_once_with( + name_filter="app/*", label_filter="\0", tags_filter=None + ) -def test_check_feature_flag_resource_etags_missing_page_etags_triggers_refresh(): +def test_check_enhanced_feature_flag_etags_missing_page_etags_triggers_refresh(): """Missing etag state for a selector should trigger a refresh instead of failing.""" mock_client = Mock() mock_feature_flag_client = Mock() @@ -544,6 +552,6 @@ def test_check_feature_flag_resource_etags_missing_page_etags_triggers_refresh() mock_feature_flag_client.list_feature_flags.return_value = mock_response page_etags = [["etag1"]] - result = wrapper.check_feature_flag_resource_etags(selects, page_etags) + result = wrapper.check_enhanced_feature_flag_etags(selects, page_etags) assert result is True diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_resources.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_enhanced_feature_flags.py similarity index 76% rename from sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_resources.py rename to sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_enhanced_feature_flags.py index e9b82ec7457c..f2685adfef0b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_resources.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_enhanced_feature_flags.py @@ -4,8 +4,8 @@ # license information. # -------------------------------------------------------------------------- """ -Tests for loading feature flags from the dedicated feature flag resource endpoint -(``FeatureFlagClient``/``FeatureFlag``), as opposed to the classic key-value based +Tests for loading feature flags from the dedicated enhanced feature flag endpoint +(``FeatureFlagClient``/``FeatureFlag``), as opposed to the key-value based ``FeatureFlagConfigurationSetting`` stored via ``AzureAppConfigurationClient``. """ import functools @@ -23,15 +23,15 @@ ) -class TestAppConfigurationProviderFeatureFlagResources(AppConfigTestCase): - """Tests for the provider loading feature flags from the dedicated feature flag resource endpoint.""" +class TestAppConfigurationProviderEnhancedFeatureFlags(AppConfigTestCase): + """Tests for the provider loading feature flags from the dedicated enhanced feature flag endpoint.""" # method: load @AppConfigProviderPreparer() @recorded_by_proxy - def test_load_feature_flag_resource(self, appconfiguration_endpoint_string): - """A feature flag created via the feature flag resource endpoint should be loaded by the provider.""" - feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + def test_load_enhanced_feature_flag(self, appconfiguration_endpoint_string): + """A feature flag created via the enhanced feature flag endpoint should be loaded by the provider.""" + feature_flag_client = self.create_enhanced_feature_flag_client(appconfiguration_endpoint_string) feature_flag = FeatureFlag(name="ResourceOnlyFeature", enabled=True) feature_flag_client.set_feature_flag(feature_flag) @@ -51,9 +51,9 @@ def test_load_feature_flag_resource(self, appconfiguration_endpoint_string): # method: load @AppConfigProviderPreparer() @recorded_by_proxy - def test_load_feature_flag_resource_disabled(self, appconfiguration_endpoint_string): - """A disabled feature flag resource should be loaded with enabled set to False.""" - feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + def test_load_enhanced_feature_flag_disabled(self, appconfiguration_endpoint_string): + """A disabled enhanced feature flag should be loaded with enabled set to False.""" + feature_flag_client = self.create_enhanced_feature_flag_client(appconfiguration_endpoint_string) feature_flag = FeatureFlag(name="ResourceDisabledFeature", enabled=False) feature_flag_client.set_feature_flag(feature_flag) @@ -72,9 +72,9 @@ def test_load_feature_flag_resource_disabled(self, appconfiguration_endpoint_str # method: load @AppConfigProviderPreparer() @recorded_by_proxy - def test_load_feature_flag_resource_with_label(self, appconfiguration_endpoint_string): - """A feature flag resource with a label should be loaded when the label filter matches.""" - feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + def test_load_enhanced_feature_flag_with_label(self, appconfiguration_endpoint_string): + """An enhanced feature flag with a label should be loaded when the label filter matches.""" + feature_flag_client = self.create_enhanced_feature_flag_client(appconfiguration_endpoint_string) feature_flag = FeatureFlag(name="ResourceLabeledFeature", enabled=True, label="test_label") feature_flag_client.set_feature_flag(feature_flag) @@ -95,9 +95,9 @@ def test_load_feature_flag_resource_with_label(self, appconfiguration_endpoint_s # method: load @AppConfigProviderPreparer() @recorded_by_proxy - def test_feature_flag_resource_selector_filters_by_name(self, appconfiguration_endpoint_string): - """The feature_flag_selectors key_filter should scope which feature flag resources are loaded.""" - feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + def test_enhanced_feature_flag_selector_filters_by_name(self, appconfiguration_endpoint_string): + """The feature_flag_selectors key_filter should scope which enhanced feature flags are loaded.""" + feature_flag_client = self.create_enhanced_feature_flag_client(appconfiguration_endpoint_string) included_flag = FeatureFlag(name="IncludedResourceFeature", enabled=True) excluded_flag = FeatureFlag(name="ExcludedResourceFeature", enabled=True) feature_flag_client.set_feature_flag(included_flag) @@ -120,16 +120,16 @@ def test_feature_flag_resource_selector_filters_by_name(self, appconfiguration_e # method: load @AppConfigProviderPreparer() @recorded_by_proxy - def test_feature_flag_resource_overrides_key_value(self, appconfiguration_endpoint_string): - """A feature flag resource should take precedence over a key-value based feature flag with the + def test_enhanced_feature_flag_overrides_key_value(self, appconfiguration_endpoint_string): + """An enhanced feature flag should take precedence over a key-value based feature flag with the same identifier when both are loaded.""" appconfig_client = self.create_appconfig_client(appconfiguration_endpoint_string) - feature_flag_client = self.create_feature_flag_client(appconfiguration_endpoint_string) + feature_flag_client = self.create_enhanced_feature_flag_client(appconfiguration_endpoint_string) kv_feature_flag = FeatureFlagConfigurationSetting(feature_id="OverlapFeature", enabled=False, label=NULL_CHAR) appconfig_client.set_configuration_setting(kv_feature_flag) - resource_feature_flag = FeatureFlag(name="OverlapFeature", enabled=True) - feature_flag_client.set_feature_flag(resource_feature_flag) + enhanced_feature_flag_obj = FeatureFlag(name="OverlapFeature", enabled=True) + feature_flag_client.set_feature_flag(enhanced_feature_flag_obj) try: client = self.create_client( @@ -139,12 +139,12 @@ def test_feature_flag_resource_overrides_key_value(self, appconfiguration_endpoi feature_flag_selectors=[SettingSelector(key_filter="OverlapFeature")], ) - # The resource-based feature flag (enabled=True) should win over the key-value based one + # The enhanced feature flag (enabled=True) should win over the key-value based one # (enabled=False) since they share the same identifier. assert has_feature_flag(client, "OverlapFeature", enabled=True) feature_flag = get_feature_flag(client, "OverlapFeature") assert feature_flag is not None - assert "name" in feature_flag + assert "id" in feature_flag finally: appconfig_client.delete_configuration_setting(key=kv_feature_flag.key, label=kv_feature_flag.label) feature_flag_client.delete_feature_flag("OverlapFeature") diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py index a2cdedb6310e..7314dc389213 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py @@ -46,7 +46,7 @@ def create_appconfig_client(self, appconfiguration_endpoint_string): cred = self.get_credential(AzureAppConfigurationClient) return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") - def create_feature_flag_client(self, appconfiguration_endpoint_string): + def create_enhanced_feature_flag_client(self, appconfiguration_endpoint_string): cred = self.get_credential(FeatureFlagClient) return FeatureFlagClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") @@ -170,23 +170,23 @@ def create_feature_flag_config_setting(key, label, enabled, tags=None): return FeatureFlagConfigurationSetting(feature_id=key, label=label, enabled=enabled, tags=tags) -def create_feature_flag_resource(name, enabled, label=None, **kwargs): +def create_enhanced_feature_flag(name, enabled, label=None, **kwargs): """ - Create a FeatureFlag resource object for use with the dedicated feature flag resource endpoint - (``FeatureFlagClient``), as opposed to the classic key-value based ``FeatureFlagConfigurationSetting``. + Create a FeatureFlag object for use with the dedicated enhanced feature flag endpoint + (``FeatureFlagClient``), as opposed to the key-value based ``FeatureFlagConfigurationSetting``. :param name: The name/identifier of the feature flag. :param enabled: Whether the feature flag is enabled. :param label: The label of the feature flag. - :return: A FeatureFlag resource object. + :return: A FeatureFlag object. :rtype: ~azure.appconfiguration.FeatureFlag """ return FeatureFlag(name=name, enabled=enabled, label=label, **kwargs) -def cleanup_feature_flag_resources(feature_flag_client, feature_flags): +def cleanup_enhanced_feature_flags(feature_flag_client, feature_flags): """ - Delete feature flag resources created via the dedicated feature flag resource endpoint. + Delete enhanced feature flags created via the dedicated enhanced feature flag endpoint. :param feature_flag_client: The FeatureFlagClient to use for cleanup. :param feature_flags: List of FeatureFlag objects (or (name, label) tuples) to delete. From 177c154eec830579a5a42e3e374d0cd845764e5e Mon Sep 17 00:00:00 2001 From: Yuan Qu Date: Thu, 30 Jul 2026 14:20:25 -0700 Subject: [PATCH 18/25] Wire FeatureFlagSelector into feature flag loading, add samples, and add enhanced feature flag telemetry with tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../appconfiguration/provider/__init__.py | 2 + .../_azureappconfigurationproviderbase.py | 61 +++++++++++-- .../provider/_client_manager.py | 19 ++-- .../azure/appconfiguration/provider/_load.py | 23 +++-- .../appconfiguration/provider/_models.py | 40 +++++++++ .../provider/_request_tracing_context.py | 4 + .../provider/aio/_async_client_manager.py | 23 +++-- .../async_enhanced_feature_flag_sample.py | 23 +++++ .../samples/enhanced_feature_flag_sample.py | 20 +++++ .../test_azureappconfigurationproviderbase.py | 20 ++++- .../test_configuration_client_manager.py | 14 +-- .../tests/test_request_tracing_context.py | 89 +++++++++++++++++++ 12 files changed, 287 insertions(+), 51 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py index 66fe656d692c..b8fd9df2ffe0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py @@ -7,6 +7,7 @@ from ._azureappconfigurationprovider import AzureAppConfigurationProvider from ._models import ( AzureAppConfigurationKeyVaultOptions, + FeatureFlagSelector, SettingSelector, WatchKey, ) @@ -18,6 +19,7 @@ "load", "AzureAppConfigurationProvider", "AzureAppConfigurationKeyVaultOptions", + "FeatureFlagSelector", "SettingSelector", "WatchKey", ] 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 b6bf97f28e2b..803c8ee86527 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -27,7 +27,7 @@ FeatureFlagConfigurationSetting, FeatureFlag, ) -from ._models import SettingSelector +from ._models import FeatureFlagSelector, SettingSelector from ._constants import ( NULL_CHAR, TELEMETRY_KEY, @@ -84,6 +84,53 @@ def _build_watched_setting(setting: Union[str, Tuple[str, str]]) -> Tuple[str, s return key, label +def _normalize_feature_flag_selectors( + selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] +) -> Tuple[List[SettingSelector], List[FeatureFlagSelector]]: + """ + Normalizes the customer-provided ``feature_flag_selectors``, which may be either a ``List[SettingSelector]`` + or a ``List[FeatureFlagSelector]`` (the two types cannot be mixed in the same list), into the two selector + lists used internally to load both kinds of feature flags: + + - kv_selectors: Used to load key-value based feature flags (``SettingSelector.key_filter`` is used as the key + filter). + - enhanced_selectors: Used to load enhanced feature flags from the dedicated feature flag resource endpoint + (``FeatureFlagSelector.name_filter`` is used as the name filter). + + :param selectors: The customer-provided feature flag selectors, or None to use the default (all feature flags + without a label). + :type selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] + :return: A tuple of (kv_selectors, enhanced_selectors). + :rtype: Tuple[List[SettingSelector], List[FeatureFlagSelector]] + """ + if not selectors: + return [SettingSelector(key_filter="*")], [FeatureFlagSelector(name_filter="*")] + + is_feature_flag_selector = [isinstance(select, FeatureFlagSelector) for select in selectors] + if any(is_feature_flag_selector) and not all(is_feature_flag_selector): + raise TypeError( + "feature_flag_selectors must be either a list of SettingSelector or a list of FeatureFlagSelector, " + "not a mix of both." + ) + + if all(is_feature_flag_selector): + kv_selectors = [ + SettingSelector(key_filter=select.name_filter, label_filter=select.label_filter, tag_filters=select.tag_filters) + for select in selectors + ] + # FeatureFlagSelector has no snapshot_name, so every selector is used for enhanced feature flags. + enhanced_selectors = list(selectors) + return kv_selectors, enhanced_selectors + + kv_selectors = list(selectors) + enhanced_selectors = [ + FeatureFlagSelector(name_filter=select.key_filter, label_filter=select.label_filter, tag_filters=select.tag_filters) + for select in selectors + if select.snapshot_name is None + ] + return kv_selectors, enhanced_selectors + + class AzureAppConfigurationProviderBase(Mapping[str, Union[str, JSON]]): # pylint: disable=too-many-instance-attributes """ Provides a dictionary-like interface to Azure App Configuration settings. Enables loading of sets of configuration @@ -106,14 +153,9 @@ def __init__(self, **kwargs: Any) -> None: } self._refresh_timer: _RefreshTimer = _RefreshTimer(**kwargs) self._feature_flag_enabled = kwargs.pop("feature_flag_enabled", False) - self._feature_flag_selectors = kwargs.pop("feature_flag_selectors", None) - if self._feature_flag_selectors is None: - self._feature_flag_selectors = [SettingSelector(key_filter="*")] - # The enhanced feature flag currently does not support snapshots, so selectors with a snapshot_name are - # filtered out. - self._enhanced_feature_flag_selectors = [ - select for select in self._feature_flag_selectors if select.snapshot_name is None - ] + self._feature_flag_selectors, self._enhanced_feature_flag_selectors = _normalize_feature_flag_selectors( + kwargs.pop("feature_flag_selectors", None) + ) self._feature_flag_refresh_timer: _RefreshTimer = _RefreshTimer(**kwargs) self._feature_flag_refresh_enabled = kwargs.pop("feature_flag_refresh_enabled", False) refresh_enabled = kwargs.pop("refresh_enabled", None) @@ -588,6 +630,7 @@ def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str, self._update_enhanced_feature_flag_telemetry_metadata(self._origin_endpoint, feature_flag, feature_flag_value) self._tracing_context.update_feature_filter_telemetry_by_names(filter_names) + self._tracing_context.uses_enhanced_feature_flags = True return feature_flag_value def _update_watched_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 184ac5a3afc3..f42aecb8e027 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 @@ -27,7 +27,7 @@ FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL, MINIMAL_CLIENT_REFRESH_INTERVAL, ) -from ._models import SettingSelector +from ._models import FeatureFlagSelector, SettingSelector from ._constants import FEATURE_FLAG_PREFIX from ._discovery import find_auto_failover_endpoints from ._snapshot_reference_parser import SnapshotReferenceParser @@ -310,14 +310,13 @@ def check_feature_flag_page_etags( @distributed_trace def load_enhanced_feature_flags( - self, feature_flag_selectors: List[SettingSelector], **kwargs + self, feature_flag_selectors: List[FeatureFlagSelector], **kwargs ) -> Tuple[List[FeatureFlag], List[List[str]]]: """ Loads enhanced feature flags from the enhanced feature flag endpoint using page-based iteration. - The enhanced feature flag endpoint currently does not support snapshots. - :param feature_flag_selectors: List of setting selectors to filter feature flags - :type feature_flag_selectors: List[SettingSelector] + :param feature_flag_selectors: List of feature flag selectors to filter feature flags + :type feature_flag_selectors: List[FeatureFlagSelector] :return: A tuple of (feature_flags, page_etags_per_selector), with one page etags entry per selector, in the same relative order as ``feature_flag_selectors``. :rtype: Tuple[List[~azure.appconfiguration.FeatureFlag], List[List[str]]] @@ -329,7 +328,7 @@ def load_enhanced_feature_flags( for select in feature_flag_selectors: selector_etags: List[str] = [] feature_flags = self._enhanced_feature_flag_client.list_feature_flags( - name_filter=select.key_filter, + name_filter=select.name_filter, label_filter=select.label_filter, tags_filter=select.tag_filters, **kwargs, @@ -343,13 +342,13 @@ def load_enhanced_feature_flags( @distributed_trace def check_enhanced_feature_flag_etags( - self, feature_flag_selectors: List[SettingSelector], page_etags: List[List[str]], **kwargs + self, feature_flag_selectors: List[FeatureFlagSelector], page_etags: List[List[str]], **kwargs ) -> bool: """ Checks if any enhanced feature flag page has changed using page etags. - :param feature_flag_selectors: List of setting selectors for feature flags - :type feature_flag_selectors: List[SettingSelector] + :param feature_flag_selectors: List of feature flag selectors for feature flags + :type feature_flag_selectors: List[FeatureFlagSelector] :param page_etags: The page etags from the last load, one entry per selector, in the same relative order as ``feature_flag_selectors``. :type page_etags: List[List[str]] @@ -364,7 +363,7 @@ def check_enhanced_feature_flag_etags( return True selector_etags = page_etags[i] feature_flags = self._enhanced_feature_flag_client.list_feature_flags( - name_filter=select.key_filter, + name_filter=select.name_filter, label_filter=select.label_filter, tags_filter=select.tag_filters, **kwargs, 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..4624ddc71ed5 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_load.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_load.py @@ -10,13 +10,14 @@ Mapping, Optional, Tuple, + Union, overload, ) from azure.core.credentials import TokenCredential from ._constants import ( DEFAULT_STARTUP_TIMEOUT, ) -from ._models import AzureAppConfigurationKeyVaultOptions, SettingSelector +from ._models import AzureAppConfigurationKeyVaultOptions, FeatureFlagSelector, SettingSelector from ._utils import ( delay_failure, process_load_parameters, @@ -46,7 +47,7 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only on_refresh_success: Optional[Callable] = None, on_refresh_error: Optional[Callable[[Exception], None]] = None, feature_flag_enabled: bool = False, - feature_flag_selectors: Optional[List[SettingSelector]] = None, + feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = None, feature_flag_refresh_enabled: bool = False, startup_timeout: int = DEFAULT_STARTUP_TIMEOUT, **kwargs, @@ -84,9 +85,11 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only :paramtype on_refresh_error: Optional[Callable[[Exception], None]] :keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False. :paramtype feature_flag_enabled: bool - :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all - feature flags without a label. - :paramtype feature_flag_selectors: List[SettingSelector] + :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. Either a list of + ~azure.appconfiguration.provider.SettingSelector or a list of + ~azure.appconfiguration.provider.FeatureFlagSelector (the two types cannot be mixed in the same list). + By default will load all feature flags without a label. + :paramtype feature_flag_selectors: Union[List[SettingSelector], List[FeatureFlagSelector]] :keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is False. :paramtype feature_flag_refresh_enabled: bool @@ -121,7 +124,7 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only on_refresh_success: Optional[Callable] = None, on_refresh_error: Optional[Callable[[Exception], None]] = None, feature_flag_enabled: bool = False, - feature_flag_selectors: Optional[List[SettingSelector]] = None, + feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = None, feature_flag_refresh_enabled: bool = False, startup_timeout: int = DEFAULT_STARTUP_TIMEOUT, **kwargs, @@ -161,9 +164,11 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only :paramtype on_refresh_error: Optional[Callable[[Exception], None]] :keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False. :paramtype feature_flag_enabled: bool - :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all - feature flags without a label. - :paramtype feature_flag_selectors: List[SettingSelector] + :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. Either a list of + ~azure.appconfiguration.provider.SettingSelector or a list of + ~azure.appconfiguration.provider.FeatureFlagSelector (the two types cannot be mixed in the same list). + By default will load all feature flags without a label. + :paramtype feature_flag_selectors: Union[List[SettingSelector], List[FeatureFlagSelector]] :keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is False. :paramtype feature_flag_refresh_enabled: bool 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..1e5f3f593141 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py @@ -103,6 +103,46 @@ def __init__( self.snapshot_name = snapshot_name +class FeatureFlagSelector: + """ + Selects a set of feature flags from the dedicated enhanced feature flag endpoint. + + :keyword name_filter: A filter to select feature flags based on their name. + :type name_filter: str + :keyword label_filter: A filter to select feature flags based on their labels. Default + value is \0 i.e. (No Label) as seen in the portal. + :type label_filter: Optional[str] + :keyword tag_filters: A filter to select feature flags based on their tags. This is a + list of strings that will be used to match tags on the feature flags. 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". + :type tag_filters: Optional[List[str]] + """ + + def __init__( + self, + *, + name_filter: Optional[str] = None, + label_filter: Optional[str] = NULL_CHAR, + tag_filters: Optional[List[str]] = None, + ): + if name_filter is None: + raise ValueError("name_filter must be specified.") + + if tag_filters is not None: + if not isinstance(tag_filters, list): + raise TypeError("tag_filters must be a list of strings.") + for tag in tag_filters: + if not tag: + raise ValueError("Tag filter cannot be an empty string or None.") + if not isinstance(tag, str) or "=" not in tag or tag.startswith("="): + raise ValueError("Tag filter " + tag + ' does not follow the format "tagName=tagValue".') + + self.name_filter = name_filter + self.label_filter = label_filter + self.tag_filters = tag_filters + + class WatchKey(NamedTuple): key: str label: str = NULL_CHAR 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 330a719613a6..3043bb867d92 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 @@ -40,6 +40,7 @@ AI_CONFIGURATION_FEATURE = "AI" AI_CHAT_COMPLETION_FEATURE = "AICC" SNAPSHOT_REFERENCE_TAG = "SnapshotRef" +ENHANCED_FEATURE_FLAG_TAG = "EnhancedFF" # Correlation context constants FEATUREMANAGEMENT_PACKAGE = "featuremanagement" @@ -84,6 +85,7 @@ def __init__(self, load_balancing_enabled: bool = False) -> None: self.uses_ai_configuration = False self.uses_aicc_configuration = False # AI Chat Completion self.uses_snapshot_reference = False + self.uses_enhanced_feature_flags = False self.uses_telemetry = False self.uses_seed = False self.max_variants: Optional[int] = None @@ -285,6 +287,8 @@ def _create_features_string(self) -> str: features_list.append(AI_CHAT_COMPLETION_FEATURE) if self.uses_snapshot_reference: features_list.append(SNAPSHOT_REFERENCE_TAG) + if self.uses_enhanced_feature_flags: + features_list.append(ENHANCED_FEATURE_FLAG_TAG) return Delimiter.join(features_list) 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 fd7c20e5ef0c..4f215d5e3619 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 @@ -25,7 +25,7 @@ FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL, MINIMAL_CLIENT_REFRESH_INTERVAL, ) -from .._models import SettingSelector +from .._models import FeatureFlagSelector, SettingSelector from .._constants import FEATURE_FLAG_PREFIX from .._snapshot_reference_parser import SnapshotReferenceParser from .._constants import SNAPSHOT_REF_CONTENT_TYPE @@ -311,16 +311,13 @@ async def check_feature_flag_page_etags( @distributed_trace async def load_enhanced_feature_flags( - self, feature_flag_selectors: List[SettingSelector], **kwargs + self, feature_flag_selectors: List[FeatureFlagSelector], **kwargs ) -> Tuple[List[FeatureFlag], List[List[str]]]: """ - Loads enhanced feature flags from the enhanced feature flag endpoint using page-based iteration, collecting - page etags for each selector. The enhanced feature flag endpoint currently does not support snapshots, so - ``feature_flag_selectors`` is expected to already be filtered to exclude selectors with a - ``snapshot_name`` (see ``ConfigurationProviderBase._enhanced_feature_flag_selectors``). + Loads enhanced feature flags from the enhanced feature flag endpoint using page-based iteration. - :param feature_flag_selectors: List of setting selectors to filter feature flags - :type feature_flag_selectors: List[SettingSelector] + :param feature_flag_selectors: List of feature flag selectors to filter feature flags + :type feature_flag_selectors: List[FeatureFlagSelector] :return: A tuple of (feature_flags, page_etags_per_selector), with one page etags entry per selector, in the same relative order as ``feature_flag_selectors``. :rtype: Tuple[List[~azure.appconfiguration.FeatureFlag], List[List[str]]] @@ -332,7 +329,7 @@ async def load_enhanced_feature_flags( for select in feature_flag_selectors: selector_etags: List[str] = [] feature_flags = self._enhanced_feature_flag_client.list_feature_flags( - name_filter=select.key_filter, + name_filter=select.name_filter, label_filter=select.label_filter, tags_filter=select.tag_filters, **kwargs, @@ -347,13 +344,13 @@ async def load_enhanced_feature_flags( @distributed_trace async def check_enhanced_feature_flag_etags( - self, feature_flag_selectors: List[SettingSelector], page_etags: List[List[str]], **kwargs + self, feature_flag_selectors: List[FeatureFlagSelector], page_etags: List[List[str]], **kwargs ) -> bool: """ Checks if any enhanced feature flag page has changed using page etags. - :param feature_flag_selectors: List of setting selectors for feature flags - :type feature_flag_selectors: List[SettingSelector] + :param feature_flag_selectors: List of feature flag selectors for feature flags + :type feature_flag_selectors: List[FeatureFlagSelector] :param page_etags: The page etags from the last load, one entry per selector, in the same relative order as ``feature_flag_selectors``. :type page_etags: List[List[str]] @@ -368,7 +365,7 @@ async def check_enhanced_feature_flag_etags( return True selector_etags = page_etags[i] feature_flags = self._enhanced_feature_flag_client.list_feature_flags( - name_filter=select.key_filter, + name_filter=select.name_filter, label_filter=select.label_filter, tags_filter=select.tag_filters, **kwargs, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py index 3b61b6275241..0e4c65f17ea3 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py @@ -69,6 +69,29 @@ async def main(): await config.close() # [END enhanced_feature_flag_selector_async] + + # [START enhanced_feature_flag_selector_with_feature_flag_selector_async] + from azure.appconfiguration.provider.aio import load + from azure.appconfiguration.provider import FeatureFlagSelector + + # FeatureFlagSelector is an alternative to SettingSelector for selecting feature flags. It uses + # name_filter instead of key_filter (the equivalent concept for enhanced feature flags), and has no + # snapshot_name since the enhanced feature flag endpoint does not support snapshots. A list of + # FeatureFlagSelector is used to filter both key-value based and enhanced feature flags; it cannot be + # mixed with SettingSelector in the same list. + config = await 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"]) + + await config.close() + # [END enhanced_feature_flag_selector_with_feature_flag_selector_async] finally: # Cleaning up the enhanced feature flag created for this sample. await feature_flag_client.delete_feature_flag("EnhancedFeatureBeta") diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py index 815a3696d0db..d5f1df92f2b2 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py @@ -59,6 +59,26 @@ enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("id") == "EnhancedFeatureBeta") print(enhanced_flag_beta["enabled"]) # [END enhanced_feature_flag_selector] + + # [START enhanced_feature_flag_selector_with_feature_flag_selector] + from azure.appconfiguration.provider import load, FeatureFlagSelector + + # FeatureFlagSelector is an alternative to SettingSelector for selecting feature flags. It uses + # name_filter instead of key_filter (the equivalent concept for enhanced feature flags), and has no + # snapshot_name since the enhanced feature flag endpoint does not support snapshots. A list of + # FeatureFlagSelector is used to filter both key-value based and enhanced feature flags; it cannot be + # mixed with SettingSelector in the same list. + 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 enhanced_feature_flag_selector_with_feature_flag_selector] finally: # Cleaning up the enhanced feature flag created for this sample. feature_flag_client.delete_feature_flag("EnhancedFeatureBeta") diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 792b02ed2440..33658513884a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -27,7 +27,7 @@ _build_watched_setting, AzureAppConfigurationProviderBase, ) -from azure.appconfiguration.provider._models import SettingSelector +from azure.appconfiguration.provider._models import SettingSelector, FeatureFlagSelector from azure.appconfiguration.provider._constants import ( NULL_CHAR, TELEMETRY_KEY, @@ -216,7 +216,8 @@ def test_initialization_with_custom_values(self): def test_enhanced_feature_flag_selectors_excludes_snapshot_selectors(self): """The enhanced feature flag endpoint doesn't support snapshots, so snapshot-name selectors should be filtered out once at startup, while the original selector list (used for the key-value store, which does - support snapshots) is left untouched.""" + support snapshots) is left untouched. Enhanced selectors are converted to FeatureFlagSelector, since the + enhanced feature flag endpoint filters by name_filter rather than key_filter.""" key_select = SettingSelector(key_filter="app:*") snapshot_select = SettingSelector(snapshot_name="my-snapshot") feature_flag_selectors = [snapshot_select, key_select] @@ -227,7 +228,10 @@ def test_enhanced_feature_flag_selectors_excludes_snapshot_selectors(self): ) self.assertEqual(provider._feature_flag_selectors, feature_flag_selectors) - self.assertEqual(provider._enhanced_feature_flag_selectors, [key_select]) + self.assertEqual(len(provider._enhanced_feature_flag_selectors), 1) + self.assertIsInstance(provider._enhanced_feature_flag_selectors[0], FeatureFlagSelector) + self.assertEqual(provider._enhanced_feature_flag_selectors[0].name_filter, key_select.key_filter) + self.assertEqual(provider._enhanced_feature_flag_selectors[0].label_filter, key_select.label_filter) def test_process_key_name_with_no_prefix(self): """Test key name processing with no matching prefix.""" @@ -461,6 +465,16 @@ def test_process_enhanced_feature_flag_minimal(self): self.assertIn("telemetry", result) self.assertNotIn("enabled", result["telemetry"]) + def test_process_enhanced_feature_flag_sets_uses_enhanced_feature_flags_tracing(self): + """Processing an enhanced feature flag should mark the tracing context as having used the enhanced + feature flag endpoint, for the Correlation-Context telemetry header.""" + self.assertFalse(self.provider._tracing_context.uses_enhanced_feature_flags) + + feature_flag = FeatureFlag(name="MyFeature", enabled=True) + self.provider._process_enhanced_feature_flag(feature_flag) + + self.assertTrue(self.provider._tracing_context.uses_enhanced_feature_flags) + def test_process_enhanced_feature_flag_with_label_and_description(self): """Test processing an enhanced feature flag with label and description.""" feature_flag = FeatureFlag(name="MyFeature", enabled=False, label="prod", description="A test feature") 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 4daa4eade5a8..bfd9c6ad6620 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 @@ -7,7 +7,7 @@ from unittest.mock import patch, call, Mock, MagicMock import pytest from azure.appconfiguration.provider._client_manager import ConfigurationClientManager, _ConfigurationClientWrapper -from azure.appconfiguration.provider._models import SettingSelector +from azure.appconfiguration.provider._models import SettingSelector, FeatureFlagSelector def _create_mock_credential(): @@ -411,7 +411,7 @@ def test_load_enhanced_feature_flags_assumes_pre_filtered_selectors(): mock_feature_flag_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) - selects = [SettingSelector(key_filter="app/*")] + selects = [FeatureFlagSelector(name_filter="app/*")] flag1 = Mock(name="flag1") mock_response = Mock() @@ -433,7 +433,7 @@ def test_load_enhanced_feature_flags_multiple_pages(): mock_feature_flag_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) - selects = [SettingSelector(key_filter="app/*")] + selects = [FeatureFlagSelector(name_filter="app/*")] flag1 = Mock(name="flag1") flag2 = Mock(name="flag2") @@ -481,7 +481,7 @@ def test_check_enhanced_feature_flag_etags_no_change(): mock_feature_flag_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) - selects = [SettingSelector(key_filter="app/*")] + selects = [FeatureFlagSelector(name_filter="app/*")] page_etags = [["etag1"]] mock_response = Mock() @@ -503,7 +503,7 @@ def test_check_enhanced_feature_flag_etags_change_detected(): mock_feature_flag_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) - selects = [SettingSelector(key_filter="app/*")] + selects = [FeatureFlagSelector(name_filter="app/*")] page_etags = [["etag1"]] mock_response = Mock() @@ -524,7 +524,7 @@ def test_check_enhanced_feature_flag_etags_assumes_pre_filtered_selectors(): mock_feature_flag_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) - selects = [SettingSelector(key_filter="app/*")] + selects = [FeatureFlagSelector(name_filter="app/*")] page_etags = [["etag1"]] mock_response = Mock() @@ -545,7 +545,7 @@ def test_check_enhanced_feature_flag_etags_missing_page_etags_triggers_refresh() mock_feature_flag_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) - selects = [SettingSelector(key_filter="app/*"), SettingSelector(key_filter="other/*")] + selects = [FeatureFlagSelector(name_filter="app/*"), FeatureFlagSelector(name_filter="other/*")] # Only one entry provided for two selectors; the first selector's page hasn't changed. mock_response = Mock() mock_response.by_page.return_value = iter([]) 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..8db3c2a39356 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 @@ -21,6 +21,7 @@ TARGETING_FILTER_NAMES, FEATURE_FLAG_USES_SEED_TAG, FEATURE_FLAG_USES_TELEMETRY_TAG, + ENHANCED_FEATURE_FLAG_TAG, ) from azure.appconfiguration.provider._constants import ( REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, @@ -513,3 +514,91 @@ def test_correlation_context_with_multiple_tags(self): self.assertIn("UsesKeyVault", correlation_header) self.assertIn("Failover", correlation_header) self.assertIn(SNAPSHOT_REFERENCE_TAG, correlation_header) + + +class TestEnhancedFeatureFlagTracking(unittest.TestCase): + """Test enhanced feature flag usage tracking in request tracing context.""" + + def test_enhanced_feature_flag_tag_constant(self): + """Test that the enhanced feature flag tag constant has the expected value.""" + self.assertEqual(ENHANCED_FEATURE_FLAG_TAG, "EnhancedFF") + + def test_initialization(self): + """Test that request tracing context initializes enhanced feature flag tracking to False.""" + context = _RequestTracingContext() + self.assertFalse(context.uses_enhanced_feature_flags) + + def test_set_enhanced_feature_flag_usage(self): + """Test setting enhanced feature flag usage in tracing context.""" + context = _RequestTracingContext() + + # Initially false + self.assertFalse(context.uses_enhanced_feature_flags) + + # Set to true + context.uses_enhanced_feature_flags = True + self.assertTrue(context.uses_enhanced_feature_flags) + + # Set back to false + context.uses_enhanced_feature_flags = False + self.assertFalse(context.uses_enhanced_feature_flags) + + def test_correlation_context_without_enhanced_feature_flags(self): + """Test correlation context header when not using the enhanced feature flag endpoint.""" + context = _RequestTracingContext() + context.uses_enhanced_feature_flags = False + + headers = {} + updated_headers = context.update_correlation_context_header( + headers=headers, + request_type="Startup", + replica_count=0, + uses_key_vault=False, + feature_flag_enabled=False, + is_failover_request=False, + ) + + correlation_header = updated_headers.get("Correlation-Context", "") + self.assertIn("RequestType=Startup", correlation_header) + self.assertNotIn(ENHANCED_FEATURE_FLAG_TAG, correlation_header) + + def test_correlation_context_with_enhanced_feature_flags(self): + """Test correlation context header when using the enhanced feature flag endpoint.""" + context = _RequestTracingContext() + context.uses_enhanced_feature_flags = True + + headers = {} + updated_headers = context.update_correlation_context_header( + headers=headers, + request_type="Startup", + replica_count=0, + uses_key_vault=False, + feature_flag_enabled=False, + is_failover_request=False, + ) + + correlation_header = updated_headers.get("Correlation-Context", "") + self.assertIn("RequestType=Startup", correlation_header) + self.assertIn(f"Features={ENHANCED_FEATURE_FLAG_TAG}", correlation_header) + + def test_correlation_context_with_enhanced_feature_flags_and_snapshot_reference(self): + """Test correlation context header format when both enhanced feature flags and snapshot references are + used, verifying both feature tags are joined by the delimiter in the Features segment.""" + context = _RequestTracingContext() + context.uses_enhanced_feature_flags = True + context.uses_snapshot_reference = True + + headers = {} + updated_headers = context.update_correlation_context_header( + headers=headers, + request_type="Startup", + replica_count=0, + uses_key_vault=False, + feature_flag_enabled=False, + is_failover_request=False, + ) + + correlation_header = updated_headers.get("Correlation-Context", "") + self.assertIn(SNAPSHOT_REFERENCE_TAG, correlation_header) + self.assertIn(ENHANCED_FEATURE_FLAG_TAG, correlation_header) + From c383fba0744a61b8e9a389ea67ea46a55aa77204 Mon Sep 17 00:00:00 2001 From: Yuan Qu Date: Thu, 30 Jul 2026 14:53:27 -0700 Subject: [PATCH 19/25] Trim redundant comments and rewrite README enhanced feature flag section to recommend FeatureFlagSelector Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure-appconfiguration-provider/README.md | 28 +++++++++++++++++-- .../appconfiguration/provider/_constants.py | 7 ++--- .../async_enhanced_feature_flag_sample.py | 6 +--- .../samples/enhanced_feature_flag_sample.py | 6 +--- .../test_azureappconfigurationproviderbase.py | 4 --- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/README.md index 31ae14598326..345d1e0a82ce 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/README.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/README.md @@ -379,7 +379,7 @@ config = load( ### 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`. +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. @@ -396,7 +396,29 @@ print(enhanced_flag_beta["enabled"]) -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. +`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. + + + +```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"]) +``` + + + +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. @@ -419,6 +441,8 @@ print(enhanced_flag_beta["enabled"]) +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. 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 2ed2e07053f3..5c44b8717e2b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -15,14 +15,11 @@ ALLOCATION_ID_KEY = "AllocationId" ETAG_KEY = "ETag" -# Identifier field required by the feature management library's schema for every feature flag entry. For -# enhanced feature flags, which do not have their own "id" concept, the enhanced feature flag's name is used -# as the value of this field. +# Identifier field required by the feature management library's schema for every feature flag entry. FEATURE_FLAG_ID_FIELD = "id" # Path segment used to build the feature flag reference URL for feature flags loaded from the key-value store. FEATURE_FLAG_KV_REFERENCE_SEGMENT = "kv" -# Path segment used to build the feature flag reference URL for enhanced feature flags loaded from the enhanced -# feature flag endpoint. +# Path segment used to build the feature flag reference URL for enhanced feature flags. ENHANCED_FEATURE_FLAG_REFERENCE_SEGMENT = "ff" # ------------------------------------------------------------------------ diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py index 0e4c65f17ea3..d4dd51215445 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py @@ -74,11 +74,7 @@ async def main(): from azure.appconfiguration.provider.aio import load from azure.appconfiguration.provider import FeatureFlagSelector - # FeatureFlagSelector is an alternative to SettingSelector for selecting feature flags. It uses - # name_filter instead of key_filter (the equivalent concept for enhanced feature flags), and has no - # snapshot_name since the enhanced feature flag endpoint does not support snapshots. A list of - # FeatureFlagSelector is used to filter both key-value based and enhanced feature flags; it cannot be - # mixed with SettingSelector in the same list. + # FeatureFlagSelector is the dedicated selector type for filtering enhanced feature flags. config = await load( endpoint=endpoint, credential=credential, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py index d5f1df92f2b2..58bd72e141df 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py @@ -63,11 +63,7 @@ # [START enhanced_feature_flag_selector_with_feature_flag_selector] from azure.appconfiguration.provider import load, FeatureFlagSelector - # FeatureFlagSelector is an alternative to SettingSelector for selecting feature flags. It uses - # name_filter instead of key_filter (the equivalent concept for enhanced feature flags), and has no - # snapshot_name since the enhanced feature flag endpoint does not support snapshots. A list of - # FeatureFlagSelector is used to filter both key-value based and enhanced feature flags; it cannot be - # mixed with SettingSelector in the same list. + # FeatureFlagSelector is the dedicated selector type for filtering enhanced feature flags. config = load( endpoint=endpoint, credential=credential, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 33658513884a..f715b2fe9dfe 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -214,10 +214,6 @@ def test_initialization_with_custom_values(self): self.assertEqual(provider._refresh_timer._interval, 60) def test_enhanced_feature_flag_selectors_excludes_snapshot_selectors(self): - """The enhanced feature flag endpoint doesn't support snapshots, so snapshot-name selectors should be - filtered out once at startup, while the original selector list (used for the key-value store, which does - support snapshots) is left untouched. Enhanced selectors are converted to FeatureFlagSelector, since the - enhanced feature flag endpoint filters by name_filter rather than key_filter.""" key_select = SettingSelector(key_filter="app:*") snapshot_select = SettingSelector(snapshot_name="my-snapshot") feature_flag_selectors = [snapshot_select, key_select] From 198846a8190dbc383478eab20e1f3457bf0093c8 Mon Sep 17 00:00:00 2001 From: Yuan Qu Date: Thu, 30 Jul 2026 14:58:13 -0700 Subject: [PATCH 20/25] Trim redundant docstring in test_configuration_client_manager.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/test_configuration_client_manager.py | 4 ---- 1 file changed, 4 deletions(-) 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 bfd9c6ad6620..986239f61dfc 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 @@ -403,10 +403,6 @@ def test_load_enhanced_feature_flags_no_feature_flag_client(): def test_load_enhanced_feature_flags_assumes_pre_filtered_selectors(): - """The enhanced feature flag endpoint does not support snapshots. Filtering out selectors with a - snapshot_name is the caller's responsibility (done once at startup via - ConfigurationProviderBase._enhanced_feature_flag_selectors), so this method should simply process whatever - selectors it is given.""" mock_client = Mock() mock_feature_flag_client = Mock() wrapper = _ConfigurationClientWrapper("https://fake.endpoint", mock_client, mock_feature_flag_client) From 969c15c27fea8a44012a57f35c850727a64c4b9f Mon Sep 17 00:00:00 2001 From: Yuan Qu Date: Thu, 30 Jul 2026 16:19:23 -0700 Subject: [PATCH 21/25] Update ENHANCED_FEATURE_FLAG_TAG from EnhancedFF to EnhFF Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/appconfiguration/provider/_request_tracing_context.py | 2 +- .../tests/test_request_tracing_context.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 3043bb867d92..a4aa3e974aae 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 @@ -40,7 +40,7 @@ AI_CONFIGURATION_FEATURE = "AI" AI_CHAT_COMPLETION_FEATURE = "AICC" SNAPSHOT_REFERENCE_TAG = "SnapshotRef" -ENHANCED_FEATURE_FLAG_TAG = "EnhancedFF" +ENHANCED_FEATURE_FLAG_TAG = "EnhFF" # Correlation context constants FEATUREMANAGEMENT_PACKAGE = "featuremanagement" 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 8db3c2a39356..0ff004b198b4 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 @@ -521,7 +521,7 @@ class TestEnhancedFeatureFlagTracking(unittest.TestCase): def test_enhanced_feature_flag_tag_constant(self): """Test that the enhanced feature flag tag constant has the expected value.""" - self.assertEqual(ENHANCED_FEATURE_FLAG_TAG, "EnhancedFF") + self.assertEqual(ENHANCED_FEATURE_FLAG_TAG, "EnhFF") def test_initialization(self): """Test that request tracing context initializes enhanced feature flag tracking to False.""" From 3a11cd2198845c7e539f8f5ac53d18aaebedb311 Mon Sep 17 00:00:00 2001 From: Yuan Qu Date: Fri, 31 Jul 2026 11:28:11 -0700 Subject: [PATCH 22/25] Fix feature flag selector type checking to support sets and reset enhanced feature flag tracing when none are loaded Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../_azureappconfigurationproviderbase.py | 19 +++++++++++-------- .../test_azureappconfigurationproviderbase.py | 11 ++++++++--- 2 files changed, 19 insertions(+), 11 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 803c8ee86527..16bceab38bfc 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -106,14 +106,17 @@ def _normalize_feature_flag_selectors( if not selectors: return [SettingSelector(key_filter="*")], [FeatureFlagSelector(name_filter="*")] - is_feature_flag_selector = [isinstance(select, FeatureFlagSelector) for select in selectors] - if any(is_feature_flag_selector) and not all(is_feature_flag_selector): - raise TypeError( - "feature_flag_selectors must be either a list of SettingSelector or a list of FeatureFlagSelector, " - "not a mix of both." - ) + selectors_iter = iter(selectors) + first_selector = next(selectors_iter) + is_feature_flag_selector = isinstance(first_selector, FeatureFlagSelector) + for select in selectors_iter: + if isinstance(select, FeatureFlagSelector) != is_feature_flag_selector: + raise TypeError( + "feature_flag_selectors must be either a list of SettingSelector or a list of FeatureFlagSelector, " + "not a mix of both." + ) - if all(is_feature_flag_selector): + if is_feature_flag_selector: kv_selectors = [ SettingSelector(key_filter=select.name_filter, label_filter=select.label_filter, tag_filters=select.tag_filters) for select in selectors @@ -498,6 +501,7 @@ def _process_and_merge_feature_flags( self._processed_enhanced_feature_flags = [ self._process_enhanced_feature_flag(ff) for ff in enhanced_feature_flags ] + self._tracing_context.uses_enhanced_feature_flags = bool(enhanced_feature_flags) if feature_flags or enhanced_feature_flags: processed_feature_flags = self._merge_feature_flags( @@ -630,7 +634,6 @@ def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str, self._update_enhanced_feature_flag_telemetry_metadata(self._origin_endpoint, feature_flag, feature_flag_value) self._tracing_context.update_feature_filter_telemetry_by_names(filter_names) - self._tracing_context.uses_enhanced_feature_flags = True return feature_flag_value def _update_watched_settings( diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index f715b2fe9dfe..476d29b2e155 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -462,15 +462,20 @@ def test_process_enhanced_feature_flag_minimal(self): self.assertNotIn("enabled", result["telemetry"]) def test_process_enhanced_feature_flag_sets_uses_enhanced_feature_flags_tracing(self): - """Processing an enhanced feature flag should mark the tracing context as having used the enhanced - feature flag endpoint, for the Correlation-Context telemetry header.""" + """Processing and merging enhanced feature flags should mark the tracing context as having used the + enhanced feature flag endpoint, for the Correlation-Context telemetry header. The flag should reset to + False if a subsequent refresh returns no enhanced feature flags.""" self.assertFalse(self.provider._tracing_context.uses_enhanced_feature_flags) feature_flag = FeatureFlag(name="MyFeature", enabled=True) - self.provider._process_enhanced_feature_flag(feature_flag) + self.provider._process_and_merge_feature_flags({}, [], [], [feature_flag]) self.assertTrue(self.provider._tracing_context.uses_enhanced_feature_flags) + self.provider._process_and_merge_feature_flags({}, [], [], []) + + self.assertFalse(self.provider._tracing_context.uses_enhanced_feature_flags) + def test_process_enhanced_feature_flag_with_label_and_description(self): """Test processing an enhanced feature flag with label and description.""" feature_flag = FeatureFlag(name="MyFeature", enabled=False, label="prod", description="A test feature") From 33fdc6bbc5a29dc532c732cdaa324a7d120fa2df Mon Sep 17 00:00:00 2001 From: Yuan Qu Date: Fri, 31 Jul 2026 15:53:16 -0700 Subject: [PATCH 23/25] 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> --- .../CHANGELOG.md | 2 + .../azure-appconfiguration-provider/README.md | 2 +- .../azure-appconfiguration-provider/api.md | 19 ++++-- .../api.metadata.yml | 4 +- .../_azureappconfigurationproviderbase.py | 61 ++++++++++-------- .../appconfiguration/provider/_constants.py | 2 +- .../provider/aio/_async_client_manager.py | 2 +- .../provider/aio/_async_load.py | 23 ++++--- .../_azureappconfigurationproviderasync.py | 3 +- .../azure-appconfiguration-provider/setup.py | 5 +- .../test_azureappconfigurationproviderbase.py | 64 ++++++++++++++----- .../tests/test_request_tracing_context.py | 1 - .../tests/{README.md => tests.md} | 22 +++++++ 13 files changed, 144 insertions(+), 66 deletions(-) rename sdk/appconfiguration/azure-appconfiguration-provider/tests/{README.md => tests.md} (76%) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md index 5d36e017dc8a..b2e72a63ba26 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md @@ -8,6 +8,8 @@ ### Breaking Changes +- 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. + ### Bugs Fixed ### Other Changes diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/README.md index 345d1e0a82ce..767ac1d2dba4 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/README.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/README.md @@ -539,7 +539,7 @@ This library uses the standard [logging](https://docs.python.org/3/library/loggi (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. +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. ## Next steps diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/api.md b/sdk/appconfiguration/azure-appconfiguration-provider/api.md index 97959ac187a1..8c4676874192 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/api.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/api.md @@ -8,7 +8,7 @@ namespace azure.appconfiguration.provider *, feature_flag_enabled: bool = False, feature_flag_refresh_enabled: bool = False, - feature_flag_selectors: Optional[List[SettingSelector]] = ..., + feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = ..., key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = ..., keyvault_client_configs: Optional[Mapping[str, JSON]] = ..., keyvault_credential: Optional[TokenCredential] = ..., @@ -31,7 +31,7 @@ namespace azure.appconfiguration.provider connection_string: str, feature_flag_enabled: bool = False, feature_flag_refresh_enabled: bool = False, - feature_flag_selectors: Optional[List[SettingSelector]] = ..., + feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = ..., key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = ..., keyvault_client_configs: Optional[Mapping[str, JSON]] = ..., keyvault_credential: Optional[TokenCredential] = ..., @@ -68,6 +68,17 @@ namespace azure.appconfiguration.provider def refresh(self, **kwargs) -> None: ... + class azure.appconfiguration.provider.FeatureFlagSelector: + + def __init__( + self, + *, + label_filter: Optional[str] = NULL_CHAR, + name_filter: Optional[str] = ..., + tag_filters: Optional[List[str]] = ... + ): ... + + class azure.appconfiguration.provider.SettingSelector: def __init__( @@ -94,7 +105,7 @@ namespace azure.appconfiguration.provider.aio *, feature_flag_enabled: bool = False, feature_flag_refresh_enabled: bool = False, - feature_flag_selectors: Optional[List[SettingSelector]] = ..., + feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = ..., key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = ..., keyvault_client_configs: Optional[Mapping[str, JSON]] = ..., keyvault_credential: Optional[AsyncTokenCredential] = ..., @@ -117,7 +128,7 @@ namespace azure.appconfiguration.provider.aio connection_string: str, feature_flag_enabled: bool = False, feature_flag_refresh_enabled: bool = False, - feature_flag_selectors: Optional[List[SettingSelector]] = ..., + feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = ..., key_vault_options: Optional[AzureAppConfigurationKeyVaultOptions] = ..., keyvault_client_configs: Optional[Mapping[str, JSON]] = ..., keyvault_credential: Optional[AsyncTokenCredential] = ..., diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/api.metadata.yml b/sdk/appconfiguration/azure-appconfiguration-provider/api.metadata.yml index 54ff9f5af349..3ea31f2567ee 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/api.metadata.yml +++ b/sdk/appconfiguration/azure-appconfiguration-provider/api.metadata.yml @@ -1,3 +1,3 @@ -apiMdSha256: 1b252a78094b95bf515ebe5ec67fc2de12ce5b595bad25438a58241ca7153b84 +apiMdSha256: 808ba824eedfb7fc2365479c13867983ee557982cd635995a9617e140c1919dc parserVersion: 0.3.28 -pythonVersion: 3.14.3 +pythonVersion: 3.12.10 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 16bceab38bfc..c42c252b2739 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -21,6 +21,7 @@ ItemsView, ValuesView, TypeVar, + cast, ) from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module ConfigurationSetting, @@ -95,7 +96,7 @@ def _normalize_feature_flag_selectors( - kv_selectors: Used to load key-value based feature flags (``SettingSelector.key_filter`` is used as the key filter). - enhanced_selectors: Used to load enhanced feature flags from the dedicated feature flag resource endpoint - (``FeatureFlagSelector.name_filter`` is used as the name filter). + (``FeatureFlagSelector.name_filter`` is used as the name filter). :param selectors: The customer-provided feature flag selectors, or None to use the default (all feature flags without a label). @@ -103,8 +104,12 @@ def _normalize_feature_flag_selectors( :return: A tuple of (kv_selectors, enhanced_selectors). :rtype: Tuple[List[SettingSelector], List[FeatureFlagSelector]] """ - if not selectors: + if selectors is None: return [SettingSelector(key_filter="*")], [FeatureFlagSelector(name_filter="*")] + if not selectors: + # An explicitly empty collection of selectors means no feature flags should be loaded, unlike None + # which falls back to the default of loading all unlabeled feature flags. + return [], [] selectors_iter = iter(selectors) first_selector = next(selectors_iter) @@ -117,18 +122,24 @@ def _normalize_feature_flag_selectors( ) if is_feature_flag_selector: + feature_flag_selectors = cast(List[FeatureFlagSelector], selectors) kv_selectors = [ - SettingSelector(key_filter=select.name_filter, label_filter=select.label_filter, tag_filters=select.tag_filters) - for select in selectors + SettingSelector( + key_filter=select.name_filter, label_filter=select.label_filter, tag_filters=select.tag_filters + ) + for select in feature_flag_selectors ] # FeatureFlagSelector has no snapshot_name, so every selector is used for enhanced feature flags. - enhanced_selectors = list(selectors) + enhanced_selectors = list(feature_flag_selectors) return kv_selectors, enhanced_selectors - kv_selectors = list(selectors) + setting_selectors = cast(List[SettingSelector], selectors) + kv_selectors = list(setting_selectors) enhanced_selectors = [ - FeatureFlagSelector(name_filter=select.key_filter, label_filter=select.label_filter, tag_filters=select.tag_filters) - for select in selectors + FeatureFlagSelector( + name_filter=select.key_filter, label_filter=select.label_filter, tag_filters=select.tag_filters + ) + for select in setting_selectors if select.snapshot_name is None ] return kv_selectors, enhanced_selectors @@ -273,7 +284,7 @@ def _update_ff_telemetry_metadata_common( # pylint: disable=too-many-positional feature_flag_reference += f"?label={label}" feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference - allocation_id = self._generate_allocation_id(feature_flag_value, reference_path_segment) + allocation_id = self._generate_allocation_id(feature_flag_value) if allocation_id: feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ALLOCATION_ID_KEY] = allocation_id @@ -287,14 +298,12 @@ def _update_ff_telemetry_metadata_common( # pylint: disable=too-many-positional self._tracing_context.update_max_variants(len(variants)) @staticmethod - def _generate_allocation_id(feature_flag_value: Dict[str, JSON], reference_path_segment: str) -> Optional[str]: + def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str]: """ Generates an allocation ID for the specified feature. 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. - :param str reference_path_segment: The path segment identifying which source the feature flag was loaded - from, e.g. "kv" for key-value based feature flags or "ff" for enhanced feature flags. :rtype: str :return: The allocation ID. """ @@ -356,13 +365,9 @@ def _generate_allocation_id(feature_flag_value: Dict[str, JSON], reference_path_ for v in sorted_variants: allocation_id += f"{base64.b64encode(v.get('name', '').encode()).decode()}," - # Key-value based feature flags store the variant value under "configuration_value". Enhanced - # feature flags store it under "value" instead. - if reference_path_segment == FEATURE_FLAG_KV_REFERENCE_SEGMENT: - value_key = "configuration_value" - else: - value_key = "value" - allocation_id += f"{json.dumps(v.get(value_key, ''), separators=(',', ':'), sort_keys=True)}" + allocation_id += ( + f"{json.dumps(v.get('configuration_value', ''), separators=(',', ':'), sort_keys=True)}" + ) allocation_id += ";" if sorted_variants: allocation_id = allocation_id[:-1] @@ -490,20 +495,20 @@ def _process_and_merge_feature_flags( feature_flags: Optional[List[FeatureFlagConfigurationSetting]], enhanced_feature_flags: Optional[List[FeatureFlag]] = None, ) -> Dict[str, Any]: - if feature_flags or enhanced_feature_flags: + if feature_flags or enhanced_feature_flags is not None: # Reset feature flag usage self._tracing_context.reset_feature_filter_usage() if feature_flags: self._processed_kv_feature_flags = [self._process_kv_feature_flag(ff) for ff in feature_flags] - if enhanced_feature_flags: + if enhanced_feature_flags is not None: self._processed_enhanced_feature_flags = [ self._process_enhanced_feature_flag(ff) for ff in enhanced_feature_flags ] self._tracing_context.uses_enhanced_feature_flags = bool(enhanced_feature_flags) - if feature_flags or enhanced_feature_flags: + if feature_flags or enhanced_feature_flags is not None: processed_feature_flags = self._merge_feature_flags( self._processed_kv_feature_flags, self._processed_enhanced_feature_flags ) @@ -534,9 +539,13 @@ def _merge_feature_flags( merged: Dict[str, Dict[str, Any]] = {} for ff in kv_feature_flags: identifier = ff.get(FEATURE_FLAG_ID_FIELD) + if identifier is None: + continue merged[identifier] = ff for ff in enhanced_feature_flags: identifier = ff.get(FEATURE_FLAG_ID_FIELD) + if identifier is None: + continue merged[identifier] = ff return list(merged.values()) @@ -554,7 +563,7 @@ def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str, """ Convert an enhanced feature flag, loaded from the enhanced feature flag endpoint, into a dictionary that matches the feature management library's schema. - Ref: https://github.com/microsoft/FeatureManagement/blob/main/Schema/FeatureFlag.v2.0.0.schema.json + Ref: https://github.com/microsoft/FeatureManagement/blob/main/Schema/FeatureFlag.v2.0.0.schema.json :param feature_flag: The enhanced feature flag. :type feature_flag: ~azure.appconfiguration.FeatureFlag @@ -588,7 +597,7 @@ def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str, feature_flag_value["variants"] = [ { "name": variant.name, - "value": variant.value, + "configuration_value": variant.value, "content_type": variant.content_type, "status_override": variant.status_override, } @@ -605,8 +614,8 @@ def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str, allocation_value["percentile"] = [ { "variant": percentile.variant, - "percentile_from": percentile.percentile_from, - "percentile_to": percentile.percentile_to, + "from": percentile.percentile_from, + "to": percentile.percentile_to, } for percentile in feature_flag.allocation.percentile ] 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 5c44b8717e2b..f368eff8c525 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -15,7 +15,7 @@ ALLOCATION_ID_KEY = "AllocationId" ETAG_KEY = "ETag" -# Identifier field required by the feature management library's schema for every feature flag entry. +# Identifier field required by the feature management library's schema for every feature flag entry. FEATURE_FLAG_ID_FIELD = "id" # Path segment used to build the feature flag reference URL for feature flags loaded from the key-value store. FEATURE_FLAG_KV_REFERENCE_SEGMENT = "kv" 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 4f215d5e3619..2e578dbfa1a6 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 @@ -347,7 +347,7 @@ async def check_enhanced_feature_flag_etags( self, feature_flag_selectors: List[FeatureFlagSelector], page_etags: List[List[str]], **kwargs ) -> bool: """ - Checks if any enhanced feature flag page has changed using page etags. + Checks if any enhanced feature flag page has changed using page etags. :param feature_flag_selectors: List of feature flag selectors for feature flags :type feature_flag_selectors: List[FeatureFlagSelector] 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..4a857b9986df 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 @@ -13,12 +13,13 @@ overload, List, Tuple, + Union, ) from azure.core.credentials_async import AsyncTokenCredential from .._constants import ( DEFAULT_STARTUP_TIMEOUT, ) -from .._models import AzureAppConfigurationKeyVaultOptions, SettingSelector +from .._models import AzureAppConfigurationKeyVaultOptions, FeatureFlagSelector, SettingSelector from .._utils import ( delay_failure, process_load_parameters, @@ -49,7 +50,7 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only on_refresh_success: Optional[Callable] = None, on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] = None, feature_flag_enabled: bool = False, - feature_flag_selectors: Optional[List[SettingSelector]] = None, + feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = None, feature_flag_refresh_enabled: bool = False, startup_timeout: int = DEFAULT_STARTUP_TIMEOUT, **kwargs, @@ -87,9 +88,11 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only :paramtype on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] :keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False. :paramtype feature_flag_enabled: bool - :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all - feature flags without a label. - :paramtype feature_flag_selectors: List[SettingSelector] + :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. Either a list of + ~azure.appconfiguration.provider.SettingSelector or a list of + ~azure.appconfiguration.provider.FeatureFlagSelector (the two types cannot be mixed in the same list). + By default will load all feature flags without a label. + :paramtype feature_flag_selectors: Union[List[SettingSelector], List[FeatureFlagSelector]] :keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is False. :paramtype feature_flag_refresh_enabled: bool @@ -124,7 +127,7 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only on_refresh_success: Optional[Callable] = None, on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] = None, feature_flag_enabled: bool = False, - feature_flag_selectors: Optional[List[SettingSelector]] = None, + feature_flag_selectors: Optional[Union[List[SettingSelector], List[FeatureFlagSelector]]] = None, feature_flag_refresh_enabled: bool = False, startup_timeout: int = DEFAULT_STARTUP_TIMEOUT, **kwargs, @@ -164,9 +167,11 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only :paramtype on_refresh_error: Optional[Callable[[Exception], Awaitable[None]]] :keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False. :paramtype feature_flag_enabled: bool - :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all - feature flags without a label. - :paramtype feature_flag_selectors: List[SettingSelector] + :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. Either a list of + ~azure.appconfiguration.provider.SettingSelector or a list of + ~azure.appconfiguration.provider.FeatureFlagSelector (the two types cannot be mixed in the same list). + By default will load all feature flags without a label. + :paramtype feature_flag_selectors: Union[List[SettingSelector], List[FeatureFlagSelector]] :keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is False. :paramtype feature_flag_refresh_enabled: bool 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 b06e5d536a80..a12289af0086 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 @@ -168,8 +168,7 @@ async def _attempt_refresh( # 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 await client.check_enhanced_feature_flag_etags( - self._enhanced_feature_flag_selectors, self._enhanced_feature_flag_etags, headers=headers, - **kwargs + self._enhanced_feature_flag_selectors, self._enhanced_feature_flag_etags, headers=headers, **kwargs ): enhanced_feature_flags, enhanced_feature_flag_etags = await client.load_enhanced_feature_flags( self._enhanced_feature_flag_selectors, headers=headers, **kwargs diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/setup.py b/sdk/appconfiguration/azure-appconfiguration-provider/setup.py index 495014bbd097..8d01796d2530 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/setup.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/setup.py @@ -61,16 +61,13 @@ "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", ], zip_safe=False, packages=find_packages(exclude=exclude_packages), - python_requires=">=3.6", + python_requires=">=3.10", install_requires=[ "azure-core>=1.31.0", "azure-appconfiguration>=1.10.0b1", diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 476d29b2e155..551257e60ac4 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -34,7 +34,6 @@ METADATA_KEY, ETAG_KEY, FEATURE_FLAG_REFERENCE_KEY, - FEATURE_FLAG_KV_REFERENCE_SEGMENT, ) from azure.appconfiguration.provider._refresh_timer import _RefreshTimer @@ -229,6 +228,23 @@ def test_enhanced_feature_flag_selectors_excludes_snapshot_selectors(self): self.assertEqual(provider._enhanced_feature_flag_selectors[0].name_filter, key_select.key_filter) self.assertEqual(provider._enhanced_feature_flag_selectors[0].label_filter, key_select.label_filter) + def test_feature_flag_selectors_none_defaults_to_all_unlabeled_flags(self): + provider = AzureAppConfigurationProviderBase(endpoint="https://test.azconfig.io") + + self.assertEqual(len(provider._feature_flag_selectors), 1) + self.assertEqual(provider._feature_flag_selectors[0].key_filter, "*") + self.assertEqual(len(provider._enhanced_feature_flag_selectors), 1) + self.assertEqual(provider._enhanced_feature_flag_selectors[0].name_filter, "*") + + def test_feature_flag_selectors_explicit_empty_list_loads_none(self): + provider = AzureAppConfigurationProviderBase( + endpoint="https://test.azconfig.io", + feature_flag_selectors=[], + ) + + self.assertEqual(provider._feature_flag_selectors, []) + self.assertEqual(provider._enhanced_feature_flag_selectors, []) + def test_process_key_name_with_no_prefix(self): """Test key name processing with no matching prefix.""" config = Mock() @@ -374,9 +390,7 @@ def test_update_ff_telemetry_metadata_max_variants(self): def test_generate_allocation_id_no_allocation(self): """Test allocation ID generation with no allocation.""" feature_flag_value: Dict[str, Any] = {"no_allocation": "here"} - result = AzureAppConfigurationProviderBase._generate_allocation_id( - feature_flag_value, FEATURE_FLAG_KV_REFERENCE_SEGMENT - ) + result = AzureAppConfigurationProviderBase._generate_allocation_id(feature_flag_value) self.assertIsNone(result) def test_generate_allocation_id_with_allocation(self): @@ -393,9 +407,7 @@ def test_generate_allocation_id_with_allocation(self): ], } - result = AzureAppConfigurationProviderBase._generate_allocation_id( - feature_flag_value, FEATURE_FLAG_KV_REFERENCE_SEGMENT - ) + result = AzureAppConfigurationProviderBase._generate_allocation_id(feature_flag_value) self.assertIsNotNone(result) self.assertIsInstance(result, str) # Should be a base64 encoded string @@ -414,9 +426,7 @@ def test_generate_allocation_id_no_variants_no_seed(self): "default_when_enabled": "Control" } } - result = AzureAppConfigurationProviderBase._generate_allocation_id( - feature_flag_value, FEATURE_FLAG_KV_REFERENCE_SEGMENT - ) + result = AzureAppConfigurationProviderBase._generate_allocation_id(feature_flag_value) # Since default_when_enabled is provided, allocated_variants won't be empty # so this should return a valid allocation ID self.assertIsNotNone(result) @@ -429,9 +439,7 @@ def test_generate_allocation_id_truly_empty(self): # No seed and no default_when_enabled } } - result = AzureAppConfigurationProviderBase._generate_allocation_id( - feature_flag_value, FEATURE_FLAG_KV_REFERENCE_SEGMENT - ) + result = AzureAppConfigurationProviderBase._generate_allocation_id(feature_flag_value) # This should return None because allocated_variants is empty and no seed self.assertIsNone(result) @@ -528,13 +536,13 @@ def test_process_enhanced_feature_flag_with_variants_and_allocation(self): self.assertEqual(len(result["variants"]), 2) self.assertEqual(result["variants"][0]["name"], "Control") - self.assertEqual(result["variants"][0]["value"], {"key": "control_value"}) + self.assertEqual(result["variants"][0]["configuration_value"], {"key": "control_value"}) self.assertEqual(result["variants"][1]["content_type"], "application/json") allocation = result["allocation"] self.assertEqual(allocation["default_when_disabled"], "Control") self.assertEqual(allocation["default_when_enabled"], "Test") - self.assertEqual(allocation["percentile"], [{"variant": "Control", "percentile_from": 0, "percentile_to": 50}]) + self.assertEqual(allocation["percentile"], [{"variant": "Control", "from": 0, "to": 50}]) self.assertEqual(allocation["user"], [{"variant": "Test", "users": ["user1"]}]) self.assertEqual(allocation["group"], [{"variant": "Test", "groups": ["group1"]}]) self.assertEqual(allocation["seed"], "1234") @@ -646,5 +654,31 @@ def test_merge_only_enhanced_flags(self): self.assertEqual(len(merged), 2) +class TestProcessAndMergeFeatureFlags(unittest.TestCase): + """Test _process_and_merge_feature_flags distinguishes None (not loaded this round) from an explicitly + empty list (loaded this round, zero found).""" + + def setUp(self): + self.provider = AzureAppConfigurationProviderBase(endpoint="https://test.azconfig.io") + + def test_enhanced_feature_flags_none_preserves_previous_processed_flags(self): + feature_flag = FeatureFlag(name="MyFeature", enabled=True) + self.provider._process_and_merge_feature_flags({}, [], None, [feature_flag]) + self.assertEqual(len(self.provider._processed_enhanced_feature_flags), 1) + + # Passing None again should leave the previously processed enhanced feature flags untouched. + self.provider._process_and_merge_feature_flags({}, [], None, None) + self.assertEqual(len(self.provider._processed_enhanced_feature_flags), 1) + + def test_enhanced_feature_flags_explicit_empty_list_clears_previous_processed_flags(self): + feature_flag = FeatureFlag(name="MyFeature", enabled=True) + self.provider._process_and_merge_feature_flags({}, [], None, [feature_flag]) + self.assertEqual(len(self.provider._processed_enhanced_feature_flags), 1) + + # An explicitly empty list means the endpoint was queried and returned zero feature flags. + self.provider._process_and_merge_feature_flags({}, [], None, []) + self.assertEqual(self.provider._processed_enhanced_feature_flags, []) + + if __name__ == "__main__": unittest.main() 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 0ff004b198b4..3393a2866463 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 @@ -601,4 +601,3 @@ def test_correlation_context_with_enhanced_feature_flags_and_snapshot_reference( correlation_header = updated_headers.get("Correlation-Context", "") self.assertIn(SNAPSHOT_REFERENCE_TAG, correlation_header) self.assertIn(ENHANCED_FEATURE_FLAG_TAG, correlation_header) - diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/tests/tests.md similarity index 76% rename from sdk/appconfiguration/azure-appconfiguration-provider/tests/README.md rename to sdk/appconfiguration/azure-appconfiguration-provider/tests/tests.md index 85f257fc1fce..af064b22e021 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/README.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/tests.md @@ -48,3 +48,25 @@ Notes: * Authentication for Entra ID-based tests relies on your local Azure CLI login (`az login`); make sure you're signed in to the subscription that contains your App Configuration store. * Add `AZURE_SKIP_LIVE_RECORDING=true` if you want to run tests live against the real store without generating/overwriting recording files (useful for a quick sanity check). * Omit `AZURE_TEST_RUN_LIVE` (or set it to `false`) to run the same tests in playback mode against existing recordings — this does not require any of the App Configuration environment variables above. + +## Pre-PR validation checks (sdist, mypy, pylint, black, snippets) + +Make sure all unit tests and live tests passed, test recodings updated, all tests against recordings also passed. + +Before opening or updating a PR, run the same static/build checks that CI enforces, using the `azpysdk` entrypoint from `eng/tools/azure-sdk-tools`. See [doc/tool_usage_guide.md](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/tool_usage_guide.md) for the full list of available checks and options (e.g. `--isolate`). + +From this package's directory (`sdk/appconfiguration/azure-appconfiguration-provider`): + +```bash +azpysdk sdist . # builds the sdist and runs the full test suite against it +azpysdk mypy . # static type checking +azpysdk pylint . # lint checks +azpysdk black . # formatting check (auto-reformats files in place) +azpysdk update_snippet . # regenerates README code snippets from sample files +``` + +Notes: + +* Run `black` again after making any other fixes, since it may reformat files you just edited. +* Run `update_snippet` after changing any `samples/*.py` file, then diff to confirm the regenerated snippets in `README.md` match what you expect. +* See [doc/dev/pylint_checking.md](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/pylint_checking.md) and [doc/dev/static_type_checking_cheat_sheet.md](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/static_type_checking_cheat_sheet.md) for guidance on fixing pylint/mypy issues. From a621f9ef796c980b22e5f2cd36c48c0f503fdd9e Mon Sep 17 00:00:00 2001 From: Yuan Qu Date: Wed, 19 Aug 2026 11:55:26 -0700 Subject: [PATCH 24/25] Reload both feature flag sources on change and validate JSON variant values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../_azureappconfigurationprovider.py | 26 ++-- .../_azureappconfigurationproviderbase.py | 50 +++++-- .../test_azureappconfigurationproviderbase.py | 140 +++++++++++++++++- ...test_provider_feature_flag_refresh_unit.py | 131 ++++++++++++++++ 4 files changed, 320 insertions(+), 27 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_refresh_unit.py 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 d6c01d6ddad8..58e9ee235882 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -144,19 +144,23 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f if self._feature_flag_refresh_enabled and self._feature_flag_refresh_timer.needs_refresh(): feature_flag_refresh_attempted = True - if not self._feature_flag_page_etags or client.check_feature_flag_page_etags( + # Key-value based feature flags and enhanced feature flags are separate resource types with their + # own change-detection state, but if either indicates a change, both are reloaded and re-merged + # together to guarantee the merged result is always correct and internally consistent. + feature_flags_changed = not self._feature_flag_page_etags or client.check_feature_flag_page_etags( self._feature_flag_selectors, self._feature_flag_page_etags, headers=headers, **kwargs - ): + ) + enhanced_feature_flags_changed = ( + 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 + ) + ) + + if feature_flags_changed or enhanced_feature_flags_changed: feature_flags, feature_flag_page_etags = client.load_feature_flags( 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 ) @@ -187,7 +191,9 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f 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 or enhanced_feature_flags) and self._on_refresh_success: + if ( + settings_refreshed or feature_flags is not None or enhanced_feature_flags is not None + ) and self._on_refresh_success: self._on_refresh_success() except AzureError as e: logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint) 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 c42c252b2739..3f32e14a5648 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -495,20 +495,24 @@ def _process_and_merge_feature_flags( feature_flags: Optional[List[FeatureFlagConfigurationSetting]], enhanced_feature_flags: Optional[List[FeatureFlag]] = None, ) -> Dict[str, Any]: - if feature_flags or enhanced_feature_flags is not None: + if feature_flags is not None or enhanced_feature_flags is not None: # Reset feature flag usage self._tracing_context.reset_feature_filter_usage() - if feature_flags: + if feature_flags is not None: + # Only overwrite the cached key-value feature flags when a refresh actually occurred. This preserves + # the previous state (including an intentional empty list) when this source wasn't refreshed. self._processed_kv_feature_flags = [self._process_kv_feature_flag(ff) for ff in feature_flags] if enhanced_feature_flags is not None: + # Only overwrite the cached enhanced feature flags when a refresh actually occurred, so the previous + # state is carried over when this source wasn't refreshed. self._processed_enhanced_feature_flags = [ self._process_enhanced_feature_flag(ff) for ff in enhanced_feature_flags ] self._tracing_context.uses_enhanced_feature_flags = bool(enhanced_feature_flags) - if feature_flags or enhanced_feature_flags is not None: + if feature_flags is not None or enhanced_feature_flags is not None: processed_feature_flags = self._merge_feature_flags( self._processed_kv_feature_flags, self._processed_enhanced_feature_flags ) @@ -559,6 +563,25 @@ def _process_kv_feature_flag(self, feature_flag: FeatureFlagConfigurationSetting # Feature flag value is not a valid JSON return {} + @staticmethod + def _parse_variant_value(value: Optional[str], content_type: Optional[str]) -> Any: + """ + Parses an enhanced feature flag variant's raw string value, similar to how a regular key-value setting's + value is processed. If the variant's content type indicates JSON, the value is parsed and returned as a + JSON object; otherwise the raw string value is returned unchanged. + + :param value: The variant's raw value, as returned by the enhanced feature flag endpoint. + :type value: Optional[str] + :param content_type: The variant's content type. + :type content_type: Optional[str] + :return: The parsed JSON object if the content type is JSON, otherwise the original raw value. + :rtype: Any + :raises json.JSONDecodeError: If the content type indicates JSON but the value is not valid JSON. + """ + if not isinstance(value, str) or not is_json_content_type(content_type or ""): + return value + return json.loads(value) + def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str, Any]: """ Convert an enhanced feature flag, loaded from the enhanced feature flag endpoint, into a dictionary that @@ -594,15 +617,18 @@ def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str, feature_flag_value["conditions"] = conditions_value if feature_flag.variants: - feature_flag_value["variants"] = [ - { - "name": variant.name, - "configuration_value": variant.value, - "content_type": variant.content_type, - "status_override": variant.status_override, - } - for variant in feature_flag.variants - ] + try: + feature_flag_value["variants"] = [ + { + "name": variant.name, + "configuration_value": self._parse_variant_value(variant.value, variant.content_type), + "content_type": variant.content_type, + "status_override": variant.status_override, + } + for variant in feature_flag.variants + ] + except json.JSONDecodeError as e: + raise ValueError(f"Enhanced feature flag '{feature_flag.name}' has an invalid variant value.") from e if feature_flag.allocation: allocation_value: Dict[str, Any] = {} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 551257e60ac4..2e23fe223f2f 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -34,6 +34,8 @@ METADATA_KEY, ETAG_KEY, FEATURE_FLAG_REFERENCE_KEY, + FEATURE_MANAGEMENT_KEY, + FEATURE_FLAG_KEY, ) from azure.appconfiguration.provider._refresh_timer import _RefreshTimer @@ -514,13 +516,17 @@ def test_process_enhanced_feature_flag_with_conditions(self): self.assertEqual(result["conditions"]["client_filters"][0]["parameters"], {"Value": "50"}) def test_process_enhanced_feature_flag_with_variants_and_allocation(self): - """Test processing an enhanced feature flag with variants and allocation.""" + """Test processing an enhanced feature flag with variants and allocation. Variant values, like regular + key-value settings, are raw strings on the wire: a variant with a JSON content type is parsed into a JSON + object, while a variant with no (or non-JSON) content type is kept as the raw string.""" feature_flag = FeatureFlag( name="MyFeature", enabled=True, variants=[ - FeatureFlagVariantDefinition(name="Control", value={"key": "control_value"}), - FeatureFlagVariantDefinition(name="Test", value={"key": "test_value"}, content_type="application/json"), + FeatureFlagVariantDefinition(name="Control", value="control_value"), + FeatureFlagVariantDefinition( + name="Test", value='{"key": "test_value"}', content_type="application/json" + ), ], allocation=FeatureFlagAllocation( default_when_disabled="Control", @@ -536,8 +542,11 @@ def test_process_enhanced_feature_flag_with_variants_and_allocation(self): self.assertEqual(len(result["variants"]), 2) self.assertEqual(result["variants"][0]["name"], "Control") - self.assertEqual(result["variants"][0]["configuration_value"], {"key": "control_value"}) + # No content type: falls back to the raw string value, unparsed. + self.assertEqual(result["variants"][0]["configuration_value"], "control_value") self.assertEqual(result["variants"][1]["content_type"], "application/json") + # JSON content type: the raw string value is parsed into a JSON object. + self.assertEqual(result["variants"][1]["configuration_value"], {"key": "test_value"}) allocation = result["allocation"] self.assertEqual(allocation["default_when_disabled"], "Control") @@ -547,6 +556,26 @@ def test_process_enhanced_feature_flag_with_variants_and_allocation(self): self.assertEqual(allocation["group"], [{"variant": "Test", "groups": ["group1"]}]) self.assertEqual(allocation["seed"], "1234") + def test_process_enhanced_feature_flag_invalid_variant_json_raises_value_error(self): + """If a variant's content type claims JSON but its value is invalid JSON, processing the enhanced feature + flag should raise a clear ValueError identifying the offending flag, chained from the underlying + JSONDecodeError.""" + feature_flag = FeatureFlag( + name="InvalidVariant", + enabled=True, + variants=[ + FeatureFlagVariantDefinition( + name="Variant", value="{ invalid json", content_type="application/json" + ) + ], + ) + + with self.assertRaises(ValueError) as context: + self.provider._process_enhanced_feature_flag(feature_flag) + + self.assertIn("InvalidVariant", str(context.exception)) + self.assertIsInstance(context.exception.__cause__, json.JSONDecodeError) + def test_process_enhanced_feature_flag_with_telemetry_and_tags(self): """Test processing an enhanced feature flag with telemetry settings and tags.""" feature_flag = FeatureFlag( @@ -584,6 +613,57 @@ def test_process_enhanced_feature_flag_updates_telemetry_metadata(self): self.assertIn("?label=prod", metadata[FEATURE_FLAG_REFERENCE_KEY]) +class TestParseVariantValue(unittest.TestCase): + """Test the _parse_variant_value static method, used to interpret an enhanced feature flag variant's raw + string value based on its content type, mirroring how regular key-value settings are processed.""" + + def test_json_content_type_parses_object(self): + result = AzureAppConfigurationProviderBase._parse_variant_value('{"key": "value"}', "application/json") + self.assertEqual(result, {"key": "value"}) + + def test_json_content_type_parses_array(self): + result = AzureAppConfigurationProviderBase._parse_variant_value("[1, 2, 3]", "application/json") + self.assertEqual(result, [1, 2, 3]) + + def test_json_content_type_with_charset_parses_object(self): + result = AzureAppConfigurationProviderBase._parse_variant_value( + '{"key": "value"}', "application/json; charset=utf-8" + ) + self.assertEqual(result, {"key": "value"}) + + def test_json_content_type_with_structured_suffix_parses_object(self): + """Content types using the '+json' structured syntax suffix (e.g. a vendor-specific media type) are + also treated as JSON.""" + result = AzureAppConfigurationProviderBase._parse_variant_value( + '{"key": "value"}', "application/vnd.microsoft.appconfig.ff+json" + ) + self.assertEqual(result, {"key": "value"}) + + def test_no_content_type_falls_back_to_raw_string(self): + result = AzureAppConfigurationProviderBase._parse_variant_value("plain_value", None) + self.assertEqual(result, "plain_value") + + def test_non_json_content_type_falls_back_to_raw_string(self): + result = AzureAppConfigurationProviderBase._parse_variant_value("plain_value", "text/plain") + self.assertEqual(result, "plain_value") + + def test_text_json_content_type_is_not_treated_as_json(self): + """'text/json' is not an 'application/*' JSON media type, so it should be treated as a raw string, even + though it contains the substring 'json'.""" + result = AzureAppConfigurationProviderBase._parse_variant_value("{ invalid json", "text/json") + self.assertEqual(result, "{ invalid json") + + def test_json_content_type_with_invalid_json_raises(self): + """If the content type claims JSON but the value is not valid JSON, parsing should raise instead of + silently falling back to the raw string.""" + with self.assertRaises(json.JSONDecodeError): + AzureAppConfigurationProviderBase._parse_variant_value("not valid json", "application/json") + + def test_none_value_returns_none(self): + result = AzureAppConfigurationProviderBase._parse_variant_value(None, "application/json") + self.assertIsNone(result) + + class TestUpdateEnhancedFeatureFlagTelemetryMetadata(unittest.TestCase): """Test the _update_enhanced_feature_flag_telemetry_metadata method.""" @@ -659,7 +739,7 @@ class TestProcessAndMergeFeatureFlags(unittest.TestCase): empty list (loaded this round, zero found).""" def setUp(self): - self.provider = AzureAppConfigurationProviderBase(endpoint="https://test.azconfig.io") + self.provider = AzureAppConfigurationProviderBase(endpoint="https://test.azconfig.io", feature_flag_enabled=True) def test_enhanced_feature_flags_none_preserves_previous_processed_flags(self): feature_flag = FeatureFlag(name="MyFeature", enabled=True) @@ -679,6 +759,56 @@ def test_enhanced_feature_flags_explicit_empty_list_clears_previous_processed_fl self.provider._process_and_merge_feature_flags({}, [], None, []) self.assertEqual(self.provider._processed_enhanced_feature_flags, []) + def test_kv_feature_flags_none_preserves_previous_processed_flags(self): + """Same as the enhanced-flag case above, but for key-value based feature flags: passing None (not + refreshed this round) must not clear or overwrite the previously cached key-value feature flags.""" + kv_flag = FeatureFlagConfigurationSetting(feature_id="MyFeature", enabled=True, label=NULL_CHAR) + self.provider._process_and_merge_feature_flags({}, [], [kv_flag], None) + self.assertEqual(len(self.provider._processed_kv_feature_flags), 1) + + # Passing None again should leave the previously processed key-value feature flags untouched. + self.provider._process_and_merge_feature_flags({}, [], None, None) + self.assertEqual(len(self.provider._processed_kv_feature_flags), 1) + + def test_kv_feature_flags_explicit_empty_list_clears_previous_processed_flags(self): + """An explicitly empty list for key-value feature flags means the store was queried and returned zero + feature flags, so the previously cached key-value feature flags should be cleared.""" + kv_flag = FeatureFlagConfigurationSetting(feature_id="MyFeature", enabled=True, label=NULL_CHAR) + self.provider._process_and_merge_feature_flags({}, [], [kv_flag], None) + self.assertEqual(len(self.provider._processed_kv_feature_flags), 1) + + self.provider._process_and_merge_feature_flags({}, [], [], None) + self.assertEqual(self.provider._processed_kv_feature_flags, []) + + def test_both_none_preserves_merged_result_unchanged(self): + """When neither source was refreshed (both None), the merge step should be skipped entirely and the + previously merged/processed feature flag list should be returned untouched.""" + kv_flag = FeatureFlagConfigurationSetting(feature_id="KvFeature", enabled=True, label=NULL_CHAR) + enhanced_flag = FeatureFlag(name="EnhancedFeature", enabled=False) + + settings = self.provider._process_and_merge_feature_flags({}, [], [kv_flag], [enhanced_flag]) + previous_merged = settings[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] + self.assertEqual(len(previous_merged), 2) + + # Neither source refreshed this round: the previously merged list is passed through as + # processed_feature_flags and should come back unchanged. + settings = self.provider._process_and_merge_feature_flags( + {}, previous_merged, None, None + ) + self.assertEqual(settings[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY], previous_merged) + + def test_only_kv_refreshed_still_merges_with_cached_enhanced_flags(self): + """If only key-value feature flags are refreshed (enhanced is None), the merge should still combine the + newly refreshed kv flags with the previously cached enhanced feature flags.""" + enhanced_flag = FeatureFlag(name="EnhancedFeature", enabled=True) + self.provider._process_and_merge_feature_flags({}, [], [], [enhanced_flag]) + self.assertEqual(len(self.provider._processed_enhanced_feature_flags), 1) + + kv_flag = FeatureFlagConfigurationSetting(feature_id="KvFeature", enabled=True, label=NULL_CHAR) + settings = self.provider._process_and_merge_feature_flags({}, [], [kv_flag], None) + merged_ids = {ff["id"] for ff in settings[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY]} + self.assertEqual(merged_ids, {"KvFeature", "EnhancedFeature"}) + if __name__ == "__main__": unittest.main() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_refresh_unit.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_refresh_unit.py new file mode 100644 index 000000000000..a8b31873fd9b --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_flag_refresh_unit.py @@ -0,0 +1,131 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +import unittest +from unittest.mock import Mock + +from azure.appconfiguration.provider._azureappconfigurationprovider import AzureAppConfigurationProvider +from azure.appconfiguration.provider._azureappconfigurationproviderbase import AzureAppConfigurationProviderBase + + +def _make_provider() -> AzureAppConfigurationProvider: + """ + Builds an AzureAppConfigurationProvider instance for unit testing ``_attempt_refresh`` without creating any + real network clients. ``AzureAppConfigurationProviderBase.__init__`` is invoked directly (it does not create + any network resources), and the subclass-specific attributes normally set up by + ``AzureAppConfigurationProvider.__init__`` (which does create real clients) are stubbed out instead. + """ + provider = AzureAppConfigurationProvider.__new__(AzureAppConfigurationProvider) + AzureAppConfigurationProviderBase.__init__( + provider, + endpoint="https://test.azconfig.io", + feature_flag_enabled=True, + feature_flag_refresh_enabled=True, + refresh_enabled=False, + ) + provider._secret_provider = Mock(uses_key_vault=False) + provider._on_refresh_success = None + provider._on_refresh_error = None + provider._configuration_mapper = None + provider._replica_client_manager = Mock() + # Force the feature flag refresh timer to be due immediately. + provider._feature_flag_refresh_timer._next_refresh_time = 0 + return provider + + +def _make_client(kv_changed: bool, enhanced_changed: bool) -> Mock: + client = Mock() + client.endpoint = "https://test.azconfig.io" + client.check_feature_flag_page_etags.return_value = kv_changed + client.check_enhanced_feature_flag_etags.return_value = enhanced_changed + client.load_feature_flags.return_value = ([], [["kv_etag"]]) + client.load_enhanced_feature_flags.return_value = ([], [["enhanced_etag"]]) + return client + + +class TestAttemptRefreshReloadsBothFeatureFlagSources(unittest.TestCase): + """Test that ``_attempt_refresh`` always reloads and re-merges both the key-value based feature flags and + the enhanced feature flags together whenever either source's change-detection indicates a change, so the + merged result is always internally consistent.""" + + def test_only_kv_changed_reloads_both(self): + provider = _make_provider() + # Seed existing etag state so change-detection is actually exercised instead of short-circuiting on + # empty state. + provider._feature_flag_page_etags = [["old_kv_etag"]] + provider._enhanced_feature_flag_etags = [["old_enhanced_etag"]] + + client = _make_client(kv_changed=True, enhanced_changed=False) + + provider._attempt_refresh(client, replica_count=0, is_failover_request=False) + + client.load_feature_flags.assert_called_once() + client.load_enhanced_feature_flags.assert_called_once() + + def test_only_enhanced_changed_reloads_both(self): + provider = _make_provider() + provider._feature_flag_page_etags = [["old_kv_etag"]] + provider._enhanced_feature_flag_etags = [["old_enhanced_etag"]] + + client = _make_client(kv_changed=False, enhanced_changed=True) + + provider._attempt_refresh(client, replica_count=0, is_failover_request=False) + + client.load_feature_flags.assert_called_once() + client.load_enhanced_feature_flags.assert_called_once() + + def test_neither_changed_reloads_neither(self): + provider = _make_provider() + provider._feature_flag_page_etags = [["old_kv_etag"]] + provider._enhanced_feature_flag_etags = [["old_enhanced_etag"]] + + client = _make_client(kv_changed=False, enhanced_changed=False) + + provider._attempt_refresh(client, replica_count=0, is_failover_request=False) + + client.load_feature_flags.assert_not_called() + client.load_enhanced_feature_flags.assert_not_called() + + def test_both_changed_reloads_both(self): + provider = _make_provider() + provider._feature_flag_page_etags = [["old_kv_etag"]] + provider._enhanced_feature_flag_etags = [["old_enhanced_etag"]] + + client = _make_client(kv_changed=True, enhanced_changed=True) + + provider._attempt_refresh(client, replica_count=0, is_failover_request=False) + + client.load_feature_flags.assert_called_once() + client.load_enhanced_feature_flags.assert_called_once() + + def test_no_previous_etag_state_reloads_both_without_checking(self): + """When there is no previous etag state at all (first refresh attempt), both sources should be loaded + without needing to call the etag-check methods.""" + provider = _make_provider() + provider._feature_flag_page_etags = [] + provider._enhanced_feature_flag_etags = [] + + client = _make_client(kv_changed=False, enhanced_changed=False) + + provider._attempt_refresh(client, replica_count=0, is_failover_request=False) + + client.load_feature_flags.assert_called_once() + client.load_enhanced_feature_flags.assert_called_once() + + def test_reload_updates_etag_state_for_both_sources(self): + provider = _make_provider() + provider._feature_flag_page_etags = [["old_kv_etag"]] + provider._enhanced_feature_flag_etags = [["old_enhanced_etag"]] + + client = _make_client(kv_changed=True, enhanced_changed=False) + + provider._attempt_refresh(client, replica_count=0, is_failover_request=False) + + self.assertEqual(provider._feature_flag_page_etags, [["kv_etag"]]) + self.assertEqual(provider._enhanced_feature_flag_etags, [["enhanced_etag"]]) + + +if __name__ == "__main__": + unittest.main() From 85ef03cd13435edc1169e228d26ee21ecadffc81 Mon Sep 17 00:00:00 2001 From: Yuan Qu Date: Thu, 20 Aug 2026 00:02:49 -0700 Subject: [PATCH 25/25] Fix pre-PR static checks; add filter parameter JSON fallback-to-string parsing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure-appconfiguration-provider/README.md | 4 +- .../assets.json | 2 +- .../_azureappconfigurationprovider.py | 148 ++++++++++++------ .../_azureappconfigurationproviderbase.py | 40 ++++- .../_azureappconfigurationproviderasync.py | 145 ++++++++++++----- .../async_enhanced_feature_flag_sample.py | 4 +- .../samples/enhanced_feature_flag_sample.py | 4 +- .../test_azureappconfigurationproviderbase.py | 109 ++++++++++++- 8 files changed, 354 insertions(+), 102 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/README.md index 767ac1d2dba4..a0a727c2307e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/README.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/README.md @@ -390,7 +390,7 @@ from azure.appconfiguration.provider import load # 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") +enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("id") == "EnhancedFeatureBeta") print(enhanced_flag_beta["enabled"]) ``` @@ -435,7 +435,7 @@ config = load( **kwargs, ) feature_flags = config["feature_management"]["feature_flags"] -enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("name") == "EnhancedFeatureBeta") +enhanced_flag_beta = next(flag for flag in feature_flags if flag.get("id") == "EnhancedFeatureBeta") print(enhanced_flag_beta["enabled"]) ``` diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index 384fa2233f20..1dc68e92db9c 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_ad9137a384" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_b61dbba519" } 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 ec5b77e2fcec..e13a31bce6bb 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -96,6 +96,95 @@ 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 _refresh_configuration_settings( + self, client: ConfigurationClient, headers: Mapping[str, str], **kwargs + ) -> Tuple[List[ConfigurationSetting], List[List[str]], bool, bool, Mapping[Tuple[str, str], Optional[str]]]: + """ + Refreshes configuration settings (excluding feature flags) using a single client, if a refresh is due. + + :param client: The configuration client to attempt the refresh against. + :type client: ~azure.appconfiguration.provider.ConfigurationClient + :param headers: The correlation-context headers to include with the requests. + :type headers: Mapping[str, str] + :return: A tuple of (configuration_settings, page_etags, settings_refreshed, refresh_attempted, + updated_watched_settings). + :rtype: Tuple[List[ConfigurationSetting], List[List[str]], bool, bool, + Mapping[Tuple[str, str], Optional[str]]] + """ + configuration_settings: List[ConfigurationSetting] = [] + page_etags: List[List[str]] = [] + settings_refreshed = False + refresh_attempted = False + updated_watched_settings: Mapping[Tuple[str, str], Optional[str]] = {} + + if self._refresh_enabled and not self._watched_settings and self._refresh_timer.needs_refresh(): + refresh_attempted = True + + if client.check_page_etags(self._selects, self._page_etags, headers=headers, **kwargs): + configuration_settings, page_etags = client.load_configuration_settings( + self._selects, headers=headers, **kwargs + ) + settings_refreshed = True + + elif self._refresh_enabled and self._watched_settings and self._refresh_timer.needs_refresh(): + refresh_attempted = True + + updated_watched_settings = client.get_updated_watched_settings( + self._watched_settings, headers=headers, **kwargs + ) + + if len(updated_watched_settings) > 0: + configuration_settings, _ = client.load_configuration_settings(self._selects, headers=headers, **kwargs) + settings_refreshed = True + + return configuration_settings, page_etags, settings_refreshed, refresh_attempted, updated_watched_settings + + def _refresh_feature_flags( + self, client: ConfigurationClient, headers: Mapping[str, str], **kwargs + ) -> Tuple[ + Optional[List[FeatureFlagConfigurationSetting]], List[List[str]], Optional[List[FeatureFlag]], List[List[str]] + ]: + """ + Refreshes key-value based and enhanced feature flags using a single client, if a refresh is due. + + Key-value based feature flags and enhanced feature flags are separate resource types with their own + change-detection state, but if either indicates a change, both are reloaded and re-merged together to + guarantee the merged result is always correct and internally consistent. + + :param client: The configuration client to attempt the refresh against. + :type client: ~azure.appconfiguration.provider.ConfigurationClient + :param headers: The correlation-context headers to include with the requests. + :type headers: Mapping[str, str] + :return: A tuple of (feature_flags, feature_flag_page_etags, enhanced_feature_flags, + enhanced_feature_flag_etags). + :rtype: Tuple[Optional[List[FeatureFlagConfigurationSetting]], List[List[str]], + Optional[List[FeatureFlag]], List[List[str]]] + """ + feature_flags: Optional[List[FeatureFlagConfigurationSetting]] = None + feature_flag_page_etags: List[List[str]] = [] + enhanced_feature_flags: Optional[List[FeatureFlag]] = None + enhanced_feature_flag_etags: List[List[str]] = [] + + feature_flags_changed = not self._feature_flag_page_etags or client.check_feature_flag_page_etags( + self._feature_flag_selectors, self._feature_flag_page_etags, headers=headers, **kwargs + ) + enhanced_feature_flags_changed = ( + 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 + ) + ) + + if feature_flags_changed or enhanced_feature_flags_changed: + feature_flags, feature_flag_page_etags = client.load_feature_flags( + self._feature_flag_selectors, headers=headers, **kwargs + ) + enhanced_feature_flags, enhanced_feature_flag_etags = client.load_enhanced_feature_flags( + self._enhanced_feature_flag_selectors, headers=headers, **kwargs + ) + + return feature_flags, feature_flag_page_etags, enhanced_feature_flags, enhanced_feature_flag_etags + def _attempt_refresh( self, client: ConfigurationClient, replica_count: int, is_failover_request: bool, **kwargs ) -> None: @@ -109,7 +198,6 @@ def _attempt_refresh( :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", {}), "Watch", @@ -117,64 +205,32 @@ def _attempt_refresh( self._secret_provider.uses_key_vault, is_failover_request, ) - 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 feature_flag_refresh_attempted = False - updated_watched_settings: Mapping[Tuple[str, str], Optional[str]] = {} 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 - - if client.check_page_etags(self._selects, self._page_etags, headers=headers, **kwargs): - configuration_settings, page_etags = client.load_configuration_settings( - self._selects, headers=headers, **kwargs - ) - settings_refreshed = True - - elif self._refresh_enabled and self._watched_settings and self._refresh_timer.needs_refresh(): - configuration_refresh_attempted = True - - updated_watched_settings = client.get_updated_watched_settings( - self._watched_settings, headers=headers, **kwargs - ) - - if len(updated_watched_settings) > 0: - configuration_settings, _ = client.load_configuration_settings( - self._selects, headers=headers, **kwargs - ) - settings_refreshed = True + ( + configuration_settings, + page_etags, + settings_refreshed, + configuration_refresh_attempted, + updated_watched_settings, + ) = self._refresh_configuration_settings(client, headers, **kwargs) if self._feature_flag_refresh_enabled and self._feature_flag_refresh_timer.needs_refresh(): feature_flag_refresh_attempted = True - # Key-value based feature flags and enhanced feature flags are separate resource types with their - # own change-detection state, but if either indicates a change, both are reloaded and re-merged - # together to guarantee the merged result is always correct and internally consistent. - feature_flags_changed = not self._feature_flag_page_etags or client.check_feature_flag_page_etags( - self._feature_flag_selectors, self._feature_flag_page_etags, headers=headers, **kwargs - ) - enhanced_feature_flags_changed = ( - 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 - ) - ) - - if feature_flags_changed or enhanced_feature_flags_changed: - feature_flags, feature_flag_page_etags = client.load_feature_flags( - self._feature_flag_selectors, headers=headers, **kwargs - ) - enhanced_feature_flags, enhanced_feature_flag_etags = client.load_enhanced_feature_flags( - self._enhanced_feature_flag_selectors, headers=headers, **kwargs - ) + ( + feature_flags, + feature_flag_page_etags, + enhanced_feature_flags, + enhanced_feature_flag_etags, + ) = self._refresh_feature_flags(client, headers, **kwargs) # Default to existing settings if no refresh occurred processed_settings = self._dict 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 67a705b85182..449943a3a4fb 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -584,6 +584,34 @@ def _parse_variant_value(value: Optional[str], content_type: Optional[str]) -> A return value return json.loads(value) + @staticmethod + def _parse_filter_parameter_value(value: Optional[str]) -> Any: + """ + Parses a single enhanced feature flag filter parameter value as a best-effort attempt. Unlike variant + values, filter parameters don't have a content type, so there's no explicit signal that a value is + intended to be JSON. Only strings that look like a JSON object or array are attempted to be parsed; + if parsing fails, or the value doesn't look like an object/array, the original raw string is returned + unchanged. This avoids misinterpreting a customer's intentional string value (e.g. "true", "123", or a + plain string) as some other JSON type. + + :param value: The filter parameter's raw string value. + :type value: Optional[str] + :return: The parsed JSON object/array if the value looks like JSON and parses successfully, otherwise + the original raw value. + :rtype: Any + """ + if not isinstance(value, str): + return value + trimmed = value.strip() + if not trimmed or trimmed[0] not in "{[": + return value + try: + return json.loads(value) + except json.JSONDecodeError: + # Not valid JSON after all: fall back to the original string, since the customer may have + # actually intended a literal string value. + return value + def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str, Any]: """ Convert an enhanced feature flag, loaded from the enhanced feature flag endpoint, into a dictionary that @@ -611,7 +639,17 @@ def _process_enhanced_feature_flag(self, feature_flag: FeatureFlag) -> Dict[str, conditions_value["requirement_type"] = feature_flag.conditions.requirement_type if feature_flag.conditions.filters: conditions_value["client_filters"] = [ - {"name": client_filter.name, "parameters": client_filter.parameters} + { + "name": client_filter.name, + "parameters": ( + { + key: self._parse_filter_parameter_value(value) + for key, value in client_filter.parameters.items() + } + if client_filter.parameters + else client_filter.parameters + ), + } for client_filter in feature_flag.conditions.filters ] filter_names = [client_filter.name for client_filter in feature_flag.conditions.filters] 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 b26e4083379d..5871f5da539d 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 @@ -107,6 +107,97 @@ def __init__(self, **kwargs: Any) -> None: "configuration_mapper", None ) + async def _refresh_configuration_settings( + self, client: ConfigurationClient, headers: Mapping[str, str], **kwargs + ) -> Tuple[List[ConfigurationSetting], List[List[str]], bool, bool, Mapping[Tuple[str, str], Optional[str]]]: + """ + Refreshes configuration settings (excluding feature flags) using a single client, if a refresh is due. + + :param client: The configuration client to attempt the refresh against. + :type client: ~azure.appconfiguration.provider.aio.ConfigurationClient + :param headers: The correlation-context headers to include with the requests. + :type headers: Mapping[str, str] + :return: A tuple of (configuration_settings, page_etags, settings_refreshed, refresh_attempted, + updated_watched_settings). + :rtype: Tuple[List[ConfigurationSetting], List[List[str]], bool, bool, + Mapping[Tuple[str, str], Optional[str]]] + """ + configuration_settings: List[ConfigurationSetting] = [] + page_etags: List[List[str]] = [] + settings_refreshed = False + refresh_attempted = False + updated_watched_settings: Mapping[Tuple[str, str], Optional[str]] = {} + + if self._refresh_enabled and not self._watched_settings and self._refresh_timer.needs_refresh(): + refresh_attempted = True + + if await client.check_page_etags(self._selects, self._page_etags, headers=headers, **kwargs): + configuration_settings, page_etags = await client.load_configuration_settings( + self._selects, headers=headers, **kwargs + ) + settings_refreshed = True + + elif self._refresh_enabled and self._watched_settings and self._refresh_timer.needs_refresh(): + refresh_attempted = True + + updated_watched_settings = await client.get_updated_watched_settings( + self._watched_settings, headers=headers, **kwargs + ) + + if len(updated_watched_settings) > 0: + configuration_settings, _ = await client.load_configuration_settings( + self._selects, headers=headers, **kwargs + ) + settings_refreshed = True + + return configuration_settings, page_etags, settings_refreshed, refresh_attempted, updated_watched_settings + + async def _refresh_feature_flags( + self, client: ConfigurationClient, headers: Mapping[str, str], **kwargs + ) -> Tuple[ + Optional[List[FeatureFlagConfigurationSetting]], List[List[str]], Optional[List[FeatureFlag]], List[List[str]] + ]: + """ + Refreshes key-value based and enhanced feature flags using a single client, if a refresh is due. + + Key-value based feature flags and enhanced feature flags are separate resource types with their own + change-detection state, but if either indicates a change, both are reloaded and re-merged together to + guarantee the merged result is always correct and internally consistent. + + :param client: The configuration client to attempt the refresh against. + :type client: ~azure.appconfiguration.provider.aio.ConfigurationClient + :param headers: The correlation-context headers to include with the requests. + :type headers: Mapping[str, str] + :return: A tuple of (feature_flags, feature_flag_page_etags, enhanced_feature_flags, + enhanced_feature_flag_etags). + :rtype: Tuple[Optional[List[FeatureFlagConfigurationSetting]], List[List[str]], + Optional[List[FeatureFlag]], List[List[str]]] + """ + feature_flags: Optional[List[FeatureFlagConfigurationSetting]] = None + feature_flag_page_etags: List[List[str]] = [] + enhanced_feature_flags: Optional[List[FeatureFlag]] = None + enhanced_feature_flag_etags: List[List[str]] = [] + + feature_flags_changed = not self._feature_flag_page_etags or await client.check_feature_flag_page_etags( + self._feature_flag_selectors, self._feature_flag_page_etags, headers=headers, **kwargs + ) + enhanced_feature_flags_changed = ( + not self._enhanced_feature_flag_etags + or await client.check_enhanced_feature_flag_etags( + self._enhanced_feature_flag_selectors, self._enhanced_feature_flag_etags, headers=headers, **kwargs + ) + ) + + if feature_flags_changed or enhanced_feature_flags_changed: + feature_flags, feature_flag_page_etags = await client.load_feature_flags( + self._feature_flag_selectors, headers=headers, **kwargs + ) + enhanced_feature_flags, enhanced_feature_flag_etags = await client.load_enhanced_feature_flags( + self._enhanced_feature_flag_selectors, headers=headers, **kwargs + ) + + return feature_flags, feature_flag_page_etags, enhanced_feature_flags, enhanced_feature_flag_etags + async def _attempt_refresh( self, client: ConfigurationClient, replica_count: int, is_failover_request: bool, **kwargs ): @@ -120,7 +211,6 @@ async def _attempt_refresh( :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", {}), "Watch", @@ -128,60 +218,33 @@ async def _attempt_refresh( self._secret_provider.uses_key_vault, is_failover_request, ) - 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 feature_flag_refresh_attempted = False - updated_watched_settings: Mapping[Tuple[str, str], Optional[str]] = {} 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 - - if await client.check_page_etags(self._selects, self._page_etags, headers=headers, **kwargs): - configuration_settings, page_etags = await client.load_configuration_settings( - self._selects, headers=headers, **kwargs - ) - settings_refreshed = True - - elif self._refresh_enabled and self._watched_settings and self._refresh_timer.needs_refresh(): - configuration_refresh_attempted = True - - updated_watched_settings = await client.get_updated_watched_settings( - self._watched_settings, headers=headers, **kwargs - ) - - if len(updated_watched_settings) > 0: - configuration_settings, _ = await client.load_configuration_settings( - self._selects, headers=headers, **kwargs - ) - settings_refreshed = True + ( + configuration_settings, + page_etags, + settings_refreshed, + configuration_refresh_attempted, + updated_watched_settings, + ) = await self._refresh_configuration_settings(client, headers, **kwargs) if self._feature_flag_refresh_enabled and self._feature_flag_refresh_timer.needs_refresh(): feature_flag_refresh_attempted = True - if not self._feature_flag_page_etags or await client.check_feature_flag_page_etags( - self._feature_flag_selectors, self._feature_flag_page_etags, headers=headers, **kwargs - ): - feature_flags, feature_flag_page_etags = await client.load_feature_flags( - self._feature_flag_selectors, headers=headers, **kwargs - ) + ( + feature_flags, + feature_flag_page_etags, + enhanced_feature_flags, + enhanced_feature_flag_etags, + ) = await self._refresh_feature_flags(client, 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 await 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 = await 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 diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py index d4dd51215445..7ae2fbcf758c 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/async_enhanced_feature_flag_sample.py @@ -18,8 +18,8 @@ import os import asyncio from sample_utilities import get_authority, get_credential, get_client_modifications -from azure.appconfiguration.aio import FeatureFlagClient -from azure.appconfiguration import FeatureFlag +from azure.appconfiguration.aio import FeatureFlagClient # type:ignore +from azure.appconfiguration import FeatureFlag # type:ignore from azure.appconfiguration.provider.aio import load from azure.appconfiguration.provider import SettingSelector diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py index 58bd72e141df..a1aeeedc6e03 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/enhanced_feature_flag_sample.py @@ -17,10 +17,10 @@ """ import os from sample_utilities import get_authority, get_credential, get_client_modifications -from azure.appconfiguration import FeatureFlag, FeatureFlagClient +from azure.appconfiguration import FeatureFlag, FeatureFlagClient # type:ignore from azure.appconfiguration.provider import load, SettingSelector -endpoint = os.environ.get("APPCONFIGURATION_ENDPOINT_STRING") +endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"] authority = get_authority(endpoint) credential = get_credential(authority) kwargs = get_client_modifications() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 828fd34ffc62..2d7eceb7244b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -520,6 +520,63 @@ def test_process_enhanced_feature_flag_with_conditions(self): self.assertEqual(result["conditions"]["client_filters"][0]["name"], "Percentage") self.assertEqual(result["conditions"]["client_filters"][0]["parameters"], {"Value": "50"}) + def test_process_enhanced_feature_flag_filter_parameter_json_object_is_parsed(self): + """Test that a filter parameter value that looks like a JSON object is parsed as JSON.""" + feature_flag = FeatureFlag( + name="MyFeature", + enabled=True, + conditions=FeatureFlagConditions( + filters=[FeatureFilter(name="Audience", parameters={"Users": '{"a": 1}'})], + ), + ) + + result = self.provider._process_enhanced_feature_flag(feature_flag) + + self.assertEqual(result["conditions"]["client_filters"][0]["parameters"], {"Users": {"a": 1}}) + + def test_process_enhanced_feature_flag_filter_parameter_json_array_is_parsed(self): + """Test that a filter parameter value that looks like a JSON array is parsed as JSON.""" + feature_flag = FeatureFlag( + name="MyFeature", + enabled=True, + conditions=FeatureFlagConditions( + filters=[FeatureFilter(name="Audience", parameters={"Groups": "[1, 2, 3]"})], + ), + ) + + result = self.provider._process_enhanced_feature_flag(feature_flag) + + self.assertEqual(result["conditions"]["client_filters"][0]["parameters"], {"Groups": [1, 2, 3]}) + + def test_process_enhanced_feature_flag_filter_parameter_invalid_json_falls_back_to_string(self): + """Test that a filter parameter value that looks like JSON but fails to parse falls back to the raw + string, rather than raising, since the customer may have intended a literal string value.""" + feature_flag = FeatureFlag( + name="MyFeature", + enabled=True, + conditions=FeatureFlagConditions( + filters=[FeatureFilter(name="Audience", parameters={"Users": "{ invalid json"})], + ), + ) + + result = self.provider._process_enhanced_feature_flag(feature_flag) + + self.assertEqual(result["conditions"]["client_filters"][0]["parameters"], {"Users": "{ invalid json"}) + + def test_process_enhanced_feature_flag_filter_parameter_plain_string_is_not_parsed(self): + """Test that a plain string filter parameter value (not starting with '{' or '[') is left unchanged.""" + feature_flag = FeatureFlag( + name="MyFeature", + enabled=True, + conditions=FeatureFlagConditions( + filters=[FeatureFilter(name="Audience", parameters={"Region": "US"})], + ), + ) + + result = self.provider._process_enhanced_feature_flag(feature_flag) + + self.assertEqual(result["conditions"]["client_filters"][0]["parameters"], {"Region": "US"}) + def test_process_enhanced_feature_flag_with_variants_and_allocation(self): """Test processing an enhanced feature flag with variants and allocation. Variant values, like regular key-value settings, are raw strings on the wire: a variant with a JSON content type is parsed into a JSON @@ -569,9 +626,7 @@ def test_process_enhanced_feature_flag_invalid_variant_json_raises_value_error(s name="InvalidVariant", enabled=True, variants=[ - FeatureFlagVariantDefinition( - name="Variant", value="{ invalid json", content_type="application/json" - ) + FeatureFlagVariantDefinition(name="Variant", value="{ invalid json", content_type="application/json") ], ) @@ -658,6 +713,46 @@ def test_text_json_content_type_is_not_treated_as_json(self): result = AzureAppConfigurationProviderBase._parse_variant_value("{ invalid json", "text/json") self.assertEqual(result, "{ invalid json") + +class TestParseFilterParameterValue(unittest.TestCase): + """Tests for _parse_filter_parameter_value, which parses enhanced feature flag filter parameter values as a + best-effort attempt, always falling back to the raw string on failure since filter parameters have no + content type to declare JSON intent (unlike variant values).""" + + def test_json_object_is_parsed(self): + result = AzureAppConfigurationProviderBase._parse_filter_parameter_value('{"a": 1}') + self.assertEqual(result, {"a": 1}) + + def test_json_array_is_parsed(self): + result = AzureAppConfigurationProviderBase._parse_filter_parameter_value("[1, 2, 3]") + self.assertEqual(result, [1, 2, 3]) + + def test_invalid_json_object_falls_back_to_string(self): + result = AzureAppConfigurationProviderBase._parse_filter_parameter_value("{ invalid json") + self.assertEqual(result, "{ invalid json") + + def test_invalid_json_array_falls_back_to_string(self): + result = AzureAppConfigurationProviderBase._parse_filter_parameter_value("[ invalid json") + self.assertEqual(result, "[ invalid json") + + def test_plain_string_is_not_parsed(self): + result = AzureAppConfigurationProviderBase._parse_filter_parameter_value("US") + self.assertEqual(result, "US") + + def test_empty_string_is_not_parsed(self): + result = AzureAppConfigurationProviderBase._parse_filter_parameter_value("") + self.assertEqual(result, "") + + def test_json_literal_null_string_is_not_parsed(self): + """The literal string 'null' doesn't start with '{' or '[', so it should be left as a raw string, not + parsed into JSON null.""" + result = AzureAppConfigurationProviderBase._parse_filter_parameter_value("null") + self.assertEqual(result, "null") + + def test_non_string_value_is_returned_unchanged(self): + result = AzureAppConfigurationProviderBase._parse_filter_parameter_value(None) + self.assertIsNone(result) + def test_json_content_type_with_invalid_json_raises(self): """If the content type claims JSON but the value is not valid JSON, parsing should raise instead of silently falling back to the raw string.""" @@ -744,7 +839,9 @@ class TestProcessAndMergeFeatureFlags(unittest.TestCase): empty list (loaded this round, zero found).""" def setUp(self): - self.provider = AzureAppConfigurationProviderBase(endpoint="https://test.azconfig.io", feature_flag_enabled=True) + self.provider = AzureAppConfigurationProviderBase( + endpoint="https://test.azconfig.io", feature_flag_enabled=True + ) def test_enhanced_feature_flags_none_preserves_previous_processed_flags(self): feature_flag = FeatureFlag(name="MyFeature", enabled=True) @@ -797,9 +894,7 @@ def test_both_none_preserves_merged_result_unchanged(self): # Neither source refreshed this round: the previously merged list is passed through as # processed_feature_flags and should come back unchanged. - settings = self.provider._process_and_merge_feature_flags( - {}, previous_merged, None, None - ) + settings = self.provider._process_and_merge_feature_flags({}, previous_merged, None, None) self.assertEqual(settings[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY], previous_merged) def test_only_kv_refreshed_still_merges_with_cached_enhanced_flags(self):