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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,28 @@ Fetching a customer
```python
import os
from unit import Unit
from unit.models.customer import *

token = os.environ.get("token")
api_url = os.environ.get("api_url")

unit = Unit(api_url, token)
customer = unit.customers.list().data[0]
print(customer.id)
```

## Retrying API Requests
API requests can fail for many reasons, from network components failures, API rate limits, timeouts or service incidents.
<br> Create requests without idempotency key won't trigger the retry mechanism, so we recommend to pass an idempotency key where applicable.

You can read about retries here: https://docs.unit.co/#retries. <br>

the default amount of retries is 0. <br>Unit initialization with retries:
```python
import os
from unit import Unit

token = os.environ.get("token")
api_url = os.environ.get("api_url")

unit = Unit(api_url, token, retries=3)
```
1 change: 0 additions & 1 deletion e2e_tests/account_end_of_day_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ def test_list_account_end_of_day():
for a in response.data:
assert a.type == "accountEndOfDay"

test_list_account_end_of_day()
5 changes: 4 additions & 1 deletion e2e_tests/application_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
import unittest
import uuid
from datetime import timedelta
from unit import Unit
from unit.models.application import *
Expand All @@ -9,6 +9,7 @@

ApplicationTypes = ["individualApplication", "businessApplication", "trustApplication"]


def create_individual_application():
device_fingerprint = DeviceFingerprint.from_json_api({
"provider": "iovation",
Expand All @@ -21,6 +22,7 @@ def create_individual_application():
Phone("1", "2025550108"),
ssn="000000003",
device_fingerprints=[device_fingerprint],
idempotency_key=str(uuid.uuid1()),
jwt_subject="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9fQ"
)

Expand Down Expand Up @@ -73,3 +75,4 @@ def test_update_business_application():
updated = client.applications.update(PatchApplicationRequest(app.data.id, "businessApplication",
tags={"patch": "test-patch"}))
assert updated.data.type == "businessApplication"

1 change: 0 additions & 1 deletion e2e_tests/event_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,3 @@ def test_fire_event():
response = client.events.fire(event_id)
assert response.data == []


1 change: 1 addition & 0 deletions e2e_tests/payment_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def create_book_payment():

def test_list_and_get_payments():
payments_ids = []

response = client.payments.list()

for t in response.data:
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
requests==2.26.0
pytest
backoff
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
requests~=2.28.0
backoff~=2.1.2c
pytest
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
keywords=['unit', 'finance', 'banking',
'banking-as-a-service', 'API', 'SDK'],
install_requires=[
'requests'
'requests', 'backoff'
],
classifiers=[
'Development Status :: 4 - Beta',
Expand Down
50 changes: 25 additions & 25 deletions unit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,28 @@


class Unit(object):
def __init__(self, api_url, token):
self.applications = ApplicationResource(api_url, token)
self.customers = CustomerResource(api_url, token)
self.accounts = AccountResource(api_url, token)
self.cards = CardResource(api_url, token)
self.transactions = TransactionResource(api_url, token)
self.payments = PaymentResource(api_url, token)
self.statements = StatementResource(api_url, token)
self.customerTokens = CustomerTokenResource(api_url, token)
self.counterparty = CounterpartyResource(api_url, token)
self.returnAch = ReturnAchResource(api_url, token)
self.applicationForms = ApplicationFormResource(api_url, token)
self.fees = FeeResource(api_url, token)
self.events = EventResource(api_url, token)
self.webhooks = WebhookResource(api_url, token)
self.institutions = InstitutionResource(api_url, token)
self.atmLocations = AtmLocationResource(api_url, token)
self.billPays = BillPayResource(api_url, token)
self.api_tokens = APITokenResource(api_url, token)
self.authorizations = AuthorizationResource(api_url, token)
self.authorization_requests = AuthorizationRequestResource(api_url, token)
self.account_end_of_day = AccountEndOfDayResource(api_url, token)
self.checkDeposits = CheckDepositResource(api_url, token)
self.disputes = DisputeResource(api_url, token)
self.rewards = RewardResource(api_url, token)
def __init__(self, api_url, token, retries=0):
self.applications = ApplicationResource(api_url, token, retries)
self.customers = CustomerResource(api_url, token, retries)
self.accounts = AccountResource(api_url, token, retries)
self.cards = CardResource(api_url, token, retries)
self.transactions = TransactionResource(api_url, token, retries)
self.payments = PaymentResource(api_url, token, retries)
self.statements = StatementResource(api_url, token, retries)
self.customerTokens = CustomerTokenResource(api_url, token, retries)
self.counterparty = CounterpartyResource(api_url, token, retries)
self.returnAch = ReturnAchResource(api_url, token, retries)
self.applicationForms = ApplicationFormResource(api_url, token, retries)
self.fees = FeeResource(api_url, token, retries)
self.events = EventResource(api_url, token, retries)
self.webhooks = WebhookResource(api_url, token, retries)
self.institutions = InstitutionResource(api_url, token, retries)
self.atmLocations = AtmLocationResource(api_url, token, retries)
self.billPays = BillPayResource(api_url, token, retries)
self.api_tokens = APITokenResource(api_url, token, retries)
self.authorizations = AuthorizationResource(api_url, token, retries)
self.authorization_requests = AuthorizationRequestResource(api_url, token, retries)
self.account_end_of_day = AccountEndOfDayResource(api_url, token, retries)
self.checkDeposits = CheckDepositResource(api_url, token, retries)
self.disputes = DisputeResource(api_url, token, retries)
self.rewards = RewardResource(api_url, token, retries)
4 changes: 2 additions & 2 deletions unit/api/account_end_of_day_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@


class AccountEndOfDayResource(BaseResource):
def __init__(self, api_url, token):
super().__init__(api_url, token)
def __init__(self, api_url, token, retries):
super().__init__(api_url, token, retries)
self.resource = "account-end-of-day"

def list(self, params: ListAccountEndOfDayParams = None) -> Union[UnitResponse[List[AccountEndOfDayDTO]], UnitError]:
Expand Down
8 changes: 5 additions & 3 deletions unit/api/account_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
from unit.models.account import *
from unit.models.codecs import DtoDecoder


class AccountResource(BaseResource):
def __init__(self, api_url, token):
super().__init__(api_url, token)
def __init__(self, api_url, token, retries):
super().__init__(api_url, token, retries)
self.resource = "accounts"

def create(self, request: CreateDepositAccountRequest) -> Union[UnitResponse[AccountDTO], UnitError]:
payload = request.to_json_api()
response = super().post(self.resource, payload)
response = super().post_create(self.resource, payload)
if super().is_20x(response.status_code):
data = response.json().get("data")
return UnitResponse[AccountDTO](DtoDecoder.decode(data), None)
Expand Down Expand Up @@ -49,6 +50,7 @@ def activate_daca(self, account_id: str) -> Union[UnitResponse[AccountDTO], Unit
else:
return UnitError.from_json_api(response.json())


def get(self, account_id: str, include: Optional[str] = "") -> Union[UnitResponse[AccountDTO], UnitError]:
response = super().get(f"{self.resource}/{account_id}", {"include": include})
if super().is_20x(response.status_code):
Expand Down
4 changes: 2 additions & 2 deletions unit/api/api_token_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@


class APITokenResource(BaseResource):
def __init__(self, api_url, token):
super().__init__(api_url, token)
def __init__(self, api_url, token, retries):
super().__init__(api_url, token, retries)
self.resource = "users"

def create(self, request: CreateAPITokenRequest) -> Union[UnitResponse[APITokenDTO], UnitError]:
Expand Down
4 changes: 2 additions & 2 deletions unit/api/applicationForm_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@


class ApplicationFormResource(BaseResource):
def __init__(self, api_url, token):
super().__init__(api_url, token)
def __init__(self, api_url, token, retries):
super().__init__(api_url, token, retries)
self.resource = "application-forms"

def create(self, request: CreateApplicationFormRequest) -> Union[UnitResponse[ApplicationFormDTO], UnitError]:
Expand Down
6 changes: 3 additions & 3 deletions unit/api/application_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@


class ApplicationResource(BaseResource):
def __init__(self, api_url, token):
super().__init__(api_url, token)
def __init__(self, api_url, token, retries):
super().__init__(api_url, token, retries)
self.resource = "applications"

def create(self, request: Union[CreateIndividualApplicationRequest, CreateBusinessApplicationRequest]) -> Union[UnitResponse[ApplicationDTO], UnitError]:
payload = request.to_json_api()
response = super().post(self.resource, payload)
response = super().post_create(self.resource, payload)

if response.ok:
data = response.json().get("data")
Expand Down
5 changes: 3 additions & 2 deletions unit/api/atmLocation_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from unit.models.atm_location import *
from unit.models.codecs import DtoDecoder, UnitEncoder


class AtmLocationResource(BaseResource):
def __init__(self, api_url, token):
super().__init__(api_url, token)
def __init__(self, api_url, token, retries):
super().__init__(api_url, token, retries)
self.resource = "atm-locations"

"""
Expand Down
4 changes: 2 additions & 2 deletions unit/api/authorization_request_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@


class AuthorizationRequestResource(BaseResource):
def __init__(self, api_url, token):
super().__init__(api_url, token)
def __init__(self, api_url, token, retries):
super().__init__(api_url, token, retries)
self.resource = "authorization-requests"

def get(self, authorization_id: str) -> Union[UnitResponse[PurchaseAuthorizationRequestDTO], UnitError]:
Expand Down
4 changes: 2 additions & 2 deletions unit/api/authorization_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@


class AuthorizationResource(BaseResource):
def __init__(self, api_url, token):
super().__init__(api_url, token)
def __init__(self, api_url, token, retries):
super().__init__(api_url, token, retries)
self.resource = "authorizations"

def get(self, authorization_id: str, include_non_authorized: Optional[bool] = False) -> Union[UnitResponse[AuthorizationDTO], UnitError]:
Expand Down
68 changes: 66 additions & 2 deletions unit/api/base_resource.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,98 @@
import json
from typing import Optional, Dict
import requests
import backoff
from typing import Optional, Dict
from unit.models.codecs import UnitEncoder

retries = 0


def backoff_idempotency_key_handler(e):
return backoff_handler(e) and idempotency_key_is_present(e)


def backoff_handler(e):
code = e.status_code
return is_timeout(code) or is_rate_limit(code) or is_server_error(code)


def is_timeout(code):
return code == 408


def is_rate_limit(code):
return code == 429


def is_server_error(code):
return 500 <= code <= 599


def idempotency_key_is_present(e):
body = json.loads(e.request.body)
if body is None:
return False

return body["data"]["attributes"].get("idempotencyKey") is not None


class BaseResource(object):
def __init__(self, api_url, token):
def __init__(self, api_url, token, retries_amount):
global retries

self.api_url = api_url.rstrip("/")
self.token = token
self.headers = {
"content-type": "application/vnd.api+json",
"authorization": f"Bearer {self.token}",
"user-agent": "unit-python-sdk"
}
retries = retries_amount

@backoff.on_predicate(backoff.expo,
backoff_handler,
max_tries=retries,
jitter=backoff.random_jitter)
def get(self, resource: str, params: Dict = None, headers: Optional[Dict[str, str]] = None):
return requests.get(f"{self.api_url}/{resource}", params=params, headers=self.__merge_headers(headers))

@backoff.on_predicate(backoff.expo,
backoff_handler,
max_tries=retries,
jitter=backoff.random_jitter)
def post(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
data = json.dumps(data, cls=UnitEncoder) if data is not None else None
return requests.post(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))\


@backoff.on_predicate(backoff.expo,
backoff_idempotency_key_handler,
max_tries=retries,
jitter=backoff.random_jitter)
def post_create(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
data = json.dumps(data, cls=UnitEncoder) if data is not None else None
return requests.post(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))

@backoff.on_predicate(backoff.expo,
backoff_handler,
max_tries=retries,
jitter=backoff.random_jitter)
def patch(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
data = json.dumps(data, cls=UnitEncoder) if data is not None else None
return requests.patch(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))

@backoff.on_predicate(backoff.expo,
backoff_handler,
max_tries=retries,
jitter=backoff.random_jitter)
def delete(self, resource: str, data: Dict = None, headers: Optional[Dict[str, str]] = None):
data = json.dumps(data, cls=UnitEncoder) if data is not None else None
return requests.delete(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))

@backoff.on_predicate(backoff.expo,
backoff_handler,
max_tries=retries,
jitter=backoff.random_jitter)
def put(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
return requests.put(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))

Expand Down
4 changes: 2 additions & 2 deletions unit/api/bill_pay_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@


class BillPayResource(BaseResource):
def __init__(self, api_url, token):
super().__init__(api_url, token)
def __init__(self, api_url, token, retries):
super().__init__(api_url, token, retries)
self.resource = "payments/billpay/billers"

def get(self, params: GetBillersParams) -> Union[UnitResponse[List[BillerDTO]], UnitError]:
Expand Down
6 changes: 3 additions & 3 deletions unit/api/card_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@


class CardResource(BaseResource):
def __init__(self, api_url, token):
super().__init__(api_url, token)
def __init__(self, api_url, token, retries):
super().__init__(api_url, token, retries)
self.resource = "cards"

def create(self, request: CreateCardRequest) -> Union[UnitResponse[Card], UnitError]:
payload = request.to_json_api()
response = super().post(self.resource, payload)
response = super().post_create(self.resource, payload)
if super().is_20x(response.status_code):
data = response.json().get("data")
return UnitResponse[Card](DtoDecoder.decode(data), None)
Expand Down
Loading