diff --git a/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md index ef7132ba207b..417109b385e7 100644 --- a/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features Added +- Added `match_conditions` parameter to `by_page()` method in `list_configuration_settings()` to efficiently monitor configuration changes using etags without fetching unchanged data. - Added support for custom authentication audiences via the `audience` keyword argument in `AzureAppConfigurationClient` constructor to enable authentication against sovereign clouds. ### Breaking Changes diff --git a/sdk/appconfiguration/azure-appconfiguration/assets.json b/sdk/appconfiguration/azure-appconfiguration/assets.json index 681ff5abc839..f7187ea50a72 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_5a7879bd17" + "Tag": "python/appconfiguration/azure-appconfiguration_43ad21e0c9" } 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 2d556b8a99e9..b6362532a0b2 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_client.py @@ -28,6 +28,7 @@ from ._models import ( ConfigurationSetting, ConfigurationSettingPropertiesPaged, + ConfigurationSettingPaged, ConfigurationSettingsFilter, ConfigurationSnapshot, ConfigurationSettingLabel, @@ -236,7 +237,7 @@ def list_configuration_settings(self, *args: Optional[str], **kwargs: Any) -> It key_filter, kwargs = get_key_filter(*args, **kwargs) label_filter, kwargs = get_label_filter(*args, **kwargs) command = functools.partial(self._impl.get_key_values_in_one_page, **kwargs) # type: ignore[attr-defined] - return ItemPaged( + return ConfigurationSettingPaged( command, key=key_filter, label=label_filter, 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 e88a8132fef7..f1c4c114a18b 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_patch.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_generated/_operations/_patch.py @@ -8,6 +8,7 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ +import json import urllib.parse from typing import Any, Iterable, List, Optional, Union, MutableMapping, Type from azure.core import MatchConditions @@ -28,9 +29,15 @@ AzureAppConfigurationClientOperationsMixin as AzureAppConfigClientOpGenerated, ClsType, build_azure_app_configuration_get_key_values_request, + prep_if_match, + prep_if_none_match, ) from .. import models as _models from .._model_base import _deserialize +from .._serialization import Serializer + +_SERIALIZER = Serializer() +_SERIALIZER.client_side_validation = False class AzureAppConfigurationClientOperationsMixin(AzureAppConfigClientOpGenerated): @@ -144,8 +151,28 @@ def prepare_request(next_link=None): } ) _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 + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + params=_next_request_params, + headers=_next_headers, ) path_format_arguments = { "endpoint": self._serialize.url( @@ -157,20 +184,31 @@ def prepare_request(next_link=None): return _request _request = prepare_request(continuation_token) - _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]: + 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 - deserialized = pipeline_response.http_response.json() + deserialized = json.loads("{}") + if response.status_code != 304: + deserialized = 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 if cls: return cls(pipeline_response, deserialized, response_headers) 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 fd79a4eb3b4c..ea4cb88e7b8e 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 @@ -6,6 +6,7 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ +import json import urllib.parse from typing import Any, AsyncIterable, List, Optional, Union, MutableMapping, Type from azure.core import MatchConditions @@ -26,8 +27,13 @@ ClsType, build_azure_app_configuration_get_key_values_request, ) +from ..._operations._operations import prep_if_match, prep_if_none_match from ... import models as _models from ..._model_base import _deserialize +from ..._serialization import Serializer + +_SERIALIZER = Serializer() +_SERIALIZER.client_side_validation = False class AzureAppConfigurationClientOperationsMixin(AzureAppConfigClientOpGenerated): @@ -141,8 +147,28 @@ def prepare_request(next_link=None): } ) _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 + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + params=_next_request_params, + headers=_next_headers, ) path_format_arguments = { "endpoint": self._serialize.url( @@ -161,13 +187,25 @@ def prepare_request(next_link=None): ) response = pipeline_response.http_response - if response.status_code not in [200]: + 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 - deserialized = pipeline_response.http_response.json() + deserialized = json.loads("{}") + if response.status_code != 304: + deserialized = 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 if cls: return cls(pipeline_response, deserialized, response_headers) diff --git a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py index 6bf229d3530e..be8f281b0d3f 100644 --- a/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_models.py @@ -2,13 +2,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +import collections import json from datetime import datetime -from typing import Any, Dict, List, Optional, Union, cast, Callable +from typing import Any, Dict, List, Optional, Union, cast, Callable, TypeVar, Iterator, AsyncIterator +from azure.core import MatchConditions +from azure.core.exceptions import AzureError from azure.core.rest import HttpResponse -from azure.core.paging import PageIterator -from azure.core.async_paging import AsyncPageIterator +from azure.core.paging import PageIterator, ItemPaged +from azure.core.async_paging import AsyncPageIterator, AsyncItemPaged, AsyncList from ._generated._serialization import Model from ._generated.models import ( KeyValue, @@ -19,6 +22,8 @@ ) from ._generated._model_base import _deserialize +ReturnType = TypeVar("ReturnType") + class ConfigurationSetting(Model): """A setting, defined by a unique combination of a key and label.""" @@ -575,81 +580,273 @@ def _return_deserialized_and_headers(_, deserialized, response_headers): return deserialized, response_headers -class ConfigurationSettingPropertiesPaged(PageIterator): - """An iterable of ConfigurationSetting properties.""" +class ConfigurationSettingPropertiesPagedBase: # pylint:disable=too-many-instance-attributes + """Base class for iterable of ConfigurationSetting properties.""" etag: str - """The etag of current page.""" + """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): - super(ConfigurationSettingPropertiesPaged, self).__init__( - self._get_next_cb, - self._extract_data_cb, - continuation_token=kwargs.get("continuation_token"), - ) + """Initialize common attributes for paged configuration settings. + + :param command: The command to execute for pagination. + :type command: Callable + """ self._command = command self._key = kwargs.get("key") 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: [ ConfigurationSetting._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[KeyValue], 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 ConfigurationSettingPropertiesPaged( + ConfigurationSettingPropertiesPagedBase, PageIterator +): # pylint:disable=too-many-instance-attributes + """An iterable of ConfigurationSetting 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( key=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): - deserialized, response_headers = get_next_return - list_of_elem = _deserialize(List[KeyValue], deserialized["items"]) - self.etag = response_headers.pop("ETag") - return deserialized.get("@nextLink") or None, iter(self._deserializer(list_of_elem)) + return self._extract_data_cb_base(get_next_return) + def __next__(self) -> Iterator[ReturnType]: + """Get the next page in the iterator. -class ConfigurationSettingPropertiesPagedAsync(AsyncPageIterator): - """An iterable of ConfigurationSetting properties.""" + :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. + """ + 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) + 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) - etag: str - """The etag of current page.""" + +class ConfigurationSettingPropertiesPagedAsync( + ConfigurationSettingPropertiesPagedBase, AsyncPageIterator +): # pylint:disable=too-many-instance-attributes + """An iterable of ConfigurationSetting properties.""" def __init__(self, command: Callable, **kwargs: Any): - super(ConfigurationSettingPropertiesPagedAsync, self).__init__( + ConfigurationSettingPropertiesPagedBase.__init__(self, command, **kwargs) + AsyncPageIterator.__init__( + self, self._get_next_cb, self._extract_data_cb, continuation_token=kwargs.get("continuation_token"), ) - self._command = command - self._key = kwargs.get("key") - self._label = kwargs.get("label") - self._accept_datetime = kwargs.get("accept_datetime") - self._select = kwargs.get("select") - self._tags = kwargs.get("tags") - self._deserializer = lambda objs: [ - ConfigurationSetting._from_generated(x) for x in objs # pylint:disable=protected-access - ] async def _get_next_cb(self, continuation_token, **kwargs): + etag = self._next_etag() return await self._command( key=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, ) async def _extract_data_cb(self, get_next_return): - deserialized, response_headers = get_next_return - list_of_elem = _deserialize(List[KeyValue], deserialized["items"]) - self.etag = response_headers.pop("ETag") - return deserialized.get("@nextLink") or None, iter(self._deserializer(list_of_elem)) + 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. + """ + 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) + + 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 ConfigurationSettingPaged(ItemPaged): + """ + An iterable of ConfigurationSettings that supports etag-based change detection. + + This class extends ItemPaged to provide efficient monitoring of 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. + + Example: + # Get initial page ETags + items = client.list_configuration_settings(key_filter="sample_*") + match_conditions = [page.etag for page in items.by_page()] + + # Later, check for changes - only changed pages are returned + items = client.list_configuration_settings(key_filter="sample_*") + for page in items.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 ConfigurationSettingPagedAsync(AsyncItemPaged): + """ + An async iterable of ConfigurationSettings that supports etag-based change detection. + + This class provides asynchronous iteration over configuration settings, 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 usage: + + async for setting in ConfigurationSettingPagedAsync(...): + # Process each setting asynchronously + print(setting) + + # 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 setting in page: + print(setting) + + 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/_azure_appconfiguration_client_async.py b/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/aio/_azure_appconfiguration_client_async.py index 43456692ee7b..975981f0de1b 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 @@ -31,6 +31,7 @@ from .._models import ( ConfigurationSetting, ConfigurationSettingPropertiesPagedAsync, + ConfigurationSettingPagedAsync, ConfigurationSettingsFilter, ConfigurationSnapshot, ConfigurationSettingLabel, @@ -243,7 +244,7 @@ def list_configuration_settings(self, *args: Optional[str], **kwargs: Any) -> As key_filter, kwargs = get_key_filter(*args, **kwargs) label_filter, kwargs = get_label_filter(*args, **kwargs) command = functools.partial(self._impl.get_key_values_in_one_page, **kwargs) # type: ignore[attr-defined] - return AsyncItemPaged( + return ConfigurationSettingPagedAsync( command, key=key_filter, label=label_filter, 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 43c8cface068..e7b8ddf18b9a 100644 --- a/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py +++ b/sdk/appconfiguration/azure-appconfiguration/tests/test_azure_appconfiguration_client.py @@ -1132,7 +1132,7 @@ def test_monitor_configuration_settings_by_page_etag(self, appconfiguration_conn self.client = client # prepare 200 configuration settings for i in range(200): - self.client.add_configuration_setting( + self.client.set_configuration_setting( ConfigurationSetting( key=f"sample_key_{str(i)}", label=f"sample_label_{str(i)}", @@ -1141,43 +1141,23 @@ def test_monitor_configuration_settings_by_page_etag(self, appconfiguration_conn # there will have 2 pages while listing, there are 100 configuration settings per page. # get page etags - page_etags = [] + match_conditions = [] items = self.client.list_configuration_settings(key_filter="sample_key_*", label_filter="sample_label_*") iterator = items.by_page() - for page in iterator: + for _ in iterator: etag = iterator.etag - page_etags.append(etag) - - # monitor page updates without changes - continuation_token = None - index = 0 - request = HttpRequest( - method="GET", - url="/kv?key=sample_key_%2A&label=sample_label_%2A&api-version=2023-10-01", - headers={ - "If-None-Match": page_etags[index], - "Accept": "application/vnd.microsoft.appconfig.kvset+json, application/problem+json", - }, - ) - first_page_response = self.client.send_request(request) - assert first_page_response.status_code == 304 - - link = first_page_response.headers.get("Link", None) - continuation_token = link[1 : link.index(">")] if link else None - index += 1 - while continuation_token: - request = HttpRequest( - method="GET", url=f"{continuation_token}", headers={"If-None-Match": page_etags[index]} - ) - index += 1 - response = self.client.send_request(request) - assert response.status_code == 304 + match_conditions.append(etag) - link = response.headers.get("Link", None) - continuation_token = link[1 : link.index(">")] if link else None + # monitor page updates without changes - only changed pages will be yielded + items = self.client.list_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.add_configuration_setting( + self.client.set_configuration_setting( ConfigurationSetting( key="sample_key_201", label="sample_label_202", @@ -1186,51 +1166,24 @@ def test_monitor_configuration_settings_by_page_etag(self, appconfiguration_conn # now we have three pages, 100 settings in first two pages and 1 setting in the last page # get page etags after updates - new_page_etags = [] + new_match_conditions = [] items = self.client.list_configuration_settings(key_filter="sample_key_*", label_filter="sample_label_*") iterator = items.by_page() - for page in iterator: + for _ in iterator: etag = iterator.etag - new_page_etags.append(etag) - - assert page_etags[0] == new_page_etags[0] - assert page_etags[1] != new_page_etags[1] - assert page_etags[2] != new_page_etags[2] - - # monitor page after updates - continuation_token = None - index = 0 - request = HttpRequest( - method="GET", - url="/kv?key=sample_key_%2A&label=sample_label_%2A&api-version=2023-10-01", - headers={ - "If-None-Match": page_etags[index], - "Accept": "application/vnd.microsoft.appconfig.kvset+json, application/problem+json", - }, - ) - first_page_response = self.client.send_request(request) - # 304 means the page doesn't have changes. - assert first_page_response.status_code == 304 - - link = first_page_response.headers.get("Link", None) - continuation_token = link[1 : link.index(">")] if link else None - index += 1 - while continuation_token: - request = HttpRequest( - method="GET", url=f"{continuation_token}", headers={"If-None-Match": page_etags[index]} - ) - index += 1 - response = self.client.send_request(request) - - # 200 means the page has changes. - assert response.status_code == 200 - items = response.json()["items"] - for item in items: - # Read the keys - pass - - link = response.headers.get("Link", None) - continuation_token = link[1 : link.index(">")] if link else None + new_match_conditions.append(etag) + + assert match_conditions[0] == new_match_conditions[0] + assert match_conditions[1] != new_match_conditions[1] + assert match_conditions[2] != new_match_conditions[2] + assert len(new_match_conditions) == 3 + + # monitor pages after updates - only changed pages will be yielded + items = self.client.list_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() 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 cc11a28af590..76a53750ae42 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 @@ -1154,7 +1154,7 @@ async def test_monitor_configuration_settings_by_page_etag(self, appconfiguratio self.client = client # prepare 200 configuration settings for i in range(200): - await client.add_configuration_setting( + await client.set_configuration_setting( ConfigurationSetting( key=f"async_sample_key_{str(i)}", label=f"async_sample_label_{str(i)}", @@ -1163,45 +1163,26 @@ async def test_monitor_configuration_settings_by_page_etag(self, appconfiguratio # there will have 2 pages while listing, there are 100 configuration settings per page. # get page etags - page_etags = [] + match_conditions = [] items = client.list_configuration_settings( key_filter="async_sample_key_*", label_filter="async_sample_label_*" ) iterator = items.by_page() - async for page in iterator: + async for _ in iterator: etag = iterator.etag - page_etags.append(etag) - - # monitor page updates without changes - continuation_token = None - index = 0 - request = HttpRequest( - method="GET", - url="/kv?key=async_sample_key_%2A&label=async_sample_label_%2A&api-version=2023-10-01", - headers={ - "If-None-Match": page_etags[index], - "Accept": "application/vnd.microsoft.appconfig.kvset+json, application/problem+json", - }, - ) - first_page_response = await client.send_request(request) - assert first_page_response.status_code == 304 - - link = first_page_response.headers.get("Link", None) - continuation_token = link[1 : link.index(">")] if link else None - index += 1 - while continuation_token: - request = HttpRequest( - method="GET", url=f"{continuation_token}", headers={"If-None-Match": page_etags[index]} - ) - index += 1 - response = await client.send_request(request) - assert response.status_code == 304 + match_conditions.append(etag) - link = response.headers.get("Link", None) - continuation_token = link[1 : link.index(">")] if link else None + # monitor page updates without changes - only changed pages will be yielded + items = client.list_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 client.add_configuration_setting( + await client.set_configuration_setting( ConfigurationSetting( key="async_sample_key_201", label="async_sample_label_202", @@ -1210,53 +1191,29 @@ async def test_monitor_configuration_settings_by_page_etag(self, appconfiguratio # now we have three pages, 100 settings in first two pages and 1 setting in the last page # get page etags after updates - new_page_etags = [] + new_match_conditions = [] items = client.list_configuration_settings( key_filter="async_sample_key_*", label_filter="async_sample_label_*" ) iterator = items.by_page() - async for page in iterator: + async for _ in iterator: etag = iterator.etag - new_page_etags.append(etag) - - assert page_etags[0] == new_page_etags[0] - assert page_etags[1] != new_page_etags[1] - assert page_etags[2] != new_page_etags[2] - - # monitor page after updates - continuation_token = None - index = 0 - request = HttpRequest( - method="GET", - url="/kv?key=async_sample_key_%2A&label=async_sample_label_%2A&api-version=2023-10-01", - headers={ - "If-None-Match": page_etags[index], - "Accept": "application/vnd.microsoft.appconfig.kvset+json, application/problem+json", - }, + new_match_conditions.append(etag) + + assert match_conditions[0] == new_match_conditions[0] + assert match_conditions[1] != new_match_conditions[1] + assert match_conditions[2] == new_match_conditions[2] + assert len(new_match_conditions) == 3 + + # monitor pages after updates - only changed pages will be yielded + items = client.list_configuration_settings( + key_filter="async_sample_key_*", label_filter="async_sample_label_*" ) - first_page_response = await client.send_request(request) - # 304 means the page doesn't have changes. - assert first_page_response.status_code == 304 - - link = first_page_response.headers.get("Link", None) - continuation_token = link[1 : link.index(">")] if link else None - index += 1 - while continuation_token: - request = HttpRequest( - method="GET", url=f"{continuation_token}", headers={"If-None-Match": page_etags[index]} - ) - index += 1 - response = await client.send_request(request) - - # 200 means the page has changes. - assert response.status_code == 200 - items = response.json()["items"] - for item in items: - # Read the keys - pass - - link = response.headers.get("Link", None) - continuation_token = link[1 : link.index(">")] if link else None + 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()