Skip to content

Commit f7c471d

Browse files
Add support for Conditional Metadata Rules API
1 parent e75f126 commit f7c471d

File tree

3 files changed

+313
-0
lines changed

3 files changed

+313
-0
lines changed

cloudinary/api.py

+73
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from cloudinary import utils
1010
from cloudinary.api_client.call_api import (
1111
call_metadata_api,
12+
call_metadata_rules_api,
1213
call_api,
1314
call_json_api,
1415
_call_v2_api,
@@ -1581,6 +1582,78 @@ def reorder_metadata_fields(order_by, direction=None, **options):
15811582
return call_metadata_api('put', uri, params, **options)
15821583

15831584

1585+
def list_metadata_rules(**options):
1586+
"""
1587+
Returns a list of all metadata rules definitions.
1588+
1589+
See: https://cloudinary.com/documentation/admin_api#get_metadata_rules
1590+
1591+
:param options: Additional optional parameters (none currently recognized).
1592+
:return: A list of metadata rules.
1593+
:rtype: Response
1594+
"""
1595+
return call_metadata_rules_api("get", [], {}, **options)
1596+
1597+
def __metadata_rule_params(rule):
1598+
"""
1599+
Builds the parameters needed for creating or updating a metadata rule.
1600+
1601+
:param rule: The rule definition.
1602+
:type rule: dict
1603+
:return: The relevant key-value pairs.
1604+
:rtype: dict
1605+
:internal
1606+
"""
1607+
return only(rule, "external_id", "metadata_field_id", "condition", "result", "name", "state")
1608+
1609+
1610+
def add_metadata_rule(rule, **options):
1611+
"""
1612+
Creates a new metadata rule definition.
1613+
1614+
See: https://cloudinary.com/documentation/admin_api#create_a_metadata_rule
1615+
1616+
:param rule: The rule to add.
1617+
:type rule: dict
1618+
:param options: Additional optional parameters (none currently recognized).
1619+
:return: The created metadata rule.
1620+
:rtype: Response
1621+
"""
1622+
return call_metadata_rules_api("post", [], __metadata_rule_params(rule), **options)
1623+
1624+
def update_metadata_rule(rule_external_id, rule, **options):
1625+
"""
1626+
Updates a metadata rule by external id.
1627+
1628+
See: https://cloudinary.com/documentation/admin_api#update_a_metadata_rule_by_id
1629+
1630+
:param rule_external_id: The ID of the metadata rule to update.
1631+
:type rule_external_id: str
1632+
:param rule: The rule definition to update.
1633+
:type rule: dict
1634+
:param options: Additional optional parameters (none currently recognized).
1635+
:return: The updated metadata rule.
1636+
:rtype: Response
1637+
"""
1638+
uri = [rule_external_id]
1639+
return call_metadata_rules_api("put", uri, __metadata_rule_params(rule), **options)
1640+
1641+
def delete_metadata_rule(rule_external_id, **options):
1642+
"""
1643+
Deletes a metadata rule definition.
1644+
1645+
See: https://cloudinary.com/documentation/admin_api#delete_a_metadata_rule_by_id
1646+
1647+
:param rule_external_id: The external ID of the rule to delete.
1648+
:type rule_external_id: str
1649+
:param options: Additional optional parameters (none currently recognized).
1650+
:return: An array with a "success" key. true value indicates a successful deletion.
1651+
:rtype: Response
1652+
"""
1653+
uri = [rule_external_id]
1654+
return call_metadata_rules_api("delete", uri, {}, **options)
1655+
1656+
15841657
def analyze(input_type, analysis_type, uri=None, **options):
15851658
"""
15861659
Analyzes an asset with the requested analysis type.

cloudinary/api_client/call_api.py

+13
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ def call_metadata_api(method, uri, params, **options):
2121
return call_json_api(method, uri, params, **options)
2222

2323

24+
def call_metadata_rules_api(method, uri, params, **options):
25+
"""Private function that assists with performing an API call to the
26+
metadata_rules part of the Admin API
27+
:param method: The HTTP method. Valid methods: get, post, put, delete
28+
:param uri: REST endpoint of the API (without 'metadata_rules')
29+
:param params: Query/body parameters passed to the method
30+
:param options: Additional options
31+
:rtype: Response
32+
"""
33+
uri = ["metadata_rules"] + (uri or [])
34+
return call_json_api(method, uri, params, **options)
35+
36+
2437
def call_json_api(method, uri, params, **options):
2538
data=None
2639
if method.upper() != 'GET':

test/test_metadata_rules.py

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import unittest
2+
from six import text_type
3+
from urllib3 import disable_warnings
4+
5+
import cloudinary
6+
from cloudinary import api
7+
from cloudinary.exceptions import BadRequest, NotFound
8+
from test.helper_test import (
9+
UNIQUE_TEST_ID, get_uri, get_params, get_method, api_response_mock, ignore_exception, get_json_body,
10+
URLLIB3_REQUEST, patch
11+
)
12+
13+
MOCK_RESPONSE = api_response_mock()
14+
15+
# External IDs for metadata fields and metadata_rules that should be created and later deleted
16+
EXTERNAL_ID_ENUM = "metadata_external_id_enum_{}".format(UNIQUE_TEST_ID)
17+
EXTERNAL_ID_SET = "metadata_external_id_set_{}".format(UNIQUE_TEST_ID)
18+
EXTERNAL_ID_METADATA_RULE_GENERAL = "metadata_rule_id_general_{}".format(UNIQUE_TEST_ID)
19+
EXTERNAL_ID_METADATA_RULE_DELETE = "metadata_rule_id_deletion_{}".format(UNIQUE_TEST_ID)
20+
21+
# Sample datasource data
22+
DATASOURCE_ENTRY_EXTERNAL_ID = "metadata_datasource_entry_external_id{}".format(UNIQUE_TEST_ID)
23+
24+
disable_warnings()
25+
26+
class MetadataRulesTest(unittest.TestCase):
27+
@patch(URLLIB3_REQUEST)
28+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
29+
def test01_list_metadata_rules(self, mocker):
30+
"""Test getting a list of all metadata rules"""
31+
32+
mocker.return_value = MOCK_RESPONSE
33+
api.list_metadata_rules()
34+
35+
self.assertTrue(get_uri(mocker).endswith("/metadata_rules"))
36+
self.assertEqual(get_method(mocker), "GET")
37+
38+
39+
@patch(URLLIB3_REQUEST)
40+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
41+
def test02_create_metadata_rule(self, mocker):
42+
"""Test creating an and metadata rule"""
43+
44+
mocker.return_value = MOCK_RESPONSE
45+
api.add_metadata_rule({
46+
"metadata_field_id": EXTERNAL_ID_ENUM,
47+
"condition": { "metadata_field_id": EXTERNAL_ID_SET, "equals": DATASOURCE_ENTRY_EXTERNAL_ID },
48+
"result": { "enable": True, "activate_values": "all" },
49+
"name": EXTERNAL_ID_ENUM
50+
})
51+
52+
self.assertTrue(get_uri(mocker).endswith("/metadata_rules"))
53+
self.assertEqual(get_method(mocker), "POST")
54+
self.assertEqual(get_json_body(mocker), {
55+
"metadata_field_id": EXTERNAL_ID_ENUM,
56+
"name": EXTERNAL_ID_ENUM,
57+
"condition": { "metadata_field_id": EXTERNAL_ID_SET, "equals": DATASOURCE_ENTRY_EXTERNAL_ID },
58+
"result": { "enable": True, "activate_values": "all" },
59+
})
60+
61+
62+
@patch(URLLIB3_REQUEST)
63+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
64+
def test03_create_and_metadata_rule(self, mocker):
65+
"""Test creating an and metadata rule"""
66+
67+
mocker.return_value = MOCK_RESPONSE
68+
api.add_metadata_rule({
69+
"metadata_field_id": EXTERNAL_ID_ENUM,
70+
"condition": {"and": [
71+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": [DATASOURCE_ENTRY_EXTERNAL_ID]},
72+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": ["value2"]}
73+
]},
74+
"result": { "enable": True, "apply_value": {"value": "value1_and_value2","mode": "default"}},
75+
"name": EXTERNAL_ID_ENUM + "_AND"
76+
})
77+
78+
self.assertTrue(get_uri(mocker).endswith("/metadata_rules"))
79+
self.assertEqual(get_method(mocker), "POST")
80+
self.assertEqual(get_json_body(mocker), {
81+
"metadata_field_id": EXTERNAL_ID_ENUM,
82+
"name": EXTERNAL_ID_ENUM + "_AND" ,
83+
"condition": {"and": [
84+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": [DATASOURCE_ENTRY_EXTERNAL_ID]},
85+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": ["value2"]}
86+
]},
87+
"result": { "enable": True, "apply_value": {"value": "value1_and_value2","mode": "default"}},
88+
})
89+
90+
91+
@patch(URLLIB3_REQUEST)
92+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
93+
def test04_create_or_metadata_rule(self,mocker):
94+
"""Test creating an or metadata rule"""
95+
96+
mocker.return_value = MOCK_RESPONSE
97+
api.add_metadata_rule({
98+
"metadata_field_id": EXTERNAL_ID_ENUM,
99+
"condition": {"or": [
100+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": [DATASOURCE_ENTRY_EXTERNAL_ID]},
101+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": ["value2"]}
102+
]},
103+
"result": { "enable": True, "apply_value": {"value": "value1_or_value2","mode": "default"}},
104+
"name": EXTERNAL_ID_ENUM + "_OR"
105+
})
106+
107+
self.assertTrue(get_uri(mocker).endswith("/metadata_rules"))
108+
self.assertEqual(get_method(mocker), "POST")
109+
self.assertEqual(get_json_body(mocker), {
110+
"metadata_field_id": EXTERNAL_ID_ENUM,
111+
"name": EXTERNAL_ID_ENUM + "_OR",
112+
"condition": {"or": [
113+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": [DATASOURCE_ENTRY_EXTERNAL_ID]},
114+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": ["value2"]}
115+
]},
116+
"result": { "enable": True, "apply_value": {"value": "value1_or_value2","mode": "default"}},
117+
})
118+
119+
120+
@patch(URLLIB3_REQUEST)
121+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
122+
def test05_create_and_or_metadata_rule(self, mocker):
123+
"""Test creating an and+or metadata rule"""
124+
125+
mocker.return_value = MOCK_RESPONSE
126+
api.add_metadata_rule({
127+
"metadata_field_id": EXTERNAL_ID_ENUM,
128+
"condition": {"and": [
129+
{ "metadata_field_id": EXTERNAL_ID_SET, "populated": True },
130+
{"or": [
131+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": [DATASOURCE_ENTRY_EXTERNAL_ID]},
132+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": ["value2"]}
133+
]}
134+
]},
135+
"result": { "enable": True, "activate_values": "all"},
136+
"name": EXTERNAL_ID_ENUM + "_AND_OR"
137+
})
138+
139+
self.assertTrue(get_uri(mocker).endswith("/metadata_rules"))
140+
self.assertEqual(get_method(mocker), "POST")
141+
self.assertEqual(get_json_body(mocker), {
142+
"metadata_field_id": EXTERNAL_ID_ENUM,
143+
"name": EXTERNAL_ID_ENUM + "_AND_OR",
144+
"condition": {"and": [
145+
{ "metadata_field_id": EXTERNAL_ID_SET, "populated": True },
146+
{"or": [
147+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": [DATASOURCE_ENTRY_EXTERNAL_ID]},
148+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": ["value2"]}
149+
]}
150+
]},
151+
"result": { "enable": True, "activate_values": "all"},
152+
})
153+
154+
155+
@patch(URLLIB3_REQUEST)
156+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
157+
def test06_create_or_and_metadata_rule(self,mocker):
158+
"""Test creating an or+and metadata rule"""
159+
160+
mocker.return_value = MOCK_RESPONSE
161+
api.add_metadata_rule({
162+
"metadata_field_id": EXTERNAL_ID_ENUM,
163+
"condition": {"or": [
164+
{"metadata_field_id": EXTERNAL_ID_SET, "populated": False },
165+
{"and": [
166+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": [DATASOURCE_ENTRY_EXTERNAL_ID]},
167+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": ["value2"]}
168+
]}
169+
]},
170+
"result": { "enable": True, "activate_values": {"external_ids": ["value1","value2"]}},
171+
"name": EXTERNAL_ID_ENUM + "_OR_AND"
172+
})
173+
174+
self.assertTrue(get_uri(mocker).endswith("/metadata_rules"))
175+
self.assertEqual(get_method(mocker), "POST")
176+
self.assertEqual(get_json_body(mocker), {
177+
"metadata_field_id": EXTERNAL_ID_ENUM,
178+
"name": EXTERNAL_ID_ENUM + "_OR_AND",
179+
"condition": {"or": [
180+
{"metadata_field_id": EXTERNAL_ID_SET, "populated": False },
181+
{"and": [
182+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": [DATASOURCE_ENTRY_EXTERNAL_ID]},
183+
{"metadata_field_id": EXTERNAL_ID_SET,"includes": ["value2"]}
184+
]}
185+
]},
186+
"result": { "enable": True, "activate_values": {"external_ids": ["value1","value2"]}},
187+
})
188+
189+
@patch(URLLIB3_REQUEST)
190+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
191+
def test07_update_metadata_rule(self,mocker):
192+
"""Update a metadata rule by external id"""
193+
mocker.return_value = MOCK_RESPONSE
194+
195+
new_name = "update_metadata_rule_new_name{}".format(EXTERNAL_ID_METADATA_RULE_GENERAL)
196+
197+
api.update_metadata_rule(EXTERNAL_ID_METADATA_RULE_GENERAL, {
198+
"metadata_field_id": EXTERNAL_ID_ENUM,
199+
"name": new_name + "_inactive",
200+
"condition": {},
201+
"result": {},
202+
"state": "inactive"
203+
})
204+
205+
target_uri = "/metadata_rules/{}".format(EXTERNAL_ID_METADATA_RULE_GENERAL)
206+
self.assertTrue(get_uri(mocker).endswith(target_uri))
207+
self.assertEqual(get_method(mocker), "PUT")
208+
self.assertEqual(get_params(mocker).get("state"), "inactive")
209+
self.assertEqual(get_params(mocker).get("name"), new_name + "_inactive")
210+
211+
@patch(URLLIB3_REQUEST)
212+
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
213+
def test08_delete_metadata_rule(self, mocker):
214+
"""Test deleting a metadata rule definition by its external id."""
215+
216+
mocker.return_value = MOCK_RESPONSE
217+
api.delete_metadata_rule(EXTERNAL_ID_METADATA_RULE_DELETE)
218+
219+
target_uri = "/metadata_rules/{}".format(EXTERNAL_ID_METADATA_RULE_DELETE)
220+
self.assertTrue(get_uri(mocker).endswith(target_uri))
221+
self.assertEqual(get_method(mocker), "DELETE")
222+
223+
self.assertEqual(get_json_body(mocker), {})
224+
225+
226+
if __name__ == "__main__":
227+
unittest.main()

0 commit comments

Comments
 (0)