Skip to content

Commit 5a2aaa4

Browse files
authored
[Cosmos] Patch API implementation (Azure#29497)
* Patch API implementation with samples, tests, changelog * Update documents.py * Update CHANGELOG.md * use StatusCodes instead of direct ints * mark patch as provisional (preview) * update version, typehints, pylint * remove PatchOperationType class * Update __init__.py * fix typehints * Update CHANGELOG.md
1 parent 0235c0b commit 5a2aaa4

File tree

11 files changed

+366
-6
lines changed

11 files changed

+366
-6
lines changed

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
## Release History
22

3-
### 4.3.2 (Unreleased)
3+
### 4.4.0b1 (Unreleased)
44

55
#### Features Added
6+
- Added **preview** partial document update (Patch API) functionality and container methods for patching items with operations. See [PR 29497](https://github.com/Azure/azure-sdk-for-python/pull/29497). For more information on Patch, please see [Azure Cosmos DB Partial Document Update](https://learn.microsoft.com/azure/cosmos-db/partial-document-update).
67

78
#### Breaking Changes
89

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,6 +1731,36 @@ def ReplaceItem(self, document_link, new_document, options=None, **kwargs):
17311731

17321732
return self.Replace(new_document, path, "docs", document_id, None, options, **kwargs)
17331733

1734+
def PatchItem(self, document_link, operations, options=None, **kwargs):
1735+
"""Patches a document and returns it.
1736+
1737+
:param str document_link: The link to the document.
1738+
:param list operations: The operations for the patch request.
1739+
:param dict options: The request options for the request.
1740+
1741+
:return:
1742+
The new Document.
1743+
:rtype:
1744+
dict
1745+
1746+
"""
1747+
path = base.GetPathFromLink(document_link)
1748+
document_id = base.GetResourceIdOrFullNameFromLink(document_link)
1749+
typ = "docs"
1750+
1751+
if options is None:
1752+
options = {}
1753+
1754+
initial_headers = self.default_headers
1755+
headers = base.GetHeaders(self, initial_headers, "patch", path, document_id, typ, options)
1756+
# Patch will use WriteEndpoint since it uses PUT operation
1757+
request_params = _request_object.RequestObject(typ, documents._OperationType.Patch)
1758+
result, self.last_response_headers = self.__Patch(path, request_params, operations, headers, **kwargs)
1759+
1760+
# update session for request mutates data on server side
1761+
self._UpdateSessionIfRequired(headers, result, self.last_response_headers)
1762+
return result
1763+
17341764
def DeleteItem(self, document_link, options=None, **kwargs):
17351765
"""Deletes a document.
17361766
@@ -2309,6 +2339,32 @@ def __Put(self, path, request_params, body, req_headers, **kwargs):
23092339
**kwargs
23102340
)
23112341

2342+
def __Patch(self, path, request_params, operations, req_headers, **kwargs):
2343+
"""Azure Cosmos 'PATCH' http request.
2344+
2345+
:params str path:
2346+
:params ~azure.cosmos.RequestObject request_params:
2347+
:params list operations:
2348+
:params dict req_headers:
2349+
2350+
:return:
2351+
Tuple of (result, headers).
2352+
:rtype:
2353+
tuple of (dict, dict)
2354+
2355+
"""
2356+
request = self.pipeline_client.patch(url=path, headers=req_headers)
2357+
return synchronized_request.SynchronizedRequest(
2358+
client=self,
2359+
request_params=request_params,
2360+
global_endpoint_manager=self._global_endpoint_manager,
2361+
connection_policy=self.connection_policy,
2362+
pipeline_client=self.pipeline_client,
2363+
request=request,
2364+
request_data=operations,
2365+
**kwargs
2366+
)
2367+
23122368
def __Delete(self, path, request_params, req_headers, **kwargs):
23132369
"""Azure Cosmos 'DELETE' http request.
23142370

sdk/cosmos/azure-cosmos/azure/cosmos/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@
1919
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2020
# SOFTWARE.
2121

22-
VERSION = "4.3.2"
22+
VERSION = "4.4.0b1"

sdk/cosmos/azure-cosmos/azure/cosmos/aio/_container.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"""Create, read, update and delete items in the Azure Cosmos DB SQL API service.
2323
"""
2424

25-
from typing import Any, Dict, Optional, Union, cast, Awaitable
25+
from typing import Any, Dict, Optional, Union, cast, Awaitable, List
2626
from azure.core.async_paging import AsyncItemPaged
2727

2828
from azure.core.tracing.decorator import distributed_trace
@@ -522,6 +522,50 @@ async def replace_item(
522522
response_hook(self.client_connection.last_response_headers, result)
523523
return result
524524

525+
@distributed_trace_async
526+
async def patch_item(
527+
self,
528+
item: Union[str, Dict[str, Any]],
529+
partition_key: Union[str, int, float, bool],
530+
patch_operations: List[Dict[str, Any]],
531+
**kwargs: Any
532+
) -> Dict[str, Any]:
533+
""" **Provisional method** Patches the specified item with the provided operations if it
534+
exists in the container.
535+
536+
If the item does not already exist in the container, an exception is raised.
537+
538+
:param item: The ID (name) or dict representing item to be patched.
539+
:type item: Union[str, Dict[str, Any]]
540+
:param partition_key: The partition key of the object to patch.
541+
:type partition_key: Union[str, int, float, bool]
542+
:param patch_operations: The list of patch operations to apply to the item.
543+
:type patch_operations: List[Dict[str, Any]]
544+
:keyword str pre_trigger_include: trigger id to be used as pre operation trigger.
545+
:keyword str post_trigger_include: trigger id to be used as post operation trigger.
546+
:keyword str session_token: Token for use with Session consistency.
547+
:keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request.
548+
:keyword str etag: An ETag value, or the wildcard character (*). Used to check if the resource
549+
has changed, and act according to the condition specified by the `match_condition` parameter.
550+
:keyword ~azure.core.MatchConditions match_condition: The match condition to use upon the etag.
551+
:keyword Callable response_hook: A callable invoked with the response metadata.
552+
:returns: A dict representing the item after the patch operations went through.
553+
:raises ~azure.cosmos.exceptions.CosmosHttpResponseError: The patch operations failed or the item with
554+
given id does not exist.
555+
:rtype: dict[str, Any]
556+
"""
557+
request_options = _build_options(kwargs)
558+
response_hook = kwargs.pop('response_hook', None)
559+
request_options["disableAutomaticIdGeneration"] = True
560+
request_options["partitionKey"] = partition_key
561+
562+
item_link = self._get_document_link(item)
563+
result = await self.client_connection.PatchItem(
564+
document_link=item_link, operations=patch_operations, options=request_options, **kwargs)
565+
if response_hook:
566+
response_hook(self.client_connection.last_response_headers, result)
567+
return result
568+
525569
@distributed_trace_async
526570
async def delete_item(
527571
self,

sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,36 @@ async def ReplaceItem(self, document_link, new_document, options=None, **kwargs)
11601160

11611161
return await self.Replace(new_document, path, "docs", document_id, None, options, **kwargs)
11621162

1163+
async def PatchItem(self, document_link, operations, options=None, **kwargs):
1164+
"""Patches a document and returns it.
1165+
1166+
:param str document_link: The link to the document.
1167+
:param list operations: The operations for the patch request.
1168+
:param dict options: The request options for the request.
1169+
1170+
:return:
1171+
The new Document.
1172+
:rtype:
1173+
dict
1174+
1175+
"""
1176+
path = base.GetPathFromLink(document_link)
1177+
document_id = base.GetResourceIdOrFullNameFromLink(document_link)
1178+
typ = "docs"
1179+
1180+
if options is None:
1181+
options = {}
1182+
1183+
initial_headers = self.default_headers
1184+
headers = base.GetHeaders(self, initial_headers, "patch", path, document_id, typ, options)
1185+
# Patch will use WriteEndpoint since it uses PUT operation
1186+
request_params = _request_object.RequestObject(typ, documents._OperationType.Patch)
1187+
result, self.last_response_headers = await self.__Patch(path, request_params, operations, headers, **kwargs)
1188+
1189+
# update session for request mutates data on server side
1190+
self._UpdateSessionIfRequired(headers, result, self.last_response_headers)
1191+
return result
1192+
11631193
async def ReplaceOffer(self, offer_link, offer, **kwargs):
11641194
"""Replaces an offer and returns it.
11651195
@@ -1263,6 +1293,32 @@ async def __Put(self, path, request_params, body, req_headers, **kwargs):
12631293
**kwargs
12641294
)
12651295

1296+
async def __Patch(self, path, request_params, operations, req_headers, **kwargs):
1297+
"""Azure Cosmos 'PATCH' http request.
1298+
1299+
:params str path:
1300+
:params ~azure.cosmos.RequestObject request_params:
1301+
:params list operations:
1302+
:params dict req_headers:
1303+
1304+
:return:
1305+
Tuple of (result, headers).
1306+
:rtype:
1307+
tuple of (dict, dict)
1308+
1309+
"""
1310+
request = self.pipeline_client.patch(url=path, headers=req_headers)
1311+
return await asynchronous_request.AsynchronousRequest(
1312+
client=self,
1313+
request_params=request_params,
1314+
global_endpoint_manager=self._global_endpoint_manager,
1315+
connection_policy=self.connection_policy,
1316+
pipeline_client=self.pipeline_client,
1317+
request=request,
1318+
request_data=operations,
1319+
**kwargs
1320+
)
1321+
12661322
async def DeleteDatabase(self, database_link, options=None, **kwargs):
12671323
"""Deletes a database.
12681324

sdk/cosmos/azure-cosmos/azure/cosmos/container.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,50 @@ def create_item(
570570
response_hook(self.client_connection.last_response_headers, result)
571571
return result
572572

573+
@distributed_trace
574+
def patch_item(
575+
self,
576+
item: Union[str, Dict[str, Any]],
577+
partition_key: Union[str, int, float, bool],
578+
patch_operations: List[Dict[str, Any]],
579+
**kwargs:Any
580+
) -> Dict[str, Any]:
581+
""" **Provisional method** Patches the specified item with the provided operations if it
582+
exists in the container.
583+
584+
If the item does not already exist in the container, an exception is raised.
585+
586+
:param item: The ID (name) or dict representing item to be patched.
587+
:type item: Union[str, Dict[str, Any]]
588+
:param partition_key: The partition key of the object to patch.
589+
:type partition_key: Union[str, int, float, bool]
590+
:param patch_operations: The list of patch operations to apply to the item.
591+
:type patch_operations: List[Dict[str, Any]]
592+
:keyword str pre_trigger_include: trigger id to be used as pre operation trigger.
593+
:keyword str post_trigger_include: trigger id to be used as post operation trigger.
594+
:keyword str session_token: Token for use with Session consistency.
595+
:keyword dict[str,str] initial_headers: Initial headers to be sent as part of the request.
596+
:keyword str etag: An ETag value, or the wildcard character (*). Used to check if the resource
597+
has changed, and act according to the condition specified by the `match_condition` parameter.
598+
:keyword ~azure.core.MatchConditions match_condition: The match condition to use upon the etag.
599+
:keyword Callable response_hook: A callable invoked with the response metadata.
600+
:returns: A dict representing the item after the patch operations went through.
601+
:raises ~azure.cosmos.exceptions.CosmosHttpResponseError: The patch operations failed or the item with
602+
given id does not exist.
603+
:rtype: dict[str, Any]
604+
"""
605+
request_options = build_options(kwargs)
606+
response_hook = kwargs.pop('response_hook', None)
607+
request_options["disableAutomaticIdGeneration"] = True
608+
request_options["partitionKey"] = partition_key
609+
610+
item_link = self._get_document_link(item)
611+
result = self.client_connection.PatchItem(
612+
document_link=item_link, operations=patch_operations, options=request_options, **kwargs)
613+
if response_hook:
614+
response_hook(self.client_connection.last_response_headers, result)
615+
return result
616+
573617
@distributed_trace
574618
def delete_item(
575619
self,

sdk/cosmos/azure-cosmos/azure/cosmos/documents.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,13 +381,14 @@ class _OperationType(object):
381381
ExecuteJavaScript = "ExecuteJavaScript"
382382
Head = "Head"
383383
HeadFeed = "HeadFeed"
384+
Patch = "Patch"
384385
Query = "Query"
386+
QueryPlan = "QueryPlan"
385387
Read = "Read"
386388
ReadFeed = "ReadFeed"
387389
Recreate = "Recreate"
388390
Replace = "Replace"
389391
SqlQuery = "SqlQuery"
390-
QueryPlan = "QueryPlan"
391392
Update = "Update"
392393
Upsert = "Upsert"
393394

sdk/cosmos/azure-cosmos/samples/document_management.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,27 @@ def upsert_item(container, doc_id):
102102

103103
print('Upserted Item\'s Id is {0}, new subtotal={1}'.format(response['id'], response['subtotal']))
104104

105+
def patch_item(container, doc_id):
106+
print('\n1.7 Patching Item by Id\n')
107+
108+
operations = [
109+
{"op": "add", "path": "/favorite_color", "value": "red"},
110+
{"op": "remove", "path": "/ttl"},
111+
{"op": "replace", "path": "/tax_amount", "value": 14},
112+
{"op": "set", "path": "/items/0/discount", "value": 20.0512},
113+
{"op": "incr", "path": "/total_due", "value": 5},
114+
{"op": "move", "from": "/freight", "path": "/service_addition"}
115+
]
116+
117+
response = container.patch_item(item=doc_id, partition_key=doc_id, patch_operations=operations)
118+
print('Patched Item\'s Id is {0}, new path favorite color={1}, removed path ttl={2}, replaced path tax_amount={3},'
119+
' set path for item at index 0 of discount={4}, increase in path total_due, new total_due={5}, move from path freight={6}'
120+
' to path service_addition={7}'.format(response["id"], response["favorite_color"], response.get("ttl"),
121+
response["tax_amount"], response["items"].get(0).get("discount"),
122+
response["total_due"], response.get("freight"), response["service_addition"]))
105123

106124
def delete_item(container, doc_id):
107-
print('\n1.7 Deleting Item by Id\n')
125+
print('\n1.8 Deleting Item by Id\n')
108126

109127
response = container.delete_item(item=doc_id, partition_key=doc_id)
110128

@@ -175,6 +193,7 @@ def run_sample():
175193
query_items(container, 'SalesOrder1')
176194
replace_item(container, 'SalesOrder1')
177195
upsert_item(container, 'SalesOrder1')
196+
patch_item(container, 'SalesOrder1')
178197
delete_item(container, 'SalesOrder1')
179198

180199
# cleanup database after sample

sdk/cosmos/azure-cosmos/samples/document_management_async.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,27 @@ async def upsert_item(container, doc_id):
120120

121121
print('Upserted Item\'s Id is {0}, new subtotal={1}'.format(response['id'], response['subtotal']))
122122

123+
async def patch_item(container, doc_id):
124+
print('\n1.7 Patching Item by Id\n')
125+
126+
operations = [
127+
{"op": "add", "path": "/favorite_color", "value": "red"},
128+
{"op": "remove", "path": "/ttl"},
129+
{"op": "replace", "path": "/tax_amount", "value": 14},
130+
{"op": "set", "path": "/items/0/discount", "value": 20.0512},
131+
{"op": "incr", "path": "/total_due", "value": 5},
132+
{"op": "move", "from": "/freight", "path": "/service_addition"}
133+
]
134+
135+
response = await container.patch_item(item=doc_id, partition_key=doc_id, patch_operations=operations)
136+
print('Patched Item\'s Id is {0}, new path favorite color={1}, removed path ttl={2}, replaced path tax_amount={3},'
137+
' set path for item at index 0 of discount={4}, increase in path total_due, new total_due={5}, '
138+
'move from path freight={6} to path service_addition={7}'.format(response["id"], response["favorite_color"],
139+
response.get("ttl"), response["tax_amount"], response["items"].get(0).get("discount"),
140+
response["total_due"], response.get("freight"), response["service_addition"]))
123141

124142
async def delete_item(container, doc_id):
125-
print('\n1.7 Deleting Item by Id\n')
143+
print('\n1.8 Deleting Item by Id\n')
126144

127145
await container.delete_item(item=doc_id, partition_key=doc_id)
128146

@@ -195,6 +213,7 @@ async def run_sample():
195213
await query_items(container, 'SalesOrder1')
196214
await replace_item(container, 'SalesOrder1')
197215
await upsert_item(container, 'SalesOrder1')
216+
await patch_item(container, 'SalesOrder1')
198217
await delete_item(container, 'SalesOrder1')
199218

200219
# cleanup database after sample

0 commit comments

Comments
 (0)