PageRenderTime 1245ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/plaid/client.py

https://gitlab.com/tpedar/plaid-python
Python | 452 lines | 389 code | 17 blank | 46 comment | 7 complexity | 4081cf3f1ddecd2d846d56b54a4f9412 MD5 | raw file
  1. import os
  2. import warnings
  3. from plaid.requester import (
  4. delete_request,
  5. get_request,
  6. http_request,
  7. patch_request,
  8. post_request
  9. )
  10. from plaid.utils import json, urljoin, to_json
  11. from plaid.errors import UnauthorizedError
  12. def inject_url(path):
  13. '''
  14. Produces a decorator which injects a url as
  15. the first argument to the decorated function
  16. `path` str
  17. '''
  18. def decorator(func):
  19. def inner_func(self, *args, **kwargs):
  20. url = urljoin(self.base_url, self.ENDPOINTS[path])
  21. return func(self, url, *args, **kwargs)
  22. return inner_func
  23. return decorator
  24. def inject_credentials(func):
  25. '''
  26. Decorator which injects a credentials object
  27. containing client_id, secret, and access_token
  28. as the first argument to the decorated function
  29. `func` function
  30. '''
  31. def inner_func(self, *args, **kwargs):
  32. if not hasattr(self, 'access_token'):
  33. raise UnauthorizedError(
  34. '{} requires `access_token`'.format(func.__name__), 1000
  35. )
  36. else:
  37. credentials = {
  38. 'client_id': self.client_id,
  39. 'secret': self.secret,
  40. 'access_token': self.access_token,
  41. }
  42. return func(self, credentials, *args, **kwargs)
  43. return inner_func
  44. def store_access_token(func):
  45. '''
  46. Decorator which extracts the access_token from valid responses
  47. and stores it on the client instance
  48. `func` function
  49. '''
  50. def inner_func(self, *args, **kwargs):
  51. response = func(self, *args, **kwargs)
  52. if response.ok:
  53. json_data = to_json(response)
  54. self.access_token = json_data.get(
  55. 'access_token',
  56. self.access_token
  57. )
  58. return response
  59. return inner_func
  60. class Client(object):
  61. '''
  62. Python Plain API v2 client https://plaid.com/
  63. See official documentation at: https://plaid.com/docs
  64. '''
  65. base_url = os.environ.get('PLAID_HOST', 'https://tartan.plaid.com')
  66. suppress_http_errors = False
  67. suppress_warnings = False
  68. ACCOUNT_TYPES = (
  69. ('amex', 'American Express',),
  70. ('bofa', 'Bank of America',),
  71. ('chase', 'Chase',),
  72. ('citi', 'Citi',),
  73. ('wells', 'Wells Fargo',),
  74. )
  75. CATEGORY_TYPES = [
  76. 'plaid',
  77. 'foursquare',
  78. 'factual',
  79. 'amex'
  80. ]
  81. ENDPOINTS = {
  82. 'auth': '/auth',
  83. 'auth_get': '/auth/get',
  84. 'auth_step': '/auth/step',
  85. 'balance': '/balance',
  86. 'connect': '/connect',
  87. 'connect_get': '/connect/get',
  88. 'connect_step': '/connect/step',
  89. 'categories': '/categories',
  90. 'category': '/categories/{}',
  91. 'info_get': '/info/get',
  92. 'institutions': '/institutions',
  93. 'institution': '/institutions/{}',
  94. 'upgrade': '/upgrade',
  95. 'exchange_token': '/exchange_token',
  96. }
  97. @classmethod
  98. def config(kls, options):
  99. '''
  100. Configure the Client Class (Client.config({}))
  101. `options` dict
  102. `url` str Fully qualified domain name (tartan or api)
  103. `suppress_errors` bool Should Plaid Errors be suppressed
  104. `suppress_warnings` bool Should Plaid warnings be suppressed
  105. '''
  106. kls.base_url = options.get('url', kls.base_url)
  107. kls.suppress_http_errors = options.get('suppress_http_errors')
  108. kls.suppress_warnings = options.get('suppress_warnings')
  109. def __init__(self, client_id, secret, access_token=None):
  110. '''
  111. `client_id` str Your Plaid client ID
  112. `secret` str Your Plaid secret
  113. `access_token` str Access token for existing user (optional)
  114. '''
  115. self.client_id = client_id
  116. self.secret = secret
  117. self.access_token = access_token
  118. if 'tartan' in self.base_url and not self.suppress_warnings:
  119. warnings.warn('''
  120. Tartan is not intended for production usage.
  121. Swap out url for https://api.plaid.com
  122. via Client.config before switching to production
  123. ''')
  124. @store_access_token
  125. def _add(self, url, account_type, login, options=None):
  126. '''
  127. Add a bank account user/login to Plaid and receive an access token
  128. unless a 2nd level of authentication is required, in which case
  129. an MFA (Multi Factor Authentication) question(s) is returned
  130. `url` str Plaid endpoint url
  131. `account_type` str The type of bank account you want to sign in
  132. to, must be one of the keys in `ACCOUNT_TYPES`
  133. `login` dict
  134. `username` str username for the bank account
  135. `password` str The password for the bank account
  136. `pin` int (optional) pin for the bank account
  137. `options` dict
  138. `webhook` str URL to hit once the account's transactions
  139. have been processed
  140. `mfa_list` boolean List all available MFA (Multi Factor
  141. Authentication) options
  142. '''
  143. return post_request(url, data={
  144. 'client_id': self.client_id,
  145. 'secret': self.secret,
  146. 'type': account_type,
  147. 'credentials': json.dumps(login),
  148. 'options': json.dumps(
  149. dict(
  150. {'list': True},
  151. **(options if options is not None else {})
  152. )
  153. ),
  154. }, suppress_errors=self.suppress_http_errors)
  155. @store_access_token
  156. @inject_credentials
  157. def _step(self, credentials, url, method, account_type, mfa, options=None):
  158. '''
  159. Perform a MFA (Multi Factor Authentication) step with access_token.
  160. `method` str HTTP Method
  161. `url` str Plaid endpoint url
  162. `account_type` str The type of bank account you're performing MFA
  163. on, must match what you used in the `connect`
  164. call
  165. `mfa` str The MFA answer, e.g. an answer to q security
  166. question or code sent to your phone, etc.
  167. `options` dict
  168. `send_method` dict The send method your MFA answer is for,
  169. e.g. {'type': phone'}, should come from
  170. the list from the `mfa_list` option in
  171. the call
  172. '''
  173. return http_request(
  174. url,
  175. method=method,
  176. data=dict({
  177. 'type': account_type,
  178. 'mfa': mfa,
  179. 'options': json.dumps(options if options is not None else {}),
  180. }, **credentials),
  181. suppress_errors=self.suppress_http_errors
  182. )
  183. @store_access_token
  184. @inject_credentials
  185. def _update(self, credentials, url, login):
  186. '''
  187. Similar to _add, save HTTP method, inclusion of access token,
  188. and absence of options / account_type
  189. '''
  190. return patch_request(
  191. url,
  192. data=dict(login, **credentials),
  193. suppress_errors=self.suppress_http_errors
  194. )
  195. # WRITE ENDPOINTS
  196. @inject_url('auth')
  197. def auth(self, url, *args, **kwargs):
  198. '''
  199. Add an Auth user to Plaid.
  200. See _add docstring for input annotation
  201. '''
  202. return self._add(url, *args, **kwargs)
  203. @inject_credentials
  204. @inject_url('auth')
  205. def auth_delete(self, url, credentials):
  206. '''
  207. Delete Auth user from Plaid
  208. '''
  209. return delete_request(
  210. url,
  211. data=credentials,
  212. suppress_errors=self.suppress_http_errors
  213. )
  214. @inject_url('auth_step')
  215. def auth_step(self, url, *args, **kwargs):
  216. '''
  217. MFA step associated with initially Auth-ing a user.
  218. See _step docstring for input annotation
  219. '''
  220. return self._step(url, 'POST', *args, **kwargs)
  221. @inject_url('auth')
  222. def auth_update(self, url, login):
  223. '''
  224. Update a user who has already authed
  225. `login` dict
  226. `username` str username for the bank account
  227. `password` str The password for the bank account
  228. `pin` int (optional) pin for the bank account
  229. '''
  230. return self._update(url, login)
  231. @inject_url('auth_step')
  232. def auth_update_step(self, url, *args, **kwargs):
  233. '''
  234. MFA step associated with updating an Auth-ed user.
  235. See _step docstring for input annotation
  236. '''
  237. return self._step(url, 'PATCH', *args, **kwargs)
  238. @inject_url('connect')
  239. def connect(self, url, *args, **kwargs):
  240. '''
  241. Add a Connect user to Plaid.
  242. See _add docstring for input annotation
  243. '''
  244. return self._add(url, *args, **kwargs)
  245. @inject_credentials
  246. @inject_url('connect')
  247. def connect_delete(self, url, credentials):
  248. '''
  249. Delete user who has connected from Plaid
  250. '''
  251. return delete_request(
  252. url,
  253. data=credentials,
  254. suppress_errors=self.suppress_http_errors
  255. )
  256. @inject_url('connect_step')
  257. def connect_step(self, url, *args, **kwargs):
  258. '''
  259. MFA step associated with initially Connect-ing a user.
  260. See _step docstring for input annotation
  261. '''
  262. return self._step(url, 'POST', *args, **kwargs)
  263. @inject_url('connect')
  264. def connect_update(self, url, login):
  265. '''
  266. Update a user who has already connected
  267. `login` dict
  268. `username` str username for the bank account
  269. `password` str The password for the bank account
  270. `pin` int (optional) pin for the bank account
  271. '''
  272. return self._update(url, login)
  273. @inject_url('connect_step')
  274. def connect_update_step(self, url, *args, **kwargs):
  275. '''
  276. MFA step associated with updating a Connect-ed user.
  277. See _step docstring for input annotation
  278. '''
  279. return self._step(url, 'PATCH', *args, **kwargs)
  280. @store_access_token
  281. @inject_url('exchange_token')
  282. def exchange_token(self, url, public_token):
  283. '''
  284. Only applicable to apps using the Link front-end SDK
  285. Exchange a Link public_token for an API access_token
  286. `public_token` str public_token returned by Link client
  287. '''
  288. return post_request(
  289. url,
  290. data={
  291. 'client_id': self.client_id,
  292. 'secret': self.secret,
  293. 'public_token': public_token,
  294. },
  295. suppress_errors=self.suppress_http_errors
  296. )
  297. @store_access_token
  298. @inject_credentials
  299. @inject_url('upgrade')
  300. def upgrade(self, url, credentials, product):
  301. '''
  302. Upgrade account to another plaid type
  303. `product` str [auth | connect]
  304. '''
  305. return post_request(
  306. url,
  307. data=dict(credentials, upgrade_to=product),
  308. suppress_errors=self.suppress_http_errors
  309. )
  310. # READ ENDPOINTS
  311. @inject_credentials
  312. @inject_url('auth_get')
  313. def auth_get(self, url, credentials):
  314. '''
  315. Fetch accounts associated with the set access_token
  316. '''
  317. return post_request(
  318. url,
  319. data=credentials,
  320. suppress_errors=self.suppress_http_errors
  321. )
  322. @inject_credentials
  323. @inject_url('balance')
  324. def balance(self, url, credentials):
  325. '''
  326. Fetch the real-time balance of the user's accounts
  327. '''
  328. return get_request(
  329. url,
  330. data=credentials,
  331. suppress_errors=self.suppress_http_errors
  332. )
  333. @inject_credentials
  334. @inject_url('connect_get')
  335. def connect_get(self, url, credentials, opts=None):
  336. '''
  337. Fetch a list of transactions, requires `access_token`
  338. `options` dict (optional)
  339. `pending` bool Fetch pending transactions (default false)
  340. `account` str Fetch transactions only from this account
  341. `gte` date Fetch transactions posted after this date
  342. (default 30 days ago)
  343. `lte` date Fetch transactions posted before this date
  344. '''
  345. return post_request(
  346. url,
  347. data=dict(
  348. credentials,
  349. **{'options': json.dumps(opts if opts is not None else {})}
  350. ),
  351. suppress_errors=self.suppress_http_errors
  352. )
  353. @inject_url('categories')
  354. def categories(self, url):
  355. '''Fetch all Plaid Categories'''
  356. return get_request(url, suppress_errors=self.suppress_http_errors)
  357. @inject_url('category')
  358. def category(self, url, category_id):
  359. '''
  360. Fetch a specific category
  361. `category_id` str Category id to fetch
  362. '''
  363. return get_request(
  364. url.format(category_id),
  365. suppress_errors=self.suppress_http_errors
  366. )
  367. @inject_credentials
  368. @inject_url('info_get')
  369. def info_get(self, url, credentials):
  370. '''
  371. Fetches info for a user
  372. '''
  373. return post_request(
  374. url,
  375. data=credentials,
  376. suppress_errors=self.suppress_http_errors
  377. )
  378. @inject_url('institutions')
  379. def institutions(self, url):
  380. '''
  381. Fetch all Plaid institutions
  382. '''
  383. return get_request(url, suppress_errors=self.suppress_http_errors)
  384. @inject_url('institution')
  385. def institution(self, url, institution_id):
  386. '''
  387. Fetch details for a single institution
  388. `institution_id` str Category id to fetch
  389. '''
  390. return get_request(
  391. url.format(institution_id),
  392. suppress_errors=self.suppress_http_errors
  393. )