diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index e8abbd59d18c..83d09d1b950c 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -3,6 +3,7 @@ ### 4.3.0b5 (Unreleased) #### Breaking Changes +- Method signatures have been updated to use keyword arguments instead of positional arguments for most method options in the async client. - Bugfix: Automatic Id generation for items was turned on for `upsert_items()` method when no 'id' value was present in document body. Method call will now require an 'id' field to be present in the document body. @@ -10,6 +11,7 @@ Method call will now require an 'id' field to be present in the document body. - Marked the GetAuthorizationMethod for deprecation since it will no longer be public in a future release. - Added samples showing how to configure retry options for both the sync and async clients. - Deprecated the `connection_retry_policy` and `retry_options` options in the sync client. +- Added user warning to non-query methods trying to use `populate_query_metrics` options. ### 4.3.0b4 (2022-04-07) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/container.py b/sdk/cosmos/azure-cosmos/azure/cosmos/container.py index 9f075eba64d5..18b22c8e2505 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/container.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/container.py @@ -22,8 +22,10 @@ """Create, read, update and delete items in the Azure Cosmos DB SQL API service. """ -from typing import Any, Dict, List, Optional, Union, Iterable, cast # pylint: disable=unused-import +from typing import Any, Dict, List, Optional, Union, Iterable, cast, overload # pylint: disable=unused-import + +import warnings from azure.core.tracing.decorator import distributed_trace # type: ignore from ._cosmos_client_connection import CosmosClientConnection @@ -108,21 +110,29 @@ def _set_partition_key(self, partition_key): return CosmosClientConnection._return_undefined_or_empty_partition_key(self.is_system_key) return partition_key + @overload + def read( + self, + *, + populate_partition_key_range_statistics: Optional[bool] = None, + populate_quota_info: Optional[bool] = None, + **kwargs + ): + ... + + @distributed_trace def read( self, - populate_query_metrics=None, # type: Optional[bool] - populate_partition_key_range_statistics=None, # type: Optional[bool] - populate_quota_info=None, # type: Optional[bool] + *args, **kwargs # type: Any ): # type: (...) -> Dict[str, Any] """Read the container properties. - :param populate_query_metrics: Enable returning query metrics in response headers. - :param populate_partition_key_range_statistics: Enable returning partition key + :keyword bool populate_partition_key_range_statistics: Enable returning partition key range statistics in response headers. - :param populate_quota_info: Enable returning collection storage quota information in response headers. + :keyword bool populate_quota_info: Enable returning collection storage quota information in response headers. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. :keyword Callable response_hook: A callable invoked with the response metadata. @@ -133,8 +143,15 @@ def read( """ request_options = build_options(kwargs) response_hook = kwargs.pop('response_hook', None) - if populate_query_metrics is not None: - request_options["populateQueryMetrics"] = populate_query_metrics + populate_query_metrics = args[0] if args else kwargs.pop('populate_query_metrics', None) + if populate_query_metrics: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) + populate_partition_key_range_statistics = args[1] if args and len(args) > 0 else kwargs.pop( + "populate_partition_key_range_statistics", None) + populate_quota_info = args[2] if args and len(args) > 1 else kwargs.pop("populate_quota_info", None) if populate_partition_key_range_statistics is not None: request_options["populatePartitionKeyRangeStatistics"] = populate_partition_key_range_statistics if populate_quota_info is not None: @@ -164,7 +181,6 @@ def read_item( :param item: The ID (name) or dict representing item to retrieve. :param partition_key: Partition key for the item to retrieve. - :param populate_query_metrics: Enable returning query metrics in response headers. :param post_trigger_include: trigger id to be used as post operation trigger. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. @@ -195,6 +211,10 @@ def read_item( if partition_key is not None: request_options["partitionKey"] = self._set_partition_key(partition_key) if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics if post_trigger_include is not None: request_options["postTriggerInclude"] = post_trigger_include @@ -219,7 +239,6 @@ def read_all_items( """List all the items in the container. :param max_item_count: Max number of items to be returned in the enumeration operation. - :param populate_query_metrics: Enable returning query metrics in response headers. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. :keyword Callable response_hook: A callable invoked with the response metadata. @@ -236,6 +255,10 @@ def read_all_items( if max_item_count is not None: feed_options["maxItemCount"] = max_item_count if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) feed_options["populateQueryMetrics"] = populate_query_metrics max_integrated_cache_staleness_in_ms = kwargs.pop('max_integrated_cache_staleness_in_ms', None) if max_integrated_cache_staleness_in_ms: @@ -409,7 +432,6 @@ def replace_item( :param item: The ID (name) or dict representing item to be replaced. :param body: A dict-like object representing the item to replace. - :param populate_query_metrics: Enable returning query metrics in response headers. :param pre_trigger_include: trigger id to be used as pre operation trigger. :param post_trigger_include: trigger id to be used as post operation trigger. :keyword str session_token: Token for use with Session consistency. @@ -428,6 +450,10 @@ def replace_item( response_hook = kwargs.pop('response_hook', None) request_options["disableAutomaticIdGeneration"] = True if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics if pre_trigger_include is not None: request_options["preTriggerInclude"] = pre_trigger_include @@ -457,7 +483,6 @@ def upsert_item( does not already exist, it is inserted. :param body: A dict-like object representing the item to update or insert. - :param populate_query_metrics: Enable returning query metrics in response headers. :param pre_trigger_include: trigger id to be used as pre operation trigger. :param post_trigger_include: trigger id to be used as post operation trigger. :keyword str session_token: Token for use with Session consistency. @@ -474,6 +499,10 @@ def upsert_item( response_hook = kwargs.pop('response_hook', None) request_options["disableAutomaticIdGeneration"] = True if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics if pre_trigger_include is not None: request_options["preTriggerInclude"] = pre_trigger_include @@ -507,7 +536,6 @@ def create_item( :func:`ContainerProxy.upsert_item` method. :param body: A dict-like object representing the item to create. - :param populate_query_metrics: Enable returning query metrics in response headers. :param pre_trigger_include: trigger id to be used as pre operation trigger. :param post_trigger_include: trigger id to be used as post operation trigger. :param indexing_directive: Indicate whether the document should be omitted from indexing. @@ -527,6 +555,10 @@ def create_item( request_options["disableAutomaticIdGeneration"] = not kwargs.pop('enable_automatic_id_generation', False) if populate_query_metrics: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics if pre_trigger_include is not None: request_options["preTriggerInclude"] = pre_trigger_include @@ -559,7 +591,6 @@ def delete_item( :param item: The ID (name) or dict representing item to be deleted. :param partition_key: Specifies the partition key value for the item. - :param populate_query_metrics: Enable returning query metrics in response headers. :param pre_trigger_include: trigger id to be used as pre operation trigger. :param post_trigger_include: trigger id to be used as post operation trigger. :keyword str session_token: Token for use with Session consistency. @@ -577,6 +608,10 @@ def delete_item( if partition_key is not None: request_options["partitionKey"] = self._set_partition_key(partition_key) if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics if pre_trigger_include is not None: request_options["preTriggerInclude"] = pre_trigger_include diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py index 3592f9d5a935..4169d6308ad4 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py @@ -238,7 +238,6 @@ def create_database( # pylint: disable=redefined-builtin Create a new database with the given ID (name). :param id: ID (name) of the database to create. - :param bool populate_query_metrics: Enable returning query metrics in response headers. :param int offer_throughput: The provisioned throughput for this offer. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. @@ -264,6 +263,10 @@ def create_database( # pylint: disable=redefined-builtin request_options = build_options(kwargs) response_hook = kwargs.pop('response_hook', None) if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics if offer_throughput is not None: request_options["offerThroughput"] = offer_throughput @@ -350,7 +353,6 @@ def list_databases( """List the databases in a Cosmos DB SQL database account. :param int max_item_count: Max number of items to be returned in the enumeration operation. - :param bool populate_query_metrics: Enable returning query metrics in response headers. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. :keyword Callable response_hook: A callable invoked with the response metadata. @@ -362,6 +364,10 @@ def list_databases( if max_item_count is not None: feed_options["maxItemCount"] = max_item_count if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) feed_options["populateQueryMetrics"] = populate_query_metrics result = self.client_connection.ReadDatabases(options=feed_options, **kwargs) @@ -387,7 +393,6 @@ def query_databases( :param bool enable_cross_partition_query: Allow scan on the queries which couldn't be served as indexing was opted out on the requested paths. :param int max_item_count: Max number of items to be returned in the enumeration operation. - :param bool populate_query_metrics: Enable returning query metrics in response headers. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. :keyword Callable response_hook: A callable invoked with the response metadata. @@ -401,6 +406,10 @@ def query_databases( if max_item_count is not None: feed_options["maxItemCount"] = max_item_count if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) feed_options["populateQueryMetrics"] = populate_query_metrics if query: @@ -430,7 +439,6 @@ def delete_database( :param database: The ID (name), dict representing the properties or :class:`DatabaseProxy` instance of the database to delete. :type database: str or dict(str, str) or ~azure.cosmos.DatabaseProxy - :param bool populate_query_metrics: Enable returning query metrics in response headers. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. :keyword str etag: An ETag value, or the wildcard character (*). Used to check if the resource @@ -443,6 +451,10 @@ def delete_database( request_options = build_options(kwargs) response_hook = kwargs.pop('response_hook', None) if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics database_link = self._get_database_link(database) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/database.py b/sdk/cosmos/azure-cosmos/azure/cosmos/database.py index 566f00cfcb0c..bd04625c7a16 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/database.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/database.py @@ -119,7 +119,6 @@ def read(self, populate_query_metrics=None, **kwargs): # type: (Optional[bool], Any) -> Dict[str, Any] """Read the database properties. - :param bool populate_query_metrics: Enable returning query metrics in response headers. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. :keyword Callable response_hook: A callable invoked with the response metadata. @@ -133,6 +132,10 @@ def read(self, populate_query_metrics=None, **kwargs): request_options = build_options(kwargs) response_hook = kwargs.pop('response_hook', None) if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics self._properties = self.client_connection.ReadDatabase( @@ -166,7 +169,6 @@ def create_container( :param partition_key: The partition key to use for the container. :param indexing_policy: The indexing policy to apply to the container. :param default_ttl: Default time to live (TTL) for items in the container. If unspecified, items do not expire. - :param populate_query_metrics: Enable returning query metrics in response headers. :param offer_throughput: The provisioned throughput for this offer. :param unique_key_policy: The unique key policy to apply to the container. :param conflict_resolution_policy: The conflict resolution policy to apply to the container. @@ -225,6 +227,10 @@ def create_container( request_options = build_options(kwargs) response_hook = kwargs.pop('response_hook', None) if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics if offer_throughput is not None: request_options["offerThroughput"] = offer_throughput @@ -314,7 +320,6 @@ def delete_container( :param container: The ID (name) of the container to delete. You can either pass in the ID of the container to delete, a :class:`ContainerProxy` instance or a dict representing the properties of the container. - :param populate_query_metrics: Enable returning query metrics in response headers. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. :keyword str etag: An ETag value, or the wildcard character (*). Used to check if the resource @@ -327,6 +332,10 @@ def delete_container( request_options = build_options(kwargs) response_hook = kwargs.pop('response_hook', None) if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics collection_link = self._get_container_link(container) @@ -369,7 +378,6 @@ def list_containers(self, max_item_count=None, populate_query_metrics=None, **kw """List the containers in the database. :param max_item_count: Max number of items to be returned in the enumeration operation. - :param populate_query_metrics: Enable returning query metrics in response headers. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. :keyword Callable response_hook: A callable invoked with the response metadata. @@ -391,6 +399,10 @@ def list_containers(self, max_item_count=None, populate_query_metrics=None, **kw if max_item_count is not None: feed_options["maxItemCount"] = max_item_count if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) feed_options["populateQueryMetrics"] = populate_query_metrics result = self.client_connection.ReadContainers( @@ -415,7 +427,6 @@ def query_containers( :param query: The Azure Cosmos DB SQL query to execute. :param parameters: Optional array of parameters to the query. Ignored if no query is provided. :param max_item_count: Max number of items to be returned in the enumeration operation. - :param populate_query_metrics: Enable returning query metrics in response headers. :keyword str session_token: Token for use with Session consistency. :keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request. :keyword Callable response_hook: A callable invoked with the response metadata. @@ -427,6 +438,10 @@ def query_containers( if max_item_count is not None: feed_options["maxItemCount"] = max_item_count if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) feed_options["populateQueryMetrics"] = populate_query_metrics result = self.client_connection.QueryContainers( @@ -488,6 +503,10 @@ def replace_container( request_options = build_options(kwargs) response_hook = kwargs.pop('response_hook', None) if populate_query_metrics is not None: + warnings.warn( + "the populate_query_metrics flag does not apply to this method and will be removed in the future", + UserWarning, + ) request_options["populateQueryMetrics"] = populate_query_metrics container_id = self._get_container_id(container) diff --git a/sdk/cosmos/azure-cosmos/test/test_backwards_compatibility.py b/sdk/cosmos/azure-cosmos/test/test_backwards_compatibility.py new file mode 100644 index 000000000000..23550d2e47da --- /dev/null +++ b/sdk/cosmos/azure-cosmos/test/test_backwards_compatibility.py @@ -0,0 +1,113 @@ +# The MIT License (MIT) +# Copyright (c) 2022 Microsoft Corporation + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import unittest +import pytest +from azure.cosmos import cosmos_client, PartitionKey, http_constants +import test_config +from unittest.mock import MagicMock + + +# This class tests the backwards compatibility of features being deprecated to ensure users are not broken before +# properly removing the methods marked for deprecation. + +pytestmark = pytest.mark.cosmosEmulator + + +@pytest.mark.usefixtures("teardown") +class TestBackwardsCompatibility(unittest.TestCase): + + configs = test_config._test_config + host = configs.host + masterKey = configs.masterKey + + populate_true = True + + @classmethod + def setUpClass(cls): + if cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_ENDPOINT_HERE]': + raise Exception( + "You must specify your Azure Cosmos account values for " + "'masterKey' and 'host' at the top of this class to run the " + "tests.") + cls.client = cosmos_client.CosmosClient(cls.host, cls.masterKey, consistency_level="Session") + cls.databaseForTest = cls.client.create_database_if_not_exists(cls.configs.TEST_DATABASE_ID, + offer_throughput=500) + cls.containerForTest = cls.databaseForTest.create_container_if_not_exists( + cls.configs.TEST_COLLECTION_SINGLE_PARTITION_ID, PartitionKey(path="/id"), offer_throughput=400) + + def side_effect_populate_partition_key_range_statistics(self, *args, **kwargs): + # Extract request headers from args + self.assertTrue(args[2][http_constants.HttpHeaders.PopulatePartitionKeyRangeStatistics] is True) + raise StopIteration + + def side_effect_populate_query_metrics(self, *args, **kwargs): + # Extract request headers from args + self.assertTrue(args[2][http_constants.HttpHeaders.PopulateQueryMetrics] is True) + raise StopIteration + + def side_effect_populate_quota_info(self, *args, **kwargs): + # Extract request headers from args + self.assertTrue(args[2][http_constants.HttpHeaders.PopulateQuotaInfo] is True) + raise StopIteration + + def test_populate_query_metrics(self): + cosmos_client_connection = self.containerForTest.client_connection + cosmos_client_connection._CosmosClientConnection__Get = MagicMock( + side_effect=self.side_effect_populate_query_metrics) + try: + self.containerForTest.read(populate_query_metrics=True) + except StopIteration: + pass + try: + self.containerForTest.read(True) + except StopIteration: + pass + + def test_populate_quota_info(self): + cosmos_client_connection = self.containerForTest.client_connection + cosmos_client_connection._CosmosClientConnection__Get = MagicMock( + side_effect=self.side_effect_populate_quota_info) + try: + self.containerForTest.read(populate_quota_info=True) + except StopIteration: + pass + try: + self.containerForTest.read(False, True) + except StopIteration: + pass + + def test_populate_partition_key_range_statistics(self): + cosmos_client_connection = self.containerForTest.client_connection + cosmos_client_connection._CosmosClientConnection__Get = MagicMock( + side_effect=self.side_effect_populate_partition_key_range_statistics) + try: + self.containerForTest.read(populate_partition_key_range_statistics=True) + except StopIteration: + pass + try: + self.containerForTest.read(False, False, True) + except StopIteration: + pass + + +if __name__ == "__main__": + unittest.main()