cloudinary 1.42.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloudinary/__init__.py +841 -0
- cloudinary/api.py +1031 -0
- cloudinary/api_client/__init__.py +0 -0
- cloudinary/api_client/call_account_api.py +34 -0
- cloudinary/api_client/call_api.py +77 -0
- cloudinary/api_client/execute_request.py +85 -0
- cloudinary/api_client/tcp_keep_alive_manager.py +119 -0
- cloudinary/auth_token.py +76 -0
- cloudinary/cache/__init__.py +0 -0
- cloudinary/cache/adapter/__init__.py +0 -0
- cloudinary/cache/adapter/cache_adapter.py +63 -0
- cloudinary/cache/adapter/key_value_cache_adapter.py +61 -0
- cloudinary/cache/responsive_breakpoints_cache.py +124 -0
- cloudinary/cache/storage/__init__.py +0 -0
- cloudinary/cache/storage/file_system_key_value_storage.py +79 -0
- cloudinary/cache/storage/key_value_storage.py +51 -0
- cloudinary/compat.py +35 -0
- cloudinary/exceptions.py +33 -0
- cloudinary/forms.py +142 -0
- cloudinary/http_client.py +43 -0
- cloudinary/models.py +135 -0
- cloudinary/poster/__init__.py +31 -0
- cloudinary/poster/encode.py +456 -0
- cloudinary/poster/streaminghttp.py +209 -0
- cloudinary/provisioning/__init__.py +6 -0
- cloudinary/provisioning/account.py +481 -0
- cloudinary/provisioning/account_config.py +42 -0
- cloudinary/search.py +143 -0
- cloudinary/search_folders.py +10 -0
- cloudinary/static/cloudinary/html/cloudinary_cors.html +43 -0
- cloudinary/static/cloudinary/js/canvas-to-blob.min.js +1 -0
- cloudinary/static/cloudinary/js/jquery.cloudinary.js +4780 -0
- cloudinary/static/cloudinary/js/jquery.fileupload-image.js +326 -0
- cloudinary/static/cloudinary/js/jquery.fileupload-process.js +178 -0
- cloudinary/static/cloudinary/js/jquery.fileupload-validate.js +125 -0
- cloudinary/static/cloudinary/js/jquery.fileupload.js +1502 -0
- cloudinary/static/cloudinary/js/jquery.iframe-transport.js +224 -0
- cloudinary/static/cloudinary/js/jquery.ui.widget.js +752 -0
- cloudinary/static/cloudinary/js/load-image.all.min.js +1 -0
- cloudinary/templates/cloudinary_direct_upload.html +12 -0
- cloudinary/templates/cloudinary_includes.html +14 -0
- cloudinary/templates/cloudinary_js_config.html +3 -0
- cloudinary/templatetags/__init__.py +1 -0
- cloudinary/templatetags/cloudinary.py +86 -0
- cloudinary/uploader.py +639 -0
- cloudinary/utils.py +1695 -0
- cloudinary-1.42.1.dist-info/LICENSE.txt +5 -0
- cloudinary-1.42.1.dist-info/METADATA +173 -0
- cloudinary-1.42.1.dist-info/RECORD +51 -0
- cloudinary-1.42.1.dist-info/WHEEL +5 -0
- cloudinary-1.42.1.dist-info/top_level.txt +1 -0
cloudinary/__init__.py
ADDED
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import logging
|
|
5
|
+
import numbers
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from math import ceil
|
|
10
|
+
|
|
11
|
+
import certifi
|
|
12
|
+
from six import python_2_unicode_compatible, add_metaclass
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("Cloudinary")
|
|
15
|
+
ch = logging.StreamHandler()
|
|
16
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
17
|
+
ch.setFormatter(formatter)
|
|
18
|
+
logger.addHandler(ch)
|
|
19
|
+
|
|
20
|
+
from cloudinary import utils
|
|
21
|
+
from cloudinary.exceptions import GeneralError
|
|
22
|
+
from cloudinary.cache import responsive_breakpoints_cache
|
|
23
|
+
from cloudinary.http_client import HttpClient
|
|
24
|
+
from cloudinary.compat import urlparse, parse_qs
|
|
25
|
+
|
|
26
|
+
from platform import python_version, platform
|
|
27
|
+
|
|
28
|
+
CERT_KWARGS = {
|
|
29
|
+
'cert_reqs': 'CERT_REQUIRED',
|
|
30
|
+
'ca_certs': certifi.where(),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"
|
|
34
|
+
OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"
|
|
35
|
+
AKAMAI_SHARED_CDN = "res.cloudinary.com"
|
|
36
|
+
SHARED_CDN = AKAMAI_SHARED_CDN
|
|
37
|
+
CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
|
38
|
+
URI_SCHEME = "cloudinary"
|
|
39
|
+
API_VERSION = "v1_1"
|
|
40
|
+
|
|
41
|
+
VERSION = "1.42.1"
|
|
42
|
+
|
|
43
|
+
_USER_PLATFORM_DETAILS = "; ".join((platform(), "Python {}".format(python_version())))
|
|
44
|
+
|
|
45
|
+
USER_AGENT = "CloudinaryPython/{} ({})".format(VERSION, _USER_PLATFORM_DETAILS)
|
|
46
|
+
""" :const: USER_AGENT """
|
|
47
|
+
|
|
48
|
+
USER_PLATFORM = ""
|
|
49
|
+
"""
|
|
50
|
+
Additional information to be passed with the USER_AGENT, e.g. "CloudinaryCLI/1.2.3".
|
|
51
|
+
This value is set in platform-specific implementations that use pycloudinary.
|
|
52
|
+
|
|
53
|
+
The format of the value should be <ProductName>/Version[ (comment)].
|
|
54
|
+
@see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
|
|
55
|
+
|
|
56
|
+
**Do not set this value in application code!**
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_user_agent():
|
|
61
|
+
"""
|
|
62
|
+
Provides the `USER_AGENT` string that is passed to the Cloudinary servers.
|
|
63
|
+
Prepends `USER_PLATFORM` if it is defined.
|
|
64
|
+
|
|
65
|
+
:returns: the user agent
|
|
66
|
+
:rtype: str
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
if USER_PLATFORM == "":
|
|
70
|
+
return USER_AGENT
|
|
71
|
+
else:
|
|
72
|
+
return USER_PLATFORM + " " + USER_AGENT
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def import_django_settings():
|
|
76
|
+
try:
|
|
77
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
from django.conf import settings as _django_settings
|
|
81
|
+
|
|
82
|
+
# We can get a situation when Django module is installed in the system, but not initialized,
|
|
83
|
+
# which means we are running not in a Django process.
|
|
84
|
+
# In this case the following line throws ImproperlyConfigured exception
|
|
85
|
+
if 'cloudinary' in _django_settings.INSTALLED_APPS:
|
|
86
|
+
from django import get_version as _get_django_version
|
|
87
|
+
global USER_PLATFORM
|
|
88
|
+
USER_PLATFORM = "Django/{django_version}".format(django_version=_get_django_version())
|
|
89
|
+
|
|
90
|
+
if 'CLOUDINARY' in dir(_django_settings):
|
|
91
|
+
return _django_settings.CLOUDINARY
|
|
92
|
+
else:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
except ImproperlyConfigured:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
except ImportError:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@add_metaclass(abc.ABCMeta)
|
|
103
|
+
class BaseConfig(object):
|
|
104
|
+
def __init__(self):
|
|
105
|
+
django_settings = import_django_settings()
|
|
106
|
+
if django_settings:
|
|
107
|
+
self.update(**django_settings)
|
|
108
|
+
|
|
109
|
+
self._load_config_from_env()
|
|
110
|
+
|
|
111
|
+
def __getattr__(self, i):
|
|
112
|
+
return self.__dict__.get(i)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _is_nested_key(key):
|
|
116
|
+
return re.match(r'\w+\[\w+\]', key)
|
|
117
|
+
|
|
118
|
+
def _put_nested_key(self, key, value):
|
|
119
|
+
chain = re.split(r'[\[\]]+', key)
|
|
120
|
+
chain = [k for k in chain if k]
|
|
121
|
+
outer = self.__dict__
|
|
122
|
+
last_key = chain.pop()
|
|
123
|
+
for inner_key in chain:
|
|
124
|
+
if inner_key in outer:
|
|
125
|
+
inner = outer[inner_key]
|
|
126
|
+
else:
|
|
127
|
+
inner = dict()
|
|
128
|
+
outer[inner_key] = inner
|
|
129
|
+
outer = inner
|
|
130
|
+
if isinstance(value, list):
|
|
131
|
+
value = value[0]
|
|
132
|
+
outer[last_key] = value
|
|
133
|
+
|
|
134
|
+
def _is_url_scheme_valid(self, url):
|
|
135
|
+
"""
|
|
136
|
+
Helper function. Validates url scheme
|
|
137
|
+
|
|
138
|
+
:param url: A named tuple containing URL components
|
|
139
|
+
|
|
140
|
+
:return: bool True on success or False on failure
|
|
141
|
+
"""
|
|
142
|
+
return url.scheme.lower() == self._uri_scheme
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _parse_cloudinary_url(cloudinary_url):
|
|
146
|
+
return urlparse(cloudinary_url)
|
|
147
|
+
|
|
148
|
+
@abc.abstractmethod
|
|
149
|
+
def _config_from_parsed_url(self, parsed_url):
|
|
150
|
+
"""Extract additional config from the parsed URL."""
|
|
151
|
+
raise NotImplementedError()
|
|
152
|
+
|
|
153
|
+
def _setup_from_parsed_url(self, parsed_url):
|
|
154
|
+
config_from_parsed_url = self._config_from_parsed_url(parsed_url)
|
|
155
|
+
self.update(**config_from_parsed_url)
|
|
156
|
+
|
|
157
|
+
for k, v in parse_qs(parsed_url.query).items():
|
|
158
|
+
if self._is_nested_key(k):
|
|
159
|
+
self._put_nested_key(k, v)
|
|
160
|
+
else:
|
|
161
|
+
self.__dict__[k] = v[0]
|
|
162
|
+
|
|
163
|
+
def _load_from_url(self, url):
|
|
164
|
+
parsed_url = self._parse_cloudinary_url(url)
|
|
165
|
+
|
|
166
|
+
return self._setup_from_parsed_url(parsed_url)
|
|
167
|
+
|
|
168
|
+
@abc.abstractmethod
|
|
169
|
+
def _load_config_from_env(self):
|
|
170
|
+
"""Load config from environment variables or URL."""
|
|
171
|
+
raise NotImplementedError()
|
|
172
|
+
|
|
173
|
+
def update(self, **keywords):
|
|
174
|
+
for k, v in keywords.items():
|
|
175
|
+
self.__dict__[k] = v
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class Config(BaseConfig):
|
|
179
|
+
def __init__(self):
|
|
180
|
+
self._uri_scheme = URI_SCHEME
|
|
181
|
+
|
|
182
|
+
super(Config, self).__init__()
|
|
183
|
+
|
|
184
|
+
if not self.signature_algorithm:
|
|
185
|
+
self.signature_algorithm = utils.SIGNATURE_SHA1
|
|
186
|
+
|
|
187
|
+
def _config_from_parsed_url(self, parsed_url):
|
|
188
|
+
if not self._is_url_scheme_valid(parsed_url):
|
|
189
|
+
raise ValueError("Invalid CLOUDINARY_URL scheme. Expecting to start with 'cloudinary://'")
|
|
190
|
+
|
|
191
|
+
is_private_cdn = parsed_url.path != ""
|
|
192
|
+
result = {
|
|
193
|
+
"cloud_name": parsed_url.hostname,
|
|
194
|
+
"api_key": parsed_url.username,
|
|
195
|
+
"api_secret": parsed_url.password,
|
|
196
|
+
"private_cdn": is_private_cdn,
|
|
197
|
+
}
|
|
198
|
+
if is_private_cdn:
|
|
199
|
+
result.update({"secure_distribution": parsed_url.path[1:]})
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
def _load_config_from_env(self):
|
|
204
|
+
if os.environ.get("CLOUDINARY_CLOUD_NAME"):
|
|
205
|
+
config_keys = [key for key in os.environ.keys()
|
|
206
|
+
if key.startswith("CLOUDINARY_") and key != "CLOUDINARY_URL"]
|
|
207
|
+
|
|
208
|
+
for full_key in config_keys:
|
|
209
|
+
conf_key = full_key[len("CLOUDINARY_"):].lower()
|
|
210
|
+
conf_val = os.environ[full_key]
|
|
211
|
+
if conf_val in ["true", "false"]:
|
|
212
|
+
conf_val = conf_val == "true"
|
|
213
|
+
|
|
214
|
+
self.update(**{conf_key: conf_val})
|
|
215
|
+
elif os.environ.get("CLOUDINARY_URL"):
|
|
216
|
+
self._load_from_url(os.environ.get("CLOUDINARY_URL"))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
_config = Config()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def config(**keywords):
|
|
223
|
+
global _config
|
|
224
|
+
_config.update(**keywords)
|
|
225
|
+
return _config
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def reset_config():
|
|
229
|
+
global _config
|
|
230
|
+
_config = Config()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
_http_client = HttpClient()
|
|
234
|
+
|
|
235
|
+
# FIXME: circular import issue
|
|
236
|
+
from cloudinary.search import Search
|
|
237
|
+
from cloudinary.search_folders import SearchFolders
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@python_2_unicode_compatible
|
|
241
|
+
class CloudinaryResource(object):
|
|
242
|
+
"""
|
|
243
|
+
Recommended sources for video tag
|
|
244
|
+
"""
|
|
245
|
+
default_video_sources = [
|
|
246
|
+
{
|
|
247
|
+
"type": "mp4",
|
|
248
|
+
"codecs": "hev1",
|
|
249
|
+
"transformations": {"video_codec": "h265"}
|
|
250
|
+
}, {
|
|
251
|
+
"type": "webm",
|
|
252
|
+
"codecs": "vp9",
|
|
253
|
+
"transformations": {"video_codec": "vp9"}
|
|
254
|
+
}, {
|
|
255
|
+
"type": "mp4",
|
|
256
|
+
"transformations": {"video_codec": "auto"}
|
|
257
|
+
}, {
|
|
258
|
+
"type": "webm",
|
|
259
|
+
"transformations": {"video_codec": "auto"}
|
|
260
|
+
},
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
def __init__(self, public_id=None, format=None, version=None,
|
|
264
|
+
signature=None, url_options=None, metadata=None, type=None, resource_type=None,
|
|
265
|
+
default_resource_type=None):
|
|
266
|
+
self.metadata = metadata
|
|
267
|
+
metadata = metadata or {}
|
|
268
|
+
self.public_id = public_id or metadata.get('public_id')
|
|
269
|
+
self.format = format or metadata.get('format')
|
|
270
|
+
self.version = version or metadata.get('version')
|
|
271
|
+
self.signature = signature or metadata.get('signature')
|
|
272
|
+
self.type = type or metadata.get('type') or "upload"
|
|
273
|
+
self.resource_type = resource_type or metadata.get('resource_type') or default_resource_type
|
|
274
|
+
self.url_options = url_options or {}
|
|
275
|
+
|
|
276
|
+
def __str__(self):
|
|
277
|
+
return self.public_id
|
|
278
|
+
|
|
279
|
+
def __len__(self):
|
|
280
|
+
return len(self.public_id) if self.public_id is not None else 0
|
|
281
|
+
|
|
282
|
+
def validate(self):
|
|
283
|
+
return self.signature == self.get_expected_signature()
|
|
284
|
+
|
|
285
|
+
def get_prep_value(self):
|
|
286
|
+
if None in [self.public_id,
|
|
287
|
+
self.type,
|
|
288
|
+
self.resource_type]:
|
|
289
|
+
return None
|
|
290
|
+
prep = ''
|
|
291
|
+
prep = prep + self.resource_type + '/' + self.type + '/'
|
|
292
|
+
if self.version:
|
|
293
|
+
prep = prep + 'v' + str(self.version) + '/'
|
|
294
|
+
prep = prep + self.public_id
|
|
295
|
+
if self.format:
|
|
296
|
+
prep = prep + '.' + self.format
|
|
297
|
+
return prep
|
|
298
|
+
|
|
299
|
+
def get_presigned(self):
|
|
300
|
+
return self.get_prep_value() + '#' + self.get_expected_signature()
|
|
301
|
+
|
|
302
|
+
def get_expected_signature(self):
|
|
303
|
+
return utils.api_sign_request({"public_id": self.public_id, "version": self.version}, config().api_secret,
|
|
304
|
+
config().signature_algorithm)
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def url(self):
|
|
308
|
+
return self.build_url(**self.url_options)
|
|
309
|
+
|
|
310
|
+
def __build_url(self, **options):
|
|
311
|
+
combined_options = dict(format=self.format, version=self.version, type=self.type,
|
|
312
|
+
resource_type=self.resource_type or "image")
|
|
313
|
+
combined_options.update(options)
|
|
314
|
+
public_id = combined_options.get('public_id') or self.public_id
|
|
315
|
+
return utils.cloudinary_url(public_id, **combined_options)
|
|
316
|
+
|
|
317
|
+
def build_url(self, **options):
|
|
318
|
+
return self.__build_url(**options)[0]
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
def default_poster_options(options):
|
|
322
|
+
options["format"] = options.get("format", "jpg")
|
|
323
|
+
|
|
324
|
+
@staticmethod
|
|
325
|
+
def default_source_types():
|
|
326
|
+
return ['webm', 'mp4', 'ogv']
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def _validate_srcset_data(srcset_data):
|
|
330
|
+
"""
|
|
331
|
+
Helper function. Validates srcset_data parameters
|
|
332
|
+
|
|
333
|
+
:param srcset_data: A dictionary containing the following keys:
|
|
334
|
+
breakpoints A list of breakpoints.
|
|
335
|
+
min_width Minimal width of the srcset images
|
|
336
|
+
max_width Maximal width of the srcset images.
|
|
337
|
+
max_images Number of srcset images to generate.
|
|
338
|
+
|
|
339
|
+
:return: bool True on success or False on failure
|
|
340
|
+
"""
|
|
341
|
+
if not all(k in srcset_data and isinstance(srcset_data[k], numbers.Number) for k in ("min_width", "max_width",
|
|
342
|
+
"max_images")):
|
|
343
|
+
logger.warning("Either valid (min_width, max_width, max_images)" +
|
|
344
|
+
"or breakpoints must be provided to the image srcset attribute")
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
if srcset_data["min_width"] > srcset_data["max_width"]:
|
|
348
|
+
logger.warning("min_width must be less than max_width")
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
if srcset_data["max_images"] <= 0:
|
|
352
|
+
logger.warning("max_images must be a positive integer")
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
return True
|
|
356
|
+
|
|
357
|
+
def _generate_breakpoints(self, srcset_data):
|
|
358
|
+
"""
|
|
359
|
+
Helper function. Calculates static responsive breakpoints using provided parameters.
|
|
360
|
+
|
|
361
|
+
Either the breakpoints or min_width, max_width, max_images must be provided.
|
|
362
|
+
|
|
363
|
+
:param srcset_data: A dictionary containing the following keys:
|
|
364
|
+
breakpoints A list of breakpoints.
|
|
365
|
+
min_width Minimal width of the srcset images
|
|
366
|
+
max_width Maximal width of the srcset images.
|
|
367
|
+
max_images Number of srcset images to generate.
|
|
368
|
+
|
|
369
|
+
:return: A list of breakpoints
|
|
370
|
+
|
|
371
|
+
:raises ValueError: In case of invalid or missing parameters
|
|
372
|
+
"""
|
|
373
|
+
breakpoints = srcset_data.get("breakpoints", list())
|
|
374
|
+
|
|
375
|
+
if breakpoints:
|
|
376
|
+
return breakpoints
|
|
377
|
+
|
|
378
|
+
if not self._validate_srcset_data(srcset_data):
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
min_width, max_width, max_images = srcset_data["min_width"], srcset_data["max_width"], srcset_data["max_images"]
|
|
382
|
+
|
|
383
|
+
if max_images == 1:
|
|
384
|
+
# if user requested only 1 image in srcset, we return max_width one
|
|
385
|
+
min_width = max_width
|
|
386
|
+
|
|
387
|
+
step_size = int(ceil(float(max_width - min_width) / (max_images - 1 if max_images > 1 else 1)))
|
|
388
|
+
|
|
389
|
+
curr_breakpoint = min_width
|
|
390
|
+
|
|
391
|
+
while curr_breakpoint < max_width:
|
|
392
|
+
breakpoints.append(curr_breakpoint)
|
|
393
|
+
curr_breakpoint += step_size
|
|
394
|
+
|
|
395
|
+
breakpoints.append(max_width)
|
|
396
|
+
|
|
397
|
+
return breakpoints
|
|
398
|
+
|
|
399
|
+
def _fetch_breakpoints(self, srcset_data=None, **options):
|
|
400
|
+
"""
|
|
401
|
+
Helper function. Retrieves responsive breakpoints list from cloudinary server
|
|
402
|
+
|
|
403
|
+
When passing special string to transformation `width` parameter of form `auto:breakpoints{parameters}:json`,
|
|
404
|
+
the response contains JSON with data of the responsive breakpoints
|
|
405
|
+
|
|
406
|
+
:param srcset_data: A dictionary containing the following keys:
|
|
407
|
+
min_width Minimal width of the srcset images
|
|
408
|
+
max_width Maximal width of the srcset images
|
|
409
|
+
bytes_step Minimal bytes step between images
|
|
410
|
+
max_images Number of srcset images to generate
|
|
411
|
+
:param options: Additional options
|
|
412
|
+
|
|
413
|
+
:return: Resulting breakpoints
|
|
414
|
+
"""
|
|
415
|
+
if srcset_data is None:
|
|
416
|
+
srcset_data = dict()
|
|
417
|
+
|
|
418
|
+
min_width = srcset_data.get("min_width", 50)
|
|
419
|
+
max_width = srcset_data.get("max_width", 1000)
|
|
420
|
+
bytes_step = srcset_data.get("bytes_step", 20000)
|
|
421
|
+
max_images = srcset_data.get("max_images", 20)
|
|
422
|
+
transformation = srcset_data.get("transformation")
|
|
423
|
+
|
|
424
|
+
kbytes_step = int(ceil(float(bytes_step) / 1024))
|
|
425
|
+
|
|
426
|
+
breakpoints_width_param = "auto:breakpoints_{min_width}_{max_width}_{kbytes_step}_{max_images}:json".format(
|
|
427
|
+
min_width=min_width, max_width=max_width, kbytes_step=kbytes_step, max_images=max_images)
|
|
428
|
+
breakpoints_url = utils.cloudinary_scaled_url(self.public_id, breakpoints_width_param, transformation, options)
|
|
429
|
+
|
|
430
|
+
return _http_client.get_json(breakpoints_url).get("breakpoints", None)
|
|
431
|
+
|
|
432
|
+
def _get_or_generate_breakpoints(self, srcset_data, **options):
|
|
433
|
+
"""
|
|
434
|
+
Helper function. Gets from cache or calculates srcset breakpoints using provided parameters
|
|
435
|
+
|
|
436
|
+
:param srcset_data: A dictionary containing the following keys:
|
|
437
|
+
breakpoints A list of breakpoints.
|
|
438
|
+
min_width Minimal width of the srcset images
|
|
439
|
+
max_width Maximal width of the srcset images
|
|
440
|
+
max_images Number of srcset images to generate
|
|
441
|
+
:param options: Additional options
|
|
442
|
+
|
|
443
|
+
:return: Resulting breakpoints
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
breakpoints = srcset_data.get("breakpoints")
|
|
447
|
+
|
|
448
|
+
if breakpoints:
|
|
449
|
+
return breakpoints
|
|
450
|
+
|
|
451
|
+
if srcset_data.get("use_cache"):
|
|
452
|
+
breakpoints = responsive_breakpoints_cache.instance.get(self.public_id, **options)
|
|
453
|
+
if not breakpoints:
|
|
454
|
+
try:
|
|
455
|
+
breakpoints = self._fetch_breakpoints(srcset_data, **options)
|
|
456
|
+
except GeneralError as e:
|
|
457
|
+
logger.warning("Failed getting responsive breakpoints: {error}".format(error=e.message))
|
|
458
|
+
|
|
459
|
+
if breakpoints:
|
|
460
|
+
responsive_breakpoints_cache.instance.set(self.public_id, breakpoints, **options)
|
|
461
|
+
|
|
462
|
+
if not breakpoints:
|
|
463
|
+
# Static calculation if cache is not enabled or we failed to fetch breakpoints
|
|
464
|
+
breakpoints = self._generate_breakpoints(srcset_data)
|
|
465
|
+
|
|
466
|
+
return breakpoints
|
|
467
|
+
|
|
468
|
+
def _generate_srcset_attribute(self, breakpoints, transformation=None, **options):
|
|
469
|
+
"""
|
|
470
|
+
Helper function. Generates srcset attribute value of the HTML img tag.
|
|
471
|
+
|
|
472
|
+
:param breakpoints: A list of breakpoints.
|
|
473
|
+
:param transformation: Custom transformation
|
|
474
|
+
:param options: Additional options
|
|
475
|
+
|
|
476
|
+
:return: Resulting srcset attribute value
|
|
477
|
+
|
|
478
|
+
:raises ValueError: In case of invalid or missing parameters
|
|
479
|
+
"""
|
|
480
|
+
if not breakpoints:
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
if transformation is None:
|
|
484
|
+
transformation = dict()
|
|
485
|
+
|
|
486
|
+
return ", ".join(["{0} {1}w".format(utils.cloudinary_scaled_url(
|
|
487
|
+
self.public_id, w, transformation, options), w) for w in breakpoints])
|
|
488
|
+
|
|
489
|
+
@staticmethod
|
|
490
|
+
def _generate_sizes_attribute(breakpoints):
|
|
491
|
+
"""
|
|
492
|
+
Helper function. Generates sizes attribute value of the HTML img tag.
|
|
493
|
+
|
|
494
|
+
:param breakpoints: A list of breakpoints.
|
|
495
|
+
|
|
496
|
+
:return: Resulting 'sizes' attribute value
|
|
497
|
+
"""
|
|
498
|
+
if not breakpoints:
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
return ", ".join("(max-width: {bp}px) {bp}px".format(bp=bp) for bp in breakpoints)
|
|
502
|
+
|
|
503
|
+
def _generate_image_responsive_attributes(self, attributes, srcset_data, **options):
|
|
504
|
+
"""
|
|
505
|
+
Helper function. Generates srcset and sizes attributes of the image tag
|
|
506
|
+
|
|
507
|
+
Create both srcset and sizes here to avoid fetching breakpoints twice
|
|
508
|
+
|
|
509
|
+
:param attributes: Existing attributes
|
|
510
|
+
:param srcset_data: A dictionary containing the following keys:
|
|
511
|
+
breakpoints A list of breakpoints.
|
|
512
|
+
min_width Minimal width of the srcset images
|
|
513
|
+
max_width Maximal width of the srcset images.
|
|
514
|
+
max_images Number of srcset images to generate.
|
|
515
|
+
:param options: Additional options
|
|
516
|
+
|
|
517
|
+
:return: The responsive attributes
|
|
518
|
+
"""
|
|
519
|
+
responsive_attributes = dict()
|
|
520
|
+
|
|
521
|
+
if not srcset_data:
|
|
522
|
+
return responsive_attributes
|
|
523
|
+
|
|
524
|
+
breakpoints = None
|
|
525
|
+
|
|
526
|
+
if "srcset" not in attributes:
|
|
527
|
+
breakpoints = self._get_or_generate_breakpoints(srcset_data, **options)
|
|
528
|
+
transformation = srcset_data.get("transformation")
|
|
529
|
+
srcset_attr = self._generate_srcset_attribute(breakpoints, transformation, **options)
|
|
530
|
+
if srcset_attr:
|
|
531
|
+
responsive_attributes["srcset"] = srcset_attr
|
|
532
|
+
|
|
533
|
+
if "sizes" not in attributes and srcset_data.get("sizes") is True:
|
|
534
|
+
if not breakpoints:
|
|
535
|
+
breakpoints = self._get_or_generate_breakpoints(srcset_data, **options)
|
|
536
|
+
sizes_attr = self._generate_sizes_attribute(breakpoints)
|
|
537
|
+
if sizes_attr:
|
|
538
|
+
responsive_attributes["sizes"] = sizes_attr
|
|
539
|
+
|
|
540
|
+
return responsive_attributes
|
|
541
|
+
|
|
542
|
+
def image(self, **options):
|
|
543
|
+
"""
|
|
544
|
+
Generates HTML img tag
|
|
545
|
+
|
|
546
|
+
:param options: Additional options
|
|
547
|
+
|
|
548
|
+
:return: Resulting img tag
|
|
549
|
+
"""
|
|
550
|
+
if options.get("resource_type", self.resource_type) == "video":
|
|
551
|
+
self.default_poster_options(options)
|
|
552
|
+
|
|
553
|
+
custom_attributes = options.pop("attributes", dict())
|
|
554
|
+
|
|
555
|
+
srcset_option = options.pop("srcset", dict())
|
|
556
|
+
srcset_data = dict()
|
|
557
|
+
|
|
558
|
+
if isinstance(srcset_option, dict):
|
|
559
|
+
srcset_data = config().srcset or dict()
|
|
560
|
+
srcset_data = srcset_data.copy()
|
|
561
|
+
srcset_data.update(srcset_option)
|
|
562
|
+
else:
|
|
563
|
+
if "srcset" not in custom_attributes:
|
|
564
|
+
custom_attributes["srcset"] = srcset_option
|
|
565
|
+
|
|
566
|
+
src, attrs = self.__build_url(**options)
|
|
567
|
+
|
|
568
|
+
client_hints = attrs.pop("client_hints", config().client_hints)
|
|
569
|
+
responsive = attrs.pop("responsive", False)
|
|
570
|
+
hidpi = attrs.pop("hidpi", False)
|
|
571
|
+
|
|
572
|
+
if (responsive or hidpi) and not client_hints:
|
|
573
|
+
attrs["data-src"] = src
|
|
574
|
+
|
|
575
|
+
classes = "cld-responsive" if responsive else "cld-hidpi"
|
|
576
|
+
if "class" in attrs:
|
|
577
|
+
classes += " " + attrs["class"]
|
|
578
|
+
attrs["class"] = classes
|
|
579
|
+
|
|
580
|
+
src = attrs.pop("responsive_placeholder", config().responsive_placeholder)
|
|
581
|
+
if src == "blank":
|
|
582
|
+
src = CL_BLANK
|
|
583
|
+
|
|
584
|
+
responsive_attrs = self._generate_image_responsive_attributes(custom_attributes, srcset_data, **options)
|
|
585
|
+
|
|
586
|
+
if responsive_attrs:
|
|
587
|
+
# width and height attributes override srcset behavior, they should be removed from html attributes.
|
|
588
|
+
for key in {"width", "height"}:
|
|
589
|
+
attrs.pop(key, None)
|
|
590
|
+
|
|
591
|
+
attrs.update(responsive_attrs)
|
|
592
|
+
# Explicitly provided attributes override options
|
|
593
|
+
attrs.update(custom_attributes)
|
|
594
|
+
|
|
595
|
+
if src:
|
|
596
|
+
attrs["src"] = src
|
|
597
|
+
|
|
598
|
+
return u"<img {0}/>".format(utils.html_attrs(attrs))
|
|
599
|
+
|
|
600
|
+
def video_thumbnail(self, **options):
|
|
601
|
+
self.default_poster_options(options)
|
|
602
|
+
return self.build_url(**options)
|
|
603
|
+
|
|
604
|
+
@staticmethod
|
|
605
|
+
def _video_mime_type(video_type, codecs=None):
|
|
606
|
+
"""
|
|
607
|
+
Helper function for video(), generates video MIME type string from video_type and codecs.
|
|
608
|
+
Example: video/mp4; codecs=mp4a.40.2
|
|
609
|
+
|
|
610
|
+
:param video_type: mp4, webm, ogg etc.
|
|
611
|
+
:param codecs: List or string of codecs. E.g.: "avc1.42E01E" or "avc1.42E01E, mp4a.40.2" or
|
|
612
|
+
["avc1.42E01E", "mp4a.40.2"]
|
|
613
|
+
|
|
614
|
+
:return: Resulting mime type
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
video_type = 'ogg' if video_type == 'ogv' else video_type
|
|
618
|
+
|
|
619
|
+
if not video_type:
|
|
620
|
+
return ""
|
|
621
|
+
|
|
622
|
+
codecs_str = ", ".join(codecs) if isinstance(codecs, (list, tuple)) else codecs
|
|
623
|
+
codecs_attr = "; codecs={codecs_str}".format(codecs_str=codecs_str) if codecs_str else ""
|
|
624
|
+
|
|
625
|
+
return "video/{}{}".format(video_type, codecs_attr)
|
|
626
|
+
|
|
627
|
+
@staticmethod
|
|
628
|
+
def _collect_video_tag_attributes(video_options):
|
|
629
|
+
"""
|
|
630
|
+
Helper function for video tag, collects remaining options and returns them as attributes
|
|
631
|
+
|
|
632
|
+
:param video_options: Remaining options
|
|
633
|
+
|
|
634
|
+
:return: Resulting attributes
|
|
635
|
+
"""
|
|
636
|
+
attributes = video_options.copy()
|
|
637
|
+
|
|
638
|
+
if 'html_width' in attributes:
|
|
639
|
+
attributes['width'] = attributes.pop('html_width')
|
|
640
|
+
if 'html_height' in attributes:
|
|
641
|
+
attributes['height'] = attributes.pop('html_height')
|
|
642
|
+
|
|
643
|
+
if "poster" in attributes and not attributes["poster"]:
|
|
644
|
+
attributes.pop("poster", None)
|
|
645
|
+
|
|
646
|
+
return attributes
|
|
647
|
+
|
|
648
|
+
def _generate_video_poster_attr(self, source, video_options):
|
|
649
|
+
"""
|
|
650
|
+
Helper function for video tag, generates video poster URL
|
|
651
|
+
|
|
652
|
+
:param source: The public ID of the resource
|
|
653
|
+
:param video_options: Additional options
|
|
654
|
+
|
|
655
|
+
:return: Resulting video poster URL
|
|
656
|
+
"""
|
|
657
|
+
if 'poster' not in video_options:
|
|
658
|
+
return self.video_thumbnail(public_id=source, **video_options)
|
|
659
|
+
|
|
660
|
+
poster_options = video_options['poster']
|
|
661
|
+
|
|
662
|
+
if not isinstance(poster_options, dict):
|
|
663
|
+
return poster_options
|
|
664
|
+
|
|
665
|
+
if 'public_id' not in poster_options:
|
|
666
|
+
return self.video_thumbnail(public_id=source, **poster_options)
|
|
667
|
+
|
|
668
|
+
return utils.cloudinary_url(poster_options['public_id'], **poster_options)[0]
|
|
669
|
+
|
|
670
|
+
def _populate_video_source_tags(self, source, options):
|
|
671
|
+
"""
|
|
672
|
+
Helper function for video tag, populates source tags from provided options.
|
|
673
|
+
|
|
674
|
+
source_types and sources are mutually exclusive, only one of them can be used.
|
|
675
|
+
If both are not provided, source types are used (for backwards compatibility)
|
|
676
|
+
|
|
677
|
+
:param source: The public ID of the video
|
|
678
|
+
:param options: Additional options
|
|
679
|
+
|
|
680
|
+
:return: Resulting source tags (may be empty)
|
|
681
|
+
"""
|
|
682
|
+
source_tags = []
|
|
683
|
+
|
|
684
|
+
# Consume all relevant options, otherwise they are left and passed as attributes
|
|
685
|
+
video_sources = options.pop('sources', [])
|
|
686
|
+
source_types = options.pop('source_types', [])
|
|
687
|
+
source_transformation = options.pop('source_transformation', {})
|
|
688
|
+
|
|
689
|
+
if video_sources and isinstance(video_sources, list):
|
|
690
|
+
# processing new source structure with codecs
|
|
691
|
+
for source_data in video_sources:
|
|
692
|
+
transformation = options.copy()
|
|
693
|
+
transformation.update(source_data.get("transformations", {}))
|
|
694
|
+
source_type = source_data.get("type", '')
|
|
695
|
+
src = utils.cloudinary_url(source, format=source_type, **transformation)[0]
|
|
696
|
+
codecs = source_data.get("codecs", [])
|
|
697
|
+
source_tags.append("<source {attributes}>".format(
|
|
698
|
+
attributes=utils.html_attrs({'src': src, 'type': self._video_mime_type(source_type, codecs)})))
|
|
699
|
+
|
|
700
|
+
return source_tags
|
|
701
|
+
|
|
702
|
+
# processing old source_types structure with out codecs
|
|
703
|
+
if not source_types:
|
|
704
|
+
source_types = self.default_source_types()
|
|
705
|
+
|
|
706
|
+
if not isinstance(source_types, (list, tuple)):
|
|
707
|
+
return source_tags
|
|
708
|
+
|
|
709
|
+
for source_type in source_types:
|
|
710
|
+
transformation = options.copy()
|
|
711
|
+
transformation.update(source_transformation.get(source_type, {}))
|
|
712
|
+
src = utils.cloudinary_url(source, format=source_type, **transformation)[0]
|
|
713
|
+
source_tags.append("<source {attributes}>".format(
|
|
714
|
+
attributes=utils.html_attrs({'src': src, 'type': self._video_mime_type(source_type)})))
|
|
715
|
+
|
|
716
|
+
return source_tags
|
|
717
|
+
|
|
718
|
+
def video(self, **options):
|
|
719
|
+
"""
|
|
720
|
+
Creates an HTML video tag for the provided +source+
|
|
721
|
+
|
|
722
|
+
Examples:
|
|
723
|
+
CloudinaryResource("mymovie.mp4").video()
|
|
724
|
+
CloudinaryResource("mymovie.mp4").video(source_types = 'webm')
|
|
725
|
+
CloudinaryResource("mymovie.ogv").video(poster = "myspecialplaceholder.jpg")
|
|
726
|
+
CloudinaryResource("mymovie.webm").video(source_types = ['webm', 'mp4'], poster = {'effect': 'sepia'})
|
|
727
|
+
|
|
728
|
+
:param options:
|
|
729
|
+
* <tt>source_types</tt> - Specify which source type the tag should include.
|
|
730
|
+
defaults to webm, mp4 and ogv.
|
|
731
|
+
* <tt>sources</tt> - Similar to source_types, but may contain codecs list.
|
|
732
|
+
source_types and sources are mutually exclusive, only one of
|
|
733
|
+
them can be used. If both are not provided, default source types
|
|
734
|
+
are used.
|
|
735
|
+
* <tt>source_transformation</tt> - specific transformations to use
|
|
736
|
+
for a specific source type.
|
|
737
|
+
* <tt>poster</tt> - override default thumbnail:
|
|
738
|
+
* url: provide an ad hoc url
|
|
739
|
+
* options: with specific poster transformations and/or Cloudinary +:public_id+
|
|
740
|
+
|
|
741
|
+
:return: Video tag
|
|
742
|
+
"""
|
|
743
|
+
public_id = options.get('public_id', self.public_id)
|
|
744
|
+
use_fetch_format = options.get('use_fetch_format', config().use_fetch_format)
|
|
745
|
+
if not use_fetch_format:
|
|
746
|
+
source = re.sub(r"\.({0})$".format("|".join(self.default_source_types())), '', public_id)
|
|
747
|
+
else:
|
|
748
|
+
source = public_id
|
|
749
|
+
|
|
750
|
+
custom_attributes = options.pop("attributes", dict())
|
|
751
|
+
|
|
752
|
+
fallback = options.pop('fallback_content', '')
|
|
753
|
+
|
|
754
|
+
# Save source types for a single video source handling (it can be a single type)
|
|
755
|
+
source_types = options.get('source_types', "")
|
|
756
|
+
|
|
757
|
+
poster_options = options.copy()
|
|
758
|
+
if "poster" not in custom_attributes:
|
|
759
|
+
options["poster"] = self._generate_video_poster_attr(source, poster_options)
|
|
760
|
+
|
|
761
|
+
if "resource_type" not in options:
|
|
762
|
+
options["resource_type"] = self.resource_type or "video"
|
|
763
|
+
|
|
764
|
+
# populate video source tags
|
|
765
|
+
source_tags = self._populate_video_source_tags(source, options)
|
|
766
|
+
|
|
767
|
+
if not source_tags:
|
|
768
|
+
source = source + '.' + utils.build_array(source_types)[0]
|
|
769
|
+
|
|
770
|
+
video_url, video_options = utils.cloudinary_url(source, **options)
|
|
771
|
+
|
|
772
|
+
if not source_tags:
|
|
773
|
+
custom_attributes['src'] = video_url
|
|
774
|
+
|
|
775
|
+
attributes = self._collect_video_tag_attributes(video_options)
|
|
776
|
+
attributes.update(custom_attributes)
|
|
777
|
+
|
|
778
|
+
sources_str = ''.join(str(x) for x in source_tags)
|
|
779
|
+
html = "<video {attributes}>{sources}{fallback}</video>".format(
|
|
780
|
+
attributes=utils.html_attrs(attributes), sources=sources_str, fallback=fallback)
|
|
781
|
+
|
|
782
|
+
return html
|
|
783
|
+
|
|
784
|
+
@staticmethod
|
|
785
|
+
def __generate_media_attr(**media_options):
|
|
786
|
+
media_query_conditions = []
|
|
787
|
+
if "min_width" in media_options:
|
|
788
|
+
media_query_conditions.append("(min-width: {}px)".format(media_options["min_width"]))
|
|
789
|
+
if "max_width" in media_options:
|
|
790
|
+
media_query_conditions.append("(max-width: {}px)".format(media_options["max_width"]))
|
|
791
|
+
|
|
792
|
+
return " and ".join(media_query_conditions)
|
|
793
|
+
|
|
794
|
+
def source(self, **options):
|
|
795
|
+
attrs = options.get("attributes") or {}
|
|
796
|
+
|
|
797
|
+
srcset_data = config().srcset or dict()
|
|
798
|
+
srcset_data = srcset_data.copy()
|
|
799
|
+
srcset_data.update(options.pop("srcset", dict()))
|
|
800
|
+
|
|
801
|
+
responsive_attrs = self._generate_image_responsive_attributes(attrs, srcset_data, **options)
|
|
802
|
+
|
|
803
|
+
attrs.update(responsive_attrs)
|
|
804
|
+
|
|
805
|
+
# `source` tag under `picture` tag uses `srcset` attribute for both `srcset` and `src` urls
|
|
806
|
+
if "srcset" not in attrs:
|
|
807
|
+
attrs["srcset"], _ = self.__build_url(**options)
|
|
808
|
+
|
|
809
|
+
if "media" not in attrs:
|
|
810
|
+
media_attr = self.__generate_media_attr(**(options.get("media", {})))
|
|
811
|
+
if media_attr:
|
|
812
|
+
attrs["media"] = media_attr
|
|
813
|
+
|
|
814
|
+
return u"<source {0}>".format(utils.html_attrs(attrs))
|
|
815
|
+
|
|
816
|
+
def picture(self, **options):
|
|
817
|
+
sub_tags = []
|
|
818
|
+
sources = options.pop("sources") or list()
|
|
819
|
+
for source in sources:
|
|
820
|
+
curr_options = deepcopy(options)
|
|
821
|
+
|
|
822
|
+
if "transformation" in source:
|
|
823
|
+
curr_options = utils.chain_transformations(curr_options, source["transformation"])
|
|
824
|
+
|
|
825
|
+
curr_options["media"] = dict((k, source[k]) for k in ['min_width', 'max_width'] if k in source)
|
|
826
|
+
|
|
827
|
+
sub_tags.append(self.source(**curr_options))
|
|
828
|
+
|
|
829
|
+
sub_tags.append(self.image(**options))
|
|
830
|
+
|
|
831
|
+
return u"<picture>{}</picture>".format("".join(sub_tags))
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
class CloudinaryImage(CloudinaryResource):
|
|
835
|
+
def __init__(self, public_id=None, **kwargs):
|
|
836
|
+
super(CloudinaryImage, self).__init__(public_id=public_id, default_resource_type="image", **kwargs)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
class CloudinaryVideo(CloudinaryResource):
|
|
840
|
+
def __init__(self, public_id=None, **kwargs):
|
|
841
|
+
super(CloudinaryVideo, self).__init__(public_id=public_id, default_resource_type="video", **kwargs)
|