From 05d25e7e80db18c5ff15b9ec2e0f0b8bd863be49 Mon Sep 17 00:00:00 2001 From: Samuel Clay Date: Sun, 31 Mar 2013 23:08:24 -0700 Subject: [PATCH] Adding Paypal API. --- vendor/paypalapi/__init__.py | 11 + vendor/paypalapi/compat.py | 128 +++++++++ vendor/paypalapi/countries.py | 290 ++++++++++++++++++++ vendor/paypalapi/exceptions.py | 45 ++++ vendor/paypalapi/interface.py | 469 +++++++++++++++++++++++++++++++++ vendor/paypalapi/response.py | 115 ++++++++ vendor/paypalapi/settings.py | 130 +++++++++ 7 files changed, 1188 insertions(+) create mode 100644 vendor/paypalapi/__init__.py create mode 100644 vendor/paypalapi/compat.py create mode 100644 vendor/paypalapi/countries.py create mode 100644 vendor/paypalapi/exceptions.py create mode 100644 vendor/paypalapi/interface.py create mode 100644 vendor/paypalapi/response.py create mode 100644 vendor/paypalapi/settings.py diff --git a/vendor/paypalapi/__init__.py b/vendor/paypalapi/__init__.py new file mode 100644 index 000000000..327029ab3 --- /dev/null +++ b/vendor/paypalapi/__init__.py @@ -0,0 +1,11 @@ +# coding=utf-8 +#noinspection PyUnresolvedReferences +from paypalapi.interface import PayPalInterface +#noinspection PyUnresolvedReferences +from paypalapi.settings import PayPalConfig +#noinspection PyUnresolvedReferences +from paypalapi.exceptions import PayPalError, PayPalConfigError, PayPalAPIResponseError +#noinspection PyUnresolvedReferences +import paypalapi.countries + +VERSION = '1.2.0' diff --git a/vendor/paypalapi/compat.py b/vendor/paypalapi/compat.py new file mode 100644 index 000000000..4e69f58f4 --- /dev/null +++ b/vendor/paypalapi/compat.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +""" +pythoncompat, from python-requests. + +Copyright (c) 2012 Kenneth Reitz. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +""" + + +import sys + +# ------- +# Pythons +# ------- + +# Syntax sugar. +_ver = sys.version_info + +#: Python 2.x? +is_py2 = (_ver[0] == 2) + +#: Python 3.x? +is_py3 = (_ver[0] == 3) + +#: Python 3.0.x +is_py30 = (is_py3 and _ver[1] == 0) + +#: Python 3.1.x +is_py31 = (is_py3 and _ver[1] == 1) + +#: Python 3.2.x +is_py32 = (is_py3 and _ver[1] == 2) + +#: Python 3.3.x +is_py33 = (is_py3 and _ver[1] == 3) + +#: Python 3.4.x +is_py34 = (is_py3 and _ver[1] == 4) + +#: Python 2.7.x +is_py27 = (is_py2 and _ver[1] == 7) + +#: Python 2.6.x +is_py26 = (is_py2 and _ver[1] == 6) + +#: Python 2.5.x +is_py25 = (is_py2 and _ver[1] == 5) + +#: Python 2.4.x +is_py24 = (is_py2 and _ver[1] == 4) # I'm assuming this is not by choice. + + +# --------- +# Platforms +# --------- + + +# Syntax sugar. +_ver = sys.version.lower() + +is_pypy = ('pypy' in _ver) +is_jython = ('jython' in _ver) +is_ironpython = ('iron' in _ver) + +# Assume CPython, if nothing else. +is_cpython = not any((is_pypy, is_jython, is_ironpython)) + +# Windows-based system. +is_windows = 'win32' in str(sys.platform).lower() + +# Standard Linux 2+ system. +is_linux = ('linux' in str(sys.platform).lower()) +is_osx = ('darwin' in str(sys.platform).lower()) +is_hpux = ('hpux' in str(sys.platform).lower()) # Complete guess. +is_solaris = ('solar==' in str(sys.platform).lower()) # Complete guess. + + +# --------- +# Specifics +# --------- + + +if is_py2: + #noinspection PyUnresolvedReferences,PyCompatibility + from urllib import quote, unquote, urlencode + #noinspection PyUnresolvedReferences,PyCompatibility + from urlparse import urlparse, urlunparse, urljoin, urlsplit + #noinspection PyUnresolvedReferences,PyCompatibility + from urllib2 import parse_http_list + #noinspection PyUnresolvedReferences,PyCompatibility + import cookielib + #noinspection PyUnresolvedReferences,PyCompatibility + from Cookie import Morsel + #noinspection PyUnresolvedReferences,PyCompatibility + from StringIO import StringIO + + bytes = str + #noinspection PyUnresolvedReferences,PyCompatibility + str = unicode + #noinspection PyUnresolvedReferences,PyCompatibility,PyUnboundLocalVariable + basestring = basestring +elif is_py3: + #noinspection PyUnresolvedReferences,PyCompatibility + from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote + #noinspection PyUnresolvedReferences,PyCompatibility + from urllib.request import parse_http_list + #noinspection PyUnresolvedReferences,PyCompatibility + from http import cookiejar as cookielib + #noinspection PyUnresolvedReferences,PyCompatibility + from http.cookies import Morsel + #noinspection PyUnresolvedReferences,PyCompatibility + from io import StringIO + + str = str + bytes = bytes + basestring = (str,bytes) diff --git a/vendor/paypalapi/countries.py b/vendor/paypalapi/countries.py new file mode 100644 index 000000000..7c9d20c7d --- /dev/null +++ b/vendor/paypalapi/countries.py @@ -0,0 +1,290 @@ +""" +Country Code List: ISO 3166-1993 (E) + +http://xml.coverpages.org/country3166.html + +A tuple of tuples of country codes and their full names. There are a few helper +functions provided if you'd rather not use the dict directly. Examples provided +in the test_countries.py unit tests. +""" + +COUNTRY_TUPLES = ( + ('US', 'United States of America'), + ('CA', 'Canada'), + ('AD', 'Andorra'), + ('AE', 'United Arab Emirates'), + ('AF', 'Afghanistan'), + ('AG', 'Antigua & Barbuda'), + ('AI', 'Anguilla'), + ('AL', 'Albania'), + ('AM', 'Armenia'), + ('AN', 'Netherlands Antilles'), + ('AO', 'Angola'), + ('AQ', 'Antarctica'), + ('AR', 'Argentina'), + ('AS', 'American Samoa'), + ('AT', 'Austria'), + ('AU', 'Australia'), + ('AW', 'Aruba'), + ('AZ', 'Azerbaijan'), + ('BA', 'Bosnia and Herzegovina'), + ('BB', 'Barbados'), + ('BD', 'Bangladesh'), + ('BE', 'Belgium'), + ('BF', 'Burkina Faso'), + ('BG', 'Bulgaria'), + ('BH', 'Bahrain'), + ('BI', 'Burundi'), + ('BJ', 'Benin'), + ('BM', 'Bermuda'), + ('BN', 'Brunei Darussalam'), + ('BO', 'Bolivia'), + ('BR', 'Brazil'), + ('BS', 'Bahama'), + ('BT', 'Bhutan'), + ('BV', 'Bouvet Island'), + ('BW', 'Botswana'), + ('BY', 'Belarus'), + ('BZ', 'Belize'), + ('CC', 'Cocos (Keeling) Islands'), + ('CF', 'Central African Republic'), + ('CG', 'Congo'), + ('CH', 'Switzerland'), + ('CI', 'Ivory Coast'), + ('CK', 'Cook Iislands'), + ('CL', 'Chile'), + ('CM', 'Cameroon'), + ('CN', 'China'), + ('CO', 'Colombia'), + ('CR', 'Costa Rica'), + ('CU', 'Cuba'), + ('CV', 'Cape Verde'), + ('CX', 'Christmas Island'), + ('CY', 'Cyprus'), + ('CZ', 'Czech Republic'), + ('DE', 'Germany'), + ('DJ', 'Djibouti'), + ('DK', 'Denmark'), + ('DM', 'Dominica'), + ('DO', 'Dominican Republic'), + ('DZ', 'Algeria'), + ('EC', 'Ecuador'), + ('EE', 'Estonia'), + ('EG', 'Egypt'), + ('EH', 'Western Sahara'), + ('ER', 'Eritrea'), + ('ES', 'Spain'), + ('ET', 'Ethiopia'), + ('FI', 'Finland'), + ('FJ', 'Fiji'), + ('FK', 'Falkland Islands (Malvinas)'), + ('FM', 'Micronesia'), + ('FO', 'Faroe Islands'), + ('FR', 'France'), + ('FX', 'France, Metropolitan'), + ('GA', 'Gabon'), + ('GB', 'United Kingdom (Great Britain)'), + ('GD', 'Grenada'), + ('GE', 'Georgia'), + ('GF', 'French Guiana'), + ('GH', 'Ghana'), + ('GI', 'Gibraltar'), + ('GL', 'Greenland'), + ('GM', 'Gambia'), + ('GN', 'Guinea'), + ('GP', 'Guadeloupe'), + ('GQ', 'Equatorial Guinea'), + ('GR', 'Greece'), + ('GS', 'South Georgia and the South Sandwich Islands'), + ('GT', 'Guatemala'), + ('GU', 'Guam'), + ('GW', 'Guinea-Bissau'), + ('GY', 'Guyana'), + ('HK', 'Hong Kong'), + ('HM', 'Heard & McDonald Islands'), + ('HN', 'Honduras'), + ('HR', 'Croatia'), + ('HT', 'Haiti'), + ('HU', 'Hungary'), + ('ID', 'Indonesia'), + ('IE', 'Ireland'), + ('IL', 'Israel'), + ('IN', 'India'), + ('IO', 'British Indian Ocean Territory'), + ('IQ', 'Iraq'), + ('IR', 'Islamic Republic of Iran'), + ('IS', 'Iceland'), + ('IT', 'Italy'), + ('JM', 'Jamaica'), + ('JO', 'Jordan'), + ('JP', 'Japan'), + ('KE', 'Kenya'), + ('KG', 'Kyrgyzstan'), + ('KH', 'Cambodia'), + ('KI', 'Kiribati'), + ('KM', 'Comoros'), + ('KN', 'St. Kitts and Nevis'), + ('KP', 'Korea, Democratic People\'s Republic of'), + ('KR', 'Korea, Republic of'), + ('KW', 'Kuwait'), + ('KY', 'Cayman Islands'), + ('KZ', 'Kazakhstan'), + ('LA', 'Lao People\'s Democratic Republic'), + ('LB', 'Lebanon'), + ('LC', 'Saint Lucia'), + ('LI', 'Liechtenstein'), + ('LK', 'Sri Lanka'), + ('LR', 'Liberia'), + ('LS', 'Lesotho'), + ('LT', 'Lithuania'), + ('LU', 'Luxembourg'), + ('LV', 'Latvia'), + ('LY', 'Libyan Arab Jamahiriya'), + ('MA', 'Morocco'), + ('MC', 'Monaco'), + ('MD', 'Moldova, Republic of'), + ('MG', 'Madagascar'), + ('MH', 'Marshall Islands'), + ('ML', 'Mali'), + ('MN', 'Mongolia'), + ('MM', 'Myanmar'), + ('MO', 'Macau'), + ('MP', 'Northern Mariana Islands'), + ('MQ', 'Martinique'), + ('MR', 'Mauritania'), + ('MS', 'Monserrat'), + ('MT', 'Malta'), + ('MU', 'Mauritius'), + ('MV', 'Maldives'), + ('MW', 'Malawi'), + ('MX', 'Mexico'), + ('MY', 'Malaysia'), + ('MZ', 'Mozambique'), + ('NA', 'Namibia'), + ('NC', 'New Caledonia'), + ('NE', 'Niger'), + ('NF', 'Norfolk Island'), + ('NG', 'Nigeria'), + ('NI', 'Nicaragua'), + ('NL', 'Netherlands'), + ('NO', 'Norway'), + ('NP', 'Nepal'), + ('NR', 'Nauru'), + ('NU', 'Niue'), + ('NZ', 'New Zealand'), + ('OM', 'Oman'), + ('PA', 'Panama'), + ('PE', 'Peru'), + ('PF', 'French Polynesia'), + ('PG', 'Papua New Guinea'), + ('PH', 'Philippines'), + ('PK', 'Pakistan'), + ('PL', 'Poland'), + ('PM', 'St. Pierre & Miquelon'), + ('PN', 'Pitcairn'), + ('PR', 'Puerto Rico'), + ('PT', 'Portugal'), + ('PW', 'Palau'), + ('PY', 'Paraguay'), + ('QA', 'Qatar'), + ('RE', 'Reunion'), + ('RO', 'Romania'), + ('RU', 'Russian Federation'), + ('RW', 'Rwanda'), + ('SA', 'Saudi Arabia'), + ('SB', 'Solomon Islands'), + ('SC', 'Seychelles'), + ('SD', 'Sudan'), + ('SE', 'Sweden'), + ('SG', 'Singapore'), + ('SH', 'St. Helena'), + ('SI', 'Slovenia'), + ('SJ', 'Svalbard & Jan Mayen Islands'), + ('SK', 'Slovakia'), + ('SL', 'Sierra Leone'), + ('SM', 'San Marino'), + ('SN', 'Senegal'), + ('SO', 'Somalia'), + ('SR', 'Suriname'), + ('ST', 'Sao Tome & Principe'), + ('SV', 'El Salvador'), + ('SY', 'Syrian Arab Republic'), + ('SZ', 'Swaziland'), + ('TC', 'Turks & Caicos Islands'), + ('TD', 'Chad'), + ('TF', 'French Southern Territories'), + ('TG', 'Togo'), + ('TH', 'Thailand'), + ('TJ', 'Tajikistan'), + ('TK', 'Tokelau'), + ('TM', 'Turkmenistan'), + ('TN', 'Tunisia'), + ('TO', 'Tonga'), + ('TP', 'East Timor'), + ('TR', 'Turkey'), + ('TT', 'Trinidad & Tobago'), + ('TV', 'Tuvalu'), + ('TW', 'Taiwan, Province of China'), + ('TZ', 'Tanzania, United Republic of'), + ('UA', 'Ukraine'), + ('UG', 'Uganda'), + ('UM', 'United States Minor Outlying Islands'), + ('UY', 'Uruguay'), + ('UZ', 'Uzbekistan'), + ('VA', 'Vatican City State (Holy See)'), + ('VC', 'St. Vincent & the Grenadines'), + ('VE', 'Venezuela'), + ('VG', 'British Virgin Islands'), + ('VI', 'United States Virgin Islands'), + ('VN', 'Viet Nam'), + ('VU', 'Vanuatu'), + ('WF', 'Wallis & Futuna Islands'), + ('WS', 'Samoa'), + ('YE', 'Yemen'), + ('YT', 'Mayotte'), + ('YU', 'Yugoslavia'), + ('ZA', 'South Africa'), + ('ZM', 'Zambia'), + ('ZR', 'Zaire'), + ('ZW', 'Zimbabwe'), + ('ZZ', 'Unknown or unspecified country'), +) + +def is_valid_country_abbrev(abbrev, case_sensitive=False): + """ + Given a country code abbreviation, check to see if it matches the + country table. + + abbrev: (str) Country code to evaluate. + case_sensitive: (bool) When True, enforce case sensitivity. + + Returns True if valid, False if not. + """ + if case_sensitive: + country_code = abbrev + else: + country_code = abbrev.upper() + + for code, full_name in COUNTRY_TUPLES: + if country_code == code: + return True + + return False + +def get_name_from_abbrev(abbrev, case_sensitive=False): + """ + Given a country code abbreviation, get the full name from the table. + + abbrev: (str) Country code to retrieve the full name of. + case_sensitive: (bool) When True, enforce case sensitivity. + """ + if case_sensitive: + country_code = abbrev + else: + country_code = abbrev.upper() + + for code, full_name in COUNTRY_TUPLES: + if country_code == code: + return full_name + + raise KeyError('No country with that country code.') diff --git a/vendor/paypalapi/exceptions.py b/vendor/paypalapi/exceptions.py new file mode 100644 index 000000000..fe05e4e0a --- /dev/null +++ b/vendor/paypalapi/exceptions.py @@ -0,0 +1,45 @@ +# coding=utf-8 +""" +Various PayPal API related exceptions. +""" + +class PayPalError(Exception): + """ + Used to denote some kind of generic error. This does not include errors + returned from PayPal API responses. Those are handled by the more + specific exception classes below. + """ + def __init__(self, message, error_code=None): + Exception.__init__(self, message, error_code) + self.message = message + self.error_code = error_code + + def __str__(self): + if self.error_code: + return "%s (Error Code: %s)" % (repr(self.message), self.error_code) + else: + return repr(self.message) + + +class PayPalConfigError(PayPalError): + """ + Raised when a configuration problem arises. + """ + pass + + +class PayPalAPIResponseError(PayPalError): + """ + Raised when there is an error coming back with a PayPal NVP API response. + + Pipe the error message from the API to the exception, along with + the error code. + """ + def __init__(self, response): + self.response = response + self.error_code = int(getattr(response, 'L_ERRORCODE0', -1)) + self.message = getattr(response, 'L_LONGMESSAGE0', None) + self.short_message = getattr(response, 'L_SHORTMESSAGE0', None) + self.correlation_id = getattr(response, 'CORRELATIONID', None) + + super(PayPalAPIResponseError, self).__init__(self.message, self.error_code) diff --git a/vendor/paypalapi/interface.py b/vendor/paypalapi/interface.py new file mode 100644 index 000000000..8075dc991 --- /dev/null +++ b/vendor/paypalapi/interface.py @@ -0,0 +1,469 @@ +# coding=utf-8 +""" +The end developer will do most of their work with the PayPalInterface class found +in this module. Configuration, querying, and manipulation can all be done +with it. +""" + +import types +import logging +from pprint import pformat + +import requests + +from paypalapi.settings import PayPalConfig +from paypalapi.response import PayPalResponse +from paypalapi.exceptions import PayPalError, PayPalAPIResponseError +from paypalapi.compat import is_py3 + +if is_py3: + #noinspection PyUnresolvedReferences + import urllib.parse +else: + import urllib + +logger = logging.getLogger('paypal.interface') + +class PayPalInterface(object): + """ + The end developers will do 95% of their work through this class. API + queries, configuration, etc, all go through here. See the __init__ method + for config related details. + """ + def __init__(self, config=None, **kwargs): + """ + Constructor, which passes all config directives to the config class + via kwargs. For example: + + paypal = PayPalInterface(API_USERNAME='somevalue') + + Optionally, you may pass a 'config' kwarg to provide your own + PayPalConfig object. + """ + if config: + # User provided their own PayPalConfig object. + self.config = config + else: + # Take the kwargs and stuff them in a new PayPalConfig object. + self.config = PayPalConfig(**kwargs) + + def _encode_utf8(self, **kwargs): + """ + UTF8 encodes all of the NVP values. + """ + if is_py3: + # This is only valid for Python 2. In Python 3, unicode is + # everywhere (yay). + return kwargs + + unencoded_pairs = kwargs + for i in unencoded_pairs.keys(): + #noinspection PyUnresolvedReferences + if isinstance(unencoded_pairs[i], types.UnicodeType): + unencoded_pairs[i] = unencoded_pairs[i].encode('utf-8') + return unencoded_pairs + + def _check_required(self, requires, **kwargs): + """ + Checks kwargs for the values specified in 'requires', which is a tuple + of strings. These strings are the NVP names of the required values. + """ + for req in requires: + # PayPal api is never mixed-case. + if req.lower() not in kwargs and req.upper() not in kwargs: + raise PayPalError('missing required : %s' % req) + + def _sanitize_locals(self, data): + """ + Remove the 'self' key in locals() + It's more explicit to do it in one function + """ + if 'self' in data: + data = data.copy() + del data['self'] + + return data + + def _call(self, method, **kwargs): + """ + Wrapper method for executing all API commands over HTTP. This method is + further used to implement wrapper methods listed here: + + https://www.x.com/docs/DOC-1374 + + ``method`` must be a supported NVP method listed at the above address. + + ``kwargs`` will be a hash of + """ + # This dict holds the key/value pairs to pass to the PayPal API. + url_values = { + 'METHOD': method, + 'VERSION': self.config.API_VERSION, + } + + if self.config.API_AUTHENTICATION_MODE == "3TOKEN": + url_values['USER'] = self.config.API_USERNAME + url_values['PWD'] = self.config.API_PASSWORD + url_values['SIGNATURE'] = self.config.API_SIGNATURE + elif self.config.API_AUTHENTICATION_MODE == "UNIPAY": + url_values['SUBJECT'] = self.config.UNIPAY_SUBJECT + + # All values passed to PayPal API must be uppercase. + for key, value in kwargs.items(): + url_values[key.upper()] = value + + # This shows all of the key/val pairs we're sending to PayPal. + if logger.isEnabledFor(logging.DEBUG): + logger.debug('PayPal NVP Query Key/Vals:\n%s' % pformat(url_values)) + + req = requests.get( + self.config.API_ENDPOINT, + params=url_values, + timeout=self.config.HTTP_TIMEOUT, + verify=self.config.API_CA_CERTS, + ) + + # Call paypal API + response = PayPalResponse(req.text, self.config) + + logger.debug('PayPal NVP API Endpoint: %s' % self.config.API_ENDPOINT) + + if not response.success: + logger.error('A PayPal API error was encountered.') + logger.error('PayPal NVP Query Key/Vals:\n%s' % pformat(url_values)) + logger.error('PayPal NVP Query Response') + logger.error(response) + raise PayPalAPIResponseError(response) + + return response + + def address_verify(self, email, street, zip): + """Shortcut for the AddressVerify method. + + ``email``:: + Email address of a PayPal member to verify. + Maximum string length: 255 single-byte characters + Input mask: ?@?.?? + ``street``:: + First line of the billing or shipping postal address to verify. + + To pass verification, the value of Street must match the first three + single-byte characters of a postal address on file for the PayPal member. + + Maximum string length: 35 single-byte characters. + Alphanumeric plus - , . ‘ # \ + Whitespace and case of input value are ignored. + ``zip``:: + Postal code to verify. + + To pass verification, the value of Zip mustmatch the first five + single-byte characters of the postal code of the verified postal + address for the verified PayPal member. + + Maximumstring length: 16 single-byte characters. + Whitespace and case of input value are ignored. + """ + args = self._sanitize_locals(locals()) + return self._call('AddressVerify', **args) + + def create_recurring_payments_profile(self, **kwargs): + """Shortcut for the CreateRecurringPaymentsProfile method. + Currently, this method only supports the Direct Payment flavor. + + It requires standard credit card information and a few additional + parameters related to the billing. e.g.: + + profile_info = { + # Credit card information + 'creditcardtype': 'Visa', + 'acct': '4812177017895760', + 'expdate': '102015', + 'cvv2': '123', + 'firstname': 'John', + 'lastname': 'Doe', + 'street': '1313 Mockingbird Lane', + 'city': 'Beverly Hills', + 'state': 'CA', + 'zip': '90110', + 'countrycode': 'US', + 'currencycode': 'USD', + # Recurring payment information + 'profilestartdate': '2010-10-25T0:0:0', + 'billingperiod': 'Month', + 'billingfrequency': '6', + 'amt': '10.00', + 'desc': '6 months of our product.' + } + response = create_recurring_payments_profile(**profile_info) + + The above NVPs compose the bare-minimum request for creating a + profile. For the complete list of parameters, visit this URI: + https://www.x.com/docs/DOC-1168 + """ + return self._call('CreateRecurringPaymentsProfile', **kwargs) + + def do_authorization(self, transactionid, amt): + """Shortcut for the DoAuthorization method. + + Use the TRANSACTIONID from DoExpressCheckoutPayment for the + ``transactionid``. The latest version of the API does not support the + creation of an Order from `DoDirectPayment`. + + The `amt` should be the same as passed to `DoExpressCheckoutPayment`. + + Flow for a payment involving a `DoAuthorization` call:: + + 1. One or many calls to `SetExpressCheckout` with pertinent order + details, returns `TOKEN` + 1. `DoExpressCheckoutPayment` with `TOKEN`, `PAYMENTACTION` set to + Order, `AMT` set to the amount of the transaction, returns + `TRANSACTIONID` + 1. `DoAuthorization` with `TRANSACTIONID` and `AMT` set to the + amount of the transaction. + 1. `DoCapture` with the `AUTHORIZATIONID` (the `TRANSACTIONID` + returned by `DoAuthorization`) + + """ + args = self._sanitize_locals(locals()) + return self._call('DoAuthorization', **args) + + def do_capture(self, authorizationid, amt, completetype='Complete', **kwargs): + """Shortcut for the DoCapture method. + + Use the TRANSACTIONID from DoAuthorization, DoDirectPayment or + DoExpressCheckoutPayment for the ``authorizationid``. + + The `amt` should be the same as the authorized transaction. + """ + kwargs.update(self._sanitize_locals(locals())) + return self._call('DoCapture', **kwargs) + + def do_direct_payment(self, paymentaction="Sale", **kwargs): + """Shortcut for the DoDirectPayment method. + + ``paymentaction`` could be 'Authorization' or 'Sale' + + To issue a Sale immediately:: + + charge = { + 'amt': '10.00', + 'creditcardtype': 'Visa', + 'acct': '4812177017895760', + 'expdate': '012010', + 'cvv2': '962', + 'firstname': 'John', + 'lastname': 'Doe', + 'street': '1 Main St', + 'city': 'San Jose', + 'state': 'CA', + 'zip': '95131', + 'countrycode': 'US', + 'currencycode': 'USD', + } + direct_payment("Sale", **charge) + + Or, since "Sale" is the default: + + direct_payment(**charge) + + To issue an Authorization, simply pass "Authorization" instead of "Sale". + + You may also explicitly set ``paymentaction`` as a keyword argument: + + ... + direct_payment(paymentaction="Sale", **charge) + """ + kwargs.update(self._sanitize_locals(locals())) + return self._call('DoDirectPayment', **kwargs) + + def do_void(self, **kwargs): + """Shortcut for the DoVoid method. + + Use the TRANSACTIONID from DoAuthorization, DoDirectPayment or + DoExpressCheckoutPayment for the ``AUTHORIZATIONID``. + + Required Kwargs + --------------- + * AUTHORIZATIONID + """ + return self._call('DoVoid', **kwargs) + + def get_express_checkout_details(self, **kwargs): + """Shortcut for the GetExpressCheckoutDetails method. + + Required Kwargs + --------------- + * TOKEN + """ + return self._call('GetExpressCheckoutDetails', **kwargs) + + def get_transaction_details(self, **kwargs): + """Shortcut for the GetTransactionDetails method. + + Use the TRANSACTIONID from DoAuthorization, DoDirectPayment or + DoExpressCheckoutPayment for the ``transactionid``. + + Required Kwargs + --------------- + + * TRANSACTIONID + """ + return self._call('GetTransactionDetails', **kwargs) + + def set_express_checkout(self, **kwargs): + """Start an Express checkout. + + You'll want to use this in conjunction with + :meth:`generate_express_checkout_redirect_url` to create a payment, + then figure out where to redirect the user to for them to + authorize the payment on PayPal's website. + + Required Kwargs + --------------- + + * PAYMENTREQUEST_0_AMT + * PAYMENTREQUEST_0_PAYMENTACTION + * RETURNURL + * CANCELURL + """ + return self._call('SetExpressCheckout', **kwargs) + + def refund_transaction(self, transactionid=None, payerid=None, **kwargs): + """Shortcut for RefundTransaction method. + Note new API supports passing a PayerID instead of a transaction id, exactly one must be provided. + Optional: + INVOICEID + REFUNDTYPE + AMT + CURRENCYCODE + NOTE + RETRYUNTIL + REFUNDSOURCE + MERCHANTSTOREDETAILS + REFUNDADVICE + REFUNDITEMDETAILS + MSGSUBID + + MERCHANSTOREDETAILS has two fields: + STOREID + TERMINALID + """ + #this line seems like a complete waste of time... kwargs should not be populated + if (transactionid is None) and (payerid is None): + raise PayPalError('RefundTransaction requires either a transactionid or a payerid') + if (transactionid is not None) and (payerid is not None): + raise PayPalError('RefundTransaction requires only one of transactionid %s and payerid %s' % (transactionid, payerid)) + if transactionid is not None: + kwargs['TRANSACTIONID'] = transactionid + else: + kwargs['PAYERID'] = payerid + + return self._call('RefundTransaction', **kwargs) + + def do_express_checkout_payment(self, **kwargs): + """Finishes an Express checkout. + + TOKEN is the token that was returned earlier by + :meth:`set_express_checkout`. This identifies the transaction. + + Required + -------- + * TOKEN + * PAYMENTACTION + * PAYERID + * AMT + + """ + return self._call('DoExpressCheckoutPayment', **kwargs) + + def generate_express_checkout_redirect_url(self, token): + """Returns the URL to redirect the user to for the Express checkout. + + Express Checkouts must be verified by the customer by redirecting them + to the PayPal website. Use the token returned in the response from + :meth:`set_express_checkout` with this function to figure out where + to redirect the user to. + + :param str token: The unique token identifying this transaction. + :rtype: str + :returns: The URL to redirect the user to for approval. + """ + url_vars = (self.config.PAYPAL_URL_BASE, token) + return "%s?cmd=_express-checkout&token=%s" % url_vars + + def generate_cart_upload_redirect_url(self, **kwargs): + """https://www.sandbox.paypal.com/webscr + ?cmd=_cart + &upload=1 + """ + required_vals = ('business', 'item_name_1', 'amount_1', 'quantity_1') + self._check_required(required_vals, **kwargs) + url = "%s?cmd=_cart&upload=1" % self.config.PAYPAL_URL_BASE + additional = self._encode_utf8(**kwargs) + if is_py3: + #noinspection PyUnresolvedReferences + additional = urllib.parse.urlencode(additional) + else: + #noinspection PyUnresolvedReferences + additional = urllib.urlencode(additional) + return url + "&" + additional + + def get_recurring_payments_profile_details(self, profileid): + """Shortcut for the GetRecurringPaymentsProfile method. + + This returns details for a recurring payment plan. The ``profileid`` is + a value included in the response retrieved by the function + ``create_recurring_payments_profile``. The profile details include the + data provided when the profile was created as well as default values + for ignored fields and some pertinent stastics. + + e.g.: + response = create_recurring_payments_profile(**profile_info) + profileid = response.PROFILEID + details = get_recurring_payments_profile(profileid) + + The response from PayPal is somewhat self-explanatory, but for a + description of each field, visit the following URI: + https://www.x.com/docs/DOC-1194 + """ + args = self._sanitize_locals(locals()) + return self._call('GetRecurringPaymentsProfileDetails', **args) + + def manage_recurring_payments_profile_status(self, profileid, action, note = None): + """Shortcut to the ManageRecurringPaymentsProfileStatus method. + + ``profileid`` is the same profile id used for getting profile details. + ``action`` should be either 'Cancel', 'Suspend', or 'Reactivate'. + ``note`` is optional and is visible to the user. It contains the reason for the change in status. + """ + args = self._sanitize_locals(locals()) + if not note: + del args['note'] + return self._call('ManageRecurringPaymentsProfileStatus', **args) + + def update_recurring_payments_profile(self, profileid, **kwargs): + """Shortcut to the UpdateRecurringPaymentsProfile method. + + ``profileid`` is the same profile id used for getting profile details. + + The keyed arguments are data in the payment profile which you wish to + change. The profileid does not change. Anything else will take the new + value. Most of, though not all of, the fields available are shared + with creating a profile, but for the complete list of parameters, you + can visit the following URI: + https://www.x.com/docs/DOC-1212 + """ + kwargs.update(self._sanitize_locals(locals())) + return self._call('UpdateRecurringPaymentsProfile', **kwargs) + + def bm_create_button(self, **kwargs): + """Shortcut to the BMButtonSearch method. + + See the docs for details on arguments: + https://cms.paypal.com/mx/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_nvp_BMCreateButton + + The L_BUTTONVARn fields are especially important, so make sure to + read those and act accordingly. See unit tests for some examples. + """ + kwargs.update(self._sanitize_locals(locals())) + return self._call('BMCreateButton', **kwargs) diff --git a/vendor/paypalapi/response.py b/vendor/paypalapi/response.py new file mode 100644 index 000000000..7be408ef8 --- /dev/null +++ b/vendor/paypalapi/response.py @@ -0,0 +1,115 @@ +# coding=utf-8 +""" +PayPalResponse parsing and processing. +""" +import logging +from pprint import pformat + +from paypalapi.compat import is_py3, is_py25 + +if is_py3: + #noinspection PyUnresolvedReferences + import urllib.parse + #noinspection PyUnresolvedReferences + parse_qs = urllib.parse.parse_qs +elif is_py25: + import cgi + #noinspection PyUnresolvedReferences, PyDeprecation + parse_qs = cgi.parse_qs +else: + # Python 2.6 and up (but not 3.0) have urlparse.parse_qs, which is copied + # from Python 2.5's cgi.parse_qs. + import urlparse + #noinspection PyUnresolvedReferences, PyDeprecation + parse_qs = urlparse.parse_qs + +logger = logging.getLogger('paypal.response') + +class PayPalResponse(object): + """ + Parse and prepare the reponse from PayPal's API. Acts as somewhat of a + glorified dictionary for API responses. + + NOTE: Don't access self.raw directly. Just do something like + PayPalResponse.someattr, going through PayPalResponse.__getattr__(). + """ + def __init__(self, query_string, config): + """ + query_string is the response from the API, in NVP format. This is + parseable by urlparse.parse_qs(), which sticks it into the + :attr:`raw` dict for retrieval by the user. + + :param str query_string: The raw response from the API server. + :param PayPalConfig config: The config object that was used to send + the query that caused this response. + """ + # A dict of NVP values. Don't access this directly, use + # PayPalResponse.attribname instead. See self.__getattr__(). + self.raw = parse_qs(query_string) + self.config = config + logger.debug("PayPal NVP API Response:\n%s" % self.__str__()) + + def __str__(self): + """ + Returns a string representation of the PayPalResponse object, in + 'pretty-print' format. + + :rtype: str + :returns: A 'pretty' string representation of the response dict. + """ + return pformat(self.raw) + + def __getattr__(self, key): + """ + Handles the retrieval of attributes that don't exist on the object + already. This is used to get API response values. Handles some + convenience stuff like discarding case and checking the cgi/urlparsed + response value dict (self.raw). + + :param str key: The response attribute to get a value for. + :rtype: str + :returns: The requested value from the API server's response. + """ + # PayPal response names are always uppercase. + key = key.upper() + try: + value = self.raw[key] + if len(value) == 1: + # For some reason, PayPal returns lists for all of the values. + # I'm not positive as to why, so we'll just take the first + # of each one. Hasn't failed us so far. + return value[0] + return value + except KeyError: + # The requested value wasn't returned in the response. + raise AttributeError(self) + + def __getitem__(self, key): + """ + Another (dict-style) means of accessing response data. + + :param str key: The response key to get a value for. + :rtype: str + :returns: The requested value from the API server's response. + """ + # PayPal response names are always uppercase. + key = key.upper() + value = self.raw[key] + if len(value) == 1: + # For some reason, PayPal returns lists for all of the values. + # I'm not positive as to why, so we'll just take the first + # of each one. Hasn't failed us so far. + return value[0] + return value + + def success(self): + """ + Checks for the presence of errors in the response. Returns ``True`` if + all is well, ``False`` otherwise. + + :rtype: bool + :returns ``True`` if PayPal says our query was successful. + """ + return self.ack.upper() in (self.config.ACK_SUCCESS, + self.config.ACK_SUCCESS_WITH_WARNING) + success = property(success) diff --git a/vendor/paypalapi/settings.py b/vendor/paypalapi/settings.py new file mode 100644 index 000000000..505fd1e6a --- /dev/null +++ b/vendor/paypalapi/settings.py @@ -0,0 +1,130 @@ +# coding=utf-8 +""" +This module contains config objects needed by paypal.interface.PayPalInterface. +Most of this is transparent to the end developer, as the PayPalConfig object +is instantiated by the PayPalInterface object. +""" +import logging +import os +from pprint import pformat + +from paypalapi.compat import basestring +from paypalapi.exceptions import PayPalConfigError + +logger = logging.getLogger('paypal.settings') + +class PayPalConfig(object): + """ + The PayPalConfig object is used to allow the developer to perform API + queries with any number of different accounts or configurations. This + is done by instantiating paypal.interface.PayPalInterface, passing config + directives as keyword args. + """ + # Used to validate correct values for certain config directives. + _valid_ = { + 'API_ENVIRONMENT': ['SANDBOX', 'PRODUCTION'], + 'API_AUTHENTICATION_MODE': ['3TOKEN', 'CERTIFICATE'], + } + + # Various API servers. + _API_ENDPOINTS = { + # In most cases, you want 3-Token. There's also Certificate-based + # authentication, which uses different servers, but that's not + # implemented. + '3TOKEN': { + 'SANDBOX': 'https://api-3t.sandbox.paypal.com/nvp', + 'PRODUCTION': 'https://api-3t.paypal.com/nvp', + } + } + + _PAYPAL_URL_BASE = { + 'SANDBOX': 'https://www.sandbox.paypal.com/webscr', + 'PRODUCTION': 'https://www.paypal.com/webscr', + } + + API_VERSION = '98.0' + + # Defaults. Used in the absence of user-specified values. + API_ENVIRONMENT = 'SANDBOX' + API_AUTHENTICATION_MODE = '3TOKEN' + + # 3TOKEN credentials + API_USERNAME = None + API_PASSWORD = None + API_SIGNATURE = None + + # API Endpoints are just API server addresses. + API_ENDPOINT = None + PAYPAL_URL_BASE = None + + # API Endpoint CA certificate chain. If this is True, do a simple SSL + # certificate check on the endpoint. If it's a full path, verify against + # a private cert. + # e.g. '/etc/ssl/certs/Verisign_Class_3_Public_Primary_Certification_Authority.pem' + API_CA_CERTS = True + + # UNIPAY credentials + UNIPAY_SUBJECT = None + + ACK_SUCCESS = "SUCCESS" + ACK_SUCCESS_WITH_WARNING = "SUCCESSWITHWARNING" + + # In seconds. Depending on your setup, this may need to be higher. + HTTP_TIMEOUT = 15.0 + + def __init__(self, **kwargs): + """ + PayPalConfig constructor. **kwargs catches all of the user-specified + config directives at time of instantiation. It is fine to set these + values post-instantiation, too. + + Some basic validation for a few values is performed below, and defaults + are applied for certain directives in the absence of + user-provided values. + """ + if kwargs.get('API_ENVIRONMENT'): + api_environment = kwargs['API_ENVIRONMENT'].upper() + # Make sure the environment is one of the acceptable values. + if api_environment not in self._valid_['API_ENVIRONMENT']: + raise PayPalConfigError('Invalid API_ENVIRONMENT') + else: + self.API_ENVIRONMENT = api_environment + + if kwargs.get('API_AUTHENTICATION_MODE'): + auth_mode = kwargs['API_AUTHENTICATION_MODE'].upper() + # Make sure the auth mode is one of the known/implemented methods. + if auth_mode not in self._valid_['API_AUTHENTICATION_MODE']: + choices = ", ".join(self._valid_['API_AUTHENTICATION_MODE']) + raise PayPalConfigError( + "Not a supported auth mode. Use one of: %s" % choices + ) + else: + self.API_AUTHENTICATION_MODE = auth_mode + + # Set the API endpoints, which is a cheesy way of saying API servers. + self.API_ENDPOINT = self._API_ENDPOINTS[self.API_AUTHENTICATION_MODE][self.API_ENVIRONMENT] + self.PAYPAL_URL_BASE = self._PAYPAL_URL_BASE[self.API_ENVIRONMENT] + + # Set the CA_CERTS location. This can either be a None, a bool, or a + # string path. + if kwargs.get('API_CA_CERTS'): + self.API_CA_CERTS = kwargs['API_CA_CERTS'] + + if isinstance(self.API_CA_CERTS, basestring) and not os.path.exists(self.API_CA_CERTS): + # A CA Cert path was specified, but it's invalid. + raise PayPalConfigError('Invalid API_CA_CERTS') + + # set the 3TOKEN required fields + if self.API_AUTHENTICATION_MODE == '3TOKEN': + for arg in ('API_USERNAME', 'API_PASSWORD', 'API_SIGNATURE'): + if arg not in kwargs: + raise PayPalConfigError('Missing in PayPalConfig: %s ' % arg) + setattr(self, arg, kwargs[arg]) + + for arg in ['HTTP_TIMEOUT']: + if arg in kwargs: + setattr(self, arg, kwargs[arg]) + + logger.debug( + 'PayPalConfig object instantiated with kwargs: %s' % pformat(kwargs) + )