Package gchecky :: Module controller
[hide private]
[frames] | no frames]

Source Code for Module gchecky.controller

  1  from base64 import b64encode 
  2  from gchecky import gxml 
  3  from gchecky import model as gmodel 
  4   
5 -class ProcessingException(Exception):
6 - def __init__(self, message, where=''):
7 self.where = where 8 return Exception.__init__(self, message)
9
10 -def html_escape(html):
11 """ 12 A simple helper that escapes '<', '>' and '&' to make sure the text 13 is safe to be output directly into html text flow. 14 @param html A (unicode or not) string to be escaped from html. 15 16 >>> safe_str = 'Is''n it a text \"with\": some fancy characters ;-)?!' 17 >>> html_escape(safe_str) == safe_str 18 True 19 >>> html_escape('Omg&, <this> is not &a safe string <to> &htmlize >_<!') 20 'Omg&amp;, &lt;this&gt; is not &amp;a safe string &lt;to&gt; &amp;htmlize &gt;_&lt;!' 21 """ 22 if not isinstance(html, basestring): 23 # Nore: html should be a string already, so don't bother applying 24 # the correct encoding - use the system defaults. 25 html = unicode(html) 26 return html.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
27
28 -class html_order(object):
29 """ 30 TODO: 31 """ 32 cart = None 33 signature = None 34 url = None 35 button = None 36 xml = None
37 - def html(self):
38 """ 39 Return the html form containing two required hidden fields 40 and the submit button in the form of Google Checkout button image. 41 """ 42 return """ 43 <form method="post" action="%s"> 44 <input type="hidden" name="cart" value="%s" /> 45 <input type="hidden" name="signature" value="%s" /> 46 <input type="image" src="%s" alt="Google Checkout" /> 47 </form> 48 """ % (html_escape(self.url), self.cart, self.signature, html_escape(self.button))
49
50 -class ControllerLevel_1(object):
51 __MERCHANT_BUTTON = 'MERCHANT_BUTTON' 52 __CLIENT_POST_CART = 'CLIENT_POST_CART' 53 __SERVER_POST_CART = 'SERVER_POST_CART' 54 __ORDER_PROCESSING = 'ORDER_PROCESSING' 55 __CLIENT_DONATION = 'CLIENT_DONATION' 56 __SERVER_DONATION = 'SERVER_DONATION' 57 __DONATION_BUTTON = 'DONATION_BUTTON' 58 __SANDBOX_URLS = {__MERCHANT_BUTTON: 'https://sandbox.google.com/checkout/buttons/checkout.gif?merchant_id=%s&w=160&h=43&style=white&variant=text', 59 __CLIENT_POST_CART:'https://sandbox.google.com/checkout/api/checkout/v2/checkout/Merchant/%s', 60 __SERVER_POST_CART:'https://sandbox.google.com/checkout/api/checkout/v2/merchantCheckout/Merchant/%s', 61 __ORDER_PROCESSING:'https://sandbox.google.com/checkout/api/checkout/v2/request/Merchant/%s', 62 __CLIENT_DONATION: 'https://sandbox.google.com/checkout/api/checkout/v2/checkout/Donations/%s', 63 __SERVER_DONATION: 'https://sandbox.google.com/checkout/api/checkout/v2/merchantCheckout/Donations/%s', 64 __DONATION_BUTTON: 'https://sandbox.google.com/checkout/buttons/donation.gif?merchant_id=%s&w=160&h=43&style=white&variant=text', 65 } 66 __PRODUCTION_URLS={__MERCHANT_BUTTON: 'https://checkout.google.com/buttons/checkout.gif?merchant_id=%s&w=160&h=43&style=white&variant=text', 67 __CLIENT_POST_CART:'https://checkout.google.com/api/checkout/v2/checkout/Merchant/%s', 68 __SERVER_POST_CART:'https://checkout.google.com/api/checkout/v2/merchantCheckout/Merchant/%s', 69 __ORDER_PROCESSING:'https://checkout.google.com/api/checkout/v2/request/Merchant/%s', 70 __CLIENT_DONATION: 'https://checkout.google.com/api/checkout/v2/checkout/Donations/%s', 71 __SERVER_DONATION: 'https://checkout.google.com/api/checkout/v2/merchantCheckout/Donations/%s', 72 __DONATION_BUTTON: 'https://checkout.google.com/buttons/donation.gif?merchant_id=%s&w=160&h=43&style=white&variant=text', 73 } 74 # Specify all the needed information such as merchant account credentials: 75 # - sandbox or production 76 # - google vendor ID 77 # - google merchant key
78 - def __init__(self, vendor_id, merchant_key, is_sandbox=True, currency='USD'):
79 self.vendor_id = vendor_id 80 self.merchant_key = merchant_key 81 self.is_sandbox = is_sandbox 82 self.currency = currency
83
84 - def _get_url(self, tag, diagnose):
85 urls = (self.is_sandbox and self.__SANDBOX_URLS 86 ) or self.__PRODUCTION_URLS 87 if urls.has_key(tag): 88 url = urls[tag] 89 if diagnose: 90 url += '/diagnose' 91 return url 92 raise Exception('Unknown url tag "' + tag + '"')
93
94 - def get_client_post_cart_url(self, diagnose):
95 return self._get_url(self.__CLIENT_POST_CART, diagnose) % (self.vendor_id,)
96
97 - def get_server_post_cart_url(self, diagnose):
98 return self._get_url(self.__SERVER_POST_CART, diagnose) % (self.vendor_id,)
99
100 - def get_checkout_button_url(self, diagnose):
101 return self._get_url(self.__MERCHANT_BUTTON, diagnose) % (self.vendor_id,)
102 get_cart_post_button = get_checkout_button_url 103
104 - def get_order_processing_url(self, diagnose):
105 return self._get_url(self.__ORDER_PROCESSING, diagnose) % (self.vendor_id,)
106
107 - def get_client_donation_url(self, diagnose):
108 return self._get_url(self.__CLIENT_DONATION, diagnose) % (self.vendor_id,)
109
110 - def get_server_donation_url(self, diagnose):
111 return self._get_url(self.__SERVER_DONATION, diagnose) % (self.vendor_id,)
112
113 - def get_donation_button_url(self, diagnose):
114 return self._get_url(self.__DONATION_BUTTON, diagnose) % (self.vendor_id,)
115
116 - def create_HMAC_SHA_signature(self, xml_text):
117 import hmac, sha 118 return hmac.new(self.merchant_key, xml_text, sha).digest()
119 120 # Specify order_id to track the order 121 # The order_id will be send back to us by google with order verification
122 - def prepare_order(self, order, order_id=None, diagnose=False):
123 cart = order.toxml() 124 125 cart64 = b64encode(cart) 126 signature64 = b64encode(self.create_HMAC_SHA_signature(cart)) 127 html = html_order() 128 html.cart = cart64 129 html.signature = signature64 130 html.url = self.get_client_post_cart_url(diagnose) 131 html.button = self.get_checkout_button_url(diagnose) 132 html.xml = cart 133 return html
134
135 - def prepare_donation(self, order, order_id=None, diagnose=False):
140
141 -class ControllerContext(object):
142 """ 143 """ 144 # Indicates the direction: True => we call GC, False => GC calls us 145 outgoing = True 146 # The request XML text 147 xml = None 148 # The request message - one of the classes in gchecky.model module 149 message = None 150 # Indicates that the message being sent is diagnose message (implies outgoing=True). 151 diagnose = False 152 # Associated google order number 153 order_id = None 154 # A serial number assigned by google to this message 155 serial = None 156 # The response message - one of the classes in gchecky.model module 157 response_message = None 158 # The response XML text 159 response_xml = None 160
161 - def __init__(self, outgoing = True):
162 self.outgoing = outgoing
163
164 -class GcheckyError(Exception):
165 """ 166 Base class for exception that could be thrown by gchecky library. 167 """
168 - def __init__(self, message, context, origin=None):
169 """ 170 @param message String message describing the problem. Can't be empty. 171 @param context An instance of gchecky.controller.ControllerContext 172 that describes the current request processing context. 173 Can't be None. 174 @param origin The original exception that caused this exception 175 to be thrown if any. Could be None. 176 """ 177 self.message = message 178 self.context = context 179 self.origin = origin 180 self.traceback = None 181 if origin is not None: 182 from traceback import format_exc 183 self.traceback = format_exc()
184
185 - def __unicode__(self):
186 return self.message
187 __str__ = __unicode__ 188 __repr__ = __unicode__
189
190 -class DataError(GcheckyError):
191 """ 192 An exception of this class occures whenever there is error in converting 193 python data to/from xml. 194 """ 195 pass
196
197 -class HandlerError(GcheckyError):
198 """ 199 An exception of this class occures whenever an exception is thrown 200 from user defined handler. 201 """ 202 pass
203
204 -class SystemError(GcheckyError):
205 """ 206 An exception of this class occures whenever there is a system error, such 207 as network being unavailable or DB down. 208 """ 209 pass
210
211 -class LibraryError(GcheckyError):
212 """ 213 An exception of this class occures whenever there is a bug encountered 214 in gchecky library. It represents a bug which should be reported as an issue 215 at U{Gchecky issue tracker <http://gchecky.googlecode.com/>}. 216 """ 217 pass
218
219 -class ControllerLevel_2(ControllerLevel_1):
220 - def on_xml_sending(self, context):
221 """ 222 This hook is called just before sending xml to GC. 223 224 @param context.xml The xml message to be sent to GC. 225 @param context.url The exact URL the message is about to be sent. 226 @return Should return nothing, because the return value is ignored. 227 """ 228 pass
229
230 - def on_xml_sent(self, context):
231 """ 232 This hook is called right after sending xml to GC. 233 234 @param context.xml The xml message to be sent to GC. 235 @param context.url The exact URL the message is about to be sent. 236 @param context.response_xml The reply xml of GC. 237 @return Should return nothing, because the return value is ignored. 238 """ 239 pass
240
241 - def on_message_sending(self, context):
242 """ 243 This hook is called just before sending xml to GC. 244 245 @param context.xml The xml message to be sent to GC. 246 @param context.url The exact URL the message is about to be sent. 247 @return Should return nothing, because the return value is ignored. 248 """ 249 pass
250
251 - def on_message_sent(self, context):
252 """ 253 This hook is called right after sending xml to GC. 254 255 @param context.xml The message to be sent to GC (an instance of one 256 of gchecky.model classes). 257 @param context.response_xml The reply message of GC. 258 @return Should return nothing, because the return value is ignored. 259 """ 260 pass
261
262 - def on_xml_receiving(self, context):
263 """ 264 This hook is called just before processing the received xml from GC. 265 266 @param context.xml The xml message received from GC. 267 @return Should return nothing, because the return value is ignored. 268 """ 269 pass
270
271 - def on_xml_received(self, context):
272 """ 273 This hook is called right after processing xml from GC. 274 275 @param context.xml The xml message received from GC. 276 @param context.response_xml The reply xml to GC. 277 @return Should return nothing, because the return value is ignored. 278 """ 279 pass
280
281 - def on_message_receiving(self, context):
282 """ 283 This hook is called just before processing the received message from GC. 284 285 @param context.message The message received from GC. 286 @return Should return nothing, because the return value is ignored. 287 """ 288 pass
289
290 - def on_message_received(self, context):
291 """ 292 This hook is called right after processing message from GC. 293 294 @param context.message The message received from GC. 295 @param context.response_message The reply object to GC (either ok_t or error_t). 296 @return Should return nothing, because the return value is ignored. 297 """ 298 pass
299
300 - def on_retrieve_order(self, order_id, context=None):
301 """ 302 This hook is called from message processing code just before calling 303 the corresponding message handler. 304 The idea is to allow user code to load order in one place and then 305 receive the loaded object as parameter in message handler. 306 This method should not throw if order is not found - instead it should 307 return None. 308 309 @param order_id The google order number corresponding to the message 310 received. 311 @return The order object that will be passed to message handlers. 312 """ 313 pass
314
315 - def handle_new_order(self, message, order_id, context, order=None):
316 """ 317 Google sends a new order notification when a buyer places an order 318 through Google Checkout. Before shipping the items in an order, 319 you should wait until you have also received the risk information 320 notification for that order as well as the order state change 321 notification informing you that the order's financial state 322 has been updated to 'CHARGEABLE'. 323 """ 324 pass
325
326 - def handle_order_state_change(self, message, order_id, context, order=None):
327 pass
328
329 - def handle_authorization_amount(self, message, order_id, context, order=None):
330 pass
331
332 - def handle_risk_information(self, message, order_id, context, order=None):
333 """ 334 Google Checkout sends a risk information notification to provide 335 financial information 336 that helps you to ensure that an order is not fraudulent. 337 """ 338 pass
339
340 - def handle_charge_amount(self, message, order_id, context, order=None):
341 pass
342
343 - def handle_refund_amount(self, message, order_id, context, order=None):
344 pass
345
346 - def handle_chargeback_amount(self, message, order_id, context, order=None):
347 pass
348
349 - def handle_notification(self, message, order_id, context, order=None):
350 """ 351 This handler is called when a message received from GC and when the more 352 specific message handler was not found or returned None (which means 353 it was not able to process the message). 354 355 @param message The message from GC to be processed. 356 @param order_id The google order number for which message is sent. 357 @param order The object loaded by on_retrieve_order(order_id) or None. 358 @return If message was processed successfully then return gmodel.ok_t(). 359 If an error occured when proessing, then the method should 360 return any other value (not-None). 361 If the message is of unknown type or can't be processed by 362 this handler then return None. 363 """ 364 # By default return None because we don' handle anything 365 pass
366
367 - def on_exception(self, exception, context):
368 """ 369 By default simply rethrow the exception ignoring context. 370 Could be used for loggin all the processing errors. 371 @param exception The exception that was caught, of (sub)type GcheckyError. 372 @param context The request context where the exception occured. 373 """ 374 raise exception
375
376 - def __call_handler(self, handler_name, context, *args, **kwargs):
377 if hasattr(self, handler_name): 378 try: 379 handler = getattr(self, handler_name) 380 return handler(context=context, *args, **kwargs) 381 except Exception, e: 382 error = "Exception in user handler '%s': %s" % (handler_name, e) 383 raise HandlerError(message=error, 384 context=context, 385 origin=e) 386 error="Unknown user handler: '%s'" % (handler_name,) 387 raise HandlerError(message=error, context=context)
388
389 - def _send_xml(self, msg, context, diagnose):
390 """ 391 The helper method that submits an xml message to GC. 392 """ 393 context.diagnose = diagnose 394 url = self.get_order_processing_url(diagnose) 395 context.url = url 396 import urllib2 397 req = urllib2.Request(url=url, data=msg) 398 req.add_header('Authorization', 399 'Basic %s' % (b64encode('%s:%s' % (self.vendor_id, 400 self.merchant_key)),)) 401 req.add_header('Content-Type', ' application/xml; charset=UTF-8') 402 req.add_header('Accept', ' application/xml; charset=UTF-8') 403 try: 404 self.__call_handler('on_xml_sending', context=context) 405 response = urllib2.urlopen(req).read() 406 self.__call_handler('on_xml_sent', context=context) 407 return response 408 except urllib2.HTTPError, e: 409 error = e.fp.read() 410 raise SystemError(message='Error in urllib2.urlopen: %s' % (error,), 411 context=context, 412 origin=e)
413
414 - def send_message(self, message, context=None, diagnose=False):
415 if context is None: 416 context = ControllerContext(outgoing=True) 417 context.message = message 418 context.diagnose = diagnose 419 420 if isinstance(message, gmodel.abstract_order_t): 421 context.order_id = message.google_order_number 422 423 try: 424 try: 425 self.__call_handler('on_message_sending', context=context) 426 message_xml = message.toxml() 427 context.xml = message_xml 428 except Exception, e: 429 error = "Error converting message to xml: '%s'" % (unicode(e), ) 430 raise DataError(message=error, context=context, origin=e) 431 response_xml = self._send_xml(message_xml, context=context, diagnose=diagnose) 432 context.response_xml = response_xml 433 434 response = self.__process_message_result(response_xml, context=context) 435 context.response_message = response 436 437 self.__call_handler('on_message_sent', context=context) 438 return response 439 except GcheckyError, e: 440 return self.on_exception(exception=e, context=context)
441
442 - def __process_message_result(self, response_xml, context):
443 try: 444 doc = gxml.Document.fromxml(response_xml) 445 except Exception, e: 446 error = "Error converting message to xml: '%s'" % (unicode(e), ) 447 raise LibraryError(message=error, context=context, origin=e) 448 449 if context.diagnose: 450 # It has to be a 'diagnosis' response, otherwise... omg!.. panic!... 451 if doc.__class__ != gmodel.diagnosis_t: 452 error = "The response has to be of type diagnosis_t, not '%s'" % (doc.__class__,) 453 raise LibraryError(message=error, 454 context=context) 455 return doc 456 457 # If the response is 'ok' or 'bye' just return, because its good 458 if doc.__class__ == gmodel.request_received_t: 459 return doc 460 461 if doc.__class__ == gmodel.bye_t: 462 return doc 463 464 # It's not 'ok' so it has to be 'error', otherwise it's an error 465 if doc.__class__ != gmodel.error_t: 466 error = "Unknown response type (expected error_t): '%s'" % (doc.__class__,) 467 raise LibraryError(message=error, context=context) 468 469 # 'error' - process it by throwing an exception with error/warning text 470 msg = 'Error message from GCheckout API:\n%s' % (doc.error_message, ) 471 if doc.warning_messages: 472 tmp = '' 473 for warning in doc.warning_messages: 474 tmp += '\n%s' % (warning,) 475 msg += ('Additional warnings:%s' % (tmp,)) 476 raise DataError(message=msg, context=context)
477
478 - def hello(self):
479 context = ControllerContext() 480 doc = self.send_message(gmodel.hello_t(), context) 481 if isinstance(doc, gxml.Document) and (doc.__class__ != gmodel.bye_t): 482 error = "Expected <bye/> but got %s" % (doc.__class__,) 483 raise LibraryError(message=error, context=context, origin=e)
484
485 - def archive_order(self, order_id):
488
489 - def unarchive_order(self, order_id):
492
493 - def send_buyer_message(self, order_id, message):
494 self.send_message(gmodel.send_buyer_message_t( 495 google_order_number = order_id, 496 message = message, 497 send_email = True 498 ))
499
500 - def add_merchant_order_number(self, order_id, merchant_order_number):
505
506 - def add_tracking_data(self, order_id, carrier, tracking_number):
512
513 - def charge_order(self, order_id, amount):
514 self.send_message(gmodel.charge_order_t( 515 google_order_number = order_id, 516 amount = gmodel.price_t(value = amount, currency = self.currency) 517 ))
518
519 - def refund_order(self, order_id, amount, reason, comment=None):
520 self.send_message(gmodel.refund_order_t( 521 google_order_number = order_id, 522 amount = gmodel.price_t(value = amount, currency = self.currency), 523 reason = reason, 524 comment = comment or None 525 ))
526
527 - def authorize_order(self, order_id):
531
532 - def cancel_order(self, order_id, reason, comment=None):
533 self.send_message(gmodel.cancel_order_t( 534 google_order_number = order_id, 535 reason = reason, 536 comment = comment or None 537 ))
538
539 - def process_order(self, order_id):
543
544 - def deliver_order(self, order_id, 545 carrier = None, tracking_number = None, 546 send_email = None):
547 tracking = None 548 if carrier or tracking_number: 549 tracking = gmodel.tracking_data_t(carrier = carrier, 550 tracking_number = tracking_number) 551 self.send_message(gmodel.deliver_order_t( 552 google_order_number = order_id, 553 tracking_data = tracking, 554 send_email = send_email or None 555 ))
556 557 # This method gets a string and returns a string
558 - def receive_xml(self, input_xml, context=None):
559 if context is None: 560 context = ControllerContext(outgoing=False) 561 context.xml = input_xml 562 try: 563 self.__call_handler('on_xml_receiving', context=context) 564 try: 565 input = gxml.Document.fromxml(input_xml) 566 context.message = input 567 except Exception, e: 568 error = 'Error reading XML: %s' % (e,) 569 raise DataError(message=error, context=context, origin=e) 570 571 result = self.receive_message(message=input, 572 order_id=input.google_order_number, 573 context=context) 574 context.response_message = result 575 576 try: 577 response_xml = result.toxml() 578 context.response_xml = response_xml 579 except Exception, e: 580 error = 'Error reading XML: %s' % (e,) 581 raise DataError(message=error, context=context, origin=e) 582 self.__call_handler('on_xml_received', context=context) 583 return response_xml 584 except GcheckyError, e: 585 return self.on_exception(exception=e, context=context)
586 587 # A dictionary of document handler names. Comes handy in receive_message. 588 __MESSAGE_HANDLERS = { 589 gmodel.new_order_notification_t: 'handle_new_order', 590 gmodel.order_state_change_notification_t: 'handle_order_state_change', 591 gmodel.authorization_amount_notification_t: 'handle_authorization_amount', 592 gmodel.risk_information_notification_t: 'handle_risk_information', 593 gmodel.charge_amount_notification_t: 'handle_charge_amount', 594 gmodel.refund_amount_notification_t: 'handle_refund_amount', 595 gmodel.chargeback_amount_notification_t: 'handle_chargeback_amount', 596 } 597
598 - def receive_message(self, message, order_id, context):
599 context.order_id = order_id 600 self.__call_handler('on_message_receiving', context=context) 601 # retreive order instance from DB given the google order number 602 order = self.__call_handler('on_retrieve_order', context=context, order_id=order_id) 603 604 # handler = None 605 result = None 606 if self.__MESSAGE_HANDLERS.has_key(message.__class__): 607 handler_name = self.__MESSAGE_HANDLERS[message.__class__] 608 result = self.__call_handler(handler_name, 609 message=message, 610 order_id=order_id, 611 context=context, 612 order=order) 613 614 if result is None: 615 result = self.__call_handler('handle_notification', 616 message=message, 617 order_id=order_id, 618 context=context, 619 order=order) 620 621 error = None 622 if result is None: 623 error = "Notification '%s' was not handled" % (message.__class__,) 624 elif not (result.__class__ is gmodel.ok_t): 625 try: 626 error = unicode(result) 627 except Exception, e: 628 error = "Invalid value returned by handler '%s': %s" % (handler_name, 629 e) 630 raise HandlerError(message=error, context=context, origin=e) 631 632 if error is not None: 633 result = gmodel.error_t(serial_number = 'error', 634 error_message=error) 635 else: 636 # TODO: Remove this after testing 637 assert result.__class__ is gmodel.ok_t 638 639 self.__call_handler('on_message_received', context=context) 640 return result
641 642 # Just an alias with a shorter name. 643 Controller = ControllerLevel_2 644 645 if __name__ == "__main__":
646 - def run_doctests():
647 import doctest 648 doctest.testmod()
649 run_doctests() 650