Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 8 additions & 3 deletions plivo/resources/phone_number_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)


Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion plivo/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# -*- coding: utf-8 -*-
__version__ = '4.60.0'
__version__ = '4.60.1'
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
121 changes: 121 additions & 0 deletions tests/resources/test_phone_number_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down Expand Up @@ -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': {
Expand Down
Loading