diff --git a/lib/cloudinary/__init__.py b/lib/cloudinary/__init__.py index da7ca9ae..7746e365 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.30.0" +VERSION = "1.32.0" _USER_PLATFORM_DETAILS = "; ".join((platform(), "Python {}".format(python_version()))) @@ -234,6 +234,7 @@ _http_client = HttpClient() # FIXME: circular import issue from cloudinary.search import Search +from cloudinary.search_folders import SearchFolders @python_2_unicode_compatible diff --git a/lib/cloudinary/api.py b/lib/cloudinary/api.py index 73d18fa1..dd34686e 100644 --- a/lib/cloudinary/api.py +++ b/lib/cloudinary/api.py @@ -188,9 +188,9 @@ def _prepare_asset_details_params(**options): :internal """ - return only(options, "exif", "faces", "colors", "image_metadata", "cinemagraph_analysis", + return only(options, "exif", "faces", "colors", "image_metadata", "media_metadata", "cinemagraph_analysis", "pages", "phash", "coordinates", "max_results", "quality_analysis", "derived_next_cursor", - "accessibility_analysis", "versions") + "accessibility_analysis", "versions", "related", "related_next_cursor") def update(public_id, **options): @@ -223,6 +223,8 @@ def update(public_id, **options): params["display_name"] = options.get("display_name") if "unique_display_name" in options: params["unique_display_name"] = options.get("unique_display_name") + if "clear_invalid" in options: + params["clear_invalid"] = options.get("clear_invalid") return call_api("post", uri, params, **options) @@ -293,6 +295,50 @@ def delete_derived_by_transformation(public_ids, transformations, return call_api("delete", uri, params, **options) +def add_related_assets(public_id, assets_to_relate, resource_type="image", type="upload", **options): + """ + Relates an asset to other assets by public IDs. + + :param public_id: The public ID of the asset to update. + :type public_id: str + :param assets_to_relate: The array of up to 10 fully_qualified_public_ids given as resource_type/type/public_id. + :type assets_to_relate: list[str] + :param type: The upload type. Defaults to "upload". + :type type: str + :param resource_type: The type of the resource. Defaults to "image". + :type resource_type: str + :param options: Additional options. + :type options: dict, optional + :return: The result of the command. + :rtype: dict + """ + uri = ["resources", "related_assets", resource_type, type, public_id] + params = {"assets_to_relate": utils.build_array(assets_to_relate)} + return call_json_api("post", uri, params, **options) + + +def delete_related_assets(public_id, assets_to_unrelate, resource_type="image", type="upload", **options): + """ + Unrelates an asset from other assets by public IDs. + + :param public_id: The public ID of the asset to update. + :type public_id: str + :param assets_to_unrelate: The array of up to 10 fully_qualified_public_ids given as resource_type/type/public_id. + :type assets_to_unrelate: list[str] + :param type: The upload type. + :type type: str + :param resource_type: The type of the resource: defaults to "image". + :type resource_type: str + :param options: Additional options. + :type options: dict, optional + :return: The result of the command. + :rtype: dict + """ + uri = ["resources", "related_assets", resource_type, type, public_id] + params = {"assets_to_unrelate": utils.build_array(assets_to_unrelate)} + return call_json_api("delete", uri, params, **options) + + def tags(**options): resource_type = options.pop("resource_type", "image") uri = ["tags", resource_type] diff --git a/lib/cloudinary/api_client/execute_request.py b/lib/cloudinary/api_client/execute_request.py index 4d6b8651..1bd52a25 100644 --- a/lib/cloudinary/api_client/execute_request.py +++ b/lib/cloudinary/api_client/execute_request.py @@ -68,9 +68,9 @@ def execute_request(http_connector, method, params, headers, auth, api_url, **op response = http_connector.request(method.upper(), api_url, processed_params, req_headers, **kw) body = response.data except HTTPError as e: - raise GeneralError("Unexpected error {0}", e.message) + raise GeneralError("Unexpected error %s" % str(e)) except socket.error as e: - raise GeneralError("Socket Error: %s" % (str(e))) + raise GeneralError("Socket Error: %s" % str(e)) try: result = json.loads(body.decode('utf-8')) diff --git a/lib/cloudinary/search.py b/lib/cloudinary/search.py index 0d97e098..0e4b0ee0 100644 --- a/lib/cloudinary/search.py +++ b/lib/cloudinary/search.py @@ -4,7 +4,11 @@ from cloudinary.api_client.call_api import call_json_api from cloudinary.utils import unique -class Search: +class Search(object): + ASSETS = 'resources' + + _endpoint = ASSETS + _KEYS_WITH_UNIQUE_VALUES = { 'sort_by': lambda x: next(iter(x)), 'aggregate': None, @@ -53,7 +57,7 @@ class Search: def execute(self, **options): """Execute the search and return results.""" options["content_type"] = 'application/json' - uri = ['resources', 'search'] + uri = [self._endpoint, 'search'] return call_json_api('post', uri, self.as_dict(), **options) def _add(self, name, value): @@ -72,3 +76,7 @@ class Search: to_return[key] = value return to_return + + def endpoint(self, endpoint): + self._endpoint = endpoint + return self diff --git a/lib/cloudinary/search_folders.py b/lib/cloudinary/search_folders.py new file mode 100644 index 00000000..87e2601d --- /dev/null +++ b/lib/cloudinary/search_folders.py @@ -0,0 +1,10 @@ +from cloudinary import Search + + +class SearchFolders(Search): + FOLDERS = 'folders' + + def __init__(self): + super(SearchFolders, self).__init__() + + self.endpoint(self.FOLDERS) diff --git a/lib/cloudinary/uploader.py b/lib/cloudinary/uploader.py index 3b118142..188c06e0 100644 --- a/lib/cloudinary/uploader.py +++ b/lib/cloudinary/uploader.py @@ -168,7 +168,8 @@ def update_metadata(metadata, public_ids, **options): "timestamp": utils.now(), "metadata": utils.encode_context(metadata), "public_ids": utils.build_array(public_ids), - "type": options.get("type") + "type": options.get("type"), + "clear_invalid": options.get("clear_invalid") } return call_api("metadata", params, **options) diff --git a/lib/cloudinary/utils.py b/lib/cloudinary/utils.py index f7f94512..7aac086b 100644 --- a/lib/cloudinary/utils.py +++ b/lib/cloudinary/utils.py @@ -78,6 +78,7 @@ __SIMPLE_UPLOAD_PARAMS = [ "backup", "faces", "image_metadata", + "media_metadata", "exif", "colors", "use_filename", @@ -1052,7 +1053,8 @@ def build_custom_headers(headers): def build_upload_params(**options): - params = {param_name: options.get(param_name) for param_name in __SIMPLE_UPLOAD_PARAMS} + params = {param_name: options.get(param_name) for param_name in __SIMPLE_UPLOAD_PARAMS if param_name in options} + params["upload_preset"] = params.pop("upload_preset", cloudinary.config().upload_preset) serialized_params = { "timestamp": now(), @@ -1577,3 +1579,19 @@ def unique(collection, key=None): to_return[key(element)] = element return list(to_return.values()) + + +def fq_public_id(public_id, resource_type="image", type="upload"): + """ + Returns the fully qualified public id of form resource_type/type/public_id. + + :param public_id: The public ID of the asset. + :type public_id: str + :param resource_type: The type of the asset. Defaults to "image". + :type resource_type: str + :param type: The upload type. Defaults to "upload". + :type type: str + + :return: + """ + return "{resource_type}/{type}/{public_id}".format(resource_type=resource_type, type=type, public_id=public_id) diff --git a/lib/urllib3/_version.py b/lib/urllib3/_version.py index 308d7f28..7c031661 100644 --- a/lib/urllib3/_version.py +++ b/lib/urllib3/_version.py @@ -1,2 +1,2 @@ # This file is protected via CODEOWNERS -__version__ = "1.26.13" +__version__ = "1.26.14" diff --git a/lib/urllib3/contrib/appengine.py b/lib/urllib3/contrib/appengine.py index f91bdd6e..a5a6d910 100644 --- a/lib/urllib3/contrib/appengine.py +++ b/lib/urllib3/contrib/appengine.py @@ -224,7 +224,7 @@ class AppEngineManager(RequestMethods): ) # Check if we should retry the HTTP response. - has_retry_after = bool(http_response.getheader("Retry-After")) + has_retry_after = bool(http_response.headers.get("Retry-After")) if retries.is_retry(method, http_response.status, has_retry_after): retries = retries.increment(method, url, response=http_response, _pool=self) log.debug("Retry: %s", url) diff --git a/lib/urllib3/contrib/ntlmpool.py b/lib/urllib3/contrib/ntlmpool.py index 41a8fd17..47166575 100644 --- a/lib/urllib3/contrib/ntlmpool.py +++ b/lib/urllib3/contrib/ntlmpool.py @@ -69,7 +69,7 @@ class NTLMConnectionPool(HTTPSConnectionPool): log.debug("Request headers: %s", headers) conn.request("GET", self.authurl, None, headers) res = conn.getresponse() - reshdr = dict(res.getheaders()) + reshdr = dict(res.headers) log.debug("Response status: %s %s", res.status, res.reason) log.debug("Response headers: %s", reshdr) log.debug("Response data: %s [...]", res.read(100)) @@ -101,7 +101,7 @@ class NTLMConnectionPool(HTTPSConnectionPool): conn.request("GET", self.authurl, None, headers) res = conn.getresponse() log.debug("Response status: %s %s", res.status, res.reason) - log.debug("Response headers: %s", dict(res.getheaders())) + log.debug("Response headers: %s", dict(res.headers)) log.debug("Response data: %s [...]", res.read()[:100]) if res.status != 200: if res.status == 401: diff --git a/lib/urllib3/response.py b/lib/urllib3/response.py index 8f1b4fa8..0bd13d40 100644 --- a/lib/urllib3/response.py +++ b/lib/urllib3/response.py @@ -666,7 +666,7 @@ class HTTPResponse(io.IOBase): def getheaders(self): warnings.warn( "HTTPResponse.getheaders() is deprecated and will be removed " - "in urllib3 v2.1.0. Instead access HTTResponse.headers directly.", + "in urllib3 v2.1.0. Instead access HTTPResponse.headers directly.", category=DeprecationWarning, stacklevel=2, ) @@ -675,7 +675,7 @@ class HTTPResponse(io.IOBase): def getheader(self, name, default=None): warnings.warn( "HTTPResponse.getheader() is deprecated and will be removed " - "in urllib3 v2.1.0. Instead use HTTResponse.headers.get(name, default).", + "in urllib3 v2.1.0. Instead use HTTPResponse.headers.get(name, default).", category=DeprecationWarning, stacklevel=2, ) diff --git a/lib/urllib3/util/url.py b/lib/urllib3/util/url.py index 94f1b8d4..3a169a43 100644 --- a/lib/urllib3/util/url.py +++ b/lib/urllib3/util/url.py @@ -63,7 +63,7 @@ IPV6_ADDRZ_RE = re.compile("^" + IPV6_ADDRZ_PAT + "$") BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + IPV6_ADDRZ_PAT[2:-2] + "$") ZONE_ID_RE = re.compile("(" + ZONE_ID_PAT + r")\]$") -_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*([0-9]{0,5}))?$") % ( +_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*?(|0|[1-9][0-9]{0,4}))?$") % ( REG_NAME_PAT, IPV4_PAT, IPV6_ADDRZ_PAT,