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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 42 additions & 30 deletions cloudinary/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from __future__ import absolute_import

import os
import re
import logging
import numbers

from math import ceil

import os
import re

from six import python_2_unicode_compatible, string_types


Expand All @@ -17,13 +16,12 @@
ch.setFormatter(formatter)
logger.addHandler(ch)

from platform import python_version


from cloudinary import utils
from cloudinary.compat import urlparse, parse_qs
from cloudinary.search import Search

from platform import python_version

CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"
OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"
AKAMAI_SHARED_CDN = "res.cloudinary.com"
Expand All @@ -48,7 +46,8 @@


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.

:returns: the user agent
Expand Down Expand Up @@ -136,7 +135,7 @@ def _is_nested_key(self, key):

def _put_nested_key(self, key, value):
chain = re.split(r'[\[\]]+', key)
chain = [key for key in chain if key]
chain = [k for k in chain if k]
outer = self.__dict__
last_key = chain.pop()
for inner_key in chain:
Expand Down Expand Up @@ -196,9 +195,11 @@ def get_prep_value(self):
return None
prep = ''
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
if self.format: prep = prep + '.' + self.format
if self.format:
prep = prep + '.' + self.format
return prep

def get_presigned(self):
Expand Down Expand Up @@ -407,30 +408,37 @@ def video_thumbnail(self, **options):
self.default_poster_options(options)
return self.build_url(**options)

# Creates an HTML video tag for the provided +source+
#
# ==== Options
# * <tt>source_types</tt> - Specify which source type the tag should include. defaults to webm, mp4 and ogv.
# * <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+
#
# ==== 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):
"""
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>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("\.({0})$".format("|".join(self.default_source_types())), '', public_id)
source = re.sub(r"\.({0})$".format("|".join(self.default_source_types())), '', public_id)

source_types = options.pop('source_types', [])
source_transformation = options.pop('source_transformation', {})
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()
if not source_types:
source_types = self.default_source_types()
video_options = options.copy()

if 'poster' in video_options:
Expand All @@ -439,11 +447,13 @@ def video(self, **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)
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 not video_options['poster']:
del video_options['poster']

nested_source_types = isinstance(source_types, list) and len(source_types) > 1
if not nested_source_types:
Expand All @@ -453,8 +463,10 @@ def video(self, **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')
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 nested_source_types:
Expand Down
93 changes: 64 additions & 29 deletions cloudinary/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,47 @@
import json
import socket

import cloudinary
import urllib3
from six import string_types
from urllib3.exceptions import HTTPError

import urllib3
import certifi

import cloudinary
from cloudinary import utils
from urllib3.exceptions import HTTPError

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

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 = {
Expand All @@ -45,6 +66,7 @@ def __init__(self, result, response, **kwargs):
self.rate_limit_reset_at = email.utils.parsedate(response.headers["x-featureratelimit-reset"])
self.rate_limit_remaining = int(response.headers["x-featureratelimit-remaining"])


_http = urllib3.PoolManager(
cert_reqs='CERT_REQUIRED',
ca_certs=certifi.where()
Expand All @@ -67,23 +89,26 @@ def resources(**options):
resource_type = options.pop("resource_type", "image")
upload_type = options.pop("type", None)
uri = ["resources", resource_type]
if upload_type: uri.append(upload_type)
params = only(options,
"next_cursor", "max_results", "prefix", "tags", "context", "moderations", "direction", "start_at")
if upload_type:
uri.append(upload_type)
params = only(options, "next_cursor", "max_results", "prefix", "tags",
"context", "moderations", "direction", "start_at")
return call_api("get", uri, params, **options)


def resources_by_tag(tag, **options):
resource_type = options.pop("resource_type", "image")
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)


def resources_by_moderation(kind, status, **options):
resource_type = options.pop("resource_type", "image")
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)


Expand All @@ -99,7 +124,8 @@ def resource(public_id, **options):
resource_type = options.pop("resource_type", "image")
upload_type = options.pop("type", "upload")
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",
"pages", "phash", "coordinates", "max_results")
return call_api("get", uri, params, **options)


Expand All @@ -114,9 +140,11 @@ def update(public_id, **options):
if "tags" in options:
params["tags"] = ",".join(utils.build_array(options["tags"]))
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:
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:
params["context"] = utils.encode_context(options.get("context"))
if "auto_tagging" in options:
Expand Down Expand Up @@ -215,14 +243,15 @@ def delete_transformation(transformation, **options):
return call_api("delete", uri, {}, **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):
uri = ["transformations", transformation_string(transformation)]
updates = only(options, "allowed_for_strict")
if "unsafe_update" in options:
updates["unsafe_update"] = transformation_string(options.get("unsafe_update"))
if not updates: raise Exception("No updates given")

if not updates:
raise Exception("No updates given")
return call_api("put", uri, updates, **options)


Expand Down Expand Up @@ -361,7 +390,8 @@ def update_streaming_profile(name, **options):
def call_json_api(method, uri, jsonBody, **options):
logger.debug(jsonBody)
data = json.dumps(jsonBody).encode('utf-8')
return _call_api(method, uri, body=data, headers={'Content-Type': 'application/json'}, **options)
return _call_api(method, uri, body=data,
headers={'Content-Type': 'application/json'}, **options)


def call_api(method, uri, params, **options):
Expand All @@ -372,11 +402,14 @@ def _call_api(method, uri, params=None, body=None, headers=None, **options):
prefix = options.pop("upload_prefix",
cloudinary.config().upload_prefix) or "https://api.cloudinary.com"
cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name)
if not cloud_name: raise Exception("Must supply cloud_name")
if not cloud_name:
raise Exception("Must supply cloud_name")
api_key = options.pop("api_key", cloudinary.config().api_key)
if not api_key: raise Exception("Must supply api_key")
if not api_key:
raise Exception("Must supply api_key")
api_secret = options.pop("api_secret", cloudinary.config().api_secret)
if not cloud_name: raise Exception("Must supply api_secret")
if not cloud_name:
raise Exception("Must supply api_secret")
api_url = "/".join([prefix, "v1_1", cloud_name] + uri)

processed_params = None
Expand Down Expand Up @@ -437,10 +470,12 @@ def transformation_string(transformation):
def __prepare_streaming_profile_params(**options):
params = only(options, "display_name")
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)
return params


def __delete_resource_params(options, **params):
p = dict(transformations=utils.build_eager(options.get('transformations')),
**only(options, "keep_original", "next_cursor", "invalidate"))
Expand Down
21 changes: 10 additions & 11 deletions cloudinary/auth_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import re
import time
from binascii import a2b_hex

from cloudinary.compat import quote_plus

AUTH_TOKEN_NAME = "__cld_token__"



def generate(url=None, acl=None, start_time=None, duration=None, expiration=None, ip=None, key=None,
token_name=AUTH_TOKEN_NAME):
def generate(url=None, acl=None, start_time=None, duration=None,
expiration=None, ip=None, key=None, token_name=AUTH_TOKEN_NAME):

if expiration is None:
if duration is not None:
Expand All @@ -20,10 +20,13 @@ def generate(url=None, acl=None, start_time=None, duration=None, expiration=None
raise Exception("Must provide either expiration or duration")

token_parts = []
if ip is not None: token_parts.append("ip=" + ip)
if start_time is not None: token_parts.append("st=%d" % start_time)
if ip is not None:
token_parts.append("ip=" + ip)
if start_time is not None:
token_parts.append("st=%d" % start_time)
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)
if url is not None:
to_sign.append("url=%s" % _escape_to_lower(url))
Expand All @@ -39,9 +42,5 @@ def _digest(message, key):

def _escape_to_lower(url):
escaped_url = quote_plus(url)

def toLowercase(match):
return match.group(0).lower()

escaped_url = re.sub(r'%..', toLowercase, escaped_url)
escaped_url = re.sub(r'%..', lambda x: x.group(0).lower(), escaped_url)
return escaped_url
3 changes: 2 additions & 1 deletion cloudinary/compat.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Copyright Cloudinary
import six.moves.urllib.parse
from six import PY3, string_types, StringIO, BytesIO

urlencode = six.moves.urllib.parse.urlencode
unquote = six.moves.urllib.parse.unquote
urlparse = six.moves.urllib.parse.urlparse
parse_qs = six.moves.urllib.parse.parse_qs
parse_qsl = six.moves.urllib.parse.parse_qsl
quote_plus = six.moves.urllib.parse.quote_plus
httplib = six.moves.http_client
from six import PY3, string_types, StringIO, BytesIO
urllib2 = six.moves.urllib.request
NotConnected = six.moves.http_client.NotConnected

Expand Down
Loading