Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cosmos] CosmosDB asynchronous client #21404

Merged
merged 61 commits into from
Dec 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
61ba8d1
initial commit
simorenoh Aug 13, 2021
15dcceb
Client Constructor (#20310)
annatisch Aug 20, 2021
bda95c3
read database
simorenoh Aug 27, 2021
c9648ab
Update simon_testfile.py
simorenoh Aug 27, 2021
80540dc
with coroutine
simorenoh Aug 30, 2021
1285438
Update simon_testfile.py
simorenoh Aug 30, 2021
992b0cd
small changes
simorenoh Aug 31, 2021
47cb688
async with returns no exceptions
simorenoh Aug 31, 2021
f3fa79f
Merge pull request #1 from Azure/simonmoreno/async
simorenoh Aug 31, 2021
0c49739
async read container
simorenoh Sep 1, 2021
47f4af5
async item read
simorenoh Sep 2, 2021
c97c946
cleaning up
simorenoh Sep 3, 2021
fcd95db
create item/ database methods
simorenoh Sep 13, 2021
36c5b90
item delete working
simorenoh Sep 13, 2021
44db2a2
docs replace functionality
simorenoh Sep 16, 2021
ec5b6ed
upsert functionality
simorenoh Sep 17, 2021
d63d052
Merge pull request #2 from simorenoh/item-read
simorenoh Oct 8, 2021
5d74c8f
missing query methods
simorenoh Oct 11, 2021
89fc2f7
CRUD for udf, sproc, triggers
simorenoh Oct 12, 2021
fdaa880
Merge branch 'Azure:main' into async-client
simorenoh Oct 12, 2021
3f9baf2
Merge branch 'Azure:main' into async-client
simorenoh Oct 12, 2021
d6650bc
Merge branch 'Azure:main' into query-functionality
simorenoh Oct 12, 2021
043dfe0
initial query logic + container methods
simorenoh Oct 13, 2021
befdb41
Merge branch 'async-client' into query-functionality
simorenoh Oct 13, 2021
8cffbe2
Merge pull request #3 from simorenoh/query-functionality
simorenoh Oct 13, 2021
72de7c8
missing some execution logic and tests
simorenoh Oct 21, 2021
5b805b8
oops
simorenoh Oct 21, 2021
8d8d0c4
fully working queries
simorenoh Oct 22, 2021
b597ca8
small fix to query_items()
simorenoh Oct 22, 2021
18319df
Update _cosmos_client_connection_async.py
simorenoh Oct 22, 2021
162c44d
Update _cosmos_client_connection.py
simorenoh Oct 22, 2021
ebbac51
documentation update
simorenoh Oct 22, 2021
43f78e6
Merge branch 'Azure:main' into main
simorenoh Oct 22, 2021
470aa5b
updated MIT dates and get_user_client() description
simorenoh Oct 22, 2021
74da690
Update CHANGELOG.md
simorenoh Oct 22, 2021
7104d63
Merge branch 'Azure:main' into main
simorenoh Oct 25, 2021
20718c7
Delete simon_testfile.py
simorenoh Oct 25, 2021
d825eaa
Merge pull request #4 from simorenoh/async-client
simorenoh Oct 25, 2021
e3c27a5
leftover retry utility
simorenoh Oct 25, 2021
3b778ad
Update README.md
simorenoh Oct 25, 2021
c6e352e
docs and removed six package
simorenoh Oct 28, 2021
8971a25
Merge remote-tracking branch 'upstream/main'
simorenoh Oct 28, 2021
52736ac
changes based on comments
simorenoh Nov 4, 2021
ad98039
small change in type hints
simorenoh Nov 4, 2021
f76c595
updated readme
simorenoh Nov 9, 2021
3f02a65
fixes based on conversations
simorenoh Nov 10, 2021
e719869
added missing type comments
simorenoh Nov 11, 2021
d03ee05
Merge branch 'Azure:main' into main
simorenoh Nov 11, 2021
02c52ee
update changelog for ci pipeline
simorenoh Nov 23, 2021
2cb4551
added typehints, moved params into keywords, added decorators, made _…
simorenoh Nov 29, 2021
cf20d35
changes based on sync with central sdk
simorenoh Dec 2, 2021
f456817
remove is_system_key from scripts (only used in execute_sproc)
simorenoh Dec 3, 2021
ea9bd16
Revert "remove is_system_key from scripts (only used in execute_sproc)"
simorenoh Dec 3, 2021
709d2eb
async script proxy using composition
simorenoh Dec 3, 2021
3277dd8
pylint
simorenoh Dec 3, 2021
a57cb4d
capitalized constants
simorenoh Dec 6, 2021
014578b
Apply suggestions from code review
simorenoh Dec 6, 2021
0d79695
closing python code snippet
simorenoh Dec 6, 2021
fdabea1
last doc updates
simorenoh Dec 7, 2021
016d0dd
Update sdk/cosmos/azure-cosmos/CHANGELOG.md
tjprescott Dec 7, 2021
8228aa9
version update
simorenoh Dec 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions sdk/cosmos/azure-cosmos/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## 4.2.1 (Unreleased)

## 4.3.0b1 (Unreleased)
**New features**
- Added language native async i/o client

## 4.2.0 (2020-10-08)

Expand Down
229 changes: 158 additions & 71 deletions sdk/cosmos/azure-cosmos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ Once you've populated the `ACCOUNT_URI` and `ACCOUNT_KEY` environment variables,
from azure.cosmos import CosmosClient

import os
url = os.environ['ACCOUNT_URI']
key = os.environ['ACCOUNT_KEY']
client = CosmosClient(url, credential=key)
URL = os.environ['ACCOUNT_URI']
KEY = os.environ['ACCOUNT_KEY']
client = CosmosClient(URL, credential=KEY)
```

## Key concepts
Expand All @@ -90,14 +90,17 @@ For more information about these resources, see [Working with Azure Cosmos datab

The keyword-argument `enable_cross_partition_query` accepts 2 options: `None` (default) or `True`.

## Note on using queries by id

When using queries that try to find items based on an **id** value, always make sure you are passing in a string type variable. Azure Cosmos DB only allows string id values and if you use any other datatype, this SDK will return no results and no error messages.

## Limitations

Currently the features below are **not supported**. For alternatives options, check the **Workarounds** section below.

### Data Plane Limitations:

* Group By queries
* Language Native async i/o
* Queries with COUNT from a DISTINCT subquery: SELECT COUNT (1) FROM (SELECT DISTINCT C.ID FROM C)
* Bulk/Transactional batch processing
* Direct TCP Mode access
Expand Down Expand Up @@ -177,6 +180,7 @@ The following sections provide several code snippets covering some of the most c
* [Get database properties](#get-database-properties "Get database properties")
* [Get database and container throughputs](#get-database-and-container-throughputs "Get database and container throughputs")
* [Modify container properties](#modify-container-properties "Modify container properties")
* [Using the asynchronous client](#using-the-asynchronous-client "Using the asynchronous client")

### Create a database

Expand All @@ -186,14 +190,14 @@ After authenticating your [CosmosClient][ref_cosmosclient], you can work with an
from azure.cosmos import CosmosClient, exceptions
import os

url = os.environ['ACCOUNT_URI']
key = os.environ['ACCOUNT_KEY']
client = CosmosClient(url, credential=key)
database_name = 'testDatabase'
URL = os.environ['ACCOUNT_URI']
KEY = os.environ['ACCOUNT_KEY']
client = CosmosClient(URL, credential=KEY)
DATABASE_NAME = 'testDatabase'
try:
database = client.create_database(database_name)
database = client.create_database(DATABASE_NAME)
except exceptions.CosmosResourceExistsError:
database = client.get_database_client(database_name)
database = client.get_database_client(DATABASE_NAME)
```

### Create a container
Expand All @@ -204,17 +208,17 @@ This example creates a container with default settings. If a container with the
from azure.cosmos import CosmosClient, PartitionKey, exceptions
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'
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'

try:
container = database.create_container(id=container_name, partition_key=PartitionKey(path="/productName"))
container = database.create_container(id=CONTAINER_NAME, partition_key=PartitionKey(path="/productName"))
except exceptions.CosmosResourceExistsError:
container = database.get_container_client(container_name)
container = database.get_container_client(CONTAINER_NAME)
except exceptions.CosmosHttpResponseError:
raise
```
Expand All @@ -231,11 +235,11 @@ The options for analytical_storage_ttl are:


```Python
container_name = 'products'
CONTAINER_NAME = 'products'
try:
container = database.create_container(id=container_name, partition_key=PartitionKey(path="/productName"),analytical_storage_ttl=-1)
container = database.create_container(id=CONTAINER_NAME, partition_key=PartitionKey(path="/productName"),analytical_storage_ttl=-1)
except exceptions.CosmosResourceExistsError:
container = database.get_container_client(container_name)
container = database.get_container_client(CONTAINER_NAME)
except exceptions.CosmosHttpResponseError:
raise
```
Expand All @@ -250,13 +254,13 @@ Retrieve an existing container from the database:
from azure.cosmos import CosmosClient
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)
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)
```

### Insert data
Expand All @@ -269,13 +273,13 @@ This example inserts several items into the container, each with a unique `id`:
from azure.cosmos import CosmosClient
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)
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)

for i in range(1, 10):
container.upsert_item({
Expand All @@ -294,13 +298,13 @@ To delete items from a container, use [ContainerProxy.delete_item][ref_container
from azure.cosmos import CosmosClient
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)
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)

for item in container.query_items(
query='SELECT * FROM products p WHERE p.productModel = "Model 2"',
Expand All @@ -320,13 +324,13 @@ This example queries a container for items with a specific `id`:
from azure.cosmos import CosmosClient
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)
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)

# Enumerate the returned items
import json
Expand Down Expand Up @@ -363,11 +367,11 @@ from azure.cosmos import CosmosClient
import os
import json

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)
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)
properties = database.read()
print(json.dumps(properties))
```
Expand All @@ -381,19 +385,19 @@ from azure.cosmos import CosmosClient
import os
import json

url = os.environ['ACCOUNT_URI']
key = os.environ['ACCOUNT_KEY']
client = CosmosClient(url, credential=key)
URL = os.environ['ACCOUNT_URI']
KEY = os.environ['ACCOUNT_KEY']
client = CosmosClient(URL, credential=KEY)

# Database
database_name = 'testDatabase'
database = client.get_database_client(database_name)
DATABASE_NAME = 'testDatabase'
database = client.get_database_client(DATABASE_NAME)
db_offer = database.read_offer()
print('Found Offer \'{0}\' for Database \'{1}\' and its throughput is \'{2}\''.format(db_offer.properties['id'], database.id, db_offer.properties['content']['offerThroughput']))

# Container with dedicated throughput only. Will return error "offer not found" for containers without dedicated throughput
container_name = 'testContainer'
container = database.get_container_client(container_name)
CONTAINER_NAME = 'testContainer'
container = database.get_container_client(CONTAINER_NAME)
container_offer = container.read_offer()
print('Found Offer \'{0}\' for Container \'{1}\' and its throughput is \'{2}\''.format(container_offer.properties['id'], container.id, container_offer.properties['content']['offerThroughput']))
```
Expand All @@ -408,13 +412,13 @@ from azure.cosmos import CosmosClient, PartitionKey
import os
import json

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)
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)

database.replace_container(
container,
Expand All @@ -428,7 +432,90 @@ print(json.dumps(container_props['defaultTtl']))

For more information on TTL, see [Time to Live for Azure Cosmos DB data][cosmos_ttl].

### Using the asynchronous client

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.

```Python
from azure.cosmos.aio import CosmosClient
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():
for i in range(10):
await container.upsert_item({
'id': 'item{0}'.format(i),
'productName': 'Widget',
'productModel': 'Model {0}'.format(i)
}
)
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simorenoh

closed manually after its use
Does it need to be closed after every function it's used in (like above), or can the client be passed around safely when doing things async?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be passed around safely, but needs to be manually disposed of once you're done using it - the alternative to this being to use the client using a with statement:
async with CosmosClient(url, key) as client: and then starting your cosmos db logic within that context

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simorenoh What happens if these async clients are not closed properly? Memory leak or something else?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have to test, but I assume that is the case - you also get an error in your code stating the client was not properly disposed of


```Python
from azure.cosmos.aio import CosmosClient
import os

URL = os.environ['ACCOUNT_URI']
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)
}
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to close the client here too?

await client.close()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is that second way to initialize I had mentioned above - the way it is done here, it gets disposed once the with statement gets exited

```

### Queries with the asynchronous client

Unlike the synchronous client, the async client does not have an `enable_cross_partition` flag in the request. Queries without a specified partition key value will attempt to do a cross partition query by default.

Query results can be iterated, but the query's raw output returns an asynchronous iterator. This means that each object from the iterator is an awaitable object, and does not yet contain the true query result. In order to obtain the query results you can use an async for loop, which awaits each result as you iterate on the object, or manually await each query result as you iterate over the asynchronous iterator.

Since the query results are an asynchronous iterator, they can't be cast into lists directly; instead, if you need to create lists from your results, use an async for loop or Python's list comprehension to populate a list:

```Python
from azure.cosmos.aio import CosmosClient
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_lists():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to close the client in this example too? I'm not seeing "with" being used either.

results = container.query_items(
query='SELECT * FROM products p WHERE p.productModel = "Model 2"')

# iterates on "results" iterator to asynchronously create a complete list of the actual query results

item_list = []
async for item in results:
item_list.append(item)

# Asynchronously creates a complete list of the actual query results. This code performs the same action as the for-loop example above.
item_list = [item async for item in results]
await client.close()
```
simorenoh marked this conversation as resolved.
Show resolved Hide resolved
## Troubleshooting

### General
Expand All @@ -441,7 +528,7 @@ For example, if you try to create a container using an ID (name) that's already

```Python
try:
database.create_container(id=container_name, partition_key=PartitionKey(path="/productName"))
database.create_container(id=CONTAINER_NAME, partition_key=PartitionKey(path="/productName"))
except exceptions.CosmosResourceExistsError:
print("""Error creating container
HTTP status code 409: The ID (name) provided for the container is already in use.
Expand Down Expand Up @@ -471,13 +558,13 @@ handler = logging.StreamHandler(stream=sys.stdout)
logger.addHandler(handler)

# This client will log detailed information about its HTTP sessions, at DEBUG level
client = CosmosClient(url, credential=key, logging_enable=True)
client = CosmosClient(URL, credential=KEY, logging_enable=True)
```

Similarly, `logging_enable` can enable detailed logging for a single operation,
even when it isn't enabled for the client:
```py
database = client.create_database(database_name, logging_enable=True)
database = client.create_database(DATABASE_NAME, logging_enable=True)
```

## Next steps
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# The MIT License (MIT)
# Copyright (c) 2021 Microsoft Corporation

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
Loading