1 from base64 import b64encode
2 from gchecky import gxml
3 from gchecky import model as gmodel
4
9
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&, <this> is not &a safe string <to> &htmlize >_<!'
21 """
22 if not isinstance(html, basestring):
23
24
25 html = unicode(html)
26 return html.replace('&', '&').replace('<', '<').replace('>', '>')
27
29 """
30 TODO:
31 """
32 cart = None
33 signature = None
34 url = None
35 button = None
36 xml = None
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
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
75
76
77
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
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
102 get_cart_post_button = get_checkout_button_url
103
106
109
112
115
117 import hmac, sha
118 return hmac.new(self.merchant_key, xml_text, sha).digest()
119
120
121
134
140
141 -class ControllerContext(object):
142 """
143 """
144
145 outgoing = True
146
147 xml = None
148
149 message = None
150
151 diagnose = False
152
153 order_id = None
154
155 serial = None
156
157 response_message = None
158
159 response_xml = None
160
161 - def __init__(self, outgoing = True):
163
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
187 __str__ = __unicode__
188 __repr__ = __unicode__
189
191 """
192 An exception of this class occures whenever there is error in converting
193 python data to/from xml.
194 """
195 pass
196
198 """
199 An exception of this class occures whenever an exception is thrown
200 from user defined handler.
201 """
202 pass
203
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
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
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
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
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
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
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
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
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
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
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
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
328
331
339
342
345
348
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
365 pass
366
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
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
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
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
458 if doc.__class__ == gmodel.request_received_t:
459 return doc
460
461 if doc.__class__ == gmodel.bye_t:
462 return doc
463
464
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
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
484
488
492
499
505
512
518
519 - def refund_order(self, order_id, amount, reason, comment=None):
526
531
538
543
544 - def deliver_order(self, order_id,
545 carrier = None, tracking_number = None,
546 send_email = None):
556
557
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
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
599 context.order_id = order_id
600 self.__call_handler('on_message_receiving', context=context)
601
602 order = self.__call_handler('on_retrieve_order', context=context, order_id=order_id)
603
604
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
637 assert result.__class__ is gmodel.ok_t
638
639 self.__call_handler('on_message_received', context=context)
640 return result
641
642
643 Controller = ControllerLevel_2
644
645 if __name__ == "__main__":
647 import doctest
648 doctest.testmod()
649 run_doctests()
650