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.
Files changed (79) hide show
  1. {cloudinary-1.45.0/cloudinary.egg-info → cloudinary-1.46.0}/PKG-INFO +1 -1
  2. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/__init__.py +1 -1
  3. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api.py +184 -1
  4. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api_client/call_account_api.py +8 -2
  5. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api_client/call_api.py +35 -8
  6. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api_client/execute_request.py +6 -3
  7. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/provisioning/__init__.py +1 -1
  8. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/provisioning/account.py +62 -6
  9. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/uploader.py +13 -2
  10. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/utils.py +57 -2
  11. {cloudinary-1.45.0 → cloudinary-1.46.0/cloudinary.egg-info}/PKG-INFO +1 -1
  12. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary.egg-info/SOURCES.txt +3 -0
  13. {cloudinary-1.45.0 → cloudinary-1.46.0}/pyproject.toml +1 -1
  14. {cloudinary-1.45.0 → cloudinary-1.46.0}/setup.py +1 -1
  15. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_api.py +5 -2
  16. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_archive.py +3 -0
  17. cloudinary-1.46.0/test/test_notifications.py +321 -0
  18. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_provisioning_api.py +191 -15
  19. cloudinary-1.46.0/test/test_request_id.py +98 -0
  20. cloudinary-1.46.0/test/test_triggers.py +162 -0
  21. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_uploader.py +3 -1
  22. {cloudinary-1.45.0 → cloudinary-1.46.0}/LICENSE.txt +0 -0
  23. {cloudinary-1.45.0 → cloudinary-1.46.0}/MANIFEST.in +0 -0
  24. {cloudinary-1.45.0 → cloudinary-1.46.0}/README.md +0 -0
  25. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api_client/__init__.py +0 -0
  26. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/api_client/tcp_keep_alive_manager.py +0 -0
  27. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/auth_token.py +0 -0
  28. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/__init__.py +0 -0
  29. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/adapter/__init__.py +0 -0
  30. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/adapter/cache_adapter.py +0 -0
  31. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/adapter/key_value_cache_adapter.py +0 -0
  32. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/responsive_breakpoints_cache.py +0 -0
  33. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/storage/__init__.py +0 -0
  34. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/storage/file_system_key_value_storage.py +0 -0
  35. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/cache/storage/key_value_storage.py +0 -0
  36. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/compat.py +0 -0
  37. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/exceptions.py +0 -0
  38. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/forms.py +0 -0
  39. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/http_client.py +0 -0
  40. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/models.py +0 -0
  41. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/poster/__init__.py +0 -0
  42. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/poster/encode.py +0 -0
  43. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/poster/streaminghttp.py +0 -0
  44. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/provisioning/account_config.py +0 -0
  45. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/search.py +0 -0
  46. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/search_folders.py +0 -0
  47. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/html/cloudinary_cors.html +0 -0
  48. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/canvas-to-blob.min.js +0 -0
  49. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.cloudinary.js +0 -0
  50. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload-image.js +0 -0
  51. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload-process.js +0 -0
  52. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload-validate.js +0 -0
  53. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload.js +0 -0
  54. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.iframe-transport.js +0 -0
  55. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.ui.widget.js +0 -0
  56. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/load-image.all.min.js +0 -0
  57. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/templates/cloudinary_direct_upload.html +0 -0
  58. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/templates/cloudinary_includes.html +0 -0
  59. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/templates/cloudinary_js_config.html +0 -0
  60. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/templatetags/__init__.py +0 -0
  61. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary/templatetags/cloudinary.py +0 -0
  62. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary.egg-info/dependency_links.txt +0 -0
  63. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary.egg-info/not-zip-safe +0 -0
  64. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary.egg-info/requires.txt +0 -0
  65. {cloudinary-1.45.0 → cloudinary-1.46.0}/cloudinary.egg-info/top_level.txt +0 -0
  66. {cloudinary-1.45.0 → cloudinary-1.46.0}/setup.cfg +0 -0
  67. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_api_authorization.py +0 -0
  68. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_auth_token.py +0 -0
  69. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_cloudinary_resource.py +0 -0
  70. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_config.py +0 -0
  71. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_expression_normalization.py +0 -0
  72. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_http_client.py +0 -0
  73. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_image.py +0 -0
  74. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_metadata.py +0 -0
  75. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_metadata_rules.py +0 -0
  76. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_search.py +0 -0
  77. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_streaming_profiles.py +0 -0
  78. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_utils.py +0 -0
  79. {cloudinary-1.45.0 → cloudinary-1.46.0}/test/test_video.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudinary
3
- Version: 1.45.0
3
+ Version: 1.46.0
4
4
  Summary: Python and Django SDK for Cloudinary
5
5
  Author-email: Cloudinary <info@cloudinary.com>
6
6
  License: Released under the MIT license.
@@ -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.45.0"
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=normalize_params(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 = json.dumps(params).encode('utf-8')
41
+ data, headers = json_body(params)
41
42
  params = None
42
43
 
43
- return _call_api(method, uri, params=params, body=data, headers={'Content-Type': 'application/json'}, **options)
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
- return call_json_api(method, uri, params=params, api_version='v2', **options)
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
- if "timeout" in options:
62
- kw["timeout"] = options["timeout"]
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, pending=None, prefix=None, last_login=None, from_date=None, to_date=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 pending: Limit results to pending users (True),
175
- users that are not pending (False),
176
- or all users (None, the default).
177
- :type pending: bool, optional
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
- "pending": pending,
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
- raise Error("Error parsing server response ({0}) - {1}. Got - {2}"
941
- .format(response.status, response.data, e))
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudinary
3
- Version: 1.45.0
3
+ Version: 1.46.0
4
4
  Summary: Python and Django SDK for Cloudinary
5
5
  Author-email: Cloudinary <info@cloudinary.com>
6
6
  License: Released under the MIT license.
@@ -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
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "cloudinary"
3
3
  description = "Python and Django SDK for Cloudinary"
4
- version = "1.45.0"
4
+ version = "1.46.0"
5
5
 
6
6
  authors = [{ name = "Cloudinary", email = "info@cloudinary.com" }]
7
7
  license = { file = "LICENSE.txt" }
@@ -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.45.0"
10
+ version = "1.46.0"
11
11
 
12
12
  with open('README.md') as file:
13
13
  long_description = file.read()