Skip to content

Commit 0a8ebf6

Browse files
authored
Add Subaccounts API (#264)
* create subaccounts class, start adding methods and testing * refactoring client class methods, testing subaccounts methods * adding balance and credit transfers/tests * adding subaccounts.transfer_number and tests * updating readme for subaccounts and numbers api * updating changelog * Bump version: 3.5.2 → 3.6.0 * removed milliseconds and added to the readme
1 parent 867d386 commit 0a8ebf6

32 files changed

+1473
-52
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.5.2
2+
current_version = 3.6.0
33
commit = True
44
tag = False
55

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 3.6.0
2+
- Adding support for the [Vonage Subaccounts API](https://developer.vonage.com/en/account/subaccounts/overview)
3+
14
# 3.5.2
25
- Using the [Vonage JWT Generator](https://github.com/Vonage/vonage-python-jwt) instead of `PyJWT` for generating JWTs.
36
- Other internal refactoring and enhancements

README.md

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ need a Vonage account. Sign up [for free at vonage.com][signup].
2020
- [Verify V1 API](#verify-v1-api)
2121
- [Number Insight API](#number-insight-api)
2222
- [Account API](#account-api)
23+
- [Subaccounts API](#subaccounts-api)
2324
- [Number Management API](#number-management-api)
2425
- [Pricing API](#pricing-api)
2526
- [Managing Secrets](#managing-secrets)
@@ -408,7 +409,7 @@ When using the `connect` action, use the parameter `from_` to specify the recipi
408409

409410
## Verify V2 API
410411

411-
V2 of the Vonage Verify API lets you send verification codes via SMS, WhatsApp, Voice and Email
412+
V2 of the Vonage Verify API lets you send verification codes via SMS, WhatsApp, Voice and Email.
412413

413414
You can also verify a user by WhatsApp Interactive Message or by Silent Authentication on their mobile device.
414415

@@ -618,6 +619,150 @@ This feature is only enabled when you enable auto-reload for your account in the
618619
client.account.topup(trx=transaction_reference)
619620
```
620621

622+
## Subaccounts API
623+
624+
This API is used to create and configure subaccounts related to your primary account and transfer credit, balances and bought numbers between accounts.
625+
626+
The subaccounts API is disabled by default. If you want to use subaccounts, [contact support](https://api.support.vonage.com) to have the API enabled on your account.
627+
628+
### Get a list of all subaccounts
629+
630+
```python
631+
client.subaccounts.list_subaccounts()
632+
```
633+
634+
### Create a subaccount
635+
636+
```python
637+
client.subaccounts.create_subaccount(name='my subaccount')
638+
639+
# With options
640+
client.subaccounts.create_subaccount(
641+
name='my subaccount',
642+
secret='Password123',
643+
use_primary_account_balance=False,
644+
)
645+
```
646+
647+
### Get information about a subaccount
648+
649+
```python
650+
client.subaccounts.get_subaccount(SUBACCOUNT_API_KEY)
651+
```
652+
653+
### Modify a subaccount
654+
655+
```python
656+
client.subaccounts.modify_subaccount(
657+
SUBACCOUNT_KEY,
658+
suspended=True,
659+
use_primary_account_balance=False,
660+
name='my modified subaccount',
661+
)
662+
```
663+
664+
### List credit transfers between accounts
665+
666+
All fields are optional. If `start_date` or `end_date` are used, the dates must be specified in UTC ISO 8601 format, e.g. `1970-01-01T00:00:00Z`. Don't use milliseconds.
667+
668+
```python
669+
client.subaccounts.list_credit_transfers(
670+
start_date='2022-03-29T14:16:56Z',
671+
end_date='2023-06-12T17:20:01Z',
672+
subaccount=SUBACCOUNT_API_KEY, # Use to show only the results that contain this key
673+
)
674+
```
675+
676+
### Transfer credit between accounts
677+
678+
Transferring credit is only possible for postpaid accounts, i.e. accounts that can have a negative balance. For prepaid and self-serve customers, account balances can be transferred between accounts (see below).
679+
680+
```python
681+
client.subaccounts.transfer_credit(
682+
from_=FROM_ACCOUNT,
683+
to=TO_ACCOUNT,
684+
amount=0.50,
685+
reference='test credit transfer',
686+
)
687+
```
688+
689+
### List balance transfers between accounts
690+
691+
All fields are optional. If `start_date` or `end_date` are used, the dates must be specified in UTC ISO 8601 format, e.g. `1970-01-01T00:00:00Z`. Don't use milliseconds.
692+
693+
```python
694+
client.subaccounts.list_balance_transfers(
695+
start_date='2022-03-29T14:16:56Z',
696+
end_date='2023-06-12T17:20:01Z',
697+
subaccount=SUBACCOUNT_API_KEY, # Use to show only the results that contain this key
698+
)
699+
```
700+
701+
### Transfer account balances between accounts
702+
703+
```python
704+
client.subaccounts.transfer_balance(
705+
from_=FROM_ACCOUNT,
706+
to=TO_ACCOUNT,
707+
amount=0.50,
708+
reference='test balance transfer',
709+
)
710+
```
711+
712+
### Transfer bought phone numbers between accounts
713+
714+
```python
715+
client.subaccounts.transfer_balance(
716+
from_=FROM_ACCOUNT,
717+
to=TO_ACCOUNT,
718+
number=NUMBER_TO_TRANSFER,
719+
country='US',
720+
)
721+
```
722+
723+
## Number Management API
724+
725+
### Get numbers associated with your account
726+
727+
```python
728+
client.numbers.get_account_numbers(size=25)
729+
```
730+
731+
### Get numbers that are available to buy
732+
733+
```python
734+
client.numbers.get_available_numbers('CA', size=25)
735+
```
736+
737+
### Buy an available number
738+
739+
```python
740+
params = {'country': 'US', 'msisdn': 'number_to_buy'}
741+
client.numbers.buy_number(params)
742+
743+
# To buy a number for a subaccount
744+
params = {'country': 'US', 'msisdn': 'number_to_buy', 'target_api_key': SUBACCOUNT_API_KEY}
745+
client.numbers.buy_number(params)
746+
```
747+
748+
### Cancel your subscription for a specific number
749+
750+
```python
751+
params = {'country': 'US', 'msisdn': 'number_to_cancel'}
752+
client.numbers.cancel_number(params)
753+
754+
# To cancel a number assigned to a subaccount
755+
params = {'country': 'US', 'msisdn': 'number_to_buy', 'target_api_key': SUBACCOUNT_API_KEY}
756+
client.numbers.cancel_number(params)
757+
```
758+
759+
### Update the behaviour of a number that you own
760+
761+
```python
762+
params = {"country": "US", "msisdn": "number_to_update", "moHttpUrl": "callback_url"}
763+
client.numbers.update_number(params)
764+
```
765+
621766
## Pricing API
622767

623768
### Get pricing for a single country
@@ -792,6 +937,7 @@ The following is a list of Vonage APIs and whether the Python SDK provides suppo
792937
| Redact API | Developer Preview ||
793938
| Reports API | Beta ||
794939
| SMS API | General Availability ||
940+
| Subaccounts API | General Availability ||
795941
| Verify API | General Availability ||
796942
| Voice API | General Availability ||
797943

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
setup(
1313
name="vonage",
14-
version="3.5.2",
14+
version="3.6.0",
1515
description="Vonage Server SDK for Python",
1616
long_description=long_description,
1717
long_description_content_type="text/markdown",

src/vonage/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .client import *
22
from .ncco_builder.ncco import *
33

4-
__version__ = "3.5.2"
4+
__version__ = "3.6.0"

src/vonage/_internal.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def _format_date_param(params, key, format="%Y-%m-%d %H:%M:%S"):
2222

2323

2424
def set_auth_type(client: Client) -> str:
25+
"""Sets the authentication type used. If a JWT Client has been created,
26+
it will create a JWT and use JWT authentication."""
27+
2528
if hasattr(client, '_jwt_client'):
2629
return 'jwt'
2730
else:

src/vonage/client.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .redact import Redact
1111
from .short_codes import ShortCodes
1212
from .sms import Sms
13+
from .subaccounts import Subaccounts
1314
from .ussd import Ussd
1415
from .voice import Voice
1516
from .verify import Verify
@@ -114,6 +115,7 @@ def __init__(
114115
self.numbers = Numbers(self)
115116
self.short_codes = ShortCodes(self)
116117
self.sms = Sms(self)
118+
self.subaccounts = Subaccounts(self)
117119
self.ussd = Ussd(self)
118120
self.verify = Verify(self)
119121
self.verify2 = Verify2(self)
@@ -176,12 +178,11 @@ def get(self, host, request_uri, params=None, auth_type=None):
176178
self._request_headers = self.headers
177179

178180
if auth_type == 'jwt':
179-
self._request_headers = self._add_jwt_to_request_headers()
181+
self._request_headers['Authorization'] = self._create_jwt_auth_string()
180182
elif auth_type == 'params':
181183
params = dict(params or {}, api_key=self.api_key, api_secret=self.api_secret)
182184
elif auth_type == 'header':
183-
hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
184-
self._request_headers = dict(self.headers or {}, Authorization=f"Basic {hash}")
185+
self._request_headers['Authorization'] = self._create_header_auth_string()
185186
else:
186187
raise InvalidAuthenticationTypeError(
187188
f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
@@ -208,12 +209,11 @@ def post(self, host, request_uri, params, auth_type=None, body_is_json=True, sup
208209
params["api_key"] = self.api_key
209210
params["sig"] = self.signature(params)
210211
elif auth_type == 'jwt':
211-
self._request_headers = self._add_jwt_to_request_headers()
212+
self._request_headers['Authorization'] = self._create_jwt_auth_string()
212213
elif auth_type == 'params':
213214
params = dict(params, api_key=self.api_key, api_secret=self.api_secret)
214215
elif auth_type == 'header':
215-
hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
216-
self._request_headers = dict(self.headers or {}, Authorization=f"Basic {hash}")
216+
self._request_headers['Authorization'] = self._create_header_auth_string()
217217
else:
218218
raise InvalidAuthenticationTypeError(
219219
f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
@@ -234,10 +234,9 @@ def put(self, host, request_uri, params, auth_type=None):
234234
self._request_headers = self.headers
235235

236236
if auth_type == 'jwt':
237-
self._request_headers = self._add_jwt_to_request_headers()
237+
self._request_headers['Authorization'] = self._create_jwt_auth_string()
238238
elif auth_type == 'header':
239-
hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
240-
self._request_headers = dict(self._request_headers or {}, Authorization=f"Basic {hash}")
239+
self._request_headers['Authorization'] = self._create_header_auth_string()
241240
else:
242241
raise InvalidAuthenticationTypeError(
243242
f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
@@ -247,15 +246,29 @@ def put(self, host, request_uri, params, auth_type=None):
247246
# All APIs that currently use put methods require a json-formatted body so don't need to check this
248247
return self.parse(host, self.session.put(uri, json=params, headers=self._request_headers, timeout=self.timeout))
249248

249+
def patch(self, host, request_uri, params, auth_type=None):
250+
uri = f"https://{host}{request_uri}"
251+
self._request_headers = self.headers
252+
253+
if auth_type == 'jwt':
254+
self._request_headers['Authorization'] = self._create_jwt_auth_string()
255+
elif auth_type == 'header':
256+
self._request_headers['Authorization'] = self._create_header_auth_string()
257+
else:
258+
raise InvalidAuthenticationTypeError(f"""Invalid authentication type.""")
259+
260+
logger.debug(f"PATCH to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}")
261+
# Only newer APIs (that expect json-bodies) currently use this method, so we will always send a json-formatted body
262+
return self.parse(host, self.session.patch(uri, json=params, headers=self._request_headers))
263+
250264
def delete(self, host, request_uri, auth_type=None):
251265
uri = f"https://{host}{request_uri}"
252266
self._request_headers = self.headers
253267

254268
if auth_type == 'jwt':
255-
self._request_headers = self._add_jwt_to_request_headers()
269+
self._request_headers['Authorization'] = self._create_jwt_auth_string()
256270
elif auth_type == 'header':
257-
hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
258-
self._request_headers = dict(self._request_headers or {}, Authorization=f"Basic {hash}")
271+
self._request_headers['Authorization'] = self._create_header_auth_string()
259272
else:
260273
raise InvalidAuthenticationTypeError(
261274
f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
@@ -303,11 +316,12 @@ def parse(self, host, response: Response):
303316
message = f"{response.status_code} response from {host}"
304317
raise ServerError(message)
305318

306-
def _add_jwt_to_request_headers(self):
307-
return dict(
308-
self.headers,
309-
Authorization=b"Bearer " + self._generate_application_jwt()
310-
)
319+
def _create_jwt_auth_string(self):
320+
return b"Bearer " + self._generate_application_jwt()
311321

312322
def _generate_application_jwt(self):
313323
return self._jwt_client.generate_application_jwt(self._jwt_claims)
324+
325+
def _create_header_auth_string(self):
326+
hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
327+
return f"Basic {hash}"

src/vonage/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,7 @@ class InvalidAuthenticationTypeError(Error):
3838

3939
class Verify2Error(ClientError):
4040
"""An error relating to the Verify (V2) API."""
41+
42+
43+
class SubaccountsError(ClientError):
44+
"""An error relating to the Subaccounts API."""

0 commit comments

Comments
 (0)