cloudinary 1.46.1__tar.gz → 1.46.3__tar.gz

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.
Files changed (94) hide show
  1. {cloudinary-1.46.1/cloudinary.egg-info → cloudinary-1.46.3}/PKG-INFO +1 -1
  2. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/__init__.py +1 -1
  3. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/api.py +2 -2
  4. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/auth_token.py +1 -1
  5. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/provisioning/account.py +1 -1
  6. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/uploader.py +46 -5
  7. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/utils.py +14 -1
  8. {cloudinary-1.46.1 → cloudinary-1.46.3/cloudinary.egg-info}/PKG-INFO +1 -1
  9. {cloudinary-1.46.1 → cloudinary-1.46.3}/pyproject.toml +1 -1
  10. {cloudinary-1.46.1 → cloudinary-1.46.3}/setup.py +1 -1
  11. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_api.py +6 -3
  12. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_auth_token.py +9 -0
  13. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_provisioning_api.py +16 -0
  14. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_uploader.py +68 -4
  15. {cloudinary-1.46.1 → cloudinary-1.46.3}/LICENSE +0 -0
  16. {cloudinary-1.46.1 → cloudinary-1.46.3}/MANIFEST.in +0 -0
  17. {cloudinary-1.46.1 → cloudinary-1.46.3}/README.md +0 -0
  18. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/api_client/__init__.py +0 -0
  19. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/api_client/call_account_api.py +0 -0
  20. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/api_client/call_api.py +0 -0
  21. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/api_client/execute_request.py +0 -0
  22. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/api_client/tcp_keep_alive_manager.py +0 -0
  23. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/cache/__init__.py +0 -0
  24. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/cache/adapter/__init__.py +0 -0
  25. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/cache/adapter/cache_adapter.py +0 -0
  26. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/cache/adapter/key_value_cache_adapter.py +0 -0
  27. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/cache/responsive_breakpoints_cache.py +0 -0
  28. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/cache/storage/__init__.py +0 -0
  29. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/cache/storage/file_system_key_value_storage.py +0 -0
  30. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/cache/storage/key_value_storage.py +0 -0
  31. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/compat.py +0 -0
  32. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/README.md +0 -0
  33. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/configure-cloudinary.md +0 -0
  34. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/get-credentials.md +0 -0
  35. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/import-and-call.md +0 -0
  36. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/moderate-upload.md +0 -0
  37. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/platform-capabilities.md +0 -0
  38. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/search-and-manage-assets.md +0 -0
  39. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/sign-browser-upload.md +0 -0
  40. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/transform-and-deliver-image.md +0 -0
  41. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/transform-and-deliver-video.md +0 -0
  42. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/troubleshoot-errors.md +0 -0
  43. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/upload-image.md +0 -0
  44. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/upload-large-video.md +0 -0
  45. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/use-structured-metadata.md +0 -0
  46. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/docs/use-with-django.md +0 -0
  47. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/exceptions.py +0 -0
  48. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/forms.py +0 -0
  49. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/http_client.py +0 -0
  50. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/models.py +0 -0
  51. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/poster/__init__.py +0 -0
  52. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/poster/encode.py +0 -0
  53. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/poster/streaminghttp.py +0 -0
  54. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/provisioning/__init__.py +0 -0
  55. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/provisioning/account_config.py +0 -0
  56. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/search.py +0 -0
  57. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/search_folders.py +0 -0
  58. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/static/cloudinary/html/cloudinary_cors.html +0 -0
  59. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/static/cloudinary/js/canvas-to-blob.min.js +0 -0
  60. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/static/cloudinary/js/jquery.cloudinary.js +0 -0
  61. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/static/cloudinary/js/jquery.fileupload-image.js +0 -0
  62. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/static/cloudinary/js/jquery.fileupload-process.js +0 -0
  63. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/static/cloudinary/js/jquery.fileupload-validate.js +0 -0
  64. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/static/cloudinary/js/jquery.fileupload.js +0 -0
  65. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/static/cloudinary/js/jquery.iframe-transport.js +0 -0
  66. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/static/cloudinary/js/jquery.ui.widget.js +0 -0
  67. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/static/cloudinary/js/load-image.all.min.js +0 -0
  68. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/templates/cloudinary_direct_upload.html +0 -0
  69. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/templates/cloudinary_includes.html +0 -0
  70. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/templates/cloudinary_js_config.html +0 -0
  71. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/templatetags/__init__.py +0 -0
  72. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary/templatetags/cloudinary.py +0 -0
  73. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary.egg-info/SOURCES.txt +0 -0
  74. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary.egg-info/dependency_links.txt +0 -0
  75. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary.egg-info/not-zip-safe +0 -0
  76. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary.egg-info/requires.txt +0 -0
  77. {cloudinary-1.46.1 → cloudinary-1.46.3}/cloudinary.egg-info/top_level.txt +0 -0
  78. {cloudinary-1.46.1 → cloudinary-1.46.3}/setup.cfg +0 -0
  79. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_api_authorization.py +0 -0
  80. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_archive.py +0 -0
  81. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_cloudinary_resource.py +0 -0
  82. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_config.py +0 -0
  83. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_expression_normalization.py +0 -0
  84. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_http_client.py +0 -0
  85. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_image.py +0 -0
  86. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_metadata.py +0 -0
  87. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_metadata_rules.py +0 -0
  88. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_notifications.py +0 -0
  89. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_request_id.py +0 -0
  90. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_search.py +0 -0
  91. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_streaming_profiles.py +0 -0
  92. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_triggers.py +0 -0
  93. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_utils.py +0 -0
  94. {cloudinary-1.46.1 → cloudinary-1.46.3}/test/test_video.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudinary
3
- Version: 1.46.1
3
+ Version: 1.46.3
4
4
  Summary: Upload, transform, optimize, and manage images and videos with Cloudinary from Python or Django.
5
5
  Author-email: Cloudinary <info@cloudinary.com>
6
6
  License: MIT License
@@ -38,7 +38,7 @@ CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAA
38
38
  URI_SCHEME = "cloudinary"
39
39
  API_VERSION = "v1_1"
40
40
 
41
- VERSION = "1.46.1"
41
+ VERSION = "1.46.3"
42
42
 
43
43
  _USER_PLATFORM_DETAILS = "; ".join((platform(), "Python {}".format(python_version())))
44
44
 
@@ -450,7 +450,7 @@ def update(public_id, **options):
450
450
  params = only(options, "moderation_status", "raw_convert",
451
451
  "quality_override", "ocr",
452
452
  "categorization", "detection", "similarity_search",
453
- "background_removal", "notification_url")
453
+ "background_removal", "notification_url", "batch_id")
454
454
  if "tags" in options:
455
455
  params["tags"] = ",".join(utils.build_array(options["tags"]))
456
456
  if "face_coordinates" in options:
@@ -1567,7 +1567,7 @@ def __delete_resource_params(options, **params):
1567
1567
  :internal
1568
1568
  """
1569
1569
  p = dict(transformations=utils.build_eager(options.get('transformations')),
1570
- **only(options, "keep_original", "next_cursor", "invalidate", "batch_id"))
1570
+ **only(options, "keep_original", "next_cursor", "invalidate", "batch_id", "notification_url"))
1571
1571
  p.update(params)
1572
1572
  return p
1573
1573
 
@@ -73,4 +73,4 @@ def _ensure_int(value):
73
73
  try:
74
74
  return int(value)
75
75
  except (ValueError, TypeError):
76
- raise ValueError("Value '" + value + "' must be an integer.")
76
+ raise ValueError("Value '{}' must be an integer.".format(value))
@@ -464,7 +464,7 @@ def user_in_user_groups(user_id, **options):
464
464
  :return: List of groups user is in
465
465
  :rtype: dict
466
466
  """
467
- uri = [USER_GROUPS_SUB_PATH, user_id]
467
+ uri = [USERS_SUB_PATH, user_id, "groups"]
468
468
  return _call_account_api("get", uri, {}, **options)
469
469
 
470
470
 
@@ -41,6 +41,9 @@ upload_options = [
41
41
 
42
42
  UPLOAD_LARGE_CHUNK_SIZE = 20000000
43
43
 
44
+ # The tags, context and metadata endpoints accept these params but omit them from signature
45
+ _NON_SIGNABLE_NOTIFICATION_PARAMS = ("notification_url", "batch_id")
46
+
44
47
 
45
48
  def upload(file, **options):
46
49
  """
@@ -378,6 +381,9 @@ def destroy(public_id, **options):
378
381
  :param options: Additional options for the deletion.
379
382
  :keyword str type: The storage type (upload, private, authenticated).
380
383
  :keyword bool invalidate: Invalidate cached copies on the CDN if True.
384
+ :keyword str notification_url: A URL to notify when the deletion is completed.
385
+ :keyword str batch_id: Correlation key for the completion notification, used to address it
386
+ when polling.
381
387
  :return: The result of the API call.
382
388
  :rtype: dict
383
389
  """
@@ -385,7 +391,9 @@ def destroy(public_id, **options):
385
391
  "timestamp": utils.now(),
386
392
  "type": options.get("type"),
387
393
  "invalidate": options.get("invalidate"),
388
- "public_id": public_id
394
+ "public_id": public_id,
395
+ "notification_url": options.get("notification_url"),
396
+ "batch_id": options.get("batch_id")
389
397
  }
390
398
  return call_api("destroy", params, **options)
391
399
 
@@ -407,6 +415,9 @@ def rename(from_public_id, to_public_id, **options):
407
415
  :keyword str to_type: Change the resource to the specified upload type.
408
416
  :keyword dict context: Set or update contextual metadata.
409
417
  :keyword dict metadata: Set or update structured metadata.
418
+ :keyword str notification_url: A URL to notify when the rename is completed.
419
+ :keyword str batch_id: Correlation key for the completion notification, used to address it
420
+ when polling.
410
421
  :return: The result of the API call.
411
422
  :rtype: dict
412
423
  """
@@ -419,7 +430,9 @@ def rename(from_public_id, to_public_id, **options):
419
430
  "to_public_id": to_public_id,
420
431
  "to_type": options.get("to_type"),
421
432
  "context": options.get("context"),
422
- "metadata": options.get("metadata")
433
+ "metadata": options.get("metadata"),
434
+ "notification_url": options.get("notification_url"),
435
+ "batch_id": options.get("batch_id")
423
436
  }
424
437
  return call_api("rename", params, **options)
425
438
 
@@ -438,6 +451,9 @@ def update_metadata(metadata, public_ids, **options):
438
451
  :keyword str resource_type: The resource type (image, raw, video). Default="image".
439
452
  :keyword str type: The storage type (upload, private, authenticated).
440
453
  :keyword bool clear_invalid: If True, remove keys that are not valid.
454
+ :keyword str notification_url: A URL to notify when the update is completed.
455
+ :keyword str batch_id: Correlation key for the completion notification, used to address it
456
+ when polling.
441
457
  :return: A list of public IDs that were updated.
442
458
  :rtype: dict
443
459
  """
@@ -446,8 +462,11 @@ def update_metadata(metadata, public_ids, **options):
446
462
  "metadata": utils.encode_context(metadata),
447
463
  "public_ids": utils.build_array(public_ids),
448
464
  "type": options.get("type"),
449
- "clear_invalid": options.get("clear_invalid")
465
+ "clear_invalid": options.get("clear_invalid"),
466
+ "notification_url": options.get("notification_url"),
467
+ "batch_id": options.get("batch_id")
450
468
  }
469
+ options["non_signable"] = _NON_SIGNABLE_NOTIFICATION_PARAMS
451
470
  return call_api("metadata", params, **options)
452
471
 
453
472
 
@@ -509,6 +528,8 @@ def generate_sprite(tag=None, urls=None, **options):
509
528
  :param urls: List of URLs to create a sprite from (only if tag not set).
510
529
  :type urls: list[str], optional
511
530
  :param options: Additional sprite configuration.
531
+ :keyword str public_id: The public ID to assign to the generated sprite. When omitted, the
532
+ server derives one from the tag or URLs. Cannot be used with mode=download.
512
533
  :return: Dictionary with metadata and URLs of generated sprite resources.
513
534
  :rtype: dict
514
535
  """
@@ -543,6 +564,8 @@ def multi(tag=None, urls=None, **options):
543
564
  :param urls: A list of image URLs (if no tag is set).
544
565
  :type urls: list[str], optional
545
566
  :param options: Additional multi-configuration options.
567
+ :keyword str public_id: The public ID to assign to the generated file. When omitted, the server
568
+ derives one from the tag or URLs. Cannot be used with mode=download.
546
569
  :return: Dictionary with metadata and URLs of the generated file.
547
570
  :rtype: dict
548
571
  """
@@ -575,6 +598,8 @@ def explode(public_id, **options):
575
598
  :param public_id: The public ID of the file to explode.
576
599
  :type public_id: str
577
600
  :param options: Additional explode options (format, notification_url, batch_id, transformation).
601
+ :keyword str type: The storage type of the file to explode (upload, private, authenticated).
602
+ Default=upload.
578
603
  :keyword str batch_id: Correlation key for the completion notification, used to address it
579
604
  when polling.
580
605
  :return: The result of the API call.
@@ -584,6 +609,7 @@ def explode(public_id, **options):
584
609
  "timestamp": utils.now(),
585
610
  "public_id": public_id,
586
611
  "format": options.get("format"),
612
+ "type": options.get("type"),
587
613
  "notification_url": options.get("notification_url"),
588
614
  "batch_id": options.get("batch_id"),
589
615
  "transformation": utils.generate_transformation_string(**options)[0]
@@ -705,6 +731,9 @@ def call_tags_api(tag, command, public_ids=None, **options):
705
731
  :param public_ids: A list of asset public IDs.
706
732
  :type public_ids: list[str], optional
707
733
  :param options: Additional options (e.g., type).
734
+ :keyword str notification_url: A URL to notify when the update is completed.
735
+ :keyword str batch_id: Correlation key for the completion notification, used to address it
736
+ when polling.
708
737
  :return: The result of the API call.
709
738
  :rtype: dict
710
739
  """
@@ -713,8 +742,11 @@ def call_tags_api(tag, command, public_ids=None, **options):
713
742
  "tag": tag,
714
743
  "public_ids": utils.build_array(public_ids),
715
744
  "command": command,
716
- "type": options.get("type")
745
+ "type": options.get("type"),
746
+ "notification_url": options.get("notification_url"),
747
+ "batch_id": options.get("batch_id")
717
748
  }
749
+ options["non_signable"] = _NON_SIGNABLE_NOTIFICATION_PARAMS
718
750
  return call_api("tags", params, **options)
719
751
 
720
752
 
@@ -731,6 +763,9 @@ def call_context_api(context, command, public_ids=None, **options):
731
763
  :param public_ids: A list of asset public IDs.
732
764
  :type public_ids: list[str], optional
733
765
  :param options: Additional options (e.g., type).
766
+ :keyword str notification_url: A URL to notify when the update is completed.
767
+ :keyword str batch_id: Correlation key for the completion notification, used to address it
768
+ when polling.
734
769
  :return: The result of the API call.
735
770
  :rtype: dict
736
771
  """
@@ -739,8 +774,11 @@ def call_context_api(context, command, public_ids=None, **options):
739
774
  "context": utils.encode_context(context),
740
775
  "public_ids": utils.build_array(public_ids),
741
776
  "command": command,
742
- "type": options.get("type")
777
+ "type": options.get("type"),
778
+ "notification_url": options.get("notification_url"),
779
+ "batch_id": options.get("batch_id")
743
780
  }
781
+ options["non_signable"] = _NON_SIGNABLE_NOTIFICATION_PARAMS
744
782
  return call_api("context", params, **options)
745
783
 
746
784
 
@@ -778,6 +816,7 @@ def text(text, **options):
778
816
 
779
817
  _SLIDESHOW_PARAMS = [
780
818
  "notification_url",
819
+ "batch_id",
781
820
  "public_id",
782
821
  "overwrite",
783
822
  "upload_preset",
@@ -791,6 +830,8 @@ def create_slideshow(**options):
791
830
  :param options: Additional parameters for the slideshow creation.
792
831
  :keyword str resource_type: The resource type, defaults to "video".
793
832
  :keyword str notification_url: A URL to be notified when the processing is completed.
833
+ :keyword str batch_id: Correlation key for the completion notification, used to address it
834
+ when polling.
794
835
  :keyword str public_id: The public ID to assign to the generated slideshow.
795
836
  :keyword bool overwrite: Whether to overwrite the slideshow if public_id already exists.
796
837
  :keyword str upload_preset: An upload preset to apply to the slideshow creation.
@@ -629,6 +629,16 @@ def json_body(params, headers=None):
629
629
 
630
630
 
631
631
  def sign_request(params, options):
632
+ """
633
+ Signs Upload API request parameters.
634
+
635
+ Keys listed in options["non_signable"] are still sent, but left out of the signature.
636
+
637
+ :param params: Params to sign and send.
638
+ :param options: Additional options, including non_signable.
639
+ :return: The params, with signature and api_key added.
640
+ :internal
641
+ """
632
642
  api_key = options.get("api_key", cloudinary.config().api_key)
633
643
  if not api_key:
634
644
  raise ValueError("Must supply api_key")
@@ -637,9 +647,11 @@ def sign_request(params, options):
637
647
  raise ValueError("Must supply api_secret")
638
648
  signature_algorithm = options.get("signature_algorithm", cloudinary.config().signature_algorithm)
639
649
  signature_version = options.get("signature_version", cloudinary.config().signature_version)
650
+ non_signable = options.get("non_signable") or ()
640
651
 
641
652
  params = cleanup_params(params)
642
- params["signature"] = api_sign_request(params, api_secret, signature_algorithm, signature_version)
653
+ params_to_sign = params if not non_signable else dict((k, v) for k, v in params.items() if k not in non_signable)
654
+ params["signature"] = api_sign_request(params_to_sign, api_secret, signature_algorithm, signature_version)
643
655
  params["api_key"] = api_key
644
656
 
645
657
  return params
@@ -1254,6 +1266,7 @@ def build_multi_and_sprite_params(**options):
1254
1266
  "notification_url": options.get("notification_url"),
1255
1267
  "tag": tag,
1256
1268
  "urls": urls,
1269
+ "public_id": options.get("public_id"),
1257
1270
  "transformation": generate_transformation_string(fetch_format=options.get("format"), **options)[0]
1258
1271
  }
1259
1272
  return params
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudinary
3
- Version: 1.46.1
3
+ Version: 1.46.3
4
4
  Summary: Upload, transform, optimize, and manage images and videos with Cloudinary from Python or Django.
5
5
  Author-email: Cloudinary <info@cloudinary.com>
6
6
  License: MIT License
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "cloudinary"
3
3
  description = "Upload, transform, optimize, and manage images and videos with Cloudinary from Python or Django."
4
- version = "1.46.1"
4
+ version = "1.46.3"
5
5
 
6
6
  authors = [{ name = "Cloudinary", email = "info@cloudinary.com" }]
7
7
  license = { file = "LICENSE" }
@@ -7,7 +7,7 @@ if version_info[0] >= 3:
7
7
  else:
8
8
  # Following code is legacy (Python 2.7 compatibility) and will be removed in the future!
9
9
  # TODO: Remove in next major update (when dropping Python 2.7 compatibility)
10
- version = "1.46.1"
10
+ version = "1.46.3"
11
11
 
12
12
  with open('README.md') as file:
13
13
  long_description = file.read()
@@ -522,9 +522,11 @@ class ApiTest(unittest.TestCase):
522
522
  """ should allow deleting resources by transformations """
523
523
  mocker.return_value = MOCK_RESPONSE
524
524
 
525
- api.delete_resources(['api_test', 'api_test2'], transformations=['c_crop,w_100'])
525
+ api.delete_resources(['api_test', 'api_test2'], transformations=['c_crop,w_100'],
526
+ notification_url="http://example.com")
526
527
  self.assertEqual(get_method(mocker), 'DELETE')
527
528
  self.assertEqual(get_param(mocker, 'transformations'), 'c_crop,w_100')
529
+ self.assertEqual(get_param(mocker, 'notification_url'), "http://example.com")
528
530
 
529
531
  api.delete_all_resources(transformations=['c_crop,w_100', {"crop": "scale", "width": 107}])
530
532
  self.assertEqual(get_method(mocker), 'DELETE')
@@ -824,11 +826,12 @@ class ApiTest(unittest.TestCase):
824
826
  @patch(URLLIB3_REQUEST)
825
827
  @unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
826
828
  def test21_notification_url(self, mocker):
827
- """ should support notification_url param """
829
+ """ should support notification_url and batch_id params """
828
830
  mocker.return_value = MOCK_RESPONSE
829
- api.update("api_test", notification_url="http://example.com")
831
+ api.update("api_test", notification_url="http://example.com", batch_id="batch_1")
830
832
  notification_url = get_param(mocker, 'notification_url')
831
833
  self.assertEqual(notification_url, "http://example.com")
834
+ self.assertEqual(get_param(mocker, 'batch_id'), "batch_1")
832
835
 
833
836
  @patch(URLLIB3_REQUEST)
834
837
  @unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
@@ -132,6 +132,15 @@ class AuthTokenTest(unittest.TestCase):
132
132
  def test_must_provide_expiration_or_duration(self):
133
133
  self.assertRaises(Exception, cloudinary.utils.generate_auth_token, acl="*", expiration=None, duration=None)
134
134
 
135
+ def test_reject_invalid_numeric_options(self):
136
+ for option in ("start_time", "duration", "expiration"):
137
+ for value in ("invalid", [300], {"seconds": 300}, complex(1, 2)):
138
+ options = {"acl": "*", "start_time": 1111111111, "duration": 300}
139
+ options[option] = value
140
+ with self.assertRaises(ValueError) as error:
141
+ cloudinary.utils.generate_auth_token(**options)
142
+ self.assertEqual(str(error.exception), "Value '{}' must be an integer.".format(value))
143
+
135
144
  def test_must_provide_acl_or_url(self):
136
145
  self.assertRaises(Exception, cloudinary.utils.generate_auth_token, start_time=1111111111, duration=300)
137
146
 
@@ -267,6 +267,22 @@ class AccountApiTest(unittest.TestCase):
267
267
  self.assertEqual("ok", named_key_del_res["message"])
268
268
 
269
269
 
270
+ class UserInUserGroupsTest(unittest.TestCase):
271
+ """
272
+ Verifies the request shape against a mocked transport, so no account credentials are needed.
273
+ """
274
+
275
+ def test_user_in_user_groups(self):
276
+ with patch(URLLIB3_REQUEST) as mocker:
277
+ mocker.return_value = api_response_mock()
278
+ cloudinary.provisioning.user_in_user_groups("user_id", account_id="account_id",
279
+ provisioning_api_key="key",
280
+ provisioning_api_secret="secret")
281
+
282
+ self.assertEqual("GET", get_method(mocker))
283
+ self.assertTrue(get_uri(mocker).endswith("/provisioning/accounts/account_id/users/user_id/groups"))
284
+
285
+
270
286
  class CreateAgentAccountTest(unittest.TestCase):
271
287
  """
272
288
  The create agent account endpoint is public, unauthenticated and rate limited per IP,
@@ -386,13 +386,16 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
386
386
  @patch(URLLIB3_REQUEST)
387
387
  @unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
388
388
  def test_rename_parameters(self, mocker):
389
- """Should support to_type, invalidate, and overwrite """
389
+ """Should support to_type, invalidate, overwrite, notification_url and batch_id"""
390
390
  mocker.return_value = MOCK_RESPONSE
391
- uploader.rename(TEST_IMAGE, TEST_IMAGE + "2", to_type='raw', invalidate=True, overwrite=False)
391
+ uploader.rename(TEST_IMAGE, TEST_IMAGE + "2", to_type='raw', invalidate=True, overwrite=False,
392
+ notification_url="poll://*", batch_id="batch_1")
392
393
 
393
394
  self.assertEqual(get_params(mocker)['to_type'], 'raw')
394
395
  self.assertTrue(get_params(mocker)['invalidate'])
395
396
  self.assertTrue(get_params(mocker)['overwrite'])
397
+ self.assertEqual(get_params(mocker)['notification_url'], "poll://*")
398
+ self.assertEqual(get_params(mocker)['batch_id'], "batch_1")
396
399
 
397
400
  @patch(URLLIB3_REQUEST)
398
401
  @unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
@@ -577,7 +580,8 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
577
580
  }
578
581
  },
579
582
  transformation={"fetch_format": "auto", "quality": "auto"},
580
- tags=['tag1', 'tag2', 'tag3']
583
+ tags=['tag1', 'tag2', 'tag3'],
584
+ batch_id="batch_1"
581
585
  )
582
586
 
583
587
  args, _ = mocker.call_args
@@ -587,6 +591,7 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
587
591
  self.assertEqual("fn_render:" + slideshow_manifest, get_params(mocker)['manifest_transformation'])
588
592
  self.assertEqual("f_auto,q_auto", get_params(mocker)['transformation'])
589
593
  self.assertEqual("tag1,tag2,tag3", get_params(mocker)['tags'])
594
+ self.assertEqual("batch_1", get_params(mocker)['batch_id'])
590
595
 
591
596
  @patch(URLLIB3_REQUEST)
592
597
  @unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
@@ -1194,6 +1199,7 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
1194
1199
  TEST_ID,
1195
1200
  page="all",
1196
1201
  format="jpg",
1202
+ type="private",
1197
1203
  notification_url="poll://*",
1198
1204
  batch_id=batch_id,
1199
1205
  )
@@ -1201,6 +1207,7 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
1201
1207
  params = get_params(request_mock)
1202
1208
  self.assertEqual(params["public_id"], TEST_ID)
1203
1209
  self.assertEqual(params["format"], "jpg")
1210
+ self.assertEqual(params["type"], "private")
1204
1211
  self.assertEqual(params["notification_url"], "poll://*")
1205
1212
  self.assertEqual(params["batch_id"], batch_id)
1206
1213
 
@@ -1237,6 +1244,10 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
1237
1244
  uploader.destroy(result.get("public_id"))
1238
1245
  self.assertIn("f_jpg,w_100", result["css_url"])
1239
1246
 
1247
+ # Generate sprite with an explicit public_id, instead of one derived from the tag
1248
+ result = uploader.generate_sprite(sprite_test_tag, public_id=sprite_test_tag + "_named")
1249
+ self.assertIn(sprite_test_tag + "_named", result["css_url"])
1250
+
1240
1251
  def test_download_sprite(self):
1241
1252
  """Should generate signed download url for sprite"""
1242
1253
  sprite_test_tag = "sprite_tag"
@@ -1286,13 +1297,17 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
1286
1297
  self.assertTrue(pdf_result.get("url").endswith(".pdf"))
1287
1298
  self.assertIn("w_111", pdf_result.get("url"))
1288
1299
 
1300
+ # Generate multi with an explicit public_id, instead of one derived from the tag
1301
+ named_result = uploader.multi(tag=multi_test_tag, public_id=multi_test_tag + "_named")
1302
+ self.assertEqual(multi_test_tag + "_named", named_result.get("public_id"))
1303
+
1289
1304
  def test_download_multi(self):
1290
1305
  """Should generate signed download url for multi"""
1291
1306
  multi_test_tag = "multi_test_tag"
1292
1307
  url_1 = "https://res.cloudinary.com/demo/image/upload/sample"
1293
1308
  url_2 = "https://res.cloudinary.com/demo/image/upload/car"
1294
1309
 
1295
- url_from_tag = uploader.download_multi(tag=multi_test_tag)
1310
+ url_from_tag = uploader.download_multi(tag=multi_test_tag, public_id=TEST_ID)
1296
1311
  url_from_urls = uploader.download_multi(urls=[url_1, url_2])
1297
1312
 
1298
1313
  self.assertTrue(url_from_tag.startswith(
@@ -1302,6 +1317,7 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
1302
1317
 
1303
1318
  parameters = parse_qs(urlparse(url_from_tag).query)
1304
1319
  self.assertEqual(multi_test_tag, parameters["tag"][0])
1320
+ self.assertEqual(TEST_ID, parameters["public_id"][0])
1305
1321
  self.assertEqual("download", parameters["mode"][0])
1306
1322
  self.assertIn("timestamp", parameters)
1307
1323
  self.assertIn("signature", parameters)
@@ -1321,6 +1337,54 @@ P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC\
1321
1337
  uploader.create_zip(tags="test-tag", target_asset_folder="test-asset-folder")
1322
1338
  self.assertEqual("test-asset-folder", get_param(mocker, "target_asset_folder"))
1323
1339
 
1340
+ @staticmethod
1341
+ def sign_request_without(params, *excluded):
1342
+ """
1343
+ Signs the params as sent in the request, leaving out the excluded keys.
1344
+
1345
+ :param params: The params as sent in the request.
1346
+ :param excluded: Keys to leave out of the signature.
1347
+ :return: The expected signature.
1348
+ """
1349
+ config = cloudinary.config()
1350
+ params_to_sign = dict((k, v) for k, v in params.items()
1351
+ if k not in excluded and k not in ("signature", "api_key"))
1352
+ return utils.api_sign_request(params_to_sign, config.api_secret, config.signature_algorithm,
1353
+ config.signature_version)
1354
+
1355
+ def assert_notification_params_unsigned(self, mocker):
1356
+ """
1357
+ Assert notification_url and batch_id were sent, but left out of the signature.
1358
+
1359
+ :param mocker: The mocked request.
1360
+ """
1361
+ params = get_params(mocker)
1362
+ self.assertEqual("poll://*", params["notification_url"])
1363
+ self.assertEqual("batch_1", params["batch_id"])
1364
+ self.assertEqual(self.sign_request_without(params, "notification_url", "batch_id"), params["signature"])
1365
+
1366
+ @patch(URLLIB3_REQUEST)
1367
+ @unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
1368
+ def test_notification_params_signing(self, mocker):
1369
+ """Should send notification_url and batch_id, unsigned on tags, context and metadata"""
1370
+ mocker.return_value = MOCK_RESPONSE
1371
+ options = {"notification_url": "poll://*", "batch_id": "batch_1"}
1372
+
1373
+ uploader.add_tag("tag1", [TEST_ID], **options)
1374
+ self.assert_notification_params_unsigned(mocker)
1375
+
1376
+ uploader.add_context({"k": "v"}, [TEST_ID], **options)
1377
+ self.assert_notification_params_unsigned(mocker)
1378
+
1379
+ uploader.update_metadata({"k": "v"}, [TEST_ID], **options)
1380
+ self.assert_notification_params_unsigned(mocker)
1381
+
1382
+ uploader.destroy(TEST_ID, **options)
1383
+ params = get_params(mocker)
1384
+ self.assertEqual("poll://*", params["notification_url"])
1385
+ self.assertEqual("batch_1", params["batch_id"])
1386
+ self.assertEqual(self.sign_request_without(params), params["signature"])
1387
+
1324
1388
 
1325
1389
  if __name__ == '__main__':
1326
1390
  unittest.main()
File without changes
File without changes
File without changes
File without changes