paypal-1.2.5/0000755000175000017500000000000012644607167014762 5ustar greg.taylorgtaylor00000000000000paypal-1.2.5/paypal/0000755000175000017500000000000012644607167016250 5ustar greg.taylorgtaylor00000000000000paypal-1.2.5/paypal/response_list.py0000644000175000017500000000440712631717521021510 0ustar greg.taylorgtaylor00000000000000# coding=utf-8 """ PayPal response parsing of list syntax. """ import logging import re from paypal.response import PayPalResponse from paypal.exceptions import PayPalAPIResponseError logger = logging.getLogger('paypal.response') class PayPalResponseList(PayPalResponse): """ Subclass of PayPalResponse, parses L_style list items and stores them in a dictionary keyed by numeric index. NOTE: Don't access self.raw directly. Just do something like PayPalResponse.someattr, going through PayPalResponse.__getattr__(). """ skippable_error_codes = [ 'ERRORCODE', 'SHORTMESSAGE', 'LONGMESSAGE', 'SEVERITYCODE'] def __init__(self, raw, config): self.raw = raw self.config = config l_regex = re.compile("L_([a-zA-Z]+)([0-9]{0,2})") # name-value pair list syntax documented at # https://developer.paypal.com/docs/classic/api/NVPAPIOverview/#id084E30EC030 # api returns max 100 items, so only two digits required self.list_items_dict = {} for key in self.raw.keys(): match = l_regex.match(key) if match: index = match.group(2) d_key = match.group(1) if isinstance(self.raw[key], list) and len(self.raw[key]) == 1: d_val = self.raw[key][0] else: d_val = self.raw[key] # Skip error codes if d_key in self.skippable_error_codes: continue if index in self.list_items_dict: # Dict for index exists, update self.list_items_dict[index][d_key] = d_val else: # Create new dict self.list_items_dict[index] = {d_key: d_val} # Log ResponseErrors from warning keys if self.raw['ACK'][0].upper() == self.config.ACK_SUCCESS_WITH_WARNING: self.errors = [PayPalAPIResponseError(self)] logger.error(self.errors) def items(self): # Convert dict like {'1':{},'2':{}, ...} to list return list(self.list_items_dict.values()) def iteritems(self): for key in self.list_items_dict.keys(): yield (key, self.list_items_dict[key]) paypal-1.2.5/paypal/settings.py0000644000175000017500000001251612621031177020452 0ustar greg.taylorgtaylor00000000000000# 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 paypal.compat import basestring from paypal.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. '3TOKEN': { 'SANDBOX': 'https://api-3t.sandbox.paypal.com/nvp', 'PRODUCTION': 'https://api-3t.paypal.com/nvp', }, 'CERTIFICATE': { 'SANDBOX': 'https://api.sandbox.paypal.com/nvp', 'PRODUCTION': 'https://api.paypal.com/nvp', }, } _PAYPAL_URL_BASE = { 'SANDBOX': 'https://www.sandbox.paypal.com/cgi-bin/webscr', 'PRODUCTION': 'https://www.paypal.com/cgi-bin/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 # CERTIFICATE credentials API_CERTIFICATE_FILENAME = None API_KEY_FILENAME = 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') # check authentication fields if self.API_AUTHENTICATION_MODE in ('3TOKEN', 'CERTIFICATE'): auth_args = ['API_USERNAME', 'API_PASSWORD'] if self.API_AUTHENTICATION_MODE == '3TOKEN': auth_args.append('API_SIGNATURE') elif self.API_AUTHENTICATION_MODE == 'CERTIFICATE': auth_args.extend(['API_CERTIFICATE_FILENAME', 'API_KEY_FILENAME']) for arg in auth_args: 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) ) paypal-1.2.5/paypal/compat.py0000644000175000017500000000672612621031177020103 0ustar greg.taylorgtaylor00000000000000# -*- 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) paypal-1.2.5/paypal/countries.py0000644000175000017500000001714512621031177020630 0ustar greg.taylorgtaylor00000000000000""" 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.') paypal-1.2.5/paypal/interface.py0000644000175000017500000004651312644606650020567 0ustar greg.taylorgtaylor00000000000000# 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 warnings import requests from paypal.settings import PayPalConfig from paypal.response import PayPalResponse from paypal.response_list import PayPalResponseList from paypal.exceptions import (PayPalError, PayPalAPIResponseError, PayPalConfigError) from paypal.compat import is_py3 if is_py3: #noinspection PyUnresolvedReferences from urllib.parse import urlencode else: from urllib import urlencode logger = logging.getLogger('paypal.interface') class PayPalInterface(object): __credentials = ['USER', 'PWD', 'SIGNATURE', 'SUBJECT'] """ 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`` the actual call parameters """ post_params = self._get_call_params(method, **kwargs) payload = post_params['data'] api_endpoint = post_params['url'] # 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(payload)) http_response = requests.post(**post_params) response = PayPalResponse(http_response.text, self.config) logger.debug('PayPal NVP API Endpoint: %s' % api_endpoint) if not response.success: raise PayPalAPIResponseError(response) return response def _get_call_params(self, method, **kwargs): """ Returns the prepared call parameters. Mind, these will be keyword arguments to ``requests.post``. ``method`` the NVP method ``kwargs`` the actual call parameters """ payload = {'METHOD': method, 'VERSION': self.config.API_VERSION} certificate = None if self.config.API_AUTHENTICATION_MODE == "3TOKEN": payload['USER'] = self.config.API_USERNAME payload['PWD'] = self.config.API_PASSWORD payload['SIGNATURE'] = self.config.API_SIGNATURE elif self.config.API_AUTHENTICATION_MODE == "CERTIFICATE": payload['USER'] = self.config.API_USERNAME payload['PWD'] = self.config.API_PASSWORD certificate = (self.config.API_CERTIFICATE_FILENAME, self.config.API_KEY_FILENAME) elif self.config.API_AUTHENTICATION_MODE == "UNIPAY": payload['SUBJECT'] = self.config.UNIPAY_SUBJECT none_configs = [config for config, value in payload.items() if value is None] if none_configs: raise PayPalConfigError( "Config(s) %s cannot be None. Please, check this " "interface's config." % none_configs) # all keys in the payload must be uppercase for key, value in kwargs.items(): payload[key.upper()] = value return {'data': payload, 'cert': certificate, 'url': self.config.API_ENDPOINT, 'timeout': self.config.HTTP_TIMEOUT, 'verify': self.config.API_CA_CERTS} 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 transaction_search(self, **kwargs): """Shortcut for the TransactionSearch method. Returns a PayPalResponseList object, which merges the L_ syntax list to a list of dictionaries with properly named keys. Note that the API will limit returned transactions to 100. Required Kwargs --------------- * STARTDATE Optional Kwargs --------------- STATUS = one of ['Pending','Processing','Success','Denied','Reversed'] """ plain = self._call('TransactionSearch', **kwargs) return PayPalResponseList(plain.raw, self.config) 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, useraction=None): """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. The button text on the PayPal page can be controlled via `useraction`. The documented possible values are `commit` and `continue`. However, any other value will only result in a warning. :param str token: The unique token identifying this transaction. :param str useraction: Control the button text on the PayPal page. :rtype: str :returns: The URL to redirect the user to for approval. """ url_vars = (self.config.PAYPAL_URL_BASE, token) url = "%s?cmd=_express-checkout&token=%s" % url_vars if useraction: if not useraction.lower() in ('commit', 'continue'): warnings.warn('useraction=%s is not documented' % useraction, RuntimeWarning) url += '&useraction=%s' % useraction return url 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) additional = 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) paypal-1.2.5/paypal/exceptions.py0000644000175000017500000000263312621031560020766 0ustar greg.taylorgtaylor00000000000000# 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) paypal-1.2.5/paypal/response.py0000644000175000017500000001010312621031177020436 0ustar greg.taylorgtaylor00000000000000# coding=utf-8 """ PayPalResponse parsing and processing. """ import logging from pprint import pformat from paypal.compat import is_py3 if is_py3: #noinspection PyUnresolvedReferences from urllib.parse import 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. from urlparse import 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 items(self): items_list = [] for key in self.raw.keys(): items_list.append((key, self.__getitem__(key))) return items_list def iteritems(self): for key in self.raw.keys(): yield (key, self.__getitem__(key)) 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) paypal-1.2.5/paypal/__init__.py0000644000175000017500000000057112644606764020366 0ustar greg.taylorgtaylor00000000000000# coding=utf-8 #noinspection PyUnresolvedReferences from paypal.interface import PayPalInterface #noinspection PyUnresolvedReferences from paypal.settings import PayPalConfig #noinspection PyUnresolvedReferences from paypal.exceptions import PayPalError, PayPalConfigError, PayPalAPIResponseError #noinspection PyUnresolvedReferences import paypal.countries VERSION = '1.2.5' paypal-1.2.5/MANIFEST.in0000644000175000017500000000022212621031177016477 0ustar greg.taylorgtaylor00000000000000include README.rst include LICENSE include AUTHORS recursive-include paypal *.py recursive-include tests *.py README exclude tests/api_details.py paypal-1.2.5/README.rst0000644000175000017500000001162512644607022016444 0ustar greg.taylorgtaylor00000000000000PayPal-Python ============= :Author: Pat Collins :Maintainer: Greg Taylor :License: Apache v2 :Status: Stable :Maintained: Quasi-maintained, looking for new maintainer (file issue if interested) This package implements Paypal's Website Payments Pro NVP API in Python. This currently includes Direct Payments (Credit card payments without a PayPal Account) and PayPal Express Checkout (Payment via a PayPal account). This module is best used in conjunction with the official PayPal `documentation`_. The stuff under "Website Payments Pro and Express Checkout API Operations". in particular. paypal-python does no real validation, doesn't hold hands, and is generally only going to handle authentication, http-level stuff, and serializing a list of keyword arguments passed to the API methods. You'll need to read the official PayPal documentation for what key/values to pass. .. _documentation: https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/howto_api_reference *NOTE: This module is not created by, endorsed by, or in any way affiliated with PayPal.* Requirements ------------ * Python 2.6 or 2.7 Installation ------------ Through pip:: pip install paypal or easy_install:: easy_install paypal or download the source, un-tar/un-zip it, cd into paypal-python, and:: python setup.py install Quick Start ----------- To run test suite, do from within the paypal-python directory:: pip install nose nosetests tests/ The meat is in paypal.interface. The docs are in the docstrings and tests. * Create a paypal.interface.PayPalInterface object * Pass it configuration kwargs (See tests.interface_factory.get_interface_obj for a good example of how this works). * That interface is how you access PayPal. Take a look at the currently implemented methods in paypal.interface. Support/Help ------------ If you have any problems, questions, or ideas, feel free to post on our `issue tracker`_. .. _issue tracker: http://github.com/gtaylor/paypal-python/issues Addendum A ---------- Instructions for setting up a Sandbox Website Payments Pro account. More detailed instructions can be found at [x.com](http://x.com) but this is what worked for me. 1. Create Sandbox account. Don't use your live PayPal account email address. 2. Login to Sandbox 3. Test Accounts -> "Preconfigured" -- the manual process sucks. 4. Make a "Seller" account 5. Don't change "login email" at all -- it seems to truncate to 6 characters. 6. I took the numeric password they gave as default and copy/pasted it into a plain text document so I could use it later to make all my test account passwords the same. 7. I chose Visa as the credit card. 8. Bank Account = "Yes" -- This is needed for a Verified account, which is needed for Website Payments Pro. 9. Put $1,000 of fake $$ into the account. At one point I tried $5,000 but the test account I created wasn't Verified automatically? Not sure if the two are related. 10. No notes. 11. "Create Account" 12. When it takes you back to the "Test Accounts" screen, it should say "Business" and "Verified" 13. When you click on "API Credentials" you should see API credentials for the corresponding test account you just created. I copy/pasted them into the same text file used above. The next step was the tricky part, at least for me. I was getting `10501` errors which means the billing agreement wasn't agreed to. Apparently you need to accept the fake billing agreement that comes along with the fake account you just created, which semi-conveniently has come packaged with an automatically created and verified fake bank account and business account "verified" status. Why couldn't the billing agreement be automatically "agreed to" as well? Back on the "Test Accounts" page, choose the account you just created and click "Enter Sandbox Test Site." It should populate the fake email address, which should be `userna_XXXXXXXXXX_biz@domain.com`. Use the copy/pasted password from step #6 and paste it into the password field and login. Now go under Merchant Services -> Website Payments Pro. In the right column there should be a link to "agree" to the billing agreement. Click this link and agree to the agreement. Now your API calls will work as expected. LICENSE ------- :: Copyright 2009 Pat Collins Copyright 2012 DUO Interactive, LLC Copyright 2014 Greg Taylor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. paypal-1.2.5/tests/0000755000175000017500000000000012644607167016124 5ustar greg.taylorgtaylor00000000000000paypal-1.2.5/tests/test_express_checkout.py0000644000175000017500000002324712621031177023106 0ustar greg.taylorgtaylor00000000000000# coding=utf-8 import unittest import warnings from mock import patch, Mock from . import interface_factory from . import api_details from paypal.exceptions import PayPalAPIResponseError, PayPalConfigError from paypal.interface import PayPalInterface from paypal.response import PayPalResponse interface = interface_factory.get_interface_obj() class TestExpressCheckout(unittest.TestCase): def setUp(self): self.returnurl = 'http://www.paypal.com' self.cancelurl = 'http://www.ebay.com' def test_sale(self): """ Tests the first part of a sale. At this point, this is a partial unit test. The user has to login to PayPal and approve the transaction, which is not something we have tackled in the unit test yet. So we'll just test the set/get_express_checkout methods. A call to `SetExpressCheckoutDetails`. A call to `DoExpressCheckoutPayment`. A call to `GetExpressCheckoutDetails`. """ setexp_response = interface.set_express_checkout( amt='10.00', returnurl=self.returnurl, cancelurl=self.cancelurl, paymentaction='Order', email=api_details.EMAIL_PERSONAL ) self.assertTrue(setexp_response) token = setexp_response.token getexp_response = interface.get_express_checkout_details(token=token) # Redirect your client to this URL for approval. redirect_url = interface.generate_express_checkout_redirect_url(token) # Once they have approved your transaction at PayPal, they'll get # directed to the returnurl value you defined in set_express_checkout() # above. This view should then call do_express_checkout_payment() with # paymentaction = 'Sale'. This will finalize and bill. def test_authorize_and_delayed_capture(self): """ Tests a four-step checkout process involving the following flow:: One or more calls to `SetExpressCheckout`. --- User goes to PayPal, logs in, and confirms shipping, taxes, and total amount. --- A call to `GetExpressCheckoutDetails`. A call to `DoExpressCheckoutPayment`. A call to `DoAuthorization`. A call to `DoCapture`. """ pass def test_authorize_and_void(self): """ Tests a four-step checkout process involving the following flow:: One or more calls to `SetExpressCheckout`. --- User goes to PayPal, logs in, and confirms shipping, taxes, and total amount. --- A call to `GetExpressCheckoutDetails`. A call to `DoExpressCheckoutPayment`. A call to `DoAuthorization`. A call to `DoVoid`. """ pass class UrlGenerationTest(unittest.TestCase): def test_no_useraction(self): redirect_url = interface.generate_express_checkout_redirect_url( 'token-abc') self.assertTrue(redirect_url.endswith( '/webscr?cmd=_express-checkout&token=token-abc')) def test_renders_useraction_commit(self): redirect_url = interface.generate_express_checkout_redirect_url( 'token-abc', useraction='commit') redirect_path = ('/webscr?cmd=_express-checkout&token=token-abc' '&useraction=commit') self.assertTrue(redirect_url.endswith(redirect_path)) def test_renders_useraction_continue(self): redirect_url = interface.generate_express_checkout_redirect_url( 'token-abc', useraction='continue') redirect_path = ('/webscr?cmd=_express-checkout&token=token-abc' '&useraction=continue') self.assertTrue(redirect_url.endswith(redirect_path)) def test_renders_any_useraction_with_warning(self): with warnings.catch_warnings(record=True) as warning_context: redirect_url = interface.generate_express_checkout_redirect_url( 'token-abc', useraction='some_action') self.assertTrue(issubclass(warning_context[0].category, RuntimeWarning)) redirect_path = ('/webscr?cmd=_express-checkout&token=token-abc' '&useraction=some_action') self.assertTrue(redirect_url.endswith(redirect_path)) class CallParamsTest(unittest.TestCase): def setUp(self): self.configs_3token = {'API_USERNAME': 'test_username', 'API_PASSWORD': 'test_password', 'API_SIGNATURE': 'test_signature', 'API_AUTHENTICATION_MODE': '3TOKEN'} self.configs_certificate = { 'API_USERNAME': 'test_username', 'API_PASSWORD': 'test_password', 'API_CERTIFICATE_FILENAME': 'test_cert_filename', 'API_KEY_FILENAME': 'test_key_filename', 'API_AUTHENTICATION_MODE': 'CERTIFICATE'} def test_returns_3token_call_params(self): interface = PayPalInterface(**self.configs_3token) call_kwargs = {'param_a': 'a1', 'param_b': 'b2'} call_params = interface._get_call_params('some_method', **call_kwargs) version = interface.config.API_VERSION expected_call_params = {'data': {'USER': 'test_username', 'PWD': 'test_password', 'SIGNATURE': 'test_signature', 'PARAM_A': 'a1', 'PARAM_B': 'b2', 'METHOD': 'some_method', 'VERSION': version}, 'cert': None, 'url': interface.config.API_ENDPOINT, 'timeout': interface.config.HTTP_TIMEOUT, 'verify': interface.config.API_CA_CERTS} self.assertEqual(expected_call_params, call_params) def test_returns_unipay_call_params(self): interface = PayPalInterface(**self.configs_3token) interface.config.API_AUTHENTICATION_MODE = 'UNIPAY' interface.config.UNIPAY_SUBJECT = 'test_subject' call_kwargs = {'param_a': 'a1', 'param_b': 'b2'} call_params = interface._get_call_params('some_method', **call_kwargs) version = interface.config.API_VERSION expected_call_params = {'data': {'SUBJECT': 'test_subject', 'PARAM_A': 'a1', 'PARAM_B': 'b2', 'METHOD': 'some_method', 'VERSION': version}, 'cert': None, 'url': interface.config.API_ENDPOINT, 'timeout': interface.config.HTTP_TIMEOUT, 'verify': interface.config.API_CA_CERTS} self.assertEqual(expected_call_params, call_params) def test_returns_certificate_call_params(self): interface = PayPalInterface(**self.configs_certificate) call_kwargs = {'param_a': 'a1', 'param_b': 'b2'} call_params = interface._get_call_params('some_method', **call_kwargs) version = interface.config.API_VERSION expected_call_params = {'data': {'USER': 'test_username', 'PWD': 'test_password', 'PARAM_A': 'a1', 'PARAM_B': 'b2', 'METHOD': 'some_method', 'VERSION': version}, 'cert': ('test_cert_filename', 'test_key_filename'), 'url': interface.config.API_ENDPOINT, 'timeout': interface.config.HTTP_TIMEOUT, 'verify': interface.config.API_CA_CERTS} self.assertEqual(expected_call_params, call_params) def test_raises_error_for_single_none_config(self): interface = PayPalInterface(**self.configs_certificate) interface.config.API_USERNAME = None with self.assertRaisesRegexp(PayPalConfigError, 'USER'): interface._get_call_params('some_method', some_param=123) def test_raises_error_for_multiple_configs(self): interface = PayPalInterface(**self.configs_certificate) interface.config.API_USERNAME = None interface.config.API_PASSWORD = None with self.assertRaisesRegexp(PayPalConfigError, r'PWD.*USER'): interface._get_call_params('some_method', some_param=123) class CallTest(unittest.TestCase): def test_posts_params(self): with patch('paypal.interface.requests.post') as post_mock: post_mock.return_value = Mock(text='ACK=SUCCESS') paypal_response = interface._call('some_method', param_a='a1', param_b='b2') expected_data = interface._get_call_params('some_method', param_a='a1', param_b='b2') post_mock.assert_called_once_with(**expected_data) self.assertIsInstance(paypal_response, PayPalResponse) self.assertTrue(paypal_response.success) def test_raises_configerror_on_error_response(self): with patch('paypal.interface.requests.post') as post_mock: post_mock.return_value = Mock(text='ACK=NO_SUCCESS') with self.assertRaises(PayPalAPIResponseError): interface._call('some_method', param='a') paypal-1.2.5/tests/api_details_blank.py0000644000175000017500000000255012621031177022110 0ustar greg.taylorgtaylor00000000000000""" This file contains your PayPal test account credentials. If you are just getting started, you'll want to copy api_details_blank.py to api_details.py, and substitute the placeholders below with your PayPal test account details. """ from paypal import PayPalConfig # Enter your test account's API details here. You'll need the 3-token # credentials, not the certificate stuff. CONFIG = PayPalConfig(API_USERNAME="xxx_xxx_apix.xxx.com", API_PASSWORD="xxxxxxxxxx", API_SIGNATURE="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", DEBUG_LEVEL=0) # The following values may be found by visiting https://developer.paypal.com/, # clicking on the 'Applications' -> 'Sandbox accounts' link in the sandbox, # and looking at the accounts listed there. # You'll need a business and a personal account created to run these tests. # The email address of your personal test account. This is typically the # customer for these tests. EMAIL_PERSONAL = 'custX_xxxxxxxxxx_per@xxxxxxxx.com' # If you view the details of your personal account, you'll see credit card # details. Enter the credit card number from there. VISA_ACCOUNT_NO = 'xxxxxxxxxxxxxxxx' # And the expiration date in the form of MMYYYY. Note that there are no slashes, # and single-digit month numbers have a leading 0 (IE: 03 for march). VISA_EXPIRATION = 'mmyyyy' paypal-1.2.5/tests/test_buttons.py0000644000175000017500000000173512621031177021224 0ustar greg.taylorgtaylor00000000000000# coding=utf-8 import unittest from . import interface_factory interface = interface_factory.get_interface_obj() class ButtonTests(unittest.TestCase): """ These test the BM button API available in Payments Standard and up. This is the cheapest and most direct route towards accepting payments. """ def test_create_button(self): """ Tests the creation of a simple button. This particular one is not stored on the PayPal account. """ button_params = { 'BUTTONCODE': 'ENCRYPTED', 'BUTTONTYPE': 'BUYNOW', 'BUTTONSUBTYPE': 'SERVICES', 'BUYNOWTEXT': 'PAYNOW', 'L_BUTTONVAR0': 'notify_url=http://test.com', 'L_BUTTONVAR1': 'amount=5.00', 'L_BUTTONVAR2': 'item_name=Testing', 'L_BUTTONVAR3': 'item_number=12345', } response = interface.bm_create_button(**button_params) self.assertEqual(response.ACK, 'Success') paypal-1.2.5/tests/interface_factory.py0000644000175000017500000000257512621031177022161 0ustar greg.taylorgtaylor00000000000000""" This module creates PayPalInterface objects for each of the unit test modules to use. We create a new one for each unittest module to reduce any chance of tainting tests by all of them using the same interface. IE: Values getting modified. See get_interface_obj() below, as well as the README in this tests directory. """ import sys import os # The unit tests import this module, so we'll do the path modification to use # this paypal project instead of any potential globally installed ones. project_root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if not project_root_dir in sys.path: sys.path.insert(0, project_root_dir) from paypal import PayPalInterface try: from tests import api_details except ImportError: print(""" ERROR: No api_details.py file exists in your paypal/tests directory. Please copy api_details_blank.py to api_details.py and modify the values to your own API developer _test_ credentials. If you don't already have test credentials, please visit: https://developer.paypal.com """) sys.exit(1) def get_interface_obj(): """ Use this function to get a PayPalInterface object with your test API credentials (as specified in api_details.py). Create new interfaces for each unit test module to avoid potential variable pollution. """ return PayPalInterface(config=api_details.CONFIG) paypal-1.2.5/tests/test_direct_payment.py0000644000175000017500000000634712621031177022541 0ustar greg.taylorgtaylor00000000000000# coding=utf-8 import unittest from paypal import PayPalAPIResponseError from . import interface_factory from . import api_details interface = interface_factory.get_interface_obj() class TestDirectPayment(unittest.TestCase): def setUp(self): self.credit_card = { 'amt': '10.00', 'creditcardtype': 'Visa', 'acct': api_details.VISA_ACCOUNT_NO, 'expdate': api_details.VISA_EXPIRATION, 'cvv2': '123', 'firstname': 'John', 'lastname': 'Doe', 'street': '1313 Mockingbird Lane', 'city': 'Beverly Hills', 'state': 'CA', 'zip': '90110', 'countrycode': 'US', 'currencycode': 'USD', } def test_sale(self): sale = interface.do_direct_payment('Sale', **self.credit_card) self.assertTrue(sale.success) details = interface.get_transaction_details(TRANSACTIONID=sale.TRANSACTIONID) self.assertTrue(details.success) self.assertEqual(details.PAYMENTSTATUS.upper(), 'COMPLETED') self.assertEqual(details.REASONCODE.upper(), 'NONE') def test_exception_handling(self): """ Make sure response exception handling is working as intended by forcing some bad values. """ new_details = self.credit_card # Set an invalid credit card number. new_details['acct'] = '123' # Make sure this raises an exception. self.assertRaises(PayPalAPIResponseError, interface.do_direct_payment, 'Sale', **new_details) def test_abbreviated_sale(self): sale = interface.do_direct_payment(**self.credit_card) self.assertTrue(sale.success) details = interface.get_transaction_details(TRANSACTIONID=sale.TRANSACTIONID) self.assertTrue(details.success) self.assertEqual(details.PAYMENTSTATUS.upper(), 'COMPLETED') self.assertEqual(details.REASONCODE.upper(), 'NONE') def test_authorize_and_delayed_capture(self): # authorize payment auth = interface.do_direct_payment('Authorization', **self.credit_card) self.assertTrue(auth.success) self.assertEqual(auth.AMT, self.credit_card['amt']) # capture payment captured = interface.do_capture(auth.TRANSACTIONID, auth.AMT) self.assertTrue(captured.success) self.assertEqual(auth.TRANSACTIONID, captured.PARENTTRANSACTIONID) self.assertEqual(captured.PAYMENTSTATUS.upper(), 'COMPLETED') self.assertEqual(captured.REASONCODE.upper(), 'NONE') def test_authorize_and_void(self): # authorize payment auth = interface.do_direct_payment('Authorization', **self.credit_card) self.assertTrue(auth.success) self.assertEqual(auth.AMT, self.credit_card['amt']) # void payment note = 'Voided the authorization.' void = interface.do_void(AUTHORIZATIONID=auth.TRANSACTIONID, NOTE=note) self.assertTrue(void.success) self.assertEqual(auth.TRANSACTIONID, void.AUTHORIZATIONID) details = interface.get_transaction_details(TRANSACTIONID=auth.TRANSACTIONID) self.assertTrue(details.success) self.assertEqual(details.PAYMENTSTATUS.upper(), 'VOIDED') paypal-1.2.5/tests/test_countries.py0000644000175000017500000000145512621031177021540 0ustar greg.taylorgtaylor00000000000000# coding=utf-8 import unittest from paypal import countries class TestCountries(unittest.TestCase): def test_is_valid_country_abbrev(self): self.assertEqual(True, countries.is_valid_country_abbrev('US')) self.assertEqual(True, countries.is_valid_country_abbrev('us')) self.assertEqual(False, countries.is_valid_country_abbrev('us', case_sensitive=True)) def test_get_name_from_abbrev(self): us_fullval = 'United States of America' self.assertEqual(us_fullval, countries.get_name_from_abbrev('US')) self.assertEqual(us_fullval, countries.get_name_from_abbrev('us')) self.assertRaises(KeyError, countries.get_name_from_abbrev, 'us', case_sensitive=True) paypal-1.2.5/tests/README0000644000175000017500000000067712621031177017001 0ustar greg.taylorgtaylor00000000000000In order to run these tests, you must copy the included api_details_blank.py file to api_details.py and substitute the API credential placeholders with your own PayPal test account credentials. All tests will fail unless you do this. Once your credentials are in place, you'll want to install nose: pip install nose Then either run all of the tests: nosetests tests Or only run one of the suites: nosetests tests/test_buttons.py paypal-1.2.5/tests/__init__.py0000644000175000017500000000000012621031177020206 0ustar greg.taylorgtaylor00000000000000paypal-1.2.5/paypal.egg-info/0000755000175000017500000000000012644607167017742 5ustar greg.taylorgtaylor00000000000000paypal-1.2.5/paypal.egg-info/requires.txt0000644000175000017500000000001112644607166022331 0ustar greg.taylorgtaylor00000000000000requests paypal-1.2.5/paypal.egg-info/SOURCES.txt0000644000175000017500000000105412644607167021626 0ustar greg.taylorgtaylor00000000000000AUTHORS LICENSE MANIFEST.in README.rst setup.cfg setup.py paypal/__init__.py paypal/compat.py paypal/countries.py paypal/exceptions.py paypal/interface.py paypal/response.py paypal/response_list.py paypal/settings.py paypal.egg-info/PKG-INFO paypal.egg-info/SOURCES.txt paypal.egg-info/dependency_links.txt paypal.egg-info/requires.txt paypal.egg-info/top_level.txt tests/README tests/__init__.py tests/api_details_blank.py tests/interface_factory.py tests/test_buttons.py tests/test_countries.py tests/test_direct_payment.py tests/test_express_checkout.pypaypal-1.2.5/paypal.egg-info/top_level.txt0000644000175000017500000000000712644607166022470 0ustar greg.taylorgtaylor00000000000000paypal paypal-1.2.5/paypal.egg-info/dependency_links.txt0000644000175000017500000000000112644607166024007 0ustar greg.taylorgtaylor00000000000000 paypal-1.2.5/paypal.egg-info/PKG-INFO0000644000175000017500000000167012644607166021042 0ustar greg.taylorgtaylor00000000000000Metadata-Version: 1.1 Name: paypal Version: 1.2.5 Summary: PayPal API implementation in Python. Home-page: https://github.com/gtaylor/paypal-python Author: Gregory Taylor Author-email: gtaylor@gc-taylor.com License: Apache Software License Download-URL: http://pypi.python.org/pypi/paypal/ Description: An implementation of PayPal's API in Python. Currently features Direct Payment (Guest), and PayPal Express checkouts. Keywords: paypal nvp Platform: Platform Independent Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Libraries :: Python Modules paypal-1.2.5/setup.cfg0000644000175000017500000000012212644607167016576 0ustar greg.taylorgtaylor00000000000000[wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 paypal-1.2.5/setup.py0000755000175000017500000000276412621031431016464 0ustar greg.taylorgtaylor00000000000000#!/usr/bin/env python import re from setuptools import setup VERSION_PATTERN = re.compile(r'VERSION\s*=\s*(.*)$', re.I) VERSION = VERSION_PATTERN.search(open('paypal/__init__.py').read()) \ .groups()[0].strip().strip('\'"') LONG_DESCRIPTION = """An implementation of PayPal's API in Python. Currently features Direct Payment (Guest), and PayPal Express checkouts. """ CLASSIFIERS = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Libraries :: Python Modules' ] required = [ 'requests', ] packages = [ 'paypal', ] setup( name='paypal', version=VERSION, description='PayPal API implementation in Python.', long_description=LONG_DESCRIPTION, author='Pat Collins', author_email='pat@burned.com', maintainer='Gregory Taylor', maintainer_email='gtaylor@gc-taylor.com', install_requires=required, url='https://github.com/gtaylor/paypal-python', download_url='http://pypi.python.org/pypi/paypal/', packages=packages, platforms=['Platform Independent'], license='Apache Software License', classifiers=CLASSIFIERS, keywords='paypal nvp', ) paypal-1.2.5/PKG-INFO0000644000175000017500000000167012644607167016063 0ustar greg.taylorgtaylor00000000000000Metadata-Version: 1.1 Name: paypal Version: 1.2.5 Summary: PayPal API implementation in Python. Home-page: https://github.com/gtaylor/paypal-python Author: Gregory Taylor Author-email: gtaylor@gc-taylor.com License: Apache Software License Download-URL: http://pypi.python.org/pypi/paypal/ Description: An implementation of PayPal's API in Python. Currently features Direct Payment (Guest), and PayPal Express checkouts. Keywords: paypal nvp Platform: Platform Independent Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Libraries :: Python Modules paypal-1.2.5/AUTHORS0000644000175000017500000000074712621031177016025 0ustar greg.taylorgtaylor00000000000000The following people have contributed their valuable time to the paypal-python project. If you submit a patch, feel free to add your name to the list. Pat Collins - Original author Greg Taylor - Current maintainer Manik Surtani - Python 2.5 compatibility Christopher Smith - API updates Piotr Staroszczyk - Various cleanup Max Brauer - minor improvements paypal-1.2.5/LICENSE0000644000175000017500000002210412621031177015751 0ustar greg.taylorgtaylor00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: * You must give any other recipients of the Work or Derivative Works a copy of this License; and * You must cause any modified files to carry prominent notices stating that You changed the files; and * You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and * If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS