diff --git a/ebay/config.ini.example b/ebay/config.ini.example index 936ea13..e475559 100644 --- a/ebay/config.ini.example +++ b/ebay/config.ini.example @@ -1,3 +1,9 @@ +#Configuration file template. +# +# Copy the file: +# config.ini.example --> config.ini +# Then fill in information marked "ENTER_HERE". + [keys] #Your secret Ebay access keys. #Generate the keys on: developer.ebay.com @@ -18,9 +24,10 @@ imgur_key = ENTER_HERE # #compatibility_level: The version used in header of the request. Tested values: # compatibility_level = 785 -# compatibility_level = 853 +# compatibility_level = 849 # See: # http://developer.ebay.com/DevZone/XML/docs/HowTo/eBayWS/eBaySchemaVersioning.html +# http://developer.ebay.com/devzone/xml/docs/releasenotes.html siteid = ENTER_HERE global_id = ENTER_HERE compatibility_level = ENTER_HERE diff --git a/ebay/finding.py b/ebay/finding.py index 5be4b9b..8873ab0 100644 --- a/ebay/finding.py +++ b/ebay/finding.py @@ -1,3 +1,9 @@ +""" +Implementation of eBay's "Finding API". + +http://developer.ebay.com/DevZone/finding/CallRef/index.html +""" + import urllib2 from lxml import etree @@ -23,6 +29,119 @@ def findItemsByKeywords(keywords, \ itemFilter=None, \ outputSelector=None, \ encoding="JSON"): + """ + Search for items by keywords. + + This call returns only little information about each item. However it + returns item IDs, that uniquely identify each item that is sold on eBay. + + Detailed information about items can be obtained with the functions + ``ebay.shopping.GetMultipleItems`` and ``ebay.shopping.GetSingleItem``. + These functions need item IDs as their input values. + + Parameters + ---------- + + keywords: str + Keywords for the search. + + affiliate: dict, None + Information that enable affiliates to receive their commission. + This information does not affect the search itself. For details + on the content of the dictionary see: + + http://developer.ebay.com/DevZone/finding/CallRef/types/Affiliate.html + + Example:: + + affiliate = {"networkId":"9", "trackingId":"1234567890"} + + buyerPostalCode: str, None + The postal code of the buyer. Used to limit distance between buyer + and seller. + + paginationInput: dict, None + Control the number of returned items per call. Also used to access + the next page (or lot) of returned items. + + The maximum number of items per call is 100, the maximum number of + pages is 100. Therefore the maximum number of results per search + is 10000. + + Example with 10 items per call, to access page 3 of the search + results:: + + paginationInput = {"entriesPerPage": "10", "pageNumber": "3"} + + sortOrder: str, None + The sort order of the results. Possible values: + + None, "EndTimeSoonest", "BestMatch", + "BidCountFewest", "BidCountMost", "CountryAscending", + "CountryDescending", "CurrentPriceHighest", "DistanceNearest", + "PricePlusShippingHighest", "PricePlusShippingLowest", "StartTimeNewest" + + aspectFilter: list of dict, None + Limit the results items with certain properties. These properties + are (physical) properties of the items themselves. + + The dictionaries have two keys: + * ``aspectName``: Name of a property, for example "Color". + * ``aspectValueName``: Value of that property, for example "Black". + + Example:: + + aspectFilter = [{"aspectName":"Color", "aspectValueName":"Black"}, + {"aspectName":"", "aspectValueName":""}] + + domainFilter: list of dicts, None + Limit the results to a certain product domain. + + Only a single dictionary can sensibly be put into the list, + because multiple domain filters are currently unsupported. + + The dictionary has only one key: + * "domainName": The name of the desired product domain. + + Example:: + + domainFilter = [{"domainName": "Other_MP3_Players"}] + + itemFilter: list of dicts, None + Limit the results to items with certain properties. These properties + are properties of the auction. + + The dictionaries can have various keys. Please consult eBay's + documentation for details: + http://developer.ebay.com/DevZone/finding/CallRef/types/ItemFilterType.html + + Example that sets a minimum and maximum price in Euro:: + + itemFilter = [{"name":"MaxPrice", "value":"100", + "paramName":"Currency", "paramValue":"EUR"}, + {"name":"MinPrice", "value":"50", + "paramName":"Currency", "paramValue":"EUR"}] + + outputSelector: list of str, None + Control the amount and type of information that is returned. + + Possible values are: + "AspectHistogram", "CategoryHistogram", "ConditionHistogram", + "GalleryInfo", "PictureURLLarge", "PictureURLSuperSize", "SellerInfo", + "StoreInfo", "UnitPriceInfo" + + Example:: + + outputSelector = ["StoreInfo", "SellerInfo", "AspectHistogram"] + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + http://developer.ebay.com/DevZone/finding/CallRef/findItemsByKeywords.html + """ root = etree.Element("findItemsByKeywords", xmlns="http://www.ebay.com/marketplace/search/v1/services") keywords_elem = etree.SubElement(root, "keywords") diff --git a/ebay/shopping.py b/ebay/shopping.py index d1fd7f0..8d15007 100644 --- a/ebay/shopping.py +++ b/ebay/shopping.py @@ -1,10 +1,47 @@ +""" +Implementation of eBay's "Shopping API". + +TODO: Implement all arguments. Many functions implement only a subset of the + arguments that are available in the eBay API call. + +http://developer.ebay.com/Devzone/shopping/docs/CallRef/index.html +""" + import requests from utils import get_config_store # Item Search def FindProducts(query, available_items, max_entries, encoding="JSON"): + """ + Return well known information about products (not actual items). + Especially return the ``ProductID`` of a certain product. + + To search for items look at the finding API. + + TODO: Implement all arguments. Depending on its arguments, this API call + can also return items. + + Parameters + ---------- + + query: str + Keywords for the search. + + available_items: bool + If true return only data for products that are available for purchase. + + max_entries: int + Maximal number of product entries that are returned. + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + http://developer.ebay.com/Devzone/shopping/docs/CallRef/FindProducts.html + """ user_param={'callname' : FindProducts.__name__, 'responseencoding' : encoding, 'QueryKeywords' : query, @@ -13,8 +50,54 @@ def FindProducts(query, available_items, max_entries, encoding="JSON"): response = get_response(user_param) return response.content + + +def FindHalfProducts(query=None, max_entries=None, product_type=None, + product_value=None, include_selector=None, + encoding="JSON"): + """ + Search ``half.com`` for information about products (not actual items). + + TODO: Calling this function with default arguments results in error. + + TODO: Bad argument names! Rename: + product_type --> product_id_type + product_value --> product_id -def FindHalfProducts(query=None, max_entries=None, product_type=None, product_value=None, include_selector=None, encoding="JSON"): + Parameters + ---------- + + query: str + Keywords for the search. + + max_entries: int + Maximal number of product entries that are returned. + + product_type: str + What type of product ID is in argument ``product_value``. + + Possible values are: + "Reference", "ISBN", "UPC", "EAN" + + product_value: str + Product ID to search for. The type of this ID is given in + argument ``product_type``. + + include_selector: str, None + Controls the amount of information that is returned. + Multiple values can be separated by commas. + + Possible values are: + None, "Items", "DomainHistogram" + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + http://developer.ebay.com/DevZone/shopping/docs/CallRef/FindHalfProducts.html + """ if product_type and product_value and include_selector: user_param = {'callname' : FindHalfProducts.__name__, 'responseencoding' : encoding, @@ -31,8 +114,34 @@ def FindHalfProducts(query=None, max_entries=None, product_type=None, product_va response = get_response(user_param) return response.content + # Item Data def GetSingleItem(item_id, include_selector=None, encoding="JSON"): + """ + Return information about a single item (listing) that can be bought. + + Parameters + ---------- + + item_id: str + Id of an item. Item IDs can be obtained with the Finding API. + + include_selector: str, None + Specify the amount of information that is returned. + If ``None`` a small number of default fields is returned. + Can consist of multiple words separated by commas. Possible values: + + Details, Description, TextDescription, ShippingCosts, ItemSpecifics, + Variations, Compatibility + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + http://developer.ebay.com/DevZone/shopping/docs/CallRef/GetSingleItem.html + """ user_param={'callname' : GetSingleItem.__name__, 'responseencoding' : encoding, 'ItemId' : item_id} @@ -42,16 +151,66 @@ def GetSingleItem(item_id, include_selector=None, encoding="JSON"): response = get_response(user_param) return response.content - + + def GetItemStatus(item_id, encoding="JSON"): + """ + Return information that frequently changes (for example: current price), + on an item. + + TODO: Make this call work with multiple item ids. + + Parameters + ---------- + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + developer.ebay.com/DevZone/shopping/docs/CallRef/GetItemStatus.html + """ user_param={'callname' : GetItemStatus.__name__, 'responseencoding' : encoding, 'ItemId' : item_id} response = get_response(user_param) return response.content + + +def GetShippingCosts(item_id, destination_country_code, destination_postal_code, + details, quantity_sold, encoding="JSON"): + """ + Return the shipping costs for an item. -def GetShippingCosts(item_id, destination_country_code, destination_postal_code, details, quantity_sold, encoding="JSON"): + Parameters + ---------- + + destination_country_code: str + Code that identifies the destination country. + For example: USA: "US", Germany: "DE" + http://developer.ebay.com/DevZone/shopping/docs/CallRef/types/CountryCodeType.html + + destination_postal_code: str + Postal code of the destination, country specific. + For example: "52068": eastern Aachen, Germany; + "10027": central New York City, USA + + details: bool + If true: return more detailed information. + + quantity_sold: int + Number of items that should be shipped together. + + encoding: str + Format of the returned data, possible values "JSON", "XML" + + See also + -------- + + http://developer.ebay.com/DevZone/shopping/docs/CallRef/GetShippingCosts.html + """ user_param={'callname' : GetShippingCosts.__name__, 'responseencoding' : encoding, 'ItemId' : item_id, @@ -62,8 +221,35 @@ def GetShippingCosts(item_id, destination_country_code, destination_postal_code, response = get_response(user_param) return response.content - + + def GetMultipleItems(item_id, include_selector=None, encoding="JSON"): + """ + Return details about multiple items (listings) on eBay. + + Parameters + ---------- + + item_id: str + Id of one item, or comma separated list of item IDs (without any + space characters). Item IDs can be obtained with the Finding API. + + include_selector: str, None + Specify the amount of information that is returned. + If ``None`` a small number of default fields is returned. + Can consist of multiple words separated by commas. Possible values: + + Details, Description, TextDescription, ShippingCosts, ItemSpecifics, + Variations, Compatibility + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + http://developer.ebay.com/Devzone/shopping/docs/CallRef/GetMultipleItems.html + """ user_param={'callname' : GetMultipleItems.__name__, 'responseencoding' : encoding, 'ItemId' : item_id} @@ -74,39 +260,109 @@ def GetMultipleItems(item_id, include_selector=None, encoding="JSON"): response = get_response(user_param) return response.content + # User Reputation def GetUserProfile(user_id, include_selector=None, encoding="JSON"): + """ + Return details about a user. + + Parameters + ---------- + + user_id: str + ID of user whose data is queried. + + include_selector: str + Control the amount of information returned. Possible values: + None, "Details", "FeedbackDetails", "FeedbackHistory" + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + http://developer.ebay.com/DevZone/shopping/docs/CallRef/GetUserProfile.html + """ user_param={'callname' : GetUserProfile.__name__, 'responseencoding' : encoding, 'UserID' : user_id} - + if include_selector: - user_param['IncludeSelector'] = include_selector + user_param['IncludeSelector'] = include_selector response = get_response(user_param) return response.content - + # eBay pop! def FindPopularSearches(query, category_id=None, encoding="JSON"): + """ + Return words that are frequently used by eBay users when they search + for items. + + Parameters + ---------- + + query: str + Keywords that express the user's interest. The returned search terms + are related to these keywords. + + category_id: str, None + One or more product category IDs. The returned search terms are + related to these categories. + Multiple IDs are specified as a comma separated list of IDs, with no + intervening spaces. + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + http://developer.ebay.com/DevZone/shopping/docs/CallRef/FindPopularSearches.html + """ user_param={'callname' : FindPopularSearches.__name__, 'responseencoding' : encoding, 'QueryKeywords' : query} if category_id: - user_param['CategoryID'] = category_id + user_param['CategoryID'] = category_id response = get_response(user_param) return response.content def FindPopularItems(query, category_id_exclude=None, encoding="JSON"): + """ + Return items that are currently popular on eBay. + + Parameters + ---------- + + query: str + Keywords to search for the item. + + category_id_exclude: str + One or more product category IDs. The returned items are **not** from + these categories. + Multiple IDs are specified as a comma separated list of IDs, with no + intervening spaces. + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + developer.ebay.com/DevZone/shopping/docs/CallRef/FindPopularItems.html + """ user_param={'callname' : FindPopularItems.__name__, 'responseencoding' : encoding, 'QueryKeywords' : query} if category_id_exclude: - user_param['CategoryIDExclude'] = category_id_exclude + user_param['CategoryIDExclude'] = category_id_exclude response = get_response(user_param) return response.content @@ -114,6 +370,40 @@ def FindPopularItems(query, category_id_exclude=None, encoding="JSON"): # Search: Bug in eBay documentation of Product Id: http://developer.ebay.com/devzone/shopping/docs/callref/FindReviewsAndGuides.html#Samples def FindReviewsandGuides(category_id=None, product_id=None, encoding="JSON"): + """ + Return URLs and descriptions of various types of guides. + + TODO: Function produces an error if called with default parameters. + + Parameters + ---------- + + category_id: str + A single category ID. Guides for that category are returned. + Product ID and category ID cannot be used together. + + Example category IDs: + * root category: "-1" + * iPods & MP3 Players: "73839" + * Digital Cameras: "29997" + * Jewelry & Watches: "281" + * Baby: "2984" + + product_id: + A single product ID. Reviews for that product are returned. + Product ID and category ID cannot be used together. + + Example product IDs: + iPod nano 5th gen. black: "77767691" + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + http://developer.ebay.com/Devzone/shopping/docs/CallRef/FindReviewsAndGuides.html + """ if category_id: user_param={'callname' : FindReviewsandGuides.__name__, 'responseencoding' : encoding, @@ -130,6 +420,37 @@ def FindReviewsandGuides(category_id=None, product_id=None, encoding="JSON"): # Utilities def GetCategoryInfo(category_id, include_selector=None, encoding="JSON"): + """ + Return information about product categories. + + Parameters + ---------- + + category_id: str + A single category ID. Guides for that category are returned. + Product ID and category ID cannot be used together. + + Example category IDs: + * root category: "-1" + * iPods & MP3 Players: "73839" + * Digital Cameras: "29997" + * Jewelry & Watches: "281" + * Baby: "2984" + + include_selector: str, None + Control the amount of data that is returned by this function. + + Possible values: + None, "ChildCategories" + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + http://developer.ebay.com/DevZone/shopping/docs/CallRef/GetCategoryInfo.html + """ if category_id: user_param={'callname' : GetCategoryInfo.__name__, 'responseencoding' : encoding, @@ -141,7 +462,22 @@ def GetCategoryInfo(category_id, include_selector=None, encoding="JSON"): response = get_response(user_param) return response.content + def GeteBayTime(encoding="JSON"): + """ + Return the official eBay time in UTC. + + Parameters + ---------- + + encoding: str + Format of the returned data, possible values: "JSON", "XML" + + See also + -------- + + http://developer.ebay.com/Devzone/shopping/docs/CallRef/GeteBayTime.html + """ user_param={'callname' : GeteBayTime.__name__, 'responseencoding' : encoding} diff --git a/tests/config-test1.ini.example b/tests/config-test1.ini.example new file mode 100644 index 0000000..3fed33d --- /dev/null +++ b/tests/config-test1.ini.example @@ -0,0 +1,69 @@ +#Configuration file template for the test suite. +# +# Copy the file: +# config-test1.ini.example --> config-test1.ini +# Then fill in information marked "ENTER_HERE". +# +# This template is used for the following tests: +# test_finding.py +# test_shopping.py + +[keys] +#Your secret Ebay access keys. +#Generate the keys on: developer.ebay.com +dev_name = ENTER_HERE +app_name = ENTER_HERE +cert_name = ENTER_HERE + +#Imgur key from: https://imgur.com/register/api_anon +imgur_key = NOT_NEEDED + +[call] +#siteid, global_id: Identify the eBay site with the items you want +# information about, e.g. +# US: siteid = 0, global_id = EBAY-US +# Germany: siteid = 77, global_id = EBAY-DE +# See: +# http://developer.ebay.com/devzone/finding/Concepts/SiteIDToGlobalID.html +# +#compatibility_level: The version used in header of the request. Tested values: +# compatibility_level = 785 +# compatibility_level = 849 +# See: +# http://developer.ebay.com/DevZone/XML/docs/HowTo/eBayWS/eBaySchemaVersioning.html +# http://developer.ebay.com/devzone/xml/docs/releasenotes.html +siteid = 0 +global_id = EBAY-US +compatibility_level = 849 + +[auth] +#You need production token here if you are using the Best Match, Client Alerts +#and Feedback API +token = NOT_NEEDED + +[research] +#The Research API credentials go here +Token = NOT_NEEDED +UserToken = NOT_NEEDED +DeveloperName = NOT_NEEDED + +[endpoints] +#Specify the endpoints here, make sure endpoints and the auth credentials + +#token match. +#To use the production system, remove `.sandbox` from the URL. However trading +#API uses different naming scheme, see at bottom. +# +finding = http://svcs.ebay.com/services/search/FindingService/v1 +shopping = http://open.api.ebay.com/shopping +# +feedback = https://svcs.ebay.com/FeedbackService +best_match = https://svcs.ebay.com/services/search/BestMatchItemDetailsService/v1 +client_alerts = http://clientalerts.sandbox.ebay.com/ws/ecasvc/ClientAlerts +merchandising = http://svcs.sandbox.ebay.com/MerchandisingService +platform_notifications = https://api.sandbox.ebay.com/wsapi +product = http://svcs.sandbox.ebay.com/services/marketplacecatalog/ProductService/v1 +research = http://api.researchadvanced.com/ +resolution_case_management = https://svcs.sandbox.ebay.com/services/resolution/ResolutionCaseManagementService/v1 +trading = https://api.sandbox.ebay.com/wsapi +# for production use: +#trading = https://api.ebay.com/ws/api.dll diff --git a/tests/test_alternative_config.py b/tests/test_alternative_config.py index a91e01f..8aeadac 100644 --- a/tests/test_alternative_config.py +++ b/tests/test_alternative_config.py @@ -1,95 +1,123 @@ ''' Tests for functionality to use arbitrary configuration files. -The standard configuration file is part of the library itself. This is -acceptable for web applications, but impossible for interactive programs. -For interactive program each user needs a separate configuration file. + +Configuration +============= + +This test script uses a custom configuration file ``config-test1.ini`` +inside directory ``tests/``. It can be most easily created, by first copying:: + + mv config-test1.ini.example config-test1.ini + +Then filling in user and application keys into ``config-test1.ini``. + + +Operation +========= The test assumes the following directory layout:: python-ebay/ ebay/ - config.ini + config.ini (optional, does'nt need to exist) tests/ + config-test1.ini + config-test1.ini.example test_alternative_config.py -The test script creates a copy of the initialization file: +The test script creates a copy of the configuration file ``config.ini``: ``config.ini.bak`` It can be used to restore the initialization file in case the original configuration file is lost. + +The test itself never uses ``ebay/config.ini``, it always uses +``tests/config-test1.ini`` for its configuration. ''' -from os import system -from os.path import join, dirname, abspath +import os +from os import path +import shutil import unittest -from lxml import etree +from lxml import objectify from ebay.utils import set_config_file -from ebay.finding import findItemsByKeywords +from ebay.shopping import GeteBayTime def relative(*paths): "Create file paths that are relative to the location of this file." - return abspath(join(dirname(abspath(__file__)), *paths)) + return path.abspath(path.join(path.dirname(__file__), *paths)) -#Arguments for `ebay.finding.findItemsByKeywords` -keywords = "ipod" -paginationInput = {"entriesPerPage": "5", "pageNumber" : "1"} -encoding = "XML" #File paths std_conf = relative("../ebay/config.ini") std_conf_back = relative("../ebay/config.ini.bak") -alt_conf = relative("../config.apikey") +alt_conf = relative("config-test1.ini") class TestAlternativeConfig(unittest.TestCase): def test_alternative_config(self): """ - Move configuration file ``config.ini`` to nonstandard location (one level - up in directory hierarchy) and try to use the library. + Ensure the standard configuration file ``ebay/config.ini`` does not + exist, and try to use the library with the alternative configuration + file ``tests/config-test1.ini`. """ - #Backup the initialization file - system("cp " + std_conf + " " + std_conf_back) - #Move initialization file to nonstandard location - system("mv " + std_conf + " " + alt_conf) + #Copy original/standard configuration file to backup location + shutil.copy(std_conf, std_conf_back) + #Remove the standard configuration file + os.remove(std_conf) + #Look where the initialization files really are - system("ls " + std_conf) - system("ls " + std_conf_back) - system("ls " + alt_conf) + os.system("ls -l ../ebay/*.ini") #Should not exist + os.system("ls -l ../tests/*.ini") #Set alternative initialization file set_config_file(alt_conf) #Use the library and test if it works - result = findItemsByKeywords(keywords=keywords, - paginationInput=paginationInput, - encoding=encoding) - root = etree.fromstring(result) - ack = root.find("{http://www.ebay.com/marketplace/search/v1/services}ack").text + result = GeteBayTime(encoding="XML") + root = objectify.fromstring(result) + ack = root.Ack.text self.assertEqual(ack, "Success") + ebay_time = root.Timestamp.text + print ebay_time + self.assertTrue(len(ebay_time) > 10, + "eBay time is a somewhat long string.") - #Move initialization file back to original location - system("mv " + alt_conf + " " + std_conf) + #Restore the original configuration file from the backup. + shutil.copy(std_conf_back, std_conf) def test_regular_config(self): "Test the library with the regular configuration file." + #Copy original/standard configuration file to backup location + shutil.copy(std_conf, std_conf_back) + #Remove the standard configuration file + os.remove(std_conf) + + #Move the alternative configuration file to the standard location, + #because we know that the alternative configuration file works. + shutil.copy(alt_conf, std_conf) + #Look where the initialization files really are - system("ls " + std_conf) - system("ls " + std_conf_back) - system("ls " + alt_conf) #should not exist + os.system("ls -l ../ebay/*.ini") + os.system("ls -l ../tests/*.ini") #Use the library and test if it works - result = findItemsByKeywords(keywords=keywords, - paginationInput=paginationInput, - encoding=encoding) - root = etree.fromstring(result) - ack = root.find("{http://www.ebay.com/marketplace/search/v1/services}ack").text + result = GeteBayTime(encoding="XML") + root = objectify.fromstring(result) + ack = root.Ack.text self.assertEqual(ack, "Success") + ebay_time = root.Timestamp.text + print ebay_time + self.assertTrue(len(ebay_time) > 10, + "eBay time is a somewhat long string.") + #Restore the original configuration file from the backup. + shutil.copy(std_conf_back, std_conf) if __name__ == "__main__": diff --git a/tests/test_finding.py b/tests/test_finding.py index 17e8760..431ae01 100644 --- a/tests/test_finding.py +++ b/tests/test_finding.py @@ -3,26 +3,47 @@ These tests are designed to work on the production system, not on Ebay's sandbox. + +This test uses a custom configuration file ``config-test1.ini`` inside the +``tests/`` directory. It can be most easily created, by first copying:: + + mv config-test1.ini.example config-test1.ini + +Then filling in user and application keys into ``config-test1.ini``. """ + +import os.path as path import unittest from lxml import etree +from ebay.utils import set_config_file from ebay.finding import (getSearchKeywordsRecommendation, getHistograms, findItemsAdvanced, findItemsByCategory, findItemsByKeywords, findItemsByProduct, findItemsIneBayStores) +def relative(*path_fragments): + 'Create a file path that is relative to the location of this file.' + return path.abspath(path.join(path.dirname(__file__), *path_fragments)) + +#Tell python-ebay to use the custom configuration file +set_config_file(relative("config-test1.ini")) + + +#Definitions of the various arguments of the finding API. encoding = "XML" #default "JSON": Output encoding keywords = "ipod" #Get category IDs with function: `ebay.shopping.GetCategoryInfo` categoryId = "73839" #iPods & MP3 Players productId = "77767691" #iPod nano 5th gen. Black. Each product has unique ID. -storeName = "Fab Finds 4 U" -#This information is encoded in URLs so the affiliate can get his commission. +storeName = "Fab Finds 4 U" #A big store that won't go away soon. +#This information is encoded in URLs so the affiliates can get their commission. affiliate = {"networkId":"9", "trackingId":"1234567890"} buyerPostalCode = "10027" #central New York City, USA -paginationInput = {"entriesPerPage": "10", "pageNumber" : "1"} +#Set number of results per call (here 10). Get additional results by +#increasing page number. +paginationInput = {"entriesPerPage": "10", "pageNumber": "1"} #http://developer.ebay.com/DevZone/finding/CallRef/types/ItemFilterType.html itemFilter = [{"name":"MaxPrice", "value":"100", "paramName":"Currency", "paramValue":"EUR"}, @@ -30,12 +51,12 @@ "paramName":"Currency", "paramValue":"EUR"}] #http://developer.ebay.com/DevZone/finding/CallRef/findItemsByKeywords.html#Request.sortOrder sortOrder = "EndTimeSoonest" -aspectFilter = [{"aspectName":"Color", "aspectValueName":"Black"}, - {"aspectName":"", "aspectValueName":""}] +aspectFilter = [{"aspectName":"Color", "aspectValueName":"Black"}, + {"aspectName":"", "aspectValueName":""}] #Multiple domain filters are currently unsupported -domainFilter = [{"domainName":"Other_MP3_Players"}] +domainFilter = [{"domainName": "Other_MP3_Players"}] #http://developer.ebay.com/DevZone/finding/CallRef/types/OutputSelectorType.html -outputSelector =["StoreInfo", "SellerInfo", "AspectHistogram"] +outputSelector = ["StoreInfo", "SellerInfo", "AspectHistogram"] class TestFindingApi(unittest.TestCase): diff --git a/tests/test_shopping.py b/tests/test_shopping.py index 8f787ba..bf1723e 100644 --- a/tests/test_shopping.py +++ b/tests/test_shopping.py @@ -1,81 +1,319 @@ +""" +Tests for package ``ebay.shopping``. + + +Configuration +============= + +This test script uses a custom configuration file ``config-test1.ini`` +inside directory ``tests/``. It can be most easily created, by first copying:: + + mv config-test1.ini.example config-test1.ini + +Then filling in user and application keys into ``config-test1.ini``. + + +These tests are designed to work on the production system, not on Ebay's +sand box. + +Some tests also call functions from the Finding API. Therefore a broken Finding +API will also cause breakage here. +""" + +import os.path as path import unittest from lxml import objectify -from ebay.shopping import * +from ebay.utils import set_config_file +from ebay.finding import findItemsByKeywords +from ebay.shopping import (FindProducts, FindHalfProducts, GetSingleItem, + GetItemStatus, GetShippingCosts, GetMultipleItems, + GetUserProfile, FindPopularSearches, + FindPopularItems, FindReviewsandGuides, + GetCategoryInfo, GeteBayTime) + + +def find_item_ids(keywords): + "Return a list of, at most 10, valid item IDs." + result = findItemsByKeywords(keywords, + paginationInput = {"entriesPerPage": "10", + "pageNumber" : "1"}, + encoding="XML") +# print result + root = objectify.fromstring(result) + ack = root.ack.text + assert ack == "Success" or ack == "Warning" + + item_ids = [] + for itemi in root.searchResult.item: + item_ids.append(itemi.itemId.text) +# print item_ids + return item_ids + +def relative(*path_fragments): + 'Create a file path that is relative to the location of this file.' + return path.abspath(path.join(path.dirname(__file__), *path_fragments)) + + +#Tell python-ebay to use the custom configuration file +set_config_file(relative("config-test1.ini")) + class TestShoppingApi(unittest.TestCase): def test_FindProducts(self): - result = FindProducts() + """ + http://developer.ebay.com/Devzone/shopping/docs/CallRef/FindProducts.html + """ + result = FindProducts(query="ipod", + available_items=True, + max_entries=10, + encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text + ack = root.Ack.text self.assertEqual(ack, "Success") + + product = root.Product + self.assertTrue(len(product) == 10, + "``product`` container has unexpected length.") + product_id_0 = product[0].ProductID.text + self.assertTrue(int(product_id_0) != 0, + "``product_id_0`` must be string representation " + "of an integer.") def test_FindHalfProducts(self): - result = FindHalfProducts() + """ + TODO: Test call with all keywords. + + http://developer.ebay.com/DevZone/shopping/docs/CallRef/FindHalfProducts.html + """ + result = FindHalfProducts(query="ipod", + max_entries=10, + product_type=None, + product_value=None, + include_selector=None, + encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text + ack = root.Ack.text self.assertEqual(ack, "Success") + + product = root.Products.Product + self.assertTrue(len(product) == 10, + "``product`` container has unexpected length.") + product_id_0 = product[0].ProductID.text + self.assertTrue(int(product_id_0) != 0, + "``product_id_0`` must be string representation " + "of an integer.") def test_GetSingleItem(self): - result = GetSingleItem() + """ + http://developer.ebay.com/DevZone/shopping/docs/CallRef/GetSingleItem.html + """ + item_ids = find_item_ids("ipod") + + result = GetSingleItem(item_ids[0], + include_selector="ShippingCosts", + encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text - self.assertEqual(ack, "Success") + ack = root.Ack.text + #There are regularly warnings like: "Cannot calculate shipping cost." + self.assertTrue(ack in ["Success", "Warning"]) + title = root.Item.Title.text + price = root.Item.ConvertedCurrentPrice.text + self.assertTrue(len(title) > 0, + "The title is most probably a string with length > 0.") + self.assertTrue(float(price) > 0, + "The price is the string representation or a float. " + "It should be > 0.") + def test_GetItemStatus(self): - result = GetSingleItem() + """ + developer.ebay.com/DevZone/shopping/docs/CallRef/GetItemStatus.html + """ + item_ids = find_item_ids("ipod") + + result = GetItemStatus(item_id=item_ids[0], + encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text + ack = root.Ack.text self.assertEqual(ack, "Success") - + + time_left = root.Item[0].TimeLeft.text + price = root.Item[0].ConvertedCurrentPrice.text + self.assertTrue(len(time_left) > 0, + "The time left is most probably a string with length > 0.") + self.assertTrue(float(price) > 0, + "The price is the string representation or a float. " + "It should be > 0.") + def test_GetShippingCosts(self): - result = GetShippingCosts() + """ + http://developer.ebay.com/DevZone/shopping/docs/CallRef/GetShippingCosts.html + """ + item_ids = find_item_ids("ipod") + + result = GetShippingCosts(item_id=item_ids[0], + destination_country_code="US", + destination_postal_code="10027", + details=True, + quantity_sold=1, + encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text + ack = root.Ack.text self.assertEqual(ack, "Success") + + shcost = root.ShippingCostSummary.ShippingServiceCost.text + self.assertTrue(float(shcost) >= 0, + "Shipping cost must be a string that represents a " + "float, and be positive.") def test_GetMultipleItems(self): - result = GetMultipleItems() + """ + http://developer.ebay.com/Devzone/shopping/docs/CallRef/GetMultipleItems.html + """ + item_ids = find_item_ids("ipod") + item_ids_str = ",".join(item_ids) + + result = GetMultipleItems(item_id=item_ids_str, + include_selector="ShippingCosts", + encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text - self.assertEqual(ack, "Success") + ack = root.Ack.text + #There are regularly warnings like: "Cannot calculate shipping cost." + self.assertTrue(ack in ["Success", "Warning"]) + + items = root.Item + for itemi in items: + title = itemi.Title.text + price = itemi.ConvertedCurrentPrice.text + self.assertTrue(len(title) > 0, + "The title is most probably a string with length > 0.") + self.assertTrue(float(price) > 0, + "The price is the string representation od a float. " + "It should be > 0.") def test_GetUserProfile(self): - result = GetUserProfile() + """ + http://developer.ebay.com/DevZone/shopping/docs/CallRef/GetUserProfile.html + """ + result = GetUserProfile(user_id= + #ID of deleted user + "bfafcc239c92d6404cd33a13648076d324750574", + include_selector="Details", + encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text + ack = root.Ack.text self.assertEqual(ack, "Success") - + + score = root.User.FeedbackScore.text + status = root.User.Status.text + self.assertTrue(int(score) > 600, + "Score must represent an integer. " + "This user's score is above 600") + self.assertTrue(status == "Deleted", "This is a deleted account.") + + def test_FindPopularSearches(self): - result = FindPopularSearches() + """ + http://developer.ebay.com/DevZone/shopping/docs/CallRef/FindPopularSearches.html + """ + result = FindPopularSearches(query="ipod,car", + category_id=None, + encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text + ack = root.Ack.text self.assertEqual(ack, "Success") + query = root.PopularSearchResult[0].QueryKeywords.text + alternative = root.PopularSearchResult[0].AlternativeSearches.text + related = root.PopularSearchResult[0].RelatedSearches.text + self.assertTrue(len(query)>1) + self.assertTrue(len(alternative)>1) + self.assertTrue(len(related)>1) + + def test_FindPopularItems(self): - result = FindPopularItems() + """ + developer.ebay.com/DevZone/shopping/docs/CallRef/FindPopularItems.html + """ + result = FindPopularItems(query="ipod,car", + category_id_exclude=None, + encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text + ack = root.Ack.text self.assertEqual(ack, "Success") + items = root.ItemArray.Item + price = items[0].ConvertedCurrentPrice.text + title = items[0].Title.text + self.assertTrue(len(items) > 1, "eBay returns multiple popular items.") + self.assertTrue(float(price) > 0, "Price is a float.") + self.assertTrue(len(title) > 10, "``title`` is a somewhat long string.") + + def test_FindReviewsandGuides(self): - result = FindReviewsandGuides() + """ + http://developer.ebay.com/Devzone/shopping/docs/CallRef/FindReviewsAndGuides.html + """ + #TODO: ``FindReviewsandGuides`` always returns with error: + # "Service unavailable." + return + + result = FindReviewsandGuides(category_id="29997", + product_id=None, + encoding="XML") + print result root = objectify.fromstring(result) - ack = root.ack.text + ack = root.Ack.text self.assertEqual(ack, "Success") def test_GetCategoryInfo(self): - result = GetCategoryInfo() + """ + http://developer.ebay.com/DevZone/shopping/docs/CallRef/GetCategoryInfo.html + """ + result = GetCategoryInfo(category_id="73839", #iPods & MP3 Players + include_selector=None, + encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text + ack = root.Ack.text self.assertEqual(ack, "Success") + cat_id = root.CategoryArray.Category.CategoryID.text + cat_name = root.CategoryArray.Category.CategoryName.text + self.assertTrue(cat_id == "73839", "Must be same ID as in query.") + self.assertTrue(cat_name.startswith("iPod"), + "Category name is: 'iPods & MP3 Player'") + + def test_GeteBayTime(self): - result = GeteBayTime() + """ + http://developer.ebay.com/Devzone/shopping/docs/CallRef/GeteBayTime.html + """ + result = GeteBayTime(encoding="XML") +# print result root = objectify.fromstring(result) - ack = root.ack.text + ack = root.Ack.text self.assertEqual(ack, "Success") - + + ebay_time = root.Timestamp.text + print ebay_time + self.assertTrue(len(ebay_time) > 10, + "eBay time is a somewhat long string.") + if __name__ == '__main__': +# #Run single test manually. +# t = TestShoppingApi("test_GetCategoryInfo") +# t.test_GetCategoryInfo() + unittest.main()