mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-08 06:00:51 -07:00
Update cloudinary-1.26.0
This commit is contained in:
parent
ebffd124f6
commit
4b28040d59
17 changed files with 1169 additions and 307 deletions
|
@ -1,13 +1,15 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import abc
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
import numbers
|
import numbers
|
||||||
import certifi
|
import certifi
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from six import python_2_unicode_compatible
|
from six import python_2_unicode_compatible, add_metaclass
|
||||||
|
|
||||||
logger = logging.getLogger("Cloudinary")
|
logger = logging.getLogger("Cloudinary")
|
||||||
ch = logging.StreamHandler()
|
ch = logging.StreamHandler()
|
||||||
|
@ -34,8 +36,9 @@ AKAMAI_SHARED_CDN = "res.cloudinary.com"
|
||||||
SHARED_CDN = AKAMAI_SHARED_CDN
|
SHARED_CDN = AKAMAI_SHARED_CDN
|
||||||
CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||||
URI_SCHEME = "cloudinary"
|
URI_SCHEME = "cloudinary"
|
||||||
|
API_VERSION = "v1_1"
|
||||||
|
|
||||||
VERSION = "1.20.0"
|
VERSION = "1.26.0"
|
||||||
|
|
||||||
USER_AGENT = "CloudinaryPython/{} (Python {})".format(VERSION, python_version())
|
USER_AGENT = "CloudinaryPython/{} (Python {})".format(VERSION, python_version())
|
||||||
""" :const: USER_AGENT """
|
""" :const: USER_AGENT """
|
||||||
|
@ -94,54 +97,20 @@ def import_django_settings():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
@add_metaclass(abc.ABCMeta)
|
||||||
|
class BaseConfig(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
django_settings = import_django_settings()
|
django_settings = import_django_settings()
|
||||||
if django_settings:
|
if django_settings:
|
||||||
self.update(**django_settings)
|
self.update(**django_settings)
|
||||||
elif os.environ.get("CLOUDINARY_CLOUD_NAME"):
|
|
||||||
self.update(
|
|
||||||
cloud_name=os.environ.get("CLOUDINARY_CLOUD_NAME"),
|
|
||||||
api_key=os.environ.get("CLOUDINARY_API_KEY"),
|
|
||||||
api_secret=os.environ.get("CLOUDINARY_API_SECRET"),
|
|
||||||
secure_distribution=os.environ.get("CLOUDINARY_SECURE_DISTRIBUTION"),
|
|
||||||
private_cdn=os.environ.get("CLOUDINARY_PRIVATE_CDN") == 'true',
|
|
||||||
api_proxy=os.environ.get("CLOUDINARY_API_PROXY"),
|
|
||||||
)
|
|
||||||
elif os.environ.get("CLOUDINARY_URL"):
|
|
||||||
cloudinary_url = os.environ.get("CLOUDINARY_URL")
|
|
||||||
self._parse_cloudinary_url(cloudinary_url)
|
|
||||||
|
|
||||||
def _parse_cloudinary_url(self, cloudinary_url):
|
self._load_config_from_env()
|
||||||
uri = urlparse(cloudinary_url)
|
|
||||||
if not self._is_url_scheme_valid(uri):
|
|
||||||
raise ValueError("Invalid CLOUDINARY_URL scheme. Expecting to start with 'cloudinary://'")
|
|
||||||
|
|
||||||
for k, v in parse_qs(uri.query).items():
|
|
||||||
if self._is_nested_key(k):
|
|
||||||
self._put_nested_key(k, v)
|
|
||||||
else:
|
|
||||||
self.__dict__[k] = v[0]
|
|
||||||
self.update(
|
|
||||||
cloud_name=uri.hostname,
|
|
||||||
api_key=uri.username,
|
|
||||||
api_secret=uri.password,
|
|
||||||
private_cdn=uri.path != ''
|
|
||||||
)
|
|
||||||
if uri.path != '':
|
|
||||||
self.update(secure_distribution=uri.path[1:])
|
|
||||||
|
|
||||||
def __getattr__(self, i):
|
def __getattr__(self, i):
|
||||||
if i in self.__dict__:
|
return self.__dict__.get(i)
|
||||||
return self.__dict__[i]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update(self, **keywords):
|
@staticmethod
|
||||||
for k, v in keywords.items():
|
def _is_nested_key(key):
|
||||||
self.__dict__[k] = v
|
|
||||||
|
|
||||||
def _is_nested_key(self, key):
|
|
||||||
return re.match(r'\w+\[\w+\]', key)
|
return re.match(r'\w+\[\w+\]', key)
|
||||||
|
|
||||||
def _put_nested_key(self, key, value):
|
def _put_nested_key(self, key, value):
|
||||||
|
@ -160,8 +129,7 @@ class Config(object):
|
||||||
value = value[0]
|
value = value[0]
|
||||||
outer[last_key] = value
|
outer[last_key] = value
|
||||||
|
|
||||||
@staticmethod
|
def _is_url_scheme_valid(self, url):
|
||||||
def _is_url_scheme_valid(url):
|
|
||||||
"""
|
"""
|
||||||
Helper function. Validates url scheme
|
Helper function. Validates url scheme
|
||||||
|
|
||||||
|
@ -169,9 +137,81 @@ class Config(object):
|
||||||
|
|
||||||
:return: bool True on success or False on failure
|
:return: bool True on success or False on failure
|
||||||
"""
|
"""
|
||||||
if not url.scheme or url.scheme.lower() != URI_SCHEME:
|
return url.scheme.lower() == self._uri_scheme
|
||||||
return False
|
|
||||||
return True
|
@staticmethod
|
||||||
|
def _parse_cloudinary_url(cloudinary_url):
|
||||||
|
return urlparse(cloudinary_url)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _config_from_parsed_url(self, parsed_url):
|
||||||
|
"""Extract additional config from the parsed URL."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _setup_from_parsed_url(self, parsed_url):
|
||||||
|
config_from_parsed_url = self._config_from_parsed_url(parsed_url)
|
||||||
|
self.update(**config_from_parsed_url)
|
||||||
|
|
||||||
|
for k, v in parse_qs(parsed_url.query).items():
|
||||||
|
if self._is_nested_key(k):
|
||||||
|
self._put_nested_key(k, v)
|
||||||
|
else:
|
||||||
|
self.__dict__[k] = v[0]
|
||||||
|
|
||||||
|
def _load_from_url(self, url):
|
||||||
|
parsed_url = self._parse_cloudinary_url(url)
|
||||||
|
|
||||||
|
return self._setup_from_parsed_url(parsed_url)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _load_config_from_env(self):
|
||||||
|
"""Load config from environment variables or URL."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def update(self, **keywords):
|
||||||
|
for k, v in keywords.items():
|
||||||
|
self.__dict__[k] = v
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseConfig):
|
||||||
|
def __init__(self):
|
||||||
|
self._uri_scheme = URI_SCHEME
|
||||||
|
|
||||||
|
super(Config, self).__init__()
|
||||||
|
|
||||||
|
if not self.signature_algorithm:
|
||||||
|
self.signature_algorithm = utils.SIGNATURE_SHA1
|
||||||
|
|
||||||
|
def _config_from_parsed_url(self, parsed_url):
|
||||||
|
if not self._is_url_scheme_valid(parsed_url):
|
||||||
|
raise ValueError("Invalid CLOUDINARY_URL scheme. Expecting to start with 'cloudinary://'")
|
||||||
|
|
||||||
|
is_private_cdn = parsed_url.path != ""
|
||||||
|
result = {
|
||||||
|
"cloud_name": parsed_url.hostname,
|
||||||
|
"api_key": parsed_url.username,
|
||||||
|
"api_secret": parsed_url.password,
|
||||||
|
"private_cdn": is_private_cdn,
|
||||||
|
}
|
||||||
|
if is_private_cdn:
|
||||||
|
result.update({"secure_distribution": parsed_url.path[1:]})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _load_config_from_env(self):
|
||||||
|
if os.environ.get("CLOUDINARY_CLOUD_NAME"):
|
||||||
|
config_keys = [key for key in os.environ.keys()
|
||||||
|
if key.startswith("CLOUDINARY_") and key != "CLOUDINARY_URL"]
|
||||||
|
|
||||||
|
for full_key in config_keys:
|
||||||
|
conf_key = full_key[len("CLOUDINARY_"):].lower()
|
||||||
|
conf_val = os.environ[full_key]
|
||||||
|
if conf_val in ["true", "false"]:
|
||||||
|
conf_val = conf_val == "true"
|
||||||
|
|
||||||
|
self.update(**{conf_key: conf_val})
|
||||||
|
elif os.environ.get("CLOUDINARY_URL"):
|
||||||
|
self._load_from_url(os.environ.get("CLOUDINARY_URL"))
|
||||||
|
|
||||||
|
|
||||||
_config = Config()
|
_config = Config()
|
||||||
|
@ -257,7 +297,8 @@ class CloudinaryResource(object):
|
||||||
return self.get_prep_value() + '#' + self.get_expected_signature()
|
return self.get_prep_value() + '#' + self.get_expected_signature()
|
||||||
|
|
||||||
def get_expected_signature(self):
|
def get_expected_signature(self):
|
||||||
return utils.api_sign_request({"public_id": self.public_id, "version": self.version}, config().api_secret)
|
return utils.api_sign_request({"public_id": self.public_id, "version": self.version}, config().api_secret,
|
||||||
|
config().signature_algorithm)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
|
@ -377,7 +418,7 @@ class CloudinaryResource(object):
|
||||||
max_images = srcset_data.get("max_images", 20)
|
max_images = srcset_data.get("max_images", 20)
|
||||||
transformation = srcset_data.get("transformation")
|
transformation = srcset_data.get("transformation")
|
||||||
|
|
||||||
kbytes_step = int(ceil(float(bytes_step)/1024))
|
kbytes_step = int(ceil(float(bytes_step) / 1024))
|
||||||
|
|
||||||
breakpoints_width_param = "auto:breakpoints_{min_width}_{max_width}_{kbytes_step}_{max_images}:json".format(
|
breakpoints_width_param = "auto:breakpoints_{min_width}_{max_width}_{kbytes_step}_{max_images}:json".format(
|
||||||
min_width=min_width, max_width=max_width, kbytes_step=kbytes_step, max_images=max_images)
|
min_width=min_width, max_width=max_width, kbytes_step=kbytes_step, max_images=max_images)
|
||||||
|
@ -623,7 +664,7 @@ class CloudinaryResource(object):
|
||||||
|
|
||||||
return utils.cloudinary_url(poster_options['public_id'], **poster_options)[0]
|
return utils.cloudinary_url(poster_options['public_id'], **poster_options)[0]
|
||||||
|
|
||||||
def _populate_video_source_tags(self, source, options):
|
def _populate_video_source_tags(self, source, options):
|
||||||
"""
|
"""
|
||||||
Helper function for video tag, populates source tags from provided options.
|
Helper function for video tag, populates source tags from provided options.
|
||||||
|
|
||||||
|
@ -750,7 +791,7 @@ class CloudinaryResource(object):
|
||||||
srcset_data = srcset_data.copy()
|
srcset_data = srcset_data.copy()
|
||||||
srcset_data.update(options.pop("srcset", dict()))
|
srcset_data.update(options.pop("srcset", dict()))
|
||||||
|
|
||||||
responsive_attrs = self._generate_image_responsive_attributes(attrs, srcset_data, **options)
|
responsive_attrs = self._generate_image_responsive_attributes(attrs, srcset_data, **options)
|
||||||
|
|
||||||
attrs.update(responsive_attrs)
|
attrs.update(responsive_attrs)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# Copyright Cloudinary
|
# Copyright Cloudinary
|
||||||
|
|
||||||
|
import datetime
|
||||||
import email.utils
|
import email.utils
|
||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
|
@ -10,6 +11,11 @@ from urllib3.exceptions import HTTPError
|
||||||
|
|
||||||
import cloudinary
|
import cloudinary
|
||||||
from cloudinary import utils
|
from cloudinary import utils
|
||||||
|
from cloudinary.api_client.call_api import (
|
||||||
|
call_api,
|
||||||
|
call_metadata_api,
|
||||||
|
call_json_api
|
||||||
|
)
|
||||||
from cloudinary.exceptions import (
|
from cloudinary.exceptions import (
|
||||||
BadRequest,
|
BadRequest,
|
||||||
AuthorizationRequired,
|
AuthorizationRequired,
|
||||||
|
@ -20,37 +26,32 @@ from cloudinary.exceptions import (
|
||||||
GeneralError
|
GeneralError
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = cloudinary.logger
|
|
||||||
|
|
||||||
EXCEPTION_CODES = {
|
|
||||||
400: BadRequest,
|
|
||||||
401: AuthorizationRequired,
|
|
||||||
403: NotAllowed,
|
|
||||||
404: NotFound,
|
|
||||||
409: AlreadyExists,
|
|
||||||
420: RateLimited,
|
|
||||||
500: GeneralError
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Response(dict):
|
|
||||||
def __init__(self, result, response, **kwargs):
|
|
||||||
super(Response, self).__init__(**kwargs)
|
|
||||||
self.update(result)
|
|
||||||
self.rate_limit_allowed = int(response.headers["x-featureratelimit-limit"])
|
|
||||||
self.rate_limit_reset_at = email.utils.parsedate(response.headers["x-featureratelimit-reset"])
|
|
||||||
self.rate_limit_remaining = int(response.headers["x-featureratelimit-remaining"])
|
|
||||||
|
|
||||||
|
|
||||||
_http = utils.get_http_connector(cloudinary.config(), cloudinary.CERT_KWARGS)
|
|
||||||
|
|
||||||
|
|
||||||
def ping(**options):
|
def ping(**options):
|
||||||
return call_api("get", ["ping"], {}, **options)
|
return call_api("get", ["ping"], {}, **options)
|
||||||
|
|
||||||
|
|
||||||
def usage(**options):
|
def usage(**options):
|
||||||
return call_api("get", ["usage"], {}, **options)
|
"""Get account usage details.
|
||||||
|
|
||||||
|
Get a report on the status of your Cloudinary account usage details, including storage, credits, bandwidth,
|
||||||
|
requests, number of resources, and add-on usage. Note that numbers are updated periodically.
|
||||||
|
|
||||||
|
See: `Get account usage details
|
||||||
|
<https://cloudinary.com/documentation/admin_api#get_account_usage_details>`_
|
||||||
|
|
||||||
|
:param options: Additional options
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Detailed usage information
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
date = options.pop("date", None)
|
||||||
|
uri = ["usage"]
|
||||||
|
if date:
|
||||||
|
if isinstance(date, datetime.date):
|
||||||
|
date = utils.encode_date_to_usage_api_format(date)
|
||||||
|
uri.append(date)
|
||||||
|
return call_api("get", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
def resource_types(**options):
|
def resource_types(**options):
|
||||||
|
@ -64,7 +65,7 @@ def resources(**options):
|
||||||
if upload_type:
|
if upload_type:
|
||||||
uri.append(upload_type)
|
uri.append(upload_type)
|
||||||
params = only(options, "next_cursor", "max_results", "prefix", "tags",
|
params = only(options, "next_cursor", "max_results", "prefix", "tags",
|
||||||
"context", "moderations", "direction", "start_at")
|
"context", "moderations", "direction", "start_at", "metadata")
|
||||||
return call_api("get", uri, params, **options)
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ def resources_by_tag(tag, **options):
|
||||||
resource_type = options.pop("resource_type", "image")
|
resource_type = options.pop("resource_type", "image")
|
||||||
uri = ["resources", resource_type, "tags", tag]
|
uri = ["resources", resource_type, "tags", tag]
|
||||||
params = only(options, "next_cursor", "max_results", "tags",
|
params = only(options, "next_cursor", "max_results", "tags",
|
||||||
"context", "moderations", "direction")
|
"context", "moderations", "direction", "metadata")
|
||||||
return call_api("get", uri, params, **options)
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,7 +81,7 @@ def resources_by_moderation(kind, status, **options):
|
||||||
resource_type = options.pop("resource_type", "image")
|
resource_type = options.pop("resource_type", "image")
|
||||||
uri = ["resources", resource_type, "moderations", kind, status]
|
uri = ["resources", resource_type, "moderations", kind, status]
|
||||||
params = only(options, "next_cursor", "max_results", "tags",
|
params = only(options, "next_cursor", "max_results", "tags",
|
||||||
"context", "moderations", "direction")
|
"context", "moderations", "direction", "metadata")
|
||||||
return call_api("get", uri, params, **options)
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
@ -92,12 +93,39 @@ def resources_by_ids(public_ids, **options):
|
||||||
return call_api("get", uri, params, **options)
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def resources_by_context(key, value=None, **options):
|
||||||
|
"""Retrieves resources (assets) with a specified context key.
|
||||||
|
This method does not return deleted assets even if they have been backed up.
|
||||||
|
|
||||||
|
See: `Get resources by context API reference
|
||||||
|
<https://cloudinary.com/documentation/admin_api#get_resources_by_context>`_
|
||||||
|
|
||||||
|
:param key: Only assets with this context key are returned
|
||||||
|
:type key: str
|
||||||
|
:param value: Only assets with this value for the context key are returned
|
||||||
|
:type value: str, optional
|
||||||
|
:param options: Additional options
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Resources (assets) with a specified context key
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
resource_type = options.pop("resource_type", "image")
|
||||||
|
uri = ["resources", resource_type, "context"]
|
||||||
|
params = only(options, "next_cursor", "max_results", "tags",
|
||||||
|
"context", "moderations", "direction", "metadata")
|
||||||
|
params["key"] = key
|
||||||
|
if value is not None:
|
||||||
|
params["value"] = value
|
||||||
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
def resource(public_id, **options):
|
def resource(public_id, **options):
|
||||||
resource_type = options.pop("resource_type", "image")
|
resource_type = options.pop("resource_type", "image")
|
||||||
upload_type = options.pop("type", "upload")
|
upload_type = options.pop("type", "upload")
|
||||||
uri = ["resources", resource_type, upload_type, public_id]
|
uri = ["resources", resource_type, upload_type, public_id]
|
||||||
params = only(options, "exif", "faces", "colors", "image_metadata", "cinemagraph_analysis",
|
params = only(options, "exif", "faces", "colors", "image_metadata", "cinemagraph_analysis",
|
||||||
"pages", "phash", "coordinates", "max_results", "quality_analysis", "derived_next_cursor")
|
"pages", "phash", "coordinates", "max_results", "quality_analysis", "derived_next_cursor",
|
||||||
|
"accessibility_analysis", "versions")
|
||||||
return call_api("get", uri, params, **options)
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
@ -327,8 +355,8 @@ def restore(public_ids, **options):
|
||||||
resource_type = options.pop("resource_type", "image")
|
resource_type = options.pop("resource_type", "image")
|
||||||
upload_type = options.pop("type", "upload")
|
upload_type = options.pop("type", "upload")
|
||||||
uri = ["resources", resource_type, upload_type, "restore"]
|
uri = ["resources", resource_type, upload_type, "restore"]
|
||||||
params = dict(public_ids=public_ids)
|
params = dict(public_ids=public_ids, **only(options, "versions"))
|
||||||
return call_api("post", uri, params, **options)
|
return call_json_api("post", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
def upload_mappings(**options):
|
def upload_mappings(**options):
|
||||||
|
@ -390,90 +418,6 @@ def update_streaming_profile(name, **options):
|
||||||
return call_api('PUT', uri, params, **options)
|
return call_api('PUT', uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
def call_json_api(method, uri, jsonBody, **options):
|
|
||||||
logger.debug(jsonBody)
|
|
||||||
data = json.dumps(jsonBody).encode('utf-8')
|
|
||||||
return _call_api(method, uri, body=data,
|
|
||||||
headers={'Content-Type': 'application/json'}, **options)
|
|
||||||
|
|
||||||
|
|
||||||
def call_api(method, uri, params, **options):
|
|
||||||
return _call_api(method, uri, params=params, **options)
|
|
||||||
|
|
||||||
|
|
||||||
def call_metadata_api(method, uri, params, **options):
|
|
||||||
"""Private function that assists with performing an API call to the
|
|
||||||
metadata_fields part of the Admin API
|
|
||||||
|
|
||||||
:param method: The HTTP method. Valid methods: get, post, put, delete
|
|
||||||
:param uri: REST endpoint of the API (without 'metadata_fields')
|
|
||||||
:param params: Query/body parameters passed to the method
|
|
||||||
:param options: Additional options
|
|
||||||
|
|
||||||
:rtype: Response
|
|
||||||
"""
|
|
||||||
uri = ["metadata_fields"] + (uri or [])
|
|
||||||
return call_json_api(method, uri, params, **options)
|
|
||||||
|
|
||||||
|
|
||||||
def _call_api(method, uri, params=None, body=None, headers=None, **options):
|
|
||||||
prefix = options.pop("upload_prefix",
|
|
||||||
cloudinary.config().upload_prefix) or "https://api.cloudinary.com"
|
|
||||||
cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name)
|
|
||||||
if not cloud_name:
|
|
||||||
raise Exception("Must supply cloud_name")
|
|
||||||
api_key = options.pop("api_key", cloudinary.config().api_key)
|
|
||||||
if not api_key:
|
|
||||||
raise Exception("Must supply api_key")
|
|
||||||
api_secret = options.pop("api_secret", cloudinary.config().api_secret)
|
|
||||||
if not cloud_name:
|
|
||||||
raise Exception("Must supply api_secret")
|
|
||||||
api_url = "/".join([prefix, "v1_1", cloud_name] + uri)
|
|
||||||
|
|
||||||
processed_params = None
|
|
||||||
if isinstance(params, dict):
|
|
||||||
processed_params = {}
|
|
||||||
for key, value in params.items():
|
|
||||||
if isinstance(value, list) or isinstance(value, tuple):
|
|
||||||
value_list = {"{}[{}]".format(key, i): i_value for i, i_value in enumerate(value)}
|
|
||||||
processed_params.update(value_list)
|
|
||||||
elif value:
|
|
||||||
processed_params[key] = value
|
|
||||||
|
|
||||||
# Add authentication
|
|
||||||
req_headers = urllib3.make_headers(
|
|
||||||
basic_auth="{0}:{1}".format(api_key, api_secret),
|
|
||||||
user_agent=cloudinary.get_user_agent()
|
|
||||||
)
|
|
||||||
if headers is not None:
|
|
||||||
req_headers.update(headers)
|
|
||||||
kw = {}
|
|
||||||
if 'timeout' in options:
|
|
||||||
kw['timeout'] = options['timeout']
|
|
||||||
if body is not None:
|
|
||||||
kw['body'] = body
|
|
||||||
try:
|
|
||||||
response = _http.request(method.upper(), api_url, processed_params, req_headers, **kw)
|
|
||||||
body = response.data
|
|
||||||
except HTTPError as e:
|
|
||||||
raise GeneralError("Unexpected error {0}", e.message)
|
|
||||||
except socket.error as e:
|
|
||||||
raise GeneralError("Socket Error: %s" % (str(e)))
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = json.loads(body.decode('utf-8'))
|
|
||||||
except Exception as e:
|
|
||||||
# Error is parsing json
|
|
||||||
raise GeneralError("Error parsing server response (%d) - %s. Got - %s" % (response.status, body, e))
|
|
||||||
|
|
||||||
if "error" in result:
|
|
||||||
exception_class = EXCEPTION_CODES.get(response.status) or Exception
|
|
||||||
exception_class = exception_class
|
|
||||||
raise exception_class("Error {0} - {1}".format(response.status, result["error"]["message"]))
|
|
||||||
|
|
||||||
return Response(result, response)
|
|
||||||
|
|
||||||
|
|
||||||
def only(source, *keys):
|
def only(source, *keys):
|
||||||
return {key: source[key] for key in keys if key in source}
|
return {key: source[key] for key in keys if key in source}
|
||||||
|
|
||||||
|
|
0
lib/cloudinary/api_client/__init__.py
Normal file
0
lib/cloudinary/api_client/__init__.py
Normal file
35
lib/cloudinary/api_client/call_account_api.py
Normal file
35
lib/cloudinary/api_client/call_account_api.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
PROVISIONING_SUB_PATH = "provisioning"
|
||||||
|
ACCOUNT_SUB_PATH = "accounts"
|
||||||
|
_http = get_http_connector(account_config(), cloudinary.CERT_KWARGS)
|
||||||
|
|
||||||
|
|
||||||
|
def _call_account_api(method, uri, params=None, headers=None, **options):
|
||||||
|
prefix = options.pop("upload_prefix",
|
||||||
|
cloudinary.config().upload_prefix) or "https://api.cloudinary.com"
|
||||||
|
account_id = options.pop("account_id", account_config().account_id)
|
||||||
|
if not account_id:
|
||||||
|
raise Exception("Must supply account_id")
|
||||||
|
provisioning_api_key = options.pop("provisioning_api_key", account_config().provisioning_api_key)
|
||||||
|
if not provisioning_api_key:
|
||||||
|
raise Exception("Must supply provisioning_api_key")
|
||||||
|
provisioning_api_secret = options.pop("provisioning_api_secret",
|
||||||
|
account_config().provisioning_api_secret)
|
||||||
|
if not provisioning_api_secret:
|
||||||
|
raise Exception("Must supply provisioning_api_secret")
|
||||||
|
provisioning_api_url = "/".join(
|
||||||
|
[prefix, cloudinary.API_VERSION, PROVISIONING_SUB_PATH, ACCOUNT_SUB_PATH, account_id] + uri)
|
||||||
|
auth = {"key": provisioning_api_key, "secret": provisioning_api_secret}
|
||||||
|
|
||||||
|
return execute_request(http_connector=_http,
|
||||||
|
method=method,
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
auth=auth,
|
||||||
|
api_url=provisioning_api_url,
|
||||||
|
**options)
|
70
lib/cloudinary/api_client/call_api.py
Normal file
70
lib/cloudinary/api_client/call_api.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
import cloudinary
|
||||||
|
from cloudinary.api_client.execute_request import execute_request
|
||||||
|
from cloudinary.utils import get_http_connector
|
||||||
|
|
||||||
|
|
||||||
|
logger = cloudinary.logger
|
||||||
|
_http = get_http_connector(cloudinary.config(), cloudinary.CERT_KWARGS)
|
||||||
|
|
||||||
|
|
||||||
|
def call_metadata_api(method, uri, params, **options):
|
||||||
|
"""Private function that assists with performing an API call to the
|
||||||
|
metadata_fields part of the Admin API
|
||||||
|
:param method: The HTTP method. Valid methods: get, post, put, delete
|
||||||
|
:param uri: REST endpoint of the API (without 'metadata_fields')
|
||||||
|
:param params: Query/body parameters passed to the method
|
||||||
|
:param options: Additional options
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
uri = ["metadata_fields"] + (uri or [])
|
||||||
|
return call_json_api(method, uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def call_json_api(method, uri, json_body, **options):
|
||||||
|
data = json.dumps(json_body).encode('utf-8')
|
||||||
|
return _call_api(method, uri, body=data, headers={'Content-Type': 'application/json'}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def call_api(method, uri, params, **options):
|
||||||
|
return _call_api(method, uri, params=params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def _call_api(method, uri, params=None, body=None, headers=None, **options):
|
||||||
|
prefix = options.pop("upload_prefix",
|
||||||
|
cloudinary.config().upload_prefix) or "https://api.cloudinary.com"
|
||||||
|
cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name)
|
||||||
|
if not cloud_name:
|
||||||
|
raise Exception("Must supply cloud_name")
|
||||||
|
|
||||||
|
api_key = options.pop("api_key", cloudinary.config().api_key)
|
||||||
|
api_secret = options.pop("api_secret", cloudinary.config().api_secret)
|
||||||
|
oauth_token = options.pop("oauth_token", cloudinary.config().oauth_token)
|
||||||
|
|
||||||
|
_validate_authorization(api_key, api_secret, oauth_token)
|
||||||
|
|
||||||
|
api_url = "/".join([prefix, cloudinary.API_VERSION, cloud_name] + uri)
|
||||||
|
auth = {"key": api_key, "secret": api_secret, "oauth_token": oauth_token}
|
||||||
|
|
||||||
|
if body is not None:
|
||||||
|
options["body"] = body
|
||||||
|
|
||||||
|
return execute_request(http_connector=_http,
|
||||||
|
method=method,
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
auth=auth,
|
||||||
|
api_url=api_url,
|
||||||
|
**options)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_authorization(api_key, api_secret, oauth_token):
|
||||||
|
if oauth_token:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
raise Exception("Must supply api_key")
|
||||||
|
|
||||||
|
if not api_secret:
|
||||||
|
raise Exception("Must supply api_secret")
|
86
lib/cloudinary/api_client/execute_request.py
Normal file
86
lib/cloudinary/api_client/execute_request.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import email.utils
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import urllib3
|
||||||
|
from urllib3.exceptions import HTTPError
|
||||||
|
|
||||||
|
import cloudinary
|
||||||
|
from cloudinary.exceptions import (
|
||||||
|
BadRequest,
|
||||||
|
AuthorizationRequired,
|
||||||
|
NotAllowed,
|
||||||
|
NotFound,
|
||||||
|
AlreadyExists,
|
||||||
|
RateLimited,
|
||||||
|
GeneralError
|
||||||
|
)
|
||||||
|
from cloudinary.utils import process_params, safe_cast, smart_escape, unquote
|
||||||
|
|
||||||
|
EXCEPTION_CODES = {
|
||||||
|
400: BadRequest,
|
||||||
|
401: AuthorizationRequired,
|
||||||
|
403: NotAllowed,
|
||||||
|
404: NotFound,
|
||||||
|
409: AlreadyExists,
|
||||||
|
420: RateLimited,
|
||||||
|
500: GeneralError
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Response(dict):
|
||||||
|
def __init__(self, result, response, **kwargs):
|
||||||
|
super(Response, self).__init__(**kwargs)
|
||||||
|
self.update(result)
|
||||||
|
|
||||||
|
self.rate_limit_allowed = safe_cast(response.headers.get("x-featureratelimit-limit"), int)
|
||||||
|
self.rate_limit_reset_at = safe_cast(response.headers.get("x-featureratelimit-reset"), email.utils.parsedate)
|
||||||
|
self.rate_limit_remaining = safe_cast(response.headers.get("x-featureratelimit-remaining"), int)
|
||||||
|
|
||||||
|
|
||||||
|
def execute_request(http_connector, method, params, headers, auth, api_url, **options):
|
||||||
|
# authentication
|
||||||
|
key = auth.get("key")
|
||||||
|
secret = auth.get("secret")
|
||||||
|
oauth_token = auth.get("oauth_token")
|
||||||
|
req_headers = urllib3.make_headers(
|
||||||
|
user_agent=cloudinary.get_user_agent()
|
||||||
|
)
|
||||||
|
if oauth_token:
|
||||||
|
req_headers["authorization"] = "Bearer {}".format(oauth_token)
|
||||||
|
else:
|
||||||
|
req_headers.update(urllib3.make_headers(basic_auth="{0}:{1}".format(key, secret)))
|
||||||
|
|
||||||
|
if headers is not None:
|
||||||
|
req_headers.update(headers)
|
||||||
|
|
||||||
|
kw = {}
|
||||||
|
if "timeout" in options:
|
||||||
|
kw["timeout"] = options["timeout"]
|
||||||
|
if "body" in options:
|
||||||
|
kw["body"] = options["body"]
|
||||||
|
|
||||||
|
processed_params = process_params(params)
|
||||||
|
|
||||||
|
api_url = smart_escape(unquote(api_url))
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = http_connector.request(method.upper(), api_url, processed_params, req_headers, **kw)
|
||||||
|
body = response.data
|
||||||
|
except HTTPError as e:
|
||||||
|
raise GeneralError("Unexpected error {0}", e.message)
|
||||||
|
except socket.error as e:
|
||||||
|
raise GeneralError("Socket Error: %s" % (str(e)))
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = json.loads(body.decode('utf-8'))
|
||||||
|
except Exception as e:
|
||||||
|
# Error is parsing json
|
||||||
|
raise GeneralError("Error parsing server response (%d) - %s. Got - %s" % (response.status, body, e))
|
||||||
|
|
||||||
|
if "error" in result:
|
||||||
|
exception_class = EXCEPTION_CODES.get(response.status) or Exception
|
||||||
|
exception_class = exception_class
|
||||||
|
raise exception_class("Error {0} - {1}".format(response.status, result["error"]["message"]))
|
||||||
|
|
||||||
|
return Response(result, response)
|
|
@ -20,6 +20,9 @@ def generate(url=None, acl=None, start_time=None, duration=None,
|
||||||
else:
|
else:
|
||||||
raise Exception("Must provide either expiration or duration")
|
raise Exception("Must provide either expiration or duration")
|
||||||
|
|
||||||
|
if url is None and acl is None:
|
||||||
|
raise Exception("Must provide either acl or url")
|
||||||
|
|
||||||
token_parts = []
|
token_parts = []
|
||||||
if ip is not None:
|
if ip is not None:
|
||||||
token_parts.append("ip=" + ip)
|
token_parts.append("ip=" + ip)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import cloudinary.uploader
|
||||||
import cloudinary.utils
|
import cloudinary.utils
|
||||||
from cloudinary import CloudinaryResource
|
from cloudinary import CloudinaryResource
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
def cl_init_js_callbacks(form, request):
|
def cl_init_js_callbacks(form, request):
|
||||||
|
@ -52,7 +52,7 @@ class CloudinaryInput(forms.TextInput):
|
||||||
|
|
||||||
class CloudinaryJsFileField(forms.Field):
|
class CloudinaryJsFileField(forms.Field):
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'required': _(u"No file selected!")
|
"required": _("No file selected!")
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, attrs=None, options=None, autosave=True, *args, **kwargs):
|
def __init__(self, attrs=None, options=None, autosave=True, *args, **kwargs):
|
||||||
|
@ -121,7 +121,7 @@ class CloudinaryUnsignedJsFileField(CloudinaryJsFileField):
|
||||||
|
|
||||||
class CloudinaryFileField(forms.FileField):
|
class CloudinaryFileField(forms.FileField):
|
||||||
my_default_error_messages = {
|
my_default_error_messages = {
|
||||||
'required': _(u"No file selected!")
|
"required": _("No file selected!")
|
||||||
}
|
}
|
||||||
default_error_messages = forms.FileField.default_error_messages.copy()
|
default_error_messages = forms.FileField.default_error_messages.copy()
|
||||||
default_error_messages.update(my_default_error_messages)
|
default_error_messages.update(my_default_error_messages)
|
||||||
|
|
|
@ -107,6 +107,8 @@ class CloudinaryField(models.Field):
|
||||||
if isinstance(value, UploadedFile):
|
if isinstance(value, UploadedFile):
|
||||||
options = {"type": self.type, "resource_type": self.resource_type}
|
options = {"type": self.type, "resource_type": self.resource_type}
|
||||||
options.update(self.options)
|
options.update(self.options)
|
||||||
|
if hasattr(value, 'seekable') and value.seekable():
|
||||||
|
value.seek(0)
|
||||||
instance_value = uploader.upload_resource(value, **options)
|
instance_value = uploader.upload_resource(value, **options)
|
||||||
setattr(model_instance, self.attname, instance_value)
|
setattr(model_instance, self.attname, instance_value)
|
||||||
if self.width_field:
|
if self.width_field:
|
||||||
|
|
5
lib/cloudinary/provisioning/__init__.py
Normal file
5
lib/cloudinary/provisioning/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .account_config import AccountConfig, account_config, reset_config
|
||||||
|
from .account import (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,
|
||||||
|
users, create_user, delete_user, user, update_user, Role)
|
366
lib/cloudinary/provisioning/account.py
Normal file
366
lib/cloudinary/provisioning/account.py
Normal file
|
@ -0,0 +1,366 @@
|
||||||
|
from cloudinary.api_client.call_account_api import _call_account_api
|
||||||
|
from cloudinary.utils import encode_list
|
||||||
|
|
||||||
|
|
||||||
|
SUB_ACCOUNTS_SUB_PATH = "sub_accounts"
|
||||||
|
USERS_SUB_PATH = "users"
|
||||||
|
USER_GROUPS_SUB_PATH = "user_groups"
|
||||||
|
|
||||||
|
|
||||||
|
class Role(object):
|
||||||
|
"""
|
||||||
|
A user role to use in the user management API (create/update user).
|
||||||
|
"""
|
||||||
|
MASTER_ADMIN = "master_admin"
|
||||||
|
ADMIN = "admin"
|
||||||
|
BILLING = "billing"
|
||||||
|
TECHNICAL_ADMIN = "technical_admin"
|
||||||
|
REPORTS = "reports"
|
||||||
|
MEDIA_LIBRARY_ADMIN = "media_library_admin"
|
||||||
|
MEDIA_LIBRARY_USER = "media_library_user"
|
||||||
|
|
||||||
|
|
||||||
|
def sub_accounts(enabled=None, ids=None, prefix=None, **options):
|
||||||
|
"""
|
||||||
|
List all sub accounts
|
||||||
|
:param enabled: Whether to only return enabled sub-accounts (true) or disabled accounts (false).
|
||||||
|
Default: all accounts are returned (both enabled and disabled).
|
||||||
|
:type enabled: bool, optional
|
||||||
|
:param ids: List of sub-account IDs. Up to 100. When provided, other filters are ignored.
|
||||||
|
:type ids: list, optional
|
||||||
|
:param prefix: Search by prefix of the sub-account name. Case-insensitive.
|
||||||
|
:type prefix: str, optional
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: A list of sub accounts
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [SUB_ACCOUNTS_SUB_PATH]
|
||||||
|
params = {"ids": ids, "enabled": enabled, "prefix": prefix}
|
||||||
|
return _call_account_api("GET", uri, params=params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def create_sub_account(name, cloud_name=None, custom_attributes=None, enabled=None,
|
||||||
|
base_account=None, **options):
|
||||||
|
"""
|
||||||
|
Create a new sub account
|
||||||
|
:param name: Name of the new sub account
|
||||||
|
:type name: str
|
||||||
|
:param cloud_name: A case-insensitive cloud name comprised of alphanumeric and underscore characters.
|
||||||
|
* Generates an error if the cloud name is not unique across all Cloudinary accounts.
|
||||||
|
:type cloud_name: str, optional
|
||||||
|
:param custom_attributes: Any custom attributes you want to associate with the sub-account
|
||||||
|
:type custom_attributes: dict, optional
|
||||||
|
:param enabled: Whether to create the account as enabled (default is enabled).
|
||||||
|
:type enabled: bool, optional
|
||||||
|
:param base_account: ID of sub-account from which to copy settings
|
||||||
|
:type base_account: str, optional
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: The created sub account
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [SUB_ACCOUNTS_SUB_PATH]
|
||||||
|
params = {"name": name,
|
||||||
|
"cloud_name": cloud_name,
|
||||||
|
"custom_attributes": custom_attributes,
|
||||||
|
"enabled": enabled,
|
||||||
|
"base_account": base_account}
|
||||||
|
return _call_account_api("POST", uri, params=params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_sub_account(sub_account_id, **options):
|
||||||
|
"""
|
||||||
|
Delete a sub account
|
||||||
|
:param sub_account_id: The id of the sub account
|
||||||
|
:type sub_account_id: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Result message
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [SUB_ACCOUNTS_SUB_PATH, sub_account_id]
|
||||||
|
return _call_account_api("delete", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def sub_account(sub_account_id, **options):
|
||||||
|
"""
|
||||||
|
Get information of a sub account
|
||||||
|
:param sub_account_id: The id of the sub account
|
||||||
|
:type sub_account_id: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: A sub account
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [SUB_ACCOUNTS_SUB_PATH, sub_account_id]
|
||||||
|
return _call_account_api("get", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def update_sub_account(sub_account_id, name=None, cloud_name=None, custom_attributes=None,
|
||||||
|
enabled=None, base_account=None,
|
||||||
|
**options):
|
||||||
|
"""
|
||||||
|
Update a sub account
|
||||||
|
:param sub_account_id: The id of the sub account
|
||||||
|
:type sub_account_id: str
|
||||||
|
:param name: Name of the account
|
||||||
|
:type name: str, optional
|
||||||
|
:param cloud_name: Unique cloud name
|
||||||
|
:type cloud_name: str, optional
|
||||||
|
:param custom_attributes: Any custom attributes you want to associate with the sub-account.
|
||||||
|
:type custom_attributes: dict, optional
|
||||||
|
:param enabled: Whether to create the account as enabled (default is enabled).
|
||||||
|
:type enabled: bool, optional
|
||||||
|
:param base_account: ID of sub-account from which to copy settings
|
||||||
|
:type base_account: str, optional
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Updated sub account
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [SUB_ACCOUNTS_SUB_PATH, sub_account_id]
|
||||||
|
params = {"name": name,
|
||||||
|
"cloud_name": cloud_name,
|
||||||
|
"custom_attributes": custom_attributes,
|
||||||
|
"enabled": enabled,
|
||||||
|
"base_account": base_account}
|
||||||
|
return _call_account_api("put", uri, params=params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def users(user_ids=None, sub_account_id=None, pending=None, prefix=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 prefix: User prefix
|
||||||
|
:type prefix: str, optional
|
||||||
|
:param options: Generic advanced options dict, see online documentation.
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: List of users associated with the account
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USERS_SUB_PATH]
|
||||||
|
user_ids = encode_list(user_ids)
|
||||||
|
params = {"ids": user_ids,
|
||||||
|
"sub_account_id": sub_account_id,
|
||||||
|
"pending": pending,
|
||||||
|
"prefix": prefix}
|
||||||
|
return _call_account_api("get", uri, params=params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(name, email, role, sub_account_ids=None, **options):
|
||||||
|
"""
|
||||||
|
Create a user
|
||||||
|
:param name: Username
|
||||||
|
:type name: str
|
||||||
|
:param email: User's email
|
||||||
|
:type email: str
|
||||||
|
:param role: User's role
|
||||||
|
:type role: str
|
||||||
|
:param sub_account_ids: Optional. Sub accounts to associate with the user
|
||||||
|
:type sub_account_ids: list, optional
|
||||||
|
:param options: Generic advanced options dict, see online documentation.
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Details of created user
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USERS_SUB_PATH]
|
||||||
|
params = {"name": name,
|
||||||
|
"email": email,
|
||||||
|
"role": role,
|
||||||
|
"sub_account_ids": sub_account_ids}
|
||||||
|
return _call_account_api("post", uri, params=params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_user(user_id, **options):
|
||||||
|
"""
|
||||||
|
Delete a user
|
||||||
|
:param user_id: The id of user to delete
|
||||||
|
:type user_id: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation.
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Result message
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USERS_SUB_PATH, user_id]
|
||||||
|
return _call_account_api("delete", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def user(user_id, **options):
|
||||||
|
"""
|
||||||
|
Get information of a user
|
||||||
|
:param user_id: The id of the user
|
||||||
|
:type user_id: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation.
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: A user
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USERS_SUB_PATH, user_id]
|
||||||
|
return _call_account_api("get", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def update_user(user_id, name=None, email=None, role=None, sub_account_ids=None, **options):
|
||||||
|
"""
|
||||||
|
Update a user
|
||||||
|
:param user_id: The id of the user to update
|
||||||
|
:type user_id: str
|
||||||
|
:param name: Username
|
||||||
|
:type name: str, optional
|
||||||
|
:param email: User's email
|
||||||
|
:type email: str, optional
|
||||||
|
:param role: User's role
|
||||||
|
:type role: Role, optional
|
||||||
|
:param sub_account_ids: The list of sub-account IDs that this user can access.
|
||||||
|
Note: This parameter is ignored if the role is specified as master_admin.
|
||||||
|
:type sub_account_ids: list, optional
|
||||||
|
:param options: Generic advanced options dict, see online documentation.
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: The updated user
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USERS_SUB_PATH, user_id]
|
||||||
|
params = {"name": name,
|
||||||
|
"email": email,
|
||||||
|
"role": role,
|
||||||
|
"sub_account_ids": sub_account_ids}
|
||||||
|
return _call_account_api("put", uri, params=params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def user_groups(**options):
|
||||||
|
"""
|
||||||
|
List all user groups
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: List of user groups
|
||||||
|
:rtype: ProvisioningAPIRespose
|
||||||
|
"""
|
||||||
|
uri = [USER_GROUPS_SUB_PATH]
|
||||||
|
return _call_account_api("get", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def create_user_group(name, **options):
|
||||||
|
"""
|
||||||
|
Create a new user group
|
||||||
|
:param name: Name of the user group
|
||||||
|
:type name: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: The newly created group
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USER_GROUPS_SUB_PATH]
|
||||||
|
params = {"name": name}
|
||||||
|
return _call_account_api("post", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_group(user_group_id, name, **options):
|
||||||
|
"""
|
||||||
|
Update a user group
|
||||||
|
:param user_group_id: The id of the user group to update
|
||||||
|
:type user_group_id: str
|
||||||
|
:param name: Name of the user group
|
||||||
|
:type name: str, optional
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: The updated group
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USER_GROUPS_SUB_PATH, user_group_id]
|
||||||
|
params = {"name": name}
|
||||||
|
return _call_account_api("put", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_user_group(user_group_id, **options):
|
||||||
|
"""
|
||||||
|
Delete a user group
|
||||||
|
:param user_group_id: The id of the user group to delete
|
||||||
|
:type user_group_id: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: The result message
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USER_GROUPS_SUB_PATH, user_group_id]
|
||||||
|
return _call_account_api("delete", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def user_group(user_group_id, **options):
|
||||||
|
"""
|
||||||
|
Get information of a user group
|
||||||
|
:param user_group_id: The id of the user group
|
||||||
|
:type user_group_id: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Details of the group
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USER_GROUPS_SUB_PATH, user_group_id]
|
||||||
|
return _call_account_api("get", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def add_user_to_group(user_group_id, user_id, **options):
|
||||||
|
"""
|
||||||
|
Add a user to a user group
|
||||||
|
:param user_group_id: The id of the user group to add the user to
|
||||||
|
:type user_group_id: str
|
||||||
|
:param user_id: The user id to add
|
||||||
|
:type user_id: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: List of users in the group
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USER_GROUPS_SUB_PATH, user_group_id, "users", user_id]
|
||||||
|
return _call_account_api("post", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_user_from_group(user_group_id, user_id, **options):
|
||||||
|
"""
|
||||||
|
Remove a user from a user group
|
||||||
|
:param user_group_id: The id of the user group to remove the user from
|
||||||
|
:type user_group_id: str
|
||||||
|
:param user_id: The id of the user to remove
|
||||||
|
:type user_id: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: List of users in the group
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USER_GROUPS_SUB_PATH, user_group_id, "users", user_id]
|
||||||
|
return _call_account_api("delete", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def user_group_users(user_group_id, **options):
|
||||||
|
"""
|
||||||
|
Get all users in a user group
|
||||||
|
:param user_group_id: The id of user group to get list of users
|
||||||
|
:type user_group_id: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: List of users in the group
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USER_GROUPS_SUB_PATH, user_group_id, "users"]
|
||||||
|
return _call_account_api("get", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def user_in_user_groups(user_id, **options):
|
||||||
|
"""
|
||||||
|
Get all user groups a user belongs to
|
||||||
|
:param user_id: The id of user
|
||||||
|
:param user_id: str
|
||||||
|
:param options: Generic advanced options dict, see online documentation
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: List of groups user is in
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
uri = [USER_GROUPS_SUB_PATH, user_id]
|
||||||
|
return _call_account_api("get", uri, {}, **options)
|
42
lib/cloudinary/provisioning/account_config.py
Normal file
42
lib/cloudinary/provisioning/account_config.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from cloudinary import BaseConfig, import_django_settings
|
||||||
|
|
||||||
|
ACCOUNT_URI_SCHEME = "account"
|
||||||
|
|
||||||
|
|
||||||
|
class AccountConfig(BaseConfig):
|
||||||
|
def __init__(self):
|
||||||
|
self._uri_scheme = ACCOUNT_URI_SCHEME
|
||||||
|
|
||||||
|
super(AccountConfig, self).__init__()
|
||||||
|
|
||||||
|
def _config_from_parsed_url(self, parsed_url):
|
||||||
|
if not self._is_url_scheme_valid(parsed_url):
|
||||||
|
raise ValueError("Invalid CLOUDINARY_ACCOUNT_URL scheme. URL should begin with 'account://'")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"account_id": parsed_url.hostname,
|
||||||
|
"provisioning_api_key": parsed_url.username,
|
||||||
|
"provisioning_api_secret": parsed_url.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _load_config_from_env(self):
|
||||||
|
if os.environ.get("CLOUDINARY_ACCOUNT_URL"):
|
||||||
|
self._load_from_url(os.environ.get("CLOUDINARY_ACCOUNT_URL"))
|
||||||
|
|
||||||
|
|
||||||
|
def account_config(**keywords):
|
||||||
|
global _account_config
|
||||||
|
_account_config.update(**keywords)
|
||||||
|
return _account_config
|
||||||
|
|
||||||
|
|
||||||
|
def reset_config():
|
||||||
|
global _account_config
|
||||||
|
_account_config = AccountConfig()
|
||||||
|
|
||||||
|
|
||||||
|
_account_config = AccountConfig()
|
|
@ -1,7 +1,7 @@
|
||||||
import json
|
import json
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from cloudinary.api import call_json_api
|
from cloudinary.api_client.call_api import call_json_api
|
||||||
|
|
||||||
|
|
||||||
class Search:
|
class Search:
|
||||||
|
|
|
@ -164,7 +164,7 @@ var slice = [].slice,
|
||||||
* @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
|
* @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
|
||||||
* @example
|
* @example
|
||||||
*
|
*
|
||||||
* function Foo(){};
|
* function Foo(){};
|
||||||
* isFunction(Foo);
|
* isFunction(Foo);
|
||||||
* // => true
|
* // => true
|
||||||
*
|
*
|
||||||
|
@ -1403,7 +1403,8 @@ var slice = [].slice,
|
||||||
"*": "mul",
|
"*": "mul",
|
||||||
"/": "div",
|
"/": "div",
|
||||||
"+": "add",
|
"+": "add",
|
||||||
"-": "sub"
|
"-": "sub",
|
||||||
|
"^": "pow",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -1472,30 +1473,37 @@ var slice = [].slice,
|
||||||
return new this(expressionStr);
|
return new this(expressionStr);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a string expression
|
* Normalize a string expression
|
||||||
* @function Cloudinary#normalize
|
* @function Cloudinary#normalize
|
||||||
* @param {string} expression a expression, e.g. "w gt 100", "width_gt_100", "width > 100"
|
* @param {string} expression a expression, e.g. "w gt 100", "width_gt_100", "width > 100"
|
||||||
* @return {string} the normalized form of the value expression, e.g. "w_gt_100"
|
* @return {string} the normalized form of the value expression, e.g. "w_gt_100"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Expression.normalize = function(expression) {
|
Expression.normalize = function(expression) {
|
||||||
var operators, pattern, replaceRE;
|
var operators, operatorsPattern, operatorsReplaceRE, predefinedVarsPattern, predefinedVarsReplaceRE;
|
||||||
if (expression == null) {
|
if (expression == null) {
|
||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
expression = String(expression);
|
expression = String(expression);
|
||||||
operators = "\\|\\||>=|<=|&&|!=|>|=|<|/|-|\\+|\\*";
|
operators = "\\|\\||>=|<=|&&|!=|>|=|<|/|-|\\+|\\*|\\^";
|
||||||
pattern = "((" + operators + ")(?=[ _])|" + Object.keys(Expression.PREDEFINED_VARS).join("|") + ")";
|
|
||||||
replaceRE = new RegExp(pattern, "g");
|
// operators
|
||||||
expression = expression.replace(replaceRE, function(match) {
|
operatorsPattern = "((" + operators + ")(?=[ _]))";
|
||||||
return Expression.OPERATORS[match] || Expression.PREDEFINED_VARS[match];
|
operatorsReplaceRE = new RegExp(operatorsPattern, "g");
|
||||||
|
expression = expression.replace(operatorsReplaceRE, function (match) {
|
||||||
|
return Expression.OPERATORS[match];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// predefined variables
|
||||||
|
predefinedVarsPattern = "(" + Object.keys(Expression.PREDEFINED_VARS).join("|") + ")";
|
||||||
|
predefinedVarsReplaceRE = new RegExp(predefinedVarsPattern, "g");
|
||||||
|
expression = expression.replace(predefinedVarsReplaceRE, function(match, p1, offset){
|
||||||
|
return (expression[offset - 1] === '$' ? match : Expression.PREDEFINED_VARS[match]);
|
||||||
|
});
|
||||||
|
|
||||||
return expression.replace(/[ _]+/g, '_');
|
return expression.replace(/[ _]+/g, '_');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize the expression
|
* Serialize the expression
|
||||||
* @return {string} the expression as a string
|
* @return {string} the expression as a string
|
||||||
|
@ -3070,7 +3078,7 @@ var slice = [].slice,
|
||||||
* @protected
|
* @protected
|
||||||
* @param {string} key - attribute name
|
* @param {string} key - attribute name
|
||||||
* @param {*|boolean} value - the value of the attribute. If the value is boolean `true`, return the key only.
|
* @param {*|boolean} value - the value of the attribute. If the value is boolean `true`, return the key only.
|
||||||
* @returns {string} the attribute
|
* @returns {string} the attribute
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -4294,12 +4302,20 @@ var slice = [].slice,
|
||||||
switch (false) {
|
switch (false) {
|
||||||
case !/w_auto:breakpoints/.test(dataSrc):
|
case !/w_auto:breakpoints/.test(dataSrc):
|
||||||
requiredWidth = maxWidth(containerWidth, tag);
|
requiredWidth = maxWidth(containerWidth, tag);
|
||||||
dataSrc = dataSrc.replace(/w_auto:breakpoints([_0-9]*)(:[0-9]+)?/, "w_auto:breakpoints$1:" + requiredWidth);
|
if (requiredWidth) {
|
||||||
|
dataSrc = dataSrc.replace(/w_auto:breakpoints([_0-9]*)(:[0-9]+)?/, "w_auto:breakpoints$1:" + requiredWidth);
|
||||||
|
} else {
|
||||||
|
setUrl = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case !(match = /w_auto(:(\d+))?/.exec(dataSrc)):
|
case !(match = /w_auto(:(\d+))?/.exec(dataSrc)):
|
||||||
requiredWidth = applyBreakpoints.call(this, tag, containerWidth, match[2], options);
|
requiredWidth = applyBreakpoints.call(this, tag, containerWidth, match[2], options);
|
||||||
requiredWidth = maxWidth(requiredWidth, tag);
|
requiredWidth = maxWidth(requiredWidth, tag);
|
||||||
dataSrc = dataSrc.replace(/w_auto[^,\/]*/g, "w_" + requiredWidth);
|
if (requiredWidth) {
|
||||||
|
dataSrc = dataSrc.replace(/w_auto[^,\/]*/g, "w_" + requiredWidth);
|
||||||
|
} else {
|
||||||
|
setUrl = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Util.removeAttribute(tag, 'width');
|
Util.removeAttribute(tag, 'width');
|
||||||
if (!options.responsive_preserve_height) {
|
if (!options.responsive_preserve_height) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load staticfiles %}
|
{% load static %}
|
||||||
|
|
||||||
<script src="{% static "js/jquery.ui.widget.js" %}" type="text/javascript"></script>
|
<script src="{% static "js/jquery.ui.widget.js" %}" type="text/javascript"></script>
|
||||||
<script src="{% static "js/jquery.iframe-transport.js" %}" type="text/javascript"></script>
|
<script src="{% static "js/jquery.iframe-transport.js" %}" type="text/javascript"></script>
|
||||||
|
|
|
@ -58,7 +58,7 @@ def upload_image(file, **options):
|
||||||
|
|
||||||
|
|
||||||
def upload_resource(file, **options):
|
def upload_resource(file, **options):
|
||||||
result = upload(file, **options)
|
result = upload_large(file, **options)
|
||||||
return cloudinary.CloudinaryResource(
|
return cloudinary.CloudinaryResource(
|
||||||
result["public_id"], version=str(result["version"]),
|
result["public_id"], version=str(result["version"]),
|
||||||
format=result.get("format"), type=result["type"],
|
format=result.get("format"), type=result["type"],
|
||||||
|
@ -131,7 +131,9 @@ def rename(from_public_id, to_public_id, **options):
|
||||||
"invalidate": options.get("invalidate"),
|
"invalidate": options.get("invalidate"),
|
||||||
"from_public_id": from_public_id,
|
"from_public_id": from_public_id,
|
||||||
"to_public_id": to_public_id,
|
"to_public_id": to_public_id,
|
||||||
"to_type": options.get("to_type")
|
"to_type": options.get("to_type"),
|
||||||
|
"context": options.get("context"),
|
||||||
|
"metadata": options.get("metadata")
|
||||||
}
|
}
|
||||||
return call_api("rename", params, **options)
|
return call_api("rename", params, **options)
|
||||||
|
|
||||||
|
@ -180,30 +182,81 @@ def create_zip(**options):
|
||||||
return create_archive(target_format="zip", **options)
|
return create_archive(target_format="zip", **options)
|
||||||
|
|
||||||
|
|
||||||
def generate_sprite(tag, **options):
|
def generate_sprite(tag=None, urls=None, **options):
|
||||||
params = {
|
"""
|
||||||
"timestamp": utils.now(),
|
Generates sprites by merging multiple images into a single large image.
|
||||||
"tag": tag,
|
|
||||||
"async": options.get("async"),
|
See: `Sprite method API reference
|
||||||
"notification_url": options.get("notification_url"),
|
<https://cloudinary.com/documentation/image_upload_api_reference#sprite_method>`_
|
||||||
"transformation": utils.generate_transformation_string(
|
|
||||||
fetch_format=options.get("format"), **options)[0]
|
:param tag: The sprite is created from all images with this tag. If not set - `urls` parameter is required
|
||||||
}
|
:type tag: str
|
||||||
|
:param urls: List of URLs to create a sprite from. Can only be used if `tag` is not set
|
||||||
|
:type urls: list
|
||||||
|
:param options: Additional options
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Dictionary with meta information URLs of generated sprite resources
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
params = utils.build_multi_and_sprite_params(tag=tag, urls=urls, **options)
|
||||||
return call_api("sprite", params, **options)
|
return call_api("sprite", params, **options)
|
||||||
|
|
||||||
|
|
||||||
def multi(tag, **options):
|
def download_generated_sprite(tag=None, urls=None, **options):
|
||||||
params = {
|
"""
|
||||||
"timestamp": utils.now(),
|
Returns signed URL for the sprite endpoint with `mode=download`
|
||||||
"tag": tag,
|
|
||||||
"format": options.get("format"),
|
:param tag: The sprite is created from all images with this tag. If not set - `urls` parameter is required
|
||||||
"async": options.get("async"),
|
:type tag: str
|
||||||
"notification_url": options.get("notification_url"),
|
:param urls: List of URLs to create a sprite from. Can only be used if `tag` is not set
|
||||||
"transformation": utils.generate_transformation_string(**options)[0]
|
:type urls: list
|
||||||
}
|
:param options: Additional options
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: The signed URL to download sprite
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
params = utils.build_multi_and_sprite_params(tag=tag, urls=urls, **options)
|
||||||
|
return utils.cloudinary_api_download_url(action="sprite", params=params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def multi(tag=None, urls=None, **options):
|
||||||
|
"""
|
||||||
|
Creates either a single animated image, video or a PDF.
|
||||||
|
|
||||||
|
See: `Upload method API reference
|
||||||
|
<https://cloudinary.com/documentation/image_upload_api_reference#multi_method>`_
|
||||||
|
|
||||||
|
:param tag: The animated image, video or PDF is created from all images with this tag.
|
||||||
|
If not set - `urls` parameter is required
|
||||||
|
:type tag: str
|
||||||
|
:param urls: List of URLs to create an animated image, video or PDF from. Can only be used if `tag` is not set
|
||||||
|
:type urls: list
|
||||||
|
:param options: Additional options
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Dictionary with meta information URLs of the generated file
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
params = utils.build_multi_and_sprite_params(tag=tag, urls=urls, **options)
|
||||||
return call_api("multi", params, **options)
|
return call_api("multi", params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def download_multi(tag=None, urls=None, **options):
|
||||||
|
"""
|
||||||
|
Returns signed URL for the multi endpoint with `mode=download`
|
||||||
|
|
||||||
|
:param tag: The sprite is created from all images with this tag. If not set - `urls` parameter is required
|
||||||
|
:type tag: str
|
||||||
|
:param urls: List of URLs to create a sprite from. Can only be used if `tag` is not set
|
||||||
|
:type urls: list
|
||||||
|
:param options: Additional options
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: The signed URL to download multi
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
params = utils.build_multi_and_sprite_params(tag=tag, urls=urls, **options)
|
||||||
|
return utils.cloudinary_api_download_url(action="multi", params=params, **options)
|
||||||
|
|
||||||
|
|
||||||
def explode(public_id, **options):
|
def explode(public_id, **options):
|
||||||
params = {
|
params = {
|
||||||
"timestamp": utils.now(),
|
"timestamp": utils.now(),
|
||||||
|
@ -347,80 +400,80 @@ def call_cacheable_api(action, params, http_headers=None, return_error=False, un
|
||||||
|
|
||||||
|
|
||||||
def call_api(action, params, http_headers=None, return_error=False, unsigned=False, file=None, timeout=None, **options):
|
def call_api(action, params, http_headers=None, return_error=False, unsigned=False, file=None, timeout=None, **options):
|
||||||
if http_headers is None:
|
params = utils.cleanup_params(params)
|
||||||
http_headers = {}
|
|
||||||
file_io = None
|
|
||||||
try:
|
|
||||||
if unsigned:
|
|
||||||
params = utils.cleanup_params(params)
|
|
||||||
else:
|
|
||||||
params = utils.sign_request(params, options)
|
|
||||||
|
|
||||||
param_list = OrderedDict()
|
headers = {"User-Agent": cloudinary.get_user_agent()}
|
||||||
for k, v in params.items():
|
|
||||||
if isinstance(v, list):
|
|
||||||
for i in range(len(v)):
|
|
||||||
param_list["{0}[{1}]".format(k, i)] = v[i]
|
|
||||||
elif v:
|
|
||||||
param_list[k] = v
|
|
||||||
|
|
||||||
api_url = utils.cloudinary_api_url(action, **options)
|
if http_headers is not None:
|
||||||
if file:
|
|
||||||
filename = options.get("filename") # Custom filename provided by user (relevant only for streams and files)
|
|
||||||
|
|
||||||
if isinstance(file, string_types):
|
|
||||||
if utils.is_remote_url(file):
|
|
||||||
# URL
|
|
||||||
name = None
|
|
||||||
data = file
|
|
||||||
else:
|
|
||||||
# file path
|
|
||||||
name = filename or file
|
|
||||||
with open(file, "rb") as opened:
|
|
||||||
data = opened.read()
|
|
||||||
elif hasattr(file, 'read') and callable(file.read):
|
|
||||||
# stream
|
|
||||||
data = file.read()
|
|
||||||
name = filename or (file.name if hasattr(file, 'name') and isinstance(file.name, str) else "stream")
|
|
||||||
elif isinstance(file, tuple):
|
|
||||||
name, data = file
|
|
||||||
else:
|
|
||||||
# Not a string, not a stream
|
|
||||||
name = filename or "file"
|
|
||||||
data = file
|
|
||||||
|
|
||||||
param_list["file"] = (name, data) if name else data
|
|
||||||
|
|
||||||
headers = {"User-Agent": cloudinary.get_user_agent()}
|
|
||||||
headers.update(http_headers)
|
headers.update(http_headers)
|
||||||
|
|
||||||
kw = {}
|
oauth_token = options.get("oauth_token", cloudinary.config().oauth_token)
|
||||||
if timeout is not None:
|
|
||||||
kw['timeout'] = timeout
|
|
||||||
|
|
||||||
code = 200
|
if oauth_token:
|
||||||
try:
|
headers["authorization"] = "Bearer {}".format(oauth_token)
|
||||||
response = _http.request("POST", api_url, param_list, headers, **kw)
|
elif not unsigned:
|
||||||
except HTTPError as e:
|
params = utils.sign_request(params, options)
|
||||||
raise Error("Unexpected error - {0!r}".format(e))
|
|
||||||
except socket.error as e:
|
|
||||||
raise Error("Socket error: {0!r}".format(e))
|
|
||||||
|
|
||||||
try:
|
param_list = []
|
||||||
result = json.loads(response.data.decode('utf-8'))
|
for k, v in params.items():
|
||||||
except Exception as e:
|
if isinstance(v, list):
|
||||||
# Error is parsing json
|
for i in v:
|
||||||
raise Error("Error parsing server response (%d) - %s. Got - %s" % (response.status, response.data, e))
|
param_list.append(("{0}[]".format(k), i))
|
||||||
|
elif v:
|
||||||
|
param_list.append((k, v))
|
||||||
|
|
||||||
if "error" in result:
|
api_url = utils.cloudinary_api_url(action, **options)
|
||||||
if response.status not in [200, 400, 401, 403, 404, 500]:
|
|
||||||
code = response.status
|
if file:
|
||||||
if return_error:
|
filename = options.get("filename") # Custom filename provided by user (relevant only for streams and files)
|
||||||
result["error"]["http_code"] = code
|
|
||||||
|
if isinstance(file, string_types):
|
||||||
|
if utils.is_remote_url(file):
|
||||||
|
# URL
|
||||||
|
name = None
|
||||||
|
data = file
|
||||||
else:
|
else:
|
||||||
raise Error(result["error"]["message"])
|
# file path
|
||||||
|
name = filename or file
|
||||||
|
with open(file, "rb") as opened:
|
||||||
|
data = opened.read()
|
||||||
|
elif hasattr(file, 'read') and callable(file.read):
|
||||||
|
# stream
|
||||||
|
data = file.read()
|
||||||
|
name = filename or (file.name if hasattr(file, 'name') and isinstance(file.name, str) else "stream")
|
||||||
|
elif isinstance(file, tuple):
|
||||||
|
name, data = file
|
||||||
|
else:
|
||||||
|
# Not a string, not a stream
|
||||||
|
name = filename or "file"
|
||||||
|
data = file
|
||||||
|
|
||||||
return result
|
param_list.append(("file", (name, data) if name else data))
|
||||||
finally:
|
|
||||||
if file_io:
|
kw = {}
|
||||||
file_io.close()
|
if timeout is not None:
|
||||||
|
kw['timeout'] = timeout
|
||||||
|
|
||||||
|
code = 200
|
||||||
|
try:
|
||||||
|
response = _http.request("POST", api_url, param_list, headers, **kw)
|
||||||
|
except HTTPError as e:
|
||||||
|
raise Error("Unexpected error - {0!r}".format(e))
|
||||||
|
except socket.error as e:
|
||||||
|
raise Error("Socket error: {0!r}".format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = json.loads(response.data.decode('utf-8'))
|
||||||
|
except Exception as e:
|
||||||
|
# Error is parsing json
|
||||||
|
raise Error("Error parsing server response (%d) - %s. Got - %s" % (response.status, response.data, e))
|
||||||
|
|
||||||
|
if "error" in result:
|
||||||
|
if response.status not in [200, 400, 401, 403, 404, 500]:
|
||||||
|
code = response.status
|
||||||
|
if return_error:
|
||||||
|
result["error"]["http_code"] = code
|
||||||
|
else:
|
||||||
|
raise Error(result["error"]["message"])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
|
@ -15,10 +15,10 @@ from collections import OrderedDict
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from numbers import Number
|
from numbers import Number
|
||||||
from urllib3 import ProxyManager, PoolManager
|
|
||||||
|
|
||||||
import six.moves.urllib.parse
|
import six.moves.urllib.parse
|
||||||
from six import iteritems
|
from six import iteritems, string_types
|
||||||
|
from urllib3 import ProxyManager, PoolManager
|
||||||
|
|
||||||
import cloudinary
|
import cloudinary
|
||||||
from cloudinary import auth_token
|
from cloudinary import auth_token
|
||||||
|
@ -37,7 +37,7 @@ DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = {"width": "auto", "crop": "limit"}
|
||||||
RANGE_VALUE_RE = r'^(?P<value>(\d+\.)?\d+)(?P<modifier>[%pP])?$'
|
RANGE_VALUE_RE = r'^(?P<value>(\d+\.)?\d+)(?P<modifier>[%pP])?$'
|
||||||
RANGE_RE = r'^(\d+\.)?\d+[%pP]?\.\.(\d+\.)?\d+[%pP]?$'
|
RANGE_RE = r'^(\d+\.)?\d+[%pP]?\.\.(\d+\.)?\d+[%pP]?$'
|
||||||
FLOAT_RE = r'^(\d+)\.(\d+)?$'
|
FLOAT_RE = r'^(\d+)\.(\d+)?$'
|
||||||
REMOTE_URL_RE = r'ftp:|https?:|s3:|gs:|data:([\w-]+\/[\w-]+)?(;[\w-]+=[\w-]+)*;base64,([a-zA-Z0-9\/+\n=]+)$'
|
REMOTE_URL_RE = r'ftp:|https?:|s3:|gs:|data:([\w-]+\/[\w-]+(\+[\w-]+)?)?(;[\w-]+=[\w-]+)*;base64,([a-zA-Z0-9\/+\n=]+)$'
|
||||||
__LAYER_KEYWORD_PARAMS = [("font_weight", "normal"),
|
__LAYER_KEYWORD_PARAMS = [("font_weight", "normal"),
|
||||||
("font_style", "normal"),
|
("font_style", "normal"),
|
||||||
("text_decoration", "none"),
|
("text_decoration", "none"),
|
||||||
|
@ -63,7 +63,9 @@ __URL_KEYS = [
|
||||||
'type',
|
'type',
|
||||||
'url_suffix',
|
'url_suffix',
|
||||||
'use_root_path',
|
'use_root_path',
|
||||||
'version'
|
'version',
|
||||||
|
'long_url_signature',
|
||||||
|
'signature_algorithm',
|
||||||
]
|
]
|
||||||
|
|
||||||
__SIMPLE_UPLOAD_PARAMS = [
|
__SIMPLE_UPLOAD_PARAMS = [
|
||||||
|
@ -79,10 +81,12 @@ __SIMPLE_UPLOAD_PARAMS = [
|
||||||
"use_filename",
|
"use_filename",
|
||||||
"unique_filename",
|
"unique_filename",
|
||||||
"discard_original_filename",
|
"discard_original_filename",
|
||||||
|
"filename_override",
|
||||||
"invalidate",
|
"invalidate",
|
||||||
"notification_url",
|
"notification_url",
|
||||||
"eager_notification_url",
|
"eager_notification_url",
|
||||||
"eager_async",
|
"eager_async",
|
||||||
|
"eval",
|
||||||
"proxy",
|
"proxy",
|
||||||
"folder",
|
"folder",
|
||||||
"overwrite",
|
"overwrite",
|
||||||
|
@ -101,6 +105,7 @@ __SIMPLE_UPLOAD_PARAMS = [
|
||||||
"auto_tagging",
|
"auto_tagging",
|
||||||
"async",
|
"async",
|
||||||
"cinemagraph_analysis",
|
"cinemagraph_analysis",
|
||||||
|
"accessibility_analysis",
|
||||||
]
|
]
|
||||||
|
|
||||||
__SERIALIZED_UPLOAD_PARAMS = [
|
__SERIALIZED_UPLOAD_PARAMS = [
|
||||||
|
@ -121,16 +126,32 @@ __SERIALIZED_UPLOAD_PARAMS = [
|
||||||
|
|
||||||
upload_params = __SIMPLE_UPLOAD_PARAMS + __SERIALIZED_UPLOAD_PARAMS
|
upload_params = __SIMPLE_UPLOAD_PARAMS + __SERIALIZED_UPLOAD_PARAMS
|
||||||
|
|
||||||
|
SHORT_URL_SIGNATURE_LENGTH = 8
|
||||||
|
LONG_URL_SIGNATURE_LENGTH = 32
|
||||||
|
|
||||||
def compute_hex_hash(s):
|
SIGNATURE_SHA1 = "sha1"
|
||||||
|
SIGNATURE_SHA256 = "sha256"
|
||||||
|
|
||||||
|
signature_algorithms = {
|
||||||
|
SIGNATURE_SHA1: hashlib.sha1,
|
||||||
|
SIGNATURE_SHA256: hashlib.sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_hex_hash(s, algorithm=SIGNATURE_SHA1):
|
||||||
"""
|
"""
|
||||||
Compute hash and convert the result to HEX string
|
Computes string hash using specified algorithm and return HEX string representation of hash.
|
||||||
|
|
||||||
:param s: string to process
|
:param s: String to compute hash for
|
||||||
|
:param algorithm: The name of algorithm to use for computing hash
|
||||||
|
|
||||||
:return: HEX string
|
:return: HEX string of computed hash value
|
||||||
"""
|
"""
|
||||||
return hashlib.sha1(to_bytes(s)).hexdigest()
|
try:
|
||||||
|
hash_fn = signature_algorithms[algorithm]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError('Unsupported hash algorithm: {}'.format(algorithm))
|
||||||
|
return hash_fn(to_bytes(s)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def build_array(arg):
|
def build_array(arg):
|
||||||
|
@ -189,7 +210,7 @@ def encode_double_array(array):
|
||||||
if len(array) > 0 and isinstance(array[0], list):
|
if len(array) > 0 and isinstance(array[0], list):
|
||||||
return "|".join([",".join([str(i) for i in build_array(inner)]) for inner in array])
|
return "|".join([",".join([str(i) for i in build_array(inner)]) for inner in array])
|
||||||
else:
|
else:
|
||||||
return ",".join([str(i) for i in array])
|
return encode_list([str(i) for i in array])
|
||||||
|
|
||||||
|
|
||||||
def encode_dict(arg):
|
def encode_dict(arg):
|
||||||
|
@ -203,16 +224,37 @@ def encode_dict(arg):
|
||||||
return arg
|
return arg
|
||||||
|
|
||||||
|
|
||||||
def encode_context(context):
|
def normalize_context_value(value):
|
||||||
"""
|
"""
|
||||||
:param context: dict of context to be encoded
|
Escape "=" and "|" delimiter characters and json encode lists
|
||||||
:return: a joined string of all keys and values properly escaped and separated by a pipe character
|
|
||||||
|
:param value: Value to escape
|
||||||
|
:type value: int or str or list or tuple
|
||||||
|
|
||||||
|
:return: The normalized value
|
||||||
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if isinstance(value, (list, tuple)):
|
||||||
|
value = json_encode(value)
|
||||||
|
|
||||||
|
return str(value).replace("=", "\\=").replace("|", "\\|")
|
||||||
|
|
||||||
|
|
||||||
|
def encode_context(context):
|
||||||
|
"""
|
||||||
|
Encode metadata fields based on incoming value.
|
||||||
|
|
||||||
|
List and tuple values are encoded to json strings.
|
||||||
|
|
||||||
|
:param context: dict of context to be encoded
|
||||||
|
|
||||||
|
:return: a joined string of all keys and values properly escaped and separated by a pipe character
|
||||||
|
"""
|
||||||
if not isinstance(context, dict):
|
if not isinstance(context, dict):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
return "|".join(("{}={}".format(k, v.replace("=", "\\=").replace("|", "\\|"))) for k, v in iteritems(context))
|
return "|".join(("{}={}".format(k, normalize_context_value(v))) for k, v in iteritems(context))
|
||||||
|
|
||||||
|
|
||||||
def json_encode(value):
|
def json_encode(value):
|
||||||
|
@ -226,6 +268,17 @@ def json_encode(value):
|
||||||
return json.dumps(value, default=__json_serializer, separators=(',', ':'))
|
return json.dumps(value, default=__json_serializer, separators=(',', ':'))
|
||||||
|
|
||||||
|
|
||||||
|
def encode_date_to_usage_api_format(date_obj):
|
||||||
|
"""
|
||||||
|
Encodes date object to `dd-mm-yyyy` format string
|
||||||
|
|
||||||
|
:param date_obj: datetime.date object to encode
|
||||||
|
|
||||||
|
:return: Encoded date as a string
|
||||||
|
"""
|
||||||
|
return date_obj.strftime('%d-%m-%Y')
|
||||||
|
|
||||||
|
|
||||||
def patch_fetch_format(options):
|
def patch_fetch_format(options):
|
||||||
"""
|
"""
|
||||||
When upload type is fetch, remove the format options.
|
When upload type is fetch, remove the format options.
|
||||||
|
@ -488,6 +541,19 @@ def process_radius(param):
|
||||||
return str(param)
|
return str(param)
|
||||||
|
|
||||||
|
|
||||||
|
def process_params(params):
|
||||||
|
processed_params = None
|
||||||
|
if isinstance(params, dict):
|
||||||
|
processed_params = {}
|
||||||
|
for key, value in params.items():
|
||||||
|
if isinstance(value, list) or isinstance(value, tuple):
|
||||||
|
value_list = {"{}[{}]".format(key, i): i_value for i, i_value in enumerate(value)}
|
||||||
|
processed_params.update(value_list)
|
||||||
|
elif value is not None:
|
||||||
|
processed_params[key] = value
|
||||||
|
return processed_params
|
||||||
|
|
||||||
|
|
||||||
def cleanup_params(params):
|
def cleanup_params(params):
|
||||||
return dict([(k, __safe_value(v)) for (k, v) in params.items() if v is not None and not v == ""])
|
return dict([(k, __safe_value(v)) for (k, v) in params.items() if v is not None and not v == ""])
|
||||||
|
|
||||||
|
@ -499,18 +565,19 @@ def sign_request(params, options):
|
||||||
api_secret = options.get("api_secret", cloudinary.config().api_secret)
|
api_secret = options.get("api_secret", cloudinary.config().api_secret)
|
||||||
if not api_secret:
|
if not api_secret:
|
||||||
raise ValueError("Must supply api_secret")
|
raise ValueError("Must supply api_secret")
|
||||||
|
signature_algorithm = options.get("signature_algorithm", cloudinary.config().signature_algorithm)
|
||||||
|
|
||||||
params = cleanup_params(params)
|
params = cleanup_params(params)
|
||||||
params["signature"] = api_sign_request(params, api_secret)
|
params["signature"] = api_sign_request(params, api_secret, signature_algorithm)
|
||||||
params["api_key"] = api_key
|
params["api_key"] = api_key
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
def api_sign_request(params_to_sign, api_secret):
|
def api_sign_request(params_to_sign, api_secret, algorithm=SIGNATURE_SHA1):
|
||||||
params = [(k + "=" + (",".join(v) if isinstance(v, list) else str(v))) for k, v in params_to_sign.items() if v]
|
params = [(k + "=" + (",".join(v) if isinstance(v, list) else str(v))) for k, v in params_to_sign.items() if v]
|
||||||
to_sign = "&".join(sorted(params))
|
to_sign = "&".join(sorted(params))
|
||||||
return compute_hex_hash(to_sign + api_secret)
|
return compute_hex_hash(to_sign + api_secret, algorithm)
|
||||||
|
|
||||||
|
|
||||||
def breakpoint_settings_mapper(breakpoint_settings):
|
def breakpoint_settings_mapper(breakpoint_settings):
|
||||||
|
@ -667,6 +734,8 @@ def cloudinary_url(source, **options):
|
||||||
url_suffix = options.pop("url_suffix", None)
|
url_suffix = options.pop("url_suffix", None)
|
||||||
use_root_path = options.pop("use_root_path", cloudinary.config().use_root_path)
|
use_root_path = options.pop("use_root_path", cloudinary.config().use_root_path)
|
||||||
auth_token = options.pop("auth_token", None)
|
auth_token = options.pop("auth_token", None)
|
||||||
|
long_url_signature = options.pop("long_url_signature", cloudinary.config().long_url_signature)
|
||||||
|
signature_algorithm = options.pop("signature_algorithm", cloudinary.config().signature_algorithm)
|
||||||
if auth_token is not False:
|
if auth_token is not False:
|
||||||
auth_token = merge(cloudinary.config().auth_token, auth_token)
|
auth_token = merge(cloudinary.config().auth_token, auth_token)
|
||||||
|
|
||||||
|
@ -692,9 +761,18 @@ def cloudinary_url(source, **options):
|
||||||
signature = None
|
signature = None
|
||||||
if sign_url and not auth_token:
|
if sign_url and not auth_token:
|
||||||
to_sign = "/".join(__compact([transformation, source_to_sign]))
|
to_sign = "/".join(__compact([transformation, source_to_sign]))
|
||||||
|
if long_url_signature:
|
||||||
|
# Long signature forces SHA256
|
||||||
|
signature_algorithm = SIGNATURE_SHA256
|
||||||
|
chars_length = LONG_URL_SIGNATURE_LENGTH
|
||||||
|
else:
|
||||||
|
chars_length = SHORT_URL_SIGNATURE_LENGTH
|
||||||
|
if signature_algorithm not in signature_algorithms:
|
||||||
|
raise ValueError("Unsupported signature algorithm '{}'".format(signature_algorithm))
|
||||||
|
hash_fn = signature_algorithms[signature_algorithm]
|
||||||
signature = "s--" + to_string(
|
signature = "s--" + to_string(
|
||||||
base64.urlsafe_b64encode(
|
base64.urlsafe_b64encode(
|
||||||
hashlib.sha1(to_bytes(to_sign + api_secret)).digest())[0:8]) + "--"
|
hash_fn(to_bytes(to_sign + api_secret)).digest())[0:chars_length]) + "--"
|
||||||
|
|
||||||
prefix = unsigned_download_url_prefix(
|
prefix = unsigned_download_url_prefix(
|
||||||
source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain,
|
source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain,
|
||||||
|
@ -708,15 +786,30 @@ def cloudinary_url(source, **options):
|
||||||
return source, options
|
return source, options
|
||||||
|
|
||||||
|
|
||||||
def cloudinary_api_url(action='upload', **options):
|
def base_api_url(path, **options):
|
||||||
cloudinary_prefix = options.get("upload_prefix", cloudinary.config().upload_prefix)\
|
cloudinary_prefix = options.get("upload_prefix", cloudinary.config().upload_prefix) \
|
||||||
or "https://api.cloudinary.com"
|
or "https://api.cloudinary.com"
|
||||||
cloud_name = options.get("cloud_name", cloudinary.config().cloud_name)
|
cloud_name = options.get("cloud_name", cloudinary.config().cloud_name)
|
||||||
|
|
||||||
if not cloud_name:
|
if not cloud_name:
|
||||||
raise ValueError("Must supply cloud_name")
|
raise ValueError("Must supply cloud_name")
|
||||||
|
|
||||||
|
path = build_array(path)
|
||||||
|
|
||||||
|
return encode_unicode_url("/".join([cloudinary_prefix, cloudinary.API_VERSION, cloud_name] + path))
|
||||||
|
|
||||||
|
|
||||||
|
def cloudinary_api_url(action='upload', **options):
|
||||||
resource_type = options.get("resource_type", "image")
|
resource_type = options.get("resource_type", "image")
|
||||||
|
|
||||||
return encode_unicode_url("/".join([cloudinary_prefix, "v1_1", cloud_name, resource_type, action]))
|
return base_api_url([resource_type, action], **options)
|
||||||
|
|
||||||
|
|
||||||
|
def cloudinary_api_download_url(action, params, **options):
|
||||||
|
params = params.copy()
|
||||||
|
params["mode"] = "download"
|
||||||
|
cloudinary_params = sign_request(params, options)
|
||||||
|
return cloudinary_api_url(action, **options) + "?" + urlencode(bracketize_seq(cloudinary_params), True)
|
||||||
|
|
||||||
|
|
||||||
def cloudinary_scaled_url(source, width, transformation, options):
|
def cloudinary_scaled_url(source, width, transformation, options):
|
||||||
|
@ -817,11 +910,7 @@ def bracketize_seq(params):
|
||||||
|
|
||||||
|
|
||||||
def download_archive_url(**options):
|
def download_archive_url(**options):
|
||||||
params = options.copy()
|
return cloudinary_api_download_url(action="generate_archive", params=archive_params(**options), **options)
|
||||||
params.update(mode="download")
|
|
||||||
cloudinary_params = sign_request(archive_params(**params), options)
|
|
||||||
return cloudinary_api_url("generate_archive", **options) + "?" + \
|
|
||||||
urlencode(bracketize_seq(cloudinary_params), True)
|
|
||||||
|
|
||||||
|
|
||||||
def download_zip_url(**options):
|
def download_zip_url(**options):
|
||||||
|
@ -830,6 +919,47 @@ def download_zip_url(**options):
|
||||||
return download_archive_url(**new_options)
|
return download_archive_url(**new_options)
|
||||||
|
|
||||||
|
|
||||||
|
def download_folder(folder_path, **options):
|
||||||
|
"""
|
||||||
|
Creates and returns a URL that when invoked creates an archive of a folder.
|
||||||
|
:param folder_path: The full path from the root that is used to generate download url.
|
||||||
|
:type folder_path: str
|
||||||
|
:param options: Additional options.
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Signed URL to download the folder.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
options["prefixes"] = folder_path
|
||||||
|
options.setdefault("resource_type", "all")
|
||||||
|
|
||||||
|
return download_archive_url(**options)
|
||||||
|
|
||||||
|
|
||||||
|
def download_backedup_asset(asset_id, version_id, **options):
|
||||||
|
"""
|
||||||
|
The returned url allows downloading the backedup asset based on the the asset ID and the version ID.
|
||||||
|
|
||||||
|
Parameters asset_id and version_id are returned with api.resource(<PUBLIC_ID1>, versions=True) API call.
|
||||||
|
|
||||||
|
:param asset_id: The asset ID of the asset.
|
||||||
|
:type asset_id: str
|
||||||
|
:param version_id: The version ID of the asset.
|
||||||
|
:type version_id: str
|
||||||
|
:param options: Additional options.
|
||||||
|
:type options: dict, optional
|
||||||
|
:return:The signed URL for downloading backup version of the asset.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"timestamp": options.get("timestamp", now()),
|
||||||
|
"asset_id": asset_id,
|
||||||
|
"version_id": version_id
|
||||||
|
}
|
||||||
|
cloudinary_params = sign_request(params, options)
|
||||||
|
|
||||||
|
return base_api_url("download_backup", **options) + "?" + urlencode(bracketize_seq(cloudinary_params), True)
|
||||||
|
|
||||||
|
|
||||||
def generate_auth_token(**options):
|
def generate_auth_token(**options):
|
||||||
token_options = merge(cloudinary.config().auth_token, options)
|
token_options = merge(cloudinary.config().auth_token, options)
|
||||||
return auth_token.generate(**token_options)
|
return auth_token.generate(**token_options)
|
||||||
|
@ -919,8 +1049,8 @@ def build_upload_params(**options):
|
||||||
"transformation": generate_transformation_string(**options)[0],
|
"transformation": generate_transformation_string(**options)[0],
|
||||||
"headers": build_custom_headers(options.get("headers")),
|
"headers": build_custom_headers(options.get("headers")),
|
||||||
"eager": build_eager(options.get("eager")),
|
"eager": build_eager(options.get("eager")),
|
||||||
"tags": options.get("tags") and ",".join(build_array(options["tags"])),
|
"tags": options.get("tags") and encode_list(build_array(options["tags"])),
|
||||||
"allowed_formats": options.get("allowed_formats") and ",".join(build_array(options["allowed_formats"])),
|
"allowed_formats": options.get("allowed_formats") and encode_list(build_array(options["allowed_formats"])),
|
||||||
"face_coordinates": encode_double_array(options.get("face_coordinates")),
|
"face_coordinates": encode_double_array(options.get("face_coordinates")),
|
||||||
"custom_coordinates": encode_double_array(options.get("custom_coordinates")),
|
"custom_coordinates": encode_double_array(options.get("custom_coordinates")),
|
||||||
"context": encode_context(options.get("context")),
|
"context": encode_context(options.get("context")),
|
||||||
|
@ -938,6 +1068,26 @@ def build_upload_params(**options):
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def build_multi_and_sprite_params(**options):
|
||||||
|
"""
|
||||||
|
Build params for multi, download_multi, generate_sprite, and download_generated_sprite methods
|
||||||
|
"""
|
||||||
|
tag = options.get("tag")
|
||||||
|
urls = options.get("urls")
|
||||||
|
if bool(tag) == bool(urls):
|
||||||
|
raise ValueError("Either 'tag' or 'urls' parameter has to be set but not both")
|
||||||
|
params = {
|
||||||
|
"mode": options.get("mode"),
|
||||||
|
"timestamp": now(),
|
||||||
|
"async": options.get("async"),
|
||||||
|
"notification_url": options.get("notification_url"),
|
||||||
|
"tag": tag,
|
||||||
|
"urls": urls,
|
||||||
|
"transformation": generate_transformation_string(fetch_format=options.get("format"), **options)[0]
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
def __process_text_options(layer, layer_parameter):
|
def __process_text_options(layer, layer_parameter):
|
||||||
font_family = layer.get("font_family")
|
font_family = layer.get("font_family")
|
||||||
font_size = layer.get("font_size")
|
font_size = layer.get("font_size")
|
||||||
|
@ -1067,22 +1217,38 @@ IF_OPERATORS = {
|
||||||
|
|
||||||
PREDEFINED_VARS = {
|
PREDEFINED_VARS = {
|
||||||
"aspect_ratio": "ar",
|
"aspect_ratio": "ar",
|
||||||
|
"aspectRatio": "ar",
|
||||||
"current_page": "cp",
|
"current_page": "cp",
|
||||||
|
"currentPage": "cp",
|
||||||
"face_count": "fc",
|
"face_count": "fc",
|
||||||
|
"faceCount": "fc",
|
||||||
"height": "h",
|
"height": "h",
|
||||||
"initial_aspect_ratio": "iar",
|
"initial_aspect_ratio": "iar",
|
||||||
|
"initialAspectRatio": "iar",
|
||||||
|
"trimmed_aspect_ratio": "tar",
|
||||||
|
"trimmedAspectRatio": "tar",
|
||||||
"initial_height": "ih",
|
"initial_height": "ih",
|
||||||
|
"initialHeight": "ih",
|
||||||
"initial_width": "iw",
|
"initial_width": "iw",
|
||||||
|
"initialWidth": "iw",
|
||||||
"page_count": "pc",
|
"page_count": "pc",
|
||||||
|
"pageCount": "pc",
|
||||||
"page_x": "px",
|
"page_x": "px",
|
||||||
|
"pageX": "px",
|
||||||
"page_y": "py",
|
"page_y": "py",
|
||||||
|
"pageY": "py",
|
||||||
"tags": "tags",
|
"tags": "tags",
|
||||||
"width": "w",
|
"width": "w",
|
||||||
"duration": "du",
|
"duration": "du",
|
||||||
"initial_duration": "idu",
|
"initial_duration": "idu",
|
||||||
|
"initialDuration": "idu",
|
||||||
|
"illustration_score": "ils",
|
||||||
|
"illustrationScore": "ils",
|
||||||
|
"context": "ctx"
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceRE = "((\\|\\||>=|<=|&&|!=|>|=|<|/|-|\\+|\\*|\^)(?=[ _])|(?<!\$)(" + '|'.join(PREDEFINED_VARS.keys()) + "))"
|
replaceRE = "((\\|\\||>=|<=|&&|!=|>|=|<|/|-|\\+|\\*|\\^)(?=[ _])|(\\$_*[^_ ]+)|(?<!\\$)(" + \
|
||||||
|
'|'.join(PREDEFINED_VARS.keys()) + "))"
|
||||||
|
|
||||||
|
|
||||||
def translate_if(match):
|
def translate_if(match):
|
||||||
|
@ -1277,13 +1443,15 @@ def check_property_enabled(f):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def verify_api_response_signature(public_id, version, signature):
|
def verify_api_response_signature(public_id, version, signature, algorithm=None):
|
||||||
"""
|
"""
|
||||||
Verifies the authenticity of an API response signature
|
Verifies the authenticity of an API response signature
|
||||||
|
|
||||||
:param public_id: The public id of the asset as returned in the API response
|
:param public_id: The public id of the asset as returned in the API response
|
||||||
:param version: The version of the asset as returned in the API response
|
:param version: The version of the asset as returned in the API response
|
||||||
:param signature: Actual signature. Can be retrieved from the X-Cld-Signature header
|
:param signature: Actual signature. Can be retrieved from the X-Cld-Signature header
|
||||||
|
:param algorithm: Name of hashing algorithm to use for calculation of HMACs.
|
||||||
|
By default uses `cloudinary.config().signature_algorithm`
|
||||||
|
|
||||||
:return: Boolean result of the validation
|
:return: Boolean result of the validation
|
||||||
"""
|
"""
|
||||||
|
@ -1293,10 +1461,14 @@ def verify_api_response_signature(public_id, version, signature):
|
||||||
parameters_to_sign = {'public_id': public_id,
|
parameters_to_sign = {'public_id': public_id,
|
||||||
'version': version}
|
'version': version}
|
||||||
|
|
||||||
return signature == api_sign_request(parameters_to_sign, cloudinary.config().api_secret)
|
return signature == api_sign_request(
|
||||||
|
parameters_to_sign,
|
||||||
|
cloudinary.config().api_secret,
|
||||||
|
algorithm or cloudinary.config().signature_algorithm
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def verify_notification_signature(body, timestamp, signature, valid_for=7200):
|
def verify_notification_signature(body, timestamp, signature, valid_for=7200, algorithm=None):
|
||||||
"""
|
"""
|
||||||
Verifies the authenticity of a notification signature
|
Verifies the authenticity of a notification signature
|
||||||
|
|
||||||
|
@ -1304,6 +1476,8 @@ def verify_notification_signature(body, timestamp, signature, valid_for=7200):
|
||||||
:param timestamp: Unix timestamp. Can be retrieved from the X-Cld-Timestamp header
|
:param timestamp: Unix timestamp. Can be retrieved from the X-Cld-Timestamp header
|
||||||
:param signature: Actual signature. Can be retrieved from the X-Cld-Signature header
|
:param signature: Actual signature. Can be retrieved from the X-Cld-Signature header
|
||||||
:param valid_for: The desired time in seconds for considering the request valid
|
:param valid_for: The desired time in seconds for considering the request valid
|
||||||
|
:param algorithm: Name of hashing algorithm to use for calculation of HMACs.
|
||||||
|
By default uses `cloudinary.config().signature_algorithm`
|
||||||
|
|
||||||
:return: Boolean result of the validation
|
:return: Boolean result of the validation
|
||||||
"""
|
"""
|
||||||
|
@ -1316,7 +1490,9 @@ def verify_notification_signature(body, timestamp, signature, valid_for=7200):
|
||||||
if not isinstance(body, str):
|
if not isinstance(body, str):
|
||||||
raise ValueError('Body should be type of string')
|
raise ValueError('Body should be type of string')
|
||||||
|
|
||||||
return signature == compute_hex_hash('{}{}{}'.format(body, timestamp, cloudinary.config().api_secret))
|
return signature == compute_hex_hash(
|
||||||
|
'{}{}{}'.format(body, timestamp, cloudinary.config().api_secret),
|
||||||
|
algorithm or cloudinary.config().signature_algorithm)
|
||||||
|
|
||||||
|
|
||||||
def get_http_connector(conf, options):
|
def get_http_connector(conf, options):
|
||||||
|
@ -1332,3 +1508,26 @@ def get_http_connector(conf, options):
|
||||||
return ProxyManager(conf.api_proxy, **options)
|
return ProxyManager(conf.api_proxy, **options)
|
||||||
else:
|
else:
|
||||||
return PoolManager(**options)
|
return PoolManager(**options)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_list(obj):
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return ",".join(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def safe_cast(val, casting_fn, default=None):
|
||||||
|
"""
|
||||||
|
Attempts to cast a value to another using a given casting function
|
||||||
|
Will return a default value if casting fails (configurable, defaults to None)
|
||||||
|
|
||||||
|
:param val: The value to cast
|
||||||
|
:param casting_fn: The casting function that will receive the value to cast
|
||||||
|
:param default: The return value if casting fails
|
||||||
|
|
||||||
|
:return: Result of casting the value or the value of the default parameter
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return casting_fn(val)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return default
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue