Skip to content

Commit c2a6dc1

Browse files
authored
Add feed_range in query_items API (#41722)
* Add feed_range to query_items API * Added overload methods for 'query_items' API * Added Tests * Added feed_range to query_item for async * Added samples for query_items with feed_range * Fix pylint error * Updated CHANGELOG.md * Fix test error * Fix test error * Changed to run feed_range async tests on emulator only * Fix tests * Fix tests * Fix tests * Fix tests with positional_args * Addressing comments - Doc string updates - Tests were updated to use existing helper methods - Added tests with query_items with feed_range and partition_key * Fix pylint error * Changed to non-public APIs for internal classes/methods * Changed to non-public APIs for internal classes/methods * Changed to non-public APIs for internal classes/methods * Pylint error * Pylint error * Address comments * Add newline at the end _utils.py
1 parent aa29c33 commit c2a6dc1

16 files changed

+1101
-271
lines changed

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### 4.14.0b2 (Unreleased)
44

55
#### Features Added
6+
* Added feed range support in `query_items`. See [PR 41722](https://github.com/Azure/azure-sdk-for-python/pull/41722).
67

78
#### Breaking Changes
89

sdk/cosmos/azure-cosmos/azure/cosmos/_container_recreate_retry_policy.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from azure.core.pipeline.transport._base import HttpRequest
2929

3030
from . import http_constants
31-
from .partition_key import _Empty, _Undefined
31+
from .partition_key import _Empty, _Undefined, _PartitionKeyKind
3232

3333

3434
# pylint: disable=protected-access
@@ -88,7 +88,7 @@ def should_extract_partition_key(self, container_cache: Optional[Dict[str, Any]]
8888
if self._headers and http_constants.HttpHeaders.PartitionKey in self._headers:
8989
current_partition_key = self._headers[http_constants.HttpHeaders.PartitionKey]
9090
partition_key_definition = container_cache["partitionKey"] if container_cache else None
91-
if partition_key_definition and partition_key_definition["kind"] == "MultiHash":
91+
if partition_key_definition and partition_key_definition["kind"] == _PartitionKeyKind.MULTI_HASH:
9292
# A null in the multihash partition key indicates a failure in extracting partition keys
9393
# from the document definition
9494
return 'null' in current_partition_key
@@ -110,7 +110,7 @@ def _extract_partition_key(self, client: Optional[Any], container_cache: Optiona
110110
elif options and isinstance(options["partitionKey"], _Empty):
111111
new_partition_key = []
112112
# else serialize using json dumps method which apart from regular values will serialize None into null
113-
elif partition_key_definition and partition_key_definition["kind"] == "MultiHash":
113+
elif partition_key_definition and partition_key_definition["kind"] == _PartitionKeyKind.MULTI_HASH:
114114
new_partition_key = json.dumps(options["partitionKey"], separators=(',', ':'))
115115
else:
116116
new_partition_key = json.dumps([options["partitionKey"]])
@@ -131,7 +131,7 @@ async def _extract_partition_key_async(self, client: Optional[Any],
131131
elif isinstance(options["partitionKey"], _Empty):
132132
new_partition_key = []
133133
# else serialize using json dumps method which apart from regular values will serialize None into null
134-
elif partition_key_definition and partition_key_definition["kind"] == "MultiHash":
134+
elif partition_key_definition and partition_key_definition["kind"] == _PartitionKeyKind.MULTI_HASH:
135135
new_partition_key = json.dumps(options["partitionKey"], separators=(',', ':'))
136136
else:
137137
new_partition_key = json.dumps([options["partitionKey"]])

sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_client_connection.py

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import os
2727
import urllib.parse
2828
import uuid
29-
from typing import Callable, Dict, Any, Iterable, List, Mapping, Optional, Sequence, Tuple, Union, cast, Type
29+
from typing import Callable, Dict, Any, Iterable, List, Mapping, Optional, Sequence, Tuple, Union, cast
3030
from typing_extensions import TypedDict
3131
from urllib3.util.retry import Retry
3232

@@ -60,6 +60,7 @@
6060
from ._base import _build_properties_cache
6161
from ._change_feed.change_feed_iterable import ChangeFeedIterable
6262
from ._change_feed.change_feed_state import ChangeFeedState
63+
from ._change_feed.feed_range_internal import FeedRangeInternalEpk
6364
from ._constants import _Constants as Constants
6465
from ._cosmos_http_logging_policy import CosmosHttpLoggingPolicy
6566
from ._cosmos_responses import CosmosDict, CosmosList
@@ -71,15 +72,12 @@
7172
from .partition_key import (
7273
_Undefined,
7374
_Empty,
74-
PartitionKey,
75+
_PartitionKeyKind,
76+
_PartitionKeyType,
77+
_SequentialPartitionKeyType,
7578
_return_undefined_or_empty_partition_key,
76-
NonePartitionKeyValue,
77-
_get_partition_key_from_partition_key_definition
7879
)
7980

80-
PartitionKeyType = Union[str, int, float, bool, Sequence[Union[str, int, float, bool, None]], Type[NonePartitionKeyValue]] # pylint: disable=line-too-long
81-
82-
8381
class CredentialDict(TypedDict, total=False):
8482
masterKey: str
8583
resourceTokens: Mapping[str, Any]
@@ -1062,7 +1060,7 @@ def QueryItems(
10621060
database_or_container_link: str,
10631061
query: Optional[Union[str, Dict[str, Any]]],
10641062
options: Optional[Mapping[str, Any]] = None,
1065-
partition_key: Optional[PartitionKeyType] = None,
1063+
partition_key: Optional[_PartitionKeyType] = None,
10661064
response_hook: Optional[Callable[[Mapping[str, Any], Dict[str, Any]], None]] = None,
10671065
**kwargs: Any
10681066
) -> ItemPaged[Dict[str, Any]]:
@@ -3165,23 +3163,21 @@ def __GetBodiesFromQueryResult(result: Dict[str, Any]) -> List[Dict[str, Any]]:
31653163
request_params = RequestObject(resource_type, documents._OperationType.SqlQuery, req_headers)
31663164
request_params.set_excluded_location_from_options(options)
31673165

3168-
# check if query has prefix partition key
3169-
isPrefixPartitionQuery = kwargs.pop("isPrefixPartitionQuery", None)
3170-
if isPrefixPartitionQuery and "partitionKeyDefinition" in kwargs:
3166+
# Check if the over lapping ranges can be populated
3167+
feed_range_epk = None
3168+
if "feed_range" in kwargs:
3169+
feed_range = kwargs.pop("feed_range")
3170+
feed_range_epk = FeedRangeInternalEpk.from_json(feed_range).get_normalized_range()
3171+
elif "prefix_partition_key_object" in kwargs and "prefix_partition_key_value" in kwargs:
3172+
prefix_partition_key_obj = kwargs.pop("prefix_partition_key_object")
3173+
prefix_partition_key_value: _SequentialPartitionKeyType = kwargs.pop("prefix_partition_key_value")
3174+
feed_range_epk = (
3175+
prefix_partition_key_obj._get_epk_range_for_prefix_partition_key(prefix_partition_key_value))
3176+
3177+
# If feed_range_epk exist, query with the range
3178+
if feed_range_epk is not None:
31713179
last_response_headers = CaseInsensitiveDict()
3172-
# here get the over lapping ranges
3173-
# Default to empty Dictionary, but unlikely to be empty as we first check if we have it in kwargs
3174-
pk_properties: Union[PartitionKey, Dict] = kwargs.pop("partitionKeyDefinition", {})
3175-
partition_key_definition = _get_partition_key_from_partition_key_definition(pk_properties)
3176-
partition_key_value: Sequence[
3177-
Union[None, bool, int, float, str, _Undefined, Type[NonePartitionKeyValue]]] = cast(
3178-
Sequence[Union[None, bool, int, float, str, _Undefined, Type[NonePartitionKeyValue]]],
3179-
pk_properties.get("partition_key")
3180-
)
3181-
feedrangeEPK = partition_key_definition._get_epk_range_for_prefix_partition_key(
3182-
partition_key_value
3183-
) # cspell:disable-line
3184-
over_lapping_ranges = self._routing_map_provider.get_overlapping_ranges(resource_id, [feedrangeEPK],
3180+
over_lapping_ranges = self._routing_map_provider.get_overlapping_ranges(resource_id, [feed_range_epk],
31853181
options)
31863182
# It is possible to get more than one over lapping range. We need to get the query results for each one
31873183
results: Dict[str, Any] = {}
@@ -3198,8 +3194,8 @@ def __GetBodiesFromQueryResult(result: Dict[str, Any]) -> List[Dict[str, Any]]:
31983194
single_range = routing_range.Range.PartitionKeyRangeToRange(over_lapping_range)
31993195
# Since the range min and max are all Upper Cased string Hex Values,
32003196
# we can compare the values lexicographically
3201-
EPK_sub_range = routing_range.Range(range_min=max(single_range.min, feedrangeEPK.min),
3202-
range_max=min(single_range.max, feedrangeEPK.max),
3197+
EPK_sub_range = routing_range.Range(range_min=max(single_range.min, feed_range_epk.min),
3198+
range_max=min(single_range.max, feed_range_epk.max),
32033199
isMinInclusive=True, isMaxInclusive=False)
32043200
if single_range.min == EPK_sub_range.min and EPK_sub_range.max == single_range.max:
32053201
# The Epk Sub Range spans exactly one physical partition
@@ -3344,7 +3340,7 @@ def _ExtractPartitionKey(
33443340
partitionKeyDefinition: Mapping[str, Any],
33453341
document: Mapping[str, Any]
33463342
) -> Union[List[Optional[Union[str, float, bool]]], str, float, bool, _Empty, _Undefined]:
3347-
if partitionKeyDefinition["kind"] == "MultiHash":
3343+
if partitionKeyDefinition["kind"] == _PartitionKeyKind.MULTI_HASH:
33483344
ret: List[Optional[Union[str, float, bool]]] = []
33493345
for partition_key_level in partitionKeyDefinition["paths"]:
33503346
# Parses the paths into a list of token each representing a property

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

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,20 @@
2727
import base64
2828
import json
2929
import time
30-
from typing import Any, Dict, Optional
30+
from typing import Any, Dict, List, Optional, Tuple
3131

3232
from ._version import VERSION
3333

3434

35-
def get_user_agent(suffix: Optional[str]) -> str:
35+
def get_user_agent(suffix: Optional[str] = None) -> str:
3636
os_name = safe_user_agent_header(platform.platform())
3737
python_version = safe_user_agent_header(platform.python_version())
3838
user_agent = "azsdk-python-cosmos/{} Python/{} ({})".format(VERSION, python_version, os_name)
3939
if suffix:
4040
user_agent += f" {suffix}"
4141
return user_agent
4242

43-
def get_user_agent_async(suffix: Optional[str]) -> str:
43+
def get_user_agent_async(suffix: Optional[str] = None) -> str:
4444
os_name = safe_user_agent_header(platform.platform())
4545
python_version = safe_user_agent_header(platform.python_version())
4646
user_agent = "azsdk-python-cosmos-async/{} Python/{} ({})".format(VERSION, python_version, os_name)
@@ -49,7 +49,7 @@ def get_user_agent_async(suffix: Optional[str]) -> str:
4949
return user_agent
5050

5151

52-
def safe_user_agent_header(s: Optional[str]) -> str:
52+
def safe_user_agent_header(s: Optional[str] = None) -> str:
5353
if s is None:
5454
s = "unknown"
5555
# remove all white spaces
@@ -59,7 +59,7 @@ def safe_user_agent_header(s: Optional[str]) -> str:
5959
return s
6060

6161

62-
def get_index_metrics_info(delimited_string: Optional[str]) -> Dict[str, Any]:
62+
def get_index_metrics_info(delimited_string: Optional[str] = None) -> Dict[str, Any]:
6363
if delimited_string is None:
6464
return {}
6565
try:
@@ -76,3 +76,73 @@ def get_index_metrics_info(delimited_string: Optional[str]) -> Dict[str, Any]:
7676

7777
def current_time_millis() -> int:
7878
return int(round(time.time() * 1000))
79+
80+
def add_args_to_kwargs(
81+
arg_names: List[str],
82+
args: Tuple[Any, ...],
83+
kwargs: Dict[str, Any]
84+
) -> None:
85+
"""Add positional arguments(args) to keyword argument dictionary(kwargs) using names in arg_names as keys.
86+
To be backward-compatible, some expected positional arguments has to be allowed. This method will verify number of
87+
maximum positional arguments and add them to the keyword argument dictionary(kwargs)
88+
89+
:param List[str] arg_names: The names of positional arguments.
90+
:param Tuple[Any, ...] args: The tuple of positional arguments.
91+
:param Dict[str, Any] kwargs: The dictionary of keyword arguments as reference. This dictionary will be updated.
92+
"""
93+
94+
if len(args) > len(arg_names):
95+
raise ValueError(f"Positional argument is out of range. Expected {len(arg_names)} arguments, "
96+
f"but got {len(args)} instead. Please review argument list in API documentation.")
97+
98+
for name, arg in zip(arg_names, args):
99+
if name in kwargs:
100+
raise ValueError(f"{name} cannot be used as positional and keyword argument at the same time.")
101+
kwargs[name] = arg
102+
103+
104+
def format_list_with_and(items: List[str]) -> str:
105+
"""Format a list of items into a string with commas and 'and' for the last item.
106+
107+
:param List[str] items: The list of items to format.
108+
:return: A formatted string with items separated by commas and 'and' before the last item.
109+
:rtype: str
110+
"""
111+
formatted_items = ""
112+
quoted = [f"'{item}'" for item in items]
113+
if len(quoted) > 2:
114+
formatted_items = ", ".join(quoted[:-1]) + ", and " + quoted[-1]
115+
elif len(quoted) == 2:
116+
formatted_items = " and ".join(quoted)
117+
elif quoted:
118+
formatted_items = quoted[0]
119+
return formatted_items
120+
121+
def verify_exclusive_arguments(
122+
exclusive_keys: List[str],
123+
**kwargs: Dict[str, Any]) -> None:
124+
"""Verify if exclusive arguments are present in kwargs.
125+
For some Cosmos SDK APIs, some arguments are exclusive, or cannot be used at the same time. This method will verify
126+
that and raise an error if exclusive arguments are present.
127+
128+
:param List[str] exclusive_keys: The names of exclusive arguments.
129+
"""
130+
keys_in_kwargs = [key for key in exclusive_keys if key in kwargs and kwargs[key] is not None]
131+
132+
if len(keys_in_kwargs) > 1:
133+
raise ValueError(f"{format_list_with_and(keys_in_kwargs)} are exclusive parameters, "
134+
f"please only set one of them.")
135+
136+
def valid_key_value_exist(
137+
kwargs: Dict[str, Any],
138+
key: str,
139+
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.
141+
142+
:param Dict[str, Any] kwargs: The dictionary of keyword arguments.
143+
:param str key: The key to check.
144+
:param Any invalid_value: The value that is considered invalid. Default is None.
145+
:return: True if the key exists and its value is not None, False otherwise.
146+
:rtype: bool
147+
"""
148+
return key in kwargs and kwargs[key] is not invalid_value

0 commit comments

Comments
 (0)