diff --git a/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md index c9e93748c548..a132dc2ecfdf 100644 --- a/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md @@ -4,6 +4,10 @@ ### Features Added +- Added `check_configuration_settings()` method to efficiently check for configuration changes using HEAD requests, returning only headers (ETags) without response bodies. +- `list_configuration_settings()` and `check_configuration_settings()` now return `ConfigurationSettingPaged` (sync) / `ConfigurationSettingPagedAsync` (async) to expose the `by_page(match_conditions=...)` API and per-page `etag` attribute for change detection. +- `ConfigurationSettingPaged` and `ConfigurationSettingPagedAsync` are now publicly exported from `azure.appconfiguration`. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/appconfiguration/azure-appconfiguration/assets.json b/sdk/appconfiguration/azure-appconfiguration/assets.json index d8aa279cfb88..56fc3830c789 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_64742b5702" + "Tag": "python/appconfiguration/azure-appconfiguration_ea09ae1741" } diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py index 4dd855d6d711..313cb8b061ee 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/__init__.py @@ -18,6 +18,8 @@ ConfigurationSettingsFilter, ConfigurationSnapshot, ConfigurationSettingLabel, + ConfigurationSettingPaged, + ConfigurationSettingPagedAsync, ) from ._generated.models import ( SnapshotStatus, @@ -44,6 +46,8 @@ "ConfigurationSettingFields", "ConfigurationSettingsFilter", "ConfigurationSettingLabel", + "ConfigurationSettingPaged", + "ConfigurationSettingPagedAsync", "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 1c7363c44c36..1afd38d4e347 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py @@ -164,15 +164,15 @@ def list_configuration_settings( accept_datetime: Optional[Union[datetime, str]] = None, fields: Optional[List[Union[str, ConfigurationSettingFields]]] = None, **kwargs: Any, - ) -> ItemPaged[ConfigurationSetting]: + ) -> ConfigurationSettingPaged: """List the configuration settings stored in the configuration service, optionally filtered by key, label, tags and accept_datetime. For more information about supported filters, see https://learn.microsoft.com/azure/azure-app-configuration/rest-api-key-value?pivots=v23-11#supported-filters. - :keyword key_filter: Filter results based on their keys. '*' can be used as wildcard in the beginning or end + :keyword key_filter: Filter results based on their keys. '*' can be used as wildcard at the end of the filter. :paramtype key_filter: str or None - :keyword label_filter: Filter results based on their label. '*' can be used as wildcard in the beginning or end + :keyword label_filter: Filter results based on their label. '*' can be used as wildcard at the end of the filter. :paramtype label_filter: str or None :keyword tags_filter: Filter results based on their tags. @@ -183,7 +183,7 @@ def list_configuration_settings( Available fields see :class:`~azure.appconfiguration.ConfigurationSettingFields`. :paramtype fields: list[str] or list[~azure.appconfiguration.ConfigurationSettingFields] or None :return: An iterator of :class:`~azure.appconfiguration.ConfigurationSetting` - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.ConfigurationSetting] + :rtype: ~azure.appconfiguration.ConfigurationSettingPaged :raises: :class:`~azure.core.exceptions.HttpResponseError`, \ :class:`~azure.core.exceptions.ClientAuthenticationError` @@ -213,7 +213,7 @@ def list_configuration_settings( snapshot_name: str, fields: Optional[List[Union[str, ConfigurationSettingFields]]] = None, **kwargs: Any, - ) -> ItemPaged[ConfigurationSetting]: + ) -> ConfigurationSettingPaged: """List the configuration settings stored under a snapshot in the configuration service, optionally filtered by fields to present in return. @@ -222,12 +222,12 @@ def list_configuration_settings( Available fields see :class:`~azure.appconfiguration.ConfigurationSettingFields`. :paramtype fields: list[str] or list[~azure.appconfiguration.ConfigurationSettingFields] or None :return: An iterator of :class:`~azure.appconfiguration.ConfigurationSetting` - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.ConfigurationSetting] + :rtype: ~azure.appconfiguration.ConfigurationSettingPaged :raises: :class:`~azure.core.exceptions.HttpResponseError` """ @distributed_trace - def list_configuration_settings(self, *args: Optional[str], **kwargs: Any) -> ItemPaged[ConfigurationSetting]: + def list_configuration_settings(self, *args: Optional[str], **kwargs: Any) -> ConfigurationSettingPaged: accept_datetime = kwargs.pop("accept_datetime", None) if isinstance(accept_datetime, datetime): accept_datetime = str(accept_datetime) @@ -237,12 +237,13 @@ def list_configuration_settings(self, *args: Optional[str], **kwargs: Any) -> It snapshot_name = kwargs.pop("snapshot_name", None) if snapshot_name is not None: - return self._impl.get_key_values( # type: ignore[return-value] + command = functools.partial(self._impl.get_key_values_in_one_page, **kwargs) # type: ignore[attr-defined] + return ConfigurationSettingPaged( + command, snapshot=snapshot_name, accept_datetime=accept_datetime, select=select, - cls=lambda objs: [ConfigurationSetting._from_generated(x) for x in objs], - **kwargs, + page_iterator_class=ConfigurationSettingPropertiesPaged, ) tags = kwargs.pop("tags_filter", None) key_filter, kwargs = get_key_filter(*args, **kwargs) @@ -258,6 +259,63 @@ def list_configuration_settings(self, *args: Optional[str], **kwargs: Any) -> It page_iterator_class=ConfigurationSettingPropertiesPaged, ) + @distributed_trace + def check_configuration_settings( + self, + *, + key_filter: Optional[str] = None, + label_filter: Optional[str] = None, + tags_filter: Optional[List[str]] = None, + accept_datetime: Optional[Union[datetime, str]] = None, + fields: Optional[List[Union[str, ConfigurationSettingFields]]] = None, + **kwargs: Any, + ) -> ConfigurationSettingPaged: + """Check configuration settings using a HEAD request, returning only headers without the + response body. This is useful for efficiently checking if settings have changed by comparing ETags. + + :keyword key_filter: Filter results based on their keys. '*' can be used as wildcard at the end + of the filter. + :paramtype key_filter: str or None + :keyword label_filter: Filter results based on their label. '*' can be used as wildcard at the end + of the filter. + :paramtype label_filter: str or None + :keyword tags_filter: Filter results based on their tags. + :paramtype tags_filter: list[str] or None + :keyword accept_datetime: Retrieve ConfigurationSetting that existed at this datetime + :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.ConfigurationSettingFields`. + :paramtype fields: list[str] or list[~azure.appconfiguration.ConfigurationSettingFields] or None + :return: A pager intended for :meth:`by_page` iteration to inspect page headers (for example, ``etag``) + and detect changed pages. This operation issues HEAD requests and does not return full + :class:`~azure.appconfiguration.ConfigurationSetting` bodies when iterated item by item. + :rtype: ~azure.appconfiguration.ConfigurationSettingPaged + :raises: :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError` + + Example + + .. code-block:: python + + items = client.check_configuration_settings(key_filter="my_key*") + for page in items.by_page(): + print(page.etag) # etag for this page + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + if fields: + fields = ["locked" if x == "read_only" else x for x in fields] + command = functools.partial(self._impl.check_key_values_in_one_page, **kwargs) # type: ignore[attr-defined] + return ConfigurationSettingPaged( + command, + key=key_filter, + label=label_filter, + accept_datetime=accept_datetime, + select=fields, + tags=tags_filter, + page_iterator_class=ConfigurationSettingPropertiesPaged, + ) + @distributed_trace def get_configuration_setting( self, @@ -473,10 +531,10 @@ def list_revisions( For more information about supported filters, see https://learn.microsoft.com/azure/azure-app-configuration/rest-api-revisions?pivots=v23-11#supported-filters. - :param key_filter: Filter results based on their keys. '*' can be used as wildcard in the beginning or end + :param key_filter: Filter results based on their keys. '*' can be used as wildcard at the end of the filter. :type key_filter: str or None - :param label_filter: Filter results based on their label. '*' can be used as wildcard in the beginning or end + :param label_filter: Filter results based on their label. '*' can be used as wildcard at the end of the filter. :type label_filter: str or None :keyword tags_filter: Filter results based on their tags. 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 c099663e0dbd..339962c778cc 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_patch.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_patch.py @@ -10,7 +10,7 @@ """ import json import urllib.parse -from typing import Any, Iterable, List, Optional, Union, MutableMapping, Type +from typing import Any, List, Optional, Union, MutableMapping, Type from azure.core import MatchConditions from azure.core.exceptions import ( ClientAuthenticationError, @@ -29,6 +29,7 @@ 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, ) @@ -41,6 +42,84 @@ 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, + ) + + 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 _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, @@ -56,8 +135,8 @@ def get_key_values_in_one_page( etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, continuation_token: Optional[str] = None, - **kwargs: Any - ) -> Iterable["_models.KeyValue"]: + **kwargs: Any, + ) -> dict: """Gets a list of key-values in one page. Gets a list of key-values in one page. @@ -93,8 +172,8 @@ def get_key_values_in_one_page( :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: An iterator like instance of KeyValue - :rtype: ~azure.core.paging.ItemPaged[~azure.appconfiguration.models.KeyValue] + :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 {} @@ -102,91 +181,39 @@ def get_key_values_in_one_page( cls: ClsType[List[_models.KeyValue]] = kwargs.pop("cls", None) - 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 + error_map = self._build_kv_error_map(match_condition) 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 - - # Add etag and match_condition to headers - _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( - "GET", - urllib.parse.urljoin(next_link, _parsed_next_link.path), - params=_next_request_params, - headers=_next_headers, - ) - 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 - - _request = prepare_request(continuation_token) - _stream = False + 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=_stream, **kwargs + _request, stream=False, **kwargs ) response = pipeline_response.http_response @@ -204,20 +231,130 @@ def prepare_request(next_link=None): raise HttpResponseError(response=response, model=error) response_headers = response.headers - deserialized = json.loads("{}") + result = json.loads("{}") if response.status_code != 304: - deserialized = pipeline_response.http_response.json() + 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(">")] - deserialized["@nextLink"] = next_link + 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, deserialized, response_headers) + return cls(pipeline_response, result, response_headers) - return deserialized + return result __all__: List[str] = [ 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 ea4cb88e7b8e..e07e11aa0aaa 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 @@ -8,7 +8,7 @@ """ import json import urllib.parse -from typing import Any, AsyncIterable, List, Optional, Union, MutableMapping, Type +from typing import Any, List, Optional, Union, MutableMapping, Type from azure.core import MatchConditions from azure.core.exceptions import ( ClientAuthenticationError, @@ -26,6 +26,7 @@ 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 @@ -37,6 +38,84 @@ 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, + ) + + 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 _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, @@ -52,8 +131,8 @@ async def get_key_values_in_one_page( etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, continuation_token: Optional[str] = None, - **kwargs: Any - ) -> AsyncIterable["_models.KeyValue"]: + **kwargs: Any, + ) -> dict: """Gets a list of key-values in one page. Gets a list of key-values in one page. @@ -89,8 +168,8 @@ async def get_key_values_in_one_page( :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: An iterator like instance of KeyValue - :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration.models.KeyValue] + :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 {} @@ -98,92 +177,39 @@ async def get_key_values_in_one_page( cls: ClsType[List[_models.KeyValue]] = kwargs.pop("cls", None) - error_map: MutableMapping[int, Type[HttpResponseError]] = { - 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 = self._build_kv_error_map(match_condition) 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 - - # Add etag and match_condition to headers - _next_headers = dict(_headers) - accept = _headers.pop("Accept", None) - if etag is not 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( - "GET", - urllib.parse.urljoin(next_link, _parsed_next_link.path), - params=_next_request_params, - headers=_next_headers, - ) - 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 - - _request = prepare_request(continuation_token) - - _stream = False + 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=_stream, **kwargs + _request, stream=False, **kwargs ) response = pipeline_response.http_response @@ -197,20 +223,130 @@ def prepare_request(next_link=None): raise HttpResponseError(response=response, model=error) response_headers = response.headers - deserialized = json.loads("{}") + result = json.loads("{}") if response.status_code != 304: - deserialized = pipeline_response.http_response.json() + 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(">")] - deserialized["@nextLink"] = next_link + 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, deserialized, response_headers) + return cls(pipeline_response, result, response_headers) - return deserialized + return result __all__: List[str] = [ diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py index 50affb792308..9d19d578048c 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py @@ -602,6 +602,7 @@ def __init__(self, command: Callable, **kwargs: Any): self._accept_datetime = kwargs.get("accept_datetime") self._select = kwargs.get("select") self._tags = kwargs.get("tags") + self._snapshot = kwargs.get("snapshot") self._etags: List[str] = kwargs.get("etags", []) self._current_etag = 0 self._match_condition = kwargs.get("match_condition") @@ -669,6 +670,7 @@ def _get_next_cb(self, continuation_token, **kwargs): accept_datetime=self._accept_datetime, select=self._select, tags=self._tags, + snapshot=self._snapshot, etag=etag, match_condition=self._match_condition, continuation_token=continuation_token, @@ -729,6 +731,7 @@ async def _get_next_cb(self, continuation_token, **kwargs): accept_datetime=self._accept_datetime, select=self._select, tags=self._tags, + snapshot=self._snapshot, etag=etag, match_condition=self._match_condition, continuation_token=continuation_token, 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 facc34344711..6959a9aa2039 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 @@ -169,15 +169,15 @@ def list_configuration_settings( accept_datetime: Optional[Union[datetime, str]] = None, fields: Optional[List[Union[str, ConfigurationSettingFields]]] = None, **kwargs: Any, - ) -> AsyncItemPaged[ConfigurationSetting]: + ) -> ConfigurationSettingPagedAsync: """List the configuration settings stored in the configuration service, optionally filtered by key, label, tags and accept_datetime. For more information about supported filters, see https://learn.microsoft.com/azure/azure-app-configuration/rest-api-key-value?pivots=v23-11#supported-filters. - :keyword key_filter: Filter results based on their keys. '*' can be used as wildcard in the beginning or end + :keyword key_filter: Filter results based on their keys. '*' can be used as wildcard at the end of the filter. :paramtype key_filter: str or None - :keyword label_filter: Filter results based on their label. '*' can be used as wildcard in the beginning or end + :keyword label_filter: Filter results based on their label. '*' can be used as wildcard at the end of the filter. :paramtype label_filter: str or None :keyword tags_filter: Filter results based on their tags. @@ -188,7 +188,7 @@ def list_configuration_settings( Available fields see :class:`~azure.appconfiguration.ConfigurationSettingFields`. :paramtype fields: list[str] or list[~azure.appconfiguration.ConfigurationSettingFields] or None :return: An async iterator of :class:`~azure.appconfiguration.ConfigurationSetting` - :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.appconfiguration.ConfigurationSetting] + :rtype: ~azure.appconfiguration.ConfigurationSettingPagedAsync :raises: :class:`~azure.core.exceptions.HttpResponseError`, \ :class:`~azure.core.exceptions.ClientAuthenticationError` @@ -218,7 +218,7 @@ def list_configuration_settings( snapshot_name: str, fields: Optional[List[Union[str, ConfigurationSettingFields]]] = None, **kwargs: Any, - ) -> AsyncItemPaged[ConfigurationSetting]: + ) -> ConfigurationSettingPagedAsync: """List the configuration settings stored under a snapshot in the configuration service, optionally filtered by accept_datetime and fields to present in return. @@ -227,12 +227,12 @@ def list_configuration_settings( Available fields see :class:`~azure.appconfiguration.ConfigurationSettingFields`. :paramtype fields: list[str] or list[~azure.appconfiguration.ConfigurationSettingFields] or None :return: An async iterator of :class:`~azure.appconfiguration.ConfigurationSetting` - :rtype: ~azure.core.paging.AsyncItemPaged[~azure.appconfiguration.ConfigurationSetting] + :rtype: ~azure.appconfiguration.ConfigurationSettingPagedAsync :raises: :class:`~azure.core.exceptions.HttpResponseError` """ @distributed_trace - def list_configuration_settings(self, *args: Optional[str], **kwargs: Any) -> AsyncItemPaged[ConfigurationSetting]: + def list_configuration_settings(self, *args: Optional[str], **kwargs: Any) -> ConfigurationSettingPagedAsync: accept_datetime = kwargs.pop("accept_datetime", None) if isinstance(accept_datetime, datetime): accept_datetime = str(accept_datetime) @@ -243,12 +243,13 @@ def list_configuration_settings(self, *args: Optional[str], **kwargs: Any) -> As snapshot_name = kwargs.pop("snapshot_name", None) if snapshot_name is not None: - return self._impl.get_key_values( # type: ignore[return-value] + command = functools.partial(self._impl.get_key_values_in_one_page, **kwargs) # type: ignore[attr-defined] + return ConfigurationSettingPagedAsync( + command, snapshot=snapshot_name, accept_datetime=accept_datetime, select=select, - cls=lambda objs: [ConfigurationSetting._from_generated(x) for x in objs], - **kwargs, + page_iterator_class=ConfigurationSettingPropertiesPagedAsync, ) tags = kwargs.pop("tags_filter", None) key_filter, kwargs = get_key_filter(*args, **kwargs) @@ -264,6 +265,64 @@ def list_configuration_settings(self, *args: Optional[str], **kwargs: Any) -> As page_iterator_class=ConfigurationSettingPropertiesPagedAsync, ) + @distributed_trace + def check_configuration_settings( + self, + *, + key_filter: Optional[str] = None, + label_filter: Optional[str] = None, + tags_filter: Optional[List[str]] = None, + accept_datetime: Optional[Union[datetime, str]] = None, + fields: Optional[List[Union[str, ConfigurationSettingFields]]] = None, + **kwargs: Any, + ) -> ConfigurationSettingPagedAsync: + """Check configuration settings using a HEAD request, returning only headers without the + response body. This is useful for efficiently checking if settings have changed by comparing ETags. + + :keyword key_filter: Filter results based on their keys. '*' can be used as wildcard at the end + of the filter. + :paramtype key_filter: str or None + :keyword label_filter: Filter results based on their label. '*' can be used as wildcard at the end + of the filter. + :paramtype label_filter: str or None + :keyword tags_filter: Filter results based on their tags. + :paramtype tags_filter: list[str] or None + :keyword accept_datetime: Retrieve ConfigurationSetting that existed at this datetime + :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.ConfigurationSettingFields`. + :paramtype fields: list[str] or list[~azure.appconfiguration.ConfigurationSettingFields] or None + :return: An async pager intended for :meth:`by_page` iteration to inspect page headers (for example, ``etag``) + and detect changed pages. This operation issues HEAD requests and does not return full + :class:`~azure.appconfiguration.ConfigurationSetting` bodies when iterated item by item. + :rtype: ~azure.appconfiguration.ConfigurationSettingPagedAsync + :raises: :class:`~azure.core.exceptions.HttpResponseError`, \ + :class:`~azure.core.exceptions.ClientAuthenticationError` + + Example + + .. code-block:: python + + # in async function + items = async_client.check_configuration_settings(key_filter="my_key*") + async for page in items.by_page(): + print(page.etag) # etag for this page + """ + if isinstance(accept_datetime, datetime): + accept_datetime = str(accept_datetime) + if fields: + fields = ["locked" if x == "read_only" else x for x in fields] + command = functools.partial(self._impl.check_key_values_in_one_page, **kwargs) # type: ignore[attr-defined] + return ConfigurationSettingPagedAsync( + command, + key=key_filter, + label=label_filter, + accept_datetime=accept_datetime, + select=fields, + tags=tags_filter, + page_iterator_class=ConfigurationSettingPropertiesPagedAsync, + ) + @distributed_trace_async async def get_configuration_setting( self, @@ -483,10 +542,10 @@ def list_revisions( For more information about supported filters, see https://learn.microsoft.com/azure/azure-app-configuration/rest-api-revisions?pivots=v23-11#supported-filters. - :param key_filter: Filter results based on their keys. '*' can be used as wildcard in the beginning or end + :param key_filter: Filter results based on their keys. '*' can be used as wildcard at the end of the filter. :type key_filter: str or None - :param label_filter: Filter results based on their label. '*' can be used as wildcard in the beginning or end + :param label_filter: Filter results based on their label. '*' can be used as wildcard at the end of the filter. :type label_filter: str or None :keyword tags_filter: Filter results based on their tags. diff --git a/sdk/appconfiguration/azure-appconfiguration/samples/check_configuration_settings_sample.py b/sdk/appconfiguration/azure-appconfiguration/samples/check_configuration_settings_sample.py new file mode 100644 index 000000000000..3e87d03c14a3 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration/samples/check_configuration_settings_sample.py @@ -0,0 +1,92 @@ +# 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: check_configuration_settings_sample.py + +DESCRIPTION: + This sample demos how to check configuration settings using HEAD requests. + HEAD requests return only headers (including ETags) without the response body, + making them useful for efficiently detecting whether settings have changed. + +USAGE: python check_configuration_settings_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 AzureAppConfigurationClient, ConfigurationSetting +from azure.identity import DefaultAzureCredential + + +def main(): + ENDPOINT = os.environ["APPCONFIGURATION_ENDPOINT_STRING"] + credential = DefaultAzureCredential() + + # Create an app config client + client = AzureAppConfigurationClient(base_url=ENDPOINT, credential=credential) + + # Set up sample configuration settings + config_setting1 = ConfigurationSetting( + key="CheckKey1", value="value1", content_type="my content type", tags={"env": "dev"} + ) + config_setting2 = ConfigurationSetting( + key="CheckKey2", value="value2", content_type="my content type", tags={"env": "dev"} + ) + client.set_configuration_setting(config_setting1) + client.set_configuration_setting(config_setting2) + + # [START check_configuration_settings] + # Use check_configuration_settings to get page ETags via HEAD requests. + # This returns only headers (no body), which is more efficient than listing all settings. + print("Checking configuration settings (HEAD request)...") + items = client.check_configuration_settings(key_filter="CheckKey*") + 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.check_configuration_settings(key_filter="CheckKey*") + 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 setting and check again + print("\nModifying a setting...") + client.set_configuration_setting( + ConfigurationSetting(key="CheckKey1", value="updated value1", content_type="my content type") + ) + + print("Checking for changes after modification...") + items = client.check_configuration_settings(key_filter="CheckKey*") + 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 check_configuration_settings] + + # Clean up + client.delete_configuration_setting(key="CheckKey1") + client.delete_configuration_setting(key="CheckKey2") + + +if __name__ == "__main__": + main() 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 ef7bda07af3b..93a2b4c43ee4 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py @@ -1195,6 +1195,73 @@ def test_monitor_configuration_settings_by_page_etag(self, appconfiguration_endp # clean up self.tear_down() + @AppConfigPreparer() + @recorded_by_proxy + def test_check_configuration_settings_by_page_etag(self, appconfiguration_endpoint_string): + # response header and are missing in python38. + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + self.set_up(appconfiguration_endpoint_string) + # prepare 200 configuration settings + for i in range(200): + self.client.set_configuration_setting( + ConfigurationSetting( + key=f"sample_key_{str(i)}", + label=f"sample_label_{str(i)}", + ) + ) + # there will have 2 pages while listing, there are 100 configuration settings per page. + + # get page etags using check (HEAD request) + match_conditions = [] + items = self.client.check_configuration_settings(key_filter="sample_key_*", label_filter="sample_label_*") + iterator = items.by_page() + for _ in iterator: + etag = iterator.etag + match_conditions.append(etag) + + # monitor page updates without changes - only changed pages will be yielded + items = self.client.check_configuration_settings(key_filter="sample_key_*", label_filter="sample_label_*") + iterator = items.by_page(match_conditions=match_conditions) + changed_pages = list(iterator) + + # No pages should be yielded since nothing changed + assert len(changed_pages) == 0 + + # do some changes + self.client.set_configuration_setting( + ConfigurationSetting( + key="sample_key_201", + label="sample_label_202", + ) + ) + # now we have three pages, 100 settings in first two pages and 1 setting in the last page + + # get page etags after updates using check (HEAD request) + new_match_conditions = [] + items = self.client.check_configuration_settings(key_filter="sample_key_*", label_filter="sample_label_*") + iterator = items.by_page() + for _ in iterator: + etag = iterator.etag + new_match_conditions.append(etag) + + before_len = len(match_conditions) + after_len = len(new_match_conditions) + # the number of pages should not decrease after adding a configuration setting + assert after_len >= before_len + + # At least one of the existing pages' ETags should differ after the update. + assert any(old_etag != new_etag for old_etag, new_etag in zip(match_conditions, new_match_conditions)) + + # monitor pages after updates - only changed pages will be yielded + items = self.client.check_configuration_settings(key_filter="sample_key_*", label_filter="sample_label_*") + iterator = items.by_page(match_conditions=new_match_conditions) + changed_pages = list(iterator) + # Should yield 0 pages + assert len(changed_pages) == 0 + + # clean up + self.tear_down() + @AppConfigPreparer() @recorded_by_proxy def test_list_labels(self, appconfiguration_endpoint_string): 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 af60c4a961a8..bbef9e6ae47c 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 @@ -1230,6 +1230,80 @@ async def test_monitor_configuration_settings_by_page_etag(self, appconfiguratio # clean up await self.tear_down() + @AppConfigPreparer() + @recorded_by_proxy_async + async def test_check_configuration_settings_by_page_etag(self, appconfiguration_endpoint_string): + # response header and are missing in python38. + set_custom_default_matcher(compare_bodies=False, excluded_headers="x-ms-content-sha256,x-ms-date") + await self.set_up(appconfiguration_endpoint_string) + # prepare 200 configuration settings + for i in range(200): + await self.client.set_configuration_setting( + ConfigurationSetting( + key=f"async_sample_key_{str(i)}", + label=f"async_sample_label_{str(i)}", + ) + ) + # there will have 2 pages while listing, there are 100 configuration settings per page. + + # get page etags using check (HEAD request) + match_conditions = [] + items = self.client.check_configuration_settings( + key_filter="async_sample_key_*", label_filter="async_sample_label_*" + ) + iterator = items.by_page() + async for _ in iterator: + etag = iterator.etag + match_conditions.append(etag) + + # monitor page updates without changes - only changed pages will be yielded + items = self.client.check_configuration_settings( + key_filter="async_sample_key_*", label_filter="async_sample_label_*" + ) + iterator = items.by_page(match_conditions=match_conditions) + changed_pages = [page async for page in iterator] + # No pages should be yielded since nothing changed + assert len(changed_pages) == 0 + + # do some changes + await self.client.set_configuration_setting( + ConfigurationSetting( + key="async_sample_key_201", + label="async_sample_label_202", + ) + ) + # now we have three pages, 100 settings in first two pages and 1 setting in the last page + + # get page etags after updates using check (HEAD request) + new_match_conditions = [] + items = self.client.check_configuration_settings( + key_filter="async_sample_key_*", label_filter="async_sample_label_*" + ) + iterator = items.by_page() + async for _ in iterator: + etag = iterator.etag + new_match_conditions.append(etag) + + before_len = len(match_conditions) + after_len = len(new_match_conditions) + # the number of pages should not decrease after adding a configuration setting + assert after_len >= before_len + + # At least one of the existing pages' ETags should differ after the update. + assert any(old != new for old, new in zip(match_conditions, new_match_conditions)) + + # monitor pages after updates - only changed pages will be yielded + items = self.client.check_configuration_settings( + key_filter="async_sample_key_*", label_filter="async_sample_label_*" + ) + iterator = items.by_page(match_conditions=new_match_conditions) + changed_pages = [page async for page in iterator] + # Should yield 0 pages + assert len(changed_pages) == 0 + + # clean up + await self.tear_down() + @AppConfigPreparer() @recorded_by_proxy_async async def test_list_labels(self, appconfiguration_endpoint_string):