diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index ab22f323bcf2..ba95dc07a2db 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -1,5 +1,12 @@ ## Release History +### 4.3.0b3 (Unreleased) + +### 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/azure/cosmos-db/consistency-levels) for more details. + ### 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/README.md b/sdk/cosmos/azure-cosmos/README.md index c0cba8d0cd54..e4f20372986a 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. +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 @@ -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) +CONTAINER_NAME = 'products' -async def create_items(): +async def create_products(): + client = CosmosClient(URL, credential=KEY) + await client.__aenter__() + 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), @@ -463,7 +465,7 @@ 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. +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 @@ -474,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 create_products(): + 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): + await container.upsert_item({ + 'id': 'item{0}'.format(i), + 'productName': 'Widget', + 'productModel': 'Model {0}'.format(i) + } + ) ``` ### Queries with the asynchronous client (Preview) 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..a8da94b53818 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 @@ -59,6 +59,8 @@ from . import _utils from .partition_key import _Undefined, _Empty +ClassType = TypeVar("ClassType") + # pylint: disable=protected-access @@ -92,7 +94,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 +141,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) @@ -210,6 +201,39 @@ def __init__( 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 + self._set_client_consistency_level(database_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 + """ + 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) + 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 + # 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 + @property def Session(self): """Gets the session object from the client. """ @@ -2560,7 +2584,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. 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..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 @@ -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 @@ -143,14 +144,6 @@ def __init__( # 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,49 @@ def _ReadEndpoint(self): return self._global_endpoint_manager.get_read_endpoint() async def _setup(self): + 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']) + 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: + # 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 + self.session = _session.Session(self.url_connection) + else: + self.session = None # type: ignore + + def _check_if_account_session_consistency( + self, + database_account: ClassType, + ) -> 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 + :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: + # 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 + + return 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..2eaf7003b59a 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): - # type: (str, Any, str, Any) -> None + def __init__(self, url, credential, consistency_level=None, **kwargs): + # type: (str, Any, Optional[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, @@ -141,8 +140,8 @@ async def close(self): await self.__aexit__() @classmethod - def from_connection_string(cls, conn_str, credential=None, consistency_level="Session", **kwargs): - # type: (str, Optional[Union[str, Dict[str, str]]], str, Any) -> CosmosClient + def from_connection_string(cls, conn_str, credential=None, consistency_level=None, **kwargs): + # 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 @@ -155,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 b7605d38607e..c581dcdf6baa 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'. @@ -159,8 +159,8 @@ class CosmosClient(object): :name: create_client """ - def __init__(self, url, credential, consistency_level="Session", **kwargs): - # type: (str, Any, str, Any) -> None + def __init__(self, url, credential, consistency_level=None, **kwargs): + # type: (str, Any, Optional[str], Any) -> None """Instantiate a new CosmosClient.""" auth = _build_auth(credential) connection_policy = _build_connection_policy(kwargs) @@ -180,8 +180,8 @@ 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): - # type: (str, Optional[Any], str, Any) -> CosmosClient + def from_connection_string(cls, conn_str, credential=None, consistency_level=None, **kwargs): + # 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 @@ -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( 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__': diff --git a/sdk/cosmos/azure-cosmos/test/test_user_configs.py b/sdk/cosmos/azure-cosmos/test/test_user_configs.py index 4100c105fd4b..d5c93ccb2ec7 100644 --- a/sdk/cosmos/azure-cosmos/test/test_user_configs.py +++ b/sdk/cosmos/azure-cosmos/test/test_user_configs.py @@ -22,16 +22,28 @@ import unittest import azure.cosmos.cosmos_client as cosmos_client +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 # properly set and saved into the different object instances that use these # user-configurable settings. pytestmark = pytest.mark.cosmosEmulator +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): @@ -46,5 +58,54 @@ 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(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, + 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) + + # 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()