Permalink
Browse files

Merge branch 'master' into perf

  • Loading branch information...
2 parents d744a71 + 9825acb commit ac1df7a515ec73a01f73b8cc2b526928dc328f47 Balanced Marshall committed Sep 6, 2012
Showing with 129 additions and 28 deletions.
  1. +1 −2 balanced/__init__.py
  2. +4 −0 balanced/exc.py
  3. +10 −9 balanced/http_client.py
  4. +57 −9 balanced/resources.py
  5. +41 −8 tests/suite.py
  6. +16 −0 tests/test_resource.py
View
3 balanced/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '0.8.10'
+__version__ = '0.8.19'
from collections import defaultdict
import contextlib
@@ -53,7 +53,6 @@ def is_configured():
def key_switcher(the_new_api_key_secret):
old_api_key = config.api_key_secret
config.api_key_secret = the_new_api_key_secret
- Resource.http_client = http_client = HTTPClient()
try:
yield
finally:
View
4 balanced/exc.py
@@ -22,3 +22,7 @@ class HTTPError(BalancedError, requests.HTTPError):
Baseclass for all HTTP exceptions.
"""
status_code = None
+
+
+class MoreInformationRequiredError(HTTPError):
+ redirect_uri = None
View
19 balanced/http_client.py
@@ -7,7 +7,7 @@
from balanced.config import Config
from balanced.utils import to_json
-from balanced.exc import HTTPError, BalancedError
+from balanced.exc import HTTPError, BalancedError, MoreInformationRequiredError
serializers = {
'application/json': to_json
@@ -34,17 +34,18 @@ def wrapped():
raise_for_status(allow_redirects=False)
except requests.HTTPError, exc:
if exc.response.status_code in REDIRECT_STATI:
- redirection = HTTPError('%s' % exc)
+ redirection = MoreInformationRequiredError('%s' % exc)
redirection.status_code = exc.response.status_code
redirection.response = exc.response
+ redirection.redirect_uri = exc.response.headers['Location']
raise redirection
deserialized = http_client.deserialize(
response_instance
)
response_instance.deserialized = deserialized
extra = deserialized.get('additional') or ''
if extra:
- extra = ' -- ' + extra + '.'
+ extra = ' -- {}.'.format(extra)
error_msg = '{name}: {code}: {msg} {extra}'.format(
name=deserialized['status'],
code=deserialized['status_code'],
@@ -108,9 +109,9 @@ class HTTPClient(threading.local, object):
config = Config()
- def __init__(self, *args, **kwargs):
+ def __init__(self, keep_alive=True, *args, **kwargs):
super(HTTPClient, self).__init__(*args, **kwargs)
- self.session = requests.session()
+ self.interface = requests.session() if keep_alive else requests
# we don't use the requests hook here because we want to expose
# that for any developer to access it directly.
@@ -120,31 +121,31 @@ def __init__(self, *args, **kwargs):
@munge_request
def get(self, uri, **kwargs):
kwargs = self.serialize(kwargs.copy())
- resp = self.session.get(uri, **kwargs)
+ resp = self.interface.get(uri, **kwargs)
if kwargs.get('return_response', True):
resp.deserialized = self.deserialize(resp)
return resp
@munge_request
def post(self, uri, data=None, **kwargs):
data = self.serialize({'data': data}).pop('data')
- resp = self.session.post(uri, data=data, **kwargs)
+ resp = self.interface.post(uri, data=data, **kwargs)
if kwargs.get('return_response', True):
resp.deserialized = self.deserialize(resp)
return resp
@munge_request
def put(self, uri, data=None, **kwargs):
data = self.serialize({'data': data}).pop('data')
- resp = self.session.put(uri, data=data, **kwargs)
+ resp = self.interface.put(uri, data=data, **kwargs)
if kwargs.get('return_response', True):
resp.deserialized = self.deserialize(resp)
return resp
@munge_request
def delete(self, uri, **kwargs):
kwargs = self.serialize(kwargs.copy())
- resp = self.session.delete(uri, **kwargs)
+ resp = self.interface.delete(uri, **kwargs)
if kwargs.get('return_response', True):
resp.deserialized = self.deserialize(resp)
return resp
View
66 balanced/resources.py
@@ -2,6 +2,7 @@
import itertools
import logging
import urlparse
+import warnings
import iso8601
@@ -62,6 +63,16 @@ def __getitem__(self, item):
else:
return list(res)
else:
+ # negative index
+ if item < 0:
+ # e.g. let len(self) = 3 and item = -1
+ # self[length of collection - item : length of collection]
+ # self[3 - 1: 3]
+ length = len(self)
+ return list(self[length + item:length])[0]
+ # positive index
+ # let item = 2
+ # self[2:3][0]
return list(self[item:item + 1])[0]
def _slice(self, start, stop):
@@ -74,6 +85,9 @@ def _slice(self, start, stop):
self.qs['offset'] = (self.offset or 0) + start
return itertools.islice(self, start, stop)
+ def __len__(self):
+ return self.total
+
def __iter__(self):
for resource in itertools.chain(self.items, self.next_page):
yield resource
@@ -468,12 +482,26 @@ class Account(Resource):
"""
__metaclass__ = resource_base(collection='accounts')
- def debit(self, amount=None, appears_on_statement_as=None,
- hold_uri=None, meta=None, description=None, source_uri=None):
+ def debit(self,
+ amount=None,
+ appears_on_statement_as=None,
+ hold_uri=None,
+ meta=None,
+ description=None,
+ source_uri=None,
+ merchant_uri=None):
"""
:rtype: A `Debit` representing a flow of money from this Account to
your Marketplace's escrow account.
-
+ :param amount: Amount to hold in cents, must be >= 50
+ :param appears_on_statement_as: description of the payment as it needs
+ to appear on customers card statement
+ :param meta: Key/value collection
+ :param description: Human readable description
+ :param source_uri: A specific funding source such as a `Card`
+ associated with this account. If not specified the `Card` most
+ recently added to this `Account` is used.
+ :param merchant_uri: merchant providing service or delivering product.
"""
if not any((amount, hold_uri)):
raise ResourceError('Must have an amount or hold uri')
@@ -489,9 +517,11 @@ def debit(self, amount=None, appears_on_statement_as=None,
meta=meta,
description=description,
source_uri=source_uri,
+ merchant_uri=merchant_uri,
).save()
- def hold(self, amount, description=None, meta=None, source_uri=None):
+ def hold(self, amount, description=None, meta=None, source_uri=None,
+ appears_on_statement_as=None):
"""
Creates a new Hold that represents a reservation of money on this
Account which can be transferred via a Debit to your Marketplace
@@ -514,17 +544,27 @@ def hold(self, amount, description=None, meta=None, source_uri=None):
meta=meta,
description=description,
source_uri=source_uri,
+ appears_on_statement_as=appears_on_statement_as,
).save()
- def credit(self, amount, description=None, meta=None,
- destination_uri=None):
+ def credit(self,
+ amount,
+ description=None,
+ meta=None,
+ destination_uri=None,
+ appears_on_statement_as=None,
+ debit_uri=None):
"""
Returns a new Credit representing a transfer of funds from your
Marketplace's escrow account to this Account.
- Args:
- destination_uri: A specific funding destination such as a
+ :param amount: Amount to hold in cents
+ :param description: Human readable description
+ :param meta: Key/value collection
+ :param destination_uri: A specific funding destination such as a
`BankAccount` associated with this account.
+ :param appears_on_statement_as: description of the payment as it needs
+ :param debit_uri: the debit corresponding to this particular credit
Returns:
A `Credit` representing the transfer of funds from your
@@ -536,7 +576,9 @@ def credit(self, amount, description=None, meta=None,
amount=amount,
meta=meta,
description=description,
+ appears_on_statement_as=appears_on_statement_as,
destination_uri=destination_uri,
+ debit_uri=debit_uri,
).save()
def add_card(self, card_uri):
@@ -625,6 +667,12 @@ def create_card(self,
:rtype: `Card`
"""
+
+ if region:
+ warnings.warn('The region parameter will be deprecated in the '
+ 'next minor version of balanced-python',
+ PendingDeprecationWarning)
+
return Card(
card_number=card_number,
expiration_month=expiration_month,
@@ -637,7 +685,7 @@ def create_card(self,
region=region,
country_code=country_code,
phone_number=phone_number,
- ).save()
+ ).save()
def create_bank_account(self,
name,
View
49 tests/suite.py
@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-
+import re
import unittest
import requests
import balanced
-from balanced.exc import NoResultFound
+from balanced.exc import NoResultFound, MoreInformationRequiredError
# fixtures
@@ -95,6 +95,17 @@
'bank_code': '121042882',
}
+PERSON_FAILING_KYC = {
+ 'type': 'person',
+ 'name': 'William James',
+ 'dob': '1842-01-01',
+ 'phone_number': '+16505551234',
+ 'street_address': '801 High St',
+ 'postal_code': '99999',
+ 'region': 'EX',
+ 'country_code': 'USA',
+ }
+
# tests
@@ -205,13 +216,13 @@ def test_06_debit_buyer_account_and_refund(self):
self.assertIsInstance(debit.account, balanced.Account)
self.assertIsInstance(debit.hold, balanced.Hold)
self.assertEqual(debit.description, 'Descripty')
- self.assertEqual(debit.fee, (1000 * 0.035))
+ self.assertEqual(debit.fee, (1000 * 0.029))
self.assertEqual(debit.appears_on_statement_as, 'atest')
refund = debit.refund(amount=100)
#self.assertTrue(refund.id.startswith('RF'))
self.assertEqual(refund.debit.uri, debit.uri)
- self.assertEqual(refund.fee, -1 * int((100 * 0.035)))
+ self.assertEqual(refund.fee, -1 * int((100 * 0.029)))
another_debit = account.debit(
amount=1000,
@@ -224,20 +235,20 @@ def test_06_debit_buyer_account_and_refund(self):
def test_07_create_hold_and_void_it(self):
account = self._find_account('buyer')
hold = account.hold(amount=1500, description='Hold me')
- self.assertEqual(hold.fee, 35)
+ self.assertEqual(hold.fee, 30)
self.assertEqual(hold.account.uri, account.uri)
self.assertFalse(hold.is_void)
self.assertEqual(hold.description, 'Hold me')
hold.void()
self.assertTrue(hold.is_void)
- self.assertEqual(hold.fee, 35) # fee still the same
+ self.assertEqual(hold.fee, 30) # fee still the same
def test_08_create_hold_and_debit_it(self):
account = self._find_account('buyer')
hold = account.hold(amount=1500)
self.assertTrue(hold.id.startswith('HL'))
debit = hold.capture()
- self.assertEqual(debit.fee, int((1500 * 0.035)))
+ self.assertEqual(debit.fee, int((1500 * 0.029)))
def test_09_create_a_person_merchant(self):
mp = self._find_marketplace()
@@ -258,7 +269,7 @@ def test_10_create_a_business_merchant(self):
merchant=BUSINESS_MERCHANT,
bank_account_uri=bank_account.uri,
)
- self.assertItemsEqual(merchant.roles, ['buyer', 'merchant'])
+ self.assertItemsEqual(merchant.roles, ['merchant'])
def test_11_create_a_business_merchant_with_existing_email_addr(self):
mp = self._find_marketplace()
@@ -319,10 +330,16 @@ def test_15_debits_without_an_account(self):
def test_16_slice_syntax(self):
total_debit = balanced.Debit.query.count()
self.assertNotEqual(total_debit, 2)
+ self.assertEqual(len(balanced.Debit.query), total_debit)
sliced_debits = balanced.Debit.query[:2]
self.assertEqual(len(sliced_debits), 2)
for debit in sliced_debits:
self.assertIsInstance(debit, balanced.Debit)
+ all_debits = balanced.Debit.query.all()
+ last = total_debit * - 1
+ for index, debit in enumerate(all_debits):
+ self.assertEqual(debit.uri,
+ balanced.Debit.query[last + index].uri)
def test_17_test_merchant_cache_busting(self):
# cache it.
@@ -412,3 +429,19 @@ def test_22_create_international_card(self):
self.assertTrue(card.id.startswith('CC'))
self.assertEqual(card.street_address,
INTERNATIONAL_CARD['street_address'])
+
+ def test_23_kyc_redirect(self):
+ try:
+ mp = balanced.Marketplace.query.one()
+ except NoResultFound:
+ mp = balanced.Marketplace().save()
+
+ redirect_pattern = ('https://www.balancedpayments.com'
+ '/marketplaces/(.*)/kyc')
+
+ with self.assertRaises(MoreInformationRequiredError) as ex:
+ mp.create_merchant('marshall@poundpay.com', PERSON_FAILING_KYC)
+
+ redirect_uri = ex.exception.redirect_uri
+ result = re.search(redirect_pattern, redirect_uri)
+ self.assertTrue(result)
View
16 tests/test_resource.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
import datetime
import unittest
+import mock
import balanced
from .application import app
@@ -79,3 +80,18 @@ def test_sort(self):
self.assertEqual(q.qs, {'sort': ['me,asc']})
q.sort(balanced.Marketplace.f.u.desc())
self.assertEqual(q.qs, {'sort': ['me,asc', 'u,desc']})
+
+
+class TestMarketplace(unittest.TestCase):
+
+ @mock.patch('balanced.resources.Card')
+ @mock.patch('balanced.resources.warnings')
+ def test_region_deprecation(self, warnings, _):
+ mkt = balanced.Marketplace()
+ mkt.create_card('John Name', '341111111111111', '12', '2020',
+ region='CA')
+ call_args, _ = warnings.warn.call_args
+ self.assertEqual(call_args[0],
+ ('The region parameter will be deprecated in the '
+ 'next minor version of balanced-python'))
+ self.assertEqual(call_args[1], PendingDeprecationWarning)

0 comments on commit ac1df7a

Please sign in to comment.