Skip to content

Commit 51c8be1

Browse files
author
Greg Taylor
committed
See CHANGES for details on this commit. Lots of cleanup, compatibility with newer PayPal API versions, and cleaning some cruft.
1 parent 1580dd3 commit 51c8be1

File tree

5 files changed

+119
-74
lines changed

5 files changed

+119
-74
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
*.pyc
2+
*.swp
23
.project
34
.pydevproject
45
.settings

CHANGES

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
Changes introduced with each version.
22

3+
1.1.0
4+
-----
5+
This version addresses compatibility with the newer versions of the PayPal API
6+
by backing off on pre-query validation. PayPal's API error messages are
7+
the safest bet, as bad as they can be at times.
8+
9+
Lots of cleanup, commenting, and creature comforts abound. Backwards-incompatible
10+
changes are prefixed with 'BIC'.
11+
12+
* BIC: Removed the KEY_ERROR setting. Less complexity is better. (gtaylor)
13+
* Adding lots of logging, which users may attach to for debugging. (gtaylor)
14+
* Removed RESPONSE_KEYERROR. Wasn't being used. (gtaylor)
15+
* Removed DEBUG_LEVEL setting. Use Python logging. (gtaylor)
16+
* Removed some of the error checking on input values, as this
17+
limits users to the old API versions. We'll have to rely
18+
on PayPal's error messages being informative. (gtaylor)
19+
* Bumped the default API version protocol to 74.0, up from 53.0. (gtaylor)
20+
321
1.0.3
422
-----
523
* Python 2.5 compatibility. (Manik)
624

725
1.0.2
826
-----
927
* Documentation updates. (gtaylor)
10-
* We now distribute with a copy of the Apache License. (Greg)
28+
* We now distribute with a copy of the Apache License. (gtaylor)
1129

1230
1.0.1
1331
-----

paypal/interface.py

+54-50
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
import socket
1010
import urllib
1111
import urllib2
12+
import logging
13+
from pprint import pformat
1214

1315
from paypal.settings import PayPalConfig
1416
from paypal.response import PayPalResponse
1517
from paypal.exceptions import PayPalError, PayPalAPIResponseError
18+
19+
logger = logging.getLogger('paypal.interface')
1620

1721
class PayPalInterface(object):
1822
"""
@@ -68,11 +72,13 @@ def _call(self, method, **kwargs):
6872
6973
``kwargs`` will be a hash of
7074
"""
75+
# Beware, this is a global setting.
7176
socket.setdefaulttimeout(self.config.HTTP_TIMEOUT)
72-
77+
78+
# This dict holds the key/value pairs to pass to the PayPal API.
7379
url_values = {
7480
'METHOD': method,
75-
'VERSION': self.config.API_VERSION
81+
'VERSION': self.config.API_VERSION,
7682
}
7783

7884
headers = {}
@@ -88,30 +94,27 @@ def _call(self, method, **kwargs):
8894
url_values['SUBJECT'] = self.config.SUBJECT
8995
# headers['X-PAYPAL-REQUEST-DATA-FORMAT'] = 'NV'
9096
# headers['X-PAYPAL-RESPONSE-DATA-FORMAT'] = 'NV'
91-
# print(headers)
9297

98+
# All values passed to PayPal API must be uppercase.
9399
for key, value in kwargs.iteritems():
94100
url_values[key.upper()] = value
95101

96-
# When in DEBUG level 2 or greater, print out the NVP pairs.
97-
if self.config.DEBUG_LEVEL >= 2:
98-
k = url_values.keys()
99-
k.sort()
100-
for i in k:
101-
print " %-20s : %s" % (i , url_values[i])
102+
# This shows all of the key/val pairs we're sending to PayPal.
103+
if logger.isEnabledFor(logging.DEBUG):
104+
logger.debug('PayPal NVP Query Key/Vals:\n%s' % pformat(url_values))
102105

103106
url = self._encode_utf8(**url_values)
104-
105107
data = urllib.urlencode(url)
106108
req = urllib2.Request(self.config.API_ENDPOINT, data, headers)
107109
response = PayPalResponse(urllib2.urlopen(req).read(), self.config)
108110

109-
if self.config.DEBUG_LEVEL >= 1:
110-
print " %-20s : %s" % ("ENDPOINT", self.config.API_ENDPOINT)
111+
logger.debug('PayPal NVP API Endpoint: %s'% self.config.API_ENDPOINT)
111112

112113
if not response.success:
113-
if self.config.DEBUG_LEVEL >= 1:
114-
print response
114+
logger.error('A PayPal API error was encountered.')
115+
logger.error('PayPal NVP Query Key/Vals:\n%s' % pformat(url_values))
116+
logger.error('PayPal NVP Query Response')
117+
logger.error(response)
115118
raise PayPalAPIResponseError(response)
116119

117120
return response
@@ -284,51 +287,52 @@ def get_transaction_details(self, transactionid):
284287
del args['self']
285288
return self._call('GetTransactionDetails', **args)
286289

287-
def set_express_checkout(self, token='', **kwargs):
288-
"""Shortcut for the SetExpressCheckout method.
289-
JV did not like the original method. found it limiting.
290+
def set_express_checkout(self, **kwargs):
291+
"""Start an Express checkout.
292+
293+
You'll want to use this in conjunction with
294+
:meth:`generate_express_checkout_redirect_url` to create a payment,
295+
then figure out where to redirect the user to for them to
296+
authorize the payment on PayPal's website.
297+
298+
Required Keys
299+
-------------
300+
301+
* PAYMENTREQUEST_0_AMT
302+
* PAYMENTREQUEST_0_PAYMENTACTION
303+
* RETURNURL
304+
* CANCELURL
290305
"""
291-
kwargs.update(locals())
292-
del kwargs['self']
293-
self._check_required(('amt',), **kwargs)
294306
return self._call('SetExpressCheckout', **kwargs)
295307

296-
def do_express_checkout_payment(self, token, **kwargs):
297-
"""Shortcut for the DoExpressCheckoutPayment method.
298-
299-
Required
300-
*TOKEN
301-
PAYMENTACTION
302-
PAYERID
303-
AMT
304-
305-
Optional
306-
RETURNFMFDETAILS
307-
GIFTMESSAGE
308-
GIFTRECEIPTENABLE
309-
GIFTWRAPNAME
310-
GIFTWRAPAMOUNT
311-
BUYERMARKETINGEMAIL
312-
SURVEYQUESTION
313-
SURVEYCHOICESELECTED
314-
CURRENCYCODE
315-
ITEMAMT
316-
SHIPPINGAMT
317-
INSURANCEAMT
318-
HANDLINGAMT
319-
TAXAMT
308+
def do_express_checkout_payment(self, **kwargs):
309+
"""Finishes an Express checkout.
320310
321-
Optional + USEFUL
322-
INVNUM - invoice number
311+
:param str token: The token that was returned earlier by
312+
:meth:`set_express_checkout`. This identifies the transaction.
313+
314+
Required
315+
--------
316+
* TOKEN
317+
* PAYMENTACTION
318+
* PAYERID
319+
* AMT
323320
324321
"""
325-
kwargs.update(locals())
326-
del kwargs['self']
327-
self._check_required(('paymentaction', 'payerid'), **kwargs)
328322
return self._call('DoExpressCheckoutPayment', **kwargs)
329323

330324
def generate_express_checkout_redirect_url(self, token):
331-
"""Submit token, get redirect url for client."""
325+
"""Returns the URL to redirect the user to for the Express checkout.
326+
327+
Express Checkouts must be verified by the customer by redirecting them
328+
to the PayPal website. Use the token returned in the response from
329+
:meth:`set_express_checkout` with this function to figure out where
330+
to redirect the user to.
331+
332+
:param str token: The unique token identifying this transaction.
333+
:rtype: str
334+
:returns: The URL to redirect the user to for approval.
335+
"""
332336
url_vars = (self.config.PAYPAL_URL_BASE, token)
333337
return "%s?cmd=_express-checkout&token=%s" % url_vars
334338

paypal/response.py

+35-11
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
try:
66
from urlparse import parse_qs
77
except ImportError:
8+
# For Python 2.5 compatibility.
89
from cgi import parse_qs
910

10-
import paypal.exceptions
11+
import logging
12+
from pprint import pformat
13+
14+
logger = logging.getLogger('paypal.response')
1115

1216
class PayPalResponse(object):
1317
"""
@@ -20,39 +24,59 @@ class PayPalResponse(object):
2024
def __init__(self, query_string, config):
2125
"""
2226
query_string is the response from the API, in NVP format. This is
23-
parseable by urlparse.parse_qs(), which sticks it into the self.raw
24-
dict for retrieval by the user.
27+
parseable by urlparse.parse_qs(), which sticks it into the
28+
:attr:`raw` dict for retrieval by the user.
29+
30+
:param str query_string: The raw response from the API server.
31+
:param PayPalConfig config: The config object that was used to send
32+
the query that caused this response.
2533
"""
2634
# A dict of NVP values. Don't access this directly, use
2735
# PayPalResponse.attribname instead. See self.__getattr__().
2836
self.raw = parse_qs(query_string)
2937
self.config = config
38+
logger.debug("PayPal NVP API Response:\n%s" % self.__str__())
3039

3140
def __str__(self):
32-
return str(self.raw)
41+
"""
42+
Returns a string representation of the PayPalResponse object, in
43+
'pretty-print' format.
44+
45+
:rtype: str
46+
:returns: A 'pretty' string representation of the response dict.
47+
"""
48+
return pformat(self.raw)
3349

3450
def __getattr__(self, key):
3551
"""
3652
Handles the retrieval of attributes that don't exist on the object
37-
already. This is used to get API response values.
53+
already. This is used to get API response values. Handles some
54+
convenience stuff like discarding case and checking the cgi/urlparsed
55+
response value dict (self.raw).
56+
57+
:param str key: The response attribute to get a value for.
58+
:rtype: str
59+
:returns: The requested value from the API server's response.
3860
"""
3961
# PayPal response names are always uppercase.
4062
key = key.upper()
4163
try:
4264
value = self.raw[key]
4365
if len(value) == 1:
66+
# TODO: Figure out why we need this.
4467
return value[0]
4568
return value
4669
except KeyError:
47-
if self.config.KEY_ERROR:
48-
raise AttributeError(self)
49-
else:
50-
return None
70+
# The requested value wasn't returned in the response.
71+
raise AttributeError(self)
5172

5273
def success(self):
5374
"""
54-
Checks for the presence of errors in the response. Returns True if
55-
all is well, False otherwise.
75+
Checks for the presence of errors in the response. Returns ``True`` if
76+
all is well, ``False`` otherwise.
77+
78+
:rtype: bool
79+
:returns ``True`` if PayPal says our query was successful.
5680
"""
5781
return self.ack.upper() in (self.config.ACK_SUCCESS,
5882
self.config.ACK_SUCCESS_WITH_WARNING)

paypal/settings.py

+10-12
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
Most of this is transparent to the end developer, as the PayPalConfig object
55
is instantiated by the PayPalInterface object.
66
"""
7+
import logging
8+
from pprint import pformat
79

810
from paypal.exceptions import PayPalConfigError, PayPalError
911

12+
logger = logging.getLogger('paypal.settings')
13+
1014
class PayPalConfig(object):
1115
"""
1216
The PayPalConfig object is used to allow the developer to perform API
@@ -36,7 +40,8 @@ class PayPalConfig(object):
3640
'production' : 'https://www.paypal.com/webscr',
3741
}
3842

39-
API_VERSION = "60.0"
43+
# If no API version is specified, defaults to the latest API.
44+
API_VERSION = '74.0'
4045

4146
# Defaults. Used in the absence of user-specified values.
4247
API_ENVIRONMENT = 'sandbox'
@@ -56,19 +61,9 @@ class PayPalConfig(object):
5661

5762
ACK_SUCCESS = "SUCCESS"
5863
ACK_SUCCESS_WITH_WARNING = "SUCCESSWITHWARNING"
59-
60-
# 0 being no debugging, 1 being some, 2 being lots.
61-
DEBUG_LEVEL = 0
6264

6365
# In seconds. Depending on your setup, this may need to be higher.
6466
HTTP_TIMEOUT = 15
65-
66-
RESPONSE_KEYERROR = "AttributeError"
67-
68-
# When True, return an AttributeError when the user tries to get an
69-
# attribute on the response that does not exist. If False or None,
70-
# return None for non-existant attribs.
71-
KEY_ERROR = True
7267

7368
def __init__(self, **kwargs):
7469
"""
@@ -105,6 +100,9 @@ def __init__(self, **kwargs):
105100
raise PayPalConfigError('Missing in PayPalConfig: %s ' % arg)
106101
setattr(self, arg, kwargs[arg])
107102

108-
for arg in ('HTTP_TIMEOUT' , 'DEBUG_LEVEL' , 'RESPONSE_KEYERROR'):
103+
for arg in ['HTTP_TIMEOUT']:
109104
if arg in kwargs:
110105
setattr(self, arg, kwargs[arg])
106+
107+
logger.debug('PayPalConfig object instantiated with kwargs: %s' %
108+
pformat(kwargs))

0 commit comments

Comments
 (0)