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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ nosetests.xml
.mr.developer.cfg
.project
.pydevproject

#pycharm
.idea
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import json

from plaid import Client


client = Client(client_id='***', secret='***')
connect = client.connect(account_type='bofa', username='***', password='***', email='[email protected]')

Expand All @@ -28,4 +27,7 @@ if connect.ok:

## Attribution & Maintenance

This repository was originally authored by [Chris Forrette](https://github.com/chrisforrette), and will be monitored and maintained (though not actively developed) by the Plaid team. Please email [email protected] with any questions.
This repository was originally authored by [Chris Forrette](https://github.com/chrisforrette), and will be monitored and maintained (though not actively developed) by the Plaid team. Please email [email protected] with any questions.

### Other Contributors
- [PK](https://github.com/gae123) - fixes and Google App Engine Support
43 changes: 30 additions & 13 deletions plaid/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from urlparse import urljoin

import requests
from http import http_request

# @todo Sandboxing?
# @todo "Single Request Call"
Expand All @@ -10,10 +10,9 @@ 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__)
func(self, *args, **kwargs)
return func(self, *args, **kwargs)
return inner_func


class Client(object):
"""
Python Plain API v2 client https://plaid.io/
Expand Down Expand Up @@ -44,7 +43,8 @@ class Client(object):
'entity': '/entity',
'categories': '/category',
'category': '/category/id/%s',
'categories_by_mapping': '/category/map'
'categories_by_mapping': '/category/map',
'balance': '/balance'
}

def __init__(self, client_id, secret, access_token=None):
Expand Down Expand Up @@ -105,7 +105,7 @@ def connect(self, account_type, username, password, email, options={}):
if options:
data['options'] = json.dumps(options)

response = requests.post(url, data=data)
response = http_request(url, 'POST', data)

if response.ok:
json_data = json.loads(response.content)
Expand Down Expand Up @@ -141,7 +141,7 @@ def step(self, account_type, mfa, options={}):
if options:
data['options'] = json.dumps(options)

return requests.post(url, data=data)
return http_request(url, 'POST', data)

@require_access_token
def delete_user(self):
Expand All @@ -156,8 +156,8 @@ def delete_user(self):
'access_token': self.access_token
}

return requests.delete(url, data=data)
return http_request(url, 'DELETE', data)


@require_access_token
def transactions(self, options={}):
Expand All @@ -180,7 +180,7 @@ def transactions(self, options={}):
if options:
data['options'] = json.dumps(options)

return requests.get(url, data=data)
return http_request(url, 'GET', data)

def entity(self, entity_id, options={}):
"""
Expand All @@ -198,14 +198,14 @@ def entity(self, entity_id, options={}):
if options:
data['options'] = json.dumps(options)

return requests.get(url, data=data)
return http_request(url, 'GET', data)

def categories(self):
"""
Fetch all categories
"""
url = urljoin(self.url, self.endpoints['categories'])
return requests.get(url)
return http_request(url, 'GET')

def category(self, category_id, options={}):
"""
Expand All @@ -219,7 +219,7 @@ def category(self, category_id, options={}):
data = {}
if options:
data['options'] = json.dumps(options)
return requests.get(url, data=data)
return http_request(url, 'GET', data)

def categories_by_mapping(self, mapping, category_type, options={}):
"""
Expand All @@ -241,5 +241,22 @@ def categories_by_mapping(self, mapping, category_type, options={}):
}
if options:
data['options'] = json.dumps(options)
return requests.get(url, data=data)
return http_request(url, 'GET', data)

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

"""
url = urljoin(self.url, self.endpoints['balance'])
data = {
'client_id': self.client_id,
'secret': self.secret,
'access_token': self.access_token
}
if options:
data['options'] = json.dumps(options)

return http_request(url, 'GET', data)

62 changes: 62 additions & 0 deletions plaid/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
##############################################################################
# Helper module that encapsulates the HTTPS request so that it can be used
# with multiple runtimes. PK Mar. 14
##############################################################################
import os
import urllib

# Command line
def _requests_http_request(url, method, data):
import requests
if method.upper() == 'GET':
return requests.get(url, data = data)
elif method.upper() == 'POST':
return requests.post(url, data = data)
elif method.upper() == 'PUT':
return requests.put(url, data = data)
elif method.upper() == 'DELETE':
return requests.delete(url, data = data)
elif method.upper() == 'PATCH':
return requests.patch(url, data = data)

assert False

# Google App Engine
def _urlfetch_http_request(url, method, data):
from google.appengine.api import urlfetch

method = method.upper()
qs = urllib.urlencode(data)
if method == 'POST':
payload = qs
else:
payload = None
url += '?' + qs

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):
if data is None:
data = {}
if _is_appengine:
return _urlfetch_http_request(url, method, data)
else:
return _requests_http_request(url, method, data)
return _inner_http_request
http_request = _outer_http_request()



9 changes: 9 additions & 0 deletions samples/gae/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
application: plaid-gae
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /.*
script: main.application
32 changes: 32 additions & 0 deletions samples/gae/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import webapp2

import plaid

# need to fill these first
CLIENT = "" # as given by Plaid
SECRET = "" # as given by Plaid
ACCESS_TOKEN = "" # I assume here that you have already created this

class MainPage(webapp2.RequestHandler):

def get(self):
self.response.headers['Content-Type'] = 'text/html'
w = self.response.write
w("<html>")
w("<head>")
w('<title>Plaid Sample</title>')
w("</head>")
w("<body>")
w("<pre>")
client = plaid.Client(CLIENT, SECRET, ACCESS_TOKEN)
#res = client.transactions({'pretty': True})
#print res.content
w(client.balance({'pretty': True}).content)
w("</pre>")
w("</body>")
w("</html>")


application = webapp2.WSGIApplication([
('/', MainPage),
], debug=True)
1 change: 1 addition & 0 deletions samples/gae/plaid
15 changes: 14 additions & 1 deletion tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,28 @@ def test_delete_user_requires_access_token():
def test_transactions():
with patch('requests.get') as mock_requests_get:
client = Client('myclientid', 'mysecret', 'token')
client.transactions()
ret = client.transactions()
assert mock_requests_get.called
assert ret is not None


def test_transactions_requires_access_token():
client = Client('myclientid', 'mysecret')
with pytest.raises(Exception):
client.transactions()

def test_balance():
with patch('requests.get') as mock_requests_get:
client = Client('myclientid', 'mysecret', 'token')
ret = client.balance()
assert mock_requests_get.called
assert ret is not None


def test_balance_requires_access_token():
client = Client('myclientid', 'mysecret')
with pytest.raises(Exception):
client.balance()

def test_entity():
with patch('requests.get') as mock_requests_get:
Expand Down