OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2012 Andy Davidoff http://www.disruptek.com/ |
| 2 # Copyright (c) 2010 Jason R. Coombs http://www.jaraco.com/ |
| 3 # Copyright (c) 2008 Chris Moyer http://coredumped.org/ |
| 4 # |
| 5 # Permission is hereby granted, free of charge, to any person obtaining a |
| 6 # copy of this software and associated documentation files (the |
| 7 # "Software"), to deal in the Software without restriction, including |
| 8 # without limitation the rights to use, copy, modify, merge, publish, dis- |
| 9 # tribute, sublicense, and/or sell copies of the Software, and to permit |
| 10 # persons to whom the Software is furnished to do so, subject to the fol- |
| 11 # lowing conditions: |
| 12 # |
| 13 # The above copyright notice and this permission notice shall be included |
| 14 # in all copies or substantial portions of the Software. |
| 15 # |
| 16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 17 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
| 18 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
| 19 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 20 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 21 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| 22 # IN THE SOFTWARE. |
| 23 |
| 24 import urllib |
| 25 import uuid |
| 26 from boto.connection import AWSQueryConnection |
| 27 from boto.fps.exception import ResponseErrorFactory |
| 28 from boto.fps.response import ResponseFactory |
| 29 import boto.fps.response |
| 30 |
| 31 __all__ = ['FPSConnection'] |
| 32 |
| 33 decorated_attrs = ('action', 'response') |
| 34 |
| 35 |
| 36 def add_attrs_from(func, to): |
| 37 for attr in decorated_attrs: |
| 38 setattr(to, attr, getattr(func, attr, None)) |
| 39 return to |
| 40 |
| 41 |
| 42 def complex_amounts(*fields): |
| 43 def decorator(func): |
| 44 def wrapper(self, *args, **kw): |
| 45 for field in filter(kw.has_key, fields): |
| 46 amount = kw.pop(field) |
| 47 kw[field + '.Value'] = getattr(amount, 'Value', str(amount)) |
| 48 kw[field + '.CurrencyCode'] = getattr(amount, 'CurrencyCode', |
| 49 self.currencycode) |
| 50 return func(self, *args, **kw) |
| 51 wrapper.__doc__ = "{0}\nComplex Amounts: {1}".format(func.__doc__, |
| 52 ', '.join(fields)) |
| 53 return add_attrs_from(func, to=wrapper) |
| 54 return decorator |
| 55 |
| 56 |
| 57 def requires(*groups): |
| 58 |
| 59 def decorator(func): |
| 60 |
| 61 def wrapper(*args, **kw): |
| 62 hasgroup = lambda x: len(x) == len(filter(kw.has_key, x)) |
| 63 if 1 != len(filter(hasgroup, groups)): |
| 64 message = ' OR '.join(['+'.join(g) for g in groups]) |
| 65 message = "{0} requires {1} argument(s)" \ |
| 66 "".format(getattr(func, 'action', 'Method'), message) |
| 67 raise KeyError(message) |
| 68 return func(*args, **kw) |
| 69 message = ' OR '.join(['+'.join(g) for g in groups]) |
| 70 wrapper.__doc__ = "{0}\nRequired: {1}".format(func.__doc__, |
| 71 message) |
| 72 return add_attrs_from(func, to=wrapper) |
| 73 return decorator |
| 74 |
| 75 |
| 76 def needs_caller_reference(func): |
| 77 |
| 78 def wrapper(*args, **kw): |
| 79 kw.setdefault('CallerReference', uuid.uuid4()) |
| 80 return func(*args, **kw) |
| 81 wrapper.__doc__ = "{0}\nUses CallerReference, defaults " \ |
| 82 "to uuid.uuid4()".format(func.__doc__) |
| 83 return add_attrs_from(func, to=wrapper) |
| 84 |
| 85 |
| 86 def api_action(*api): |
| 87 |
| 88 def decorator(func): |
| 89 action = ''.join(api or map(str.capitalize, func.func_name.split('_'))) |
| 90 response = ResponseFactory(action) |
| 91 if hasattr(boto.fps.response, action + 'Response'): |
| 92 response = getattr(boto.fps.response, action + 'Response') |
| 93 |
| 94 def wrapper(self, *args, **kw): |
| 95 return func(self, action, response, *args, **kw) |
| 96 wrapper.action, wrapper.response = action, response |
| 97 wrapper.__doc__ = "FPS {0} API call\n{1}".format(action, |
| 98 func.__doc__) |
| 99 return wrapper |
| 100 return decorator |
| 101 |
| 102 |
| 103 class FPSConnection(AWSQueryConnection): |
| 104 |
| 105 APIVersion = '2010-08-28' |
| 106 ResponseError = ResponseErrorFactory |
| 107 currencycode = 'USD' |
| 108 |
| 109 def __init__(self, *args, **kw): |
| 110 self.currencycode = kw.pop('CurrencyCode', self.currencycode) |
| 111 kw.setdefault('host', 'fps.sandbox.amazonaws.com') |
| 112 AWSQueryConnection.__init__(self, *args, **kw) |
| 113 |
| 114 def _required_auth_capability(self): |
| 115 return ['fps'] |
| 116 |
| 117 @needs_caller_reference |
| 118 @complex_amounts('SettlementAmount') |
| 119 @requires(['CreditInstrumentId', 'SettlementAmount.Value', |
| 120 'SenderTokenId', 'SettlementAmount.CurrencyCode']) |
| 121 @api_action() |
| 122 def settle_debt(self, action, response, **kw): |
| 123 """Allows a caller to initiate a transaction that atomically |
| 124 transfers money from a sender's payment instrument to the |
| 125 recipient, while decreasing corresponding debt balance. |
| 126 """ |
| 127 return self.get_object(action, kw, response) |
| 128 |
| 129 @requires(['TransactionId']) |
| 130 @api_action() |
| 131 def get_transaction_status(self, action, response, **kw): |
| 132 """Gets the latest status of a transaction. |
| 133 """ |
| 134 return self.get_object(action, kw, response) |
| 135 |
| 136 @requires(['StartDate']) |
| 137 @api_action() |
| 138 def get_account_activity(self, action, response, **kw): |
| 139 """Returns transactions for a given date range. |
| 140 """ |
| 141 return self.get_object(action, kw, response) |
| 142 |
| 143 @requires(['TransactionId']) |
| 144 @api_action() |
| 145 def get_transaction(self, action, response, **kw): |
| 146 """Returns all details of a transaction. |
| 147 """ |
| 148 return self.get_object(action, kw, response) |
| 149 |
| 150 @api_action() |
| 151 def get_outstanding_debt_balance(self, action, response): |
| 152 """Returns the total outstanding balance for all the credit |
| 153 instruments for the given creditor account. |
| 154 """ |
| 155 return self.get_object(action, {}, response) |
| 156 |
| 157 @requires(['PrepaidInstrumentId']) |
| 158 @api_action() |
| 159 def get_prepaid_balance(self, action, response, **kw): |
| 160 """Returns the balance available on the given prepaid instrument. |
| 161 """ |
| 162 return self.get_object(action, kw, response) |
| 163 |
| 164 @api_action() |
| 165 def get_total_prepaid_liability(self, action, response): |
| 166 """Returns the total liability held by the given account |
| 167 corresponding to all the prepaid instruments owned by the |
| 168 account. |
| 169 """ |
| 170 return self.get_object(action, {}, response) |
| 171 |
| 172 @api_action() |
| 173 def get_account_balance(self, action, response): |
| 174 """Returns the account balance for an account in real time. |
| 175 """ |
| 176 return self.get_object(action, {}, response) |
| 177 |
| 178 @needs_caller_reference |
| 179 @requires(['PaymentInstruction', 'TokenType']) |
| 180 @api_action() |
| 181 def install_payment_instruction(self, action, response, **kw): |
| 182 """Installs a payment instruction for caller. |
| 183 """ |
| 184 return self.get_object(action, kw, response) |
| 185 |
| 186 @needs_caller_reference |
| 187 @requires(['returnURL', 'pipelineName']) |
| 188 def cbui_url(self, **kw): |
| 189 """Generate a signed URL for the Co-Branded service API given |
| 190 arguments as payload. |
| 191 """ |
| 192 sandbox = 'sandbox' in self.host and 'payments-sandbox' or 'payments' |
| 193 endpoint = 'authorize.{0}.amazon.com'.format(sandbox) |
| 194 base = '/cobranded-ui/actions/start' |
| 195 |
| 196 validpipelines = ('SingleUse', 'MultiUse', 'Recurring', 'Recipient', |
| 197 'SetupPrepaid', 'SetupPostpaid', 'EditToken') |
| 198 assert kw['pipelineName'] in validpipelines, "Invalid pipelineName" |
| 199 kw.update({ |
| 200 'signatureMethod': 'HmacSHA256', |
| 201 'signatureVersion': '2', |
| 202 }) |
| 203 kw.setdefault('callerKey', self.aws_access_key_id) |
| 204 |
| 205 safestr = lambda x: x is not None and str(x) or '' |
| 206 safequote = lambda x: urllib.quote(safestr(x), safe='~') |
| 207 payload = sorted([(k, safequote(v)) for k, v in kw.items()]) |
| 208 |
| 209 encoded = lambda p: '&'.join([k + '=' + v for k, v in p]) |
| 210 canonical = '\n'.join(['GET', endpoint, base, encoded(payload)]) |
| 211 signature = self._auth_handler.sign_string(canonical) |
| 212 payload += [('signature', safequote(signature))] |
| 213 payload.sort() |
| 214 |
| 215 return 'https://{0}{1}?{2}'.format(endpoint, base, encoded(payload)) |
| 216 |
| 217 @needs_caller_reference |
| 218 @complex_amounts('TransactionAmount') |
| 219 @requires(['SenderTokenId', 'TransactionAmount.Value', |
| 220 'TransactionAmount.CurrencyCode']) |
| 221 @api_action() |
| 222 def reserve(self, action, response, **kw): |
| 223 """Reserve API is part of the Reserve and Settle API conjunction |
| 224 that serve the purpose of a pay where the authorization and |
| 225 settlement have a timing difference. |
| 226 """ |
| 227 return self.get_object(action, kw, response) |
| 228 |
| 229 @needs_caller_reference |
| 230 @complex_amounts('TransactionAmount') |
| 231 @requires(['SenderTokenId', 'TransactionAmount.Value', |
| 232 'TransactionAmount.CurrencyCode']) |
| 233 @api_action() |
| 234 def pay(self, action, response, **kw): |
| 235 """Allows calling applications to move money from a sender to |
| 236 a recipient. |
| 237 """ |
| 238 return self.get_object(action, kw, response) |
| 239 |
| 240 @requires(['TransactionId']) |
| 241 @api_action() |
| 242 def cancel(self, action, response, **kw): |
| 243 """Cancels an ongoing transaction and puts it in cancelled state. |
| 244 """ |
| 245 return self.get_object(action, kw, response) |
| 246 |
| 247 @complex_amounts('TransactionAmount') |
| 248 @requires(['ReserveTransactionId', 'TransactionAmount.Value', |
| 249 'TransactionAmount.CurrencyCode']) |
| 250 @api_action() |
| 251 def settle(self, action, response, **kw): |
| 252 """The Settle API is used in conjunction with the Reserve API and |
| 253 is used to settle previously reserved transaction. |
| 254 """ |
| 255 return self.get_object(action, kw, response) |
| 256 |
| 257 @complex_amounts('RefundAmount') |
| 258 @requires(['TransactionId', 'RefundAmount.Value', |
| 259 'CallerReference', 'RefundAmount.CurrencyCode']) |
| 260 @api_action() |
| 261 def refund(self, action, response, **kw): |
| 262 """Refunds a previously completed transaction. |
| 263 """ |
| 264 return self.get_object(action, kw, response) |
| 265 |
| 266 @requires(['RecipientTokenId']) |
| 267 @api_action() |
| 268 def get_recipient_verification_status(self, action, response, **kw): |
| 269 """Returns the recipient status. |
| 270 """ |
| 271 return self.get_object(action, kw, response) |
| 272 |
| 273 @requires(['CallerReference'], ['TokenId']) |
| 274 @api_action() |
| 275 def get_token_by_caller(self, action, response, **kw): |
| 276 """Returns the details of a particular token installed by this |
| 277 calling application using the subway co-branded UI. |
| 278 """ |
| 279 return self.get_object(action, kw, response) |
| 280 |
| 281 @requires(['UrlEndPoint', 'HttpParameters']) |
| 282 @api_action() |
| 283 def verify_signature(self, action, response, **kw): |
| 284 """Verify the signature that FPS sent in IPN or callback urls. |
| 285 """ |
| 286 return self.get_object(action, kw, response) |
| 287 |
| 288 @api_action() |
| 289 def get_tokens(self, action, response, **kw): |
| 290 """Returns a list of tokens installed on the given account. |
| 291 """ |
| 292 return self.get_object(action, kw, response) |
| 293 |
| 294 @requires(['TokenId']) |
| 295 @api_action() |
| 296 def get_token_usage(self, action, response, **kw): |
| 297 """Returns the usage of a token. |
| 298 """ |
| 299 return self.get_object(action, kw, response) |
| 300 |
| 301 @requires(['TokenId']) |
| 302 @api_action() |
| 303 def cancel_token(self, action, response, **kw): |
| 304 """Cancels any token installed by the calling application on |
| 305 its own account. |
| 306 """ |
| 307 return self.get_object(action, kw, response) |
| 308 |
| 309 @needs_caller_reference |
| 310 @complex_amounts('FundingAmount') |
| 311 @requires(['PrepaidInstrumentId', 'FundingAmount.Value', |
| 312 'SenderTokenId', 'FundingAmount.CurrencyCode']) |
| 313 @api_action() |
| 314 def fund_prepaid(self, action, response, **kw): |
| 315 """Funds the prepaid balance on the given prepaid instrument. |
| 316 """ |
| 317 return self.get_object(action, kw, response) |
| 318 |
| 319 @requires(['CreditInstrumentId']) |
| 320 @api_action() |
| 321 def get_debt_balance(self, action, response, **kw): |
| 322 """Returns the balance corresponding to the given credit instrument. |
| 323 """ |
| 324 return self.get_object(action, kw, response) |
| 325 |
| 326 @needs_caller_reference |
| 327 @complex_amounts('AdjustmentAmount') |
| 328 @requires(['CreditInstrumentId', 'AdjustmentAmount.Value', |
| 329 'AdjustmentAmount.CurrencyCode']) |
| 330 @api_action() |
| 331 def write_off_debt(self, action, response, **kw): |
| 332 """Allows a creditor to write off the debt balance accumulated |
| 333 partially or fully at any time. |
| 334 """ |
| 335 return self.get_object(action, kw, response) |
| 336 |
| 337 @requires(['SubscriptionId']) |
| 338 @api_action() |
| 339 def get_transactions_for_subscription(self, action, response, **kw): |
| 340 """Returns the transactions for a given subscriptionID. |
| 341 """ |
| 342 return self.get_object(action, kw, response) |
| 343 |
| 344 @requires(['SubscriptionId']) |
| 345 @api_action() |
| 346 def get_subscription_details(self, action, response, **kw): |
| 347 """Returns the details of Subscription for a given subscriptionID. |
| 348 """ |
| 349 return self.get_object(action, kw, response) |
| 350 |
| 351 @needs_caller_reference |
| 352 @complex_amounts('RefundAmount') |
| 353 @requires(['SubscriptionId']) |
| 354 @api_action() |
| 355 def cancel_subscription_and_refund(self, action, response, **kw): |
| 356 """Cancels a subscription. |
| 357 """ |
| 358 message = "If you specify a RefundAmount, " \ |
| 359 "you must specify CallerReference." |
| 360 assert not 'RefundAmount.Value' in kw \ |
| 361 or 'CallerReference' in kw, message |
| 362 return self.get_object(action, kw, response) |
| 363 |
| 364 @requires(['TokenId']) |
| 365 @api_action() |
| 366 def get_payment_instruction(self, action, response, **kw): |
| 367 """Gets the payment instruction of a token. |
| 368 """ |
| 369 return self.get_object(action, kw, response) |
OLD | NEW |