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
2 changes: 1 addition & 1 deletion plaid/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from client import Client, require_access_token
from client import Client, require_access_token
98 changes: 62 additions & 36 deletions plaid/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
def require_access_token(func):
def inner_func(self, *args, **kwargs):
if not self.access_token:
raise Exception('`%s` method requires `access_token`' % func.__name__)
raise Exception('`%s` method requires `access_token`' %
func.__name__)
return func(self, *args, **kwargs)
return inner_func

Expand Down Expand Up @@ -56,7 +57,7 @@ def __init__(self, client_id, secret, access_token=None):
self.client_id = client_id
self.secret = secret
self.access_token = None

if access_token:
self.set_access_token(access_token)

Expand All @@ -71,22 +72,29 @@ def get_account_types(self):

# Endpoints

def connect(self, account_type, username, password, email, options={}):
def connect(self, account_type, username, password, email, options=None):
"""
Add a bank account user/login to Plaid and receive an access token
unless a 2nd level of authentication is required, in which case
unless a 2nd level of authentication is required, in which case
an MFA (Multi Factor Authentication) question(s) is returned

`account_type` str The type of bank account you want to sign in to, must
be one of the keys in `ACCOUNT_TYPES`
`username` str The username for the bank account you want to sign in to
`password` str The password for the bank account you want to sign in to
`email` str The email address associated with the bank account
`account_type` str The type of bank account you want to sign in
to, must be one of the keys in `ACCOUNT_TYPES`
`username` str The username for the bank account you want to
sign in to
`password` str The password for the bank account you want to
sign in to
`email` str The email address associated with the bank
account
`options` dict
`pretty` boolean Whether to return nicely formatted JSON or not
`webhook` str URL to hit once the account's transactions have been processed
`mfa_list` boolean List all available MFA (Multi Factor Authentication) options
`pretty` boolean Whether to return nicely formatted JSON
`webhook` str URL to hit once the account's transactions
have been processed
`mfa_list` boolean List all available MFA (Multi Factor
Authentication) options
"""
if options is None:
options = {}
url = urljoin(self.url, self.endpoints['connect'])

credentials = {
Expand Down Expand Up @@ -115,19 +123,24 @@ def connect(self, account_type, username, password, email, options={}):
return response

@require_access_token
def step(self, account_type, mfa, options={}):
def step(self, account_type, mfa, options=None):
"""
Perform a MFA (Multi Factor Authentication) step, requires `access_token`

`account_type` str The type of bank account you're performing MFA on,
must match what you used in the `connect` call
`mfa` str The MFA answer, e.g. an answer to q security question or
code sent to your phone, etc.
Perform a MFA (Multi Factor Authentication) step, requires
`access_token`

`account_type` str The type of bank account you're performing MFA
on, must match what you used in the `connect`
call
`mfa` str The MFA answer, e.g. an answer to q security
question or code sent to your phone, etc.
`options` dict
`send_method` dict The send method your MFA answer is for, e.g. {'type': Phone'},
should come from the list from the `mfa_list` option in the
`connect` call
`send_method` dict The send method your MFA answer is for,
e.g. {'type': Phone'}, should come from
the list from the `mfa_list` option in
the `connect` call
"""
if options is None:
options = {}
url = urljoin(self.url, self.endpoints['step'])

data = {
Expand Down Expand Up @@ -160,14 +173,17 @@ def delete_user(self):


@require_access_token
def transactions(self, options={}):
def transactions(self, options=None):
"""
Fetch a list of transactions, requires `access_token`

`options` dict
`pretty` boolean Whether to return nicely formatted JSON or not
`last` str Collect all transactions since this transaction ID
`pretty` boolean Whether to return nicely formatted JSON
`last` str Collect all transactions since this
transaction ID
"""
if options is None:
options = {}
url = urljoin(self.url, self.endpoints['connect'])

data = {
Expand All @@ -182,14 +198,16 @@ def transactions(self, options={}):

return http_request(url, 'GET', data)

def entity(self, entity_id, options={}):
def entity(self, entity_id, options=None):
"""
Fetch a specific entity's data

`entity_id` str Entity id to fetch
`options` dict
`pretty` boolean Whether to return nicely formatted JSON or not
"""
if options is None:
options = {}
url = urljoin(self.url, self.endpoints['entity'])
data = {
'entity_id': entity_id
Expand All @@ -207,33 +225,40 @@ def categories(self):
url = urljoin(self.url, self.endpoints['categories'])
return http_request(url, 'GET')

def category(self, category_id, options={}):
def category(self, category_id, options=None):
"""
Fetch a specific category

`category_id` str Category id to fetch
`options` dict
`pretty` boolean Whether to return nicely formatted JSON or not
"""
if options is None:
options = {}
url = urljoin(self.url, self.endpoints['category']) % category_id
data = {}
if options:
data['options'] = json.dumps(options)
return http_request(url, 'GET', data)

def categories_by_mapping(self, mapping, category_type, options={}):
def categories_by_mapping(self, mapping, category_type, options=None):
"""
Fetch category data by category mapping and data source

`mapping` str The category mapping to explore, e.g. "Food > Spanish Restaurant",
see all categories here:
`mapping` str The category mapping to explore,
e.g. "Food > Spanish Restaurant",
see all categories here:
https://github.com/plaid/Support/blob/master/categories.md
`category_type` str The category data source, must be a value from `CATEGORY_TYPES`
`category_type` str The category data source, must be a value from
`CATEGORY_TYPES`
`options` dict
`pretty` boolean Whether to return nicely formatted JSON or not
`full_match` boolean Whether to try an exact match for `mapping`. Setting
to `False` will return best match.
`pretty` boolean Whether to return nicely formatted JSON
`full_match` boolean Whether to try an exact match for
`mapping`. Setting to `False` will
return best match.
"""
if options is None:
options = {}
url = urljoin(self.url, self.endpoints['categories_by_mapping'])
data = {
'mapping': mapping,
Expand All @@ -244,11 +269,13 @@ def categories_by_mapping(self, mapping, category_type, options={}):
return http_request(url, 'GET', data)

@require_access_token
def balance(self, options = {}):
def balance(self, options=None):
"""
Fetch the real-time balance of the user's accounts

"""
if options is None:
options = {}
url = urljoin(self.url, self.endpoints['balance'])
data = {
'client_id': self.client_id,
Expand All @@ -259,4 +286,3 @@ def balance(self, options = {}):
data['options'] = json.dumps(options)

return http_request(url, 'GET', data)

29 changes: 12 additions & 17 deletions plaid/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
def _requests_http_request(url, method, data):
import requests
if method.upper() == 'GET':
return requests.get(url, data = data)
return requests.get(url, data=data)
elif method.upper() == 'POST':
return requests.post(url, data = data)
return requests.post(url, data=data)
elif method.upper() == 'PUT':
return requests.put(url, data = data)
return requests.put(url, data=data)
elif method.upper() == 'DELETE':
return requests.delete(url, data = data)
return requests.delete(url, data=data)
elif method.upper() == 'PATCH':
return requests.patch(url, data = data)
return requests.patch(url, data=data)

assert False

Expand All @@ -33,22 +33,20 @@ def _urlfetch_http_request(url, method, data):
payload = None
url += '?' + qs

response = urlfetch.fetch(url,
follow_redirects = True,
method = method,
payload = payload
)

response = urlfetch.fetch(url, follow_redirects=True, method=method,
payload=payload)
response.ok = response.status_code >= 200 and response.status_code < 300
return response


def _outer_http_request():
# We use _is_appengine to cache the one time computation of os.environ.get()
# We do this closure so that _is_appengine is not a file scope variable
ss = os.environ.get('SERVER_SOFTWARE')
_is_appengine = (ss and (ss.startswith('Development/') or ss.startswith('Google App Engine/')))
def _inner_http_request(url, method, data = None):
_is_appengine = os.environ.get('SERVER_SOFTWARE', '').split('/')[0] in (
'Development',
'Google App Engine',
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not exactly equivalent, @gae123, as SERVER_SOFTWARE=Development will now match. I assume this will be fine, though, and the good thing is that _is_appengine is now guaranteed to be a Boolean (previously it could have been None, instead).

def _inner_http_request(url, method, data=None):
if data is None:
data = {}
if _is_appengine:
Expand All @@ -57,6 +55,3 @@ def _inner_http_request(url, method, data = None):
return _requests_http_request(url, method, data)
return _inner_http_request
http_request = _outer_http_request()