diff --git a/lib/cloudinary/__init__.py b/lib/cloudinary/__init__.py index 4422b38d..8278200c 100644 --- a/lib/cloudinary/__init__.py +++ b/lib/cloudinary/__init__.py @@ -38,7 +38,7 @@ CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAA URI_SCHEME = "cloudinary" API_VERSION = "v1_1" -VERSION = "1.26.0" +VERSION = "1.28.0" USER_AGENT = "CloudinaryPython/{} (Python {})".format(VERSION, python_version()) """ :const: USER_AGENT """ diff --git a/lib/cloudinary/api.py b/lib/cloudinary/api.py index 7c1fd534..58002254 100644 --- a/lib/cloudinary/api.py +++ b/lib/cloudinary/api.py @@ -93,6 +93,25 @@ def resources_by_ids(public_ids, **options): return call_api("get", uri, params, **options) +def resources_by_asset_ids(asset_ids, **options): + """Retrieves the resources (assets) indicated in the asset IDs. + This method does not return deleted assets even if they have been backed up. + + See: `Get resources by context API reference + `_ + + :param asset_ids: The requested asset IDs. + :type asset_ids: list[str] + :param options: Additional options + :type options: dict, optional + :return: Resources (assets) as indicated in the asset IDs + :rtype: Response + """ + uri = ["resources", 'by_asset_ids'] + params = dict(only(options, "tags", "moderations", "context"), asset_ids=asset_ids) + return call_api("get", uri, params, **options) + + def resources_by_context(key, value=None, **options): """Retrieves resources (assets) with a specified context key. This method does not return deleted assets even if they have been backed up. @@ -123,12 +142,40 @@ 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", "cinemagraph_analysis", - "pages", "phash", "coordinates", "max_results", "quality_analysis", "derived_next_cursor", - "accessibility_analysis", "versions") + params = _prepare_asset_details_params(**options) return call_api("get", uri, params, **options) +def resource_by_asset_id(asset_id, **options): + """ + Returns the details of the specified asset and all its derived assets by asset id. + + :param asset_id: The Asset ID of the asset + :type asset_id: string + :param options: Additional options + :type options: dict, optional + :return: Resource (asset) of a specific asset_id + :rtype: Response + """ + uri = ["resources", asset_id] + params = _prepare_asset_details_params(**options) + return call_api("get", uri, params, **options) + + +def _prepare_asset_details_params(**options): + """ + Prepares optional parameters for resource_by_asset_id API calls. + + :param options: Additional options + :return: Optional parameters + + :internal + """ + return only(options, "exif", "faces", "colors", "image_metadata", "cinemagraph_analysis", + "pages", "phash", "coordinates", "max_results", "quality_analysis", "derived_next_cursor", + "accessibility_analysis", "versions") + + def update(public_id, **options): resource_type = options.pop("resource_type", "image") upload_type = options.pop("type", "upload") @@ -595,3 +642,32 @@ def restore_metadata_field_datasource(field_external_id, entries_external_ids, * uri = [field_external_id, 'datasource_restore'] params = {"external_ids": entries_external_ids} return call_metadata_api("post", uri, params, **options) + + +def reorder_metadata_field_datasource(field_external_id, order_by, direction=None, **options): + """Reorders metadata field datasource. Currently, supports only value. + + :param field_external_id: The ID of the metadata field. + :param order_by: Criteria for the order. Currently, supports only value. + :param direction: Optional (gets either asc or desc). + :param options: Additional options. + + :rtype: Response + """ + uri = [field_external_id, 'datasource', 'order'] + params = {'order_by': order_by, 'direction': direction} + return call_metadata_api('post', uri, params, **options) + + +def reorder_metadata_fields(order_by, direction=None, **options): + """Reorders metadata fields. + + :param order_by: Criteria for the order (one of the fields 'label', 'external_id', 'created_at'). + :param direction: Optional (gets either asc or desc). + :param options: Additional options. + + :rtype: Response + """ + uri = ['order'] + params = {'order_by': order_by, 'direction': direction} + return call_metadata_api('put', uri, params, **options) diff --git a/lib/cloudinary/provisioning/account.py b/lib/cloudinary/provisioning/account.py index 9dc97cf1..6607f101 100644 --- a/lib/cloudinary/provisioning/account.py +++ b/lib/cloudinary/provisioning/account.py @@ -97,9 +97,7 @@ def sub_account(sub_account_id, **options): return _call_account_api("get", uri, {}, **options) -def update_sub_account(sub_account_id, name=None, cloud_name=None, custom_attributes=None, - enabled=None, base_account=None, - **options): +def update_sub_account(sub_account_id, name=None, cloud_name=None, custom_attributes=None, enabled=None, **options): """ Update a sub account :param sub_account_id: The id of the sub account @@ -112,8 +110,6 @@ def update_sub_account(sub_account_id, name=None, cloud_name=None, custom_attrib :type custom_attributes: dict, optional :param enabled: Whether to create the account as enabled (default is enabled). :type enabled: bool, optional - :param base_account: ID of sub-account from which to copy settings - :type base_account: str, optional :param options: Generic advanced options dict, see online documentation :type options: dict, optional :return: Updated sub account @@ -123,8 +119,7 @@ def update_sub_account(sub_account_id, name=None, cloud_name=None, custom_attrib params = {"name": name, "cloud_name": cloud_name, "custom_attributes": custom_attributes, - "enabled": enabled, - "base_account": base_account} + "enabled": enabled} return _call_account_api("put", uri, params=params, **options) diff --git a/lib/cloudinary/search.py b/lib/cloudinary/search.py index 0b65d3b9..0d97e098 100644 --- a/lib/cloudinary/search.py +++ b/lib/cloudinary/search.py @@ -1,10 +1,16 @@ import json -from copy import deepcopy from cloudinary.api_client.call_api import call_json_api +from cloudinary.utils import unique class Search: + _KEYS_WITH_UNIQUE_VALUES = { + 'sort_by': lambda x: next(iter(x)), + 'aggregate': None, + 'with_field': None, + } + """Build and execute a search query.""" def __init__(self): self.query = {} @@ -42,7 +48,7 @@ class Search: return self def to_json(self): - return json.dumps(self.query) + return json.dumps(self.as_dict()) def execute(self, **options): """Execute the search and return results.""" @@ -57,4 +63,12 @@ class Search: return self def as_dict(self): - return deepcopy(self.query) + to_return = {} + + for key, value in self.query.items(): + if key in self._KEYS_WITH_UNIQUE_VALUES: + value = unique(value, self._KEYS_WITH_UNIQUE_VALUES[key]) + + to_return[key] = value + + return to_return diff --git a/lib/cloudinary/static/js/jquery.cloudinary.js b/lib/cloudinary/static/js/jquery.cloudinary.js index cbe7eaff..998054b9 100644 --- a/lib/cloudinary/static/js/jquery.cloudinary.js +++ b/lib/cloudinary/static/js/jquery.cloudinary.js @@ -802,7 +802,7 @@ var slice = [].slice, function TextLayer(options) { var keys; TextLayer.__super__.constructor.call(this, options); - keys = ["resourceType", "resourceType", "fontFamily", "fontSize", "fontWeight", "fontStyle", "textDecoration", "textAlign", "stroke", "letterSpacing", "lineSpacing", "fontHinting", "fontAntialiasing", "text"]; + keys = ["resourceType", "resourceType", "fontFamily", "fontSize", "fontWeight", "fontStyle", "textDecoration", "textAlign", "stroke", "letterSpacing", "lineSpacing", "fontHinting", "fontAntialiasing", "text", "textStyle"]; if (options != null) { keys.forEach((function(_this) { return function(key) { @@ -886,6 +886,11 @@ var slice = [].slice, return this; }; + TextLayer.prototype.textStyle = function(textStyle) { + this.options.textStyle = textStyle; + return this; + }; + /** * generate the string representation of the layer @@ -921,6 +926,10 @@ var slice = [].slice, }; TextLayer.prototype.textStyleIdentifier = function() { + // Note: if a text-style argument is provided as a whole, it overrides everything else, no mix and match. + if (!Util.isEmpty(this.options.textStyle)) { + return this.options.textStyle; + } var components; components = []; if (this.options.fontWeight !== "normal") { diff --git a/lib/cloudinary/uploader.py b/lib/cloudinary/uploader.py index 9ee1c77d..3b1c63b3 100644 --- a/lib/cloudinary/uploader.py +++ b/lib/cloudinary/uploader.py @@ -3,15 +3,14 @@ import json import os import socket -import certifi from six import string_types -from urllib3 import PoolManager, ProxyManager from urllib3.exceptions import HTTPError import cloudinary from cloudinary import utils -from cloudinary.exceptions import Error from cloudinary.cache.responsive_breakpoints_cache import instance as responsive_breakpoints_cache_instance +from cloudinary.exceptions import Error +from cloudinary.utils import build_eager try: from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox @@ -24,6 +23,11 @@ try: # Python 2.7+ except ImportError: from urllib3.packages.ordered_dict import OrderedDict +try: # Python 3.4+ + from pathlib import Path as PathLibPathType +except ImportError: + PathLibPathType = None + if is_appengine_sandbox(): # AppEngineManager uses AppEngine's URLFetch API behind the scenes _http = AppEngineManager() @@ -58,7 +62,12 @@ def upload_image(file, **options): def upload_resource(file, **options): - result = upload_large(file, **options) + upload_func = upload + if hasattr(file, 'size') and file.size > UPLOAD_LARGE_CHUNK_SIZE: + upload_func = upload_large + + result = upload_func(file, **options) + return cloudinary.CloudinaryResource( result["public_id"], version=str(result["version"]), format=result.get("format"), type=result["type"], @@ -363,6 +372,39 @@ def text(text, **options): return call_api("text", params, **options) +_SLIDESHOW_PARAMS = [ + "notification_url", + "public_id", + "overwrite", + "upload_preset", +] + + +def create_slideshow(**options): + """ + Creates auto-generated video slideshows. + + :param options: The optional parameters. See the upload API documentation. + + :return: a dictionary with details about created slideshow + """ + options["resource_type"] = options.get("resource_type", "video") + + params = {param_name: options.get(param_name) for param_name in _SLIDESHOW_PARAMS} + + serialized_params = { + "timestamp": utils.now(), + "transformation": build_eager(options.get("transformation")), + "manifest_transformation": build_eager(options.get("manifest_transformation")), + "manifest_json": options.get("manifest_json") and utils.json_encode(options.get("manifest_json")), + "tags": options.get("tags") and utils.encode_list(utils.build_array(options["tags"])), + } + + params.update(serialized_params) + + return call_api("create_slideshow", params, **options) + + def _save_responsive_breakpoints_to_cache(result): """ Saves responsive breakpoints parsed from upload result to cache @@ -427,7 +469,10 @@ def call_api(action, params, http_headers=None, return_error=False, unsigned=Fal if file: filename = options.get("filename") # Custom filename provided by user (relevant only for streams and files) - if isinstance(file, string_types): + if PathLibPathType and isinstance(file, PathLibPathType): + name = filename or file.name + data = file.read_bytes() + elif isinstance(file, string_types): if utils.is_remote_url(file): # URL name = None diff --git a/lib/cloudinary/utils.py b/lib/cloudinary/utils.py index 276a0a7d..bad3b991 100644 --- a/lib/cloudinary/utils.py +++ b/lib/cloudinary/utils.py @@ -70,6 +70,7 @@ __URL_KEYS = [ __SIMPLE_UPLOAD_PARAMS = [ "public_id", + "public_id_prefix", "callback", "format", "type", @@ -80,6 +81,8 @@ __SIMPLE_UPLOAD_PARAMS = [ "colors", "use_filename", "unique_filename", + "display_name", + "use_filename_as_display_name", "discard_original_filename", "filename_override", "invalidate", @@ -89,6 +92,7 @@ __SIMPLE_UPLOAD_PARAMS = [ "eval", "proxy", "folder", + "asset_folder", "overwrite", "moderation", "raw_convert", @@ -526,6 +530,9 @@ def process_video_codec_param(param): out_param = out_param + ':' + param['profile'] if 'level' in param: out_param = out_param + ':' + param['level'] + if param.get('b_frames') is False: + out_param = out_param + ':' + 'bframes_no' + return out_param @@ -1089,6 +1096,10 @@ def build_multi_and_sprite_params(**options): def __process_text_options(layer, layer_parameter): + text_style = str(layer.get("text_style", "")) + if text_style and not text_style.isspace(): + return text_style + font_family = layer.get("font_family") font_size = layer.get("font_size") keywords = [] @@ -1247,7 +1258,7 @@ PREDEFINED_VARS = { "context": "ctx" } -replaceRE = "((\\|\\||>=|<=|&&|!=|>|=|<|/|-|\\+|\\*|\\^)(?=[ _])|(\\$_*[^_ ]+)|(?=|<=|&&|!=|>|=|<|/|-|\\+|\\*|\\^)(?=[ _])|(\\$_*[^_ ]+)|(?