mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 05:01:14 -07:00
Update cloudinary to 1.20.0
This commit is contained in:
parent
1c56d9c513
commit
2984629b39
27 changed files with 2865 additions and 923 deletions
|
@ -1,29 +1,43 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
import numbers
|
||||||
|
import certifi
|
||||||
|
from math import ceil
|
||||||
|
from six import python_2_unicode_compatible
|
||||||
|
|
||||||
logger = logging.getLogger("Cloudinary")
|
logger = logging.getLogger("Cloudinary")
|
||||||
ch = logging.StreamHandler()
|
ch = logging.StreamHandler()
|
||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
ch.setFormatter(formatter)
|
ch.setFormatter(formatter)
|
||||||
logger.addHandler(ch)
|
logger.addHandler(ch)
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from six import python_2_unicode_compatible
|
|
||||||
|
|
||||||
from cloudinary import utils
|
from cloudinary import utils
|
||||||
|
from cloudinary.exceptions import GeneralError
|
||||||
|
from cloudinary.cache import responsive_breakpoints_cache
|
||||||
|
from cloudinary.http_client import HttpClient
|
||||||
from cloudinary.compat import urlparse, parse_qs
|
from cloudinary.compat import urlparse, parse_qs
|
||||||
from cloudinary.search import Search
|
|
||||||
|
from platform import python_version
|
||||||
|
|
||||||
|
CERT_KWARGS = {
|
||||||
|
'cert_reqs': 'CERT_REQUIRED',
|
||||||
|
'ca_certs': certifi.where(),
|
||||||
|
}
|
||||||
|
|
||||||
CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"
|
CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"
|
||||||
OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"
|
OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"
|
||||||
AKAMAI_SHARED_CDN = "res.cloudinary.com"
|
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"
|
||||||
|
|
||||||
VERSION = "1.11.0"
|
VERSION = "1.20.0"
|
||||||
USER_AGENT = "CloudinaryPython/" + VERSION
|
|
||||||
|
USER_AGENT = "CloudinaryPython/{} (Python {})".format(VERSION, python_version())
|
||||||
""" :const: USER_AGENT """
|
""" :const: USER_AGENT """
|
||||||
|
|
||||||
USER_PLATFORM = ""
|
USER_PLATFORM = ""
|
||||||
|
@ -39,7 +53,8 @@ The format of the value should be <ProductName>/Version[ (comment)].
|
||||||
|
|
||||||
|
|
||||||
def get_user_agent():
|
def get_user_agent():
|
||||||
"""Provides the `USER_AGENT` string that is passed to the Cloudinary servers.
|
"""
|
||||||
|
Provides the `USER_AGENT` string that is passed to the Cloudinary servers.
|
||||||
Prepends `USER_PLATFORM` if it is defined.
|
Prepends `USER_PLATFORM` if it is defined.
|
||||||
|
|
||||||
:returns: the user agent
|
:returns: the user agent
|
||||||
|
@ -54,15 +69,27 @@ def get_user_agent():
|
||||||
|
|
||||||
def import_django_settings():
|
def import_django_settings():
|
||||||
try:
|
try:
|
||||||
import django.conf
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if 'CLOUDINARY' in dir(django.conf.settings):
|
from django.conf import settings as _django_settings
|
||||||
return django.conf.settings.CLOUDINARY
|
|
||||||
|
# We can get a situation when Django module is installed in the system, but not initialized,
|
||||||
|
# which means we are running not in a Django process.
|
||||||
|
# In this case the following line throws ImproperlyConfigured exception
|
||||||
|
if 'cloudinary' in _django_settings.INSTALLED_APPS:
|
||||||
|
from django import get_version as _get_django_version
|
||||||
|
global USER_PLATFORM
|
||||||
|
USER_PLATFORM = "Django/{django_version}".format(django_version=_get_django_version())
|
||||||
|
|
||||||
|
if 'CLOUDINARY' in dir(_django_settings):
|
||||||
|
return _django_settings.CLOUDINARY
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except ImproperlyConfigured:
|
except ImproperlyConfigured:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -78,14 +105,18 @@ class Config(object):
|
||||||
api_key=os.environ.get("CLOUDINARY_API_KEY"),
|
api_key=os.environ.get("CLOUDINARY_API_KEY"),
|
||||||
api_secret=os.environ.get("CLOUDINARY_API_SECRET"),
|
api_secret=os.environ.get("CLOUDINARY_API_SECRET"),
|
||||||
secure_distribution=os.environ.get("CLOUDINARY_SECURE_DISTRIBUTION"),
|
secure_distribution=os.environ.get("CLOUDINARY_SECURE_DISTRIBUTION"),
|
||||||
private_cdn=os.environ.get("CLOUDINARY_PRIVATE_CDN") == 'true'
|
private_cdn=os.environ.get("CLOUDINARY_PRIVATE_CDN") == 'true',
|
||||||
|
api_proxy=os.environ.get("CLOUDINARY_API_PROXY"),
|
||||||
)
|
)
|
||||||
elif os.environ.get("CLOUDINARY_URL"):
|
elif os.environ.get("CLOUDINARY_URL"):
|
||||||
cloudinary_url = os.environ.get("CLOUDINARY_URL")
|
cloudinary_url = os.environ.get("CLOUDINARY_URL")
|
||||||
self._parse_cloudinary_url(cloudinary_url)
|
self._parse_cloudinary_url(cloudinary_url)
|
||||||
|
|
||||||
def _parse_cloudinary_url(self, cloudinary_url):
|
def _parse_cloudinary_url(self, cloudinary_url):
|
||||||
uri = urlparse(cloudinary_url.replace("cloudinary://", "http://"))
|
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():
|
for k, v in parse_qs(uri.query).items():
|
||||||
if self._is_nested_key(k):
|
if self._is_nested_key(k):
|
||||||
self._put_nested_key(k, v)
|
self._put_nested_key(k, v)
|
||||||
|
@ -115,7 +146,7 @@ class Config(object):
|
||||||
|
|
||||||
def _put_nested_key(self, key, value):
|
def _put_nested_key(self, key, value):
|
||||||
chain = re.split(r'[\[\]]+', key)
|
chain = re.split(r'[\[\]]+', key)
|
||||||
chain = [key for key in chain if key]
|
chain = [k for k in chain if k]
|
||||||
outer = self.__dict__
|
outer = self.__dict__
|
||||||
last_key = chain.pop()
|
last_key = chain.pop()
|
||||||
for inner_key in chain:
|
for inner_key in chain:
|
||||||
|
@ -128,7 +159,21 @@ class Config(object):
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
value = value[0]
|
value = value[0]
|
||||||
outer[last_key] = value
|
outer[last_key] = value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_url_scheme_valid(url):
|
||||||
|
"""
|
||||||
|
Helper function. Validates url scheme
|
||||||
|
|
||||||
|
:param url: A named tuple containing URL components
|
||||||
|
|
||||||
|
:return: bool True on success or False on failure
|
||||||
|
"""
|
||||||
|
if not url.scheme or url.scheme.lower() != URI_SCHEME:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
_config = Config()
|
_config = Config()
|
||||||
|
|
||||||
|
|
||||||
|
@ -143,8 +188,35 @@ def reset_config():
|
||||||
_config = Config()
|
_config = Config()
|
||||||
|
|
||||||
|
|
||||||
|
_http_client = HttpClient()
|
||||||
|
|
||||||
|
# FIXME: circular import issue
|
||||||
|
from cloudinary.search import Search
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CloudinaryResource(object):
|
class CloudinaryResource(object):
|
||||||
|
"""
|
||||||
|
Recommended sources for video tag
|
||||||
|
"""
|
||||||
|
default_video_sources = [
|
||||||
|
{
|
||||||
|
"type": "mp4",
|
||||||
|
"codecs": "hev1",
|
||||||
|
"transformations": {"video_codec": "h265"}
|
||||||
|
}, {
|
||||||
|
"type": "webm",
|
||||||
|
"codecs": "vp9",
|
||||||
|
"transformations": {"video_codec": "vp9"}
|
||||||
|
}, {
|
||||||
|
"type": "mp4",
|
||||||
|
"transformations": {"video_codec": "auto"}
|
||||||
|
}, {
|
||||||
|
"type": "webm",
|
||||||
|
"transformations": {"video_codec": "auto"}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, public_id=None, format=None, version=None,
|
def __init__(self, public_id=None, format=None, version=None,
|
||||||
signature=None, url_options=None, metadata=None, type=None, resource_type=None,
|
signature=None, url_options=None, metadata=None, type=None, resource_type=None,
|
||||||
default_resource_type=None):
|
default_resource_type=None):
|
||||||
|
@ -174,9 +246,11 @@ class CloudinaryResource(object):
|
||||||
return None
|
return None
|
||||||
prep = ''
|
prep = ''
|
||||||
prep = prep + self.resource_type + '/' + self.type + '/'
|
prep = prep + self.resource_type + '/' + self.type + '/'
|
||||||
if self.version: prep = prep + 'v' + str(self.version) + '/'
|
if self.version:
|
||||||
|
prep = prep + 'v' + str(self.version) + '/'
|
||||||
prep = prep + self.public_id
|
prep = prep + self.public_id
|
||||||
if self.format: prep = prep + '.' + self.format
|
if self.format:
|
||||||
|
prep = prep + '.' + self.format
|
||||||
return prep
|
return prep
|
||||||
|
|
||||||
def get_presigned(self):
|
def get_presigned(self):
|
||||||
|
@ -199,28 +273,283 @@ class CloudinaryResource(object):
|
||||||
def build_url(self, **options):
|
def build_url(self, **options):
|
||||||
return self.__build_url(**options)[0]
|
return self.__build_url(**options)[0]
|
||||||
|
|
||||||
def default_poster_options(self, options):
|
@staticmethod
|
||||||
|
def default_poster_options(options):
|
||||||
options["format"] = options.get("format", "jpg")
|
options["format"] = options.get("format", "jpg")
|
||||||
|
|
||||||
def default_source_types(self):
|
@staticmethod
|
||||||
|
def default_source_types():
|
||||||
return ['webm', 'mp4', 'ogv']
|
return ['webm', 'mp4', 'ogv']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_srcset_data(srcset_data):
|
||||||
|
"""
|
||||||
|
Helper function. Validates srcset_data parameters
|
||||||
|
|
||||||
|
:param srcset_data: A dictionary containing the following keys:
|
||||||
|
breakpoints A list of breakpoints.
|
||||||
|
min_width Minimal width of the srcset images
|
||||||
|
max_width Maximal width of the srcset images.
|
||||||
|
max_images Number of srcset images to generate.
|
||||||
|
|
||||||
|
:return: bool True on success or False on failure
|
||||||
|
"""
|
||||||
|
if not all(k in srcset_data and isinstance(srcset_data[k], numbers.Number) for k in ("min_width", "max_width",
|
||||||
|
"max_images")):
|
||||||
|
logger.warning("Either valid (min_width, max_width, max_images)" +
|
||||||
|
"or breakpoints must be provided to the image srcset attribute")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if srcset_data["min_width"] > srcset_data["max_width"]:
|
||||||
|
logger.warning("min_width must be less than max_width")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if srcset_data["max_images"] <= 0:
|
||||||
|
logger.warning("max_images must be a positive integer")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _generate_breakpoints(self, srcset_data):
|
||||||
|
"""
|
||||||
|
Helper function. Calculates static responsive breakpoints using provided parameters.
|
||||||
|
|
||||||
|
Either the breakpoints or min_width, max_width, max_images must be provided.
|
||||||
|
|
||||||
|
:param srcset_data: A dictionary containing the following keys:
|
||||||
|
breakpoints A list of breakpoints.
|
||||||
|
min_width Minimal width of the srcset images
|
||||||
|
max_width Maximal width of the srcset images.
|
||||||
|
max_images Number of srcset images to generate.
|
||||||
|
|
||||||
|
:return: A list of breakpoints
|
||||||
|
|
||||||
|
:raises ValueError: In case of invalid or missing parameters
|
||||||
|
"""
|
||||||
|
breakpoints = srcset_data.get("breakpoints", list())
|
||||||
|
|
||||||
|
if breakpoints:
|
||||||
|
return breakpoints
|
||||||
|
|
||||||
|
if not self._validate_srcset_data(srcset_data):
|
||||||
|
return None
|
||||||
|
|
||||||
|
min_width, max_width, max_images = srcset_data["min_width"], srcset_data["max_width"], srcset_data["max_images"]
|
||||||
|
|
||||||
|
if max_images == 1:
|
||||||
|
# if user requested only 1 image in srcset, we return max_width one
|
||||||
|
min_width = max_width
|
||||||
|
|
||||||
|
step_size = int(ceil(float(max_width - min_width) / (max_images - 1 if max_images > 1 else 1)))
|
||||||
|
|
||||||
|
curr_breakpoint = min_width
|
||||||
|
|
||||||
|
while curr_breakpoint < max_width:
|
||||||
|
breakpoints.append(curr_breakpoint)
|
||||||
|
curr_breakpoint += step_size
|
||||||
|
|
||||||
|
breakpoints.append(max_width)
|
||||||
|
|
||||||
|
return breakpoints
|
||||||
|
|
||||||
|
def _fetch_breakpoints(self, srcset_data=None, **options):
|
||||||
|
"""
|
||||||
|
Helper function. Retrieves responsive breakpoints list from cloudinary server
|
||||||
|
|
||||||
|
When passing special string to transformation `width` parameter of form `auto:breakpoints{parameters}:json`,
|
||||||
|
the response contains JSON with data of the responsive breakpoints
|
||||||
|
|
||||||
|
:param srcset_data: A dictionary containing the following keys:
|
||||||
|
min_width Minimal width of the srcset images
|
||||||
|
max_width Maximal width of the srcset images
|
||||||
|
bytes_step Minimal bytes step between images
|
||||||
|
max_images Number of srcset images to generate
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:return: Resulting breakpoints
|
||||||
|
"""
|
||||||
|
if srcset_data is None:
|
||||||
|
srcset_data = dict()
|
||||||
|
|
||||||
|
min_width = srcset_data.get("min_width", 50)
|
||||||
|
max_width = srcset_data.get("max_width", 1000)
|
||||||
|
bytes_step = srcset_data.get("bytes_step", 20000)
|
||||||
|
max_images = srcset_data.get("max_images", 20)
|
||||||
|
transformation = srcset_data.get("transformation")
|
||||||
|
|
||||||
|
kbytes_step = int(ceil(float(bytes_step)/1024))
|
||||||
|
|
||||||
|
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)
|
||||||
|
breakpoints_url = utils.cloudinary_scaled_url(self.public_id, breakpoints_width_param, transformation, options)
|
||||||
|
|
||||||
|
return _http_client.get_json(breakpoints_url).get("breakpoints", None)
|
||||||
|
|
||||||
|
def _get_or_generate_breakpoints(self, srcset_data, **options):
|
||||||
|
"""
|
||||||
|
Helper function. Gets from cache or calculates srcset breakpoints using provided parameters
|
||||||
|
|
||||||
|
:param srcset_data: A dictionary containing the following keys:
|
||||||
|
breakpoints A list of breakpoints.
|
||||||
|
min_width Minimal width of the srcset images
|
||||||
|
max_width Maximal width of the srcset images
|
||||||
|
max_images Number of srcset images to generate
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:return: Resulting breakpoints
|
||||||
|
"""
|
||||||
|
|
||||||
|
breakpoints = srcset_data.get("breakpoints")
|
||||||
|
|
||||||
|
if breakpoints:
|
||||||
|
return breakpoints
|
||||||
|
|
||||||
|
if srcset_data.get("use_cache"):
|
||||||
|
breakpoints = responsive_breakpoints_cache.instance.get(self.public_id, **options)
|
||||||
|
if not breakpoints:
|
||||||
|
try:
|
||||||
|
breakpoints = self._fetch_breakpoints(srcset_data, **options)
|
||||||
|
except GeneralError as e:
|
||||||
|
logger.warning("Failed getting responsive breakpoints: {error}".format(error=e.message))
|
||||||
|
|
||||||
|
if breakpoints:
|
||||||
|
responsive_breakpoints_cache.instance.set(self.public_id, breakpoints, **options)
|
||||||
|
|
||||||
|
if not breakpoints:
|
||||||
|
# Static calculation if cache is not enabled or we failed to fetch breakpoints
|
||||||
|
breakpoints = self._generate_breakpoints(srcset_data)
|
||||||
|
|
||||||
|
return breakpoints
|
||||||
|
|
||||||
|
def _generate_srcset_attribute(self, breakpoints, transformation=None, **options):
|
||||||
|
"""
|
||||||
|
Helper function. Generates srcset attribute value of the HTML img tag.
|
||||||
|
|
||||||
|
:param breakpoints: A list of breakpoints.
|
||||||
|
:param transformation: Custom transformation
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:return: Resulting srcset attribute value
|
||||||
|
|
||||||
|
:raises ValueError: In case of invalid or missing parameters
|
||||||
|
"""
|
||||||
|
if not breakpoints:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if transformation is None:
|
||||||
|
transformation = dict()
|
||||||
|
|
||||||
|
return ", ".join(["{0} {1}w".format(utils.cloudinary_scaled_url(
|
||||||
|
self.public_id, w, transformation, options), w) for w in breakpoints])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_sizes_attribute(breakpoints):
|
||||||
|
"""
|
||||||
|
Helper function. Generates sizes attribute value of the HTML img tag.
|
||||||
|
|
||||||
|
:param breakpoints: A list of breakpoints.
|
||||||
|
|
||||||
|
:return: Resulting 'sizes' attribute value
|
||||||
|
"""
|
||||||
|
if not breakpoints:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ", ".join("(max-width: {bp}px) {bp}px".format(bp=bp) for bp in breakpoints)
|
||||||
|
|
||||||
|
def _generate_image_responsive_attributes(self, attributes, srcset_data, **options):
|
||||||
|
"""
|
||||||
|
Helper function. Generates srcset and sizes attributes of the image tag
|
||||||
|
|
||||||
|
Create both srcset and sizes here to avoid fetching breakpoints twice
|
||||||
|
|
||||||
|
:param attributes: Existing attributes
|
||||||
|
:param srcset_data: A dictionary containing the following keys:
|
||||||
|
breakpoints A list of breakpoints.
|
||||||
|
min_width Minimal width of the srcset images
|
||||||
|
max_width Maximal width of the srcset images.
|
||||||
|
max_images Number of srcset images to generate.
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:return: The responsive attributes
|
||||||
|
"""
|
||||||
|
responsive_attributes = dict()
|
||||||
|
|
||||||
|
if not srcset_data:
|
||||||
|
return responsive_attributes
|
||||||
|
|
||||||
|
breakpoints = None
|
||||||
|
|
||||||
|
if "srcset" not in attributes:
|
||||||
|
breakpoints = self._get_or_generate_breakpoints(srcset_data, **options)
|
||||||
|
transformation = srcset_data.get("transformation")
|
||||||
|
srcset_attr = self._generate_srcset_attribute(breakpoints, transformation, **options)
|
||||||
|
if srcset_attr:
|
||||||
|
responsive_attributes["srcset"] = srcset_attr
|
||||||
|
|
||||||
|
if "sizes" not in attributes and srcset_data.get("sizes") is True:
|
||||||
|
if not breakpoints:
|
||||||
|
breakpoints = self._get_or_generate_breakpoints(srcset_data, **options)
|
||||||
|
sizes_attr = self._generate_sizes_attribute(breakpoints)
|
||||||
|
if sizes_attr:
|
||||||
|
responsive_attributes["sizes"] = sizes_attr
|
||||||
|
|
||||||
|
return responsive_attributes
|
||||||
|
|
||||||
def image(self, **options):
|
def image(self, **options):
|
||||||
|
"""
|
||||||
|
Generates HTML img tag
|
||||||
|
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:return: Resulting img tag
|
||||||
|
"""
|
||||||
if options.get("resource_type", self.resource_type) == "video":
|
if options.get("resource_type", self.resource_type) == "video":
|
||||||
self.default_poster_options(options)
|
self.default_poster_options(options)
|
||||||
|
|
||||||
|
custom_attributes = options.pop("attributes", dict())
|
||||||
|
|
||||||
|
srcset_option = options.pop("srcset", dict())
|
||||||
|
srcset_data = dict()
|
||||||
|
|
||||||
|
if isinstance(srcset_option, dict):
|
||||||
|
srcset_data = config().srcset or dict()
|
||||||
|
srcset_data = srcset_data.copy()
|
||||||
|
srcset_data.update(srcset_option)
|
||||||
|
else:
|
||||||
|
if "srcset" not in custom_attributes:
|
||||||
|
custom_attributes["srcset"] = srcset_option
|
||||||
|
|
||||||
src, attrs = self.__build_url(**options)
|
src, attrs = self.__build_url(**options)
|
||||||
|
|
||||||
client_hints = attrs.pop("client_hints", config().client_hints)
|
client_hints = attrs.pop("client_hints", config().client_hints)
|
||||||
responsive = attrs.pop("responsive", False)
|
responsive = attrs.pop("responsive", False)
|
||||||
hidpi = attrs.pop("hidpi", False)
|
hidpi = attrs.pop("hidpi", False)
|
||||||
|
|
||||||
if (responsive or hidpi) and not client_hints:
|
if (responsive or hidpi) and not client_hints:
|
||||||
attrs["data-src"] = src
|
attrs["data-src"] = src
|
||||||
classes = "cld-responsive" if responsive else "cld-hidpi"
|
|
||||||
if "class" in attrs: classes += " " + attrs["class"]
|
|
||||||
attrs["class"] = classes
|
|
||||||
src = attrs.pop("responsive_placeholder", config().responsive_placeholder)
|
|
||||||
if src == "blank": src = CL_BLANK
|
|
||||||
|
|
||||||
if src: attrs["src"] = src
|
classes = "cld-responsive" if responsive else "cld-hidpi"
|
||||||
|
if "class" in attrs:
|
||||||
|
classes += " " + attrs["class"]
|
||||||
|
attrs["class"] = classes
|
||||||
|
|
||||||
|
src = attrs.pop("responsive_placeholder", config().responsive_placeholder)
|
||||||
|
if src == "blank":
|
||||||
|
src = CL_BLANK
|
||||||
|
|
||||||
|
responsive_attrs = self._generate_image_responsive_attributes(custom_attributes, srcset_data, **options)
|
||||||
|
|
||||||
|
if responsive_attrs:
|
||||||
|
# width and height attributes override srcset behavior, they should be removed from html attributes.
|
||||||
|
for key in {"width", "height"}:
|
||||||
|
attrs.pop(key, None)
|
||||||
|
|
||||||
|
attrs.update(responsive_attrs)
|
||||||
|
# Explicitly provided attributes override options
|
||||||
|
attrs.update(custom_attributes)
|
||||||
|
|
||||||
|
if src:
|
||||||
|
attrs["src"] = src
|
||||||
|
|
||||||
return u"<img {0}/>".format(utils.html_attrs(attrs))
|
return u"<img {0}/>".format(utils.html_attrs(attrs))
|
||||||
|
|
||||||
|
@ -228,69 +557,231 @@ class CloudinaryResource(object):
|
||||||
self.default_poster_options(options)
|
self.default_poster_options(options)
|
||||||
return self.build_url(**options)
|
return self.build_url(**options)
|
||||||
|
|
||||||
# Creates an HTML video tag for the provided +source+
|
@staticmethod
|
||||||
#
|
def _video_mime_type(video_type, codecs=None):
|
||||||
# ==== Options
|
"""
|
||||||
# * <tt>source_types</tt> - Specify which source type the tag should include. defaults to webm, mp4 and ogv.
|
Helper function for video(), generates video MIME type string from video_type and codecs.
|
||||||
# * <tt>source_transformation</tt> - specific transformations to use for a specific source type.
|
Example: video/mp4; codecs=mp4a.40.2
|
||||||
# * <tt>poster</tt> - override default thumbnail:
|
|
||||||
# * url: provide an ad hoc url
|
|
||||||
# * options: with specific poster transformations and/or Cloudinary +:public_id+
|
|
||||||
#
|
|
||||||
# ==== Examples
|
|
||||||
# CloudinaryResource("mymovie.mp4").video()
|
|
||||||
# CloudinaryResource("mymovie.mp4").video(source_types = 'webm')
|
|
||||||
# CloudinaryResource("mymovie.ogv").video(poster = "myspecialplaceholder.jpg")
|
|
||||||
# CloudinaryResource("mymovie.webm").video(source_types = ['webm', 'mp4'], poster = {'effect': 'sepia'})
|
|
||||||
def video(self, **options):
|
|
||||||
public_id = options.get('public_id', self.public_id)
|
|
||||||
source = re.sub("\.({0})$".format("|".join(self.default_source_types())), '', public_id)
|
|
||||||
|
|
||||||
|
:param video_type: mp4, webm, ogg etc.
|
||||||
|
:param codecs: List or string of codecs. E.g.: "avc1.42E01E" or "avc1.42E01E, mp4a.40.2" or
|
||||||
|
["avc1.42E01E", "mp4a.40.2"]
|
||||||
|
|
||||||
|
:return: Resulting mime type
|
||||||
|
"""
|
||||||
|
|
||||||
|
video_type = 'ogg' if video_type == 'ogv' else video_type
|
||||||
|
|
||||||
|
if not video_type:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
codecs_str = ", ".join(codecs) if isinstance(codecs, (list, tuple)) else codecs
|
||||||
|
codecs_attr = "; codecs={codecs_str}".format(codecs_str=codecs_str) if codecs_str else ""
|
||||||
|
|
||||||
|
return "video/{}{}".format(video_type, codecs_attr)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _collect_video_tag_attributes(video_options):
|
||||||
|
"""
|
||||||
|
Helper function for video tag, collects remaining options and returns them as attributes
|
||||||
|
|
||||||
|
:param video_options: Remaining options
|
||||||
|
|
||||||
|
:return: Resulting attributes
|
||||||
|
"""
|
||||||
|
attributes = video_options.copy()
|
||||||
|
|
||||||
|
if 'html_width' in attributes:
|
||||||
|
attributes['width'] = attributes.pop('html_width')
|
||||||
|
if 'html_height' in attributes:
|
||||||
|
attributes['height'] = attributes.pop('html_height')
|
||||||
|
|
||||||
|
if "poster" in attributes and not attributes["poster"]:
|
||||||
|
attributes.pop("poster", None)
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
def _generate_video_poster_attr(self, source, video_options):
|
||||||
|
"""
|
||||||
|
Helper function for video tag, generates video poster URL
|
||||||
|
|
||||||
|
:param source: The public ID of the resource
|
||||||
|
:param video_options: Additional options
|
||||||
|
|
||||||
|
:return: Resulting video poster URL
|
||||||
|
"""
|
||||||
|
if 'poster' not in video_options:
|
||||||
|
return self.video_thumbnail(public_id=source, **video_options)
|
||||||
|
|
||||||
|
poster_options = video_options['poster']
|
||||||
|
|
||||||
|
if not isinstance(poster_options, dict):
|
||||||
|
return poster_options
|
||||||
|
|
||||||
|
if 'public_id' not in poster_options:
|
||||||
|
return self.video_thumbnail(public_id=source, **poster_options)
|
||||||
|
|
||||||
|
return utils.cloudinary_url(poster_options['public_id'], **poster_options)[0]
|
||||||
|
|
||||||
|
def _populate_video_source_tags(self, source, options):
|
||||||
|
"""
|
||||||
|
Helper function for video tag, populates source tags from provided options.
|
||||||
|
|
||||||
|
source_types and sources are mutually exclusive, only one of them can be used.
|
||||||
|
If both are not provided, source types are used (for backwards compatibility)
|
||||||
|
|
||||||
|
:param source: The public ID of the video
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:return: Resulting source tags (may be empty)
|
||||||
|
"""
|
||||||
|
source_tags = []
|
||||||
|
|
||||||
|
# Consume all relevant options, otherwise they are left and passed as attributes
|
||||||
|
video_sources = options.pop('sources', [])
|
||||||
source_types = options.pop('source_types', [])
|
source_types = options.pop('source_types', [])
|
||||||
source_transformation = options.pop('source_transformation', {})
|
source_transformation = options.pop('source_transformation', {})
|
||||||
|
|
||||||
|
if video_sources and isinstance(video_sources, list):
|
||||||
|
# processing new source structure with codecs
|
||||||
|
for source_data in video_sources:
|
||||||
|
transformation = options.copy()
|
||||||
|
transformation.update(source_data.get("transformations", {}))
|
||||||
|
source_type = source_data.get("type", '')
|
||||||
|
src = utils.cloudinary_url(source, format=source_type, **transformation)[0]
|
||||||
|
codecs = source_data.get("codecs", [])
|
||||||
|
source_tags.append("<source {attributes}>".format(
|
||||||
|
attributes=utils.html_attrs({'src': src, 'type': self._video_mime_type(source_type, codecs)})))
|
||||||
|
|
||||||
|
return source_tags
|
||||||
|
|
||||||
|
# processing old source_types structure with out codecs
|
||||||
|
if not source_types:
|
||||||
|
source_types = self.default_source_types()
|
||||||
|
|
||||||
|
if not isinstance(source_types, (list, tuple)):
|
||||||
|
return source_tags
|
||||||
|
|
||||||
|
for source_type in source_types:
|
||||||
|
transformation = options.copy()
|
||||||
|
transformation.update(source_transformation.get(source_type, {}))
|
||||||
|
src = utils.cloudinary_url(source, format=source_type, **transformation)[0]
|
||||||
|
source_tags.append("<source {attributes}>".format(
|
||||||
|
attributes=utils.html_attrs({'src': src, 'type': self._video_mime_type(source_type)})))
|
||||||
|
|
||||||
|
return source_tags
|
||||||
|
|
||||||
|
def video(self, **options):
|
||||||
|
"""
|
||||||
|
Creates an HTML video tag for the provided +source+
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
CloudinaryResource("mymovie.mp4").video()
|
||||||
|
CloudinaryResource("mymovie.mp4").video(source_types = 'webm')
|
||||||
|
CloudinaryResource("mymovie.ogv").video(poster = "myspecialplaceholder.jpg")
|
||||||
|
CloudinaryResource("mymovie.webm").video(source_types = ['webm', 'mp4'], poster = {'effect': 'sepia'})
|
||||||
|
|
||||||
|
:param options:
|
||||||
|
* <tt>source_types</tt> - Specify which source type the tag should include.
|
||||||
|
defaults to webm, mp4 and ogv.
|
||||||
|
* <tt>sources</tt> - Similar to source_types, but may contain codecs list.
|
||||||
|
source_types and sources are mutually exclusive, only one of
|
||||||
|
them can be used. If both are not provided, default source types
|
||||||
|
are used.
|
||||||
|
* <tt>source_transformation</tt> - specific transformations to use
|
||||||
|
for a specific source type.
|
||||||
|
* <tt>poster</tt> - override default thumbnail:
|
||||||
|
* url: provide an ad hoc url
|
||||||
|
* options: with specific poster transformations and/or Cloudinary +:public_id+
|
||||||
|
|
||||||
|
:return: Video tag
|
||||||
|
"""
|
||||||
|
public_id = options.get('public_id', self.public_id)
|
||||||
|
source = re.sub(r"\.({0})$".format("|".join(self.default_source_types())), '', public_id)
|
||||||
|
|
||||||
|
custom_attributes = options.pop("attributes", dict())
|
||||||
|
|
||||||
fallback = options.pop('fallback_content', '')
|
fallback = options.pop('fallback_content', '')
|
||||||
options['resource_type'] = options.pop('resource_type', self.resource_type or 'video')
|
|
||||||
|
|
||||||
if not source_types: source_types = self.default_source_types()
|
# Save source types for a single video source handling (it can be a single type)
|
||||||
video_options = options.copy()
|
source_types = options.get('source_types', "")
|
||||||
|
|
||||||
if 'poster' in video_options:
|
poster_options = options.copy()
|
||||||
poster_options = video_options['poster']
|
if "poster" not in custom_attributes:
|
||||||
if isinstance(poster_options, dict):
|
options["poster"] = self._generate_video_poster_attr(source, poster_options)
|
||||||
if 'public_id' in poster_options:
|
|
||||||
video_options['poster'] = utils.cloudinary_url(poster_options['public_id'], **poster_options)[0]
|
|
||||||
else:
|
|
||||||
video_options['poster'] = self.video_thumbnail(public_id=source, **poster_options)
|
|
||||||
else:
|
|
||||||
video_options['poster'] = self.video_thumbnail(public_id=source, **options)
|
|
||||||
|
|
||||||
if not video_options['poster']: del video_options['poster']
|
if "resource_type" not in options:
|
||||||
|
options["resource_type"] = self.resource_type or "video"
|
||||||
|
|
||||||
nested_source_types = isinstance(source_types, list) and len(source_types) > 1
|
# populate video source tags
|
||||||
if not nested_source_types:
|
source_tags = self._populate_video_source_tags(source, options)
|
||||||
|
|
||||||
|
if not source_tags:
|
||||||
source = source + '.' + utils.build_array(source_types)[0]
|
source = source + '.' + utils.build_array(source_types)[0]
|
||||||
|
|
||||||
video_url = utils.cloudinary_url(source, **video_options)
|
video_url, video_options = utils.cloudinary_url(source, **options)
|
||||||
video_options = video_url[1]
|
|
||||||
if not nested_source_types:
|
|
||||||
video_options['src'] = video_url[0]
|
|
||||||
if 'html_width' in video_options: video_options['width'] = video_options.pop('html_width')
|
|
||||||
if 'html_height' in video_options: video_options['height'] = video_options.pop('html_height')
|
|
||||||
|
|
||||||
sources = ""
|
if not source_tags:
|
||||||
if nested_source_types:
|
custom_attributes['src'] = video_url
|
||||||
for source_type in source_types:
|
|
||||||
transformation = options.copy()
|
|
||||||
transformation.update(source_transformation.get(source_type, {}))
|
|
||||||
src = utils.cloudinary_url(source, format=source_type, **transformation)[0]
|
|
||||||
video_type = "ogg" if source_type == 'ogv' else source_type
|
|
||||||
mime_type = "video/" + video_type
|
|
||||||
sources += "<source {attributes}>".format(attributes=utils.html_attrs({'src': src, 'type': mime_type}))
|
|
||||||
|
|
||||||
|
attributes = self._collect_video_tag_attributes(video_options)
|
||||||
|
attributes.update(custom_attributes)
|
||||||
|
|
||||||
|
sources_str = ''.join(str(x) for x in source_tags)
|
||||||
html = "<video {attributes}>{sources}{fallback}</video>".format(
|
html = "<video {attributes}>{sources}{fallback}</video>".format(
|
||||||
attributes=utils.html_attrs(video_options), sources=sources, fallback=fallback)
|
attributes=utils.html_attrs(attributes), sources=sources_str, fallback=fallback)
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __generate_media_attr(**media_options):
|
||||||
|
media_query_conditions = []
|
||||||
|
if "min_width" in media_options:
|
||||||
|
media_query_conditions.append("(min-width: {}px)".format(media_options["min_width"]))
|
||||||
|
if "max_width" in media_options:
|
||||||
|
media_query_conditions.append("(max-width: {}px)".format(media_options["max_width"]))
|
||||||
|
|
||||||
|
return " and ".join(media_query_conditions)
|
||||||
|
|
||||||
|
def source(self, **options):
|
||||||
|
attrs = options.get("attributes") or {}
|
||||||
|
|
||||||
|
srcset_data = config().srcset or dict()
|
||||||
|
srcset_data = srcset_data.copy()
|
||||||
|
srcset_data.update(options.pop("srcset", dict()))
|
||||||
|
|
||||||
|
responsive_attrs = self._generate_image_responsive_attributes(attrs, srcset_data, **options)
|
||||||
|
|
||||||
|
attrs.update(responsive_attrs)
|
||||||
|
|
||||||
|
# `source` tag under `picture` tag uses `srcset` attribute for both `srcset` and `src` urls
|
||||||
|
if "srcset" not in attrs:
|
||||||
|
attrs["srcset"], _ = self.__build_url(**options)
|
||||||
|
|
||||||
|
if "media" not in attrs:
|
||||||
|
media_attr = self.__generate_media_attr(**(options.get("media", {})))
|
||||||
|
if media_attr:
|
||||||
|
attrs["media"] = media_attr
|
||||||
|
|
||||||
|
return u"<source {0}>".format(utils.html_attrs(attrs))
|
||||||
|
|
||||||
|
def picture(self, **options):
|
||||||
|
sub_tags = []
|
||||||
|
sources = options.pop("sources") or list()
|
||||||
|
for source in sources:
|
||||||
|
curr_options = deepcopy(options)
|
||||||
|
|
||||||
|
if "transformation" in source:
|
||||||
|
curr_options = utils.chain_transformations(curr_options, source["transformation"])
|
||||||
|
|
||||||
|
curr_options["media"] = dict((k, source[k]) for k in ['min_width', 'max_width'] if k in source)
|
||||||
|
|
||||||
|
sub_tags.append(self.source(**curr_options))
|
||||||
|
|
||||||
|
sub_tags.append(self.image(**options))
|
||||||
|
|
||||||
|
return u"<picture>{}</picture>".format("".join(sub_tags))
|
||||||
|
|
||||||
|
|
||||||
class CloudinaryImage(CloudinaryResource):
|
class CloudinaryImage(CloudinaryResource):
|
||||||
def __init__(self, public_id=None, **kwargs):
|
def __init__(self, public_id=None, **kwargs):
|
||||||
|
|
|
@ -4,28 +4,24 @@ import email.utils
|
||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import cloudinary
|
|
||||||
from six import string_types
|
|
||||||
|
|
||||||
import urllib3
|
import urllib3
|
||||||
import certifi
|
from six import string_types
|
||||||
|
|
||||||
from cloudinary import utils
|
|
||||||
from urllib3.exceptions import HTTPError
|
from urllib3.exceptions import HTTPError
|
||||||
|
|
||||||
|
import cloudinary
|
||||||
|
from cloudinary import utils
|
||||||
|
from cloudinary.exceptions import (
|
||||||
|
BadRequest,
|
||||||
|
AuthorizationRequired,
|
||||||
|
NotAllowed,
|
||||||
|
NotFound,
|
||||||
|
AlreadyExists,
|
||||||
|
RateLimited,
|
||||||
|
GeneralError
|
||||||
|
)
|
||||||
|
|
||||||
logger = cloudinary.logger
|
logger = cloudinary.logger
|
||||||
|
|
||||||
# intentionally one-liners
|
|
||||||
class Error(Exception): pass
|
|
||||||
class NotFound(Error): pass
|
|
||||||
class NotAllowed(Error): pass
|
|
||||||
class AlreadyExists(Error): pass
|
|
||||||
class RateLimited(Error): pass
|
|
||||||
class BadRequest(Error): pass
|
|
||||||
class GeneralError(Error): pass
|
|
||||||
class AuthorizationRequired(Error): pass
|
|
||||||
|
|
||||||
|
|
||||||
EXCEPTION_CODES = {
|
EXCEPTION_CODES = {
|
||||||
400: BadRequest,
|
400: BadRequest,
|
||||||
401: AuthorizationRequired,
|
401: AuthorizationRequired,
|
||||||
|
@ -45,10 +41,8 @@ class Response(dict):
|
||||||
self.rate_limit_reset_at = email.utils.parsedate(response.headers["x-featureratelimit-reset"])
|
self.rate_limit_reset_at = email.utils.parsedate(response.headers["x-featureratelimit-reset"])
|
||||||
self.rate_limit_remaining = int(response.headers["x-featureratelimit-remaining"])
|
self.rate_limit_remaining = int(response.headers["x-featureratelimit-remaining"])
|
||||||
|
|
||||||
_http = urllib3.PoolManager(
|
|
||||||
cert_reqs='CERT_REQUIRED',
|
_http = utils.get_http_connector(cloudinary.config(), cloudinary.CERT_KWARGS)
|
||||||
ca_certs=certifi.where()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def ping(**options):
|
def ping(**options):
|
||||||
|
@ -67,23 +61,26 @@ def resources(**options):
|
||||||
resource_type = options.pop("resource_type", "image")
|
resource_type = options.pop("resource_type", "image")
|
||||||
upload_type = options.pop("type", None)
|
upload_type = options.pop("type", None)
|
||||||
uri = ["resources", resource_type]
|
uri = ["resources", resource_type]
|
||||||
if upload_type: uri.append(upload_type)
|
if upload_type:
|
||||||
params = only(options,
|
uri.append(upload_type)
|
||||||
"next_cursor", "max_results", "prefix", "tags", "context", "moderations", "direction", "start_at")
|
params = only(options, "next_cursor", "max_results", "prefix", "tags",
|
||||||
|
"context", "moderations", "direction", "start_at")
|
||||||
return call_api("get", uri, params, **options)
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
def resources_by_tag(tag, **options):
|
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", "context", "moderations", "direction")
|
params = only(options, "next_cursor", "max_results", "tags",
|
||||||
|
"context", "moderations", "direction")
|
||||||
return call_api("get", uri, params, **options)
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
def resources_by_moderation(kind, status, **options):
|
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", "context", "moderations", "direction")
|
params = only(options, "next_cursor", "max_results", "tags",
|
||||||
|
"context", "moderations", "direction")
|
||||||
return call_api("get", uri, params, **options)
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,7 +96,8 @@ 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", "pages", "phash", "coordinates", "max_results")
|
params = only(options, "exif", "faces", "colors", "image_metadata", "cinemagraph_analysis",
|
||||||
|
"pages", "phash", "coordinates", "max_results", "quality_analysis", "derived_next_cursor")
|
||||||
return call_api("get", uri, params, **options)
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
@ -114,9 +112,11 @@ def update(public_id, **options):
|
||||||
if "tags" in options:
|
if "tags" in options:
|
||||||
params["tags"] = ",".join(utils.build_array(options["tags"]))
|
params["tags"] = ",".join(utils.build_array(options["tags"]))
|
||||||
if "face_coordinates" in options:
|
if "face_coordinates" in options:
|
||||||
params["face_coordinates"] = utils.encode_double_array(options.get("face_coordinates"))
|
params["face_coordinates"] = utils.encode_double_array(
|
||||||
|
options.get("face_coordinates"))
|
||||||
if "custom_coordinates" in options:
|
if "custom_coordinates" in options:
|
||||||
params["custom_coordinates"] = utils.encode_double_array(options.get("custom_coordinates"))
|
params["custom_coordinates"] = utils.encode_double_array(
|
||||||
|
options.get("custom_coordinates"))
|
||||||
if "context" in options:
|
if "context" in options:
|
||||||
params["context"] = utils.encode_context(options.get("context"))
|
params["context"] = utils.encode_context(options.get("context"))
|
||||||
if "auto_tagging" in options:
|
if "auto_tagging" in options:
|
||||||
|
@ -167,8 +167,7 @@ def delete_derived_resources(derived_resource_ids, **options):
|
||||||
def delete_derived_by_transformation(public_ids, transformations,
|
def delete_derived_by_transformation(public_ids, transformations,
|
||||||
resource_type='image', type='upload', invalidate=None,
|
resource_type='image', type='upload', invalidate=None,
|
||||||
**options):
|
**options):
|
||||||
"""
|
"""Delete derived resources of public ids, identified by transformations
|
||||||
Delete derived resources of public ids, identified by transformations
|
|
||||||
|
|
||||||
:param public_ids: the base resources
|
:param public_ids: the base resources
|
||||||
:type public_ids: list of str
|
:type public_ids: list of str
|
||||||
|
@ -202,33 +201,49 @@ def tags(**options):
|
||||||
|
|
||||||
def transformations(**options):
|
def transformations(**options):
|
||||||
uri = ["transformations"]
|
uri = ["transformations"]
|
||||||
return call_api("get", uri, only(options, "next_cursor", "max_results"), **options)
|
params = only(options, "named", "next_cursor", "max_results")
|
||||||
|
|
||||||
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
def transformation(transformation, **options):
|
def transformation(transformation, **options):
|
||||||
uri = ["transformations", transformation_string(transformation)]
|
uri = ["transformations"]
|
||||||
return call_api("get", uri, only(options, "next_cursor", "max_results"), **options)
|
|
||||||
|
params = only(options, "next_cursor", "max_results")
|
||||||
|
params["transformation"] = utils.build_single_eager(transformation)
|
||||||
|
|
||||||
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
def delete_transformation(transformation, **options):
|
def delete_transformation(transformation, **options):
|
||||||
uri = ["transformations", transformation_string(transformation)]
|
uri = ["transformations"]
|
||||||
return call_api("delete", uri, {}, **options)
|
|
||||||
|
params = {"transformation": utils.build_single_eager(transformation)}
|
||||||
|
|
||||||
|
return call_api("delete", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
# updates - currently only supported update is the "allowed_for_strict" boolean flag and unsafe_update
|
# updates - currently only supported update is the "allowed_for_strict"
|
||||||
|
# boolean flag and unsafe_update
|
||||||
def update_transformation(transformation, **options):
|
def update_transformation(transformation, **options):
|
||||||
uri = ["transformations", transformation_string(transformation)]
|
uri = ["transformations"]
|
||||||
|
|
||||||
updates = only(options, "allowed_for_strict")
|
updates = only(options, "allowed_for_strict")
|
||||||
|
|
||||||
if "unsafe_update" in options:
|
if "unsafe_update" in options:
|
||||||
updates["unsafe_update"] = transformation_string(options.get("unsafe_update"))
|
updates["unsafe_update"] = transformation_string(options.get("unsafe_update"))
|
||||||
if not updates: raise Exception("No updates given")
|
|
||||||
|
updates["transformation"] = utils.build_single_eager(transformation)
|
||||||
|
|
||||||
return call_api("put", uri, updates, **options)
|
return call_api("put", uri, updates, **options)
|
||||||
|
|
||||||
|
|
||||||
def create_transformation(name, definition, **options):
|
def create_transformation(name, definition, **options):
|
||||||
uri = ["transformations", name]
|
uri = ["transformations"]
|
||||||
return call_api("post", uri, {"transformation": transformation_string(definition)}, **options)
|
|
||||||
|
params = {"name": name, "transformation": utils.build_single_eager(definition)}
|
||||||
|
|
||||||
|
return call_api("post", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
def publish_by_ids(public_ids, **options):
|
def publish_by_ids(public_ids, **options):
|
||||||
|
@ -271,7 +286,7 @@ def update_upload_preset(name, **options):
|
||||||
uri = ["upload_presets", name]
|
uri = ["upload_presets", name]
|
||||||
params = utils.build_upload_params(**options)
|
params = utils.build_upload_params(**options)
|
||||||
params = utils.cleanup_params(params)
|
params = utils.cleanup_params(params)
|
||||||
params.update(only(options, "unsigned", "disallow_public_id"))
|
params.update(only(options, "unsigned", "disallow_public_id", "live"))
|
||||||
return call_api("put", uri, params, **options)
|
return call_api("put", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
@ -279,16 +294,33 @@ def create_upload_preset(**options):
|
||||||
uri = ["upload_presets"]
|
uri = ["upload_presets"]
|
||||||
params = utils.build_upload_params(**options)
|
params = utils.build_upload_params(**options)
|
||||||
params = utils.cleanup_params(params)
|
params = utils.cleanup_params(params)
|
||||||
params.update(only(options, "unsigned", "disallow_public_id", "name"))
|
params.update(only(options, "unsigned", "disallow_public_id", "name", "live"))
|
||||||
return call_api("post", uri, params, **options)
|
return call_api("post", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def create_folder(path, **options):
|
||||||
|
return call_api("post", ["folders", path], {}, **options)
|
||||||
|
|
||||||
|
|
||||||
def root_folders(**options):
|
def root_folders(**options):
|
||||||
return call_api("get", ["folders"], {}, **options)
|
return call_api("get", ["folders"], only(options, "next_cursor", "max_results"), **options)
|
||||||
|
|
||||||
|
|
||||||
def subfolders(of_folder_path, **options):
|
def subfolders(of_folder_path, **options):
|
||||||
return call_api("get", ["folders", of_folder_path], {}, **options)
|
return call_api("get", ["folders", of_folder_path], only(options, "next_cursor", "max_results"), **options)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_folder(path, **options):
|
||||||
|
"""Deletes folder
|
||||||
|
|
||||||
|
Deleted folder must be empty, but can have descendant empty sub folders
|
||||||
|
|
||||||
|
:param path: The folder to delete
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
return call_api("delete", ["folders", path], {}, **options)
|
||||||
|
|
||||||
|
|
||||||
def restore(public_ids, **options):
|
def restore(public_ids, **options):
|
||||||
|
@ -361,29 +393,48 @@ def update_streaming_profile(name, **options):
|
||||||
def call_json_api(method, uri, jsonBody, **options):
|
def call_json_api(method, uri, jsonBody, **options):
|
||||||
logger.debug(jsonBody)
|
logger.debug(jsonBody)
|
||||||
data = json.dumps(jsonBody).encode('utf-8')
|
data = json.dumps(jsonBody).encode('utf-8')
|
||||||
return _call_api(method, uri, body=data, headers={'Content-Type': 'application/json'}, **options)
|
return _call_api(method, uri, body=data,
|
||||||
|
headers={'Content-Type': 'application/json'}, **options)
|
||||||
|
|
||||||
|
|
||||||
def call_api(method, uri, params, **options):
|
def call_api(method, uri, params, **options):
|
||||||
return _call_api(method, uri, params=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):
|
def _call_api(method, uri, params=None, body=None, headers=None, **options):
|
||||||
prefix = options.pop("upload_prefix",
|
prefix = options.pop("upload_prefix",
|
||||||
cloudinary.config().upload_prefix) or "https://api.cloudinary.com"
|
cloudinary.config().upload_prefix) or "https://api.cloudinary.com"
|
||||||
cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name)
|
cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name)
|
||||||
if not cloud_name: raise Exception("Must supply cloud_name")
|
if not cloud_name:
|
||||||
|
raise Exception("Must supply cloud_name")
|
||||||
api_key = options.pop("api_key", cloudinary.config().api_key)
|
api_key = options.pop("api_key", cloudinary.config().api_key)
|
||||||
if not api_key: raise Exception("Must supply api_key")
|
if not api_key:
|
||||||
|
raise Exception("Must supply api_key")
|
||||||
api_secret = options.pop("api_secret", cloudinary.config().api_secret)
|
api_secret = options.pop("api_secret", cloudinary.config().api_secret)
|
||||||
if not cloud_name: raise Exception("Must supply api_secret")
|
if not cloud_name:
|
||||||
|
raise Exception("Must supply api_secret")
|
||||||
api_url = "/".join([prefix, "v1_1", cloud_name] + uri)
|
api_url = "/".join([prefix, "v1_1", cloud_name] + uri)
|
||||||
|
|
||||||
processed_params = None
|
processed_params = None
|
||||||
if isinstance(params, dict):
|
if isinstance(params, dict):
|
||||||
processed_params = {}
|
processed_params = {}
|
||||||
for key, value in params.items():
|
for key, value in params.items():
|
||||||
if isinstance(value, list):
|
if isinstance(value, list) or isinstance(value, tuple):
|
||||||
value_list = {"{}[{}]".format(key, i): i_value for i, i_value in enumerate(value)}
|
value_list = {"{}[{}]".format(key, i): i_value for i, i_value in enumerate(value)}
|
||||||
processed_params.update(value_list)
|
processed_params.update(value_list)
|
||||||
elif value:
|
elif value:
|
||||||
|
@ -437,12 +488,166 @@ def transformation_string(transformation):
|
||||||
def __prepare_streaming_profile_params(**options):
|
def __prepare_streaming_profile_params(**options):
|
||||||
params = only(options, "display_name")
|
params = only(options, "display_name")
|
||||||
if "representations" in options:
|
if "representations" in options:
|
||||||
representations = [{"transformation": transformation_string(trans)} for trans in options["representations"]]
|
representations = [{"transformation": transformation_string(trans)}
|
||||||
|
for trans in options["representations"]]
|
||||||
params["representations"] = json.dumps(representations)
|
params["representations"] = json.dumps(representations)
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
def __delete_resource_params(options, **params):
|
def __delete_resource_params(options, **params):
|
||||||
p = dict(transformations=utils.build_eager(options.get('transformations')),
|
p = dict(transformations=utils.build_eager(options.get('transformations')),
|
||||||
**only(options, "keep_original", "next_cursor", "invalidate"))
|
**only(options, "keep_original", "next_cursor", "invalidate"))
|
||||||
p.update(params)
|
p.update(params)
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def list_metadata_fields(**options):
|
||||||
|
"""Returns a list of all metadata field definitions
|
||||||
|
|
||||||
|
See: `Get metadata fields API reference <https://cloudinary.com/documentation/admin_api#get_metadata_fields>`_
|
||||||
|
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
return call_metadata_api("get", [], {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def metadata_field_by_field_id(field_external_id, **options):
|
||||||
|
"""Gets a metadata field by external id
|
||||||
|
|
||||||
|
See: `Get metadata field by external ID API reference
|
||||||
|
<https://cloudinary.com/documentation/admin_api#get_a_metadata_field_by_external_id>`_
|
||||||
|
|
||||||
|
:param field_external_id: The ID of the metadata field to retrieve
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
uri = [field_external_id]
|
||||||
|
return call_metadata_api("get", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def add_metadata_field(field, **options):
|
||||||
|
"""Creates a new metadata field definition
|
||||||
|
|
||||||
|
See: `Create metadata field API reference <https://cloudinary.com/documentation/admin_api#create_a_metadata_field>`_
|
||||||
|
|
||||||
|
:param field: The field to add
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
params = only(field, "type", "external_id", "label", "mandatory",
|
||||||
|
"default_value", "validation", "datasource")
|
||||||
|
return call_metadata_api("post", [], params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def update_metadata_field(field_external_id, field, **options):
|
||||||
|
"""Updates a metadata field by external id
|
||||||
|
|
||||||
|
Updates a metadata field definition (partially, no need to pass the entire
|
||||||
|
object) passed as JSON data.
|
||||||
|
|
||||||
|
See `Generic structure of a metadata field
|
||||||
|
<https://cloudinary.com/documentation/admin_api#generic_structure_of_a_metadata_field>`_ for details.
|
||||||
|
|
||||||
|
:param field_external_id: The id of the metadata field to update
|
||||||
|
:param field: The field definition
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
uri = [field_external_id]
|
||||||
|
params = only(field, "label", "mandatory", "default_value", "validation")
|
||||||
|
return call_metadata_api("put", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_metadata_field(field_external_id, **options):
|
||||||
|
"""Deletes a metadata field definition.
|
||||||
|
The field should no longer be considered a valid candidate for all other endpoints
|
||||||
|
|
||||||
|
See: `Delete metadata field API reference
|
||||||
|
<https://cloudinary.com/documentation/admin_api#delete_a_metadata_field_by_external_id>`_
|
||||||
|
|
||||||
|
:param field_external_id: The external id of the field to delete
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:return: An array with a "message" key. "ok" value indicates a successful deletion.
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
uri = [field_external_id]
|
||||||
|
return call_metadata_api("delete", uri, {}, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_datasource_entries(field_external_id, entries_external_id, **options):
|
||||||
|
"""Deletes entries in a metadata field datasource
|
||||||
|
|
||||||
|
Deletes (blocks) the datasource entries for a specified metadata field
|
||||||
|
definition. Sets the state of the entries to inactive. This is a soft delete,
|
||||||
|
the entries still exist under the hood and can be activated again with the
|
||||||
|
restore datasource entries method.
|
||||||
|
|
||||||
|
See: `Delete entries in a metadata field datasource API reference
|
||||||
|
<https://cloudinary.com/documentation/admin_api#delete_entries_in_a_metadata_field_datasource>`_
|
||||||
|
|
||||||
|
:param field_external_id: The id of the field to update
|
||||||
|
:param entries_external_id: The ids of all the entries to delete from the
|
||||||
|
datasource
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
uri = [field_external_id, "datasource"]
|
||||||
|
params = {"external_ids": entries_external_id}
|
||||||
|
return call_metadata_api("delete", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def update_metadata_field_datasource(field_external_id, entries_external_id, **options):
|
||||||
|
"""Updates a metadata field datasource
|
||||||
|
|
||||||
|
Updates the datasource of a supported field type (currently only enum and set),
|
||||||
|
passed as JSON data. The update is partial: datasource entries with an
|
||||||
|
existing external_id will be updated and entries with new external_id's (or
|
||||||
|
without external_id's) will be appended.
|
||||||
|
|
||||||
|
See: `Update a metadata field datasource API reference
|
||||||
|
<https://cloudinary.com/documentation/admin_api#update_a_metadata_field_datasource>`_
|
||||||
|
|
||||||
|
:param field_external_id: The external id of the field to update
|
||||||
|
:param entries_external_id:
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
values = []
|
||||||
|
for item in entries_external_id:
|
||||||
|
external = only(item, "external_id", "value")
|
||||||
|
if external:
|
||||||
|
values.append(external)
|
||||||
|
|
||||||
|
uri = [field_external_id, "datasource"]
|
||||||
|
params = {"values": values}
|
||||||
|
return call_metadata_api("put", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_metadata_field_datasource(field_external_id, entries_external_ids, **options):
|
||||||
|
"""Restores entries in a metadata field datasource
|
||||||
|
|
||||||
|
Restores (unblocks) any previously deleted datasource entries for a specified
|
||||||
|
metadata field definition.
|
||||||
|
Sets the state of the entries to active.
|
||||||
|
|
||||||
|
See: `Restore entries in a metadata field datasource API reference
|
||||||
|
<https://cloudinary.com/documentation/admin_api#restore_entries_in_a_metadata_field_datasource>`_
|
||||||
|
|
||||||
|
:param field_external_id: The ID of the metadata field
|
||||||
|
:param entries_external_ids: An array of IDs of datasource entries to restore
|
||||||
|
(unblock)
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
uri = [field_external_id, 'datasource_restore']
|
||||||
|
params = {"external_ids": entries_external_ids}
|
||||||
|
return call_metadata_api("post", uri, params, **options)
|
||||||
|
|
|
@ -3,33 +3,37 @@ import hmac
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from binascii import a2b_hex
|
from binascii import a2b_hex
|
||||||
from cloudinary.compat import quote_plus
|
|
||||||
|
|
||||||
AUTH_TOKEN_NAME = "__cld_token__"
|
AUTH_TOKEN_NAME = "__cld_token__"
|
||||||
|
AUTH_TOKEN_SEPARATOR = "~"
|
||||||
|
AUTH_TOKEN_UNSAFE_RE = r'([ "#%&\'\/:;<=>?@\[\\\]^`{\|}~]+)'
|
||||||
|
|
||||||
|
|
||||||
|
def generate(url=None, acl=None, start_time=None, duration=None,
|
||||||
def generate(url=None, acl=None, start_time=None, duration=None, expiration=None, ip=None, key=None,
|
expiration=None, ip=None, key=None, token_name=AUTH_TOKEN_NAME):
|
||||||
token_name=AUTH_TOKEN_NAME):
|
|
||||||
|
|
||||||
if expiration is None:
|
if expiration is None:
|
||||||
if duration is not None:
|
if duration is not None:
|
||||||
start = start_time if start_time is not None else int(time.mktime(time.gmtime()))
|
start = start_time if start_time is not None else int(time.time())
|
||||||
expiration = start + duration
|
expiration = start + duration
|
||||||
else:
|
else:
|
||||||
raise Exception("Must provide either expiration or duration")
|
raise Exception("Must provide either expiration or duration")
|
||||||
|
|
||||||
token_parts = []
|
token_parts = []
|
||||||
if ip is not None: token_parts.append("ip=" + ip)
|
if ip is not None:
|
||||||
if start_time is not None: token_parts.append("st=%d" % start_time)
|
token_parts.append("ip=" + ip)
|
||||||
|
if start_time is not None:
|
||||||
|
token_parts.append("st=%d" % start_time)
|
||||||
token_parts.append("exp=%d" % expiration)
|
token_parts.append("exp=%d" % expiration)
|
||||||
if acl is not None: token_parts.append("acl=%s" % _escape_to_lower(acl))
|
if acl is not None:
|
||||||
|
token_parts.append("acl=%s" % _escape_to_lower(acl))
|
||||||
to_sign = list(token_parts)
|
to_sign = list(token_parts)
|
||||||
if url is not None:
|
if url is not None and acl is None:
|
||||||
to_sign.append("url=%s" % _escape_to_lower(url))
|
to_sign.append("url=%s" % _escape_to_lower(url))
|
||||||
auth = _digest("~".join(to_sign), key)
|
auth = _digest(AUTH_TOKEN_SEPARATOR.join(to_sign), key)
|
||||||
token_parts.append("hmac=%s" % auth)
|
token_parts.append("hmac=%s" % auth)
|
||||||
return "%(token_name)s=%(token)s" % {"token_name": token_name, "token": "~".join(token_parts)}
|
return "%(token_name)s=%(token)s" % {"token_name": token_name, "token": AUTH_TOKEN_SEPARATOR.join(token_parts)}
|
||||||
|
|
||||||
|
|
||||||
def _digest(message, key):
|
def _digest(message, key):
|
||||||
|
@ -38,10 +42,8 @@ def _digest(message, key):
|
||||||
|
|
||||||
|
|
||||||
def _escape_to_lower(url):
|
def _escape_to_lower(url):
|
||||||
escaped_url = quote_plus(url)
|
# There is a circular import issue in this file, need to resolve it in the next major release
|
||||||
|
from cloudinary.utils import smart_escape
|
||||||
def toLowercase(match):
|
escaped_url = smart_escape(url, unsafe=AUTH_TOKEN_UNSAFE_RE)
|
||||||
return match.group(0).lower()
|
escaped_url = re.sub(r"%[0-9A-F]{2}", lambda x: x.group(0).lower(), escaped_url)
|
||||||
|
|
||||||
escaped_url = re.sub(r'%..', toLowercase, escaped_url)
|
|
||||||
return escaped_url
|
return escaped_url
|
||||||
|
|
0
lib/cloudinary/cache/__init__.py
vendored
Normal file
0
lib/cloudinary/cache/__init__.py
vendored
Normal file
0
lib/cloudinary/cache/adapter/__init__.py
vendored
Normal file
0
lib/cloudinary/cache/adapter/__init__.py
vendored
Normal file
63
lib/cloudinary/cache/adapter/cache_adapter.py
vendored
Normal file
63
lib/cloudinary/cache/adapter/cache_adapter.py
vendored
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class CacheAdapter:
|
||||||
|
"""
|
||||||
|
CacheAdapter Abstract Base Class
|
||||||
|
"""
|
||||||
|
__metaclass__ = ABCMeta
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get(self, public_id, type, resource_type, transformation, format):
|
||||||
|
"""
|
||||||
|
Gets value specified by parameters
|
||||||
|
|
||||||
|
:param public_id: The public ID of the resource
|
||||||
|
:param type: The storage type
|
||||||
|
:param resource_type: The type of the resource
|
||||||
|
:param transformation: The transformation string
|
||||||
|
:param format: The format of the resource
|
||||||
|
|
||||||
|
:return: None|mixed value, None if not found
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set(self, public_id, type, resource_type, transformation, format, value):
|
||||||
|
"""
|
||||||
|
Sets value specified by parameters
|
||||||
|
|
||||||
|
:param public_id: The public ID of the resource
|
||||||
|
:param type: The storage type
|
||||||
|
:param resource_type: The type of the resource
|
||||||
|
:param transformation: The transformation string
|
||||||
|
:param format: The format of the resource
|
||||||
|
:param value: The value to set
|
||||||
|
|
||||||
|
:return: bool True on success or False on failure
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete(self, public_id, type, resource_type, transformation, format):
|
||||||
|
"""
|
||||||
|
Deletes entry specified by parameters
|
||||||
|
|
||||||
|
:param public_id: The public ID of the resource
|
||||||
|
:param type: The storage type
|
||||||
|
:param resource_type: The type of the resource
|
||||||
|
:param transformation: The transformation string
|
||||||
|
:param format: The format of the resource
|
||||||
|
|
||||||
|
:return: bool True on success or False on failure
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def flush_all(self):
|
||||||
|
"""
|
||||||
|
Flushes all entries from cache
|
||||||
|
|
||||||
|
:return: bool True on success or False on failure
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
61
lib/cloudinary/cache/adapter/key_value_cache_adapter.py
vendored
Normal file
61
lib/cloudinary/cache/adapter/key_value_cache_adapter.py
vendored
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import json
|
||||||
|
from hashlib import sha1
|
||||||
|
|
||||||
|
from cloudinary.cache.adapter.cache_adapter import CacheAdapter
|
||||||
|
from cloudinary.cache.storage.key_value_storage import KeyValueStorage
|
||||||
|
from cloudinary.utils import check_property_enabled
|
||||||
|
|
||||||
|
|
||||||
|
class KeyValueCacheAdapter(CacheAdapter):
|
||||||
|
"""
|
||||||
|
A cache adapter for a key-value storage type
|
||||||
|
"""
|
||||||
|
def __init__(self, storage):
|
||||||
|
"""Create a new adapter for the provided storage interface"""
|
||||||
|
if not isinstance(storage, KeyValueStorage):
|
||||||
|
raise ValueError("An instance of valid KeyValueStorage must be provided")
|
||||||
|
|
||||||
|
self._key_value_storage = storage
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self):
|
||||||
|
return self._key_value_storage is not None
|
||||||
|
|
||||||
|
@check_property_enabled
|
||||||
|
def get(self, public_id, type, resource_type, transformation, format):
|
||||||
|
key = self.generate_cache_key(public_id, type, resource_type, transformation, format)
|
||||||
|
value_str = self._key_value_storage.get(key)
|
||||||
|
return json.loads(value_str) if value_str else value_str
|
||||||
|
|
||||||
|
@check_property_enabled
|
||||||
|
def set(self, public_id, type, resource_type, transformation, format, value):
|
||||||
|
key = self.generate_cache_key(public_id, type, resource_type, transformation, format)
|
||||||
|
return self._key_value_storage.set(key, json.dumps(value))
|
||||||
|
|
||||||
|
@check_property_enabled
|
||||||
|
def delete(self, public_id, type, resource_type, transformation, format):
|
||||||
|
return self._key_value_storage.delete(
|
||||||
|
self.generate_cache_key(public_id, type, resource_type, transformation, format)
|
||||||
|
)
|
||||||
|
|
||||||
|
@check_property_enabled
|
||||||
|
def flush_all(self):
|
||||||
|
return self._key_value_storage.clear()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_cache_key(public_id, type, resource_type, transformation, format):
|
||||||
|
"""
|
||||||
|
Generates key-value storage key from parameters
|
||||||
|
|
||||||
|
:param public_id: The public ID of the resource
|
||||||
|
:param type: The storage type
|
||||||
|
:param resource_type: The type of the resource
|
||||||
|
:param transformation: The transformation string
|
||||||
|
:param format: The format of the resource
|
||||||
|
|
||||||
|
:return: Resulting cache key
|
||||||
|
"""
|
||||||
|
|
||||||
|
valid_params = [p for p in [public_id, type, resource_type, transformation, format] if p]
|
||||||
|
|
||||||
|
return sha1("/".join(valid_params).encode("utf-8")).hexdigest()
|
124
lib/cloudinary/cache/responsive_breakpoints_cache.py
vendored
Normal file
124
lib/cloudinary/cache/responsive_breakpoints_cache.py
vendored
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import copy
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
import cloudinary
|
||||||
|
from cloudinary.cache.adapter.cache_adapter import CacheAdapter
|
||||||
|
from cloudinary.utils import check_property_enabled
|
||||||
|
|
||||||
|
|
||||||
|
class ResponsiveBreakpointsCache:
|
||||||
|
"""
|
||||||
|
Caches breakpoint values for image resources
|
||||||
|
"""
|
||||||
|
def __init__(self, **cache_options):
|
||||||
|
"""
|
||||||
|
Initialize the cache
|
||||||
|
|
||||||
|
:param cache_options: Cache configuration options
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._cache_adapter = None
|
||||||
|
|
||||||
|
cache_adapter = cache_options.get("cache_adapter")
|
||||||
|
|
||||||
|
self.set_cache_adapter(cache_adapter)
|
||||||
|
|
||||||
|
def set_cache_adapter(self, cache_adapter):
|
||||||
|
"""
|
||||||
|
Assigns cache adapter
|
||||||
|
|
||||||
|
:param cache_adapter: The cache adapter used to store and retrieve values
|
||||||
|
|
||||||
|
:return: Returns True if the cache_adapter is valid
|
||||||
|
"""
|
||||||
|
if cache_adapter is None or not isinstance(cache_adapter, CacheAdapter):
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._cache_adapter = cache_adapter
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self):
|
||||||
|
"""
|
||||||
|
Indicates whether cache is enabled or not
|
||||||
|
|
||||||
|
:return: Rrue if a _cache_adapter has been set
|
||||||
|
"""
|
||||||
|
return self._cache_adapter is not None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _options_to_parameters(**options):
|
||||||
|
"""
|
||||||
|
Extract the parameters required in order to calculate the key of the cache.
|
||||||
|
|
||||||
|
:param options: Input options
|
||||||
|
|
||||||
|
:return: A list of values used to calculate the cache key
|
||||||
|
"""
|
||||||
|
options_copy = copy.deepcopy(options)
|
||||||
|
transformation, _ = cloudinary.utils.generate_transformation_string(**options_copy)
|
||||||
|
file_format = options.get("format", "")
|
||||||
|
storage_type = options.get("type", "upload")
|
||||||
|
resource_type = options.get("resource_type", "image")
|
||||||
|
|
||||||
|
return storage_type, resource_type, transformation, file_format
|
||||||
|
|
||||||
|
@check_property_enabled
|
||||||
|
def get(self, public_id, **options):
|
||||||
|
"""
|
||||||
|
Retrieve the breakpoints of a particular derived resource identified by the public_id and options
|
||||||
|
|
||||||
|
:param public_id: The public ID of the resource
|
||||||
|
:param options: The public ID of the resource
|
||||||
|
|
||||||
|
:return: Array of responsive breakpoints, None if not found
|
||||||
|
"""
|
||||||
|
params = self._options_to_parameters(**options)
|
||||||
|
|
||||||
|
return self._cache_adapter.get(public_id, *params)
|
||||||
|
|
||||||
|
@check_property_enabled
|
||||||
|
def set(self, public_id, value, **options):
|
||||||
|
"""
|
||||||
|
Set responsive breakpoints identified by public ID and options
|
||||||
|
|
||||||
|
:param public_id: The public ID of the resource
|
||||||
|
:param value: Array of responsive breakpoints to set
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:return: True on success or False on failure
|
||||||
|
"""
|
||||||
|
if not (isinstance(value, (list, tuple))):
|
||||||
|
raise ValueError("A list of breakpoints is expected")
|
||||||
|
|
||||||
|
storage_type, resource_type, transformation, file_format = self._options_to_parameters(**options)
|
||||||
|
|
||||||
|
return self._cache_adapter.set(public_id, storage_type, resource_type, transformation, file_format, value)
|
||||||
|
|
||||||
|
@check_property_enabled
|
||||||
|
def delete(self, public_id, **options):
|
||||||
|
"""
|
||||||
|
Delete responsive breakpoints identified by public ID and options
|
||||||
|
|
||||||
|
:param public_id: The public ID of the resource
|
||||||
|
:param options: Additional options
|
||||||
|
|
||||||
|
:return: True on success or False on failure
|
||||||
|
"""
|
||||||
|
params = self._options_to_parameters(**options)
|
||||||
|
|
||||||
|
return self._cache_adapter.delete(public_id, *params)
|
||||||
|
|
||||||
|
@check_property_enabled
|
||||||
|
def flush_all(self):
|
||||||
|
"""
|
||||||
|
Flush all entries from cache
|
||||||
|
|
||||||
|
:return: True on success or False on failure
|
||||||
|
"""
|
||||||
|
return self._cache_adapter.flush_all()
|
||||||
|
|
||||||
|
|
||||||
|
instance = ResponsiveBreakpointsCache()
|
0
lib/cloudinary/cache/storage/__init__.py
vendored
Normal file
0
lib/cloudinary/cache/storage/__init__.py
vendored
Normal file
79
lib/cloudinary/cache/storage/file_system_key_value_storage.py
vendored
Normal file
79
lib/cloudinary/cache/storage/file_system_key_value_storage.py
vendored
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import glob
|
||||||
|
from tempfile import gettempdir
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import errno
|
||||||
|
|
||||||
|
from cloudinary.cache.storage.key_value_storage import KeyValueStorage
|
||||||
|
|
||||||
|
|
||||||
|
class FileSystemKeyValueStorage(KeyValueStorage):
|
||||||
|
"""File-based key-value storage"""
|
||||||
|
_item_ext = ".cldci"
|
||||||
|
|
||||||
|
def __init__(self, root_path):
|
||||||
|
"""
|
||||||
|
Create a new Storage object.
|
||||||
|
|
||||||
|
All files will be stored under the root_path location
|
||||||
|
|
||||||
|
:param root_path: The base folder for all storage files
|
||||||
|
"""
|
||||||
|
if root_path is None:
|
||||||
|
root_path = gettempdir()
|
||||||
|
|
||||||
|
if not os.path.isdir(root_path):
|
||||||
|
os.makedirs(root_path)
|
||||||
|
|
||||||
|
self._root_path = root_path
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
if not self._exists(key):
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(self._get_key_full_path(key), 'r') as f:
|
||||||
|
value = f.read()
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
with open(self._get_key_full_path(key), 'w') as f:
|
||||||
|
f.write(value)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def delete(self, key):
|
||||||
|
try:
|
||||||
|
os.remove(self._get_key_full_path(key))
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.ENOENT: # errno.ENOENT - no such file or directory
|
||||||
|
raise # re-raise exception if a different error occurred
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
for cache_item_path in glob.iglob(os.path.join(self._root_path, '*' + self._item_ext)):
|
||||||
|
os.remove(cache_item_path)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _get_key_full_path(self, key):
|
||||||
|
"""
|
||||||
|
Generate the file path for the key
|
||||||
|
|
||||||
|
:param key: The key
|
||||||
|
|
||||||
|
:return: The absolute path of the value file associated with the key
|
||||||
|
"""
|
||||||
|
return os.path.join(self._root_path, key + self._item_ext)
|
||||||
|
|
||||||
|
def _exists(self, key):
|
||||||
|
"""
|
||||||
|
Indicate whether key exists
|
||||||
|
|
||||||
|
:param key: The key
|
||||||
|
|
||||||
|
:return: bool True if the file for the given key exists
|
||||||
|
"""
|
||||||
|
return os.path.isfile(self._get_key_full_path(key))
|
51
lib/cloudinary/cache/storage/key_value_storage.py
vendored
Normal file
51
lib/cloudinary/cache/storage/key_value_storage.py
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class KeyValueStorage:
|
||||||
|
"""
|
||||||
|
A simple key-value storage abstract base class
|
||||||
|
"""
|
||||||
|
__metaclass__ = ABCMeta
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get(self, key):
|
||||||
|
"""
|
||||||
|
Get a value identified by the given key
|
||||||
|
|
||||||
|
:param key: The unique identifier
|
||||||
|
|
||||||
|
:return: The value identified by key or None if no value was found
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set(self, key, value):
|
||||||
|
"""
|
||||||
|
Store the value identified by the key
|
||||||
|
|
||||||
|
:param key: The unique identifier
|
||||||
|
:param value: Value to store
|
||||||
|
|
||||||
|
:return: bool True on success or False on failure
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete(self, key):
|
||||||
|
"""
|
||||||
|
Deletes item by key
|
||||||
|
|
||||||
|
:param key: The unique identifier
|
||||||
|
|
||||||
|
:return: bool True on success or False on failure
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
Clears all entries
|
||||||
|
|
||||||
|
:return: bool True on success or False on failure
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
|
@ -1,5 +1,7 @@
|
||||||
# Copyright Cloudinary
|
# Copyright Cloudinary
|
||||||
import six.moves.urllib.parse
|
import six.moves.urllib.parse
|
||||||
|
from six import PY3, string_types, StringIO, BytesIO
|
||||||
|
|
||||||
urlencode = six.moves.urllib.parse.urlencode
|
urlencode = six.moves.urllib.parse.urlencode
|
||||||
unquote = six.moves.urllib.parse.unquote
|
unquote = six.moves.urllib.parse.unquote
|
||||||
urlparse = six.moves.urllib.parse.urlparse
|
urlparse = six.moves.urllib.parse.urlparse
|
||||||
|
@ -7,7 +9,6 @@ parse_qs = six.moves.urllib.parse.parse_qs
|
||||||
parse_qsl = six.moves.urllib.parse.parse_qsl
|
parse_qsl = six.moves.urllib.parse.parse_qsl
|
||||||
quote_plus = six.moves.urllib.parse.quote_plus
|
quote_plus = six.moves.urllib.parse.quote_plus
|
||||||
httplib = six.moves.http_client
|
httplib = six.moves.http_client
|
||||||
from six import PY3, string_types, StringIO, BytesIO
|
|
||||||
urllib2 = six.moves.urllib.request
|
urllib2 = six.moves.urllib.request
|
||||||
NotConnected = six.moves.http_client.NotConnected
|
NotConnected = six.moves.http_client.NotConnected
|
||||||
|
|
||||||
|
|
33
lib/cloudinary/exceptions.py
Normal file
33
lib/cloudinary/exceptions.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Copyright Cloudinary
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotAllowed(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AlreadyExists(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimited(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequest(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GeneralError(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationRequired(Error):
|
||||||
|
pass
|
|
@ -1,9 +1,10 @@
|
||||||
from django import forms
|
import json
|
||||||
from cloudinary import CloudinaryResource
|
import re
|
||||||
|
|
||||||
import cloudinary.uploader
|
import cloudinary.uploader
|
||||||
import cloudinary.utils
|
import cloudinary.utils
|
||||||
import re
|
from cloudinary import CloudinaryResource
|
||||||
import json
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,8 +17,8 @@ def cl_init_js_callbacks(form, request):
|
||||||
class CloudinaryInput(forms.TextInput):
|
class CloudinaryInput(forms.TextInput):
|
||||||
input_type = 'file'
|
input_type = 'file'
|
||||||
|
|
||||||
def render(self, name, value, attrs=None):
|
def render(self, name, value, attrs=None, renderer=None):
|
||||||
attrs = self.build_attrs(attrs)
|
attrs = dict(self.attrs, **attrs)
|
||||||
options = attrs.get('options', {})
|
options = attrs.get('options', {})
|
||||||
attrs["options"] = ''
|
attrs["options"] = ''
|
||||||
|
|
||||||
|
@ -27,14 +28,16 @@ class CloudinaryInput(forms.TextInput):
|
||||||
else:
|
else:
|
||||||
params = cloudinary.utils.sign_request(params, options)
|
params = cloudinary.utils.sign_request(params, options)
|
||||||
|
|
||||||
if 'resource_type' not in options: options['resource_type'] = 'auto'
|
if 'resource_type' not in options:
|
||||||
|
options['resource_type'] = 'auto'
|
||||||
cloudinary_upload_url = cloudinary.utils.cloudinary_api_url("upload", **options)
|
cloudinary_upload_url = cloudinary.utils.cloudinary_api_url("upload", **options)
|
||||||
|
|
||||||
attrs["data-url"] = cloudinary_upload_url
|
attrs["data-url"] = cloudinary_upload_url
|
||||||
attrs["data-form-data"] = json.dumps(params)
|
attrs["data-form-data"] = json.dumps(params)
|
||||||
attrs["data-cloudinary-field"] = name
|
attrs["data-cloudinary-field"] = name
|
||||||
chunk_size = options.get("chunk_size", None)
|
chunk_size = options.get("chunk_size", None)
|
||||||
if chunk_size: attrs["data-max-chunk-size"] = chunk_size
|
if chunk_size:
|
||||||
|
attrs["data-max-chunk-size"] = chunk_size
|
||||||
attrs["class"] = " ".join(["cloudinary-fileupload", attrs.get("class", "")])
|
attrs["class"] = " ".join(["cloudinary-fileupload", attrs.get("class", "")])
|
||||||
|
|
||||||
widget = super(CloudinaryInput, self).render("file", None, attrs=attrs)
|
widget = super(CloudinaryInput, self).render("file", None, attrs=attrs)
|
||||||
|
@ -53,8 +56,10 @@ class CloudinaryJsFileField(forms.Field):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, attrs=None, options=None, autosave=True, *args, **kwargs):
|
def __init__(self, attrs=None, options=None, autosave=True, *args, **kwargs):
|
||||||
if attrs is None: attrs = {}
|
if attrs is None:
|
||||||
if options is None: options = {}
|
attrs = {}
|
||||||
|
if options is None:
|
||||||
|
options = {}
|
||||||
self.autosave = autosave
|
self.autosave = autosave
|
||||||
attrs = attrs.copy()
|
attrs = attrs.copy()
|
||||||
attrs["options"] = options.copy()
|
attrs["options"] = options.copy()
|
||||||
|
@ -70,7 +75,8 @@ class CloudinaryJsFileField(forms.Field):
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
"""Convert to CloudinaryResource"""
|
"""Convert to CloudinaryResource"""
|
||||||
if not value: return None
|
if not value:
|
||||||
|
return None
|
||||||
m = re.search(r'^([^/]+)/([^/]+)/v(\d+)/([^#]+)#([^/]+)$', value)
|
m = re.search(r'^([^/]+)/([^/]+)/v(\d+)/([^#]+)#([^/]+)$', value)
|
||||||
if not m:
|
if not m:
|
||||||
raise forms.ValidationError("Invalid format")
|
raise forms.ValidationError("Invalid format")
|
||||||
|
@ -95,7 +101,8 @@ class CloudinaryJsFileField(forms.Field):
|
||||||
"""Validate the signature"""
|
"""Validate the signature"""
|
||||||
# Use the parent's handling of required fields, etc.
|
# Use the parent's handling of required fields, etc.
|
||||||
super(CloudinaryJsFileField, self).validate(value)
|
super(CloudinaryJsFileField, self).validate(value)
|
||||||
if not value: return
|
if not value:
|
||||||
|
return
|
||||||
if not value.validate():
|
if not value.validate():
|
||||||
raise forms.ValidationError("Signature mismatch")
|
raise forms.ValidationError("Signature mismatch")
|
||||||
|
|
||||||
|
@ -108,7 +115,8 @@ class CloudinaryUnsignedJsFileField(CloudinaryJsFileField):
|
||||||
options = {}
|
options = {}
|
||||||
options = options.copy()
|
options = options.copy()
|
||||||
options.update({"unsigned": True, "upload_preset": upload_preset})
|
options.update({"unsigned": True, "upload_preset": upload_preset})
|
||||||
super(CloudinaryUnsignedJsFileField, self).__init__(attrs, options, autosave, *args, **kwargs)
|
super(CloudinaryUnsignedJsFileField, self).__init__(
|
||||||
|
attrs, options, autosave, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CloudinaryFileField(forms.FileField):
|
class CloudinaryFileField(forms.FileField):
|
||||||
|
@ -117,7 +125,7 @@ class CloudinaryFileField(forms.FileField):
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|
||||||
def __init__(self, options=None, autosave=True, *args, **kwargs):
|
def __init__(self, options=None, autosave=True, *args, **kwargs):
|
||||||
self.autosave = autosave
|
self.autosave = autosave
|
||||||
self.options = options or {}
|
self.options = options or {}
|
||||||
|
|
43
lib/cloudinary/http_client.py
Normal file
43
lib/cloudinary/http_client.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import certifi
|
||||||
|
from urllib3 import PoolManager
|
||||||
|
from urllib3.exceptions import HTTPError
|
||||||
|
|
||||||
|
from cloudinary.exceptions import GeneralError
|
||||||
|
|
||||||
|
|
||||||
|
class HttpClient:
|
||||||
|
DEFAULT_HTTP_TIMEOUT = 60
|
||||||
|
|
||||||
|
def __init__(self, **options):
|
||||||
|
# Lazy initialization of the client, to improve performance when HttpClient is initialized but not used
|
||||||
|
self._http_client_instance = None
|
||||||
|
self.timeout = options.get("timeout", self.DEFAULT_HTTP_TIMEOUT)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _http_client(self):
|
||||||
|
if self._http_client_instance is None:
|
||||||
|
self._http_client_instance = PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
|
||||||
|
return self._http_client_instance
|
||||||
|
|
||||||
|
def get_json(self, url):
|
||||||
|
try:
|
||||||
|
response = self._http_client.request("GET", url, timeout=self.timeout)
|
||||||
|
body = response.data
|
||||||
|
except HTTPError as e:
|
||||||
|
raise GeneralError("Unexpected error %s" % str(e))
|
||||||
|
except socket.error as e:
|
||||||
|
raise GeneralError("Socket Error: %s" % str(e))
|
||||||
|
|
||||||
|
if response.status != 200:
|
||||||
|
raise GeneralError("Server returned unexpected status code - {} - {}".format(response.status,
|
||||||
|
response.data))
|
||||||
|
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))
|
||||||
|
|
||||||
|
return result
|
|
@ -1,10 +1,10 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
from cloudinary import CloudinaryResource, forms, uploader
|
from cloudinary import CloudinaryResource, forms, uploader
|
||||||
|
|
||||||
from django.core.files.uploadedfile import UploadedFile
|
from django.core.files.uploadedfile import UploadedFile
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from cloudinary.uploader import upload_options
|
||||||
|
from cloudinary.utils import upload_params
|
||||||
|
|
||||||
# Add introspection rules for South, if it's installed.
|
# Add introspection rules for South, if it's installed.
|
||||||
try:
|
try:
|
||||||
|
@ -13,15 +13,23 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
CLOUDINARY_FIELD_DB_RE = r'(?:(?P<resource_type>image|raw|video)/(?P<type>upload|private|authenticated)/)?(?:v(?P<version>\d+)/)?(?P<public_id>.*?)(\.(?P<format>[^.]+))?$'
|
CLOUDINARY_FIELD_DB_RE = r'(?:(?P<resource_type>image|raw|video)/' \
|
||||||
|
r'(?P<type>upload|private|authenticated)/)?' \
|
||||||
|
r'(?:v(?P<version>\d+)/)?' \
|
||||||
|
r'(?P<public_id>.*?)' \
|
||||||
|
r'(\.(?P<format>[^.]+))?$'
|
||||||
|
|
||||||
|
|
||||||
# Taken from six - https://pythonhosted.org/six/
|
|
||||||
def with_metaclass(meta, *bases):
|
def with_metaclass(meta, *bases):
|
||||||
"""Create a base class with a metaclass."""
|
"""
|
||||||
# This requires a bit of explanation: the basic idea is to make a dummy
|
Create a base class with a metaclass.
|
||||||
# metaclass for one level of class instantiation that replaces itself with
|
|
||||||
# the actual metaclass.
|
This requires a bit of explanation: the basic idea is to make a dummy
|
||||||
|
metaclass for one level of class instantiation that replaces itself with
|
||||||
|
the actual metaclass.
|
||||||
|
|
||||||
|
Taken from six - https://pythonhosted.org/six/
|
||||||
|
"""
|
||||||
class metaclass(meta):
|
class metaclass(meta):
|
||||||
def __new__(cls, name, this_bases, d):
|
def __new__(cls, name, this_bases, d):
|
||||||
return meta(name, bases, d)
|
return meta(name, bases, d)
|
||||||
|
@ -32,23 +40,32 @@ class CloudinaryField(models.Field):
|
||||||
description = "A resource stored in Cloudinary"
|
description = "A resource stored in Cloudinary"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
options = {'max_length': 255}
|
|
||||||
self.default_form_class = kwargs.pop("default_form_class", forms.CloudinaryFileField)
|
self.default_form_class = kwargs.pop("default_form_class", forms.CloudinaryFileField)
|
||||||
options.update(kwargs)
|
self.type = kwargs.pop("type", "upload")
|
||||||
self.type = options.pop("type", "upload")
|
self.resource_type = kwargs.pop("resource_type", "image")
|
||||||
self.resource_type = options.pop("resource_type", "image")
|
self.width_field = kwargs.pop("width_field", None)
|
||||||
self.width_field = options.pop("width_field", None)
|
self.height_field = kwargs.pop("height_field", None)
|
||||||
self.height_field = options.pop("height_field", None)
|
# Collect all options related to Cloudinary upload
|
||||||
super(CloudinaryField, self).__init__(*args, **options)
|
self.options = {key: kwargs.pop(key) for key in set(kwargs.keys()) if key in upload_params + upload_options}
|
||||||
|
|
||||||
|
field_options = kwargs
|
||||||
|
field_options['max_length'] = 255
|
||||||
|
super(CloudinaryField, self).__init__(*args, **field_options)
|
||||||
|
|
||||||
def get_internal_type(self):
|
def get_internal_type(self):
|
||||||
return 'CharField'
|
return 'CharField'
|
||||||
|
|
||||||
def value_to_string(self, obj):
|
def value_to_string(self, obj):
|
||||||
# We need to support both legacy `_get_val_from_obj` and new `value_from_object` models.Field methods.
|
"""
|
||||||
# It would be better to wrap it with try -> except AttributeError -> fallback to legacy.
|
We need to support both legacy `_get_val_from_obj` and new `value_from_object` models.Field methods.
|
||||||
# Unfortunately, we can catch AttributeError exception from `value_from_object` function itself.
|
It would be better to wrap it with try -> except AttributeError -> fallback to legacy.
|
||||||
# Parsing exception string is an overkill here, that's why we check for attribute existence
|
Unfortunately, we can catch AttributeError exception from `value_from_object` function itself.
|
||||||
|
Parsing exception string is an overkill here, that's why we check for attribute existence
|
||||||
|
|
||||||
|
:param obj: Value to serialize
|
||||||
|
|
||||||
|
:return: Serialized value
|
||||||
|
"""
|
||||||
|
|
||||||
if hasattr(self, 'value_from_object'):
|
if hasattr(self, 'value_from_object'):
|
||||||
value = self.value_from_object(obj)
|
value = self.value_from_object(obj)
|
||||||
|
@ -69,38 +86,33 @@ class CloudinaryField(models.Field):
|
||||||
format=m.group('format')
|
format=m.group('format')
|
||||||
)
|
)
|
||||||
|
|
||||||
def from_db_value(self, value, expression, connection, context):
|
def from_db_value(self, value, expression, connection, *args, **kwargs):
|
||||||
if value is None:
|
# TODO: when dropping support for versions prior to 2.0, you may return
|
||||||
return value
|
# the signature to from_db_value(value, expression, connection)
|
||||||
return self.parse_cloudinary_resource(value)
|
if value is not None:
|
||||||
|
return self.parse_cloudinary_resource(value)
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if isinstance(value, CloudinaryResource):
|
if isinstance(value, CloudinaryResource):
|
||||||
return value
|
return value
|
||||||
elif isinstance(value, UploadedFile):
|
elif isinstance(value, UploadedFile):
|
||||||
return value
|
return value
|
||||||
elif value is None:
|
elif value is None or value is False:
|
||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
return self.parse_cloudinary_resource(value)
|
return self.parse_cloudinary_resource(value)
|
||||||
|
|
||||||
def upload_options_with_filename(self, model_instance, filename):
|
|
||||||
return self.upload_options(model_instance)
|
|
||||||
|
|
||||||
def upload_options(self, model_instance):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def pre_save(self, model_instance, add):
|
def pre_save(self, model_instance, add):
|
||||||
value = super(CloudinaryField, self).pre_save(model_instance, add)
|
value = super(CloudinaryField, self).pre_save(model_instance, add)
|
||||||
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.upload_options_with_filename(model_instance, value.name))
|
options.update(self.options)
|
||||||
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:
|
||||||
setattr(model_instance, self.width_field, instance_value.metadata['width'])
|
setattr(model_instance, self.width_field, instance_value.metadata.get('width'))
|
||||||
if self.height_field:
|
if self.height_field:
|
||||||
setattr(model_instance, self.height_field, instance_value.metadata['height'])
|
setattr(model_instance, self.height_field, instance_value.metadata.get('height'))
|
||||||
return self.get_prep_value(instance_value)
|
return self.get_prep_value(instance_value)
|
||||||
else:
|
else:
|
||||||
return value
|
return value
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
# MIT licensed code copied from https://bitbucket.org/chrisatlee/poster
|
# MIT licensed code copied from https://bitbucket.org/chrisatlee/poster
|
||||||
#
|
#
|
||||||
# Copyright (c) 2011 Chris AtLee
|
# Copyright (c) 2011 Chris AtLee
|
||||||
#
|
#
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
# in the Software without restriction, including without limitation the rights
|
# in the Software without restriction, including without limitation the rights
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
# furnished to do so, subject to the following conditions:
|
# furnished to do so, subject to the following conditions:
|
||||||
#
|
#
|
||||||
# The above copyright notice and this permission notice shall be included in
|
# The above copyright notice and this permission notice shall be included in
|
||||||
# all copies or substantial portions of the Software.
|
# all copies or substantial portions of the Software.
|
||||||
#
|
#
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
@ -28,7 +28,4 @@ New releases of poster will always have a version number that compares greater
|
||||||
than an older version of poster.
|
than an older version of poster.
|
||||||
New in version 0.6."""
|
New in version 0.6."""
|
||||||
|
|
||||||
import cloudinary.poster.streaminghttp
|
version = (0, 8, 2) # Thanks JP!
|
||||||
import cloudinary.poster.encode
|
|
||||||
|
|
||||||
version = (0, 8, 2) # Thanks JP!
|
|
||||||
|
|
|
@ -6,9 +6,17 @@ as multipart/form-data suitable for a HTTP POST or PUT request.
|
||||||
|
|
||||||
multipart/form-data is the standard way to upload files over HTTP"""
|
multipart/form-data is the standard way to upload files over HTTP"""
|
||||||
|
|
||||||
__all__ = ['gen_boundary', 'encode_and_quote', 'MultipartParam',
|
import mimetypes
|
||||||
'encode_string', 'encode_file_header', 'get_body_size', 'get_headers',
|
import os
|
||||||
'multipart_encode']
|
import re
|
||||||
|
from email.header import Header
|
||||||
|
|
||||||
|
from cloudinary.compat import (PY3, advance_iterator, quote_plus, to_bytearray,
|
||||||
|
to_bytes, to_string)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'gen_boundary', 'encode_and_quote', 'MultipartParam', 'encode_string',
|
||||||
|
'encode_file_header', 'get_body_size', 'get_headers', 'multipart_encode']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from io import UnsupportedOperation
|
from io import UnsupportedOperation
|
||||||
|
@ -17,25 +25,19 @@ except ImportError:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
def gen_boundary():
|
def gen_boundary():
|
||||||
"""Returns a random string to use as the boundary for a message"""
|
"""Returns a random string to use as the boundary for a message"""
|
||||||
return uuid.uuid4().hex
|
return uuid.uuid4().hex
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import random, sha
|
import random
|
||||||
|
import sha
|
||||||
|
|
||||||
def gen_boundary():
|
def gen_boundary():
|
||||||
"""Returns a random string to use as the boundary for a message"""
|
"""Returns a random string to use as the boundary for a message"""
|
||||||
bits = random.getrandbits(160)
|
bits = random.getrandbits(160)
|
||||||
return sha.new(str(bits)).hexdigest()
|
return sha.new(str(bits)).hexdigest()
|
||||||
|
|
||||||
import re, os, mimetypes
|
|
||||||
from cloudinary.compat import (PY3, string_types, to_bytes, to_string,
|
|
||||||
to_bytearray, quote_plus, advance_iterator)
|
|
||||||
try:
|
|
||||||
from email.header import Header
|
|
||||||
except ImportError:
|
|
||||||
# Python 2.4
|
|
||||||
from email.Header import Header
|
|
||||||
|
|
||||||
if PY3:
|
if PY3:
|
||||||
def encode_and_quote(data):
|
def encode_and_quote(data):
|
||||||
if data is None:
|
if data is None:
|
||||||
|
@ -47,7 +49,7 @@ else:
|
||||||
"""If ``data`` is unicode, return quote_plus(data.encode("utf-8")) otherwise return quote_plus(data)"""
|
"""If ``data`` is unicode, return quote_plus(data.encode("utf-8")) otherwise return quote_plus(data)"""
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(data, unicode):
|
if isinstance(data, unicode):
|
||||||
data = data.encode("utf-8")
|
data = data.encode("utf-8")
|
||||||
return quote_plus(data)
|
return quote_plus(data)
|
||||||
|
@ -65,13 +67,15 @@ if PY3:
|
||||||
return to_bytes(str(s))
|
return to_bytes(str(s))
|
||||||
else:
|
else:
|
||||||
def _strify(s):
|
def _strify(s):
|
||||||
"""If s is a unicode string, encode it to UTF-8 and return the results, otherwise return str(s), or None if s is None"""
|
"""If s is a unicode string, encode it to UTF-8 and return the results,
|
||||||
|
otherwise return str(s), or None if s is None"""
|
||||||
if s is None:
|
if s is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(s, unicode):
|
if isinstance(s, unicode):
|
||||||
return s.encode("utf-8")
|
return s.encode("utf-8")
|
||||||
return str(s)
|
return str(s)
|
||||||
|
|
||||||
|
|
||||||
class MultipartParam(object):
|
class MultipartParam(object):
|
||||||
"""Represents a single parameter in a multipart/form-data request
|
"""Represents a single parameter in a multipart/form-data request
|
||||||
|
|
||||||
|
@ -105,7 +109,7 @@ class MultipartParam(object):
|
||||||
transferred, and the total size.
|
transferred, and the total size.
|
||||||
"""
|
"""
|
||||||
def __init__(self, name, value=None, filename=None, filetype=None,
|
def __init__(self, name, value=None, filename=None, filetype=None,
|
||||||
filesize=None, fileobj=None, cb=None):
|
filesize=None, fileobj=None, cb=None):
|
||||||
self.name = Header(name).encode()
|
self.name = Header(name).encode()
|
||||||
self.value = _strify(value)
|
self.value = _strify(value)
|
||||||
if filename is None:
|
if filename is None:
|
||||||
|
@ -141,7 +145,7 @@ class MultipartParam(object):
|
||||||
fileobj.seek(0, 2)
|
fileobj.seek(0, 2)
|
||||||
self.filesize = fileobj.tell()
|
self.filesize = fileobj.tell()
|
||||||
fileobj.seek(0)
|
fileobj.seek(0)
|
||||||
except:
|
except Exception:
|
||||||
raise ValueError("Could not determine filesize")
|
raise ValueError("Could not determine filesize")
|
||||||
|
|
||||||
def __cmp__(self, other):
|
def __cmp__(self, other):
|
||||||
|
@ -169,9 +173,9 @@ class MultipartParam(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return cls(paramname, filename=os.path.basename(filename),
|
return cls(paramname, filename=os.path.basename(filename),
|
||||||
filetype=mimetypes.guess_type(filename)[0],
|
filetype=mimetypes.guess_type(filename)[0],
|
||||||
filesize=os.path.getsize(filename),
|
filesize=os.path.getsize(filename),
|
||||||
fileobj=open(filename, "rb"))
|
fileobj=open(filename, "rb"))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_params(cls, params):
|
def from_params(cls, params):
|
||||||
|
@ -204,7 +208,7 @@ class MultipartParam(object):
|
||||||
filetype = None
|
filetype = None
|
||||||
|
|
||||||
retval.append(cls(name=name, filename=filename,
|
retval.append(cls(name=name, filename=filename,
|
||||||
filetype=filetype, fileobj=value))
|
filetype=filetype, fileobj=value))
|
||||||
else:
|
else:
|
||||||
retval.append(cls(name, value))
|
retval.append(cls(name, value))
|
||||||
return retval
|
return retval
|
||||||
|
@ -216,8 +220,8 @@ class MultipartParam(object):
|
||||||
headers = ["--%s" % boundary]
|
headers = ["--%s" % boundary]
|
||||||
|
|
||||||
if self.filename:
|
if self.filename:
|
||||||
disposition = 'form-data; name="%s"; filename="%s"' % (self.name,
|
disposition = 'form-data; name="%s"; filename="%s"' % (
|
||||||
to_string(self.filename))
|
self.name, to_string(self.filename))
|
||||||
else:
|
else:
|
||||||
disposition = 'form-data; name="%s"' % self.name
|
disposition = 'form-data; name="%s"' % self.name
|
||||||
|
|
||||||
|
@ -267,8 +271,8 @@ class MultipartParam(object):
|
||||||
self.cb(self, current, total)
|
self.cb(self, current, total)
|
||||||
last_block = to_bytearray("")
|
last_block = to_bytearray("")
|
||||||
encoded_boundary = "--%s" % encode_and_quote(boundary)
|
encoded_boundary = "--%s" % encode_and_quote(boundary)
|
||||||
boundary_exp = re.compile(to_bytes("^%s$" % re.escape(encoded_boundary)),
|
boundary_exp = re.compile(
|
||||||
re.M)
|
to_bytes("^%s$" % re.escape(encoded_boundary)), re.M)
|
||||||
while True:
|
while True:
|
||||||
block = self.fileobj.read(blocksize)
|
block = self.fileobj.read(blocksize)
|
||||||
if not block:
|
if not block:
|
||||||
|
@ -296,6 +300,7 @@ class MultipartParam(object):
|
||||||
|
|
||||||
return len(self.encode_hdr(boundary)) + 2 + valuesize
|
return len(self.encode_hdr(boundary)) + 2 + valuesize
|
||||||
|
|
||||||
|
|
||||||
def encode_string(boundary, name, value):
|
def encode_string(boundary, name, value):
|
||||||
"""Returns ``name`` and ``value`` encoded as a multipart/form-data
|
"""Returns ``name`` and ``value`` encoded as a multipart/form-data
|
||||||
variable. ``boundary`` is the boundary string used throughout
|
variable. ``boundary`` is the boundary string used throughout
|
||||||
|
@ -303,8 +308,8 @@ def encode_string(boundary, name, value):
|
||||||
|
|
||||||
return MultipartParam(name, value).encode(boundary)
|
return MultipartParam(name, value).encode(boundary)
|
||||||
|
|
||||||
def encode_file_header(boundary, paramname, filesize, filename=None,
|
|
||||||
filetype=None):
|
def encode_file_header(boundary, paramname, filesize, filename=None, filetype=None):
|
||||||
"""Returns the leading data for a multipart/form-data field that contains
|
"""Returns the leading data for a multipart/form-data field that contains
|
||||||
file data.
|
file data.
|
||||||
|
|
||||||
|
@ -324,7 +329,8 @@ def encode_file_header(boundary, paramname, filesize, filename=None,
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return MultipartParam(paramname, filesize=filesize, filename=filename,
|
return MultipartParam(paramname, filesize=filesize, filename=filename,
|
||||||
filetype=filetype).encode_hdr(boundary)
|
filetype=filetype).encode_hdr(boundary)
|
||||||
|
|
||||||
|
|
||||||
def get_body_size(params, boundary):
|
def get_body_size(params, boundary):
|
||||||
"""Returns the number of bytes that the multipart/form-data encoding
|
"""Returns the number of bytes that the multipart/form-data encoding
|
||||||
|
@ -332,6 +338,7 @@ def get_body_size(params, boundary):
|
||||||
size = sum(p.get_size(boundary) for p in MultipartParam.from_params(params))
|
size = sum(p.get_size(boundary) for p in MultipartParam.from_params(params))
|
||||||
return size + len(boundary) + 6
|
return size + len(boundary) + 6
|
||||||
|
|
||||||
|
|
||||||
def get_headers(params, boundary):
|
def get_headers(params, boundary):
|
||||||
"""Returns a dictionary with Content-Type and Content-Length headers
|
"""Returns a dictionary with Content-Type and Content-Length headers
|
||||||
for the multipart/form-data encoding of ``params``."""
|
for the multipart/form-data encoding of ``params``."""
|
||||||
|
@ -341,6 +348,7 @@ def get_headers(params, boundary):
|
||||||
headers['Content-Length'] = str(get_body_size(params, boundary))
|
headers['Content-Length'] = str(get_body_size(params, boundary))
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
class multipart_yielder:
|
class multipart_yielder:
|
||||||
def __init__(self, params, boundary, cb):
|
def __init__(self, params, boundary, cb):
|
||||||
self.params = params
|
self.params = params
|
||||||
|
@ -396,6 +404,7 @@ class multipart_yielder:
|
||||||
for param in self.params:
|
for param in self.params:
|
||||||
param.reset()
|
param.reset()
|
||||||
|
|
||||||
|
|
||||||
def multipart_encode(params, boundary=None, cb=None):
|
def multipart_encode(params, boundary=None, cb=None):
|
||||||
"""Encode ``params`` as multipart/form-data.
|
"""Encode ``params`` as multipart/form-data.
|
||||||
|
|
||||||
|
|
|
@ -27,15 +27,18 @@ Example usage:
|
||||||
... {'Content-Length': str(len(s))})
|
... {'Content-Length': str(len(s))})
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys, socket
|
import socket
|
||||||
from cloudinary.compat import httplib, urllib2, NotConnected
|
import sys
|
||||||
|
|
||||||
|
from cloudinary.compat import NotConnected, httplib, urllib2
|
||||||
|
|
||||||
__all__ = ['StreamingHTTPConnection', 'StreamingHTTPRedirectHandler',
|
__all__ = ['StreamingHTTPConnection', 'StreamingHTTPRedirectHandler',
|
||||||
'StreamingHTTPHandler', 'register_openers']
|
'StreamingHTTPHandler', 'register_openers']
|
||||||
|
|
||||||
if hasattr(httplib, 'HTTPS'):
|
if hasattr(httplib, 'HTTPS'):
|
||||||
__all__.extend(['StreamingHTTPSHandler', 'StreamingHTTPSConnection'])
|
__all__.extend(['StreamingHTTPSHandler', 'StreamingHTTPSConnection'])
|
||||||
|
|
||||||
|
|
||||||
class _StreamingHTTPMixin:
|
class _StreamingHTTPMixin:
|
||||||
"""Mixin class for HTTP and HTTPS connections that implements a streaming
|
"""Mixin class for HTTP and HTTPS connections that implements a streaming
|
||||||
send method."""
|
send method."""
|
||||||
|
@ -62,7 +65,7 @@ class _StreamingHTTPMixin:
|
||||||
print("send:", repr(value))
|
print("send:", repr(value))
|
||||||
try:
|
try:
|
||||||
blocksize = 8192
|
blocksize = 8192
|
||||||
if hasattr(value, 'read') :
|
if hasattr(value, 'read'):
|
||||||
if hasattr(value, 'seek'):
|
if hasattr(value, 'seek'):
|
||||||
value.seek(0)
|
value.seek(0)
|
||||||
if self.debuglevel > 0:
|
if self.debuglevel > 0:
|
||||||
|
@ -86,10 +89,12 @@ class _StreamingHTTPMixin:
|
||||||
self.close()
|
self.close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
class StreamingHTTPConnection(_StreamingHTTPMixin, httplib.HTTPConnection):
|
class StreamingHTTPConnection(_StreamingHTTPMixin, httplib.HTTPConnection):
|
||||||
"""Subclass of `httplib.HTTPConnection` that overrides the `send()` method
|
"""Subclass of `httplib.HTTPConnection` that overrides the `send()` method
|
||||||
to support iterable body objects"""
|
to support iterable body objects"""
|
||||||
|
|
||||||
|
|
||||||
class StreamingHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
|
class StreamingHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
|
||||||
"""Subclass of `urllib2.HTTPRedirectHandler` that overrides the
|
"""Subclass of `urllib2.HTTPRedirectHandler` that overrides the
|
||||||
`redirect_request` method to properly handle redirected POST requests
|
`redirect_request` method to properly handle redirected POST requests
|
||||||
|
@ -114,7 +119,7 @@ class StreamingHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
|
||||||
"""
|
"""
|
||||||
m = req.get_method()
|
m = req.get_method()
|
||||||
if (code in (301, 302, 303, 307) and m in ("GET", "HEAD")
|
if (code in (301, 302, 303, 307) and m in ("GET", "HEAD")
|
||||||
or code in (301, 302, 303) and m == "POST"):
|
or code in (301, 302, 303) and m == "POST"):
|
||||||
# Strictly (according to RFC 2616), 301 or 302 in response
|
# Strictly (according to RFC 2616), 301 or 302 in response
|
||||||
# to a POST MUST NOT cause a redirection without confirmation
|
# to a POST MUST NOT cause a redirection without confirmation
|
||||||
# from the user (of urllib2, in this case). In practice,
|
# from the user (of urllib2, in this case). In practice,
|
||||||
|
@ -125,14 +130,16 @@ class StreamingHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
|
||||||
newheaders = dict((k, v) for k, v in req.headers.items()
|
newheaders = dict((k, v) for k, v in req.headers.items()
|
||||||
if k.lower() not in (
|
if k.lower() not in (
|
||||||
"content-length", "content-type")
|
"content-length", "content-type")
|
||||||
)
|
)
|
||||||
return urllib2.Request(newurl,
|
return urllib2.Request(
|
||||||
headers=newheaders,
|
newurl,
|
||||||
origin_req_host=req.get_origin_req_host(),
|
headers=newheaders,
|
||||||
unverifiable=True)
|
origin_req_host=req.get_origin_req_host(),
|
||||||
|
unverifiable=True)
|
||||||
else:
|
else:
|
||||||
raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
|
raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
|
||||||
|
|
||||||
|
|
||||||
class StreamingHTTPHandler(urllib2.HTTPHandler):
|
class StreamingHTTPHandler(urllib2.HTTPHandler):
|
||||||
"""Subclass of `urllib2.HTTPHandler` that uses
|
"""Subclass of `urllib2.HTTPHandler` that uses
|
||||||
StreamingHTTPConnection as its http connection class."""
|
StreamingHTTPConnection as its http connection class."""
|
||||||
|
@ -156,9 +163,9 @@ class StreamingHTTPHandler(urllib2.HTTPHandler):
|
||||||
"No Content-Length specified for iterable body")
|
"No Content-Length specified for iterable body")
|
||||||
return urllib2.HTTPHandler.do_request_(self, req)
|
return urllib2.HTTPHandler.do_request_(self, req)
|
||||||
|
|
||||||
|
|
||||||
if hasattr(httplib, 'HTTPS'):
|
if hasattr(httplib, 'HTTPS'):
|
||||||
class StreamingHTTPSConnection(_StreamingHTTPMixin,
|
class StreamingHTTPSConnection(_StreamingHTTPMixin, httplib.HTTPSConnection):
|
||||||
httplib.HTTPSConnection):
|
|
||||||
"""Subclass of `httplib.HTTSConnection` that overrides the `send()`
|
"""Subclass of `httplib.HTTSConnection` that overrides the `send()`
|
||||||
method to support iterable body objects"""
|
method to support iterable body objects"""
|
||||||
|
|
||||||
|
@ -179,7 +186,7 @@ if hasattr(httplib, 'HTTPS'):
|
||||||
if hasattr(data, 'read') or hasattr(data, 'next'):
|
if hasattr(data, 'read') or hasattr(data, 'next'):
|
||||||
if not req.has_header('Content-length'):
|
if not req.has_header('Content-length'):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"No Content-Length specified for iterable body")
|
"No Content-Length specified for iterable body")
|
||||||
return urllib2.HTTPSHandler.do_request_(self, req)
|
return urllib2.HTTPSHandler.do_request_(self, req)
|
||||||
|
|
||||||
|
|
||||||
|
@ -188,7 +195,8 @@ def get_handlers():
|
||||||
if hasattr(httplib, "HTTPS"):
|
if hasattr(httplib, "HTTPS"):
|
||||||
handlers.append(StreamingHTTPSHandler)
|
handlers.append(StreamingHTTPSHandler)
|
||||||
return handlers
|
return handlers
|
||||||
|
|
||||||
|
|
||||||
def register_openers():
|
def register_openers():
|
||||||
"""Register the streaming http handlers in the global urllib2 default
|
"""Register the streaming http handlers in the global urllib2 default
|
||||||
opener object.
|
opener object.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import json
|
import json
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from . import api
|
|
||||||
|
from cloudinary.api import call_json_api
|
||||||
|
|
||||||
|
|
||||||
class Search:
|
class Search:
|
||||||
|
@ -46,8 +47,8 @@ class Search:
|
||||||
def execute(self, **options):
|
def execute(self, **options):
|
||||||
"""Execute the search and return results."""
|
"""Execute the search and return results."""
|
||||||
options["content_type"] = 'application/json'
|
options["content_type"] = 'application/json'
|
||||||
uri = ['resources','search']
|
uri = ['resources', 'search']
|
||||||
return api.call_json_api('post', uri, self.as_dict(), **options)
|
return call_json_api('post', uri, self.as_dict(), **options)
|
||||||
|
|
||||||
def _add(self, name, value):
|
def _add(self, name, value):
|
||||||
if name not in self.query:
|
if name not in self.query:
|
||||||
|
@ -56,4 +57,4 @@ class Search:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
return deepcopy(self.query)
|
return deepcopy(self.query)
|
||||||
|
|
|
@ -802,7 +802,7 @@ var slice = [].slice,
|
||||||
function TextLayer(options) {
|
function TextLayer(options) {
|
||||||
var keys;
|
var keys;
|
||||||
TextLayer.__super__.constructor.call(this, options);
|
TextLayer.__super__.constructor.call(this, options);
|
||||||
keys = ["resourceType", "resourceType", "fontFamily", "fontSize", "fontWeight", "fontStyle", "textDecoration", "textAlign", "stroke", "letterSpacing", "lineSpacing", "text"];
|
keys = ["resourceType", "resourceType", "fontFamily", "fontSize", "fontWeight", "fontStyle", "textDecoration", "textAlign", "stroke", "letterSpacing", "lineSpacing", "fontHinting", "fontAntialiasing", "text"];
|
||||||
if (options != null) {
|
if (options != null) {
|
||||||
keys.forEach((function(_this) {
|
keys.forEach((function(_this) {
|
||||||
return function(key) {
|
return function(key) {
|
||||||
|
@ -871,6 +871,16 @@ var slice = [].slice,
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TextLayer.prototype.fontAntialiasing = function(fontAntialiasing){
|
||||||
|
this.options.fontAntialiasing = fontAntialiasing;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
TextLayer.prototype.fontHinting = function(fontHinting ){
|
||||||
|
this.options.fontHinting = fontHinting ;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
TextLayer.prototype.text = function(text) {
|
TextLayer.prototype.text = function(text) {
|
||||||
this.options.text = text;
|
this.options.text = text;
|
||||||
return this;
|
return this;
|
||||||
|
@ -932,6 +942,12 @@ var slice = [].slice,
|
||||||
if (!(Util.isEmpty(this.options.lineSpacing) && !Util.isNumberLike(this.options.lineSpacing))) {
|
if (!(Util.isEmpty(this.options.lineSpacing) && !Util.isNumberLike(this.options.lineSpacing))) {
|
||||||
components.push("line_spacing_" + this.options.lineSpacing);
|
components.push("line_spacing_" + this.options.lineSpacing);
|
||||||
}
|
}
|
||||||
|
if (this.options.fontAntialiasing !== "none") {
|
||||||
|
components.push("antialias_"+this.options.fontAntialiasing);
|
||||||
|
}
|
||||||
|
if (this.options.fontHinting !== "none") {
|
||||||
|
components.push("hinting_"+this.options.fontHinting);
|
||||||
|
}
|
||||||
if (!Util.isEmpty(Util.compact(components))) {
|
if (!Util.isEmpty(Util.compact(components))) {
|
||||||
if (Util.isEmpty(this.options.fontFamily)) {
|
if (Util.isEmpty(this.options.fontFamily)) {
|
||||||
throw "Must supply fontFamily. " + components;
|
throw "Must supply fontFamily. " + components;
|
||||||
|
@ -2780,6 +2796,20 @@ var slice = [].slice,
|
||||||
return this.param(value, "gravity", "g");
|
return this.param(value, "gravity", "g");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Transformation.prototype.fps = function(value) {
|
||||||
|
return this.param(value, "fps", "fps", (function(_this) {
|
||||||
|
return function(fps) {
|
||||||
|
if (Util.isString(fps)) {
|
||||||
|
return fps;
|
||||||
|
} else if (Util.isArray(fps)) {
|
||||||
|
return fps.join("-");
|
||||||
|
} else {
|
||||||
|
return fps;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(this));
|
||||||
|
};
|
||||||
|
|
||||||
Transformation.prototype.height = function(value) {
|
Transformation.prototype.height = function(value) {
|
||||||
return this.param(value, "height", "h", (function(_this) {
|
return this.param(value, "height", "h", (function(_this) {
|
||||||
return function() {
|
return function() {
|
||||||
|
|
28
lib/cloudinary/static/js/jquery.fileupload.js
vendored
28
lib/cloudinary/static/js/jquery.fileupload.js
vendored
|
@ -43,7 +43,7 @@
|
||||||
'|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
|
'|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
|
||||||
).test(window.navigator.userAgent) ||
|
).test(window.navigator.userAgent) ||
|
||||||
// Feature detection for all other devices:
|
// Feature detection for all other devices:
|
||||||
$('<input type="file">').prop('disabled'));
|
$('<input type="file"/>').prop('disabled'));
|
||||||
|
|
||||||
// The FileReader API is not actually used, but works as feature detection,
|
// The FileReader API is not actually used, but works as feature detection,
|
||||||
// as some Safari versions (5?) support XHR file uploads via the FormData API,
|
// as some Safari versions (5?) support XHR file uploads via the FormData API,
|
||||||
|
@ -261,6 +261,9 @@
|
||||||
// Callback for dragover events of the dropZone(s):
|
// Callback for dragover events of the dropZone(s):
|
||||||
// dragover: function (e) {}, // .bind('fileuploaddragover', func);
|
// dragover: function (e) {}, // .bind('fileuploaddragover', func);
|
||||||
|
|
||||||
|
// Callback before the start of each chunk upload request (before form data initialization):
|
||||||
|
// chunkbeforesend: function (e, data) {}, // .bind('fileuploadchunkbeforesend', func);
|
||||||
|
|
||||||
// Callback for the start of each chunk upload request:
|
// Callback for the start of each chunk upload request:
|
||||||
// chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
|
// chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
|
||||||
|
|
||||||
|
@ -434,6 +437,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_deinitProgressListener: function (options) {
|
||||||
|
var xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
|
||||||
|
if (xhr.upload) {
|
||||||
|
$(xhr.upload).unbind('progress');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_isInstanceOf: function (type, obj) {
|
_isInstanceOf: function (type, obj) {
|
||||||
// Cross-frame instanceof check
|
// Cross-frame instanceof check
|
||||||
return Object.prototype.toString.call(obj) === '[object ' + type + ']';
|
return Object.prototype.toString.call(obj) === '[object ' + type + ']';
|
||||||
|
@ -453,7 +463,7 @@
|
||||||
}
|
}
|
||||||
if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
|
if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
|
||||||
options.headers['Content-Disposition'] = 'attachment; filename="' +
|
options.headers['Content-Disposition'] = 'attachment; filename="' +
|
||||||
encodeURI(file.name) + '"';
|
encodeURI(file.uploadName || file.name) + '"';
|
||||||
}
|
}
|
||||||
if (!multipart) {
|
if (!multipart) {
|
||||||
options.contentType = file.type || 'application/octet-stream';
|
options.contentType = file.type || 'application/octet-stream';
|
||||||
|
@ -489,7 +499,11 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (options.blob) {
|
if (options.blob) {
|
||||||
formData.append(paramName, options.blob, file.name);
|
formData.append(
|
||||||
|
paramName,
|
||||||
|
options.blob,
|
||||||
|
file.uploadName || file.name
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
$.each(options.files, function (index, file) {
|
$.each(options.files, function (index, file) {
|
||||||
// This check allows the tests to run with
|
// This check allows the tests to run with
|
||||||
|
@ -762,6 +776,8 @@
|
||||||
// Expose the chunk bytes position range:
|
// Expose the chunk bytes position range:
|
||||||
o.contentRange = 'bytes ' + ub + '-' +
|
o.contentRange = 'bytes ' + ub + '-' +
|
||||||
(ub + o.chunkSize - 1) + '/' + fs;
|
(ub + o.chunkSize - 1) + '/' + fs;
|
||||||
|
// Trigger chunkbeforesend to allow form data to be updated for this chunk
|
||||||
|
that._trigger('chunkbeforesend', null, o);
|
||||||
// Process the upload data (the blob and potential form data):
|
// Process the upload data (the blob and potential form data):
|
||||||
that._initXHRData(o);
|
that._initXHRData(o);
|
||||||
// Add progress listeners for this chunk upload:
|
// Add progress listeners for this chunk upload:
|
||||||
|
@ -808,6 +824,9 @@
|
||||||
o.context,
|
o.context,
|
||||||
[jqXHR, textStatus, errorThrown]
|
[jqXHR, textStatus, errorThrown]
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
.always(function () {
|
||||||
|
that._deinitProgressListener(o);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
this._enhancePromise(promise);
|
this._enhancePromise(promise);
|
||||||
|
@ -909,6 +928,7 @@
|
||||||
}).fail(function (jqXHR, textStatus, errorThrown) {
|
}).fail(function (jqXHR, textStatus, errorThrown) {
|
||||||
that._onFail(jqXHR, textStatus, errorThrown, options);
|
that._onFail(jqXHR, textStatus, errorThrown, options);
|
||||||
}).always(function (jqXHRorResult, textStatus, jqXHRorError) {
|
}).always(function (jqXHRorResult, textStatus, jqXHRorError) {
|
||||||
|
that._deinitProgressListener(options);
|
||||||
that._onAlways(
|
that._onAlways(
|
||||||
jqXHRorResult,
|
jqXHRorResult,
|
||||||
textStatus,
|
textStatus,
|
||||||
|
@ -1126,7 +1146,7 @@
|
||||||
dirReader = entry.createReader();
|
dirReader = entry.createReader();
|
||||||
readEntries();
|
readEntries();
|
||||||
} else {
|
} else {
|
||||||
// Return an empy list for file system items
|
// Return an empty list for file system items
|
||||||
// other than files or directories:
|
// other than files or directories:
|
||||||
dfd.resolve([]);
|
dfd.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
1176
lib/cloudinary/static/js/jquery.ui.widget.js
vendored
1176
lib/cloudinary/static/js/jquery.ui.widget.js
vendored
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
@ -2,15 +2,14 @@ from __future__ import absolute_import
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import cloudinary
|
||||||
|
from cloudinary import CloudinaryResource, utils
|
||||||
|
from cloudinary.compat import PY3
|
||||||
|
from cloudinary.forms import CloudinaryJsFileField, cl_init_js_callbacks
|
||||||
from django import template
|
from django import template
|
||||||
from django.forms import Form
|
from django.forms import Form
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
import cloudinary
|
|
||||||
from cloudinary import CloudinaryResource, utils, uploader
|
|
||||||
from cloudinary.forms import CloudinaryJsFileField, cl_init_js_callbacks
|
|
||||||
from cloudinary.compat import PY3
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,9 +56,9 @@ def cloudinary_direct_upload_field(field_name="image", request=None):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
"""Deprecated - please use cloudinary_direct_upload_field, or a proper form"""
|
|
||||||
@register.inclusion_tag('cloudinary_direct_upload.html')
|
@register.inclusion_tag('cloudinary_direct_upload.html')
|
||||||
def cloudinary_direct_upload(callback_url, **options):
|
def cloudinary_direct_upload(callback_url, **options):
|
||||||
|
"""Deprecated - please use cloudinary_direct_upload_field, or a proper form"""
|
||||||
params = utils.build_upload_params(callback=callback_url, **options)
|
params = utils.build_upload_params(callback=callback_url, **options)
|
||||||
params = utils.sign_request(params, options)
|
params = utils.sign_request(params, options)
|
||||||
|
|
||||||
|
@ -75,6 +74,8 @@ def cloudinary_includes(processing=False):
|
||||||
|
|
||||||
|
|
||||||
CLOUDINARY_JS_CONFIG_PARAMS = ("api_key", "cloud_name", "private_cdn", "secure_distribution", "cdn_subdomain")
|
CLOUDINARY_JS_CONFIG_PARAMS = ("api_key", "cloud_name", "private_cdn", "secure_distribution", "cdn_subdomain")
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('cloudinary_js_config.html')
|
@register.inclusion_tag('cloudinary_js_config.html')
|
||||||
def cloudinary_js_config():
|
def cloudinary_js_config():
|
||||||
config = cloudinary.config()
|
config = cloudinary.config()
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
# Copyright Cloudinary
|
# Copyright Cloudinary
|
||||||
import json
|
import json
|
||||||
import re
|
import os
|
||||||
import socket
|
import socket
|
||||||
from os.path import getsize
|
|
||||||
|
import certifi
|
||||||
|
from six import string_types
|
||||||
|
from urllib3 import PoolManager, ProxyManager
|
||||||
|
from urllib3.exceptions import HTTPError
|
||||||
|
|
||||||
import cloudinary
|
import cloudinary
|
||||||
import urllib3
|
|
||||||
import certifi
|
|
||||||
from cloudinary import utils
|
from cloudinary import utils
|
||||||
from cloudinary.api import Error
|
from cloudinary.exceptions import Error
|
||||||
from cloudinary.compat import string_types
|
from cloudinary.cache.responsive_breakpoints_cache import instance as responsive_breakpoints_cache_instance
|
||||||
from urllib3.exceptions import HTTPError
|
|
||||||
from urllib3 import PoolManager
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox
|
from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox
|
||||||
|
@ -29,15 +29,21 @@ if is_appengine_sandbox():
|
||||||
_http = AppEngineManager()
|
_http = AppEngineManager()
|
||||||
else:
|
else:
|
||||||
# PoolManager uses a socket-level API behind the scenes
|
# PoolManager uses a socket-level API behind the scenes
|
||||||
_http = PoolManager(
|
_http = utils.get_http_connector(cloudinary.config(), cloudinary.CERT_KWARGS)
|
||||||
cert_reqs='CERT_REQUIRED',
|
|
||||||
ca_certs=certifi.where()
|
upload_options = [
|
||||||
)
|
"filename",
|
||||||
|
"timeout",
|
||||||
|
"chunk_size",
|
||||||
|
"use_cache"
|
||||||
|
]
|
||||||
|
|
||||||
|
UPLOAD_LARGE_CHUNK_SIZE = 20000000
|
||||||
|
|
||||||
|
|
||||||
def upload(file, **options):
|
def upload(file, **options):
|
||||||
params = utils.build_upload_params(**options)
|
params = utils.build_upload_params(**options)
|
||||||
return call_api("upload", params, file=file, **options)
|
return call_cacheable_api("upload", params, file=file, **options)
|
||||||
|
|
||||||
|
|
||||||
def unsigned_upload(file, upload_preset, **options):
|
def unsigned_upload(file, upload_preset, **options):
|
||||||
|
@ -55,35 +61,56 @@ def upload_resource(file, **options):
|
||||||
result = upload(file, **options)
|
result = upload(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"], resource_type=result["resource_type"], metadata=result)
|
format=result.get("format"), type=result["type"],
|
||||||
|
resource_type=result["resource_type"], metadata=result)
|
||||||
|
|
||||||
|
|
||||||
def upload_large(file, **options):
|
def upload_large(file, **options):
|
||||||
""" Upload large files. """
|
""" Upload large files. """
|
||||||
upload_id = utils.random_public_id()
|
if utils.is_remote_url(file):
|
||||||
with open(file, 'rb') as file_io:
|
return upload(file, **options)
|
||||||
results = None
|
|
||||||
current_loc = 0
|
if hasattr(file, 'read') and callable(file.read):
|
||||||
chunk_size = options.get("chunk_size", 20000000)
|
file_io = file
|
||||||
file_size = getsize(file)
|
else:
|
||||||
chunk = file_io.read(chunk_size)
|
file_io = open(file, 'rb')
|
||||||
while chunk:
|
|
||||||
range = "bytes {0}-{1}/{2}".format(current_loc, current_loc + len(chunk) - 1, file_size)
|
upload_result = None
|
||||||
current_loc += len(chunk)
|
|
||||||
|
with file_io:
|
||||||
|
upload_id = utils.random_public_id()
|
||||||
|
current_loc = 0
|
||||||
|
chunk_size = options.get("chunk_size", UPLOAD_LARGE_CHUNK_SIZE)
|
||||||
|
file_size = utils.file_io_size(file_io)
|
||||||
|
|
||||||
|
file_name = options.get(
|
||||||
|
"filename",
|
||||||
|
file_io.name if hasattr(file_io, 'name') and isinstance(file_io.name, str) else "stream")
|
||||||
|
|
||||||
|
chunk = file_io.read(chunk_size)
|
||||||
|
|
||||||
|
while chunk:
|
||||||
|
content_range = "bytes {0}-{1}/{2}".format(current_loc, current_loc + len(chunk) - 1, file_size)
|
||||||
|
current_loc += len(chunk)
|
||||||
|
http_headers = {"Content-Range": content_range, "X-Unique-Upload-Id": upload_id}
|
||||||
|
|
||||||
|
upload_result = upload_large_part((file_name, chunk), http_headers=http_headers, **options)
|
||||||
|
|
||||||
|
options["public_id"] = upload_result.get("public_id")
|
||||||
|
|
||||||
results = upload_large_part((file, chunk),
|
|
||||||
http_headers={"Content-Range": range, "X-Unique-Upload-Id": upload_id},
|
|
||||||
**options)
|
|
||||||
options["public_id"] = results.get("public_id")
|
|
||||||
chunk = file_io.read(chunk_size)
|
chunk = file_io.read(chunk_size)
|
||||||
return results
|
|
||||||
|
return upload_result
|
||||||
|
|
||||||
|
|
||||||
def upload_large_part(file, **options):
|
def upload_large_part(file, **options):
|
||||||
""" Upload large files. """
|
""" Upload large files. """
|
||||||
params = utils.build_upload_params(**options)
|
params = utils.build_upload_params(**options)
|
||||||
if 'resource_type' not in options: options['resource_type'] = "raw"
|
|
||||||
return call_api("upload", params, file=file, **options)
|
if 'resource_type' not in options:
|
||||||
|
options['resource_type'] = "raw"
|
||||||
|
|
||||||
|
return call_cacheable_api("upload", params, file=file, **options)
|
||||||
|
|
||||||
|
|
||||||
def destroy(public_id, **options):
|
def destroy(public_id, **options):
|
||||||
|
@ -91,7 +118,7 @@ def destroy(public_id, **options):
|
||||||
"timestamp": utils.now(),
|
"timestamp": utils.now(),
|
||||||
"type": options.get("type"),
|
"type": options.get("type"),
|
||||||
"invalidate": options.get("invalidate"),
|
"invalidate": options.get("invalidate"),
|
||||||
"public_id": public_id
|
"public_id": public_id
|
||||||
}
|
}
|
||||||
return call_api("destroy", params, **options)
|
return call_api("destroy", params, **options)
|
||||||
|
|
||||||
|
@ -103,15 +130,43 @@ def rename(from_public_id, to_public_id, **options):
|
||||||
"overwrite": options.get("overwrite"),
|
"overwrite": options.get("overwrite"),
|
||||||
"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")
|
||||||
}
|
}
|
||||||
return call_api("rename", params, **options)
|
return call_api("rename", params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def update_metadata(metadata, public_ids, **options):
|
||||||
|
"""
|
||||||
|
Populates metadata fields with the given values. Existing values will be overwritten.
|
||||||
|
|
||||||
|
Any metadata-value pairs given are merged with any existing metadata-value pairs
|
||||||
|
(an empty value for an existing metadata field clears the value)
|
||||||
|
|
||||||
|
:param metadata: A list of custom metadata fields (by external_id) and the values to assign to each
|
||||||
|
of them.
|
||||||
|
:param public_ids: An array of Public IDs of assets uploaded to Cloudinary.
|
||||||
|
:param options: Options such as
|
||||||
|
*resource_type* (the type of file. Default: image. Valid values: image, raw, or video) and
|
||||||
|
*type* (The storage type. Default: upload. Valid values: upload, private, or authenticated.)
|
||||||
|
|
||||||
|
:return: A list of public IDs that were updated
|
||||||
|
:rtype: mixed
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"timestamp": utils.now(),
|
||||||
|
"metadata": utils.encode_context(metadata),
|
||||||
|
"public_ids": utils.build_array(public_ids),
|
||||||
|
"type": options.get("type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return call_api("metadata", params, **options)
|
||||||
|
|
||||||
|
|
||||||
def explicit(public_id, **options):
|
def explicit(public_id, **options):
|
||||||
params = utils.build_upload_params(**options)
|
params = utils.build_upload_params(**options)
|
||||||
params["public_id"] = public_id
|
params["public_id"] = public_id
|
||||||
return call_api("explicit", params, **options)
|
return call_cacheable_api("explicit", params, **options)
|
||||||
|
|
||||||
|
|
||||||
def create_archive(**options):
|
def create_archive(**options):
|
||||||
|
@ -131,7 +186,8 @@ def generate_sprite(tag, **options):
|
||||||
"tag": tag,
|
"tag": tag,
|
||||||
"async": options.get("async"),
|
"async": options.get("async"),
|
||||||
"notification_url": options.get("notification_url"),
|
"notification_url": options.get("notification_url"),
|
||||||
"transformation": utils.generate_transformation_string(fetch_format=options.get("format"), **options)[0]
|
"transformation": utils.generate_transformation_string(
|
||||||
|
fetch_format=options.get("format"), **options)[0]
|
||||||
}
|
}
|
||||||
return call_api("sprite", params, **options)
|
return call_api("sprite", params, **options)
|
||||||
|
|
||||||
|
@ -177,8 +233,10 @@ def replace_tag(tag, public_ids=None, **options):
|
||||||
def remove_all_tags(public_ids, **options):
|
def remove_all_tags(public_ids, **options):
|
||||||
"""
|
"""
|
||||||
Remove all tags from the specified public IDs.
|
Remove all tags from the specified public IDs.
|
||||||
|
|
||||||
:param public_ids: the public IDs of the resources to update
|
:param public_ids: the public IDs of the resources to update
|
||||||
:param options: additional options passed to the request
|
:param options: additional options passed to the request
|
||||||
|
|
||||||
:return: dictionary with a list of public IDs that were updated
|
:return: dictionary with a list of public IDs that were updated
|
||||||
"""
|
"""
|
||||||
return call_tags_api(None, "remove_all", public_ids, **options)
|
return call_tags_api(None, "remove_all", public_ids, **options)
|
||||||
|
@ -187,9 +245,11 @@ def remove_all_tags(public_ids, **options):
|
||||||
def add_context(context, public_ids, **options):
|
def add_context(context, public_ids, **options):
|
||||||
"""
|
"""
|
||||||
Add a context keys and values. If a particular key already exists, the value associated with the key is updated.
|
Add a context keys and values. If a particular key already exists, the value associated with the key is updated.
|
||||||
|
|
||||||
:param context: dictionary of context
|
:param context: dictionary of context
|
||||||
:param public_ids: the public IDs of the resources to update
|
:param public_ids: the public IDs of the resources to update
|
||||||
:param options: additional options passed to the request
|
:param options: additional options passed to the request
|
||||||
|
|
||||||
:return: dictionary with a list of public IDs that were updated
|
:return: dictionary with a list of public IDs that were updated
|
||||||
"""
|
"""
|
||||||
return call_context_api(context, "add", public_ids, **options)
|
return call_context_api(context, "add", public_ids, **options)
|
||||||
|
@ -198,8 +258,10 @@ def add_context(context, public_ids, **options):
|
||||||
def remove_all_context(public_ids, **options):
|
def remove_all_context(public_ids, **options):
|
||||||
"""
|
"""
|
||||||
Remove all custom context from the specified public IDs.
|
Remove all custom context from the specified public IDs.
|
||||||
|
|
||||||
:param public_ids: the public IDs of the resources to update
|
:param public_ids: the public IDs of the resources to update
|
||||||
:param options: additional options passed to the request
|
:param options: additional options passed to the request
|
||||||
|
|
||||||
:return: dictionary with a list of public IDs that were updated
|
:return: dictionary with a list of public IDs that were updated
|
||||||
"""
|
"""
|
||||||
return call_context_api(None, "remove_all", public_ids, **options)
|
return call_context_api(None, "remove_all", public_ids, **options)
|
||||||
|
@ -227,17 +289,18 @@ def call_context_api(context, command, public_ids=None, **options):
|
||||||
return call_api("context", params, **options)
|
return call_api("context", params, **options)
|
||||||
|
|
||||||
|
|
||||||
TEXT_PARAMS = ["public_id",
|
TEXT_PARAMS = [
|
||||||
"font_family",
|
"public_id",
|
||||||
"font_size",
|
"font_family",
|
||||||
"font_color",
|
"font_size",
|
||||||
"text_align",
|
"font_color",
|
||||||
"font_weight",
|
"text_align",
|
||||||
"font_style",
|
"font_weight",
|
||||||
"background",
|
"font_style",
|
||||||
"opacity",
|
"background",
|
||||||
"text_decoration"
|
"opacity",
|
||||||
]
|
"text_decoration"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def text(text, **options):
|
def text(text, **options):
|
||||||
|
@ -247,6 +310,42 @@ def text(text, **options):
|
||||||
return call_api("text", params, **options)
|
return call_api("text", params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def _save_responsive_breakpoints_to_cache(result):
|
||||||
|
"""
|
||||||
|
Saves responsive breakpoints parsed from upload result to cache
|
||||||
|
|
||||||
|
:param result: Upload result
|
||||||
|
"""
|
||||||
|
if "responsive_breakpoints" not in result:
|
||||||
|
return
|
||||||
|
|
||||||
|
if "public_id" not in result:
|
||||||
|
# We have some faulty result, nothing to cache
|
||||||
|
return
|
||||||
|
|
||||||
|
options = dict((k, result[k]) for k in ["type", "resource_type"] if k in result)
|
||||||
|
|
||||||
|
for transformation in result.get("responsive_breakpoints", []):
|
||||||
|
options["raw_transformation"] = transformation.get("transformation", "")
|
||||||
|
options["format"] = os.path.splitext(transformation["breakpoints"][0]["url"])[1][1:]
|
||||||
|
breakpoints = [bp["width"] for bp in transformation["breakpoints"]]
|
||||||
|
responsive_breakpoints_cache_instance.set(result["public_id"], breakpoints, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def call_cacheable_api(action, params, http_headers=None, return_error=False, unsigned=False, file=None, timeout=None,
|
||||||
|
**options):
|
||||||
|
"""
|
||||||
|
Calls Upload API and saves results to cache (if enabled)
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = call_api(action, params, http_headers, return_error, unsigned, file, timeout, **options)
|
||||||
|
|
||||||
|
if "use_cache" in options or cloudinary.config().use_cache:
|
||||||
|
_save_responsive_breakpoints_to_cache(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
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:
|
if http_headers is None:
|
||||||
http_headers = {}
|
http_headers = {}
|
||||||
|
@ -267,26 +366,27 @@ def call_api(action, params, http_headers=None, return_error=False, unsigned=Fal
|
||||||
|
|
||||||
api_url = utils.cloudinary_api_url(action, **options)
|
api_url = utils.cloudinary_api_url(action, **options)
|
||||||
if file:
|
if file:
|
||||||
|
filename = options.get("filename") # Custom filename provided by user (relevant only for streams and files)
|
||||||
|
|
||||||
if isinstance(file, string_types):
|
if isinstance(file, string_types):
|
||||||
if re.match(r'ftp:|https?:|s3:|data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$', file):
|
if utils.is_remote_url(file):
|
||||||
# URL
|
# URL
|
||||||
name = None
|
name = None
|
||||||
data = file
|
data = file
|
||||||
else:
|
else:
|
||||||
# file path
|
# file path
|
||||||
name = file
|
name = filename or file
|
||||||
with open(file, "rb") as opened:
|
with open(file, "rb") as opened:
|
||||||
data = opened.read()
|
data = opened.read()
|
||||||
elif hasattr(file, 'read') and callable(file.read):
|
elif hasattr(file, 'read') and callable(file.read):
|
||||||
# stream
|
# stream
|
||||||
data = file.read()
|
data = file.read()
|
||||||
name = file.name if hasattr(file, 'name') and isinstance(file.name, str) else "stream"
|
name = filename or (file.name if hasattr(file, 'name') and isinstance(file.name, str) else "stream")
|
||||||
elif isinstance(file, tuple):
|
elif isinstance(file, tuple):
|
||||||
name = None
|
name, data = file
|
||||||
data = file
|
|
||||||
else:
|
else:
|
||||||
# Not a string, not a stream
|
# Not a string, not a stream
|
||||||
name = "file"
|
name = filename or "file"
|
||||||
data = file
|
data = file
|
||||||
|
|
||||||
param_list["file"] = (name, data) if name else data
|
param_list["file"] = (name, data) if name else data
|
||||||
|
@ -310,16 +410,17 @@ def call_api(action, params, http_headers=None, return_error=False, unsigned=Fal
|
||||||
result = json.loads(response.data.decode('utf-8'))
|
result = json.loads(response.data.decode('utf-8'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Error is parsing json
|
# Error is parsing json
|
||||||
raise Error("Error parsing server response (%d) - %s. Got - %s", response.status, response, e)
|
raise Error("Error parsing server response (%d) - %s. Got - %s" % (response.status, response.data, e))
|
||||||
|
|
||||||
if "error" in result:
|
if "error" in result:
|
||||||
if response.status not in [200, 400, 401, 403, 404, 500]:
|
if response.status not in [200, 400, 401, 403, 404, 500]:
|
||||||
code = response.status
|
code = response.status
|
||||||
if return_error:
|
if return_error:
|
||||||
result["error"]["http_code"] = code
|
result["error"]["http_code"] = code
|
||||||
else:
|
else:
|
||||||
raise Error(result["error"]["message"])
|
raise Error(result["error"]["message"])
|
||||||
|
|
||||||
return result
|
return result
|
||||||
finally:
|
finally:
|
||||||
if file_io: file_io.close()
|
if file_io:
|
||||||
|
file_io.close()
|
||||||
|
|
|
@ -3,15 +3,19 @@ import base64
|
||||||
import copy
|
import copy
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
|
import urllib
|
||||||
import zlib
|
import zlib
|
||||||
from collections import OrderedDict
|
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 urllib3 import ProxyManager, PoolManager
|
||||||
|
|
||||||
import six.moves.urllib.parse
|
import six.moves.urllib.parse
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
@ -33,12 +37,101 @@ 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=]+)$'
|
||||||
__LAYER_KEYWORD_PARAMS = [("font_weight", "normal"),
|
__LAYER_KEYWORD_PARAMS = [("font_weight", "normal"),
|
||||||
("font_style", "normal"),
|
("font_style", "normal"),
|
||||||
("text_decoration", "none"),
|
("text_decoration", "none"),
|
||||||
("text_align", None),
|
("text_align", None),
|
||||||
("stroke", "none")]
|
("stroke", "none")]
|
||||||
|
|
||||||
|
# a list of keys used by the cloudinary_url function
|
||||||
|
__URL_KEYS = [
|
||||||
|
'api_secret',
|
||||||
|
'auth_token',
|
||||||
|
'cdn_subdomain',
|
||||||
|
'cloud_name',
|
||||||
|
'cname',
|
||||||
|
'format',
|
||||||
|
'private_cdn',
|
||||||
|
'resource_type',
|
||||||
|
'secure',
|
||||||
|
'secure_cdn_subdomain',
|
||||||
|
'secure_distribution',
|
||||||
|
'shorten',
|
||||||
|
'sign_url',
|
||||||
|
'ssl_detected',
|
||||||
|
'type',
|
||||||
|
'url_suffix',
|
||||||
|
'use_root_path',
|
||||||
|
'version'
|
||||||
|
]
|
||||||
|
|
||||||
|
__SIMPLE_UPLOAD_PARAMS = [
|
||||||
|
"public_id",
|
||||||
|
"callback",
|
||||||
|
"format",
|
||||||
|
"type",
|
||||||
|
"backup",
|
||||||
|
"faces",
|
||||||
|
"image_metadata",
|
||||||
|
"exif",
|
||||||
|
"colors",
|
||||||
|
"use_filename",
|
||||||
|
"unique_filename",
|
||||||
|
"discard_original_filename",
|
||||||
|
"invalidate",
|
||||||
|
"notification_url",
|
||||||
|
"eager_notification_url",
|
||||||
|
"eager_async",
|
||||||
|
"proxy",
|
||||||
|
"folder",
|
||||||
|
"overwrite",
|
||||||
|
"moderation",
|
||||||
|
"raw_convert",
|
||||||
|
"quality_override",
|
||||||
|
"quality_analysis",
|
||||||
|
"ocr",
|
||||||
|
"categorization",
|
||||||
|
"detection",
|
||||||
|
"similarity_search",
|
||||||
|
"background_removal",
|
||||||
|
"upload_preset",
|
||||||
|
"phash",
|
||||||
|
"return_delete_token",
|
||||||
|
"auto_tagging",
|
||||||
|
"async",
|
||||||
|
"cinemagraph_analysis",
|
||||||
|
]
|
||||||
|
|
||||||
|
__SERIALIZED_UPLOAD_PARAMS = [
|
||||||
|
"timestamp",
|
||||||
|
"transformation",
|
||||||
|
"headers",
|
||||||
|
"eager",
|
||||||
|
"tags",
|
||||||
|
"allowed_formats",
|
||||||
|
"face_coordinates",
|
||||||
|
"custom_coordinates",
|
||||||
|
"context",
|
||||||
|
"auto_tagging",
|
||||||
|
"responsive_breakpoints",
|
||||||
|
"access_control",
|
||||||
|
"metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
upload_params = __SIMPLE_UPLOAD_PARAMS + __SERIALIZED_UPLOAD_PARAMS
|
||||||
|
|
||||||
|
|
||||||
|
def compute_hex_hash(s):
|
||||||
|
"""
|
||||||
|
Compute hash and convert the result to HEX string
|
||||||
|
|
||||||
|
:param s: string to process
|
||||||
|
|
||||||
|
:return: HEX string
|
||||||
|
"""
|
||||||
|
return hashlib.sha1(to_bytes(s)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def build_array(arg):
|
def build_array(arg):
|
||||||
if isinstance(arg, list):
|
if isinstance(arg, list):
|
||||||
|
@ -133,6 +226,22 @@ def json_encode(value):
|
||||||
return json.dumps(value, default=__json_serializer, separators=(',', ':'))
|
return json.dumps(value, default=__json_serializer, separators=(',', ':'))
|
||||||
|
|
||||||
|
|
||||||
|
def patch_fetch_format(options):
|
||||||
|
"""
|
||||||
|
When upload type is fetch, remove the format options.
|
||||||
|
In addition, set the fetch_format options to the format value unless it was already set.
|
||||||
|
Mutates the options parameter!
|
||||||
|
|
||||||
|
:param options: URL and transformation options
|
||||||
|
"""
|
||||||
|
if options.get("type", "upload") != "fetch":
|
||||||
|
return
|
||||||
|
|
||||||
|
resource_format = options.pop("format", None)
|
||||||
|
if "fetch_format" not in options:
|
||||||
|
options["fetch_format"] = resource_format
|
||||||
|
|
||||||
|
|
||||||
def generate_transformation_string(**options):
|
def generate_transformation_string(**options):
|
||||||
responsive_width = options.pop("responsive_width", cloudinary.config().responsive_width)
|
responsive_width = options.pop("responsive_width", cloudinary.config().responsive_width)
|
||||||
size = options.pop("size", None)
|
size = options.pop("size", None)
|
||||||
|
@ -165,6 +274,7 @@ def generate_transformation_string(**options):
|
||||||
return generate_transformation_string(**bs)[0]
|
return generate_transformation_string(**bs)[0]
|
||||||
else:
|
else:
|
||||||
return generate_transformation_string(transformation=bs)[0]
|
return generate_transformation_string(transformation=bs)[0]
|
||||||
|
|
||||||
base_transformations = list(map(recurse, base_transformations))
|
base_transformations = list(map(recurse, base_transformations))
|
||||||
named_transformation = None
|
named_transformation = None
|
||||||
else:
|
else:
|
||||||
|
@ -186,11 +296,11 @@ def generate_transformation_string(**options):
|
||||||
flags = ".".join(build_array(options.pop("flags", None)))
|
flags = ".".join(build_array(options.pop("flags", None)))
|
||||||
dpr = options.pop("dpr", cloudinary.config().dpr)
|
dpr = options.pop("dpr", cloudinary.config().dpr)
|
||||||
duration = norm_range_value(options.pop("duration", None))
|
duration = norm_range_value(options.pop("duration", None))
|
||||||
start_offset = norm_range_value(options.pop("start_offset", None))
|
start_offset = norm_auto_range_value(options.pop("start_offset", None))
|
||||||
end_offset = norm_range_value(options.pop("end_offset", None))
|
end_offset = norm_range_value(options.pop("end_offset", None))
|
||||||
offset = split_range(options.pop("offset", None))
|
offset = split_range(options.pop("offset", None))
|
||||||
if offset:
|
if offset:
|
||||||
start_offset = norm_range_value(offset[0])
|
start_offset = norm_auto_range_value(offset[0])
|
||||||
end_offset = norm_range_value(offset[1])
|
end_offset = norm_range_value(offset[1])
|
||||||
|
|
||||||
video_codec = process_video_codec_param(options.pop("video_codec", None))
|
video_codec = process_video_codec_param(options.pop("video_codec", None))
|
||||||
|
@ -202,6 +312,9 @@ def generate_transformation_string(**options):
|
||||||
overlay = process_layer(options.pop("overlay", None), "overlay")
|
overlay = process_layer(options.pop("overlay", None), "overlay")
|
||||||
underlay = process_layer(options.pop("underlay", None), "underlay")
|
underlay = process_layer(options.pop("underlay", None), "underlay")
|
||||||
if_value = process_conditional(options.pop("if", None))
|
if_value = process_conditional(options.pop("if", None))
|
||||||
|
custom_function = process_custom_function(options.pop("custom_function", None))
|
||||||
|
custom_pre_function = process_custom_pre_function(options.pop("custom_pre_function", None))
|
||||||
|
fps = process_fps(options.pop("fps", None))
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"a": normalize_expression(angle),
|
"a": normalize_expression(angle),
|
||||||
|
@ -215,19 +328,22 @@ def generate_transformation_string(**options):
|
||||||
"e": normalize_expression(effect),
|
"e": normalize_expression(effect),
|
||||||
"eo": normalize_expression(end_offset),
|
"eo": normalize_expression(end_offset),
|
||||||
"fl": flags,
|
"fl": flags,
|
||||||
|
"fn": custom_function or custom_pre_function,
|
||||||
|
"fps": fps,
|
||||||
"h": normalize_expression(height),
|
"h": normalize_expression(height),
|
||||||
|
"ki": process_ki(options.pop("keyframe_interval", None)),
|
||||||
"l": overlay,
|
"l": overlay,
|
||||||
"o": normalize_expression(options.pop('opacity',None)),
|
"o": normalize_expression(options.pop('opacity', None)),
|
||||||
"q": normalize_expression(options.pop('quality',None)),
|
"q": normalize_expression(options.pop('quality', None)),
|
||||||
"r": normalize_expression(options.pop('radius',None)),
|
"r": process_radius(options.pop('radius', None)),
|
||||||
"so": normalize_expression(start_offset),
|
"so": normalize_expression(start_offset),
|
||||||
"t": named_transformation,
|
"t": named_transformation,
|
||||||
"u": underlay,
|
"u": underlay,
|
||||||
"w": normalize_expression(width),
|
"w": normalize_expression(width),
|
||||||
"x": normalize_expression(options.pop('x',None)),
|
"x": normalize_expression(options.pop('x', None)),
|
||||||
"y": normalize_expression(options.pop('y',None)),
|
"y": normalize_expression(options.pop('y', None)),
|
||||||
"vc": video_codec,
|
"vc": video_codec,
|
||||||
"z": normalize_expression(options.pop('zoom',None))
|
"z": normalize_expression(options.pop('zoom', None))
|
||||||
}
|
}
|
||||||
simple_params = {
|
simple_params = {
|
||||||
"ac": "audio_codec",
|
"ac": "audio_codec",
|
||||||
|
@ -239,7 +355,6 @@ def generate_transformation_string(**options):
|
||||||
"dn": "density",
|
"dn": "density",
|
||||||
"f": "fetch_format",
|
"f": "fetch_format",
|
||||||
"g": "gravity",
|
"g": "gravity",
|
||||||
"ki": "keyframe_interval",
|
|
||||||
"p": "prefix",
|
"p": "prefix",
|
||||||
"pg": "page",
|
"pg": "page",
|
||||||
"sp": "streaming_profile",
|
"sp": "streaming_profile",
|
||||||
|
@ -249,9 +364,9 @@ def generate_transformation_string(**options):
|
||||||
for param, option in simple_params.items():
|
for param, option in simple_params.items():
|
||||||
params[param] = options.pop(option, None)
|
params[param] = options.pop(option, None)
|
||||||
|
|
||||||
variables = options.pop('variables',{})
|
variables = options.pop('variables', {})
|
||||||
var_params = []
|
var_params = []
|
||||||
for key,value in options.items():
|
for key, value in options.items():
|
||||||
if re.match(r'^\$', key):
|
if re.match(r'^\$', key):
|
||||||
var_params.append(u"{0}_{1}".format(key, normalize_expression(str(value))))
|
var_params.append(u"{0}_{1}".format(key, normalize_expression(str(value))))
|
||||||
|
|
||||||
|
@ -261,7 +376,6 @@ def generate_transformation_string(**options):
|
||||||
for var in variables:
|
for var in variables:
|
||||||
var_params.append(u"{0}_{1}".format(var[0], normalize_expression(str(var[1]))))
|
var_params.append(u"{0}_{1}".format(var[0], normalize_expression(str(var[1]))))
|
||||||
|
|
||||||
|
|
||||||
variables = ','.join(var_params)
|
variables = ','.join(var_params)
|
||||||
|
|
||||||
sorted_params = sorted([param + "_" + str(value) for param, value in params.items() if (value or value == 0)])
|
sorted_params = sorted([param + "_" + str(value) for param, value in params.items() if (value or value == 0)])
|
||||||
|
@ -270,10 +384,14 @@ def generate_transformation_string(**options):
|
||||||
|
|
||||||
if if_value is not None:
|
if if_value is not None:
|
||||||
sorted_params.insert(0, "if_" + str(if_value))
|
sorted_params.insert(0, "if_" + str(if_value))
|
||||||
|
|
||||||
|
if "raw_transformation" in options and (options["raw_transformation"] or options["raw_transformation"] == 0):
|
||||||
|
sorted_params.append(options.pop("raw_transformation"))
|
||||||
|
|
||||||
transformation = ",".join(sorted_params)
|
transformation = ",".join(sorted_params)
|
||||||
if "raw_transformation" in options:
|
|
||||||
transformation = transformation + "," + options.pop("raw_transformation")
|
|
||||||
transformations = base_transformations + [transformation]
|
transformations = base_transformations + [transformation]
|
||||||
|
|
||||||
if responsive_width:
|
if responsive_width:
|
||||||
responsive_width_transformation = cloudinary.config().responsive_width_transformation \
|
responsive_width_transformation = cloudinary.config().responsive_width_transformation \
|
||||||
or DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION
|
or DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION
|
||||||
|
@ -287,6 +405,31 @@ def generate_transformation_string(**options):
|
||||||
return url, options
|
return url, options
|
||||||
|
|
||||||
|
|
||||||
|
def chain_transformations(options, transformations):
|
||||||
|
"""
|
||||||
|
Helper function, allows chaining transformations to the end of transformations list
|
||||||
|
|
||||||
|
The result of this function is an updated options parameter
|
||||||
|
|
||||||
|
:param options: Original options
|
||||||
|
:param transformations: Transformations to chain at the end
|
||||||
|
|
||||||
|
:return: Resulting options
|
||||||
|
"""
|
||||||
|
|
||||||
|
transformations = copy.deepcopy(transformations)
|
||||||
|
|
||||||
|
transformations = build_array(transformations)
|
||||||
|
# preserve url options
|
||||||
|
url_options = dict((o, options[o]) for o in __URL_KEYS if o in options)
|
||||||
|
|
||||||
|
transformations.insert(0, options)
|
||||||
|
|
||||||
|
url_options["transformation"] = transformations
|
||||||
|
|
||||||
|
return url_options
|
||||||
|
|
||||||
|
|
||||||
def is_fraction(width):
|
def is_fraction(width):
|
||||||
width = str(width)
|
width = str(width)
|
||||||
return re.match(FLOAT_RE, width) and float(width) < 1
|
return re.match(FLOAT_RE, width) and float(width) < 1
|
||||||
|
@ -302,18 +445,26 @@ def split_range(range):
|
||||||
|
|
||||||
|
|
||||||
def norm_range_value(value):
|
def norm_range_value(value):
|
||||||
if value is None: return None
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
match = re.match(RANGE_VALUE_RE, str(value))
|
match = re.match(RANGE_VALUE_RE, str(value))
|
||||||
|
|
||||||
if match is None: return None
|
if match is None:
|
||||||
|
return None
|
||||||
|
|
||||||
modifier = ''
|
modifier = ''
|
||||||
if match.group('modifier') is not None:
|
if match.group('modifier') is not None:
|
||||||
modifier = 'p'
|
modifier = 'p'
|
||||||
return match.group('value') + modifier
|
return match.group('value') + modifier
|
||||||
|
|
||||||
|
|
||||||
|
def norm_auto_range_value(value):
|
||||||
|
if value == "auto":
|
||||||
|
return value
|
||||||
|
return norm_range_value(value)
|
||||||
|
|
||||||
|
|
||||||
def process_video_codec_param(param):
|
def process_video_codec_param(param):
|
||||||
out_param = param
|
out_param = param
|
||||||
if isinstance(out_param, dict):
|
if isinstance(out_param, dict):
|
||||||
|
@ -325,15 +476,29 @@ def process_video_codec_param(param):
|
||||||
return out_param
|
return out_param
|
||||||
|
|
||||||
|
|
||||||
|
def process_radius(param):
|
||||||
|
if param is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(param, (list, tuple)):
|
||||||
|
if not 1 <= len(param) <= 4:
|
||||||
|
raise ValueError("Invalid radius param")
|
||||||
|
return ':'.join(normalize_expression(t) for t in param)
|
||||||
|
|
||||||
|
return str(param)
|
||||||
|
|
||||||
|
|
||||||
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 == ""])
|
||||||
|
|
||||||
|
|
||||||
def sign_request(params, options):
|
def sign_request(params, options):
|
||||||
api_key = options.get("api_key", cloudinary.config().api_key)
|
api_key = options.get("api_key", cloudinary.config().api_key)
|
||||||
if not api_key: raise ValueError("Must supply api_key")
|
if not api_key:
|
||||||
|
raise ValueError("Must supply api_key")
|
||||||
api_secret = options.get("api_secret", cloudinary.config().api_secret)
|
api_secret = options.get("api_secret", cloudinary.config().api_secret)
|
||||||
if not api_secret: raise ValueError("Must supply api_secret")
|
if not api_secret:
|
||||||
|
raise ValueError("Must supply api_secret")
|
||||||
|
|
||||||
params = cleanup_params(params)
|
params = cleanup_params(params)
|
||||||
params["signature"] = api_sign_request(params, api_secret)
|
params["signature"] = api_sign_request(params, api_secret)
|
||||||
|
@ -345,7 +510,7 @@ def sign_request(params, options):
|
||||||
def api_sign_request(params_to_sign, api_secret):
|
def api_sign_request(params_to_sign, api_secret):
|
||||||
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 hashlib.sha1(to_bytes(to_sign + api_secret)).hexdigest()
|
return compute_hex_hash(to_sign + api_secret)
|
||||||
|
|
||||||
|
|
||||||
def breakpoint_settings_mapper(breakpoint_settings):
|
def breakpoint_settings_mapper(breakpoint_settings):
|
||||||
|
@ -370,11 +535,13 @@ def finalize_source(source, format, url_suffix):
|
||||||
source_to_sign = source
|
source_to_sign = source
|
||||||
else:
|
else:
|
||||||
source = unquote(source)
|
source = unquote(source)
|
||||||
if not PY3: source = source.encode('utf8')
|
if not PY3:
|
||||||
|
source = source.encode('utf8')
|
||||||
source = smart_escape(source)
|
source = smart_escape(source)
|
||||||
source_to_sign = source
|
source_to_sign = source
|
||||||
if url_suffix is not None:
|
if url_suffix is not None:
|
||||||
if re.search(r'[\./]', url_suffix): raise ValueError("url_suffix should not include . or /")
|
if re.search(r'[\./]', url_suffix):
|
||||||
|
raise ValueError("url_suffix should not include . or /")
|
||||||
source = source + "/" + url_suffix
|
source = source + "/" + url_suffix
|
||||||
if format is not None:
|
if format is not None:
|
||||||
source = source + "." + format
|
source = source + "." + format
|
||||||
|
@ -396,7 +563,8 @@ def finalize_resource_type(resource_type, type, url_suffix, use_root_path, short
|
||||||
raise ValueError("URL Suffix only supported for image/upload and raw/upload")
|
raise ValueError("URL Suffix only supported for image/upload and raw/upload")
|
||||||
|
|
||||||
if use_root_path:
|
if use_root_path:
|
||||||
if (resource_type == "image" and upload_type == "upload") or (resource_type == "images" and upload_type is None):
|
if (resource_type == "image" and upload_type == "upload") or (
|
||||||
|
resource_type == "images" and upload_type is None):
|
||||||
resource_type = None
|
resource_type = None
|
||||||
upload_type = None
|
upload_type = None
|
||||||
else:
|
else:
|
||||||
|
@ -409,28 +577,33 @@ def finalize_resource_type(resource_type, type, url_suffix, use_root_path, short
|
||||||
return resource_type, upload_type
|
return resource_type, upload_type
|
||||||
|
|
||||||
|
|
||||||
def unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure,
|
def unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain,
|
||||||
secure_distribution):
|
secure_cdn_subdomain, cname, secure, secure_distribution):
|
||||||
"""cdn_subdomain and secure_cdn_subdomain
|
"""cdn_subdomain and secure_cdn_subdomain
|
||||||
1) Customers in shared distribution (e.g. res.cloudinary.com)
|
1) Customers in shared distribution (e.g. res.cloudinary.com)
|
||||||
if cdn_domain is true uses res-[1-5].cloudinary.com for both http and https. Setting secure_cdn_subdomain to false disables this for https.
|
if cdn_domain is true uses res-[1-5].cloudinary.com for both http and https.
|
||||||
|
Setting secure_cdn_subdomain to false disables this for https.
|
||||||
2) Customers with private cdn
|
2) Customers with private cdn
|
||||||
if cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for http
|
if cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for http
|
||||||
if secure_cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for https (please contact support if you require this)
|
if secure_cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for https
|
||||||
|
(please contact support if you require this)
|
||||||
3) Customers with cname
|
3) Customers with cname
|
||||||
if cdn_domain is true uses a[1-5].cname for http. For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution."""
|
if cdn_domain is true uses a[1-5].cname for http. For https, uses the same naming scheme
|
||||||
|
as 1 for shared distribution and as 2 for private distribution."""
|
||||||
shared_domain = not private_cdn
|
shared_domain = not private_cdn
|
||||||
shard = __crc(source)
|
shard = __crc(source)
|
||||||
if secure:
|
if secure:
|
||||||
if secure_distribution is None or secure_distribution == cloudinary.OLD_AKAMAI_SHARED_CDN:
|
if secure_distribution is None or secure_distribution == cloudinary.OLD_AKAMAI_SHARED_CDN:
|
||||||
secure_distribution = cloud_name + "-res.cloudinary.com" if private_cdn else cloudinary.SHARED_CDN
|
secure_distribution = cloud_name + "-res.cloudinary.com" \
|
||||||
|
if private_cdn else cloudinary.SHARED_CDN
|
||||||
|
|
||||||
shared_domain = shared_domain or secure_distribution == cloudinary.SHARED_CDN
|
shared_domain = shared_domain or secure_distribution == cloudinary.SHARED_CDN
|
||||||
if secure_cdn_subdomain is None and shared_domain:
|
if secure_cdn_subdomain is None and shared_domain:
|
||||||
secure_cdn_subdomain = cdn_subdomain
|
secure_cdn_subdomain = cdn_subdomain
|
||||||
|
|
||||||
if secure_cdn_subdomain:
|
if secure_cdn_subdomain:
|
||||||
secure_distribution = re.sub('res.cloudinary.com', "res-" + shard + ".cloudinary.com", secure_distribution)
|
secure_distribution = re.sub('res.cloudinary.com', "res-" + shard + ".cloudinary.com",
|
||||||
|
secure_distribution)
|
||||||
|
|
||||||
prefix = "https://" + secure_distribution
|
prefix = "https://" + secure_distribution
|
||||||
elif cname:
|
elif cname:
|
||||||
|
@ -438,10 +611,12 @@ def unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain,
|
||||||
prefix = "http://" + subdomain + cname
|
prefix = "http://" + subdomain + cname
|
||||||
else:
|
else:
|
||||||
subdomain = cloud_name + "-res" if private_cdn else "res"
|
subdomain = cloud_name + "-res" if private_cdn else "res"
|
||||||
if cdn_subdomain: subdomain = subdomain + "-" + shard
|
if cdn_subdomain:
|
||||||
|
subdomain = subdomain + "-" + shard
|
||||||
prefix = "http://" + subdomain + ".cloudinary.com"
|
prefix = "http://" + subdomain + ".cloudinary.com"
|
||||||
|
|
||||||
if shared_domain: prefix += "/" + cloud_name
|
if shared_domain:
|
||||||
|
prefix += "/" + cloud_name
|
||||||
|
|
||||||
return prefix
|
return prefix
|
||||||
|
|
||||||
|
@ -460,16 +635,23 @@ def merge(*dict_args):
|
||||||
def cloudinary_url(source, **options):
|
def cloudinary_url(source, **options):
|
||||||
original_source = source
|
original_source = source
|
||||||
|
|
||||||
|
patch_fetch_format(options)
|
||||||
type = options.pop("type", "upload")
|
type = options.pop("type", "upload")
|
||||||
if type == 'fetch':
|
|
||||||
options["fetch_format"] = options.get("fetch_format", options.pop("format", None))
|
|
||||||
transformation, options = generate_transformation_string(**options)
|
transformation, options = generate_transformation_string(**options)
|
||||||
|
|
||||||
resource_type = options.pop("resource_type", "image")
|
resource_type = options.pop("resource_type", "image")
|
||||||
|
|
||||||
|
force_version = options.pop("force_version", cloudinary.config().force_version)
|
||||||
|
if force_version is None:
|
||||||
|
force_version = True
|
||||||
|
|
||||||
version = options.pop("version", None)
|
version = options.pop("version", None)
|
||||||
|
|
||||||
format = options.pop("format", None)
|
format = options.pop("format", None)
|
||||||
cdn_subdomain = options.pop("cdn_subdomain", cloudinary.config().cdn_subdomain)
|
cdn_subdomain = options.pop("cdn_subdomain", cloudinary.config().cdn_subdomain)
|
||||||
secure_cdn_subdomain = options.pop("secure_cdn_subdomain", cloudinary.config().secure_cdn_subdomain)
|
secure_cdn_subdomain = options.pop("secure_cdn_subdomain",
|
||||||
|
cloudinary.config().secure_cdn_subdomain)
|
||||||
cname = options.pop("cname", cloudinary.config().cname)
|
cname = options.pop("cname", cloudinary.config().cname)
|
||||||
shorten = options.pop("shorten", cloudinary.config().shorten)
|
shorten = options.pop("shorten", cloudinary.config().shorten)
|
||||||
|
|
||||||
|
@ -478,7 +660,8 @@ def cloudinary_url(source, **options):
|
||||||
raise ValueError("Must supply cloud_name in tag or in configuration")
|
raise ValueError("Must supply cloud_name in tag or in configuration")
|
||||||
secure = options.pop("secure", cloudinary.config().secure)
|
secure = options.pop("secure", cloudinary.config().secure)
|
||||||
private_cdn = options.pop("private_cdn", cloudinary.config().private_cdn)
|
private_cdn = options.pop("private_cdn", cloudinary.config().private_cdn)
|
||||||
secure_distribution = options.pop("secure_distribution", cloudinary.config().secure_distribution)
|
secure_distribution = options.pop("secure_distribution",
|
||||||
|
cloudinary.config().secure_distribution)
|
||||||
sign_url = options.pop("sign_url", cloudinary.config().sign_url)
|
sign_url = options.pop("sign_url", cloudinary.config().sign_url)
|
||||||
api_secret = options.pop("api_secret", cloudinary.config().api_secret)
|
api_secret = options.pop("api_secret", cloudinary.config().api_secret)
|
||||||
url_suffix = options.pop("url_suffix", None)
|
url_suffix = options.pop("url_suffix", None)
|
||||||
|
@ -490,15 +673,19 @@ def cloudinary_url(source, **options):
|
||||||
if (not source) or type == "upload" and re.match(r'^https?:', source):
|
if (not source) or type == "upload" and re.match(r'^https?:', source):
|
||||||
return original_source, options
|
return original_source, options
|
||||||
|
|
||||||
resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
|
resource_type, type = finalize_resource_type(
|
||||||
|
resource_type, type, url_suffix, use_root_path, shorten)
|
||||||
source, source_to_sign = finalize_source(source, format, url_suffix)
|
source, source_to_sign = finalize_source(source, format, url_suffix)
|
||||||
|
|
||||||
if source_to_sign.find("/") >= 0 \
|
if not version and force_version \
|
||||||
|
and source_to_sign.find("/") >= 0 \
|
||||||
and not re.match(r'^https?:/', source_to_sign) \
|
and not re.match(r'^https?:/', source_to_sign) \
|
||||||
and not re.match(r'^v[0-9]+', source_to_sign) \
|
and not re.match(r'^v[0-9]+', source_to_sign):
|
||||||
and not version:
|
|
||||||
version = "1"
|
version = "1"
|
||||||
if version: version = "v" + str(version)
|
if version:
|
||||||
|
version = "v" + str(version)
|
||||||
|
else:
|
||||||
|
version = None
|
||||||
|
|
||||||
transformation = re.sub(r'([^:])/+', r'\1/', transformation)
|
transformation = re.sub(r'([^:])/+', r'\1/', transformation)
|
||||||
|
|
||||||
|
@ -506,35 +693,84 @@ def cloudinary_url(source, **options):
|
||||||
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]))
|
||||||
signature = "s--" + to_string(
|
signature = "s--" + to_string(
|
||||||
base64.urlsafe_b64encode(hashlib.sha1(to_bytes(to_sign + api_secret)).digest())[0:8]) + "--"
|
base64.urlsafe_b64encode(
|
||||||
|
hashlib.sha1(to_bytes(to_sign + api_secret)).digest())[0:8]) + "--"
|
||||||
|
|
||||||
prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname,
|
prefix = unsigned_download_url_prefix(
|
||||||
secure, secure_distribution)
|
source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain,
|
||||||
source = "/".join(__compact([prefix, resource_type, type, signature, transformation, version, source]))
|
cname, secure, secure_distribution)
|
||||||
|
source = "/".join(__compact(
|
||||||
|
[prefix, resource_type, type, signature, transformation, version, source]))
|
||||||
if sign_url and auth_token:
|
if sign_url and auth_token:
|
||||||
path = urlparse(source).path
|
path = urlparse(source).path
|
||||||
token = cloudinary.auth_token.generate( **merge(auth_token, {"url": path}))
|
token = cloudinary.auth_token.generate(**merge(auth_token, {"url": path}))
|
||||||
source = "%s?%s" % (source, token)
|
source = "%s?%s" % (source, token)
|
||||||
return source, options
|
return source, options
|
||||||
|
|
||||||
|
|
||||||
def cloudinary_api_url(action='upload', **options):
|
def cloudinary_api_url(action='upload', **options):
|
||||||
cloudinary_prefix = options.get("upload_prefix", cloudinary.config().upload_prefix) or "https://api.cloudinary.com"
|
cloudinary_prefix = options.get("upload_prefix", cloudinary.config().upload_prefix)\
|
||||||
|
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: raise ValueError("Must supply cloud_name")
|
if not cloud_name:
|
||||||
|
raise ValueError("Must supply cloud_name")
|
||||||
resource_type = options.get("resource_type", "image")
|
resource_type = options.get("resource_type", "image")
|
||||||
return "/".join([cloudinary_prefix, "v1_1", cloud_name, resource_type, action])
|
|
||||||
|
return encode_unicode_url("/".join([cloudinary_prefix, "v1_1", cloud_name, resource_type, action]))
|
||||||
|
|
||||||
|
|
||||||
# Based on ruby's CGI::unescape. In addition does not escape / :
|
def cloudinary_scaled_url(source, width, transformation, options):
|
||||||
def smart_escape(source,unsafe = r"([^a-zA-Z0-9_.\-\/:]+)"):
|
"""
|
||||||
|
Generates a cloudinary url scaled to specified width.
|
||||||
|
|
||||||
|
:param source: The resource
|
||||||
|
:param width: Width in pixels of the srcset item
|
||||||
|
:param transformation: Custom transformation that overrides transformations provided in options
|
||||||
|
:param options: A dict with additional options
|
||||||
|
|
||||||
|
:return: Resulting URL of the item
|
||||||
|
"""
|
||||||
|
|
||||||
|
# preserve options from being destructed
|
||||||
|
options = copy.deepcopy(options)
|
||||||
|
|
||||||
|
if transformation:
|
||||||
|
if isinstance(transformation, string_types):
|
||||||
|
transformation = {"raw_transformation": transformation}
|
||||||
|
|
||||||
|
# Remove all transformation related options
|
||||||
|
options = dict((o, options[o]) for o in __URL_KEYS if o in options)
|
||||||
|
options.update(transformation)
|
||||||
|
|
||||||
|
scale_transformation = {"crop": "scale", "width": width}
|
||||||
|
|
||||||
|
url_options = options
|
||||||
|
patch_fetch_format(url_options)
|
||||||
|
url_options = chain_transformations(url_options, scale_transformation)
|
||||||
|
|
||||||
|
return cloudinary_url(source, **url_options)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def smart_escape(source, unsafe=r"([^a-zA-Z0-9_.\-\/:]+)"):
|
||||||
|
"""
|
||||||
|
Based on ruby's CGI::unescape. In addition does not escape / :
|
||||||
|
|
||||||
|
:param source: Source string to escape
|
||||||
|
:param unsafe: Unsafe characters
|
||||||
|
|
||||||
|
:return: Escaped string
|
||||||
|
"""
|
||||||
def pack(m):
|
def pack(m):
|
||||||
return to_bytes('%' + "%".join(["%02X" % x for x in struct.unpack('B' * len(m.group(1)), m.group(1))]).upper())
|
return to_bytes('%' + "%".join(
|
||||||
|
["%02X" % x for x in struct.unpack('B' * len(m.group(1)), m.group(1))]
|
||||||
|
).upper())
|
||||||
|
|
||||||
return to_string(re.sub(to_bytes(unsafe), pack, to_bytes(source)))
|
return to_string(re.sub(to_bytes(unsafe), pack, to_bytes(source)))
|
||||||
|
|
||||||
|
|
||||||
def random_public_id():
|
def random_public_id():
|
||||||
return ''.join(random.SystemRandom().choice(string.ascii_lowercase + string.digits) for _ in range(16))
|
return ''.join(random.SystemRandom().choice(string.ascii_lowercase + string.digits)
|
||||||
|
for _ in range(16))
|
||||||
|
|
||||||
|
|
||||||
def signed_preloaded_image(result):
|
def signed_preloaded_image(result):
|
||||||
|
@ -584,7 +820,8 @@ def download_archive_url(**options):
|
||||||
params = options.copy()
|
params = options.copy()
|
||||||
params.update(mode="download")
|
params.update(mode="download")
|
||||||
cloudinary_params = sign_request(archive_params(**params), options)
|
cloudinary_params = sign_request(archive_params(**params), options)
|
||||||
return cloudinary_api_url("generate_archive", **options) + "?" + urlencode(bracketize_seq(cloudinary_params), True)
|
return cloudinary_api_url("generate_archive", **options) + "?" + \
|
||||||
|
urlencode(bracketize_seq(cloudinary_params), True)
|
||||||
|
|
||||||
|
|
||||||
def download_zip_url(**options):
|
def download_zip_url(**options):
|
||||||
|
@ -592,10 +829,12 @@ def download_zip_url(**options):
|
||||||
new_options.update(target_format="zip")
|
new_options.update(target_format="zip")
|
||||||
return download_archive_url(**new_options)
|
return download_archive_url(**new_options)
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
def archive_params(**options):
|
def archive_params(**options):
|
||||||
if options.get("timestamp") is None:
|
if options.get("timestamp") is None:
|
||||||
timestamp = now()
|
timestamp = now()
|
||||||
|
@ -613,6 +852,8 @@ def archive_params(**options):
|
||||||
"phash": options.get("phash"),
|
"phash": options.get("phash"),
|
||||||
"prefixes": options.get("prefixes") and build_array(options.get("prefixes")),
|
"prefixes": options.get("prefixes") and build_array(options.get("prefixes")),
|
||||||
"public_ids": options.get("public_ids") and build_array(options.get("public_ids")),
|
"public_ids": options.get("public_ids") and build_array(options.get("public_ids")),
|
||||||
|
"fully_qualified_public_ids": options.get("fully_qualified_public_ids") and build_array(
|
||||||
|
options.get("fully_qualified_public_ids")),
|
||||||
"skip_transformation_name": options.get("skip_transformation_name"),
|
"skip_transformation_name": options.get("skip_transformation_name"),
|
||||||
"tags": options.get("tags") and build_array(options.get("tags")),
|
"tags": options.get("tags") and build_array(options.get("tags")),
|
||||||
"target_format": options.get("target_format"),
|
"target_format": options.get("target_format"),
|
||||||
|
@ -629,15 +870,32 @@ def archive_params(**options):
|
||||||
def build_eager(transformations):
|
def build_eager(transformations):
|
||||||
if transformations is None:
|
if transformations is None:
|
||||||
return None
|
return None
|
||||||
eager = []
|
|
||||||
for tr in build_array(transformations):
|
return "|".join([build_single_eager(et) for et in build_array(transformations)])
|
||||||
if isinstance(tr, string_types):
|
|
||||||
single_eager = tr
|
|
||||||
else:
|
def build_single_eager(options):
|
||||||
ext = tr.get("format")
|
"""
|
||||||
single_eager = "/".join([x for x in [generate_transformation_string(**tr)[0], ext] if x])
|
Builds a single eager transformation which consists of transformation and (optionally) format joined by "/"
|
||||||
eager.append(single_eager)
|
|
||||||
return "|".join(eager)
|
:param options: Options containing transformation parameters and (optionally) a "format" key
|
||||||
|
format can be a string value (jpg, gif, etc) or can be set to "" (empty string).
|
||||||
|
The latter leads to transformation ending with "/", which means "No extension, use original format"
|
||||||
|
If format is not provided or set to None, only transformation is used (without the trailing "/")
|
||||||
|
|
||||||
|
:return: Resulting eager transformation string
|
||||||
|
"""
|
||||||
|
if isinstance(options, string_types):
|
||||||
|
return options
|
||||||
|
|
||||||
|
trans_str = generate_transformation_string(**options)[0]
|
||||||
|
|
||||||
|
if not trans_str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
file_format = options.get("format")
|
||||||
|
|
||||||
|
return trans_str + ("/" + file_format if file_format is not None else "")
|
||||||
|
|
||||||
|
|
||||||
def build_custom_headers(headers):
|
def build_custom_headers(headers):
|
||||||
|
@ -653,49 +911,30 @@ def build_custom_headers(headers):
|
||||||
|
|
||||||
|
|
||||||
def build_upload_params(**options):
|
def build_upload_params(**options):
|
||||||
params = {"timestamp": now(),
|
params = {param_name: options.get(param_name) for param_name in __SIMPLE_UPLOAD_PARAMS}
|
||||||
"transformation": generate_transformation_string(**options)[0],
|
|
||||||
"public_id": options.get("public_id"),
|
serialized_params = {
|
||||||
"callback": options.get("callback"),
|
"timestamp": now(),
|
||||||
"format": options.get("format"),
|
"metadata": encode_context(options.get("metadata")),
|
||||||
"type": options.get("type"),
|
"transformation": generate_transformation_string(**options)[0],
|
||||||
"backup": options.get("backup"),
|
"headers": build_custom_headers(options.get("headers")),
|
||||||
"faces": options.get("faces"),
|
"eager": build_eager(options.get("eager")),
|
||||||
"image_metadata": options.get("image_metadata"),
|
"tags": options.get("tags") and ",".join(build_array(options["tags"])),
|
||||||
"exif": options.get("exif"),
|
"allowed_formats": options.get("allowed_formats") and ",".join(build_array(options["allowed_formats"])),
|
||||||
"colors": options.get("colors"),
|
"face_coordinates": encode_double_array(options.get("face_coordinates")),
|
||||||
"headers": build_custom_headers(options.get("headers")),
|
"custom_coordinates": encode_double_array(options.get("custom_coordinates")),
|
||||||
"eager": build_eager(options.get("eager")),
|
"context": encode_context(options.get("context")),
|
||||||
"use_filename": options.get("use_filename"),
|
"auto_tagging": options.get("auto_tagging") and str(options.get("auto_tagging")),
|
||||||
"unique_filename": options.get("unique_filename"),
|
"responsive_breakpoints": generate_responsive_breakpoints_string(options.get("responsive_breakpoints")),
|
||||||
"discard_original_filename": options.get("discard_original_filename"),
|
"access_control": options.get("access_control") and json_encode(
|
||||||
"invalidate": options.get("invalidate"),
|
build_list_of_dicts(options.get("access_control")))
|
||||||
"notification_url": options.get("notification_url"),
|
}
|
||||||
"eager_notification_url": options.get("eager_notification_url"),
|
|
||||||
"eager_async": options.get("eager_async"),
|
# make sure that we are in-sync with __SERIALIZED_UPLOAD_PARAMS which are in use by other methods
|
||||||
"proxy": options.get("proxy"),
|
serialized_params = {param_name: serialized_params[param_name] for param_name in __SERIALIZED_UPLOAD_PARAMS}
|
||||||
"folder": options.get("folder"),
|
|
||||||
"overwrite": options.get("overwrite"),
|
params.update(serialized_params)
|
||||||
"tags": options.get("tags") and ",".join(build_array(options["tags"])),
|
|
||||||
"allowed_formats": options.get("allowed_formats") and ",".join(build_array(options["allowed_formats"])),
|
|
||||||
"face_coordinates": encode_double_array(options.get("face_coordinates")),
|
|
||||||
"custom_coordinates": encode_double_array(options.get("custom_coordinates")),
|
|
||||||
"context": encode_context(options.get("context")),
|
|
||||||
"moderation": options.get("moderation"),
|
|
||||||
"raw_convert": options.get("raw_convert"),
|
|
||||||
"quality_override": options.get("quality_override"),
|
|
||||||
"ocr": options.get("ocr"),
|
|
||||||
"categorization": options.get("categorization"),
|
|
||||||
"detection": options.get("detection"),
|
|
||||||
"similarity_search": options.get("similarity_search"),
|
|
||||||
"background_removal": options.get("background_removal"),
|
|
||||||
"upload_preset": options.get("upload_preset"),
|
|
||||||
"phash": options.get("phash"),
|
|
||||||
"return_delete_token": options.get("return_delete_token"),
|
|
||||||
"auto_tagging": options.get("auto_tagging") and str(options.get("auto_tagging")),
|
|
||||||
"responsive_breakpoints": generate_responsive_breakpoints_string(options.get("responsive_breakpoints")),
|
|
||||||
"async": options.get("async"),
|
|
||||||
"access_control": options.get("access_control") and json_encode(build_list_of_dicts(options.get("access_control")))}
|
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
@ -716,6 +955,14 @@ def __process_text_options(layer, layer_parameter):
|
||||||
if line_spacing is not None:
|
if line_spacing is not None:
|
||||||
keywords.append("line_spacing_" + str(line_spacing))
|
keywords.append("line_spacing_" + str(line_spacing))
|
||||||
|
|
||||||
|
font_antialiasing = layer.get("font_antialiasing")
|
||||||
|
if font_antialiasing is not None:
|
||||||
|
keywords.append("antialias_" + str(font_antialiasing))
|
||||||
|
|
||||||
|
font_hinting = layer.get("font_hinting")
|
||||||
|
if font_hinting is not None:
|
||||||
|
keywords.append("hinting_" + str(font_hinting))
|
||||||
|
|
||||||
if font_size is None and font_family is None and len(keywords) == 0:
|
if font_size is None and font_family is None and len(keywords) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -778,12 +1025,12 @@ def process_layer(layer, layer_parameter):
|
||||||
|
|
||||||
if text is not None:
|
if text is not None:
|
||||||
var_pattern = VAR_NAME_RE
|
var_pattern = VAR_NAME_RE
|
||||||
match = re.findall(var_pattern,text)
|
match = re.findall(var_pattern, text)
|
||||||
|
|
||||||
parts= filter(lambda p: p is not None, re.split(var_pattern,text))
|
parts = filter(lambda p: p is not None, re.split(var_pattern, text))
|
||||||
encoded_text = []
|
encoded_text = []
|
||||||
for part in parts:
|
for part in parts:
|
||||||
if re.match(var_pattern,part):
|
if re.match(var_pattern, part):
|
||||||
encoded_text.append(part)
|
encoded_text.append(part)
|
||||||
else:
|
else:
|
||||||
encoded_text.append(smart_escape(smart_escape(part, r"([,/])")))
|
encoded_text.append(smart_escape(smart_escape(part, r"([,/])")))
|
||||||
|
@ -801,6 +1048,7 @@ def process_layer(layer, layer_parameter):
|
||||||
|
|
||||||
return ':'.join(components)
|
return ':'.join(components)
|
||||||
|
|
||||||
|
|
||||||
IF_OPERATORS = {
|
IF_OPERATORS = {
|
||||||
"=": 'eq',
|
"=": 'eq',
|
||||||
"!=": 'ne',
|
"!=": 'ne',
|
||||||
|
@ -813,7 +1061,8 @@ IF_OPERATORS = {
|
||||||
"*": 'mul',
|
"*": 'mul',
|
||||||
"/": 'div',
|
"/": 'div',
|
||||||
"+": 'add',
|
"+": 'add',
|
||||||
"-": 'sub'
|
"-": 'sub',
|
||||||
|
"^": 'pow'
|
||||||
}
|
}
|
||||||
|
|
||||||
PREDEFINED_VARS = {
|
PREDEFINED_VARS = {
|
||||||
|
@ -828,17 +1077,69 @@ PREDEFINED_VARS = {
|
||||||
"page_x": "px",
|
"page_x": "px",
|
||||||
"page_y": "py",
|
"page_y": "py",
|
||||||
"tags": "tags",
|
"tags": "tags",
|
||||||
"width": "w"
|
"width": "w",
|
||||||
|
"duration": "du",
|
||||||
|
"initial_duration": "idu",
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceRE = "((\\|\\||>=|<=|&&|!=|>|=|<|/|-|\\+|\\*)(?=[ _])|" + '|'.join(PREDEFINED_VARS.keys())+ ")"
|
replaceRE = "((\\|\\||>=|<=|&&|!=|>|=|<|/|-|\\+|\\*|\^)(?=[ _])|(?<!\$)(" + '|'.join(PREDEFINED_VARS.keys()) + "))"
|
||||||
|
|
||||||
|
|
||||||
def translate_if(match):
|
def translate_if(match):
|
||||||
name = match.group(0)
|
name = match.group(0)
|
||||||
return IF_OPERATORS.get(name,
|
return IF_OPERATORS.get(name,
|
||||||
PREDEFINED_VARS.get(name,
|
PREDEFINED_VARS.get(name,
|
||||||
name))
|
name))
|
||||||
|
|
||||||
|
|
||||||
|
def process_custom_function(custom_function):
|
||||||
|
if not isinstance(custom_function, dict):
|
||||||
|
return custom_function
|
||||||
|
|
||||||
|
function_type = custom_function.get("function_type")
|
||||||
|
source = custom_function.get("source")
|
||||||
|
if function_type == "remote":
|
||||||
|
source = base64url_encode(source)
|
||||||
|
|
||||||
|
return ":".join([function_type, source])
|
||||||
|
|
||||||
|
|
||||||
|
def process_custom_pre_function(custom_function):
|
||||||
|
value = process_custom_function(custom_function)
|
||||||
|
return "pre:{0}".format(value) if value else None
|
||||||
|
|
||||||
|
|
||||||
|
def process_fps(fps):
|
||||||
|
"""
|
||||||
|
Serializes fps transformation parameter
|
||||||
|
|
||||||
|
:param fps: A single number, a list of mixed type, a string, including open-ended and closed range values
|
||||||
|
Examples: '24-29.97', 24, 24.973, '-24', [24, 29.97]
|
||||||
|
|
||||||
|
:return: string
|
||||||
|
"""
|
||||||
|
if not isinstance(fps, (list, tuple)):
|
||||||
|
return fps
|
||||||
|
|
||||||
|
return "-".join(normalize_expression(f) for f in fps)
|
||||||
|
|
||||||
|
|
||||||
|
def process_ki(ki):
|
||||||
|
"""
|
||||||
|
Serializes keyframe_interval parameter
|
||||||
|
:param ki: Keyframe interval. Should be either a string or a positive real number.
|
||||||
|
:return: string
|
||||||
|
"""
|
||||||
|
if ki is None:
|
||||||
|
return None
|
||||||
|
if isinstance(ki, string_types):
|
||||||
|
return ki
|
||||||
|
if not isinstance(ki, Number):
|
||||||
|
raise ValueError("Keyframe interval should be a number or a string")
|
||||||
|
if ki <= 0:
|
||||||
|
raise ValueError("Keyframe interval should be greater than zero")
|
||||||
|
return str(float(ki))
|
||||||
|
|
||||||
|
|
||||||
def process_conditional(conditional):
|
def process_conditional(conditional):
|
||||||
if conditional is None:
|
if conditional is None:
|
||||||
|
@ -846,8 +1147,9 @@ def process_conditional(conditional):
|
||||||
result = normalize_expression(conditional)
|
result = normalize_expression(conditional)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def normalize_expression(expression):
|
def normalize_expression(expression):
|
||||||
if re.match(r'^!.+!$',str(expression)): # quoted string
|
if re.match(r'^!.+!$', str(expression)): # quoted string
|
||||||
return expression
|
return expression
|
||||||
elif expression:
|
elif expression:
|
||||||
result = str(expression)
|
result = str(expression)
|
||||||
|
@ -857,6 +1159,7 @@ def normalize_expression(expression):
|
||||||
else:
|
else:
|
||||||
return expression
|
return expression
|
||||||
|
|
||||||
|
|
||||||
def __join_pair(key, value):
|
def __join_pair(key, value):
|
||||||
if value is None or value == "":
|
if value is None or value == "":
|
||||||
return None
|
return None
|
||||||
|
@ -898,15 +1201,134 @@ def base64_encode_url(url):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = unquote(url)
|
url = unquote(url)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
url = smart_escape(url)
|
url = smart_escape(url)
|
||||||
b64 = base64.b64encode(url.encode('utf-8'))
|
b64 = base64.b64encode(url.encode('utf-8'))
|
||||||
return b64.decode('ascii')
|
return b64.decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
def base64url_encode(data):
|
||||||
|
"""
|
||||||
|
Url safe version of urlsafe_b64encode with stripped `=` sign at the end.
|
||||||
|
|
||||||
|
:param data: input data
|
||||||
|
|
||||||
|
:return: Base64 URL safe encoded string
|
||||||
|
"""
|
||||||
|
return to_string(base64.urlsafe_b64encode(to_bytes(data)))
|
||||||
|
|
||||||
|
|
||||||
|
def encode_unicode_url(url_str):
|
||||||
|
"""
|
||||||
|
Quote and encode possible unicode url string (applicable for python2)
|
||||||
|
|
||||||
|
:param url_str: Url string to encode
|
||||||
|
|
||||||
|
:return: Encoded string
|
||||||
|
"""
|
||||||
|
if six.PY2:
|
||||||
|
url_str = urllib.quote(url_str.encode('utf-8'), ":/?#[]@!$&'()*+,;=")
|
||||||
|
|
||||||
|
return url_str
|
||||||
|
|
||||||
|
|
||||||
def __json_serializer(obj):
|
def __json_serializer(obj):
|
||||||
"""JSON serializer for objects not serializable by default json code"""
|
"""JSON serializer for objects not serializable by default json code"""
|
||||||
if isinstance(obj, (datetime, date)):
|
if isinstance(obj, (datetime, date)):
|
||||||
return obj.isoformat()
|
return obj.isoformat()
|
||||||
raise TypeError("Object of type %s is not JSON serializable" % type(obj))
|
raise TypeError("Object of type %s is not JSON serializable" % type(obj))
|
||||||
|
|
||||||
|
|
||||||
|
def is_remote_url(file):
|
||||||
|
"""Basic URL scheme check to define if it's remote URL"""
|
||||||
|
return isinstance(file, string_types) and re.match(REMOTE_URL_RE, file)
|
||||||
|
|
||||||
|
|
||||||
|
def file_io_size(file_io):
|
||||||
|
"""
|
||||||
|
Helper function for getting file-like object size(suitable for both files and streams)
|
||||||
|
|
||||||
|
:param file_io: io.IOBase
|
||||||
|
|
||||||
|
:return: size
|
||||||
|
"""
|
||||||
|
initial_position = file_io.tell()
|
||||||
|
file_io.seek(0, os.SEEK_END)
|
||||||
|
size = file_io.tell()
|
||||||
|
file_io.seek(initial_position, os.SEEK_SET)
|
||||||
|
|
||||||
|
return size
|
||||||
|
|
||||||
|
|
||||||
|
def check_property_enabled(f):
|
||||||
|
"""
|
||||||
|
Used as a class method decorator to check whether class is enabled(self.enabled is True)
|
||||||
|
|
||||||
|
:param f: function to call
|
||||||
|
|
||||||
|
:return: None if not enabled, otherwise calls function f
|
||||||
|
"""
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not args[0].enabled:
|
||||||
|
return None
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def verify_api_response_signature(public_id, version, 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 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
|
||||||
|
|
||||||
|
:return: Boolean result of the validation
|
||||||
|
"""
|
||||||
|
if not cloudinary.config().api_secret:
|
||||||
|
raise Exception('Api secret key is empty')
|
||||||
|
|
||||||
|
parameters_to_sign = {'public_id': public_id,
|
||||||
|
'version': version}
|
||||||
|
|
||||||
|
return signature == api_sign_request(parameters_to_sign, cloudinary.config().api_secret)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_notification_signature(body, timestamp, signature, valid_for=7200):
|
||||||
|
"""
|
||||||
|
Verifies the authenticity of a notification signature
|
||||||
|
|
||||||
|
:param body: Json of the request's body
|
||||||
|
: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 valid_for: The desired time in seconds for considering the request valid
|
||||||
|
|
||||||
|
:return: Boolean result of the validation
|
||||||
|
"""
|
||||||
|
if not cloudinary.config().api_secret:
|
||||||
|
raise Exception('Api secret key is empty')
|
||||||
|
|
||||||
|
if timestamp < time.time() - valid_for:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not isinstance(body, str):
|
||||||
|
raise ValueError('Body should be type of string')
|
||||||
|
|
||||||
|
return signature == compute_hex_hash('{}{}{}'.format(body, timestamp, cloudinary.config().api_secret))
|
||||||
|
|
||||||
|
|
||||||
|
def get_http_connector(conf, options):
|
||||||
|
"""
|
||||||
|
Used to create http connector, depends on api_proxy configuration parameter
|
||||||
|
|
||||||
|
:param conf: configuration object
|
||||||
|
:param options: additional options
|
||||||
|
|
||||||
|
:return: ProxyManager if api_proxy is set, otherwise PoolManager object
|
||||||
|
"""
|
||||||
|
if conf.api_proxy:
|
||||||
|
return ProxyManager(conf.api_proxy, **options)
|
||||||
|
else:
|
||||||
|
return PoolManager(**options)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue