Skip to content

Commit ec5aec3

Browse files
tvaron3simorenoh
authored andcommitted
None Options for Container APIs (#44098)
* change workloads based on feedback * add staging yml file * add staging yml file * test_none_options * test_none_options * update changelog * revert unrelated file * react to copilot comments
1 parent 7433465 commit ec5aec3

5 files changed

Lines changed: 373 additions & 2 deletions

File tree

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
## Release History
22

3+
### 4.14.3 (Unreleased)
4+
5+
#### Features Added
6+
* Added support for Per Partition Automatic Failover. To enable this feature, you must follow the guide [here](https://learn.microsoft.com/azure/cosmos-db/how-to-configure-per-partition-automatic-failover). See [PR 41588](https://github.com/Azure/azure-sdk-for-python/pull/41588).
7+
8+
#### Breaking Changes
9+
10+
#### Bugs Fixed
11+
* Fixed bug when passing in None for some option in `query_items` would cause unexpected errors. See [PR 44098](https://github.com/Azure/azure-sdk-for-python/pull/44098)
12+
13+
#### Other Changes
14+
* Added cross-regional retries for 503 (Service Unavailable) errors. See [PR 41588](https://github.com/Azure/azure-sdk-for-python/pull/41588).
15+
316
### 4.14.2 (2025-11-14)
417

518
#### Features Added

sdk/cosmos/azure-cosmos/azure/cosmos/_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,17 @@ def valid_key_value_exist(
137137
kwargs: dict[str, Any],
138138
key: str,
139139
invalid_value: Any = None) -> bool:
140-
"""Check if a valid key and value exists in kwargs. By default, it checks if the value is not None.
140+
"""Check if a valid key and value exists in kwargs. It always checks if the value is not None and it will remove
141+
from the kwargs the None value.
141142
142143
:param dict[str, Any] kwargs: The dictionary of keyword arguments.
143144
:param str key: The key to check.
144145
:param Any invalid_value: The value that is considered invalid. Default is None.
145146
:return: True if the key exists and its value is not None, False otherwise.
146147
:rtype: bool
147148
"""
149+
if key in kwargs and kwargs[key] is None:
150+
kwargs.pop(key)
151+
return False
152+
148153
return key in kwargs and kwargs[key] is not invalid_value
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import unittest
4+
import uuid
5+
6+
import pytest
7+
8+
from azure.cosmos import CosmosClient
9+
import test_config
10+
from azure.cosmos.exceptions import CosmosHttpResponseError
11+
12+
13+
@pytest.mark.cosmosEmulator
14+
class TestNoneOptions(unittest.TestCase):
15+
configs = test_config.TestConfig
16+
host = configs.host
17+
masterKey = configs.masterKey
18+
connectionPolicy = configs.connectionPolicy
19+
20+
def setUp(self) -> None:
21+
self.client = CosmosClient(self.host, self.masterKey)
22+
self.database = self.client.get_database_client(self.configs.TEST_DATABASE_ID)
23+
self.container = self.database.get_container_client(self.configs.TEST_SINGLE_PARTITION_CONTAINER_ID)
24+
25+
def _create_sample_item(self):
26+
item = {"id": str(uuid.uuid4()), "pk": "pk-value", "value": 42}
27+
self.container.create_item(item, pre_trigger_include=None, post_trigger_include=None, indexing_directive=None,
28+
enable_automatic_id_generation=False, session_token=None, initial_headers=None,
29+
priority=None, no_response=None, retry_write=None, throughput_bucket=None)
30+
return item
31+
32+
def test_container_read_none_options(self):
33+
result = self.container.read(populate_partition_key_range_statistics=None, populate_quota_info=None,
34+
priority=None, initial_headers=None)
35+
assert result
36+
37+
def test_container_create_item_none_options(self):
38+
item = {"id": str(uuid.uuid4()), "pk": "pk-value", "value": 1}
39+
created = self.container.create_item(item, pre_trigger_include=None, post_trigger_include=None,
40+
indexing_directive=None, enable_automatic_id_generation=False,
41+
session_token=None, initial_headers=None, priority=None, no_response=None,
42+
retry_write=None, throughput_bucket=None)
43+
assert created["id"] == item["id"]
44+
45+
def test_container_read_item_none_options(self):
46+
item = self._create_sample_item()
47+
read_back = self.container.read_item(item["id"], partition_key=item["pk"], post_trigger_include=None,
48+
session_token=None, initial_headers=None,
49+
max_integrated_cache_staleness_in_ms=None, priority=None,
50+
throughput_bucket=None)
51+
assert read_back["id"] == item["id"]
52+
53+
def test_container_read_all_items_none_options(self):
54+
self._create_sample_item()
55+
pager = self.container.read_all_items(max_item_count=None, session_token=None, initial_headers=None,
56+
max_integrated_cache_staleness_in_ms=None, priority=None,
57+
throughput_bucket=None)
58+
items = list(pager)
59+
assert len(items) >= 1
60+
61+
def test_container_read_items_none_options(self):
62+
item = self._create_sample_item()
63+
results = self.container.read_items([(item["id"], item["pk"])], max_concurrency=None, consistency_level=None,
64+
session_token=None, initial_headers=None, excluded_locations=None,
65+
priority=None, throughput_bucket=None)
66+
assert any(r["id"] == item["id"] for r in results)
67+
68+
def test_container_query_items_none_options_partition(self):
69+
self._create_sample_item()
70+
pager = self.container.query_items("SELECT * FROM c", continuation_token_limit=None, enable_scan_in_query=None,
71+
initial_headers=None, max_integrated_cache_staleness_in_ms=None, max_item_count=None,
72+
parameters=None, partition_key=None, populate_index_metrics=None,
73+
populate_query_metrics=None, priority=None, response_hook=None, session_token=None,
74+
throughput_bucket=None, enable_cross_partition_query=True)
75+
items = list(pager)
76+
assert len(items) >= 1
77+
78+
def test_upsert_item_none_options(self):
79+
item = {"id": str(uuid.uuid4()), "pk": "pk-value", "value": 5}
80+
upserted = self.container.upsert_item(item, pre_trigger_include=None, post_trigger_include=None,
81+
session_token=None, initial_headers=None, etag=None,
82+
match_condition=None, priority=None, no_response=None,
83+
retry_write=None, throughput_bucket=None)
84+
assert upserted["id"] == item["id"]
85+
86+
def test_replace_item_none_options(self):
87+
item = self._create_sample_item()
88+
new_body = {"id": item["id"], "pk": item["pk"], "value": 999}
89+
replaced = self.container.replace_item(item["id"], new_body, pre_trigger_include=None,
90+
post_trigger_include=None, session_token=None,
91+
initial_headers=None, etag=None, match_condition=None,
92+
priority=None, no_response=None, retry_write=None,
93+
throughput_bucket=None)
94+
assert replaced["value"] == 999
95+
96+
def test_patch_item_none_options(self):
97+
item = self._create_sample_item()
98+
operations = [{"op": "add", "path": "/patched", "value": True}]
99+
patched = self.container.patch_item(item["id"], partition_key=item["pk"], patch_operations=operations,
100+
filter_predicate=None, pre_trigger_include=None, post_trigger_include=None,
101+
session_token=None, etag=None, match_condition=None, priority=None,
102+
no_response=None, retry_write=None, throughput_bucket=None)
103+
assert patched["patched"] is True
104+
105+
def test_delete_item_none_options(self):
106+
item = self._create_sample_item()
107+
self.container.delete_item(item["id"], partition_key=item["pk"], pre_trigger_include=None,
108+
post_trigger_include=None, session_token=None, initial_headers=None,
109+
etag=None, match_condition=None, priority=None, retry_write=None,
110+
throughput_bucket=None)
111+
with self.assertRaises(CosmosHttpResponseError):
112+
self.container.read_item(item["id"], partition_key=item["pk"], post_trigger_include=None,
113+
session_token=None, initial_headers=None,
114+
max_integrated_cache_staleness_in_ms=None, priority=None,
115+
throughput_bucket=None)
116+
117+
def test_get_throughput_none_options(self):
118+
tp = self.container.get_throughput(response_hook=None)
119+
assert tp.offer_throughput > 0
120+
121+
def test_list_conflicts_none_options(self):
122+
pager = self.container.list_conflicts(max_item_count=None, response_hook=None)
123+
conflicts = list(pager)
124+
assert conflicts == conflicts # may be empty
125+
126+
def test_query_conflicts_none_options(self):
127+
pager = self.container.query_conflicts("SELECT * FROM c", parameters=None, partition_key=None,
128+
max_item_count=None, response_hook=None, enable_cross_partition_query=True)
129+
conflicts = list(pager)
130+
assert conflicts == conflicts
131+
132+
def test_delete_all_items_by_partition_key_none_options(self):
133+
pk_value = "delete-pk"
134+
for _ in range(2):
135+
item = {"id": str(uuid.uuid4()), "pk": pk_value, "value": 1}
136+
self.container.create_item(item, pre_trigger_include=None, post_trigger_include=None, indexing_directive=None,
137+
enable_automatic_id_generation=False, session_token=None, initial_headers=None,
138+
priority=None, no_response=None, retry_write=None, throughput_bucket=None)
139+
self.container.delete_all_items_by_partition_key(pk_value, pre_trigger_include=None, post_trigger_include=None,
140+
session_token=None, throughput_bucket=None)
141+
pager = self.container.query_items("SELECT * FROM c WHERE c.pk = @pk", parameters=[{"name": "@pk", "value": pk_value}],
142+
partition_key=None, continuation_token_limit=None, enable_scan_in_query=None,
143+
initial_headers=None, max_integrated_cache_staleness_in_ms=None, max_item_count=None,
144+
populate_index_metrics=None, populate_query_metrics=None, priority=None,
145+
response_hook=None, session_token=None, throughput_bucket=None)
146+
_items = list(pager)
147+
assert _items == _items
148+
149+
def test_execute_item_batch_none_options(self):
150+
pk_value = "batch-pk"
151+
id1 = str(uuid.uuid4())
152+
id2 = str(uuid.uuid4())
153+
ops = [
154+
("create", ({"id": id1, "pk": pk_value},)),
155+
("create", ({"id": id2, "pk": pk_value},)),
156+
]
157+
batch_result = self.container.execute_item_batch(ops, partition_key=pk_value,
158+
pre_trigger_include=None, post_trigger_include=None,
159+
session_token=None, priority=None,
160+
throughput_bucket=None)
161+
assert any(r.get("resourceBody").get("id") == id1 for r in batch_result) or any(r.get("resourceBody").get("id") == id2 for r in batch_result)
162+
163+
def test_query_items_change_feed_none_options(self):
164+
#Create an item, then acquire the change feed pager to verify the item appears in the feed.
165+
self.container.create_item({"id": str(uuid.uuid4()), "pk": "cf-pk", "value": 100},
166+
pre_trigger_include=None, post_trigger_include=None, indexing_directive=None,
167+
enable_automatic_id_generation=False, session_token=None, initial_headers=None,
168+
priority=None, no_response=None, retry_write=None, throughput_bucket=None)
169+
pager = self.container.query_items_change_feed(max_item_count=None, start_time="Beginning", partition_key=None,
170+
priority=None, mode=None, response_hook=None)
171+
changes = list(pager)
172+
assert len(changes) >= 1

0 commit comments

Comments
 (0)