Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cloudinary/api_client/call_account_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cloudinary
from cloudinary.api_client.execute_request import execute_request
from cloudinary.provisioning.account_config import account_config
from cloudinary.utils import get_http_connector, normalize_params
from cloudinary.utils import get_http_connector, json_body, normalize_params

PROVISIONING_SUB_PATH = "provisioning"
ACCOUNT_SUB_PATH = "accounts"
Expand Down Expand Up @@ -46,9 +46,15 @@ def _execute_account_request(method, uri, auth, params=None, headers=None, **opt
api_version = options.pop("api_version", cloudinary.API_VERSION)
provisioning_api_url = "/".join([prefix, api_version, PROVISIONING_SUB_PATH] + uri)

params = normalize_params(params)

if method.upper() != "GET":
options["body"], headers = json_body(params, headers)
params = None

return execute_request(http_connector=_http,
method=method,
params=normalize_params(params),
params=params,
headers=headers,
auth=auth,
api_url=provisioning_api_url,
Expand Down
9 changes: 5 additions & 4 deletions cloudinary/api_client/call_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import cloudinary
from cloudinary.api_client.execute_request import execute_request
from cloudinary.utils import get_http_connector, normalize_params
from cloudinary.utils import get_http_connector, json_body, normalize_params

logger = cloudinary.logger
_http = get_http_connector(cloudinary.config(), cloudinary.CERT_KWARGS)
Expand Down Expand Up @@ -35,12 +35,13 @@ def call_metadata_rules_api(method, uri, params, **options):


def call_json_api(method, uri, params, **options):
data=None
data = None
headers = {'Content-Type': 'application/json'}
if method.upper() != 'GET':
data = json.dumps(params).encode('utf-8')
data, headers = json_body(params)
params = None

return _call_api(method, uri, params=params, body=data, headers={'Content-Type': 'application/json'}, **options)
return _call_api(method, uri, params=params, body=data, headers=headers, **options)


def _call_v2_api(method, uri, params, **options):
Expand Down
2 changes: 1 addition & 1 deletion cloudinary/provisioning/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .account_config import AccountConfig, account_config, reset_config
from .account import (create_agent_account,
from .account import (create_agent_account, create_cloud,
sub_accounts, create_sub_account, delete_sub_account, sub_account, update_sub_account,
user_groups, create_user_group, update_user_group, delete_user_group, user_group,
add_user_to_group, remove_user_from_group, user_group_users, user_in_user_groups,
Expand Down
68 changes: 62 additions & 6 deletions cloudinary/provisioning/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from cloudinary.utils import encode_list

AGENTS_SUB_PATH = "agents"
CLOUDS_SUB_PATH = "clouds"
SUB_ACCOUNTS_SUB_PATH = "sub_accounts"
USERS_SUB_PATH = "users"
USER_GROUPS_SUB_PATH = "user_groups"
Expand Down Expand Up @@ -60,6 +61,61 @@ def create_agent_account(email, agent_framework, agent_llm_model, agent_goal, sd
return _call_public_account_api("POST", uri, params=params, **options)


def create_cloud(delivery_ips=None, email=None, agent_framework=None, agent_llm_model=None, agent_goal=None,
sdk_framework=None, **options):
"""
Create a Claimable Cloud, intended for use by AI agents.

Creates a temporary cloud whose credentials work immediately, with media delivery restricted
to an IP allow-list. No verification email is sent. Unless a human claims it via the returned
claim_url, the cloud is disabled when it expires; claiming makes it permanent, keeps the
credentials and assets, and lifts the IP restriction.

Creation only: a cloud cannot be read, updated or deleted, so the delivery IPs are fixed for
its lifetime. If they are wrong, create another cloud or claim this one.

The restriction covers media delivery only, not the Upload and Admin APIs, so it is not a
confidentiality control. Uploads succeeding while delivery fails with `x-cld-error: ACL deny`
is the expected symptom of it.

This endpoint is public and unauthenticated, and is rate-limited per IP address.

:param delivery_ips: Up to three additional IP addresses permitted to deliver media, for
hosts other than the caller. The caller's own resolved address is
always appended, so omitting this is the usual call; the literal
"requester_ip" is replaced by that address. IPv4 and IPv6 are
accepted, CIDR ranges are not. Non-public addresses are dropped and
the call fails unless at least one public address remains, so read
delivery_ips back from the response rather than assuming the list
sent was stored.
:type delivery_ips: list[str], optional
:param email: Email address to associate the claim with. Not verified, and no mail
is sent to it; a placeholder is generated when omitted.
:type email: str, optional
:param agent_framework: The name of the agent framework used to create the cloud.
:type agent_framework: str, optional
:param agent_llm_model: The LLM model powering the agent.
:type agent_llm_model: str, optional
:param agent_goal: A short description of what the agent is trying to achieve.
:type agent_goal: str, optional
:param sdk_framework: The Cloudinary SDK framework the agent intends to use.
:type sdk_framework: str, optional
:param options: Generic advanced options dict, see online documentation
:type options: dict, optional
:return: The created Claimable Cloud, including working credentials,
the claim URL and the expiry time
:rtype: dict
"""
uri = [CLOUDS_SUB_PATH]
params = {"delivery_ips": delivery_ips,
"email": email,
"agent_framework": agent_framework,
"agent_llm_model": agent_llm_model,
"agent_goal": agent_goal,
"sdk_framework": sdk_framework}
return _call_public_account_api("POST", uri, params=params, **options)


def sub_accounts(enabled=None, ids=None, prefix=None, **options):
"""
List all sub accounts
Expand Down Expand Up @@ -163,18 +219,18 @@ def update_sub_account(sub_account_id, name=None, cloud_name=None, custom_attrib
return _call_account_api("put", uri, params=params, **options)


def users(user_ids=None, sub_account_id=None, pending=None, prefix=None, last_login=None, from_date=None, to_date=None,
def users(user_ids=None, sub_account_id=None, status=None, prefix=None, last_login=None, from_date=None, to_date=None,
**options):
"""
List all users
:param user_ids: The ids of the users to fetch
:type user_ids: list, optional
:param sub_account_id: The id of a sub account
:type sub_account_id: str, optional
:param pending: Limit results to pending users (True),
users that are not pending (False),
or all users (None, the default).
:type pending: bool, optional
:param status: Limit results to users of this status: "pending" for users who have not yet
set a password, otherwise a user status such as "active". All users when
omitted.
:type status: str, optional
:param prefix: User prefix
:type prefix: str, optional
:param last_login: Return only users that last logged in in the specified range of dates (true),
Expand All @@ -193,7 +249,7 @@ def users(user_ids=None, sub_account_id=None, pending=None, prefix=None, last_lo
user_ids = encode_list(user_ids)
params = {"ids": user_ids,
"sub_account_id": sub_account_id,
"pending": pending,
"status": status,
"prefix": prefix,
"last_login": last_login,
"from": from_date,
Expand Down
14 changes: 14 additions & 0 deletions cloudinary/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,20 @@ def normalize_params(params):
return dict([(k, __bool_string(v)) for (k, v) in params.items() if v is not None and not v == ""])


def json_body(params, headers=None):
"""
Encodes params as a JSON request body with the matching Content-Type.

:param params: Params to serialize.
:param headers: Headers to extend. A Content-Type already present is kept.
:return: Tuple of the encoded body and the resulting headers.
"""
headers = dict(headers or {})
headers.setdefault("Content-Type", "application/json")

return json.dumps(params).encode("utf-8"), headers


def sign_request(params, options):
api_key = options.get("api_key", cloudinary.config().api_key)
if not api_key:
Expand Down
Loading
Loading