Amazon.com, often called the world's largest store, has opened up their entire catalog of products for integrating directly into other Web sites using either SOAP or REST (XML over HTTP). Through their various partnership programs, associates and third-party vendors can earn referral fees, manage inventories, and search product information for competitive pricing. In order to show off some of the utility of the Amazon Web service API, we are going to show you how to build a GUI tool that lets you search for items of similar type and within several product categories in the Amazon.com catalog. As is typical of Web service providers, the examples and tools provided by the Amazon.com developer's kit do not include Python bindings or libraries. Not to worry, though -- working with the Amazon API through Python is very straightforward, perhaps even simpler than the usual languages and tools.
For this project, our tool will be crafted using Python 2.3; SOAPpy version 0.10.4 will be used to proxy and marshall the API, and we'll use the wxPython toolkit for building our GUI. SOAPpy, since version 0.10.3, depends on the IEEE 754 floating point handler package fpconst (currently version 0.6.0). Hopefully, this will be included in a future Python release. See Resources for links to download these tools.
Go to the Amazon.com Web services site (see Resources) and download the free developer's kit and then register for a free developer's token, which you will need to pass in every method call to the Web service API. Before we get too deep into building our tool, let's test the Amazon API. Let's perform a keyword search on Amazon.com for books related to 'spotted owl'. Digging into the WSDL for the API, we find an appropriate method, the KeywordSearchRequest (see Listing 1).
Listing 1. Excerpt from the Amazon Web service API WSDL showing the KeywordSearchRequest.
...
<message name="KeywordSearchRequest">
<!-- Messages for Amazon Web APIs -->
<part name="KeywordSearchRequest" type="typens:KeywordRequest"/>
</message>
<message name="KeywordSearchResponse">
<part name="return" type="typens:ProductInfo"/>
</message>
<xsd:complexType name="KeywordRequest">
<xsd:all>
<xsd:element name="keyword" type="xsd:string"/>
<xsd:element name="page" type="xsd:string"/>
<xsd:element name="mode" type="xsd:string"/>
<xsd:element name="tag" type="xsd:string"/>
<xsd:element name="type" type="xsd:string"/>
<xsd:element name="devtag" type="xsd:string"/>
<xsd:element name="sort" type="xsd:string" minOccurs="0"/>
<xsd:element name="locale" type="xsd:string" minOccurs="0"/>
<xsd:element name="price" type="xsd:string" minOccurs="0"/>
</xsd:all>
</xsd:complexType>
<xsd:complexType name="ProductInfo">
<xsd:all>
<xsd:element name="TotalResults" type="xsd:string" minOccurs="0"/>
<!-- Total number of Search Results -->
<xsd:element name="TotalPages" type="xsd:string" minOccurs="0"/>
<!-- Total number of Pages of Search Results -->
<xsd:element name="ListName" type="xsd:string" minOccurs="0"/>
<!-- Listmania list name -->
<xsd:element name="Details" type="typens:DetailsArray" minOccurs="0"/>
</xsd:all>
</xsd:complexType>
...
|
The KeywordSearchRequest takes a single parameter, a structure containing the required parameters for a query. The obvious keyword parameter is first. Amazon.com will return results in pages of up to ten items, specified by the page parameter. The remaining parameters are not so obvious. Consulting the API documentation (yes, we have to look beyond the WSDL), we find that mode designates product-line, such as "books," "music," "dvd," or "kitchen" -- and the term used must match a list of specific categories. We will be using the US version of the mode parameter here. Alternatively, the API has UK, German, and Japanese versions of mode -- for example "books-uk," "books-de," and "books-jp," respectively.
Users in Amazon's Associates program can place their Associates ID in the tag parameter, potentially earning referral fees for purchases made in connection with the query. The details returned for each item can be set through the type parameter, allowed values being "lite" or "heavy." Finally (for US) is the devtag which must contain your developer's token. The remaining parameters are straightforward, but we won't use them here.
Upon successfully completing the query, a KeywordSearchResponse will be returned or, if the query found no matching items, a SOAP fault will be thrown that contains a message stating so. The actual response information is wrapped in a ProductInfo structure. Beyond the obvious TotalResults and TotalPages, we are interested in the Details component. The contents of the Details structure within the DetailsArray for each result depends on the mode product-line, as specified in the API documentation (see Resources).
So, let's write a simple app to perform a test query (see Listing 2).
Listing 2. amazon_soap_test.py - Simple Python code to test the Amazon Web service API.
#!/usr/bin/python
import SOAPpy
url = 'http://soap.amazon.com/schemas3/AmazonWebServices.wsdl'
proxy = SOAPpy.WSDL.Proxy(url)
# show methods retrieved from WSDL
print '%d methods in WSDL:' % len(proxy.methods) + '\n'
for key in proxy.methods.keys():
print key
print
# search request
_query = 'spotted owl'
request = { 'keyword': _query,
'page': '1',
'mode': 'books',
'tag': '',
'type': 'lite',
'devtag': 'INSERT YOUR TOKEN HERE' }
results = proxy.KeywordSearchRequest(request)
# display results
print 'Amazon.com search for " ' + _query + ' "\n'
print 'total pages of results (max 10 per page): ' + str(results.TotalPages)
print 'total results: ' + str(results.TotalResults) + '\n'
# only show first result here
if (results.TotalResults > 0):
print 'displaying first result (of %s):\n' %results.TotalResults
details = results.Details[0]
# we must use the _keys() method of SOAPpy Types.py for arrayType
for key in details._keys():
print key + ': ' + details[key]
print
|
After importing the SOAPpy library, we use it to create a proxy for the Amazon Web service API from the WSDL published on their site. The WSDL.Proxy from SOAPpy offers a powerful tool for accessing APIs defined with WSDL. Before making any calls using the proxy, we print out a list of the methods exposed in the Amazon API. Calling SOAP methods is then as simple as constructing the appropriate parameters, as specified in the WSDL and described in the API documentation, and making the method call on the proxy object. In this case, we create a request structure, populating it with parameters appropriate to our query.
Note that we use the _keys() method from SOAPpy's Types.py module, as the details component of the results is a SOAP arrayType, which maps roughly to a Python dictionary.
There is a bug in version 0.10.4 of SOAPpy on trying to load a WSDL definition from a URL (see Listing 3).
Listing 3. Traceback of using SOAPpy to load WSDL from URL.
Traceback (most recent call last):
File "./amazon_soap_test.py", line 21, in ?
proxy = SOAPpy.WSDL.Proxy(url)
File "/usr/lib/python2.3/site-packages/SOAPpy/WSDL.py", line 61, in __init__
self.wsdl = reader.loadFromStream(stream)
File "/usr/lib/python2.3/site-packages/SOAPpy/wstools/WSDLTools.py",
line 28, in loadFromStream
wsdl.location = file.name
AttributeError: addinfourl instance has no attribute 'name'
|
Directly loading the WSDL from a URL requires a little quick-fix to the WSDL.py module in SOAPpy 0.10.4 -- shown in Listing 4 -- basically, you change the assignment call from reader.loadFromStream(stream) to reader.loadFromURL(wsdlsource).
Listing 4. Diff showing a quick-fix to WSDL.py in SOAPpy 0.10.4.
60,61c60 < stream = urllib.urlopen(wsdlsource) < self.wsdl = reader.loadFromStream(stream) --- > self.wsdl = reader.loadFromURL(wsdlsource) |
And voila! Our little test app works (see Listing 5). Of course, rather than modifying SOAPpy, we could also retrieve the WSDL file from Amazon.com using a browser (see Resources) and then load it from a local file -- this does work in the 0.10.4 version of SOAPpy.
Listing 5. Output of amazon_soap_test.py.
[scott@baal amazon_api]$ python amazon_soap_test.py 26 methods in WSDL: BlendedSearchRequest WishlistSearchRequest ClearShoppingCartRequest MarketplaceSearchRequest BrowseNodeSearchRequest SimilaritySearchRequest SkuSearchRequest SellerSearchRequest SellerProfileSearchRequest ActorSearchRequest AsinSearchRequest ListManiaSearchRequest AuthorSearchRequest GetShoppingCartRequest PowerSearchRequest ExchangeSearchRequest DirectorSearchRequest TextStreamSearchRequest ModifyShoppingCartItemsRequest KeywordSearchRequest ArtistSearchRequest UpcSearchRequest GetTransactionDetailsRequest AddShoppingCartItemsRequest RemoveShoppingCartItemsRequest ManufacturerSearchRequest Amazon.com search for " spotted owl " total pages of results (max 10 per page): 6 total results: 52 displaying first result (of 52): Asin: 0060248912 ImageUrlSmall: http://images.amazon.com/images/P/0060248912.01.THUMBZZZ.jpg ProductName: There's an Owl in the Shower ListPrice: $14.95 Availability: THIS TITLE IS CURRENTLY NOT AVAILABLE. If you would like to purchase this title, we recommend that you occasionally check this page to see if it has become available. ReleaseDate: September, 1995 Catalog: Book <SOAPpy.Types.typedArrayType at 1084200524> Manufacturer: Harpercollins Juvenile Books Url: http://www.amazon.com/exec/obidos/ASIN/0060248912/?dev-t= D1V63VYIH286CL%26camp=2025%26link_code=sp1 UsedPrice: $1.25 ImageUrlMedium: http://images.amazon.com/images/P/0060248912.01.MZZZZZZZ.jpg ImageUrlLarge: http://images.amazon.com/images/P/0060248912.01.LZZZZZZZ.jpg OurPrice: $14.95 |
As you can see from the first part of Listing 5, there are 26 request methods defined in the WSDL for the Amazon API. Details on parameters and return structures can be found in the WSDL and the developer documentation from Amazon. We'll only be using the KeywordSearchRequest method in our examples here, although the code could certainly be extended to perform more advanced queries.
Now for something completely different ...
Now we can take our knowledge of the Amazon Web service API and do something useful with it. Python is often faulted by many people for not keeping up with the other SOAP-aware languages in the area of GUI creation. Actually, it's very easy to create powerful and truly cross-platform user interfaces with Python. To prove it, we'll create a simple little GUI application using Python and the wxPython UI library to perform keyword searches for books, music, and dvds. Figure 1 shows our GUI in action.
Figure 1. A simple GUI to search Amazon.com for books, music, and dvds.
After selecting the product line (also known as mode in Amazon speak), enter your keyword query (words separated by spaces) and hit the 'Search Amazon' button. The lights on your network hub will start blinking and after a few seconds, the 'Query Results' list box will fill. Select an item to see details and, if available, an image of the item. For simplicity, we only fetch the first 'page' of up to ten items. This could easily be extended.
Listing 6 shows the code for our application. Basically, it's our test application embedded in a user-interface and extended to search for music and dvds and to display details and images of found items.
Listing 6. amazon_widget.py - Python code for the Amazon.com Search GUI.
#!/usr/bin/python
# setup SOAP proxy
import SOAPpy
file = 'AmazonWebServices.wsdl'
amazon = SOAPpy.WSDL.Proxy(file)
# import the wxPython libraries
import wxPython.wx
from wxPython.wx import *
import urllib
# developer token for amazon api
DEV_TOKEN = 'INSERT YOUR TOKEN HERE'
# event ids
ID_SEARCH = 100
ID_SELCHG = 101
#------------------------------------------------------------------------------
# amazonWidgetFrame - the main window for our app
#------------------------------------------------------------------------------
class amazonWidgetFrame(wxFrame):
def __init__(self, parent, ID, title):
wxFrame.__init__(self, parent, ID, title, wxDefaultPosition, wxSize(500, 475))
# add a status bar
self.CreateStatusBar()
self.SetStatusText("")
# add other widgets to frame
frame_box = wxBoxSizer(wxVERTICAL)
# query part
query_box = wxBoxSizer(wxHORIZONTAL)
# set the mode (product line to search)
mode_box =
wxStaticBoxSizer(wxStaticBox(self, -1, "Product Line"), wxVERTICAL)
self.mode_books =
wxRadioButton(self, -1, "books",
wxDefaultPosition, wxDefaultSize, wxRB_GROUP)
self.mode_music = wxRadioButton(self, -1, "music",
wxDefaultPosition, wxDefaultSize)
self.mode_dvds = wxRadioButton(self, -1, "dvd",
wxDefaultPosition, wxDefaultSize)
mode_box.Add(self.mode_books, 0, wxLEFT|wxRIGHT, 10)
mode_box.Add(self.mode_music, 0, wxLEFT|wxRIGHT, 10)
mode_box.Add(self.mode_dvds, 0, wxLEFT|wxRIGHT, 10)
query_box.Add(mode_box, 0, wxALL, 5)
# edit and button
qtext_box =
wxStaticBoxSizer(wxStaticBox(self, -1, "Enter Query"), wxHORIZONTAL)
self.search_text =
wxTextCtrl(self, -1, "python web", wxDefaultPosition, (200, -1))
qtext_box.Add(self.search_text, 1, wxALL, 5)
qtext_box.Add(wxButton(self, ID_SEARCH, "Search Amazon"), 0, wxALL, 5)
query_box.Add(qtext_box, 0, wxALL, 5)
frame_box.Add(query_box, 0, wxBOTTOM, 5)
# results part
results_box = wxBoxSizer(wxHORIZONTAL)
# details
details_box =
wxStaticBoxSizer(wxStaticBox(self, -1, "Query Results"), wxVERTICAL)
self.results_list =
wxListBox(self, ID_SELCHG, wxDefaultPosition, (300,175),
[], wxLB_ALWAYS_SB)
details_box.Add(self.results_list, 0, wxALL, 5)
hbox = wxBoxSizer(wxHORIZONTAL)
tbox = wxBoxSizer(wxVERTICAL)
tbox.Add(wxStaticText(self, -1, "Author/Artist: "), 0, wxLEFT, 5)
tbox.Add(wxStaticText(self, -1, ""), 0, wxLEFT, 5)
hbox.Add(tbox, 0)
hbox.Add(20,0,1)
self.authors = wxListBox(self, -1, wxDefaultPosition, (200,65), [])
hbox.Add(self.authors, 0, wxALIGN_RIGHT)
details_box.Add(hbox, 0, wxBOTTOM, 5)
hbox = wxBoxSizer(wxHORIZONTAL)
hbox.Add(wxStaticText(self, -1, "Release Date: "), 0, wxLEFT, 5)
self.release_date = wxStaticText(self, -1, " ",
wxDefaultPosition, wxDefaultSize)
hbox.Add(self.release_date, 0)
details_box.Add(hbox, 0)
hbox = wxBoxSizer(wxHORIZONTAL)
hbox.Add(wxStaticText(self, -1, "List Price: "), 0, wxLEFT, 5)
self.list_price = wxStaticText(self, -1, " ",
wxDefaultPosition, wxDefaultSize)
hbox.Add(self.list_price, 0)
details_box.Add(hbox, 0)
hbox = wxBoxSizer(wxHORIZONTAL)
hbox.Add(wxStaticText(self, -1, "Amazon.com Price: "), 0, wxLEFT, 5)
self.amazon_price = wxStaticText(self, -1, " ",
wxDefaultPosition, wxDefaultSize)
hbox.Add(self.amazon_price, 0)
details_box.Add(hbox, 0, wxBOTTOM, 5)
results_box.Add(details_box, 0, wxALL, 5)
# image
image_box =
wxStaticBoxSizer(wxStaticBox(self, -1, "Product Image"), wxHORIZONTAL)
# inline class for drawing our product image
class amazonImagePanel(wxPanel):
def __init__(self, parent, id, position, size):
wxPanel.__init__(self, parent, id, position, size)
self.parent = parent
self.image = None
EVT_PAINT(self, self.OnPaint)
def OnPaint(self, evt):
dc = wxPaintDC(self)
if self.image:
bitmap = wxBitmapFromImage(self.image)
dc.DrawBitmap(bitmap, 0, 0, false)
self.image_canvas = amazonImagePanel(self, -1, wxDefaultPosition, (130,150))
image_box.Add(self.image_canvas, 1, wxALL, 5)
results_box.Add(image_box, 0, wxALL, 5)
# finally...
frame_box.Add(results_box, 0, wxTOP, 5)
# add main sizer
self.SetSizer(frame_box)
# set event handlers
EVT_BUTTON(self, ID_SEARCH, self.OnSearch)
EVT_LISTBOX(self, ID_SELCHG, self.OnSelectionChange)
#---------------------------------------------------------------------
# build and call query - if returns OK, set the GUI fields
#---------------------------------------------------------------------
def OnSearch(self, event):
# fetch mode and query text
query = self.search_text.GetValue()
self.mode = 'books'
if self.mode_music.GetValue():
self.mode = 'music'
elif self.mode_dvds.GetValue():
self.mode = 'dvd'
request = { 'keyword': query, 'page': '1', 'mode': self.mode,
'tag': '', 'type': 'lite', 'devtag': DEV_TOKEN }
# do the query
try:
tmp_results = amazon.KeywordSearchRequest(request)
except SOAPpy.faultType:
self.SetStatusText('There were no exact matches for the search')
else:
self.results = tmp_results
self.SetStatusText('total results: ' + str(self.results.TotalResults) +
' (only showing first 10)')
# load up results box
items = []
images = []
for detail in self.results.Details:
items.append(detail['ProductName'])
images.append(detail['ImageUrlMedium'])
self.results_list.Set(items)
self.results_list.SetFirstItem(0)
# clear displayed image
self.authors.Set([])
self.release_date.SetLabel('')
self.list_price.SetLabel('')
self.amazon_price.SetLabel('')
self.image_canvas.image = None
self.image_canvas.Refresh()
# cache images
self.loadImages(images)
#---------------------------------------------------------------------
# whenever the product selection changes, update other fields
#---------------------------------------------------------------------
def OnSelectionChange(self, event):
# fetch selected item from results
sel = self.results_list.GetSelections()
detail = self.results.Details[sel[0]]
# set the author/artist field
items = []
if self.mode == 'books':
if 'Authors' in detail._keys():
authors = detail['Authors']
for author in authors:
items.append(author)
self.authors.Set(items)
elif self.mode == 'music':
if 'Artists' in detail._keys():
authors = detail['Artists']
for author in authors:
items.append(author)
self.authors.Set(items)
elif self.mode == 'dvd':
# only in 'heavy' search type
if 'Directors' in detail._keys():
authors = detail['Directors']
for author in authors:
items.append(author)
self.authors.Set(items)
# set release date and pricing fields
if 'ReleaseDate' in detail._keys():
self.release_date.SetLabel(detail['ReleaseDate'])
if 'ListPrice' in detail._keys():
self.list_price.SetLabel(detail['ListPrice'])
if 'OurPrice' in detail._keys():
self.amazon_price.SetLabel(detail['OurPrice'])
# fetch image
image_name = 'image_tmp_' + str(sel[0]) + '.jpg'
self.image_canvas.image = wxImage(image_name)
self.image_canvas.Refresh()
#---------------------------------------------------------------------
# retrieve from amazon.com and temporarily save images
#---------------------------------------------------------------------
def loadImages(self, images):
i = 0
for url in images:
image_name = 'image_tmp_' + str(i) + '.jpg'
pic = urllib.urlopen(url)
img = pic.read()
pic.close()
f = open(image_name,'wb')
f.write(img)
f.close()
i = i+1
#------------------------------------------------------------------------------
# simple wxApp
#------------------------------------------------------------------------------
class amazonWidgetApp(wxApp):
def __init__(self, parent):
wxApp.__init__(self, parent)
def OnInit(self):
# start GUI
frame = amazonWidgetFrame(NULL, -1, "Amazon.com SOAP Widget")
frame.Show(true)
self.SetTopWindow(frame)
return true
#------------------------------------------------------------------------------
# run this when module gets called
#------------------------------------------------------------------------------
if __name__ == '__main__':
# must call this for wxImage to load JPEGs
wx.wxInitAllImageHandlers()
app = amazonWidgetApp(0)
app.MainLoop()
|
We chose to use the wxPython library rather than TkInter or other smaller GUI toolkits available for Python. It's basically a Python wrapper around the wxWindows library, a very fast and complete cross-platform windowing framework. For illustrative purposes in creating the GUI, we did the control layout by hand, rather than use one of the available wxWindows WYSIWYG layout tools, such as wxDesigner. This makes up the bulk of the code in the amazonFrame.__init__( ) method. We made extensive use of the wxBoxSizer control to manage the layout of the interesting controls.
One thing to note is the requirement to import SOAPpy and create our proxy before importing wxPython, due to a threading issue with the wxWindows event loop. Also, we load the WSDL from local file rather than from the Amazon.com site for start-up performance reasons.
When the application is run, amazonWidgetApp is created and MainLoop( ) is called to start the event loop. Note the class amazonImagePanel defined inline on the __init__( ) method of amazonWidgetFrame. We define two main event handlers: OnSearch( ) to handle pressing the "Search Amazon" button and OnSelectionChange( ) to handle selection of items in the "Query Results" list box.
OnSearch( ) gets the mode setting from the GUI, builds a request structure, and then calls the KeywordSearchRequest SOAP method on the amazon SOAPpy proxy. The SOAP method call is performed within a try - except block, which is generally good style, but here we do it specifically to catch a SOAP.faultType exception. We should check the exception type, but here we'll just assume it means that no results were found. If all is OK, we keep the results in the self.results attribute of amazonWidgetFrame, populate the "Query Results" list box, and fetch from Amazon.com all of the images referenced in the details of the results.
OnSelectionChange( ) fetches the appropriate details from self.results and populates the respective components on amazonWidgetFrame, finally loading the right image and forcing a repaint of the canvas.
- Participate in the discussion forum.
- Register at Amazon.com Web services to get a free developer's token and to download the developer's kit. The kit includes documentation, the SOAP WSDL, and examples and tools in other programming languages.
- Retrieve the Amazon Web APIs WSDL file directly from http://soap.amazon.com/schemas3/AmazonWebServices.wsdl.
- See www.Python.org for the world of Python, including the standard documentation and to download Python.
- Get the recent release of SOAPpy 0.10.4 from Sourceforge.net. SOAPpy, since version 0.10.3, depends on the IEEE 754 floating point handler package fpconst.
- Go to wxPython.org for the latest copy of wxPython, as well as documentation and other resources.
- Read the previous column of this series on the Google Web service API or check out all the Python Web services developer columns in this series.
Scott Archer is a software architect and co-founder of GlowingOrb, Inc., a software tools developer focusing on model-driven solutions and their integration into core business processes. Mr. Archer holds an M.Phil in Computational Molecular Biology from the University of Hong Kong. You can contact Mr. Archer at scott.archer at glowingorb.com.

Uche Ogbuji is a consultant and co-founder of Fourthought Inc., a software vendor and consultancy specializing in XML solutions for enterprise knowledge management applications. Fourthought develops 4Suite, open source platforms for XML middleware. You can contact Mr. Ogbuji at uche@ogbuji.net.
Comments (Undergoing maintenance)





