Skip to content

Commit

Permalink
Add Subaccounts API (#264)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
maxkahan committed Jun 14, 2023
1 parent 867d386 commit 0a8ebf6
Show file tree
Hide file tree
Showing 32 changed files with 1,473 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 3.5.2
current_version = 3.6.0
commit = True
tag = False

Expand Down
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 3.6.0
- Adding support for the [Vonage Subaccounts API](https://developer.vonage.com/en/account/subaccounts/overview)

# 3.5.2
- Using the [Vonage JWT Generator](https://github.com/Vonage/vonage-python-jwt) instead of `PyJWT` for generating JWTs.
- Other internal refactoring and enhancements
Expand Down
148 changes: 147 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ need a Vonage account. Sign up [for free at vonage.com][signup].
- [Verify V1 API](#verify-v1-api)
- [Number Insight API](#number-insight-api)
- [Account API](#account-api)
- [Subaccounts API](#subaccounts-api)
- [Number Management API](#number-management-api)
- [Pricing API](#pricing-api)
- [Managing Secrets](#managing-secrets)
Expand Down Expand Up @@ -408,7 +409,7 @@ When using the `connect` action, use the parameter `from_` to specify the recipi

## Verify V2 API

V2 of the Vonage Verify API lets you send verification codes via SMS, WhatsApp, Voice and Email
V2 of the Vonage Verify API lets you send verification codes via SMS, WhatsApp, Voice and Email.

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

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

## Subaccounts API

This API is used to create and configure subaccounts related to your primary account and transfer credit, balances and bought numbers between accounts.

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.

### Get a list of all subaccounts

```python
client.subaccounts.list_subaccounts()
```

### Create a subaccount

```python
client.subaccounts.create_subaccount(name='my subaccount')

# With options
client.subaccounts.create_subaccount(
name='my subaccount',
secret='Password123',
use_primary_account_balance=False,
)
```

### Get information about a subaccount

```python
client.subaccounts.get_subaccount(SUBACCOUNT_API_KEY)
```

### Modify a subaccount

```python
client.subaccounts.modify_subaccount(
SUBACCOUNT_KEY,
suspended=True,
use_primary_account_balance=False,
name='my modified subaccount',
)
```

### List credit transfers between accounts

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.

```python
client.subaccounts.list_credit_transfers(
start_date='2022-03-29T14:16:56Z',
end_date='2023-06-12T17:20:01Z',
subaccount=SUBACCOUNT_API_KEY, # Use to show only the results that contain this key
)
```

### Transfer credit between accounts

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).

```python
client.subaccounts.transfer_credit(
from_=FROM_ACCOUNT,
to=TO_ACCOUNT,
amount=0.50,
reference='test credit transfer',
)
```

### List balance transfers between accounts

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.

```python
client.subaccounts.list_balance_transfers(
start_date='2022-03-29T14:16:56Z',
end_date='2023-06-12T17:20:01Z',
subaccount=SUBACCOUNT_API_KEY, # Use to show only the results that contain this key
)
```

### Transfer account balances between accounts

```python
client.subaccounts.transfer_balance(
from_=FROM_ACCOUNT,
to=TO_ACCOUNT,
amount=0.50,
reference='test balance transfer',
)
```

### Transfer bought phone numbers between accounts

```python
client.subaccounts.transfer_balance(
from_=FROM_ACCOUNT,
to=TO_ACCOUNT,
number=NUMBER_TO_TRANSFER,
country='US',
)
```

## Number Management API

### Get numbers associated with your account

```python
client.numbers.get_account_numbers(size=25)
```

### Get numbers that are available to buy

```python
client.numbers.get_available_numbers('CA', size=25)
```

### Buy an available number

```python
params = {'country': 'US', 'msisdn': 'number_to_buy'}
client.numbers.buy_number(params)

# To buy a number for a subaccount
params = {'country': 'US', 'msisdn': 'number_to_buy', 'target_api_key': SUBACCOUNT_API_KEY}
client.numbers.buy_number(params)
```

### Cancel your subscription for a specific number

```python
params = {'country': 'US', 'msisdn': 'number_to_cancel'}
client.numbers.cancel_number(params)

# To cancel a number assigned to a subaccount
params = {'country': 'US', 'msisdn': 'number_to_buy', 'target_api_key': SUBACCOUNT_API_KEY}
client.numbers.cancel_number(params)
```

### Update the behaviour of a number that you own

```python
params = {"country": "US", "msisdn": "number_to_update", "moHttpUrl": "callback_url"}
client.numbers.update_number(params)
```

## Pricing API

### Get pricing for a single country
Expand Down Expand Up @@ -792,6 +937,7 @@ The following is a list of Vonage APIs and whether the Python SDK provides suppo
| Redact API | Developer Preview ||
| Reports API | Beta ||
| SMS API | General Availability ||
| Subaccounts API | General Availability ||
| Verify API | General Availability ||
| Voice API | General Availability ||

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

setup(
name="vonage",
version="3.5.2",
version="3.6.0",
description="Vonage Server SDK for Python",
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
2 changes: 1 addition & 1 deletion src/vonage/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .client import *
from .ncco_builder.ncco import *

__version__ = "3.5.2"
__version__ = "3.6.0"
3 changes: 3 additions & 0 deletions src/vonage/_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ def _format_date_param(params, key, format="%Y-%m-%d %H:%M:%S"):


def set_auth_type(client: Client) -> str:
"""Sets the authentication type used. If a JWT Client has been created,
it will create a JWT and use JWT authentication."""

if hasattr(client, '_jwt_client'):
return 'jwt'
else:
Expand Down
48 changes: 31 additions & 17 deletions src/vonage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .redact import Redact
from .short_codes import ShortCodes
from .sms import Sms
from .subaccounts import Subaccounts
from .ussd import Ussd
from .voice import Voice
from .verify import Verify
Expand Down Expand Up @@ -114,6 +115,7 @@ def __init__(
self.numbers = Numbers(self)
self.short_codes = ShortCodes(self)
self.sms = Sms(self)
self.subaccounts = Subaccounts(self)
self.ussd = Ussd(self)
self.verify = Verify(self)
self.verify2 = Verify2(self)
Expand Down Expand Up @@ -176,12 +178,11 @@ def get(self, host, request_uri, params=None, auth_type=None):
self._request_headers = self.headers

if auth_type == 'jwt':
self._request_headers = self._add_jwt_to_request_headers()
self._request_headers['Authorization'] = self._create_jwt_auth_string()
elif auth_type == 'params':
params = dict(params or {}, api_key=self.api_key, api_secret=self.api_secret)
elif auth_type == 'header':
hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
self._request_headers = dict(self.headers or {}, Authorization=f"Basic {hash}")
self._request_headers['Authorization'] = self._create_header_auth_string()
else:
raise InvalidAuthenticationTypeError(
f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
Expand All @@ -208,12 +209,11 @@ def post(self, host, request_uri, params, auth_type=None, body_is_json=True, sup
params["api_key"] = self.api_key
params["sig"] = self.signature(params)
elif auth_type == 'jwt':
self._request_headers = self._add_jwt_to_request_headers()
self._request_headers['Authorization'] = self._create_jwt_auth_string()
elif auth_type == 'params':
params = dict(params, api_key=self.api_key, api_secret=self.api_secret)
elif auth_type == 'header':
hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
self._request_headers = dict(self.headers or {}, Authorization=f"Basic {hash}")
self._request_headers['Authorization'] = self._create_header_auth_string()
else:
raise InvalidAuthenticationTypeError(
f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
Expand All @@ -234,10 +234,9 @@ def put(self, host, request_uri, params, auth_type=None):
self._request_headers = self.headers

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

def patch(self, host, request_uri, params, auth_type=None):
uri = f"https://{host}{request_uri}"
self._request_headers = self.headers

if auth_type == 'jwt':
self._request_headers['Authorization'] = self._create_jwt_auth_string()
elif auth_type == 'header':
self._request_headers['Authorization'] = self._create_header_auth_string()
else:
raise InvalidAuthenticationTypeError(f"""Invalid authentication type.""")

logger.debug(f"PATCH to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}")
# Only newer APIs (that expect json-bodies) currently use this method, so we will always send a json-formatted body
return self.parse(host, self.session.patch(uri, json=params, headers=self._request_headers))

def delete(self, host, request_uri, auth_type=None):
uri = f"https://{host}{request_uri}"
self._request_headers = self.headers

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

def _add_jwt_to_request_headers(self):
return dict(
self.headers,
Authorization=b"Bearer " + self._generate_application_jwt()
)
def _create_jwt_auth_string(self):
return b"Bearer " + self._generate_application_jwt()

def _generate_application_jwt(self):
return self._jwt_client.generate_application_jwt(self._jwt_claims)

def _create_header_auth_string(self):
hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
return f"Basic {hash}"
4 changes: 4 additions & 0 deletions src/vonage/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ class InvalidAuthenticationTypeError(Error):

class Verify2Error(ClientError):
"""An error relating to the Verify (V2) API."""


class SubaccountsError(ClientError):
"""An error relating to the Subaccounts API."""
Loading

0 comments on commit 0a8ebf6

Please sign in to comment.