cloudinary 1.44.4__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.44.4/cloudinary.egg-info → cloudinary-1.46.0}/PKG-INFO +1 -1
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/__init__.py +1 -1
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/api.py +184 -1
- cloudinary-1.46.0/cloudinary/api_client/call_account_api.py +61 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/api_client/call_api.py +35 -8
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/api_client/execute_request.py +11 -8
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/provisioning/__init__.py +2 -1
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/provisioning/account.py +103 -7
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/uploader.py +13 -2
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/utils.py +57 -2
- {cloudinary-1.44.4 → cloudinary-1.46.0/cloudinary.egg-info}/PKG-INFO +1 -1
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary.egg-info/SOURCES.txt +3 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/pyproject.toml +1 -1
- {cloudinary-1.44.4 → cloudinary-1.46.0}/setup.py +1 -1
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_api.py +5 -2
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_archive.py +3 -0
- cloudinary-1.46.0/test/test_notifications.py +321 -0
- cloudinary-1.46.0/test/test_provisioning_api.py +535 -0
- cloudinary-1.46.0/test/test_request_id.py +98 -0
- cloudinary-1.46.0/test/test_triggers.py +162 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_uploader.py +3 -1
- cloudinary-1.44.4/cloudinary/api_client/call_account_api.py +0 -34
- cloudinary-1.44.4/test/test_provisioning_api.py +0 -268
- {cloudinary-1.44.4 → cloudinary-1.46.0}/LICENSE.txt +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/MANIFEST.in +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/README.md +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/api_client/__init__.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/api_client/tcp_keep_alive_manager.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/auth_token.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/cache/__init__.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/cache/adapter/__init__.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/cache/adapter/cache_adapter.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/cache/adapter/key_value_cache_adapter.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/cache/responsive_breakpoints_cache.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/cache/storage/__init__.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/cache/storage/file_system_key_value_storage.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/cache/storage/key_value_storage.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/compat.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/exceptions.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/forms.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/http_client.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/models.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/poster/__init__.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/poster/encode.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/poster/streaminghttp.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/provisioning/account_config.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/search.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/search_folders.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/static/cloudinary/html/cloudinary_cors.html +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/canvas-to-blob.min.js +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.cloudinary.js +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload-image.js +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload-process.js +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload-validate.js +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.fileupload.js +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.iframe-transport.js +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/jquery.ui.widget.js +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/static/cloudinary/js/load-image.all.min.js +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/templates/cloudinary_direct_upload.html +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/templates/cloudinary_includes.html +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/templates/cloudinary_js_config.html +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/templatetags/__init__.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary/templatetags/cloudinary.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary.egg-info/dependency_links.txt +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary.egg-info/not-zip-safe +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary.egg-info/requires.txt +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/cloudinary.egg-info/top_level.txt +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/setup.cfg +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_api_authorization.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_auth_token.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_cloudinary_resource.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_config.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_expression_normalization.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_http_client.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_image.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_metadata.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_metadata_rules.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_search.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_streaming_profiles.py +0 -0
- {cloudinary-1.44.4 → cloudinary-1.46.0}/test/test_utils.py +0 -0
- {cloudinary-1.44.4 → 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
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import cloudinary
|
|
2
|
+
from cloudinary.api_client.execute_request import execute_request
|
|
3
|
+
from cloudinary.provisioning.account_config import account_config
|
|
4
|
+
from cloudinary.utils import get_http_connector, json_body, normalize_params
|
|
5
|
+
|
|
6
|
+
PROVISIONING_SUB_PATH = "provisioning"
|
|
7
|
+
ACCOUNT_SUB_PATH = "accounts"
|
|
8
|
+
_http = get_http_connector(account_config(), cloudinary.CERT_KWARGS)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Account-scoped, authenticated call: provisioning/accounts/{account_id}/...
|
|
12
|
+
def _call_account_api(method, uri, params=None, headers=None, **options):
|
|
13
|
+
account_uri = [ACCOUNT_SUB_PATH, _account_id(options)] + uri
|
|
14
|
+
return _execute_account_request(method, account_uri, _account_auth(options),
|
|
15
|
+
params=params, headers=headers, **options)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Public, unauthenticated call: provisioning/... with no account_id or credentials
|
|
19
|
+
def _call_public_account_api(method, uri, params=None, headers=None, **options):
|
|
20
|
+
return _execute_account_request(method, uri, {"anonymous": True},
|
|
21
|
+
params=params, headers=headers, **options)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _account_id(options):
|
|
25
|
+
account_id = options.pop("account_id", account_config().account_id)
|
|
26
|
+
if not account_id:
|
|
27
|
+
raise Exception("Must supply account_id")
|
|
28
|
+
return account_id
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _account_auth(options):
|
|
32
|
+
provisioning_api_key = options.pop("provisioning_api_key", account_config().provisioning_api_key)
|
|
33
|
+
if not provisioning_api_key:
|
|
34
|
+
raise Exception("Must supply provisioning_api_key")
|
|
35
|
+
provisioning_api_secret = options.pop("provisioning_api_secret", account_config().provisioning_api_secret)
|
|
36
|
+
if not provisioning_api_secret:
|
|
37
|
+
raise Exception("Must supply provisioning_api_secret")
|
|
38
|
+
return {"key": provisioning_api_key, "secret": provisioning_api_secret}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Core transport: builds the provisioning URL and dispatches with the resolved auth.
|
|
42
|
+
# The API version can be overridden via the "api_version" option (defaults to cloudinary.API_VERSION).
|
|
43
|
+
def _execute_account_request(method, uri, auth, params=None, headers=None, **options):
|
|
44
|
+
prefix = options.pop("upload_prefix",
|
|
45
|
+
cloudinary.config().upload_prefix) or "https://api.cloudinary.com"
|
|
46
|
+
api_version = options.pop("api_version", cloudinary.API_VERSION)
|
|
47
|
+
provisioning_api_url = "/".join([prefix, api_version, PROVISIONING_SUB_PATH] + uri)
|
|
48
|
+
|
|
49
|
+
params = normalize_params(params)
|
|
50
|
+
|
|
51
|
+
if method.upper() != "GET":
|
|
52
|
+
options["body"], headers = json_body(params, headers)
|
|
53
|
+
params = None
|
|
54
|
+
|
|
55
|
+
return execute_request(http_connector=_http,
|
|
56
|
+
method=method,
|
|
57
|
+
params=params,
|
|
58
|
+
headers=headers,
|
|
59
|
+
auth=auth,
|
|
60
|
+
api_url=provisioning_api_url,
|
|
61
|
+
**options)
|
|
@@ -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,17 +39,18 @@ 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):
|
|
44
|
-
|
|
46
|
+
anonymous = auth.get("anonymous")
|
|
45
47
|
key = auth.get("key")
|
|
46
48
|
secret = auth.get("secret")
|
|
47
49
|
oauth_token = auth.get("oauth_token")
|
|
48
|
-
req_headers = urllib3.make_headers(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
req_headers = urllib3.make_headers(user_agent=cloudinary.get_user_agent())
|
|
51
|
+
if anonymous:
|
|
52
|
+
pass
|
|
53
|
+
elif oauth_token:
|
|
52
54
|
req_headers["authorization"] = "Bearer {}".format(oauth_token)
|
|
53
55
|
else:
|
|
54
56
|
req_headers.update(urllib3.make_headers(basic_auth="{0}:{1}".format(key, secret)))
|
|
@@ -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,6 @@
|
|
|
1
1
|
from .account_config import AccountConfig, account_config, reset_config
|
|
2
|
-
from .account import (
|
|
2
|
+
from .account import (create_agent_account, create_cloud,
|
|
3
|
+
sub_accounts, create_sub_account, delete_sub_account, sub_account, update_sub_account,
|
|
3
4
|
user_groups, create_user_group, update_user_group, delete_user_group, user_group,
|
|
4
5
|
add_user_to_group, remove_user_from_group, user_group_users, user_in_user_groups,
|
|
5
6
|
users, create_user, delete_user, user, update_user, access_keys, generate_access_key,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
from cloudinary.api_client.call_account_api import _call_account_api
|
|
1
|
+
from cloudinary.api_client.call_account_api import _call_account_api, _call_public_account_api, ACCOUNT_SUB_PATH
|
|
2
2
|
from cloudinary.utils import encode_list
|
|
3
3
|
|
|
4
|
+
AGENTS_SUB_PATH = "agents"
|
|
5
|
+
CLOUDS_SUB_PATH = "clouds"
|
|
4
6
|
SUB_ACCOUNTS_SUB_PATH = "sub_accounts"
|
|
5
7
|
USERS_SUB_PATH = "users"
|
|
6
8
|
USER_GROUPS_SUB_PATH = "user_groups"
|
|
@@ -20,6 +22,100 @@ class Role(object):
|
|
|
20
22
|
MEDIA_LIBRARY_USER = "media_library_user"
|
|
21
23
|
|
|
22
24
|
|
|
25
|
+
def create_agent_account(email, agent_framework, agent_llm_model, agent_goal, sdk_framework=None, **options):
|
|
26
|
+
"""
|
|
27
|
+
Create a Cloudinary account on behalf of a human, intended for use by AI agents.
|
|
28
|
+
|
|
29
|
+
Creates a Free-plan account with a single, initially disabled product environment and sends a
|
|
30
|
+
verification email so the human can set a password and activate the account. The returned
|
|
31
|
+
credentials are inert until the email is verified.
|
|
32
|
+
|
|
33
|
+
This endpoint is public and unauthenticated, and is rate-limited per IP address.
|
|
34
|
+
|
|
35
|
+
:param email: The email address of the human on whose behalf the account is created.
|
|
36
|
+
A verification email is sent to this address.
|
|
37
|
+
:type email: str
|
|
38
|
+
:param agent_framework: The name of the agent framework used to create the account.
|
|
39
|
+
Must be between 2 and 100 characters.
|
|
40
|
+
:type agent_framework: str
|
|
41
|
+
:param agent_llm_model: The LLM model powering the agent. Must be between 2 and 100 characters.
|
|
42
|
+
:type agent_llm_model: str
|
|
43
|
+
:param agent_goal: A short description of what the agent is trying to achieve.
|
|
44
|
+
Must be between 2 and 300 characters.
|
|
45
|
+
:type agent_goal: str
|
|
46
|
+
:param sdk_framework: The Cloudinary SDK framework the agent intends to use.
|
|
47
|
+
Must be between 2 and 100 characters.
|
|
48
|
+
:type sdk_framework: str, optional
|
|
49
|
+
:param options: Generic advanced options dict, see online documentation
|
|
50
|
+
:type options: dict, optional
|
|
51
|
+
:return: The created agent account, including inert credentials for its
|
|
52
|
+
single product environment
|
|
53
|
+
:rtype: dict
|
|
54
|
+
"""
|
|
55
|
+
uri = [AGENTS_SUB_PATH, ACCOUNT_SUB_PATH]
|
|
56
|
+
params = {"email": email,
|
|
57
|
+
"agent_framework": agent_framework,
|
|
58
|
+
"agent_llm_model": agent_llm_model,
|
|
59
|
+
"agent_goal": agent_goal,
|
|
60
|
+
"sdk_framework": sdk_framework}
|
|
61
|
+
return _call_public_account_api("POST", uri, params=params, **options)
|
|
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
|
+
|
|
23
119
|
def sub_accounts(enabled=None, ids=None, prefix=None, **options):
|
|
24
120
|
"""
|
|
25
121
|
List all sub accounts
|
|
@@ -123,7 +219,7 @@ def update_sub_account(sub_account_id, name=None, cloud_name=None, custom_attrib
|
|
|
123
219
|
return _call_account_api("put", uri, params=params, **options)
|
|
124
220
|
|
|
125
221
|
|
|
126
|
-
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,
|
|
127
223
|
**options):
|
|
128
224
|
"""
|
|
129
225
|
List all users
|
|
@@ -131,10 +227,10 @@ def users(user_ids=None, sub_account_id=None, pending=None, prefix=None, last_lo
|
|
|
131
227
|
:type user_ids: list, optional
|
|
132
228
|
:param sub_account_id: The id of a sub account
|
|
133
229
|
:type sub_account_id: str, optional
|
|
134
|
-
:param
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
: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
|
|
138
234
|
:param prefix: User prefix
|
|
139
235
|
:type prefix: str, optional
|
|
140
236
|
:param last_login: Return only users that last logged in in the specified range of dates (true),
|
|
@@ -153,7 +249,7 @@ def users(user_ids=None, sub_account_id=None, pending=None, prefix=None, last_lo
|
|
|
153
249
|
user_ids = encode_list(user_ids)
|
|
154
250
|
params = {"ids": user_ids,
|
|
155
251
|
"sub_account_id": sub_account_id,
|
|
156
|
-
"
|
|
252
|
+
"status": status,
|
|
157
253
|
"prefix": prefix,
|
|
158
254
|
"last_login": last_login,
|
|
159
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
|