1# Copyright Cloudinary
2
3import datetime
4import email.utils
5import json
6import socket
7
8import urllib3
9from six import string_types
10from urllib3.exceptions import HTTPError
11
12import cloudinary
13from cloudinary import utils
14from cloudinary.api_client.call_api import (
15    call_api,
16    call_metadata_api,
17    call_json_api
18)
19from cloudinary.exceptions import (
20    BadRequest,
21    AuthorizationRequired,
22    NotAllowed,
23    NotFound,
24    AlreadyExists,
25    RateLimited,
26    GeneralError
27)
28
29
30def ping(**options):
31    return call_api("get", ["ping"], {}, **options)
32
33
34def usage(**options):
35    """Get account usage details.
36
37    Get a report on the status of your Cloudinary account usage details, including storage, credits, bandwidth,
38    requests, number of resources, and add-on usage. Note that numbers are updated periodically.
39
40    See: `Get account usage details
41    <https://cloudinary.com/documentation/admin_api#get_account_usage_details>`_
42
43    :param options:     Additional options
44    :type options:      dict, optional
45    :return:            Detailed usage information
46    :rtype:             Response
47    """
48    date = options.pop("date", None)
49    uri = ["usage"]
50    if date:
51        if isinstance(date, datetime.date):
52            date = utils.encode_date_to_usage_api_format(date)
53        uri.append(date)
54    return call_api("get", uri, {}, **options)
55
56
57def resource_types(**options):
58    return call_api("get", ["resources"], {}, **options)
59
60
61def resources(**options):
62    resource_type = options.pop("resource_type", "image")
63    upload_type = options.pop("type", None)
64    uri = ["resources", resource_type]
65    if upload_type:
66        uri.append(upload_type)
67    params = only(options, "next_cursor", "max_results", "prefix", "tags",
68                  "context", "moderations", "direction", "start_at", "metadata")
69    return call_api("get", uri, params, **options)
70
71
72def resources_by_tag(tag, **options):
73    resource_type = options.pop("resource_type", "image")
74    uri = ["resources", resource_type, "tags", tag]
75    params = only(options, "next_cursor", "max_results", "tags",
76                  "context", "moderations", "direction", "metadata")
77    return call_api("get", uri, params, **options)
78
79
80def resources_by_moderation(kind, status, **options):
81    resource_type = options.pop("resource_type", "image")
82    uri = ["resources", resource_type, "moderations", kind, status]
83    params = only(options, "next_cursor", "max_results", "tags",
84                  "context", "moderations", "direction", "metadata")
85    return call_api("get", uri, params, **options)
86
87
88def resources_by_ids(public_ids, **options):
89    resource_type = options.pop("resource_type", "image")
90    upload_type = options.pop("type", "upload")
91    uri = ["resources", resource_type, upload_type]
92    params = dict(only(options, "tags", "moderations", "context"), public_ids=public_ids)
93    return call_api("get", uri, params, **options)
94
95
96def resources_by_asset_ids(asset_ids, **options):
97    """Retrieves the resources (assets) indicated in the asset IDs.
98    This method does not return deleted assets even if they have been backed up.
99
100    See: `Get resources by context API reference
101    <https://cloudinary.com/documentation/admin_api#get_resources>`_
102
103    :param asset_ids:   The requested asset IDs.
104    :type asset_ids:    list[str]
105    :param options:     Additional options
106    :type options:      dict, optional
107    :return:            Resources (assets) as indicated in the asset IDs
108    :rtype:             Response
109    """
110    uri = ["resources", 'by_asset_ids']
111    params = dict(only(options, "tags", "moderations", "context"), asset_ids=asset_ids)
112    return call_api("get", uri, params, **options)
113
114
115def resources_by_context(key, value=None, **options):
116    """Retrieves resources (assets) with a specified context key.
117    This method does not return deleted assets even if they have been backed up.
118
119    See: `Get resources by context API reference
120    <https://cloudinary.com/documentation/admin_api#get_resources_by_context>`_
121
122    :param key:         Only assets with this context key are returned
123    :type key:          str
124    :param value:       Only assets with this value for the context key are returned
125    :type value:        str, optional
126    :param options:     Additional options
127    :type options:      dict, optional
128    :return:            Resources (assets) with a specified context key
129    :rtype:             Response
130    """
131    resource_type = options.pop("resource_type", "image")
132    uri = ["resources", resource_type, "context"]
133    params = only(options, "next_cursor", "max_results", "tags",
134                "context", "moderations", "direction", "metadata")
135    params["key"] = key
136    if value is not None:
137        params["value"] = value
138    return call_api("get", uri, params, **options)
139
140
141def resource(public_id, **options):
142    resource_type = options.pop("resource_type", "image")
143    upload_type = options.pop("type", "upload")
144    uri = ["resources", resource_type, upload_type, public_id]
145    params = _prepare_asset_details_params(**options)
146    return call_api("get", uri, params, **options)
147
148
149def resource_by_asset_id(asset_id, **options):
150    """
151    Returns the details of the specified asset and all its derived assets by asset id.
152
153    :param asset_id:    The Asset ID of the asset
154    :type asset_id:     string
155    :param options:     Additional options
156    :type options:      dict, optional
157    :return:            Resource (asset) of a specific asset_id
158    :rtype:             Response
159    """
160    uri = ["resources", asset_id]
161    params = _prepare_asset_details_params(**options)
162    return call_api("get", uri, params, **options)
163
164
165def _prepare_asset_details_params(**options):
166    """
167    Prepares optional parameters for resource_by_asset_id API calls.
168
169    :param options: Additional options
170    :return: Optional parameters
171
172    :internal
173    """
174    return only(options, "exif", "faces", "colors", "image_metadata", "cinemagraph_analysis",
175                "pages", "phash", "coordinates", "max_results", "quality_analysis", "derived_next_cursor",
176                "accessibility_analysis", "versions")
177
178
179def update(public_id, **options):
180    resource_type = options.pop("resource_type", "image")
181    upload_type = options.pop("type", "upload")
182    uri = ["resources", resource_type, upload_type, public_id]
183    params = only(options, "moderation_status", "raw_convert",
184                  "quality_override", "ocr",
185                  "categorization", "detection", "similarity_search",
186                  "background_removal", "notification_url")
187    if "tags" in options:
188        params["tags"] = ",".join(utils.build_array(options["tags"]))
189    if "face_coordinates" in options:
190        params["face_coordinates"] = utils.encode_double_array(
191            options.get("face_coordinates"))
192    if "custom_coordinates" in options:
193        params["custom_coordinates"] = utils.encode_double_array(
194            options.get("custom_coordinates"))
195    if "context" in options:
196        params["context"] = utils.encode_context(options.get("context"))
197    if "auto_tagging" in options:
198        params["auto_tagging"] = str(options.get("auto_tagging"))
199    if "access_control" in options:
200        params["access_control"] = utils.json_encode(utils.build_list_of_dicts(options.get("access_control")))
201
202    return call_api("post", uri, params, **options)
203
204
205def delete_resources(public_ids, **options):
206    resource_type = options.pop("resource_type", "image")
207    upload_type = options.pop("type", "upload")
208    uri = ["resources", resource_type, upload_type]
209    params = __delete_resource_params(options, public_ids=public_ids)
210    return call_api("delete", uri, params, **options)
211
212
213def delete_resources_by_prefix(prefix, **options):
214    resource_type = options.pop("resource_type", "image")
215    upload_type = options.pop("type", "upload")
216    uri = ["resources", resource_type, upload_type]
217    params = __delete_resource_params(options, prefix=prefix)
218    return call_api("delete", uri, params, **options)
219
220
221def delete_all_resources(**options):
222    resource_type = options.pop("resource_type", "image")
223    upload_type = options.pop("type", "upload")
224    uri = ["resources", resource_type, upload_type]
225    params = __delete_resource_params(options, all=True)
226    return call_api("delete", uri, params, **options)
227
228
229def delete_resources_by_tag(tag, **options):
230    resource_type = options.pop("resource_type", "image")
231    uri = ["resources", resource_type, "tags", tag]
232    params = __delete_resource_params(options)
233    return call_api("delete", uri, params, **options)
234
235
236def delete_derived_resources(derived_resource_ids, **options):
237    uri = ["derived_resources"]
238    params = {"derived_resource_ids": derived_resource_ids}
239    return call_api("delete", uri, params, **options)
240
241
242def delete_derived_by_transformation(public_ids, transformations,
243                                     resource_type='image', type='upload', invalidate=None,
244                                     **options):
245    """Delete derived resources of public ids, identified by transformations
246
247    :param public_ids: the base resources
248    :type public_ids: list of str
249    :param transformations: the transformation of derived resources, optionally including the format
250    :type transformations: list of (dict or str)
251    :param type: The upload type
252    :type type: str
253    :param resource_type: The type of the resource: defaults to "image"
254    :type resource_type: str
255    :param invalidate: (optional) True to invalidate the resources after deletion
256    :type invalidate: bool
257    :return: a list of the public ids for which derived resources were deleted
258    :rtype: dict
259    """
260    uri = ["resources", resource_type, type]
261    if not isinstance(public_ids, list):
262        public_ids = [public_ids]
263    params = {"public_ids": public_ids,
264              "transformations": utils.build_eager(transformations),
265              "keep_original": True}
266    if invalidate is not None:
267        params['invalidate'] = invalidate
268    return call_api("delete", uri, params, **options)
269
270
271def tags(**options):
272    resource_type = options.pop("resource_type", "image")
273    uri = ["tags", resource_type]
274    return call_api("get", uri, only(options, "next_cursor", "max_results", "prefix"), **options)
275
276
277def transformations(**options):
278    uri = ["transformations"]
279    params = only(options, "named", "next_cursor", "max_results")
280
281    return call_api("get", uri, params, **options)
282
283
284def transformation(transformation, **options):
285    uri = ["transformations"]
286
287    params = only(options, "next_cursor", "max_results")
288    params["transformation"] = utils.build_single_eager(transformation)
289
290    return call_api("get", uri, params, **options)
291
292
293def delete_transformation(transformation, **options):
294    uri = ["transformations"]
295
296    params = {"transformation": utils.build_single_eager(transformation)}
297
298    return call_api("delete", uri, params, **options)
299
300
301# updates - currently only supported update is the "allowed_for_strict"
302# boolean flag and unsafe_update
303def update_transformation(transformation, **options):
304    uri = ["transformations"]
305
306    updates = only(options, "allowed_for_strict")
307
308    if "unsafe_update" in options:
309        updates["unsafe_update"] = transformation_string(options.get("unsafe_update"))
310
311    updates["transformation"] = utils.build_single_eager(transformation)
312
313    return call_api("put", uri, updates, **options)
314
315
316def create_transformation(name, definition, **options):
317    uri = ["transformations"]
318
319    params = {"name": name, "transformation": utils.build_single_eager(definition)}
320
321    return call_api("post", uri, params, **options)
322
323
324def publish_by_ids(public_ids, **options):
325    resource_type = options.pop("resource_type", "image")
326    uri = ["resources", resource_type, "publish_resources"]
327    params = dict(only(options, "type", "overwrite", "invalidate"), public_ids=public_ids)
328    return call_api("post", uri, params, **options)
329
330
331def publish_by_prefix(prefix, **options):
332    resource_type = options.pop("resource_type", "image")
333    uri = ["resources", resource_type, "publish_resources"]
334    params = dict(only(options, "type", "overwrite", "invalidate"), prefix=prefix)
335    return call_api("post", uri, params, **options)
336
337
338def publish_by_tag(tag, **options):
339    resource_type = options.pop("resource_type", "image")
340    uri = ["resources", resource_type, "publish_resources"]
341    params = dict(only(options, "type", "overwrite", "invalidate"), tag=tag)
342    return call_api("post", uri, params, **options)
343
344
345def upload_presets(**options):
346    uri = ["upload_presets"]
347    return call_api("get", uri, only(options, "next_cursor", "max_results"), **options)
348
349
350def upload_preset(name, **options):
351    uri = ["upload_presets", name]
352    return call_api("get", uri, only(options, "max_results"), **options)
353
354
355def delete_upload_preset(name, **options):
356    uri = ["upload_presets", name]
357    return call_api("delete", uri, {}, **options)
358
359
360def update_upload_preset(name, **options):
361    uri = ["upload_presets", name]
362    params = utils.build_upload_params(**options)
363    params = utils.cleanup_params(params)
364    params.update(only(options, "unsigned", "disallow_public_id", "live"))
365    return call_api("put", uri, params, **options)
366
367
368def create_upload_preset(**options):
369    uri = ["upload_presets"]
370    params = utils.build_upload_params(**options)
371    params = utils.cleanup_params(params)
372    params.update(only(options, "unsigned", "disallow_public_id", "name", "live"))
373    return call_api("post", uri, params, **options)
374
375
376def create_folder(path, **options):
377    return call_api("post", ["folders", path], {}, **options)
378
379
380def root_folders(**options):
381    return call_api("get", ["folders"], only(options, "next_cursor", "max_results"), **options)
382
383
384def subfolders(of_folder_path, **options):
385    return call_api("get", ["folders", of_folder_path], only(options, "next_cursor", "max_results"), **options)
386
387
388def delete_folder(path, **options):
389    """Deletes folder
390
391    Deleted folder must be empty, but can have descendant empty sub folders
392
393    :param path: The folder to delete
394    :param options: Additional options
395
396    :rtype: Response
397    """
398    return call_api("delete", ["folders", path], {}, **options)
399
400
401def restore(public_ids, **options):
402    resource_type = options.pop("resource_type", "image")
403    upload_type = options.pop("type", "upload")
404    uri = ["resources", resource_type, upload_type, "restore"]
405    params = dict(public_ids=public_ids, **only(options, "versions"))
406    return call_json_api("post", uri, params, **options)
407
408
409def upload_mappings(**options):
410    uri = ["upload_mappings"]
411    return call_api("get", uri, only(options, "next_cursor", "max_results"), **options)
412
413
414def upload_mapping(name, **options):
415    uri = ["upload_mappings"]
416    params = dict(folder=name)
417    return call_api("get", uri, params, **options)
418
419
420def delete_upload_mapping(name, **options):
421    uri = ["upload_mappings"]
422    params = dict(folder=name)
423    return call_api("delete", uri, params, **options)
424
425
426def update_upload_mapping(name, **options):
427    uri = ["upload_mappings"]
428    params = dict(folder=name)
429    params.update(only(options, "template"))
430    return call_api("put", uri, params, **options)
431
432
433def create_upload_mapping(name, **options):
434    uri = ["upload_mappings"]
435    params = dict(folder=name)
436    params.update(only(options, "template"))
437    return call_api("post", uri, params, **options)
438
439
440def list_streaming_profiles(**options):
441    uri = ["streaming_profiles"]
442    return call_api('GET', uri, {}, **options)
443
444
445def get_streaming_profile(name, **options):
446    uri = ["streaming_profiles", name]
447    return call_api('GET', uri, {}, **options)
448
449
450def delete_streaming_profile(name, **options):
451    uri = ["streaming_profiles", name]
452    return call_api('DELETE', uri, {}, **options)
453
454
455def create_streaming_profile(name, **options):
456    uri = ["streaming_profiles"]
457    params = __prepare_streaming_profile_params(**options)
458    params["name"] = name
459    return call_api('POST', uri, params, **options)
460
461
462def update_streaming_profile(name, **options):
463    uri = ["streaming_profiles", name]
464    params = __prepare_streaming_profile_params(**options)
465    return call_api('PUT', uri, params, **options)
466
467
468def only(source, *keys):
469    return {key: source[key] for key in keys if key in source}
470
471
472def transformation_string(transformation):
473    if isinstance(transformation, string_types):
474        return transformation
475    else:
476        return cloudinary.utils.generate_transformation_string(**transformation)[0]
477
478
479def __prepare_streaming_profile_params(**options):
480    params = only(options, "display_name")
481    if "representations" in options:
482        representations = [{"transformation": transformation_string(trans)}
483                           for trans in options["representations"]]
484        params["representations"] = json.dumps(representations)
485    return params
486
487
488def __delete_resource_params(options, **params):
489    p = dict(transformations=utils.build_eager(options.get('transformations')),
490             **only(options, "keep_original", "next_cursor", "invalidate"))
491    p.update(params)
492    return p
493
494
495def list_metadata_fields(**options):
496    """Returns a list of all metadata field definitions
497
498    See: `Get metadata fields API reference <https://cloudinary.com/documentation/admin_api#get_metadata_fields>`_
499
500    :param options: Additional options
501
502    :rtype: Response
503    """
504    return call_metadata_api("get", [], {}, **options)
505
506
507def metadata_field_by_field_id(field_external_id, **options):
508    """Gets a metadata field by external id
509
510    See: `Get metadata field by external ID API reference
511    <https://cloudinary.com/documentation/admin_api#get_a_metadata_field_by_external_id>`_
512
513    :param field_external_id: The ID of the metadata field to retrieve
514    :param options: Additional options
515
516    :rtype: Response
517    """
518    uri = [field_external_id]
519    return call_metadata_api("get", uri, {}, **options)
520
521
522def add_metadata_field(field, **options):
523    """Creates a new metadata field definition
524
525    See: `Create metadata field API reference <https://cloudinary.com/documentation/admin_api#create_a_metadata_field>`_
526
527    :param field: The field to add
528    :param options: Additional options
529
530    :rtype: Response
531    """
532    params = only(field, "type", "external_id", "label", "mandatory",
533                  "default_value", "validation", "datasource")
534    return call_metadata_api("post", [], params, **options)
535
536
537def update_metadata_field(field_external_id, field, **options):
538    """Updates a metadata field by external id
539
540    Updates a metadata field definition (partially, no need to pass the entire
541    object) passed as JSON data.
542
543    See `Generic structure of a metadata field
544    <https://cloudinary.com/documentation/admin_api#generic_structure_of_a_metadata_field>`_ for details.
545
546    :param field_external_id: The id of the metadata field to update
547    :param field: The field definition
548    :param options: Additional options
549
550    :rtype: Response
551    """
552    uri = [field_external_id]
553    params = only(field, "label", "mandatory", "default_value", "validation")
554    return call_metadata_api("put", uri, params, **options)
555
556
557def delete_metadata_field(field_external_id, **options):
558    """Deletes a metadata field definition.
559    The field should no longer be considered a valid candidate for all other endpoints
560
561    See: `Delete metadata field API reference
562    <https://cloudinary.com/documentation/admin_api#delete_a_metadata_field_by_external_id>`_
563
564    :param field_external_id: The external id of the field to delete
565    :param options: Additional options
566
567    :return: An array with a "message" key. "ok" value indicates a successful deletion.
568    :rtype: Response
569    """
570    uri = [field_external_id]
571    return call_metadata_api("delete", uri, {}, **options)
572
573
574def delete_datasource_entries(field_external_id, entries_external_id, **options):
575    """Deletes entries in a metadata field datasource
576
577    Deletes (blocks) the datasource entries for a specified metadata field
578    definition. Sets the state of the entries to inactive. This is a soft delete,
579    the entries still exist under the hood and can be activated again with the
580    restore datasource entries method.
581
582    See: `Delete entries in a metadata field datasource API reference
583    <https://cloudinary.com/documentation/admin_api#delete_entries_in_a_metadata_field_datasource>`_
584
585    :param field_external_id: The id of the field to update
586    :param  entries_external_id: The ids of all the entries to delete from the
587                                 datasource
588    :param options: Additional options
589
590    :rtype: Response
591    """
592    uri = [field_external_id, "datasource"]
593    params = {"external_ids": entries_external_id}
594    return call_metadata_api("delete", uri, params, **options)
595
596
597def update_metadata_field_datasource(field_external_id, entries_external_id, **options):
598    """Updates a metadata field datasource
599
600    Updates the datasource of a supported field type (currently only enum and set),
601    passed as JSON data. The update is partial: datasource entries with an
602    existing external_id will be updated and entries with new external_id's (or
603    without external_id's) will be appended.
604
605    See: `Update a metadata field datasource API reference
606    <https://cloudinary.com/documentation/admin_api#update_a_metadata_field_datasource>`_
607
608    :param field_external_id: The external id of the field to update
609    :param entries_external_id:
610    :param options: Additional options
611
612    :rtype: Response
613    """
614    values = []
615    for item in entries_external_id:
616        external = only(item, "external_id", "value")
617        if external:
618            values.append(external)
619
620    uri = [field_external_id, "datasource"]
621    params = {"values": values}
622    return call_metadata_api("put", uri, params, **options)
623
624
625def restore_metadata_field_datasource(field_external_id, entries_external_ids, **options):
626    """Restores entries in a metadata field datasource
627
628    Restores (unblocks) any previously deleted datasource entries for a specified
629    metadata field definition.
630    Sets the state of the entries to active.
631
632    See: `Restore entries in a metadata field datasource API reference
633    <https://cloudinary.com/documentation/admin_api#restore_entries_in_a_metadata_field_datasource>`_
634
635    :param field_external_id: The ID of the metadata field
636    :param entries_external_ids: An array of IDs of datasource entries to restore
637                                 (unblock)
638    :param options: Additional options
639
640    :rtype: Response
641    """
642    uri = [field_external_id, 'datasource_restore']
643    params = {"external_ids": entries_external_ids}
644    return call_metadata_api("post", uri, params, **options)
645
646
647def reorder_metadata_field_datasource(field_external_id, order_by, direction=None, **options):
648    """Reorders metadata field datasource. Currently, supports only value.
649
650    :param field_external_id: The ID of the metadata field.
651    :param order_by: Criteria for the order. Currently, supports only value.
652    :param direction: Optional (gets either asc or desc).
653    :param options: Additional options.
654
655    :rtype: Response
656    """
657    uri = [field_external_id, 'datasource', 'order']
658    params = {'order_by': order_by, 'direction': direction}
659    return call_metadata_api('post', uri, params, **options)
660
661
662def reorder_metadata_fields(order_by, direction=None, **options):
663    """Reorders metadata fields.
664
665    :param order_by: Criteria for the order (one of the fields 'label', 'external_id', 'created_at').
666    :param direction: Optional (gets either asc or desc).
667    :param options: Additional options.
668
669    :rtype: Response
670    """
671    uri = ['order']
672    params = {'order_by': order_by, 'direction': direction}
673    return call_metadata_api('put', uri, params, **options)
674