From b10c70d356c95b17957698e92eb276885a93a54f Mon Sep 17 00:00:00 2001 From: Constantine Nathanson Date: Thu, 27 Aug 2026 14:54:03 +0300 Subject: [PATCH] Add batch_id to explode parameters `explode` built its params inline and did not serialize `batch_id`, so a client-supplied value never reached the wire. The server accepts it on this endpoint (`before_action :capture_notification_batch_id` covers every UploadController action) and needs it to partition the completion notification into a pollable batch: the poll destination reads the key from ThreadContext, which is populated only from a client value. Without one the notification is enqueued unpartitioned and cannot be polled, even though the response and the payload both echo a server-minted `batch_id`. Co-Authored-By: Claude Opus 5 --- cloudinary/uploader.py | 5 ++++- test/test_uploader.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cloudinary/uploader.py b/cloudinary/uploader.py index a626da4..379f8d1 100644 --- a/cloudinary/uploader.py +++ b/cloudinary/uploader.py @@ -574,7 +574,9 @@ def explode(public_id, **options): :param public_id: The public ID of the file to explode. :type public_id: str - :param options: Additional explode options (format, notification_url, transformation). + :param options: Additional explode options (format, notification_url, batch_id, transformation). + :keyword str batch_id: Correlation key for the completion notification, used to address it + when polling. :return: The result of the API call. :rtype: dict """ @@ -583,6 +585,7 @@ def explode(public_id, **options): "public_id": public_id, "format": options.get("format"), "notification_url": options.get("notification_url"), + "batch_id": options.get("batch_id"), "transformation": utils.generate_transformation_string(**options)[0] } return call_api("explode", params, **options) diff --git a/test/test_uploader.py b/test/test_uploader.py index 3e60d72..d0f7534 100644 --- a/test/test_uploader.py +++ b/test/test_uploader.py @@ -1183,6 +1183,27 @@ def test_various_upload_parameters(self, request_mock): for param in options.keys(): self.assertIn(param, params) + @patch(URLLIB3_REQUEST) + def test_explode_parameters(self, request_mock): + """Should support notification_url and batch_id in explode""" + request_mock.return_value = MOCK_RESPONSE + + batch_id = "batch_{}".format(UNIQUE_ID) + + uploader.explode( + TEST_ID, + page="all", + format="jpg", + notification_url="poll://*", + batch_id=batch_id, + ) + + params = get_params(request_mock) + self.assertEqual(params["public_id"], TEST_ID) + self.assertEqual(params["format"], "jpg") + self.assertEqual(params["notification_url"], "poll://*") + self.assertEqual(params["batch_id"], batch_id) + @unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret") def test_eval_upload_parameter(self): """Should support eval in upload"""