cloudinary 1.44.3__tar.gz → 1.45.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.3/cloudinary.egg-info → cloudinary-1.45.0}/PKG-INFO +1 -1
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/__init__.py +1 -1
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/api_client/call_account_api.py +28 -7
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/api_client/execute_request.py +5 -5
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/provisioning/__init__.py +2 -1
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/provisioning/account.py +41 -1
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/utils.py +4 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0/cloudinary.egg-info}/PKG-INFO +1 -1
- {cloudinary-1.44.3 → cloudinary-1.45.0}/pyproject.toml +1 -1
- {cloudinary-1.44.3 → cloudinary-1.45.0}/setup.py +1 -1
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_provisioning_api.py +92 -1
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_utils.py +49 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/LICENSE.txt +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/MANIFEST.in +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/README.md +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/api.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/api_client/__init__.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/api_client/call_api.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/api_client/tcp_keep_alive_manager.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/auth_token.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/cache/__init__.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/cache/adapter/__init__.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/cache/adapter/cache_adapter.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/cache/adapter/key_value_cache_adapter.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/cache/responsive_breakpoints_cache.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/cache/storage/__init__.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/cache/storage/file_system_key_value_storage.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/cache/storage/key_value_storage.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/compat.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/exceptions.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/forms.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/http_client.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/models.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/poster/__init__.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/poster/encode.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/poster/streaminghttp.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/provisioning/account_config.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/search.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/search_folders.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/html/cloudinary_cors.html +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/canvas-to-blob.min.js +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.cloudinary.js +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.fileupload-image.js +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.fileupload-process.js +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.fileupload-validate.js +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.fileupload.js +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.iframe-transport.js +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.ui.widget.js +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/load-image.all.min.js +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/templates/cloudinary_direct_upload.html +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/templates/cloudinary_includes.html +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/templates/cloudinary_js_config.html +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/templatetags/__init__.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/templatetags/cloudinary.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/uploader.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary.egg-info/SOURCES.txt +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary.egg-info/dependency_links.txt +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary.egg-info/not-zip-safe +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary.egg-info/requires.txt +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary.egg-info/top_level.txt +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/setup.cfg +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_api.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_api_authorization.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_archive.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_auth_token.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_cloudinary_resource.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_config.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_expression_normalization.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_http_client.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_image.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_metadata.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_metadata_rules.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_search.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_streaming_profiles.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.0}/test/test_uploader.py +0 -0
- {cloudinary-1.44.3 → cloudinary-1.45.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.45.0"
|
|
42
42
|
|
|
43
43
|
_USER_PLATFORM_DETAILS = "; ".join((platform(), "Python {}".format(python_version())))
|
|
44
44
|
|
|
@@ -8,22 +8,43 @@ ACCOUNT_SUB_PATH = "accounts"
|
|
|
8
8
|
_http = get_http_connector(account_config(), cloudinary.CERT_KWARGS)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
# Account-scoped, authenticated call: provisioning/accounts/{account_id}/...
|
|
11
12
|
def _call_account_api(method, uri, params=None, headers=None, **options):
|
|
12
|
-
|
|
13
|
-
|
|
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):
|
|
14
25
|
account_id = options.pop("account_id", account_config().account_id)
|
|
15
26
|
if not account_id:
|
|
16
27
|
raise Exception("Must supply account_id")
|
|
28
|
+
return account_id
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _account_auth(options):
|
|
17
32
|
provisioning_api_key = options.pop("provisioning_api_key", account_config().provisioning_api_key)
|
|
18
33
|
if not provisioning_api_key:
|
|
19
34
|
raise Exception("Must supply provisioning_api_key")
|
|
20
|
-
provisioning_api_secret = options.pop("provisioning_api_secret",
|
|
21
|
-
account_config().provisioning_api_secret)
|
|
35
|
+
provisioning_api_secret = options.pop("provisioning_api_secret", account_config().provisioning_api_secret)
|
|
22
36
|
if not provisioning_api_secret:
|
|
23
37
|
raise Exception("Must supply provisioning_api_secret")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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)
|
|
27
48
|
|
|
28
49
|
return execute_request(http_connector=_http,
|
|
29
50
|
method=method,
|
|
@@ -41,14 +41,14 @@ class Response(dict):
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def execute_request(http_connector, method, params, headers, auth, api_url, **options):
|
|
44
|
-
|
|
44
|
+
anonymous = auth.get("anonymous")
|
|
45
45
|
key = auth.get("key")
|
|
46
46
|
secret = auth.get("secret")
|
|
47
47
|
oauth_token = auth.get("oauth_token")
|
|
48
|
-
req_headers = urllib3.make_headers(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
req_headers = urllib3.make_headers(user_agent=cloudinary.get_user_agent())
|
|
49
|
+
if anonymous:
|
|
50
|
+
pass
|
|
51
|
+
elif oauth_token:
|
|
52
52
|
req_headers["authorization"] = "Bearer {}".format(oauth_token)
|
|
53
53
|
else:
|
|
54
54
|
req_headers.update(urllib3.make_headers(basic_auth="{0}:{1}".format(key, secret)))
|
|
@@ -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,
|
|
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,7 @@
|
|
|
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"
|
|
4
5
|
SUB_ACCOUNTS_SUB_PATH = "sub_accounts"
|
|
5
6
|
USERS_SUB_PATH = "users"
|
|
6
7
|
USER_GROUPS_SUB_PATH = "user_groups"
|
|
@@ -20,6 +21,45 @@ class Role(object):
|
|
|
20
21
|
MEDIA_LIBRARY_USER = "media_library_user"
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def create_agent_account(email, agent_framework, agent_llm_model, agent_goal, sdk_framework=None, **options):
|
|
25
|
+
"""
|
|
26
|
+
Create a Cloudinary account on behalf of a human, intended for use by AI agents.
|
|
27
|
+
|
|
28
|
+
Creates a Free-plan account with a single, initially disabled product environment and sends a
|
|
29
|
+
verification email so the human can set a password and activate the account. The returned
|
|
30
|
+
credentials are inert until the email is verified.
|
|
31
|
+
|
|
32
|
+
This endpoint is public and unauthenticated, and is rate-limited per IP address.
|
|
33
|
+
|
|
34
|
+
:param email: The email address of the human on whose behalf the account is created.
|
|
35
|
+
A verification email is sent to this address.
|
|
36
|
+
:type email: str
|
|
37
|
+
:param agent_framework: The name of the agent framework used to create the account.
|
|
38
|
+
Must be between 2 and 100 characters.
|
|
39
|
+
:type agent_framework: str
|
|
40
|
+
:param agent_llm_model: The LLM model powering the agent. Must be between 2 and 100 characters.
|
|
41
|
+
:type agent_llm_model: str
|
|
42
|
+
:param agent_goal: A short description of what the agent is trying to achieve.
|
|
43
|
+
Must be between 2 and 300 characters.
|
|
44
|
+
:type agent_goal: str
|
|
45
|
+
:param sdk_framework: The Cloudinary SDK framework the agent intends to use.
|
|
46
|
+
Must be between 2 and 100 characters.
|
|
47
|
+
:type sdk_framework: str, optional
|
|
48
|
+
:param options: Generic advanced options dict, see online documentation
|
|
49
|
+
:type options: dict, optional
|
|
50
|
+
:return: The created agent account, including inert credentials for its
|
|
51
|
+
single product environment
|
|
52
|
+
:rtype: dict
|
|
53
|
+
"""
|
|
54
|
+
uri = [AGENTS_SUB_PATH, ACCOUNT_SUB_PATH]
|
|
55
|
+
params = {"email": email,
|
|
56
|
+
"agent_framework": agent_framework,
|
|
57
|
+
"agent_llm_model": agent_llm_model,
|
|
58
|
+
"agent_goal": agent_goal,
|
|
59
|
+
"sdk_framework": sdk_framework}
|
|
60
|
+
return _call_public_account_api("POST", uri, params=params, **options)
|
|
61
|
+
|
|
62
|
+
|
|
23
63
|
def sub_accounts(enabled=None, ids=None, prefix=None, **options):
|
|
24
64
|
"""
|
|
25
65
|
List all sub accounts
|
|
@@ -642,6 +642,8 @@ def api_sign_request(params_to_sign, api_secret, algorithm=SIGNATURE_SHA1, signa
|
|
|
642
642
|
- Version 2+: Includes parameter encoding to prevent parameter smuggling
|
|
643
643
|
:return: Computed signature
|
|
644
644
|
"""
|
|
645
|
+
if not api_secret:
|
|
646
|
+
raise ValueError("Must supply api_secret")
|
|
645
647
|
to_sign = api_string_to_sign(params_to_sign, signature_version)
|
|
646
648
|
return compute_hex_hash(to_sign + api_secret, algorithm)
|
|
647
649
|
|
|
@@ -875,6 +877,8 @@ def cloudinary_url(source, **options):
|
|
|
875
877
|
|
|
876
878
|
signature = None
|
|
877
879
|
if sign_url and (not auth_token or auth_token.pop('set_url_signature', False)):
|
|
880
|
+
if not api_secret:
|
|
881
|
+
raise ValueError("Must supply api_secret")
|
|
878
882
|
to_sign = "/".join(__compact([transformation, source_to_sign]))
|
|
879
883
|
if long_url_signature:
|
|
880
884
|
# Long signature forces SHA256
|
|
@@ -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.45.0"
|
|
11
11
|
|
|
12
12
|
with open('README.md') as file:
|
|
13
13
|
long_description = file.read()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import unittest
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
|
|
@@ -8,7 +9,8 @@ import cloudinary.provisioning.account
|
|
|
8
9
|
from cloudinary.provisioning import account_config, reset_config
|
|
9
10
|
from cloudinary.exceptions import AuthorizationRequired, NotFound
|
|
10
11
|
|
|
11
|
-
from test.helper_test import UNIQUE_SUB_ACCOUNT_ID, UNIQUE_TEST_ID
|
|
12
|
+
from test.helper_test import (UNIQUE_SUB_ACCOUNT_ID, UNIQUE_TEST_ID, URLLIB3_REQUEST, patch, api_response_mock,
|
|
13
|
+
get_uri, get_method, get_params, get_headers)
|
|
12
14
|
|
|
13
15
|
disable_warnings()
|
|
14
16
|
|
|
@@ -85,6 +87,7 @@ class AccountApiTest(unittest.TestCase):
|
|
|
85
87
|
@unittest.skipUnless(cloudinary.provisioning.account_config().provisioning_api_secret,
|
|
86
88
|
"requires provisioning_api_key/provisioning_api_secret")
|
|
87
89
|
def test_get_all_sub_accounts(self):
|
|
90
|
+
|
|
88
91
|
res = cloudinary.provisioning.sub_accounts(True)
|
|
89
92
|
|
|
90
93
|
sub_account_by_id = [sub_account for sub_account in res["sub_accounts"]
|
|
@@ -264,5 +267,93 @@ class AccountApiTest(unittest.TestCase):
|
|
|
264
267
|
self.assertEqual("ok", named_key_del_res["message"])
|
|
265
268
|
|
|
266
269
|
|
|
270
|
+
class CreateAgentAccountTest(unittest.TestCase):
|
|
271
|
+
"""
|
|
272
|
+
The create agent account endpoint is public, unauthenticated and rate limited per IP,
|
|
273
|
+
so it is verified against a mocked transport rather than the live API.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def test_create_agent_account(self):
|
|
277
|
+
with patch(URLLIB3_REQUEST) as mocker:
|
|
278
|
+
mocker.return_value = api_response_mock()
|
|
279
|
+
cloudinary.provisioning.create_agent_account(
|
|
280
|
+
"jane@example.com",
|
|
281
|
+
agent_framework="langchain",
|
|
282
|
+
agent_llm_model="claude-opus-4-8",
|
|
283
|
+
agent_goal="Build a product image gallery",
|
|
284
|
+
sdk_framework="python",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
self.assertEqual("POST", get_method(mocker))
|
|
288
|
+
self.assertTrue(get_uri(mocker).endswith("/provisioning/agents/accounts"))
|
|
289
|
+
|
|
290
|
+
params = get_params(mocker)
|
|
291
|
+
self.assertEqual("jane@example.com", params["email"])
|
|
292
|
+
self.assertEqual("langchain", params["agent_framework"])
|
|
293
|
+
self.assertEqual("claude-opus-4-8", params["agent_llm_model"])
|
|
294
|
+
self.assertEqual("Build a product image gallery", params["agent_goal"])
|
|
295
|
+
self.assertEqual("python", params["sdk_framework"])
|
|
296
|
+
|
|
297
|
+
def test_create_agent_account_is_unauthenticated(self):
|
|
298
|
+
with patch(URLLIB3_REQUEST) as mocker:
|
|
299
|
+
mocker.return_value = api_response_mock()
|
|
300
|
+
cloudinary.provisioning.create_agent_account(
|
|
301
|
+
"jane@example.com",
|
|
302
|
+
agent_framework="langchain",
|
|
303
|
+
agent_llm_model="claude-opus-4-8",
|
|
304
|
+
agent_goal="Build a product image gallery",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# The endpoint is public - no authorization header must be sent.
|
|
308
|
+
headers = get_headers(mocker)
|
|
309
|
+
self.assertNotIn("authorization", {k.lower() for k in headers})
|
|
310
|
+
|
|
311
|
+
def test_create_agent_account_omits_unset_sdk_framework(self):
|
|
312
|
+
with patch(URLLIB3_REQUEST) as mocker:
|
|
313
|
+
mocker.return_value = api_response_mock()
|
|
314
|
+
cloudinary.provisioning.create_agent_account(
|
|
315
|
+
"jane@example.com",
|
|
316
|
+
agent_framework="langchain",
|
|
317
|
+
agent_llm_model="claude-opus-4-8",
|
|
318
|
+
agent_goal="Build a product image gallery",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
self.assertNotIn("sdk_framework", get_params(mocker))
|
|
322
|
+
|
|
323
|
+
def test_create_agent_account_parses_response(self):
|
|
324
|
+
body = json.dumps({
|
|
325
|
+
"external_id": "0aaaaa1bbbbb2ccccc3ddddd4eeeee5f",
|
|
326
|
+
"email": "jane@example.com",
|
|
327
|
+
"plan_name": "free",
|
|
328
|
+
"product_environments": [{
|
|
329
|
+
"external_id": "abcde1fghij2klmno3pqrst4uvwxy5z",
|
|
330
|
+
"cloud_name": "product1",
|
|
331
|
+
"api_key": "123456789012345",
|
|
332
|
+
"api_secret": "asdf1JKL2xyz3ABc4s3c5reT01DfaKez",
|
|
333
|
+
"api_environment_variable":
|
|
334
|
+
"CLOUDINARY_URL=cloudinary://123456789012345:asdf1JKL2xyz3ABc4s3c5reT01DfaKez@product1",
|
|
335
|
+
}],
|
|
336
|
+
"guidance": "A verification email has been sent to the supplied email address.",
|
|
337
|
+
})
|
|
338
|
+
with patch(URLLIB3_REQUEST) as mocker:
|
|
339
|
+
mocker.return_value = api_response_mock(body)
|
|
340
|
+
res = cloudinary.provisioning.create_agent_account(
|
|
341
|
+
"jane@example.com",
|
|
342
|
+
agent_framework="langchain",
|
|
343
|
+
agent_llm_model="claude-opus-4-8",
|
|
344
|
+
agent_goal="Build a product image gallery",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
self.assertEqual("free", res["plan_name"])
|
|
348
|
+
self.assertEqual("jane@example.com", res["email"])
|
|
349
|
+
self.assertEqual(1, len(res["product_environments"]))
|
|
350
|
+
product_environment = res["product_environments"][0]
|
|
351
|
+
self.assertEqual("product1", product_environment["cloud_name"])
|
|
352
|
+
self.assertEqual("123456789012345", product_environment["api_key"])
|
|
353
|
+
self.assertEqual("asdf1JKL2xyz3ABc4s3c5reT01DfaKez", product_environment["api_secret"])
|
|
354
|
+
self.assertIn("CLOUDINARY_URL=cloudinary://", product_environment["api_environment_variable"])
|
|
355
|
+
self.assertIn("guidance", res)
|
|
356
|
+
|
|
357
|
+
|
|
267
358
|
if __name__ == '__main__':
|
|
268
359
|
unittest.main()
|
|
@@ -21,6 +21,7 @@ from cloudinary.utils import (
|
|
|
21
21
|
encode_unicode_url,
|
|
22
22
|
base64url_encode,
|
|
23
23
|
patch_fetch_format,
|
|
24
|
+
cloudinary_url,
|
|
24
25
|
cloudinary_scaled_url,
|
|
25
26
|
chain_transformations,
|
|
26
27
|
generate_transformation_string,
|
|
@@ -1591,6 +1592,54 @@ class TestUtils(unittest.TestCase):
|
|
|
1591
1592
|
self.assertEqual(signed_params_v1['signature'], expected_sig_v1)
|
|
1592
1593
|
self.assertEqual(signed_params_v2['signature'], expected_sig_v2)
|
|
1593
1594
|
|
|
1595
|
+
def test_cloudinary_url_sign_without_secret_raises(self):
|
|
1596
|
+
"""Signing without a secret should raise ValueError"""
|
|
1597
|
+
cloudinary.config(cloud_name="test123", api_secret=None)
|
|
1598
|
+
with self.assertRaises(ValueError) as context:
|
|
1599
|
+
cloudinary_url("sample", sign_url=True)
|
|
1600
|
+
self.assertEqual(str(context.exception), "Must supply api_secret")
|
|
1601
|
+
|
|
1602
|
+
def test_cloudinary_url_unsigned_without_secret_works(self):
|
|
1603
|
+
"""Unsigned URL should work without a secret"""
|
|
1604
|
+
cloudinary.config(cloud_name="test123", api_secret=None)
|
|
1605
|
+
url, _ = cloudinary_url("sample")
|
|
1606
|
+
self.assertIn("test123", url)
|
|
1607
|
+
self.assertNotIn("s--", url)
|
|
1608
|
+
|
|
1609
|
+
def test_cloudinary_url_sign_with_secret_works(self):
|
|
1610
|
+
"""Signing with a secret should work and include signature"""
|
|
1611
|
+
cloudinary.config(cloud_name="test123", api_key="key", api_secret="secret")
|
|
1612
|
+
url, _ = cloudinary_url("sample", sign_url=True)
|
|
1613
|
+
self.assertIn("s--", url)
|
|
1614
|
+
self.assertIn("test123", url)
|
|
1615
|
+
|
|
1616
|
+
def test_cloudinary_url_per_call_secret_override(self):
|
|
1617
|
+
"""Per-call api_secret override should sign successfully"""
|
|
1618
|
+
cloudinary.config(cloud_name="test123", api_secret=None)
|
|
1619
|
+
url, _ = cloudinary_url("sample", sign_url=True, api_secret="override_secret")
|
|
1620
|
+
self.assertIn("s--", url)
|
|
1621
|
+
self.assertIn("test123", url)
|
|
1622
|
+
|
|
1623
|
+
def test_api_sign_request_without_secret_raises(self):
|
|
1624
|
+
"""api_sign_request with None secret should raise ValueError"""
|
|
1625
|
+
params = {"a": "b"}
|
|
1626
|
+
with self.assertRaises(ValueError) as context:
|
|
1627
|
+
api_sign_request(params, None)
|
|
1628
|
+
self.assertEqual(str(context.exception), "Must supply api_secret")
|
|
1629
|
+
|
|
1630
|
+
def test_api_sign_request_with_empty_string_raises(self):
|
|
1631
|
+
"""api_sign_request with empty string secret should raise ValueError"""
|
|
1632
|
+
params = {"a": "b"}
|
|
1633
|
+
with self.assertRaises(ValueError) as context:
|
|
1634
|
+
api_sign_request(params, "")
|
|
1635
|
+
self.assertEqual(str(context.exception), "Must supply api_secret")
|
|
1636
|
+
|
|
1637
|
+
def test_api_sign_request_with_secret_works(self):
|
|
1638
|
+
"""api_sign_request with a real secret should work"""
|
|
1639
|
+
params = dict(cloud_name=API_SIGN_REQUEST_CLOUD_NAME, timestamp=1568810420, username="user@cloudinary.com")
|
|
1640
|
+
signature = api_sign_request(params, API_SIGN_REQUEST_TEST_SECRET)
|
|
1641
|
+
self.assertEqual(signature, "14c00ba6d0dfdedbc86b316847d95b9e6cd46d94")
|
|
1642
|
+
|
|
1594
1643
|
|
|
1595
1644
|
if __name__ == '__main__':
|
|
1596
1645
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/cache/storage/file_system_key_value_storage.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/html/cloudinary_cors.html
RENAMED
|
File without changes
|
{cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/canvas-to-blob.min.js
RENAMED
|
File without changes
|
{cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.cloudinary.js
RENAMED
|
File without changes
|
{cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.fileupload-image.js
RENAMED
|
File without changes
|
{cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.fileupload-process.js
RENAMED
|
File without changes
|
|
File without changes
|
{cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.fileupload.js
RENAMED
|
File without changes
|
{cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/jquery.iframe-transport.js
RENAMED
|
File without changes
|
|
File without changes
|
{cloudinary-1.44.3 → cloudinary-1.45.0}/cloudinary/static/cloudinary/js/load-image.all.min.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|