From 19cdeee9fa8471ecc2bbf61eb3e9abfe7f750264 Mon Sep 17 00:00:00 2001 From: Koushik SK Date: Fri, 17 Apr 2026 08:38:17 +0530 Subject: [PATCH 1/3] fix: correct Requirements params and ensure multipart for create/update - Requirements.get() now only sends non-None params instead of always sending all three (which resulted in ?country_iso=None queries) - _build_compliance_multipart returns empty dict instead of None when no documents, ensuring create/update always use multipart/form-data Co-Authored-By: Claude Opus 4.6 (1M context) --- plivo/resources/phone_number_compliance.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plivo/resources/phone_number_compliance.py b/plivo/resources/phone_number_compliance.py index 272a9d04..3e479a20 100644 --- a/plivo/resources/phone_number_compliance.py +++ b/plivo/resources/phone_number_compliance.py @@ -13,10 +13,17 @@ class PhoneNumberComplianceRequirements(PlivoResourceInterface): def get(self, country_iso=None, number_type=None, user_type=None): # GET /PhoneNumber/Compliance/Requirements + params = {} + if country_iso: + params['country_iso'] = country_iso + if number_type: + params['number_type'] = number_type + if user_type: + params['user_type'] = user_type return self.client.request( 'GET', ('PhoneNumber', 'Compliance', 'Requirements'), - dict(country_iso=country_iso, number_type=number_type, user_type=user_type) + params ) @@ -114,6 +121,4 @@ def _build_compliance_multipart(data, documents): for idx, doc_path in enumerate(documents): field_name = 'documents[{}].file'.format(idx) files[field_name] = (os.path.basename(doc_path), open(doc_path, 'rb')) - if not files: - files = None return payload, files From d4c02cc280599a3ade9219d202e39853ab837fde Mon Sep 17 00:00:00 2001 From: Koushik SK Date: Fri, 17 Apr 2026 08:47:23 +0530 Subject: [PATCH 2/3] chore: bump version to 4.60.1 Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 5 +++++ plivo/version.py | 2 +- setup.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e9ccb8f..b3bc619d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Change Log +## [4.60.1](https://github.com/plivo/plivo-python/tree/v4.60.1) (2026-04-17) +**Bug Fix - PhoneNumber Compliance API** +- Fixed Requirements.get() sending None values as query params when not provided +- Fixed create()/update() sending JSON instead of multipart when no document files provided + ## [4.60.0](https://github.com/plivo/plivo-python/tree/v4.60.0) (2026-04-08) **Feature - PhoneNumber Compliance API support** - Added `phone_number_compliance_requirements` resource for discovering compliance requirements by country, number type, and user type diff --git a/plivo/version.py b/plivo/version.py index 9d187116..d9200218 100644 --- a/plivo/version.py +++ b/plivo/version.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = '4.60.0' +__version__ = '4.60.1' diff --git a/setup.py b/setup.py index 3c2ab227..5aa4ad1e 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='plivo', - version='4.60.0', + version='4.60.1', description='A Python SDK to make voice calls & send SMS using Plivo and to generate Plivo XML', long_description=long_description, url='https://github.com/plivo/plivo-python', From fa7bdb4b9a2e84c5fbf382bc0324501d2f4d1c6e Mon Sep 17 00:00:00 2001 From: Koushik SK Date: Fri, 17 Apr 2026 10:37:10 +0530 Subject: [PATCH 3/3] test: add tests for compliance query param filtering and multipart without documents Verify that Requirements.get() with partial/no arguments does not leak None values into the URL query string, and that create/update use multipart encoding even when no document files are attached. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../resources/test_phone_number_compliance.py | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/resources/test_phone_number_compliance.py b/tests/resources/test_phone_number_compliance.py index b5e16a57..bd7e62a6 100644 --- a/tests/resources/test_phone_number_compliance.py +++ b/tests/resources/test_phone_number_compliance.py @@ -68,6 +68,45 @@ def test_get_requirements_empty_document_types(self): self.assertEqual(self.client.current_request.method, 'GET') self.assertEqual(len(response.document_types), 0) + def test_get_requirements_partial_args_no_none_in_url(self): + """Calling get() with only country_iso should not send + number_type=None or user_type=None in the query string.""" + expected_response = { + 'requirement_id': 'req_partial', + 'document_types': [] + } + self.client.set_expected_response( + status_code=200, data_to_return=expected_response) + + self.client.phone_number_compliance_requirements.get( + country_iso='IN') + + url = self.client.current_request.url + self.assertIn('country_iso=IN', url) + self.assertNotIn('None', url) + self.assertNotIn('number_type', url) + self.assertNotIn('user_type', url) + self.assertEqual(self.client.current_request.method, 'GET') + + def test_get_requirements_no_args_no_none_in_url(self): + """Calling get() with no arguments should produce a clean URL + with no query parameters containing None.""" + expected_response = { + 'requirement_id': 'req_noargs', + 'document_types': [] + } + self.client.set_expected_response( + status_code=200, data_to_return=expected_response) + + self.client.phone_number_compliance_requirements.get() + + url = self.client.current_request.url + self.assertNotIn('None', url) + self.assertNotIn('number_type', url) + self.assertNotIn('user_type', url) + self.assertNotIn('country_iso', url) + self.assertEqual(self.client.current_request.method, 'GET') + class PhoneNumberComplianceApplicationsTest(PlivoResourceTestCase): @@ -115,6 +154,88 @@ def test_create_multipart_data_structure(self): self.assertEqual(self.client.current_request.method, 'POST') self.assertEqual(response.compliance_id, 'comp_def456') + def test_create_without_documents(self): + """Create without documents parameter should succeed and use + multipart/form-data (not application/json).""" + expected_response = { + 'compliance_id': 'comp_nodoc', + 'message': 'created' + } + self.client.set_expected_response( + status_code=201, data_to_return=expected_response) + + response = self.client.phone_number_compliance.create( + data={ + 'country_iso': 'IN', + 'number_type': 'local', + 'alias': 'India Local', + 'end_user': {'name': 'Test User'}, + }) + + self.assertEqual(self.client.current_request.method, 'POST') + self.assertEqual(response.compliance_id, 'comp_nodoc') + # Verify the request uses multipart encoding, not JSON + content_type = self.client.current_request.headers['Content-Type'] + self.assertTrue( + any([ + 'multipart' in content_type, + 'www-form-urlencoded' in content_type + ])) + + def test_create_with_documents_none(self): + """Create with documents=None should succeed and use + multipart/form-data.""" + expected_response = { + 'compliance_id': 'comp_docnone', + 'message': 'created' + } + self.client.set_expected_response( + status_code=201, data_to_return=expected_response) + + response = self.client.phone_number_compliance.create( + data={ + 'country_iso': 'GB', + 'number_type': 'mobile', + 'alias': 'UK Mobile', + 'end_user': {'name': 'Jane Doe'}, + }, + documents=None) + + self.assertEqual(self.client.current_request.method, 'POST') + self.assertEqual(response.compliance_id, 'comp_docnone') + content_type = self.client.current_request.headers['Content-Type'] + self.assertTrue( + any([ + 'multipart' in content_type, + 'www-form-urlencoded' in content_type + ])) + + def test_update_without_documents(self): + """Update without documents should succeed and use + multipart/form-data.""" + expected_response = { + 'message': 'updated', + 'compliance': { + 'compliance_id': 'comp_upd_nodoc', + 'status': 'pending' + } + } + self.client.set_expected_response( + status_code=200, data_to_return=expected_response) + + response = self.client.phone_number_compliance.update( + 'comp_upd_nodoc', + data={'alias': 'New Alias'}) + + self.assertEqual(self.client.current_request.method, 'PATCH') + self.assertEqual(response.message, 'updated') + content_type = self.client.current_request.headers['Content-Type'] + self.assertTrue( + any([ + 'multipart' in content_type, + 'www-form-urlencoded' in content_type + ])) + def test_list(self): expected_response = { 'meta': {