From 51664b6c05d319f20faf5767ad91299a3f18a9fa Mon Sep 17 00:00:00 2001 From: simorenoh Date: Wed, 26 Jan 2022 19:22:54 -0500 Subject: [PATCH 01/23] consistency level gets set to default found in database account --- sdk/cosmos/azure-cosmos/CHANGELOG.md | 6 +++ .../azure-cosmos/azure/cosmos/_constants.py | 1 + .../azure/cosmos/_cosmos_client_connection.py | 54 +++++++++++++------ .../azure/cosmos/cosmos_client.py | 2 +- .../azure-cosmos/test/test_user_configs.py | 25 ++++++++- 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index ab22f323bcf2..b6607ae3fa7a 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -1,5 +1,11 @@ ## Release History +### 4.3.0b3 (Unreleased) + +### Other changes +- Default consistency level for the sync and async clients is no longer "Session" and will instead be set to the + consistency level of the cosmos account on initialization. + ### 4.3.0b2 (2022-01-25) This version and all future versions will require Python 3.6+. Python 2.7 is no longer supported. diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/_constants.py b/sdk/cosmos/azure-cosmos/azure/cosmos/_constants.py index f6ab055fe159..2d916bd737be 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/_constants.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/_constants.py @@ -27,6 +27,7 @@ class _Constants(object): """Constants used in the azure-cosmos package""" UserConsistencyPolicy = "userConsistencyPolicy" + DefaultConsistencyLevel = "defaultConsistencyLevel" # GlobalDB related constants WritableLocations = "writableLocations" diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py index d5d4ddbc8cb0..f5883d820615 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py @@ -26,7 +26,7 @@ """ # https://github.com/PyCQA/pylint/issues/3112 # Currently pylint is locked to 2.3.3 and this is fixed in 2.4.4 -from typing import Dict, Any, Optional # pylint: disable=unused-import +from typing import Dict, Any, Optional, TypeVar # pylint: disable=unused-import import urllib.parse from urllib3.util.retry import Retry from azure.core.paging import ItemPaged # type: ignore @@ -45,7 +45,7 @@ from . import _base as base from . import documents -from .documents import ConnectionPolicy +from .documents import ConnectionPolicy, DatabaseAccount from . import _constants as constants from . import http_constants from . import _query_iterable as query_iterable @@ -59,7 +59,7 @@ from . import _utils from .partition_key import _Undefined, _Empty - +ClassType = TypeVar("ClassType") # pylint: disable=protected-access @@ -92,7 +92,7 @@ def __init__( url_connection, # type: str auth, # type: Dict[str, Any] connection_policy=None, # type: Optional[ConnectionPolicy] - consistency_level=documents.ConsistencyLevel.Session, # type: str + consistency_level=None, # type: Optional[str] **kwargs # type: Any ): # type: (...) -> None @@ -139,20 +139,9 @@ def __init__( http_constants.HttpHeaders.IsContinuationExpected: False, } - if consistency_level is not None: - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level - # Keeps the latest response headers from server. self.last_response_headers = None - if consistency_level == documents.ConsistencyLevel.Session: - # create a session - this is maintained only if the default consistency level - # on the client is set to session, or if the user explicitly sets it as a property - # via setter - self.session = _session.Session(self.url_connection) - else: - self.session = None # type: ignore - self._useMultipleWriteLocations = False self._global_endpoint_manager = global_endpoint_manager._GlobalEndpointManager(self) @@ -207,9 +196,16 @@ def __init__( # Routing map provider self._routing_map_provider = routing_map_provider.SmartRoutingMapProvider(self) + # Set initial consistency for single _GetDatabaseAccount request + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Strong database_account = self._global_endpoint_manager._GetDatabaseAccount(**kwargs) self._global_endpoint_manager.force_refresh(database_account) + print(self.default_headers) + + # Use database_account to verify consistency level to be used + self._check_and_set_consistency_level(database_account, consistency_level) + @property def Session(self): """Gets the session object from the client. """ @@ -2592,3 +2588,31 @@ def _return_undefined_or_empty_partition_key(is_system_key): if is_system_key: return _Empty return _Undefined + + def _check_and_set_consistency_level( + self, + database_account: ClassType, + consistency_level: Optional[str] = None, + ) -> None: + """Checks if consistency level param was passed in by user and sets it to that value or to the account default. + + :param database_account: The database account to be used to check consistency levels + :type database_account: ~azure.cosmos.documents.DatabaseAccount + :param consistency_level: The consistency level passed in by the user for the client + :type consistency_level: Optional[str] + :rtype: None + """ + if consistency_level is None: + # If no consistency level was passed in by user, set to default level present in account + user_consistency_policy = database_account.ConsistencyPolicy + consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) + + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level + + if consistency_level == documents.ConsistencyLevel.Session: + # create a session - this is maintained only if the default consistency level + # on the client is set to session, or if the user explicitly sets it as a property + # via setter + self.session = _session.Session(self.url_connection) + else: + self.session = None # type: ignore diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py index b7605d38607e..6b86b8e2c7de 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py @@ -159,7 +159,7 @@ class CosmosClient(object): :name: create_client """ - def __init__(self, url, credential, consistency_level="Session", **kwargs): + def __init__(self, url, credential, consistency_level=None, **kwargs): # type: (str, Any, str, Any) -> None """Instantiate a new CosmosClient.""" auth = _build_auth(credential) diff --git a/sdk/cosmos/azure-cosmos/test/test_user_configs.py b/sdk/cosmos/azure-cosmos/test/test_user_configs.py index 4100c105fd4b..30bbe407fdc4 100644 --- a/sdk/cosmos/azure-cosmos/test/test_user_configs.py +++ b/sdk/cosmos/azure-cosmos/test/test_user_configs.py @@ -22,16 +22,17 @@ import unittest import azure.cosmos.cosmos_client as cosmos_client +from azure.cosmos import http_constants import pytest from test_config import _test_config - # This test class serves to test user-configurable options and verify they are # properly set and saved into the different object instances that use these # user-configurable settings. pytestmark = pytest.mark.cosmosEmulator + @pytest.mark.usefixtures("teardown") class TestUserConfigs(unittest.TestCase): @@ -46,5 +47,27 @@ def test_enable_endpoint_discovery(self): self.assertTrue(client_default.client_connection.connection_policy.EnableEndpointDiscovery) self.assertTrue(client_true.client_connection.connection_policy.EnableEndpointDiscovery) + def test_default_account_consistency(self): + # These tests use the emulator, which has a default consistency of "Session" + # If your account has a different level of consistency, make sure it's not the same as the custom_level below + + client = cosmos_client.CosmosClient(url=_test_config.host, credential=_test_config.masterKey) + database_account = client.get_database_account() + account_consistency_level = database_account.ConsistencyPolicy["defaultConsistencyLevel"] + self.assertEqual(client.client_connection.default_headers[http_constants.HttpHeaders.ConsistencyLevel], + account_consistency_level) + + # Now testing a user-defined consistency level as opposed to using the account one + custom_level = "Strong" + client = cosmos_client.CosmosClient(url=_test_config.host, credential=_test_config.masterKey, + consistency_level=custom_level) + database_account = client.get_database_account() + account_consistency_level = database_account.ConsistencyPolicy["defaultConsistencyLevel"] + # Here they're not equal, since the headers being used make the client use a different level of consistency + self.assertNotEqual( + client.client_connection.default_headers[http_constants.HttpHeaders.ConsistencyLevel], + account_consistency_level) + + if __name__ == "__main__": unittest.main() From edb78a4be5ccff84ed78efd5d009ef3b19bea7c5 Mon Sep 17 00:00:00 2001 From: simorenoh Date: Wed, 26 Jan 2022 23:11:04 -0500 Subject: [PATCH 02/23] async client default change --- .../azure/cosmos/_cosmos_client_connection.py | 2 +- .../aio/_cosmos_client_connection_async.py | 56 ++++++++++++++----- .../azure/cosmos/aio/cosmos_client.py | 5 +- .../azure/cosmos/cosmos_client.py | 2 +- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py index f5883d820615..9a436f43a230 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py @@ -45,7 +45,7 @@ from . import _base as base from . import documents -from .documents import ConnectionPolicy, DatabaseAccount +from .documents import ConnectionPolicy from . import _constants as constants from . import http_constants from . import _query_iterable as query_iterable diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py index e0236292d0f8..95b0ddf8d424 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py @@ -26,7 +26,7 @@ """ # https://github.com/PyCQA/pylint/issues/3112 # Currently pylint is locked to 2.3.3 and this is fixed in 2.4.4 -from typing import Dict, Any, Optional # pylint: disable=unused-import +from typing import Dict, Any, Optional, TypeVar # pylint: disable=unused-import from urllib.parse import urlparse from urllib3.util.retry import Retry from azure.core.async_paging import AsyncItemPaged @@ -59,6 +59,7 @@ from .. import _utils from ..partition_key import _Undefined, _Empty +ClassType = TypeVar("ClassType") # pylint: disable=protected-access class CosmosClientConnection(object): # pylint: disable=too-many-public-methods,too-many-instance-attributes @@ -90,7 +91,7 @@ def __init__( url_connection, # type: str auth, # type: Dict[str, Any] connection_policy=None, # type: Optional[ConnectionPolicy] - consistency_level=documents.ConsistencyLevel.Session, # type: str + consistency_level=None, # type: Optional[str] **kwargs # type: Any ): # type: (...) -> None @@ -137,20 +138,12 @@ def __init__( http_constants.HttpHeaders.IsContinuationExpected: False, } - if consistency_level is not None: - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level + print("initial consistency level: {}".format(consistency_level)) # Keeps the latest response headers from server. self.last_response_headers = None - if consistency_level == documents.ConsistencyLevel.Session: - # create a session - this is maintained only if the default consistency level - # on the client is set to session, or if the user explicitly sets it as a property - # via setter - self.session = _session.Session(self.url_connection) - else: - self.session = None # type: ignore - self._useMultipleWriteLocations = False self._global_endpoint_manager = global_endpoint_manager_async._GlobalEndpointManager(self) @@ -232,10 +225,47 @@ def _ReadEndpoint(self): return self._global_endpoint_manager.get_read_endpoint() async def _setup(self): + # Save the choice that was made (either None or some value) and branch to set or get the consistency + user_defined_consistency = self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] + # Set default header setting to any consistency level for the first request to _GetDatabaseAccount below + if user_defined_consistency is None: + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Strong + + if user_defined_consistency == documents.ConsistencyLevel.Session: + # create a Session if the user wants Session consistency + self.session = _session.Session(self.url_connection) + else: + self.session = None # type: ignore + if 'database_account' not in self._setup_kwargs: - self._setup_kwargs['database_account'] = await self._global_endpoint_manager._GetDatabaseAccount( + database_account = await self._global_endpoint_manager._GetDatabaseAccount( **self._setup_kwargs) + self._setup_kwargs['database_account'] = database_account await self._global_endpoint_manager.force_refresh(self._setup_kwargs['database_account']) + self._check_and_set_consistency_level(database_account, user_defined_consistency) + + def _check_and_set_consistency_level( + self, + database_account: ClassType, + consistency_level: Optional[str] = None, + ) -> None: + """Checks if consistency level param was passed in by user and sets it to that value or to the account default. + :param database_account: The database account to be used to check consistency levels + :type database_account: ~azure.cosmos.documents.DatabaseAccount + :param consistency_level: The consistency level passed in by the user for the client + :type consistency_level: Optional[str] + :rtype: None + """ + if consistency_level is None: + # If no consistency level was passed in by user, set to default level present in account + user_consistency_policy = database_account.ConsistencyPolicy + consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) + if consistency_level is documents.ConsistencyLevel.Session: + # If None was passed in but DatabaseAccount has Session consistency level, start a Session + self.session = _session.Session() + # Set header to be used as final consistency for client + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level + def _GetDatabaseIdWithPathForUser(self, database_link, user): # pylint: disable=no-self-use CosmosClientConnection.__ValidateResource(user) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py index f46e7f1aa620..e5f6fba340cd 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py @@ -96,7 +96,7 @@ class CosmosClient(object): :param str url: The URL of the Cosmos DB account. :param credential: Can be the account key, or a dictionary of resource tokens. :type credential: str or dict[str, str] - :keyword str consistency_level: Consistency level to use for the session. The default value is "Session". + :param str consistency_level: Consistency level to use for the session. The default value is None (Account level). .. admonition:: Example: @@ -109,11 +109,10 @@ class CosmosClient(object): :name: create_client """ - def __init__(self, url, credential, **kwargs): + def __init__(self, url, credential, consistency_level=None, **kwargs): # type: (str, Any, str, Any) -> None """Instantiate a new CosmosClient.""" auth = _build_auth(credential) - consistency_level = kwargs.get('consistency_level', 'Session') connection_policy = _build_connection_policy(kwargs) self.client_connection = CosmosClientConnection( url, diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py index 6b86b8e2c7de..c33ffd864d28 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py @@ -126,7 +126,7 @@ class CosmosClient(object): :param str url: The URL of the Cosmos DB account. :param credential: Can be the account key, or a dictionary of resource tokens. :type credential: str or dict[str, str] - :param str consistency_level: Consistency level to use for the session. The default value is "Session". + :param str consistency_level: Consistency level to use for the session. The default value is None (Account level). :keyword int timeout: An absolute timeout in seconds, for the combined HTTP request and response processing. :keyword int request_timeout: The HTTP request timeout in milliseconds. :keyword str connection_mode: The connection mode for the client - currently only supports 'Gateway'. From fd98dae6fb1fbaca2699d1575d865cef8771d6a1 Mon Sep 17 00:00:00 2001 From: simorenoh Date: Wed, 26 Jan 2022 23:33:58 -0500 Subject: [PATCH 03/23] updated docs based on finding and updated samples to reflect best practices --- sdk/cosmos/azure-cosmos/README.md | 17 +- .../azure/cosmos/aio/cosmos_client.py | 2 +- .../azure/cosmos/cosmos_client.py | 2 +- .../azure-cosmos/samples/examples_async.py | 278 +++++++++--------- .../samples/index_management_async.py | 43 ++- ...npartitioned_container_operations_async.py | 68 ++--- 6 files changed, 211 insertions(+), 199 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/README.md b/sdk/cosmos/azure-cosmos/README.md index c0cba8d0cd54..96dfd0c9eb8b 100644 --- a/sdk/cosmos/azure-cosmos/README.md +++ b/sdk/cosmos/azure-cosmos/README.md @@ -439,6 +439,7 @@ For more information on TTL, see [Time to Live for Azure Cosmos DB data][cosmos_ ### Using the asynchronous client (Preview) The asynchronous cosmos client is a separate client that looks and works in a similar fashion to the existing synchronous client. However, the async client needs to be imported separately and its methods need to be used with the async/await keywords. +Due to its asynchronous nature, it also needs to be warmed up and then closed down when used. The example below shows how by using the client's __aenter__() and close() methods. ```Python from azure.cosmos.aio import CosmosClient @@ -446,13 +447,14 @@ import os URL = os.environ['ACCOUNT_URI'] KEY = os.environ['ACCOUNT_KEY'] -client = CosmosClient(URL, credential=KEY) -DATABASE_NAME = 'testDatabase' -database = client.get_database_client(DATABASE_NAME) -CONTAINER_NAME = 'products' -container = database.get_container_client(CONTAINER_NAME) -async def create_items(): +async def main(): + client = CosmosClient(URL, credential=KEY) + await client.__aenter__() + DATABASE_NAME = 'testDatabase' + database = client.get_database_client(DATABASE_NAME) + CONTAINER_NAME = 'products' + container = database.get_container_client(CONTAINER_NAME) for i in range(10): await container.upsert_item({ 'id': 'item{0}'.format(i), @@ -463,7 +465,8 @@ async def create_items(): await client.close() # the async client must be closed manually if it's not initialized in a with statement ``` -It is also worth pointing out that the asynchronous client has to be closed manually after its use, either by initializing it using async with or calling the close() method directly like shown above. +However, instead of taking care of it manually with those extra lines, you can use the `async with` keywords. This creates a context manager that will warm up, initialize and later clean up +and close the client once you're out of the statement. The example below shows how to start the client this way. ```Python from azure.cosmos.aio import CosmosClient diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py index e5f6fba340cd..4194ef260f97 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py @@ -110,7 +110,7 @@ class CosmosClient(object): """ def __init__(self, url, credential, consistency_level=None, **kwargs): - # type: (str, Any, str, Any) -> None + # type: (str, Any, Optional[str], Any) -> None """Instantiate a new CosmosClient.""" auth = _build_auth(credential) connection_policy = _build_connection_policy(kwargs) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py index c33ffd864d28..a02c42cac288 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py @@ -160,7 +160,7 @@ class CosmosClient(object): """ def __init__(self, url, credential, consistency_level=None, **kwargs): - # type: (str, Any, str, Any) -> None + # type: (str, Any, Optional[str], Any) -> None """Instantiate a new CosmosClient.""" auth = _build_auth(credential) connection_policy = _build_connection_policy(kwargs) diff --git a/sdk/cosmos/azure-cosmos/samples/examples_async.py b/sdk/cosmos/azure-cosmos/samples/examples_async.py index efeab8375f58..e0c1693e3da7 100644 --- a/sdk/cosmos/azure-cosmos/samples/examples_async.py +++ b/sdk/cosmos/azure-cosmos/samples/examples_async.py @@ -6,143 +6,157 @@ url = os.environ["ACCOUNT_URI"] key = os.environ["ACCOUNT_KEY"] + async def examples_async(): # All interaction with Cosmos DB starts with an instance of the CosmosClient # In order to use the asynchronous client, we need to use async/await keywords, # which can only be used within async methods like examples_async() here + # Since this is an asynchronous client, in order to properly use it you also have to warm it up and close it down. + # One way to do it would be like below (all of these statements would be necessary if you want to do it this way). + + async_client = CosmosClient(url, key) + await async_client.__aenter__() + + # [CODE LOGIC HERE, CLOSING WITH THE STATEMENT BELOW WHEN DONE] + + await async_client.close() + + # Or better, you can use the `async with` keywords like below to start your clients - these keywords + # create a context manager that automatically warms up, initializes, and cleans up the client, so you don't have to. + # [START create_client] - client = CosmosClient(url, key) - # [END create_client] - - # Create a database in the account using the CosmosClient, - # specifying that the operation shouldn't throw an exception - # if a database with the given ID already exists. - # [START create_database] - database_name = "testDatabase" - try: - database = await client.create_database(id=database_name) - except exceptions.CosmosResourceExistsError: - database = client.get_database_client(database_id=database_name) - # [END create_database] - - # Create a container, handling the exception if a container with the - # same ID (name) already exists in the database. - # [START create_container] - container_name = "products" - try: - container = await database.create_container( - id=container_name, partition_key=PartitionKey(path="/productName") - ) - except exceptions.CosmosResourceExistsError: + async with CosmosClient(url, key) as client: + # [END create_client] + + # Create a database in the account using the CosmosClient, + # specifying that the operation shouldn't throw an exception + # if a database with the given ID already exists. + # [START create_database] + database_name = "testDatabase" + try: + database = await client.create_database(id=database_name) + except exceptions.CosmosResourceExistsError: + database = client.get_database_client(database_id=database_name) + # [END create_database] + + # Create a container, handling the exception if a container with the + # same ID (name) already exists in the database. + # [START create_container] + container_name = "products" + try: + container = await database.create_container( + id=container_name, partition_key=PartitionKey(path="/productName") + ) + except exceptions.CosmosResourceExistsError: + container = database.get_container_client(container_name) + # [END create_container] + + # Create a container with custom settings. This example + # creates a container with a custom partition key. + # [START create_container_with_settings] + customer_container_name = "customers" + try: + customer_container = await database.create_container( + id=customer_container_name, + partition_key=PartitionKey(path="/city"), + default_ttl=200, + ) + except exceptions.CosmosResourceExistsError: + customer_container = database.get_container_client(customer_container_name) + # [END create_container_with_settings] + + # Retrieve a container by walking down the resource hierarchy + # (client->database->container), handling the exception generated + # if no container with the specified ID was found in the database. + # [START get_container] + database = client.get_database_client(database_name) container = database.get_container_client(container_name) - # [END create_container] - - # Create a container with custom settings. This example - # creates a container with a custom partition key. - # [START create_container_with_settings] - customer_container_name = "customers" - try: - customer_container = await database.create_container( - id=customer_container_name, - partition_key=PartitionKey(path="/city"), - default_ttl=200, - ) - except exceptions.CosmosResourceExistsError: - customer_container = database.get_container_client(customer_container_name) - # [END create_container_with_settings] - - # Retrieve a container by walking down the resource hierarchy - # (client->database->container), handling the exception generated - # if no container with the specified ID was found in the database. - # [START get_container] - database = client.get_database_client(database_name) - container = database.get_container_client(container_name) - # [END get_container] - - # [START list_containers] - database = client.get_database_client(database_name) - for container in database.list_containers(): - print("Container ID: {}".format(container['id'])) - # [END list_containers] - - # Insert new items by defining a dict and calling Container.upsert_item - # [START upsert_items] - container = database.get_container_client(container_name) - for i in range(1, 10): - await container.upsert_item( - dict(id="item{}".format(i), productName="Widget", productModel="Model {}".format(i)) + # [END get_container] + + # [START list_containers] + database = client.get_database_client(database_name) + for container in database.list_containers(): + print("Container ID: {}".format(container['id'])) + # [END list_containers] + + # Insert new items by defining a dict and calling Container.upsert_item + # [START upsert_items] + container = database.get_container_client(container_name) + for i in range(1, 10): + await container.upsert_item( + dict(id="item{}".format(i), productName="Widget", productModel="Model {}".format(i)) + ) + # [END upsert_items] + + # Modify an existing item in the container + # [START update_item] + item = await container.read_item("item2", partition_key="Widget") + item["productModel"] = "DISCONTINUED" + updated_item = await container.upsert_item(item) + # [END update_item] + + # Query the items in a container using SQL-like syntax. This example + # gets all items whose product model hasn't been discontinued. + # The asynchronous client returns asynchronous iterators for its query methods; + # as such, we iterate over it by using an async for loop + # [START query_items] + import json + + async for item in container.query_items( + query='SELECT * FROM products p WHERE p.productModel <> "DISCONTINUED"', + enable_cross_partition_query=True, + ): + print(json.dumps(item, indent=True)) + # [END query_items] + + # Parameterized queries are also supported. This example + # gets all items whose product model has been discontinued. + # [START query_items_param] + discontinued_items = container.query_items( + query='SELECT * FROM products p WHERE p.productModel = @model AND p.productName="Widget"', + parameters=[dict(name="@model", value="DISCONTINUED")], ) - # [END upsert_items] - - # Modify an existing item in the container - # [START update_item] - item = await container.read_item("item2", partition_key="Widget") - item["productModel"] = "DISCONTINUED" - updated_item = await container.upsert_item(item) - # [END update_item] - - # Query the items in a container using SQL-like syntax. This example - # gets all items whose product model hasn't been discontinued. - # The asynchronous client returns asynchronous iterators for its query methods; - # as such, we iterate over it by using an async for loop - # [START query_items] - import json - - async for item in container.query_items( - query='SELECT * FROM products p WHERE p.productModel <> "DISCONTINUED"', - enable_cross_partition_query=True, - ): - print(json.dumps(item, indent=True)) - # [END query_items] - - # Parameterized queries are also supported. This example - # gets all items whose product model has been discontinued. - # [START query_items_param] - discontinued_items = container.query_items( - query='SELECT * FROM products p WHERE p.productModel = @model AND p.productName="Widget"', - parameters=[dict(name="@model", value="DISCONTINUED")], - ) - async for item in discontinued_items: - print(json.dumps(item, indent=True)) - # [END query_items_param] - - # Delete items from the container. - # The Cosmos DB SQL API does not support 'DELETE' queries, - # so deletes must be done with the delete_item method - # on the container. - # [START delete_items] - async for item in container.query_items( - query='SELECT * FROM products p WHERE p.productModel = "DISCONTINUED" AND p.productName="Widget"' - ): - await container.delete_item(item, partition_key="Widget") - # [END delete_items] - - # Retrieve the properties of a database - # [START get_database_properties] - properties = await database.read() - print(json.dumps(properties, indent=True)) - # [END get_database_properties] - - # Modify the properties of an existing container - # This example sets the default time to live (TTL) for items in the - # container to 3600 seconds (1 hour). An item in container is deleted - # when the TTL has elapsed since it was last edited. - # [START reset_container_properties] - # Set the TTL on the container to 3600 seconds (one hour) - await database.replace_container(container, partition_key=PartitionKey(path='/productName'), default_ttl=3600) - - # Display the new TTL setting for the container - container_props = await database.get_container_client(container_name).read() - print("New container TTL: {}".format(json.dumps(container_props['defaultTtl']))) - # [END reset_container_properties] - - # Create a user in the database. - # [START create_user] - try: - await database.create_user(dict(id="Walter Harp")) - except exceptions.CosmosResourceExistsError: - print("A user with that ID already exists.") - except exceptions.CosmosHttpResponseError as failure: - print("Failed to create user. Status code:{}".format(failure.status_code)) - # [END create_user] + async for item in discontinued_items: + print(json.dumps(item, indent=True)) + # [END query_items_param] + + # Delete items from the container. + # The Cosmos DB SQL API does not support 'DELETE' queries, + # so deletes must be done with the delete_item method + # on the container. + # [START delete_items] + async for item in container.query_items( + query='SELECT * FROM products p WHERE p.productModel = "DISCONTINUED" AND p.productName="Widget"' + ): + await container.delete_item(item, partition_key="Widget") + # [END delete_items] + + # Retrieve the properties of a database + # [START get_database_properties] + properties = await database.read() + print(json.dumps(properties, indent=True)) + # [END get_database_properties] + + # Modify the properties of an existing container + # This example sets the default time to live (TTL) for items in the + # container to 3600 seconds (1 hour). An item in container is deleted + # when the TTL has elapsed since it was last edited. + # [START reset_container_properties] + # Set the TTL on the container to 3600 seconds (one hour) + await database.replace_container(container, partition_key=PartitionKey(path='/productName'), default_ttl=3600) + + # Display the new TTL setting for the container + container_props = await database.get_container_client(container_name).read() + print("New container TTL: {}".format(json.dumps(container_props['defaultTtl']))) + # [END reset_container_properties] + + # Create a user in the database. + # [START create_user] + try: + await database.create_user(dict(id="Walter Harp")) + except exceptions.CosmosResourceExistsError: + print("A user with that ID already exists.") + except exceptions.CosmosHttpResponseError as failure: + print("Failed to create user. Status code:{}".format(failure.status_code)) + # [END create_user] diff --git a/sdk/cosmos/azure-cosmos/samples/index_management_async.py b/sdk/cosmos/azure-cosmos/samples/index_management_async.py index 53fbde79053d..09c8a58fa6b5 100644 --- a/sdk/cosmos/azure-cosmos/samples/index_management_async.py +++ b/sdk/cosmos/azure-cosmos/samples/index_management_async.py @@ -626,37 +626,36 @@ async def perform_multi_orderby_query(db): async def run_sample(): try: - client = obtain_client() - await fetch_all_databases(client) + async with obtain_client() as client: + await fetch_all_databases(client) - # Create database if doesn't exist already. - created_db = await client.create_database_if_not_exists(DATABASE_ID) - print(created_db) + # Create database if doesn't exist already. + created_db = await client.create_database_if_not_exists(DATABASE_ID) + print(created_db) - # 1. Exclude a document from the index - await explicitly_exclude_from_index(created_db) + # 1. Exclude a document from the index + await explicitly_exclude_from_index(created_db) - # 2. Use manual (instead of automatic) indexing - await use_manual_indexing(created_db) + # 2. Use manual (instead of automatic) indexing + await use_manual_indexing(created_db) - # 4. Exclude specified document paths from the index - await exclude_paths_from_index(created_db) + # 4. Exclude specified document paths from the index + await exclude_paths_from_index(created_db) - # 5. Force a range scan operation on a hash indexed path - await range_scan_on_hash_index(created_db) + # 5. Force a range scan operation on a hash indexed path + await range_scan_on_hash_index(created_db) - # 6. Use range indexes on strings - await use_range_indexes_on_strings(created_db) + # 6. Use range indexes on strings + await use_range_indexes_on_strings(created_db) - # 7. Perform an index transform - await perform_index_transformations(created_db) + # 7. Perform an index transform + await perform_index_transformations(created_db) - # 8. Perform Multi Orderby queries using composite indexes - await perform_multi_orderby_query(created_db) + # 8. Perform Multi Orderby queries using composite indexes + await perform_multi_orderby_query(created_db) - print('Sample done, cleaning up sample-generated data') - await client.delete_database(DATABASE_ID) - await client.close() + print('Sample done, cleaning up sample-generated data') + await client.delete_database(DATABASE_ID) except exceptions.AzureError as e: raise e diff --git a/sdk/cosmos/azure-cosmos/samples/nonpartitioned_container_operations_async.py b/sdk/cosmos/azure-cosmos/samples/nonpartitioned_container_operations_async.py index 9b24cfe71533..2756af6bf129 100644 --- a/sdk/cosmos/azure-cosmos/samples/nonpartitioned_container_operations_async.py +++ b/sdk/cosmos/azure-cosmos/samples/nonpartitioned_container_operations_async.py @@ -234,43 +234,39 @@ def get_sales_order_v2(item_id): async def run_sample(): - client = cosmos_client.CosmosClient(HOST, MASTER_KEY) - try: - # setup database for this sample + async with cosmos_client.CosmosClient(HOST, MASTER_KEY) as client: try: - db = await client.create_database(id=DATABASE_ID) - except exceptions.CosmosResourceExistsError: - db = await client.get_database_client(DATABASE_ID) - - # setup container for this sample - try: - container, document = create_nonpartitioned_container(db) - print('Container with id \'{0}\' created'.format(CONTAINER_ID)) - - except exceptions.CosmosResourceExistsError: - print('Container with id \'{0}\' was found'.format(CONTAINER_ID)) - - # Read Item created in non partitioned container using older API version - await read_item(container, document) - await create_items(container) - await read_items(container) - await query_items(container, 'SalesOrder1') - await replace_item(container, 'SalesOrder1') - await upsert_item(container, 'SalesOrder1') - await delete_item(container, 'SalesOrder1') - - # cleanup database after sample - try: - await client.delete_database(db) - except exceptions.CosmosResourceNotFoundError: - pass - - except exceptions.CosmosHttpResponseError as e: - print('\nrun_sample has caught an error. {0}'.format(e.message)) - - finally: - await client.close() - print("\nrun_sample done") + # setup database for this sample + try: + db = await client.create_database(id=DATABASE_ID) + except exceptions.CosmosResourceExistsError: + db = await client.get_database_client(DATABASE_ID) + + # setup container for this sample + try: + container, document = create_nonpartitioned_container(db) + print('Container with id \'{0}\' created'.format(CONTAINER_ID)) + + except exceptions.CosmosResourceExistsError: + print('Container with id \'{0}\' was found'.format(CONTAINER_ID)) + + # Read Item created in non partitioned container using older API version + await read_item(container, document) + await create_items(container) + await read_items(container) + await query_items(container, 'SalesOrder1') + await replace_item(container, 'SalesOrder1') + await upsert_item(container, 'SalesOrder1') + await delete_item(container, 'SalesOrder1') + + # cleanup database after sample + try: + await client.delete_database(db) + except exceptions.CosmosResourceNotFoundError: + pass + + except exceptions.CosmosHttpResponseError as e: + print('\nrun_sample has caught an error. {0}'.format(e.message)) if __name__ == '__main__': From df02ee4bdd70eccc7c5bf4a030b47634f498e27a Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Thu, 27 Jan 2022 10:23:08 -0500 Subject: [PATCH 04/23] Update CHANGELOG.md --- sdk/cosmos/azure-cosmos/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index b6607ae3fa7a..db966401c210 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -4,7 +4,7 @@ ### Other changes - Default consistency level for the sync and async clients is no longer "Session" and will instead be set to the - consistency level of the cosmos account on initialization. + consistency level of the user's cosmos account setting on initialization. ### 4.3.0b2 (2022-01-25) From dcac0b21688c63471fc67818d88d2105e15f13fd Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Thu, 27 Jan 2022 10:25:19 -0500 Subject: [PATCH 05/23] Update README.md --- sdk/cosmos/azure-cosmos/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/README.md b/sdk/cosmos/azure-cosmos/README.md index 96dfd0c9eb8b..e2298785bff4 100644 --- a/sdk/cosmos/azure-cosmos/README.md +++ b/sdk/cosmos/azure-cosmos/README.md @@ -465,8 +465,7 @@ async def main(): await client.close() # the async client must be closed manually if it's not initialized in a with statement ``` -However, instead of taking care of it manually with those extra lines, you can use the `async with` keywords. This creates a context manager that will warm up, initialize and later clean up -and close the client once you're out of the statement. The example below shows how to start the client this way. +However, instead of taking care of it manually with those extra lines, you can use the `async with` keywords. This creates a context manager that will warm up, initialize and later clean up and close the client once you're out of the statement. The example below shows how to start the client this way. ```Python from azure.cosmos.aio import CosmosClient From 68d9a97f6d9ff68f145b8c281439b82179cb8c5e Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Thu, 27 Jan 2022 10:27:15 -0500 Subject: [PATCH 06/23] Update README.md --- sdk/cosmos/azure-cosmos/README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/README.md b/sdk/cosmos/azure-cosmos/README.md index e2298785bff4..1f0793013d54 100644 --- a/sdk/cosmos/azure-cosmos/README.md +++ b/sdk/cosmos/azure-cosmos/README.md @@ -476,16 +476,17 @@ KEY = os.environ['ACCOUNT_KEY'] DATABASE_NAME = 'testDatabase' CONTAINER_NAME = 'products' -async with CosmosClient(URL, credential=KEY) as client: # the with statement will automatically close the async client - database = client.get_database_client(DATABASE_NAME) - container = database.get_container_client(CONTAINER_NAME) - for i in range(10): - await container.upsert_item({ - 'id': 'item{0}'.format(i), - 'productName': 'Widget', - 'productModel': 'Model {0}'.format(i) - } - ) +async def main(): + async with CosmosClient(URL, credential=KEY) as client: # the with statement will automatically warm up, initialize, and close the async client + database = client.get_database_client(DATABASE_NAME) + container = database.get_container_client(CONTAINER_NAME) + for i in range(10): + await container.upsert_item({ + 'id': 'item{0}'.format(i), + 'productName': 'Widget', + 'productModel': 'Model {0}'.format(i) + } + ) ``` ### Queries with the asynchronous client (Preview) From 8b006a63941c8bed56a9b14aa050d17cbffdb175 Mon Sep 17 00:00:00 2001 From: simorenoh Date: Thu, 27 Jan 2022 10:32:57 -0500 Subject: [PATCH 07/23] Update README.md --- sdk/cosmos/azure-cosmos/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/README.md b/sdk/cosmos/azure-cosmos/README.md index 1f0793013d54..cb904b13a4a9 100644 --- a/sdk/cosmos/azure-cosmos/README.md +++ b/sdk/cosmos/azure-cosmos/README.md @@ -447,13 +447,13 @@ import os URL = os.environ['ACCOUNT_URI'] KEY = os.environ['ACCOUNT_KEY'] +DATABASE_NAME = 'testDatabase' +CONTAINER_NAME = 'products' -async def main(): +async def create_products(): client = CosmosClient(URL, credential=KEY) await client.__aenter__() - DATABASE_NAME = 'testDatabase' database = client.get_database_client(DATABASE_NAME) - CONTAINER_NAME = 'products' container = database.get_container_client(CONTAINER_NAME) for i in range(10): await container.upsert_item({ @@ -476,8 +476,8 @@ KEY = os.environ['ACCOUNT_KEY'] DATABASE_NAME = 'testDatabase' CONTAINER_NAME = 'products' -async def main(): - async with CosmosClient(URL, credential=KEY) as client: # the with statement will automatically warm up, initialize, and close the async client +async def create_products(): + async with CosmosClient(URL, credential=KEY) as client: # the with statement will automatically warm up, initialize,# and close the async client database = client.get_database_client(DATABASE_NAME) container = database.get_container_client(CONTAINER_NAME) for i in range(10): From 36cf3be763869e9d3f74595be583a254e5a4377b Mon Sep 17 00:00:00 2001 From: simorenoh Date: Thu, 27 Jan 2022 11:50:10 -0500 Subject: [PATCH 08/23] Update CHANGELOG.md --- sdk/cosmos/azure-cosmos/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index db966401c210..befb26e6d349 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -2,9 +2,9 @@ ### 4.3.0b3 (Unreleased) -### Other changes +### Bugs fixed - Default consistency level for the sync and async clients is no longer "Session" and will instead be set to the - consistency level of the user's cosmos account setting on initialization. + consistency level of the user's cosmos account setting on initialization ### 4.3.0b2 (2022-01-25) From 6f7151ec50ada48a4b56389b9f9dcacb218f0109 Mon Sep 17 00:00:00 2001 From: simorenoh Date: Thu, 27 Jan 2022 12:29:46 -0500 Subject: [PATCH 09/23] formatting --- .../azure/cosmos/_cosmos_client_connection.py | 74 ++++++++++--------- .../aio/_cosmos_client_connection_async.py | 31 ++++---- 2 files changed, 58 insertions(+), 47 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py index 9a436f43a230..68a237b4de5f 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py @@ -60,6 +60,8 @@ from .partition_key import _Undefined, _Empty ClassType = TypeVar("ClassType") + + # pylint: disable=protected-access @@ -197,14 +199,49 @@ def __init__( self._routing_map_provider = routing_map_provider.SmartRoutingMapProvider(self) # Set initial consistency for single _GetDatabaseAccount request - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Strong + if consistency_level is not None: + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level + if consistency_level == documents.ConsistencyLevel.Session: + # create a session - this is maintained only if the default consistency level + # on the client is set to session, or if the user explicitly sets it as a property + # via setter + self.session = _session.Session(self.url_connection) + else: + self.session = None # type: ignore + else: + # Set to an arbitrary consistency level to make request for account level consistency + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Strong + database_account = self._global_endpoint_manager._GetDatabaseAccount(**kwargs) self._global_endpoint_manager.force_refresh(database_account) - print(self.default_headers) + # Use database_account if no consistency passed in to verify consistency level to be used + if consistency_level is None: + self._set_account_consistency_level(database_account) + + def _set_account_consistency_level( + self, + database_account: ClassType, + ) -> None: + """Checks if consistency level param was passed in by user and sets it to that value or to the account default. + + :param database_account: The database account to be used to check consistency levels + :type database_account: ~azure.cosmos.documents.DatabaseAccount + :rtype: None + """ + # Set to default level present in account + user_consistency_policy = database_account.ConsistencyPolicy + consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) - # Use database_account to verify consistency level to be used - self._check_and_set_consistency_level(database_account, consistency_level) + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level + + if consistency_level == documents.ConsistencyLevel.Session: + # create a session - this is maintained only if the default consistency level + # on the client is set to session, or if the user explicitly sets it as a property + # via setter + self.session = _session.Session(self.url_connection) + else: + self.session = None # type: ignore @property def Session(self): @@ -2556,7 +2593,6 @@ def refresh_routing_map_provider(self): # re-initializes the routing map provider, effectively refreshing the current partition key range cache self._routing_map_provider = routing_map_provider.SmartRoutingMapProvider(self) - def _UpdateSessionIfRequired(self, request_headers, response_result, response_headers): """ Updates session if necessary. @@ -2588,31 +2624,3 @@ def _return_undefined_or_empty_partition_key(is_system_key): if is_system_key: return _Empty return _Undefined - - def _check_and_set_consistency_level( - self, - database_account: ClassType, - consistency_level: Optional[str] = None, - ) -> None: - """Checks if consistency level param was passed in by user and sets it to that value or to the account default. - - :param database_account: The database account to be used to check consistency levels - :type database_account: ~azure.cosmos.documents.DatabaseAccount - :param consistency_level: The consistency level passed in by the user for the client - :type consistency_level: Optional[str] - :rtype: None - """ - if consistency_level is None: - # If no consistency level was passed in by user, set to default level present in account - user_consistency_policy = database_account.ConsistencyPolicy - consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) - - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level - - if consistency_level == documents.ConsistencyLevel.Session: - # create a session - this is maintained only if the default consistency level - # on the client is set to session, or if the user explicitly sets it as a property - # via setter - self.session = _session.Session(self.url_connection) - else: - self.session = None # type: ignore diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py index 95b0ddf8d424..231a974f75e7 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py @@ -139,7 +139,6 @@ def __init__( } self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level - print("initial consistency level: {}".format(consistency_level)) # Keeps the latest response headers from server. self.last_response_headers = None @@ -242,30 +241,34 @@ async def _setup(self): **self._setup_kwargs) self._setup_kwargs['database_account'] = database_account await self._global_endpoint_manager.force_refresh(self._setup_kwargs['database_account']) - self._check_and_set_consistency_level(database_account, user_defined_consistency) - def _check_and_set_consistency_level( + # Use database_account if no consistency passed in to verify consistency level to be used + if user_defined_consistency is None: + self._set_account_consistency_level(database_account) + + def _set_account_consistency_level( self, database_account: ClassType, - consistency_level: Optional[str] = None, ) -> None: """Checks if consistency level param was passed in by user and sets it to that value or to the account default. :param database_account: The database account to be used to check consistency levels :type database_account: ~azure.cosmos.documents.DatabaseAccount - :param consistency_level: The consistency level passed in by the user for the client - :type consistency_level: Optional[str] :rtype: None """ - if consistency_level is None: - # If no consistency level was passed in by user, set to default level present in account - user_consistency_policy = database_account.ConsistencyPolicy - consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) - if consistency_level is documents.ConsistencyLevel.Session: - # If None was passed in but DatabaseAccount has Session consistency level, start a Session - self.session = _session.Session() - # Set header to be used as final consistency for client + # Set to default level present in account + user_consistency_policy = database_account.ConsistencyPolicy + consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level + if consistency_level == documents.ConsistencyLevel.Session: + # create a session - this is maintained only if the default consistency level + # on the client is set to session, or if the user explicitly sets it as a property + # via setter + self.session = _session.Session(self.url_connection) + else: + self.session = None # type: ignore + def _GetDatabaseIdWithPathForUser(self, database_link, user): # pylint: disable=no-self-use CosmosClientConnection.__ValidateResource(user) From 69df86dda5a82cbb2431939ca3e89d587df1924d Mon Sep 17 00:00:00 2001 From: simorenoh Date: Thu, 27 Jan 2022 12:33:31 -0500 Subject: [PATCH 10/23] formatting --- .../azure/cosmos/aio/_cosmos_client_connection_async.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py index 231a974f75e7..686eaac3c50b 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py @@ -236,15 +236,18 @@ async def _setup(self): else: self.session = None # type: ignore + database_account = None if 'database_account' not in self._setup_kwargs: database_account = await self._global_endpoint_manager._GetDatabaseAccount( **self._setup_kwargs) self._setup_kwargs['database_account'] = database_account await self._global_endpoint_manager.force_refresh(self._setup_kwargs['database_account']) + else: + database_account = self._setup_kwargs.get('database_account') - # Use database_account if no consistency passed in to verify consistency level to be used - if user_defined_consistency is None: - self._set_account_consistency_level(database_account) + # Use database_account if no consistency passed in to verify consistency level to be used + if user_defined_consistency is None: + self._set_account_consistency_level(database_account) def _set_account_consistency_level( self, From 5b4f4067543f974a613f66e6e729b44fcbd385aa Mon Sep 17 00:00:00 2001 From: simorenoh Date: Thu, 27 Jan 2022 13:43:56 -0500 Subject: [PATCH 11/23] updated consistency for first request to Eventual (lowest latency) --- .../azure-cosmos/azure/cosmos/_cosmos_client_connection.py | 4 ++-- .../azure/cosmos/aio/_cosmos_client_connection_async.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py index 68a237b4de5f..fe0cdffe4542 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py @@ -209,8 +209,8 @@ def __init__( else: self.session = None # type: ignore else: - # Set to an arbitrary consistency level to make request for account level consistency - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Strong + # Set header setting to lowest latency consistency level for the first request to _GetDatabaseAccount + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Eventual database_account = self._global_endpoint_manager._GetDatabaseAccount(**kwargs) self._global_endpoint_manager.force_refresh(database_account) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py index 686eaac3c50b..19d57562ae9c 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py @@ -226,9 +226,9 @@ def _ReadEndpoint(self): async def _setup(self): # Save the choice that was made (either None or some value) and branch to set or get the consistency user_defined_consistency = self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] - # Set default header setting to any consistency level for the first request to _GetDatabaseAccount below + # Set header setting to lowest latency consistency level for the first request to _GetDatabaseAccount if user_defined_consistency is None: - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Strong + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Eventual if user_defined_consistency == documents.ConsistencyLevel.Session: # create a Session if the user wants Session consistency From c19f655ece8865f5ea889be30d7445a4250eb22d Mon Sep 17 00:00:00 2001 From: simorenoh Date: Thu, 27 Jan 2022 14:09:01 -0500 Subject: [PATCH 12/23] pylint --- .../azure/cosmos/_cosmos_client_connection.py | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py index fe0cdffe4542..6c318d1a97a5 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py @@ -198,41 +198,34 @@ def __init__( # Routing map provider self._routing_map_provider = routing_map_provider.SmartRoutingMapProvider(self) - # Set initial consistency for single _GetDatabaseAccount request - if consistency_level is not None: - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level - if consistency_level == documents.ConsistencyLevel.Session: - # create a session - this is maintained only if the default consistency level - # on the client is set to session, or if the user explicitly sets it as a property - # via setter - self.session = _session.Session(self.url_connection) - else: - self.session = None # type: ignore - else: - # Set header setting to lowest latency consistency level for the first request to _GetDatabaseAccount - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Eventual + # Set header setting to lowest latency consistency level for the first request to _GetDatabaseAccount + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Eventual database_account = self._global_endpoint_manager._GetDatabaseAccount(**kwargs) self._global_endpoint_manager.force_refresh(database_account) # Use database_account if no consistency passed in to verify consistency level to be used - if consistency_level is None: - self._set_account_consistency_level(database_account) + self._set_client_consistency_level(database_account, consistency_level) - def _set_account_consistency_level( + def _set_client_consistency_level( self, database_account: ClassType, + consistency_level: Optional[str], ) -> None: """Checks if consistency level param was passed in by user and sets it to that value or to the account default. :param database_account: The database account to be used to check consistency levels :type database_account: ~azure.cosmos.documents.DatabaseAccount + :param consistency_level: The consistency level passed in by the user + :type consistency_level: Optional[str] :rtype: None """ - # Set to default level present in account - user_consistency_policy = database_account.ConsistencyPolicy - consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) + if consistency_level is None: + # Set to default level present in account + user_consistency_policy = database_account.ConsistencyPolicy + consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) + # Set consistency level header to be used for the client self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level if consistency_level == documents.ConsistencyLevel.Session: From ce1295127a710266f0ca6b804c33759dcdc95168 Mon Sep 17 00:00:00 2001 From: simorenoh Date: Thu, 27 Jan 2022 17:55:02 -0500 Subject: [PATCH 13/23] from_connection_string methods --- sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py | 6 +++--- sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py index 4194ef260f97..733af5fc63c8 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py @@ -140,7 +140,7 @@ async def close(self): await self.__aexit__() @classmethod - def from_connection_string(cls, conn_str, credential=None, consistency_level="Session", **kwargs): + def from_connection_string(cls, conn_str, credential=None, consistency_level=None, **kwargs): # type: (str, Optional[Union[str, Dict[str, str]]], str, Any) -> CosmosClient """Create a CosmosClient instance from a connection string. @@ -154,8 +154,8 @@ def from_connection_string(cls, conn_str, credential=None, consistency_level="Se :type credential: str or Dict[str, str] :param conn_str: The connection string. :type conn_str: str - :param consistency_level: Consistency level to use for the session. The default value is "Session". - :type consistency_level: str + :param consistency_level: Consistency level to use for the session. The default value is None (Account level). + :type consistency_level: Optional[str] """ settings = _parse_connection_str(conn_str, credential) return cls( diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py index a02c42cac288..daf74a1151f5 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py @@ -180,7 +180,7 @@ def __exit__(self, *args): return self.client_connection.pipeline_client.__exit__(*args) @classmethod - def from_connection_string(cls, conn_str, credential=None, consistency_level="Session", **kwargs): + def from_connection_string(cls, conn_str, credential=None, consistency_level=None, **kwargs): # type: (str, Optional[Any], str, Any) -> CosmosClient """Create a CosmosClient instance from a connection string. @@ -191,8 +191,8 @@ def from_connection_string(cls, conn_str, credential=None, consistency_level="Se :param credential: Alternative credentials to use instead of the key provided in the connection string. :type credential: str or dict(str, str) - :param str consistency_level: - Consistency level to use for the session. The default value is "Session". + :param Optional[str] consistency_level: + Consistency level to use for the session. The default value is None (Account level). """ settings = _parse_connection_str(conn_str, credential) return cls( From 84987ce405cb919f57b073849cf0058613ccfc9b Mon Sep 17 00:00:00 2001 From: simorenoh Date: Thu, 27 Jan 2022 17:56:03 -0500 Subject: [PATCH 14/23] from_connection_string2 --- sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py | 2 +- sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py index 733af5fc63c8..2eaf7003b59a 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/cosmos_client.py @@ -141,7 +141,7 @@ async def close(self): @classmethod def from_connection_string(cls, conn_str, credential=None, consistency_level=None, **kwargs): - # type: (str, Optional[Union[str, Dict[str, str]]], str, Any) -> CosmosClient + # type: (str, Optional[Union[str, Dict[str, str]]], Optional[str], Any) -> CosmosClient """Create a CosmosClient instance from a connection string. This can be retrieved from the Azure portal.For full list of optional diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py index daf74a1151f5..c581dcdf6baa 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/cosmos_client.py @@ -181,7 +181,7 @@ def __exit__(self, *args): @classmethod def from_connection_string(cls, conn_str, credential=None, consistency_level=None, **kwargs): - # type: (str, Optional[Any], str, Any) -> CosmosClient + # type: (str, Optional[Any], Optional[str], Any) -> CosmosClient """Create a CosmosClient instance from a connection string. This can be retrieved from the Azure portal.For full list of optional From 2d56ebd873f6c06f14f3a28f39283cc3d4c12b8d Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Fri, 28 Jan 2022 14:24:38 -0500 Subject: [PATCH 15/23] Update sdk/cosmos/azure-cosmos/README.md Co-authored-by: Gahl Levy <75269480+gahl-levy@users.noreply.github.com> --- sdk/cosmos/azure-cosmos/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/cosmos/azure-cosmos/README.md b/sdk/cosmos/azure-cosmos/README.md index cb904b13a4a9..2a32926a9db5 100644 --- a/sdk/cosmos/azure-cosmos/README.md +++ b/sdk/cosmos/azure-cosmos/README.md @@ -465,7 +465,7 @@ async def create_products(): await client.close() # the async client must be closed manually if it's not initialized in a with statement ``` -However, instead of taking care of it manually with those extra lines, you can use the `async with` keywords. This creates a context manager that will warm up, initialize and later clean up and close the client once you're out of the statement. The example below shows how to start the client this way. +Instead of manually opening and closing the client, you can use the `async with` keywords. This creates a context manager that will initialize and later close the client once you're out of the statement. The example below shows how to do so. ```Python from azure.cosmos.aio import CosmosClient From f018c897d5814e5d9274e512b70f68bc45d4ea7e Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Fri, 28 Jan 2022 14:25:16 -0500 Subject: [PATCH 16/23] Apply suggestions from code review Co-authored-by: Gahl Levy <75269480+gahl-levy@users.noreply.github.com> --- sdk/cosmos/azure-cosmos/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/README.md b/sdk/cosmos/azure-cosmos/README.md index 2a32926a9db5..39a68acaad52 100644 --- a/sdk/cosmos/azure-cosmos/README.md +++ b/sdk/cosmos/azure-cosmos/README.md @@ -439,7 +439,7 @@ For more information on TTL, see [Time to Live for Azure Cosmos DB data][cosmos_ ### Using the asynchronous client (Preview) The asynchronous cosmos client is a separate client that looks and works in a similar fashion to the existing synchronous client. However, the async client needs to be imported separately and its methods need to be used with the async/await keywords. -Due to its asynchronous nature, it also needs to be warmed up and then closed down when used. The example below shows how by using the client's __aenter__() and close() methods. +The Async client needs to be initialized and closed after usage. The example below shows how to do so by using the client's __aenter__() and close() methods. ```Python from azure.cosmos.aio import CosmosClient @@ -477,7 +477,7 @@ DATABASE_NAME = 'testDatabase' CONTAINER_NAME = 'products' async def create_products(): - async with CosmosClient(URL, credential=KEY) as client: # the with statement will automatically warm up, initialize,# and close the async client + async with CosmosClient(URL, credential=KEY) as client: # the with statement will automatically initialize and close the async client database = client.get_database_client(DATABASE_NAME) container = database.get_container_client(CONTAINER_NAME) for i in range(10): From 2732fa1c8ef63f955bbe3e842ad219ec52a0ae2c Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Mon, 31 Jan 2022 14:37:36 -0500 Subject: [PATCH 17/23] Update README.md --- sdk/cosmos/azure-cosmos/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/cosmos/azure-cosmos/README.md b/sdk/cosmos/azure-cosmos/README.md index 39a68acaad52..cbcfb42d9421 100644 --- a/sdk/cosmos/azure-cosmos/README.md +++ b/sdk/cosmos/azure-cosmos/README.md @@ -465,7 +465,7 @@ async def create_products(): await client.close() # the async client must be closed manually if it's not initialized in a with statement ``` -Instead of manually opening and closing the client, you can use the `async with` keywords. This creates a context manager that will initialize and later close the client once you're out of the statement. The example below shows how to do so. +Instead of manually opening and closing the client, you can and should use the `async with` keywords. This creates a context manager that will initialize and later close the client once you're out of the statement. The example below shows how to do so. ```Python from azure.cosmos.aio import CosmosClient From 7116654c5e04f7cc8c1245be4a5e55f32c7ed110 Mon Sep 17 00:00:00 2001 From: simorenoh Date: Wed, 2 Feb 2022 13:10:38 -0500 Subject: [PATCH 18/23] removed forceful header usage, changed setup to only check for Session consistency to start client session --- .../azure/cosmos/_cosmos_client_connection.py | 9 +++------ .../aio/_cosmos_client_connection_async.py | 19 +++++++++---------- .../azure-cosmos/test/test_user_configs.py | 17 +++++++++++++---- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py index 6c318d1a97a5..ca44ee67cff8 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py @@ -198,9 +198,6 @@ def __init__( # Routing map provider self._routing_map_provider = routing_map_provider.SmartRoutingMapProvider(self) - # Set header setting to lowest latency consistency level for the first request to _GetDatabaseAccount - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Eventual - database_account = self._global_endpoint_manager._GetDatabaseAccount(**kwargs) self._global_endpoint_manager.force_refresh(database_account) @@ -224,9 +221,9 @@ def _set_client_consistency_level( # Set to default level present in account user_consistency_policy = database_account.ConsistencyPolicy consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) - - # Set consistency level header to be used for the client - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level + else: + # Set consistency level header to be used for the client + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level if consistency_level == documents.ConsistencyLevel.Session: # create a session - this is maintained only if the default consistency level diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py index 19d57562ae9c..546e574dbc48 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py @@ -138,7 +138,8 @@ def __init__( http_constants.HttpHeaders.IsContinuationExpected: False, } - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level + if consistency_level is not None: + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level # Keeps the latest response headers from server. self.last_response_headers = None @@ -225,10 +226,10 @@ def _ReadEndpoint(self): async def _setup(self): # Save the choice that was made (either None or some value) and branch to set or get the consistency - user_defined_consistency = self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] - # Set header setting to lowest latency consistency level for the first request to _GetDatabaseAccount - if user_defined_consistency is None: - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = documents.ConsistencyLevel.Eventual + if self.default_headers.get(http_constants.HttpHeaders.ConsistencyLevel): + user_defined_consistency = self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] + else: + user_defined_consistency = None if user_defined_consistency == documents.ConsistencyLevel.Session: # create a Session if the user wants Session consistency @@ -247,13 +248,13 @@ async def _setup(self): # Use database_account if no consistency passed in to verify consistency level to be used if user_defined_consistency is None: - self._set_account_consistency_level(database_account) + self._check_if_session_consistency(database_account) - def _set_account_consistency_level( + def _check_if_session_consistency( self, database_account: ClassType, ) -> None: - """Checks if consistency level param was passed in by user and sets it to that value or to the account default. + """Checks account consistency level to set client Session if needed. :param database_account: The database account to be used to check consistency levels :type database_account: ~azure.cosmos.documents.DatabaseAccount :rtype: None @@ -262,8 +263,6 @@ def _set_account_consistency_level( user_consistency_policy = database_account.ConsistencyPolicy consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) - self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level - if consistency_level == documents.ConsistencyLevel.Session: # create a session - this is maintained only if the default consistency level # on the client is set to session, or if the user explicitly sets it as a property diff --git a/sdk/cosmos/azure-cosmos/test/test_user_configs.py b/sdk/cosmos/azure-cosmos/test/test_user_configs.py index 30bbe407fdc4..6c3d67b3a9f3 100644 --- a/sdk/cosmos/azure-cosmos/test/test_user_configs.py +++ b/sdk/cosmos/azure-cosmos/test/test_user_configs.py @@ -22,7 +22,7 @@ import unittest import azure.cosmos.cosmos_client as cosmos_client -from azure.cosmos import http_constants +from azure.cosmos import http_constants, exceptions import pytest from test_config import _test_config @@ -32,6 +32,7 @@ pytestmark = pytest.mark.cosmosEmulator +DATABASE_ID = "PythonSDKUserConfigTest" @pytest.mark.usefixtures("teardown") class TestUserConfigs(unittest.TestCase): @@ -54,11 +55,10 @@ def test_default_account_consistency(self): client = cosmos_client.CosmosClient(url=_test_config.host, credential=_test_config.masterKey) database_account = client.get_database_account() account_consistency_level = database_account.ConsistencyPolicy["defaultConsistencyLevel"] - self.assertEqual(client.client_connection.default_headers[http_constants.HttpHeaders.ConsistencyLevel], - account_consistency_level) + self.assertEqual(account_consistency_level, "Session") # Now testing a user-defined consistency level as opposed to using the account one - custom_level = "Strong" + custom_level = "Eventual" client = cosmos_client.CosmosClient(url=_test_config.host, credential=_test_config.masterKey, consistency_level=custom_level) database_account = client.get_database_account() @@ -68,6 +68,15 @@ def test_default_account_consistency(self): client.client_connection.default_headers[http_constants.HttpHeaders.ConsistencyLevel], account_consistency_level) + # Test for failure when trying to set consistency to higher level than account level + custom_level = "Strong" + client = cosmos_client.CosmosClient(url=_test_config.host, credential=_test_config.masterKey, + consistency_level=custom_level) + try: + client.create_database(DATABASE_ID) + except exceptions.CosmosHttpResponseError as e: + self.assertEqual(e.status_code, http_constants.StatusCodes.BAD_REQUEST) + if __name__ == "__main__": unittest.main() From c4d688ba52b9d1a5b79dc76ac7e8ad9be18c6204 Mon Sep 17 00:00:00 2001 From: simorenoh Date: Wed, 2 Feb 2022 15:48:21 -0500 Subject: [PATCH 19/23] need to set header if Session consistency for updating session if needed (thanks Jake!) --- .../azure-cosmos/azure/cosmos/_cosmos_client_connection.py | 1 + .../azure/cosmos/aio/_cosmos_client_connection_async.py | 1 + 2 files changed, 2 insertions(+) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py index ca44ee67cff8..a8da94b53818 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py @@ -229,6 +229,7 @@ def _set_client_consistency_level( # create a session - this is maintained only if the default consistency level # on the client is set to session, or if the user explicitly sets it as a property # via setter + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level self.session = _session.Session(self.url_connection) else: self.session = None # type: ignore diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py index 546e574dbc48..e77ee9f03997 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py @@ -267,6 +267,7 @@ def _check_if_session_consistency( # create a session - this is maintained only if the default consistency level # on the client is set to session, or if the user explicitly sets it as a property # via setter + self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level self.session = _session.Session(self.url_connection) else: self.session = None # type: ignore From c126d3386242c7293d357c23bfd7476cd65c985d Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Wed, 2 Feb 2022 15:54:38 -0500 Subject: [PATCH 20/23] Apply suggestions from code review Kushagra improved documentation and comments Co-authored-by: Kushagra Thapar --- sdk/cosmos/azure-cosmos/CHANGELOG.md | 3 ++- sdk/cosmos/azure-cosmos/README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index befb26e6d349..3fa88f7d56b4 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -4,7 +4,8 @@ ### Bugs fixed - Default consistency level for the sync and async clients is no longer "Session" and will instead be set to the - consistency level of the user's cosmos account setting on initialization + consistency level of the user's cosmos account setting on initialization if not passed during client initialization. + This change will impact client application in terms of RUs and latency. Users relying on default `Session` consistency will need to pass it explicitly if their account consistency is different than `Session`. Please see [Consistency Levels in Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/consistency-levels) for more details. ### 4.3.0b2 (2022-01-25) diff --git a/sdk/cosmos/azure-cosmos/README.md b/sdk/cosmos/azure-cosmos/README.md index cbcfb42d9421..e4f20372986a 100644 --- a/sdk/cosmos/azure-cosmos/README.md +++ b/sdk/cosmos/azure-cosmos/README.md @@ -465,7 +465,7 @@ async def create_products(): await client.close() # the async client must be closed manually if it's not initialized in a with statement ``` -Instead of manually opening and closing the client, you can and should use the `async with` keywords. This creates a context manager that will initialize and later close the client once you're out of the statement. The example below shows how to do so. +Instead of manually opening and closing the client, it is highly recommended to use the `async with` keywords. This creates a context manager that will initialize and later close the client once you're out of the statement. The example below shows how to do so. ```Python from azure.cosmos.aio import CosmosClient From 00fcfd22b11bbcb073dfd83393f2e68f6748fea9 Mon Sep 17 00:00:00 2001 From: simorenoh Date: Wed, 2 Feb 2022 16:20:28 -0500 Subject: [PATCH 21/23] added test for session token --- .../azure-cosmos/test/test_user_configs.py | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/test/test_user_configs.py b/sdk/cosmos/azure-cosmos/test/test_user_configs.py index 6c3d67b3a9f3..d5c93ccb2ec7 100644 --- a/sdk/cosmos/azure-cosmos/test/test_user_configs.py +++ b/sdk/cosmos/azure-cosmos/test/test_user_configs.py @@ -22,8 +22,9 @@ import unittest import azure.cosmos.cosmos_client as cosmos_client -from azure.cosmos import http_constants, exceptions +from azure.cosmos import http_constants, exceptions, PartitionKey import pytest +import uuid from test_config import _test_config # This test class serves to test user-configurable options and verify they are @@ -32,7 +33,16 @@ pytestmark = pytest.mark.cosmosEmulator -DATABASE_ID = "PythonSDKUserConfigTest" +DATABASE_ID = "PythonSDKUserConfigTesters" +CONTAINER_ID = "PythonSDKTestContainer" + +def get_test_item(): + item = { + 'id': 'Async_' + str(uuid.uuid4()), + 'test_object': True, + 'lastName': 'Smith' + } + return item @pytest.mark.usefixtures("teardown") class TestUserConfigs(unittest.TestCase): @@ -57,6 +67,25 @@ def test_default_account_consistency(self): account_consistency_level = database_account.ConsistencyPolicy["defaultConsistencyLevel"] self.assertEqual(account_consistency_level, "Session") + # Testing the session token logic works without user passing in Session explicitly + database = client.create_database(DATABASE_ID) + container = database.create_container(id=CONTAINER_ID, partition_key=PartitionKey(path="/id")) + container.create_item(body=get_test_item()) + session_token = client.client_connection.last_response_headers[http_constants.CookieHeaders.SessionToken] + item2 = get_test_item() + container.create_item(body=item2) + session_token2 = client.client_connection.last_response_headers[http_constants.CookieHeaders.SessionToken] + + # Check Session token is being updated to reflect new item created + self.assertNotEqual(session_token, session_token2) + + container.read_item(item=item2.get("id"), partition_key=item2.get("id")) + read_session_token = client.client_connection.last_response_headers[http_constants.CookieHeaders.SessionToken] + + # Check Session token remains the same for read operation as with previous create item operation + self.assertEqual(session_token2, read_session_token) + client.delete_database(DATABASE_ID) + # Now testing a user-defined consistency level as opposed to using the account one custom_level = "Eventual" client = cosmos_client.CosmosClient(url=_test_config.host, credential=_test_config.masterKey, From 7c098ea7755ebbaaa5a8ff84f0dbafc5655d4c1b Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Wed, 2 Feb 2022 16:53:19 -0500 Subject: [PATCH 22/23] Update CHANGELOG.md --- sdk/cosmos/azure-cosmos/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index 3fa88f7d56b4..ba95dc07a2db 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -5,7 +5,7 @@ ### Bugs fixed - Default consistency level for the sync and async clients is no longer "Session" and will instead be set to the consistency level of the user's cosmos account setting on initialization if not passed during client initialization. - This change will impact client application in terms of RUs and latency. Users relying on default `Session` consistency will need to pass it explicitly if their account consistency is different than `Session`. Please see [Consistency Levels in Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/consistency-levels) for more details. + This change will impact client application in terms of RUs and latency. Users relying on default `Session` consistency will need to pass it explicitly if their account consistency is different than `Session`. Please see [Consistency Levels in Azure Cosmos DB](https://docs.microsoft.com/azure/cosmos-db/consistency-levels) for more details. ### 4.3.0b2 (2022-01-25) From ae64f9604befce33b531b915d5cbb4b3dd7374dc Mon Sep 17 00:00:00 2001 From: simorenoh Date: Thu, 3 Feb 2022 10:48:41 -0500 Subject: [PATCH 23/23] Update _cosmos_client_connection_async.py --- .../aio/_cosmos_client_connection_async.py | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py index e77ee9f03997..0e6e26ea30ee 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py @@ -225,11 +225,21 @@ def _ReadEndpoint(self): return self._global_endpoint_manager.get_read_endpoint() async def _setup(self): + + if 'database_account' not in self._setup_kwargs: + database_account = await self._global_endpoint_manager._GetDatabaseAccount( + **self._setup_kwargs) + self._setup_kwargs['database_account'] = database_account + await self._global_endpoint_manager.force_refresh(self._setup_kwargs['database_account']) + else: + database_account = self._setup_kwargs.get('database_account') + # Save the choice that was made (either None or some value) and branch to set or get the consistency if self.default_headers.get(http_constants.HttpHeaders.ConsistencyLevel): user_defined_consistency = self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] else: - user_defined_consistency = None + # Use database_account if no consistency passed in to verify consistency level to be used + user_defined_consistency = self._check_if_account_session_consistency(database_account) if user_defined_consistency == documents.ConsistencyLevel.Session: # create a Session if the user wants Session consistency @@ -237,40 +247,26 @@ async def _setup(self): else: self.session = None # type: ignore - database_account = None - if 'database_account' not in self._setup_kwargs: - database_account = await self._global_endpoint_manager._GetDatabaseAccount( - **self._setup_kwargs) - self._setup_kwargs['database_account'] = database_account - await self._global_endpoint_manager.force_refresh(self._setup_kwargs['database_account']) - else: - database_account = self._setup_kwargs.get('database_account') - - # Use database_account if no consistency passed in to verify consistency level to be used - if user_defined_consistency is None: - self._check_if_session_consistency(database_account) - - def _check_if_session_consistency( + def _check_if_account_session_consistency( self, database_account: ClassType, - ) -> None: - """Checks account consistency level to set client Session if needed. + ) -> str: + """Checks account consistency level to set header if needed. :param database_account: The database account to be used to check consistency levels :type database_account: ~azure.cosmos.documents.DatabaseAccount - :rtype: None + :returns consistency_level: the account consistency level + :rtype: str """ # Set to default level present in account user_consistency_policy = database_account.ConsistencyPolicy consistency_level = user_consistency_policy.get(constants._Constants.DefaultConsistencyLevel) if consistency_level == documents.ConsistencyLevel.Session: - # create a session - this is maintained only if the default consistency level - # on the client is set to session, or if the user explicitly sets it as a property - # via setter + # We only set the header if we're using session consistency in the account in order to keep + # the current update_session logic which uses the header self.default_headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level - self.session = _session.Session(self.url_connection) - else: - self.session = None # type: ignore + + return consistency_level def _GetDatabaseIdWithPathForUser(self, database_link, user): # pylint: disable=no-self-use