cloudinary 1.45.0__tar.gz → 1.46.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudinary-1.45.0/cloudinary.egg-info → cloudinary-1.46.0}/PKG-INFO +1 -1
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/__init__.py +1 -1
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api.py +184 -1
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api_client/call_account_api.py +8 -2
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api_client/call_api.py +35 -8
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api_client/execute_request.py +6 -3
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/provisioning/__init__.py +1 -1
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/provisioning/account.py +62 -6
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/uploader.py +13 -2
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/utils.py +57 -2
- {cloudinary-1.45.0 → cloudinary-1.46.0/cloudinary.egg-info}/PKG-INFO +1 -1
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary.egg-info/SOURCES.txt +3 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/pyproject.toml +1 -1
- {cloudinary-1.45.0 → cloudinary-1.46.0}/setup.py +1 -1
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_api.py +5 -2
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_archive.py +3 -0
- cloudinary-1.46.0/test/test_notifications.py +321 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_provisioning_api.py +191 -15
- cloudinary-1.46.0/test/test_request_id.py +98 -0
- cloudinary-1.46.0/test/test_triggers.py +162 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_uploader.py +3 -1
- {cloudinary-1.45.0 → cloudinary-1.46.0}/LICENSE.txt +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/MANIFEST.in +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/README.md +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api_client/__init__.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api_client/tcp_keep_alive_manager.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/auth_token.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/__init__.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/adapter/__init__.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/adapter/cache_adapter.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/adapter/key_value_cache_adapter.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/responsive_breakpoints_cache.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/storage/__init__.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/storage/file_system_key_value_storage.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/storage/key_value_storage.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/compat.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/exceptions.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/forms.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/http_client.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/models.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/poster/__init__.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/poster/encode.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/poster/streaminghttp.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/provisioning/account_config.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/search.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/search_folders.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/html/cloudinary_cors.html +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/canvas-to-blob.min.js +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.cloudinary.js +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload-image.js +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload-process.js +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload-validate.js +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload.js +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.iframe-transport.js +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.ui.widget.js +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/load-image.all.min.js +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/templates/cloudinary_direct_upload.html +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/templates/cloudinary_includes.html +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/templates/cloudinary_js_config.html +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/templatetags/__init__.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/templatetags/cloudinary.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary.egg-info/dependency_links.txt +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary.egg-info/not-zip-safe +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary.egg-info/requires.txt +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary.egg-info/top_level.txt +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/setup.cfg +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_api_authorization.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_auth_token.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_cloudinary_resource.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_config.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_expression_normalization.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_http_client.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_image.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_metadata.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_metadata_rules.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_search.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_streaming_profiles.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_utils.py +0 -0
- {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_video.py +0 -0
|
@@ -38,7 +38,7 @@ CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAA
|
|
|
38
38
|
URI_SCHEME = "cloudinary"
|
|
39
39
|
API_VERSION = "v1_1"
|
|
40
40
|
|
|
41
|
-
VERSION = "1.
|
|
41
|
+
VERSION = "1.46.0"
|
|
42
42
|
|
|
43
43
|
_USER_PLATFORM_DETAILS = "; ".join((platform(), "Python {}".format(python_version())))
|
|
44
44
|
|
|
@@ -1323,6 +1323,189 @@ def update_streaming_profile(name, **options):
|
|
|
1323
1323
|
return call_json_api('PUT', uri, params, **options)
|
|
1324
1324
|
|
|
1325
1325
|
|
|
1326
|
+
def triggers(**options):
|
|
1327
|
+
"""
|
|
1328
|
+
Lists all notification triggers.
|
|
1329
|
+
|
|
1330
|
+
:param options: Additional options.
|
|
1331
|
+
:keyword str event_type: Restricts the list to triggers of a single event type.
|
|
1332
|
+
:return: A dictionary with a "triggers" key and a "total" count.
|
|
1333
|
+
:rtype: Response
|
|
1334
|
+
"""
|
|
1335
|
+
params = {"event_type": options.pop("event_type", None)}
|
|
1336
|
+
return call_json_api("get", ["triggers"], params, **options)
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def create_trigger(uri, event_type, uri_type=None, **options):
|
|
1340
|
+
"""
|
|
1341
|
+
Creates a notification trigger.
|
|
1342
|
+
|
|
1343
|
+
:param uri: The destination of the notification. An https:// webhook URL, or a
|
|
1344
|
+
poll:// destination when uri_type is "poll" - either "poll://<channel>"
|
|
1345
|
+
for a named channel or "poll://*" for an anonymous one addressed by batch_id.
|
|
1346
|
+
:type uri: str
|
|
1347
|
+
:param event_type: The event that fires the trigger. One of:
|
|
1348
|
+
"all", "access_control_changed", "bulk_refresh_auto_fetch",
|
|
1349
|
+
"create_folder", "delete", "delete_by_token", "delete_folder",
|
|
1350
|
+
"eager", "error", "explode", "generate_archive", "info",
|
|
1351
|
+
"invalidate_custom_cdn", "moderation", "moderation_summary",
|
|
1352
|
+
"move", "move_or_rename_asset_folder", "multi", "publish",
|
|
1353
|
+
"rename", "report", "resource_context_changed",
|
|
1354
|
+
"resource_display_name_changed", "resource_metadata_changed",
|
|
1355
|
+
"resource_tags_changed", "restore_asset_version", "sprite",
|
|
1356
|
+
"upload".
|
|
1357
|
+
The server owns this list and may extend it; values are passed
|
|
1358
|
+
through unvalidated.
|
|
1359
|
+
:type event_type: str
|
|
1360
|
+
:param uri_type: How the notification is delivered:
|
|
1361
|
+
- "webhook" (the default): delivers the notification as an HTTP POST
|
|
1362
|
+
to the destination URL.
|
|
1363
|
+
- "flow": dispatches the notification to a Cloudinary flow.
|
|
1364
|
+
- "poll": buffers the notification for retrieval with `notifications`
|
|
1365
|
+
instead of an outbound HTTP request.
|
|
1366
|
+
:type uri_type: str, optional
|
|
1367
|
+
:param options: Additional options.
|
|
1368
|
+
:return: The created trigger.
|
|
1369
|
+
:rtype: Response
|
|
1370
|
+
"""
|
|
1371
|
+
params = {"uri": uri, "event_type": event_type, "uri_type": uri_type}
|
|
1372
|
+
return call_json_api("post", ["triggers"], params, **options)
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def update_trigger(trigger_id, new_uri, **options):
|
|
1376
|
+
"""
|
|
1377
|
+
Updates the destination URI of an existing trigger.
|
|
1378
|
+
|
|
1379
|
+
:param trigger_id: The ID of the trigger to update.
|
|
1380
|
+
:type trigger_id: str
|
|
1381
|
+
:param new_uri: The new destination URI.
|
|
1382
|
+
:type new_uri: str
|
|
1383
|
+
:param options: Additional options.
|
|
1384
|
+
:return: A dictionary with a "message" key.
|
|
1385
|
+
:rtype: Response
|
|
1386
|
+
"""
|
|
1387
|
+
return call_json_api("put", ["triggers", trigger_id], {"new_uri": new_uri}, **options)
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def delete_trigger(trigger_id, **options):
|
|
1391
|
+
"""
|
|
1392
|
+
Deletes a notification trigger.
|
|
1393
|
+
|
|
1394
|
+
:param trigger_id: The ID of the trigger to delete.
|
|
1395
|
+
:type trigger_id: str
|
|
1396
|
+
:param options: Additional options.
|
|
1397
|
+
:return: A dictionary with a "message" key.
|
|
1398
|
+
:rtype: Response
|
|
1399
|
+
"""
|
|
1400
|
+
return call_json_api("delete", ["triggers", trigger_id], {}, **options)
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
def test_trigger(trigger_id, sample_data=None, **options):
|
|
1404
|
+
"""
|
|
1405
|
+
Evaluates a trigger's filter against sample data, without delivering a notification.
|
|
1406
|
+
|
|
1407
|
+
:param trigger_id: The ID of the trigger to test.
|
|
1408
|
+
:type trigger_id: str
|
|
1409
|
+
:param sample_data: The sample notification payload to evaluate the filter against.
|
|
1410
|
+
:type sample_data: dict, optional
|
|
1411
|
+
:param options: Additional options.
|
|
1412
|
+
:return: A dictionary with "trigger_id", "filter_present" and "filter_result" keys.
|
|
1413
|
+
:rtype: Response
|
|
1414
|
+
"""
|
|
1415
|
+
params = {"sample_data": sample_data}
|
|
1416
|
+
return call_json_api("post", ["triggers", trigger_id, "test"], params, **options)
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
def notifications(channel=None, batch_id=None, max_messages=None, wait_seconds=None,
|
|
1420
|
+
visibility_timeout=None, **options):
|
|
1421
|
+
"""
|
|
1422
|
+
Gets notifications buffered for a poll destination.
|
|
1423
|
+
|
|
1424
|
+
Long-polls exactly one addressing dimension - a named channel or a batch_id. Holds the
|
|
1425
|
+
connection up to wait_seconds, returning as soon as messages are claimed. A claimed
|
|
1426
|
+
message stays invisible for visibility_timeout seconds and is then redelivered, unless
|
|
1427
|
+
it is acknowledged with `ack_notifications` first.
|
|
1428
|
+
|
|
1429
|
+
Each message carries the parsed notification as "payload" and the byte-exact signed
|
|
1430
|
+
bytes, base64url encoded, as "signed_payload". Verify signatures against
|
|
1431
|
+
signed_payload, never against a re-serialization of payload::
|
|
1432
|
+
|
|
1433
|
+
raw = cloudinary.utils.base64url_decode(message["signed_payload"])
|
|
1434
|
+
cloudinary.utils.verify_notification_signature(
|
|
1435
|
+
raw, message["timestamp"], message["signature"])
|
|
1436
|
+
|
|
1437
|
+
:param channel: The named channel to drain ("poll://<channel>" destinations).
|
|
1438
|
+
Mutually exclusive with batch_id.
|
|
1439
|
+
:type channel: str, optional
|
|
1440
|
+
:param batch_id: The batch to drain ("poll://*" anonymous destinations).
|
|
1441
|
+
Mutually exclusive with channel.
|
|
1442
|
+
:type batch_id: str, optional
|
|
1443
|
+
:param max_messages: The maximum number of messages to return. 1-100, default 10.
|
|
1444
|
+
:type max_messages: int, optional
|
|
1445
|
+
:param wait_seconds: The maximum seconds to hold the long-poll open when empty.
|
|
1446
|
+
0-60, default 20.
|
|
1447
|
+
:type wait_seconds: int, optional
|
|
1448
|
+
:param visibility_timeout: The seconds a claimed message stays invisible before
|
|
1449
|
+
redelivery. 1-600, default 30.
|
|
1450
|
+
:type visibility_timeout: int, optional
|
|
1451
|
+
:param options: Additional options.
|
|
1452
|
+
:keyword int timeout: The HTTP timeout, also configurable via `cloudinary.config(timeout=...)`.
|
|
1453
|
+
When set, it must exceed wait_seconds, or the long poll is cut off
|
|
1454
|
+
client-side before the server answers.
|
|
1455
|
+
:return: Zero or more claimed messages, as {"messages": [...]}. The "messages" key is
|
|
1456
|
+
absent when nothing is buffered - read it with .get("messages", []).
|
|
1457
|
+
:rtype: Response
|
|
1458
|
+
:raises ValueError: If neither or both of channel and batch_id are given.
|
|
1459
|
+
"""
|
|
1460
|
+
if bool(channel) == bool(batch_id):
|
|
1461
|
+
raise ValueError("Supply exactly one of 'channel' or 'batch_id'")
|
|
1462
|
+
|
|
1463
|
+
params = {
|
|
1464
|
+
"channel": channel,
|
|
1465
|
+
"batch_id": batch_id,
|
|
1466
|
+
"max_messages": max_messages,
|
|
1467
|
+
"wait_seconds": wait_seconds,
|
|
1468
|
+
"visibility_timeout": visibility_timeout,
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
return __call_notifications_api("get", ["messages"], params, **options)
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
def ack_notifications(receipt_handles, **options):
|
|
1475
|
+
"""
|
|
1476
|
+
Acknowledges claimed notifications, deleting them from the buffer.
|
|
1477
|
+
|
|
1478
|
+
Unacknowledged notifications are redelivered once their visibility timeout expires.
|
|
1479
|
+
|
|
1480
|
+
:param receipt_handles: The receipt handles of the notifications to acknowledge, as
|
|
1481
|
+
returned by `notifications`. A single handle may be passed as a
|
|
1482
|
+
string. Maximum 100 per call.
|
|
1483
|
+
:type receipt_handles: list[str] or str
|
|
1484
|
+
:param options: Additional options.
|
|
1485
|
+
:return: The result of the call.
|
|
1486
|
+
:rtype: Response
|
|
1487
|
+
"""
|
|
1488
|
+
if isinstance(receipt_handles, string_types):
|
|
1489
|
+
receipt_handles = [receipt_handles]
|
|
1490
|
+
|
|
1491
|
+
return __call_notifications_api("post", ["messages", "ack"],
|
|
1492
|
+
{"receipt_handles": receipt_handles}, **options)
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def __call_notifications_api(method, uri, params, **options):
|
|
1496
|
+
"""
|
|
1497
|
+
Private function that assists with performing an API call to the notifications module.
|
|
1498
|
+
|
|
1499
|
+
:param method: The HTTP method. Valid methods: get, post, put, delete
|
|
1500
|
+
:param uri: REST endpoint of the API (without 'notifications' or the cloud name)
|
|
1501
|
+
:param params: Query/body parameters passed to the method
|
|
1502
|
+
:param options: Additional options
|
|
1503
|
+
:rtype: Response
|
|
1504
|
+
:internal
|
|
1505
|
+
"""
|
|
1506
|
+
return _call_v2_api(method, uri, params, module="notifications", **options)
|
|
1507
|
+
|
|
1508
|
+
|
|
1326
1509
|
def only(source, *keys):
|
|
1327
1510
|
"""
|
|
1328
1511
|
Returns a dictionary containing only the specified keys from the source.
|
|
@@ -1384,7 +1567,7 @@ def __delete_resource_params(options, **params):
|
|
|
1384
1567
|
:internal
|
|
1385
1568
|
"""
|
|
1386
1569
|
p = dict(transformations=utils.build_eager(options.get('transformations')),
|
|
1387
|
-
**only(options, "keep_original", "next_cursor", "invalidate"))
|
|
1570
|
+
**only(options, "keep_original", "next_cursor", "invalidate", "batch_id"))
|
|
1388
1571
|
p.update(params)
|
|
1389
1572
|
return p
|
|
1390
1573
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import cloudinary
|
|
2
2
|
from cloudinary.api_client.execute_request import execute_request
|
|
3
3
|
from cloudinary.provisioning.account_config import account_config
|
|
4
|
-
from cloudinary.utils import get_http_connector, normalize_params
|
|
4
|
+
from cloudinary.utils import get_http_connector, json_body, normalize_params
|
|
5
5
|
|
|
6
6
|
PROVISIONING_SUB_PATH = "provisioning"
|
|
7
7
|
ACCOUNT_SUB_PATH = "accounts"
|
|
@@ -46,9 +46,15 @@ def _execute_account_request(method, uri, auth, params=None, headers=None, **opt
|
|
|
46
46
|
api_version = options.pop("api_version", cloudinary.API_VERSION)
|
|
47
47
|
provisioning_api_url = "/".join([prefix, api_version, PROVISIONING_SUB_PATH] + uri)
|
|
48
48
|
|
|
49
|
+
params = normalize_params(params)
|
|
50
|
+
|
|
51
|
+
if method.upper() != "GET":
|
|
52
|
+
options["body"], headers = json_body(params, headers)
|
|
53
|
+
params = None
|
|
54
|
+
|
|
49
55
|
return execute_request(http_connector=_http,
|
|
50
56
|
method=method,
|
|
51
|
-
params=
|
|
57
|
+
params=params,
|
|
52
58
|
headers=headers,
|
|
53
59
|
auth=auth,
|
|
54
60
|
api_url=provisioning_api_url,
|
|
@@ -2,7 +2,7 @@ import json
|
|
|
2
2
|
|
|
3
3
|
import cloudinary
|
|
4
4
|
from cloudinary.api_client.execute_request import execute_request
|
|
5
|
-
from cloudinary.utils import get_http_connector, normalize_params
|
|
5
|
+
from cloudinary.utils import get_http_connector, json_body, normalize_params
|
|
6
6
|
|
|
7
7
|
logger = cloudinary.logger
|
|
8
8
|
_http = get_http_connector(cloudinary.config(), cloudinary.CERT_KWARGS)
|
|
@@ -35,23 +35,50 @@ def call_metadata_rules_api(method, uri, params, **options):
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def call_json_api(method, uri, params, **options):
|
|
38
|
-
data=None
|
|
38
|
+
data = None
|
|
39
|
+
headers = {'Content-Type': 'application/json'}
|
|
39
40
|
if method.upper() != 'GET':
|
|
40
|
-
data =
|
|
41
|
+
data, headers = json_body(params)
|
|
41
42
|
params = None
|
|
42
43
|
|
|
43
|
-
return _call_api(method, uri, params=params, body=data, headers=
|
|
44
|
+
return _call_api(method, uri, params=params, body=data, headers=headers, **options)
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def _call_v2_api(method, uri, params, **options):
|
|
47
|
-
|
|
47
|
+
def _call_v2_api(method, uri, params, module=None, **options):
|
|
48
|
+
"""Private function that assists with performing a v2 API call.
|
|
49
|
+
|
|
50
|
+
:param method: The HTTP method. Valid methods: get, post, put, delete
|
|
51
|
+
:param uri: REST endpoint of the API (without the module or the cloud name)
|
|
52
|
+
:param params: Query/body parameters passed to the method
|
|
53
|
+
:param module: The v2 module the endpoint belongs to, for module-first endpoints
|
|
54
|
+
(`/v2/{module}/{cloud_name}/...`). Omit for context-first endpoints
|
|
55
|
+
(`/v2/{cloud_name}/...`).
|
|
56
|
+
:param options: Additional options
|
|
57
|
+
:rtype: Response
|
|
58
|
+
"""
|
|
59
|
+
return call_json_api(method, uri, params=params, api_version='v2', module=module, **options)
|
|
48
60
|
|
|
49
61
|
|
|
50
62
|
def call_api(method, uri, params, **options):
|
|
51
63
|
return _call_api(method, uri, params=params, **options)
|
|
52
64
|
|
|
53
65
|
|
|
54
|
-
def _call_api(method, uri, params=None, body=None, headers=None, extra_headers=None, **options):
|
|
66
|
+
def _call_api(method, uri, params=None, body=None, headers=None, extra_headers=None, module=None, **options):
|
|
67
|
+
"""
|
|
68
|
+
Performs an API call.
|
|
69
|
+
|
|
70
|
+
:param method: The HTTP method. Valid methods: get, post, put, delete
|
|
71
|
+
:param uri: REST endpoint of the API, as a list of path segments
|
|
72
|
+
:param params: Query/body parameters passed to the method
|
|
73
|
+
:param body: An already serialized request body
|
|
74
|
+
:param headers: Request headers
|
|
75
|
+
:param extra_headers: Additional headers, merged into headers
|
|
76
|
+
:param module: The API module the endpoint belongs to. When given, the module precedes
|
|
77
|
+
the cloud name in the URL (`/{api_version}/{module}/{cloud_name}/...`),
|
|
78
|
+
which is the v2 API convention. When omitted, the cloud name comes first.
|
|
79
|
+
:param options: Additional options
|
|
80
|
+
:rtype: Response
|
|
81
|
+
"""
|
|
55
82
|
prefix = options.pop("upload_prefix",
|
|
56
83
|
cloudinary.config().upload_prefix) or "https://api.cloudinary.com"
|
|
57
84
|
cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name)
|
|
@@ -66,7 +93,7 @@ def _call_api(method, uri, params=None, body=None, headers=None, extra_headers=N
|
|
|
66
93
|
auth = {"key": api_key, "secret": api_secret, "oauth_token": oauth_token}
|
|
67
94
|
|
|
68
95
|
api_version = options.pop("api_version", cloudinary.API_VERSION)
|
|
69
|
-
api_url = "/".join([prefix, api_version, cloud_name] + uri)
|
|
96
|
+
api_url = "/".join(filter(None, [prefix, api_version, module, cloud_name] + uri))
|
|
70
97
|
|
|
71
98
|
if body is not None:
|
|
72
99
|
options["body"] = body
|
|
@@ -26,7 +26,8 @@ EXCEPTION_CODES = {
|
|
|
26
26
|
409: AlreadyExists,
|
|
27
27
|
420: RateLimited,
|
|
28
28
|
429: RateLimited,
|
|
29
|
-
500: GeneralError
|
|
29
|
+
500: GeneralError,
|
|
30
|
+
503: GeneralError
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
|
|
@@ -38,6 +39,7 @@ class Response(dict):
|
|
|
38
39
|
self.rate_limit_allowed = safe_cast(response.headers.get("x-featureratelimit-limit"), int)
|
|
39
40
|
self.rate_limit_reset_at = safe_cast(response.headers.get("x-featureratelimit-reset"), email.utils.parsedate)
|
|
40
41
|
self.rate_limit_remaining = safe_cast(response.headers.get("x-featureratelimit-remaining"), int)
|
|
42
|
+
self.request_id = response.headers.get("x-request-id")
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
def execute_request(http_connector, method, params, headers, auth, api_url, **options):
|
|
@@ -58,8 +60,9 @@ def execute_request(http_connector, method, params, headers, auth, api_url, **op
|
|
|
58
60
|
|
|
59
61
|
api_url = smart_escape(unquote(api_url))
|
|
60
62
|
kw = {}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
timeout = options.get("timeout", cloudinary.config().timeout)
|
|
64
|
+
if timeout is not None:
|
|
65
|
+
kw["timeout"] = timeout
|
|
63
66
|
if "body" in options:
|
|
64
67
|
kw["body"] = options["body"]
|
|
65
68
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .account_config import AccountConfig, account_config, reset_config
|
|
2
|
-
from .account import (create_agent_account,
|
|
2
|
+
from .account import (create_agent_account, create_cloud,
|
|
3
3
|
sub_accounts, create_sub_account, delete_sub_account, sub_account, update_sub_account,
|
|
4
4
|
user_groups, create_user_group, update_user_group, delete_user_group, user_group,
|
|
5
5
|
add_user_to_group, remove_user_from_group, user_group_users, user_in_user_groups,
|
|
@@ -2,6 +2,7 @@ from cloudinary.api_client.call_account_api import _call_account_api, _call_publ
|
|
|
2
2
|
from cloudinary.utils import encode_list
|
|
3
3
|
|
|
4
4
|
AGENTS_SUB_PATH = "agents"
|
|
5
|
+
CLOUDS_SUB_PATH = "clouds"
|
|
5
6
|
SUB_ACCOUNTS_SUB_PATH = "sub_accounts"
|
|
6
7
|
USERS_SUB_PATH = "users"
|
|
7
8
|
USER_GROUPS_SUB_PATH = "user_groups"
|
|
@@ -60,6 +61,61 @@ def create_agent_account(email, agent_framework, agent_llm_model, agent_goal, sd
|
|
|
60
61
|
return _call_public_account_api("POST", uri, params=params, **options)
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
def create_cloud(delivery_ips=None, email=None, agent_framework=None, agent_llm_model=None, agent_goal=None,
|
|
65
|
+
sdk_framework=None, **options):
|
|
66
|
+
"""
|
|
67
|
+
Create a Claimable Cloud, intended for use by AI agents.
|
|
68
|
+
|
|
69
|
+
Creates a temporary cloud whose credentials work immediately, with media delivery restricted
|
|
70
|
+
to an IP allow-list. No verification email is sent. Unless a human claims it via the returned
|
|
71
|
+
claim_url, the cloud is disabled when it expires; claiming makes it permanent, keeps the
|
|
72
|
+
credentials and assets, and lifts the IP restriction.
|
|
73
|
+
|
|
74
|
+
Creation only: a cloud cannot be read, updated or deleted, so the delivery IPs are fixed for
|
|
75
|
+
its lifetime. If they are wrong, create another cloud or claim this one.
|
|
76
|
+
|
|
77
|
+
The restriction covers media delivery only, not the Upload and Admin APIs, so it is not a
|
|
78
|
+
confidentiality control. Uploads succeeding while delivery fails with `x-cld-error: ACL deny`
|
|
79
|
+
is the expected symptom of it.
|
|
80
|
+
|
|
81
|
+
This endpoint is public and unauthenticated, and is rate-limited per IP address.
|
|
82
|
+
|
|
83
|
+
:param delivery_ips: Up to three additional IP addresses permitted to deliver media, for
|
|
84
|
+
hosts other than the caller. The caller's own resolved address is
|
|
85
|
+
always appended, so omitting this is the usual call; the literal
|
|
86
|
+
"requester_ip" is replaced by that address. IPv4 and IPv6 are
|
|
87
|
+
accepted, CIDR ranges are not. Non-public addresses are dropped and
|
|
88
|
+
the call fails unless at least one public address remains, so read
|
|
89
|
+
delivery_ips back from the response rather than assuming the list
|
|
90
|
+
sent was stored.
|
|
91
|
+
:type delivery_ips: list[str], optional
|
|
92
|
+
:param email: Email address to associate the claim with. Not verified, and no mail
|
|
93
|
+
is sent to it; a placeholder is generated when omitted.
|
|
94
|
+
:type email: str, optional
|
|
95
|
+
:param agent_framework: The name of the agent framework used to create the cloud.
|
|
96
|
+
:type agent_framework: str, optional
|
|
97
|
+
:param agent_llm_model: The LLM model powering the agent.
|
|
98
|
+
:type agent_llm_model: str, optional
|
|
99
|
+
:param agent_goal: A short description of what the agent is trying to achieve.
|
|
100
|
+
:type agent_goal: str, optional
|
|
101
|
+
:param sdk_framework: The Cloudinary SDK framework the agent intends to use.
|
|
102
|
+
:type sdk_framework: str, optional
|
|
103
|
+
:param options: Generic advanced options dict, see online documentation
|
|
104
|
+
:type options: dict, optional
|
|
105
|
+
:return: The created Claimable Cloud, including working credentials,
|
|
106
|
+
the claim URL and the expiry time
|
|
107
|
+
:rtype: dict
|
|
108
|
+
"""
|
|
109
|
+
uri = [CLOUDS_SUB_PATH]
|
|
110
|
+
params = {"delivery_ips": delivery_ips,
|
|
111
|
+
"email": email,
|
|
112
|
+
"agent_framework": agent_framework,
|
|
113
|
+
"agent_llm_model": agent_llm_model,
|
|
114
|
+
"agent_goal": agent_goal,
|
|
115
|
+
"sdk_framework": sdk_framework}
|
|
116
|
+
return _call_public_account_api("POST", uri, params=params, **options)
|
|
117
|
+
|
|
118
|
+
|
|
63
119
|
def sub_accounts(enabled=None, ids=None, prefix=None, **options):
|
|
64
120
|
"""
|
|
65
121
|
List all sub accounts
|
|
@@ -163,7 +219,7 @@ def update_sub_account(sub_account_id, name=None, cloud_name=None, custom_attrib
|
|
|
163
219
|
return _call_account_api("put", uri, params=params, **options)
|
|
164
220
|
|
|
165
221
|
|
|
166
|
-
def users(user_ids=None, sub_account_id=None,
|
|
222
|
+
def users(user_ids=None, sub_account_id=None, status=None, prefix=None, last_login=None, from_date=None, to_date=None,
|
|
167
223
|
**options):
|
|
168
224
|
"""
|
|
169
225
|
List all users
|
|
@@ -171,10 +227,10 @@ def users(user_ids=None, sub_account_id=None, pending=None, prefix=None, last_lo
|
|
|
171
227
|
:type user_ids: list, optional
|
|
172
228
|
:param sub_account_id: The id of a sub account
|
|
173
229
|
:type sub_account_id: str, optional
|
|
174
|
-
:param
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
:type
|
|
230
|
+
:param status: Limit results to users of this status: "pending" for users who have not yet
|
|
231
|
+
set a password, otherwise a user status such as "active". All users when
|
|
232
|
+
omitted.
|
|
233
|
+
:type status: str, optional
|
|
178
234
|
:param prefix: User prefix
|
|
179
235
|
:type prefix: str, optional
|
|
180
236
|
:param last_login: Return only users that last logged in in the specified range of dates (true),
|
|
@@ -193,7 +249,7 @@ def users(user_ids=None, sub_account_id=None, pending=None, prefix=None, last_lo
|
|
|
193
249
|
user_ids = encode_list(user_ids)
|
|
194
250
|
params = {"ids": user_ids,
|
|
195
251
|
"sub_account_id": sub_account_id,
|
|
196
|
-
"
|
|
252
|
+
"status": status,
|
|
197
253
|
"prefix": prefix,
|
|
198
254
|
"last_login": last_login,
|
|
199
255
|
"from": from_date,
|
|
@@ -157,6 +157,8 @@ def upload(file, **options):
|
|
|
157
157
|
If True, performs analysis for cinemagraph creation.
|
|
158
158
|
:keyword bool accessibility_analysis:
|
|
159
159
|
If True, performs accessibility (image alt text) analysis.
|
|
160
|
+
:keyword str batch_id:
|
|
161
|
+
The batch identifier used to retrieve notifications from a `poll://*` destination.
|
|
160
162
|
:keyword int timestamp:
|
|
161
163
|
A UNIX timestamp to sign the request. Defaults to now().
|
|
162
164
|
:keyword dict or list transformation:
|
|
@@ -934,18 +936,27 @@ def call_api(action, params, http_headers=None, return_error=False, unsigned=Fal
|
|
|
934
936
|
except socket.error as e:
|
|
935
937
|
raise Error("Socket error: {0!r}".format(e))
|
|
936
938
|
|
|
939
|
+
request_id = response.headers.get("x-request-id")
|
|
940
|
+
|
|
937
941
|
try:
|
|
938
942
|
result = json.loads(response.data.decode('utf-8'))
|
|
939
943
|
except Exception as e:
|
|
940
|
-
|
|
941
|
-
|
|
944
|
+
message = "Error parsing server response ({0}) - {1}. Got - {2}".format(
|
|
945
|
+
response.status, response.data, e)
|
|
946
|
+
if request_id:
|
|
947
|
+
message += ". Request ID: {0}".format(request_id)
|
|
948
|
+
raise Error(message)
|
|
942
949
|
|
|
943
950
|
if "error" in result:
|
|
944
951
|
if return_error:
|
|
945
952
|
result["error"]["http_code"] = response.status
|
|
953
|
+
result["error"]["request_id"] = request_id
|
|
946
954
|
return result
|
|
947
955
|
|
|
948
956
|
exception_class = EXCEPTION_CODES.get(response.status) or Error
|
|
949
957
|
raise exception_class(result["error"]["message"])
|
|
950
958
|
|
|
959
|
+
if request_id:
|
|
960
|
+
result["request_id"] = request_id
|
|
961
|
+
|
|
951
962
|
return result
|
|
@@ -122,6 +122,7 @@ __SIMPLE_UPLOAD_PARAMS = [
|
|
|
122
122
|
"cinemagraph_analysis",
|
|
123
123
|
"accessibility_analysis",
|
|
124
124
|
"auto_chaptering",
|
|
125
|
+
"batch_id",
|
|
125
126
|
]
|
|
126
127
|
|
|
127
128
|
__SERIALIZED_UPLOAD_PARAMS = [
|
|
@@ -613,6 +614,20 @@ def normalize_params(params):
|
|
|
613
614
|
return dict([(k, __bool_string(v)) for (k, v) in params.items() if v is not None and not v == ""])
|
|
614
615
|
|
|
615
616
|
|
|
617
|
+
def json_body(params, headers=None):
|
|
618
|
+
"""
|
|
619
|
+
Encodes params as a JSON request body with the matching Content-Type.
|
|
620
|
+
|
|
621
|
+
:param params: Params to serialize.
|
|
622
|
+
:param headers: Headers to extend. A Content-Type already present is kept.
|
|
623
|
+
:return: Tuple of the encoded body and the resulting headers.
|
|
624
|
+
"""
|
|
625
|
+
headers = dict(headers or {})
|
|
626
|
+
headers.setdefault("Content-Type", "application/json")
|
|
627
|
+
|
|
628
|
+
return json.dumps(params).encode("utf-8"), headers
|
|
629
|
+
|
|
630
|
+
|
|
616
631
|
def sign_request(params, options):
|
|
617
632
|
api_key = options.get("api_key", cloudinary.config().api_key)
|
|
618
633
|
if not api_key:
|
|
@@ -1092,6 +1107,7 @@ def archive_params(**options):
|
|
|
1092
1107
|
params = {
|
|
1093
1108
|
"allow_missing": options.get("allow_missing"),
|
|
1094
1109
|
"async": options.get("async"),
|
|
1110
|
+
"batch_id": options.get("batch_id"),
|
|
1095
1111
|
"expires_at": options.get("expires_at"),
|
|
1096
1112
|
"flatten_folders": options.get("flatten_folders"),
|
|
1097
1113
|
"flatten_transformations": options.get("flatten_transformations"),
|
|
@@ -1234,6 +1250,7 @@ def build_multi_and_sprite_params(**options):
|
|
|
1234
1250
|
"mode": options.get("mode"),
|
|
1235
1251
|
"timestamp": now(),
|
|
1236
1252
|
"async": options.get("async"),
|
|
1253
|
+
"batch_id": options.get("batch_id"),
|
|
1237
1254
|
"notification_url": options.get("notification_url"),
|
|
1238
1255
|
"tag": tag,
|
|
1239
1256
|
"urls": urls,
|
|
@@ -1555,6 +1572,17 @@ def base64url_encode(data):
|
|
|
1555
1572
|
return to_string(base64.urlsafe_b64encode(to_bytes(data)))
|
|
1556
1573
|
|
|
1557
1574
|
|
|
1575
|
+
def base64url_decode(data):
|
|
1576
|
+
"""
|
|
1577
|
+
Url safe version of urlsafe_b64decode that restores the `=` padding if it was stripped.
|
|
1578
|
+
|
|
1579
|
+
:param data: Base64 URL safe encoded string, padded or not
|
|
1580
|
+
|
|
1581
|
+
:return: Decoded string
|
|
1582
|
+
"""
|
|
1583
|
+
return to_string(base64.urlsafe_b64decode(to_bytes(data + "=" * (-len(data) % 4))))
|
|
1584
|
+
|
|
1585
|
+
|
|
1558
1586
|
def encode_unicode_url(url_str):
|
|
1559
1587
|
"""
|
|
1560
1588
|
Quote and encode possible unicode url string (applicable for python2)
|
|
@@ -1645,7 +1673,9 @@ def verify_notification_signature(body, timestamp, signature, valid_for=7200, al
|
|
|
1645
1673
|
Verifies the authenticity of a notification signature
|
|
1646
1674
|
|
|
1647
1675
|
:param body: Json of the request's body
|
|
1648
|
-
:param timestamp: Unix timestamp. Can be retrieved from the X-Cld-Timestamp header
|
|
1676
|
+
:param timestamp: Unix timestamp. Can be retrieved from the X-Cld-Timestamp header, or from
|
|
1677
|
+
the `timestamp` of a notification returned by `api.notifications`. Both
|
|
1678
|
+
deliver it as a string, which is accepted here.
|
|
1649
1679
|
:param signature: Actual signature. Can be retrieved from the X-Cld-Signature header
|
|
1650
1680
|
:param valid_for: The desired time in seconds for considering the request valid
|
|
1651
1681
|
:param algorithm: Name of hashing algorithm to use for calculation of HMACs.
|
|
@@ -1656,7 +1686,7 @@ def verify_notification_signature(body, timestamp, signature, valid_for=7200, al
|
|
|
1656
1686
|
if not cloudinary.config().api_secret:
|
|
1657
1687
|
raise Exception('Api secret key is empty')
|
|
1658
1688
|
|
|
1659
|
-
if timestamp < time.time() - valid_for:
|
|
1689
|
+
if int(timestamp) < time.time() - valid_for:
|
|
1660
1690
|
return False
|
|
1661
1691
|
|
|
1662
1692
|
if not isinstance(body, str):
|
|
@@ -1667,6 +1697,31 @@ def verify_notification_signature(body, timestamp, signature, valid_for=7200, al
|
|
|
1667
1697
|
algorithm or cloudinary.config().signature_algorithm)
|
|
1668
1698
|
|
|
1669
1699
|
|
|
1700
|
+
def verify_notification(message, valid_for=7200, algorithm=None):
|
|
1701
|
+
"""
|
|
1702
|
+
Verifies the authenticity of a signed notification message.
|
|
1703
|
+
|
|
1704
|
+
:param message: Signed message, with `signed_payload`, `signature` and `timestamp` keys
|
|
1705
|
+
:type message: dict
|
|
1706
|
+
:param valid_for: The desired time in seconds for considering the message valid
|
|
1707
|
+
:param algorithm: Name of hashing algorithm to use for calculation of HMACs.
|
|
1708
|
+
By default, uses `cloudinary.config().signature_algorithm`
|
|
1709
|
+
|
|
1710
|
+
:return: Boolean result of the validation
|
|
1711
|
+
:raises ValueError: If the message is missing signed_payload, signature or timestamp
|
|
1712
|
+
"""
|
|
1713
|
+
missing = [key for key in ("signed_payload", "signature", "timestamp") if key not in message]
|
|
1714
|
+
if missing:
|
|
1715
|
+
raise ValueError("Message is missing required key(s): {}".format(", ".join(missing)))
|
|
1716
|
+
|
|
1717
|
+
return verify_notification_signature(
|
|
1718
|
+
base64url_decode(message["signed_payload"]),
|
|
1719
|
+
message["timestamp"],
|
|
1720
|
+
message["signature"],
|
|
1721
|
+
valid_for=valid_for,
|
|
1722
|
+
algorithm=algorithm)
|
|
1723
|
+
|
|
1724
|
+
|
|
1670
1725
|
def get_http_connector(conf, options):
|
|
1671
1726
|
"""
|
|
1672
1727
|
Used to create http connector, depends on api_proxy and disable_tcp_keep_alive configuration parameters.
|
|
@@ -66,9 +66,12 @@ test/test_http_client.py
|
|
|
66
66
|
test/test_image.py
|
|
67
67
|
test/test_metadata.py
|
|
68
68
|
test/test_metadata_rules.py
|
|
69
|
+
test/test_notifications.py
|
|
69
70
|
test/test_provisioning_api.py
|
|
71
|
+
test/test_request_id.py
|
|
70
72
|
test/test_search.py
|
|
71
73
|
test/test_streaming_profiles.py
|
|
74
|
+
test/test_triggers.py
|
|
72
75
|
test/test_uploader.py
|
|
73
76
|
test/test_utils.py
|
|
74
77
|
test/test_video.py
|
|
@@ -7,7 +7,7 @@ if version_info[0] >= 3:
|
|
|
7
7
|
else:
|
|
8
8
|
# Following code is legacy (Python 2.7 compatibility) and will be removed in the future!
|
|
9
9
|
# TODO: Remove in next major update (when dropping Python 2.7 compatibility)
|
|
10
|
-
version = "1.
|
|
10
|
+
version = "1.46.0"
|
|
11
11
|
|
|
12
12
|
with open('README.md') as file:
|
|
13
13
|
long_description = file.read()
|