Skip to content

Commit 4822e5c

Browse files
authored
Computed properties Python Support (#33626)
* add keyword for computed properties Adds the keyword for computed properties in container creation * Add keyword text Added keyword description for computed properties * added support for computed properties in python This adds support for computed properties in the python SDK. * remove unecessary lines removed code used for local testing * update changelog Updated changelog * update changelog * Broken Link fix pipeline complained about link in changelog being broken, updated the link. * update changelog * update docstrings for computed properties keyword Old docstrings descriptions were too long. Updated to better match the docstrings in java and provide a link to access further information about computed properties.
1 parent 56b5c3a commit 4822e5c

File tree

5 files changed

+159
-2
lines changed

5 files changed

+159
-2
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.5.2b4 (Unreleased)
44

55
#### Features Added
6+
* Added **preview** support for Computed Properties on Python SDK (Must be enabled on the account level before it can be used). See [PR 33626](https://github.com/Azure/azure-sdk-for-python/pull/33626).
67

78
#### Breaking Changes
89

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ async def create_container(
175175
has changed, and act according to the condition specified by the `match_condition` parameter.
176176
:keyword match_condition: The match condition to use upon the etag.
177177
:paramtype match_condition: ~azure.core.MatchConditions
178+
:keyword List[Dict[str, str]] computed_properties: Sets The computed properties for this container in the Azure
179+
Cosmos DB Service. For more Information on how to use computed properties visit
180+
`here: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet`
178181
:keyword response_hook: A callable invoked with the response metadata.
179182
:paramtype response_hook: Callable[[Dict[str, str], Dict[str, Any]], None]
180183
:keyword int analytical_storage_ttl: Analytical store time to live (TTL) for items in the container. A value of
@@ -225,6 +228,9 @@ async def create_container(
225228
analytical_storage_ttl = kwargs.pop("analytical_storage_ttl", None)
226229
if analytical_storage_ttl is not None:
227230
definition["analyticalStorageTtl"] = analytical_storage_ttl
231+
computed_properties = kwargs.pop('computed_properties', None)
232+
if computed_properties:
233+
definition["computedProperties"] = computed_properties
228234

229235
request_options = _build_options(kwargs)
230236
response_hook = kwargs.pop('response_hook', None)
@@ -269,6 +275,9 @@ async def create_container_if_not_exists(
269275
has changed, and act according to the condition specified by the `match_condition` parameter.
270276
:keyword match_condition: The match condition to use upon the etag.
271277
:paramtype match_condition: ~azure.core.MatchConditions
278+
:keyword List[Dict[str, str]] computed_properties: Sets The computed properties for this container in the Azure
279+
Cosmos DB Service. For more Information on how to use computed properties visit
280+
`here: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet`
272281
:keyword response_hook: A callable invoked with the response metadata.
273282
:paramtype response_hook: Callable[[Dict[str, str], Dict[str, Any]], None]
274283
:keyword int analytical_storage_ttl: Analytical store time to live (TTL) for items in the container. A value of
@@ -284,6 +293,7 @@ async def create_container_if_not_exists(
284293
conflict_resolution_policy = kwargs.pop('conflict_resolution_policy', None)
285294
analytical_storage_ttl = kwargs.pop("analytical_storage_ttl", None)
286295
offer_throughput = kwargs.pop('offer_throughput', None)
296+
computed_properties = kwargs.pop("computed_properties", None)
287297
try:
288298
container_proxy = self.get_container_client(id)
289299
await container_proxy.read(**kwargs)
@@ -297,7 +307,8 @@ async def create_container_if_not_exists(
297307
offer_throughput=offer_throughput,
298308
unique_key_policy=unique_key_policy,
299309
conflict_resolution_policy=conflict_resolution_policy,
300-
analytical_storage_ttl=analytical_storage_ttl
310+
analytical_storage_ttl=analytical_storage_ttl,
311+
computed_properties=computed_properties
301312
)
302313

303314
def get_container_client(self, container: Union[str, ContainerProxy, Dict[str, Any]]) -> ContainerProxy:

sdk/cosmos/azure-cosmos/azure/cosmos/database.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ def create_container( # pylint:disable=docstring-missing-param
184184
:keyword int analytical_storage_ttl: Analytical store time to live (TTL) for items in the container. A value of
185185
None leaves analytical storage off and a value of -1 turns analytical storage on with no TTL. Please
186186
note that analytical storage can only be enabled on Synapse Link enabled accounts.
187+
:keyword List[Dict[str, str]] computed_properties: Sets The computed properties for this container in the Azure
188+
Cosmos DB Service. For more Information on how to use computed properties visit
189+
`here: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet`
187190
:returns: A `ContainerProxy` instance representing the new container.
188191
:raises ~azure.cosmos.exceptions.CosmosHttpResponseError: The container creation failed.
189192
:rtype: ~azure.cosmos.ContainerProxy
@@ -224,7 +227,9 @@ def create_container( # pylint:disable=docstring-missing-param
224227
analytical_storage_ttl = kwargs.pop("analytical_storage_ttl", None)
225228
if analytical_storage_ttl is not None:
226229
definition["analyticalStorageTtl"] = analytical_storage_ttl
227-
230+
computed_properties = kwargs.pop('computed_properties', None)
231+
if computed_properties:
232+
definition["computedProperties"] = computed_properties
228233
request_options = build_options(kwargs)
229234
response_hook = kwargs.pop('response_hook', None)
230235
if populate_query_metrics is not None:
@@ -279,11 +284,15 @@ def create_container_if_not_exists( # pylint:disable=docstring-missing-param
279284
:keyword int analytical_storage_ttl: Analytical store time to live (TTL) for items in the container. A value of
280285
None leaves analytical storage off and a value of -1 turns analytical storage on with no TTL. Please
281286
note that analytical storage can only be enabled on Synapse Link enabled accounts.
287+
:keyword List[Dict[str, str]] computed_properties: Sets The computed properties for this container in the Azure
288+
Cosmos DB Service. For more Information on how to use computed properties visit
289+
`here: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet`
282290
:returns: A `ContainerProxy` instance representing the container.
283291
:raises ~azure.cosmos.exceptions.CosmosHttpResponseError: The container read or creation failed.
284292
:rtype: ~azure.cosmos.ContainerProxy
285293
"""
286294
analytical_storage_ttl = kwargs.pop("analytical_storage_ttl", None)
295+
computed_properties = kwargs.pop("computed_properties", None)
287296
try:
288297
container_proxy = self.get_container_client(id)
289298
container_proxy.read(
@@ -302,6 +311,7 @@ def create_container_if_not_exists( # pylint:disable=docstring-missing-param
302311
unique_key_policy=unique_key_policy,
303312
conflict_resolution_policy=conflict_resolution_policy,
304313
analytical_storage_ttl=analytical_storage_ttl,
314+
computed_properties=computed_properties
305315
)
306316

307317
@distributed_trace

sdk/cosmos/azure-cosmos/test/test_query.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,73 @@ def test_continuation_token_size_limit_query(self):
798798
self.assertLessEqual(len(token.encode('utf-8')), 1024)
799799
self.created_db.delete_container(container)
800800

801+
def test_computed_properties_query(self):
802+
computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"},
803+
{'name': "cp_power",
804+
'query': "SELECT VALUE POWER(c.val, 2) FROM c"},
805+
{'name': "cp_str_len", 'query': "SELECT VALUE LENGTH(c.stringProperty) FROM c"}]
806+
items = [
807+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixOne', 'db_group': 'GroUp1'},
808+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixTwo', 'db_group': 'GrOUp1'},
809+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord1', 'db_group': 'GroUp2'},
810+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord2', 'db_group': 'groUp1'},
811+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord3', 'db_group': 'GroUp3'},
812+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord4', 'db_group': 'GrOUP1'},
813+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord5', 'db_group': 'GroUp2'},
814+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 0, 'stringProperty': 'randomWord6', 'db_group': 'group1'},
815+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 3, 'stringProperty': 'randomWord7', 'db_group': 'group2'},
816+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 2, 'stringProperty': 'randomWord8', 'db_group': 'GroUp3'}
817+
]
818+
created_database = self.config.create_database_if_not_exist(self.client)
819+
created_collection = self.created_db.create_container_if_not_exists(
820+
"computed_properties_query_test_" + str(uuid.uuid4()), PartitionKey(path="/pk")
821+
, computed_properties=computed_properties)
822+
823+
# Create Items
824+
for item in items:
825+
created_collection.create_item(body=item)
826+
# Check that computed properties were properly sent
827+
self.assertListEqual(computed_properties, created_collection._get_properties()["computedProperties"])
828+
829+
# Test 0: Negative test, test if using non-existent computed property
830+
queried_items = list(
831+
created_collection.query_items(query='Select * from c Where c.cp_upper = "GROUP2"',
832+
partition_key="test"))
833+
self.assertEqual(len(queried_items), 0)
834+
835+
# Test 1: Test first computed property
836+
queried_items = list(
837+
created_collection.query_items(query='Select * from c Where c.cp_lower = "group1"', partition_key="test"))
838+
self.assertEqual(len(queried_items), 5)
839+
840+
# Test 1 Negative: Test if using non-existent string in group property returns nothing
841+
queried_items = list(
842+
created_collection.query_items(query='Select * from c Where c.cp_lower = "group4"', partition_key="test"))
843+
self.assertEqual(len(queried_items), 0)
844+
845+
# Test 2: Test second computed property
846+
queried_items = list(
847+
created_collection.query_items(query='Select * from c Where c.cp_power = 25', partition_key="test"))
848+
self.assertEqual(len(queried_items), 7)
849+
850+
# Test 2 Negative: Test Non-Existent POWER
851+
queried_items = list(
852+
created_collection.query_items(query='Select * from c Where c.cp_power = 16', partition_key="test"))
853+
self.assertEqual(len(queried_items), 0)
854+
855+
# Test 3: Test Third Computed Property
856+
queried_items = list(
857+
created_collection.query_items(query='Select * from c Where c.cp_str_len = 9', partition_key="test"))
858+
self.assertEqual(len(queried_items), 2)
859+
860+
# Test 3 Negative: Test Str length that isn't there
861+
queried_items = list(
862+
created_collection.query_items(query='Select * from c Where c.cp_str_len = 3', partition_key="test"))
863+
self.assertEqual(len(queried_items), 0)
864+
865+
self.client.delete_database(created_database)
866+
867+
801868
def _MockNextFunction(self):
802869
if self.count < len(self.payloads):
803870
item, result = self.get_mock_result(self.payloads, self.count)

sdk/cosmos/azure-cosmos/test/test_query_async.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,74 @@ async def test_continuation_token_size_limit_query_async(self):
844844
assert len(token.encode('utf-8')) <= 1024
845845
await self.created_db.delete_container(container)
846846

847+
@pytest.mark.asyncio
848+
async def test_computed_properties_query(self):
849+
computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"},
850+
{'name': "cp_power",
851+
'query': "SELECT VALUE POWER(c.val, 2) FROM c"},
852+
{'name': "cp_str_len", 'query': "SELECT VALUE LENGTH(c.stringProperty) FROM c"}]
853+
items = [
854+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixOne', 'db_group': 'GroUp1'},
855+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixTwo', 'db_group': 'GrOUp1'},
856+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord1', 'db_group': 'GroUp2'},
857+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord2', 'db_group': 'groUp1'},
858+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord3', 'db_group': 'GroUp3'},
859+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord4', 'db_group': 'GrOUP1'},
860+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord5', 'db_group': 'GroUp2'},
861+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 0, 'stringProperty': 'randomWord6', 'db_group': 'group1'},
862+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 3, 'stringProperty': 'randomWord7', 'db_group': 'group2'},
863+
{'id': str(uuid.uuid4()), 'pk': 'test', 'val': 2, 'stringProperty': 'randomWord8', 'db_group': 'GroUp3'}
864+
]
865+
created_database = await self.config.create_database_if_not_exist(self.client)
866+
created_collection = await self.created_db.create_container_if_not_exists(
867+
"computed_properties_query_test_" + str(uuid.uuid4()), PartitionKey(path="/pk")
868+
, computed_properties=computed_properties)
869+
870+
# Create Items
871+
for item in items:
872+
await created_collection.create_item(body=item)
873+
874+
# Check if computed properties were set
875+
container_properties = await created_collection._get_properties()
876+
assert computed_properties == container_properties["computedProperties"]
877+
878+
# Test 0: Negative test, test if using non-existent computed property
879+
queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_upper = "GROUP2"',
880+
partition_key="test")]
881+
assert len(queried_items) == 0
882+
883+
# Test 1: Test first computed property
884+
queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_lower = "group1"',
885+
partition_key="test")]
886+
assert len(queried_items) == 5
887+
888+
# Test 1 Negative: Test if using non-existent string in group property returns nothing
889+
queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_lower = "group4"',
890+
partition_key="test")]
891+
assert len(queried_items) == 0
892+
893+
# Test 2: Test second computed property
894+
queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_power = 25',
895+
partition_key="test")]
896+
assert len(queried_items) == 7
897+
898+
# Test 2 Negative: Test Non-Existent POWER
899+
queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_power = 16',
900+
partition_key="test")]
901+
assert len(queried_items) == 0
902+
903+
# Test 3: Test Third Computed Property
904+
queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_str_len = 9',
905+
partition_key="test")]
906+
assert len(queried_items) == 2
907+
908+
# Test 3 Negative: Test Str length that isn't there
909+
queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_str_len = 3',
910+
partition_key="test")]
911+
assert len(queried_items) == 0
912+
913+
await self.client.delete_database(created_database)
914+
847915
def _MockNextFunction(self):
848916
if self.count < len(self.payloads):
849917
item, result = self.get_mock_result(self.payloads, self.count)

0 commit comments

Comments
 (0)