ebay-testing/ebay.py

461 lines
19 KiB
Python

# Imports first
# This is the sdk found at https://github.com/timotheus/ebaysdk-python
from ebaysdk.trading import Connection as Trading
from ebaysdk.exception import ConnectionError, ConnectionResponseError
# Parallel requires Gevent and Grequests modules to be available on the server (requires at least 2.7.5)
from ebaysdk.parallel import Parallel
import datetime
import csv
# Application Settings
app_id = ''
dev_id = ''
crt_id = ''
domain = 'api.sandbox.ebay.com'
# User Identification
usr_token = ''
# CSV file data
csv_path = ''
csv_delimiter = ','
csv_quote = '\''
# We can probably put the DateRange functions into a class
# Creates a dateRange list for use with glue
def setDateRange(days=None, start=None, stop=None, rangeType=None):
# Default to searching for the current 24 hours, minus 1ms
# Formats the date presented to it according to what we need, defaults to today
def checkDate(theDate=None):
if theDate != None:
if type(theDate) == type(datetime.datetime.today()):
return theDate
elif type(theDate) == type(''):
try:
return datetime.datetime.strptime(theDate, '%Y-%m-%d')
except ValueError:
return datetime.datetime.today()
else:
return datetime.datetime.today()
else:
return datetime.datetime.today()
# Set our days to an int, defaults to 0
try:
days = int(days)
except (TypeError, ValueError):
days = 0
# Set our rangeType to str, defaults to 'end'
if rangeType != None:
rangeType = str(rangeType)
else:
rangeType = 'end'
# Set the days argument to search forward/backward more than one day
delta = datetime.timedelta(days)
start_time = checkDate(start)
if stop != None:
end_time = checkDate(stop)
else:
end_time = start_time + delta
# Convert our dates into a format that the api can recognize (ISO 8601)
start_time = start_time.strftime("%Y-%m-%dT00:00:00.000Z")
# Force the future to be the absolute end of the day
end_time = end_time.strftime("%Y-%m-%dT23:59:59.999Z")
# if the end_time is in the past, reverse order (EG, the delta is a negative number)
if end_time < start_time:
start_time_old = start_time
start_time = end_time
end_time = start_time_old
return {'from': start_time, 'to': end_time, 'type': rangeType}
# We call this a couple times, so it gets its own function
def switchDateRange(list=None, range=None):
# Switch statement to check which type of dateRange to search by
# Will always return a valid dateRange for use as various itemArgs
# Defaults to a dateRange ending today
if (range == None) | (isinstance(range, dict) != True):
# Sets the default range (items that are ending today)
range = setDateRange()
if (list == None) | (isinstance(list, dict) != True):
list = {}
# This can be start/mod/end
def rangeType(condition=None):
# No condition to check, defaults to End
if condition == None:
return 'End'
# Cast the condition to a string
condition = str(condition)
switch = {
'start': 'Start', # Covers StartTimeFrom and StartTimeTo - Items that started in this date range
'mod': 'Mod', # Covers ModTimeFrom and ModTimeTo - Items modified in this date range
'end': 'End' # Covers EndTimeFrom and EndTimeTo - Items that end within this date range
}
# Return the approiate text or End if no ID matches
return switch.get(condition, 'End')
# Override the dates in sellerList with values from dateRange, if provided
if ('from' in range) & ('type' in range):
# Remove the list of keys from sellerList - We can only have one search range
for o in ['EndTimeFrom','ModTimeFrom','StartTimeFrom']:
try:
del list[o]
except KeyError:
continue
list[rangeType(range['type'])+'TimeFrom'] = range['from']
if ('to' in range) & ('type' in range):
# Remove the list of keys from sellerList - We can only have one search range
for o in ['EndTimeTo','ModTimeTo','StartTimeTo']:
try:
del list[o]
except KeyError:
continue
list[rangeType(range['type'])+'TimeTo'] = range['to']
return list
# Here are the error codes that we currently have
# None - No events have happened yet
# 0 - Everything is fine
# 1 - The API variable is either unset, or is not a valid connection to the ebay API
# 2 - The required list is either unset, or is not the required type - Usually dict, but can be a list of dicts
# 3 - EndTimeFrom or EndTimeTo were not set in the list - getSeller only
# 4 - Relates to 3, but specifically refers to a dateRange not being available for glue
# 5 - No data (useful) returned by API - A field of data we need is None
# minor logic checking - We don't want to get trackbacks for silly reasons
# getSeller gets a list of our itemIDs
def getSeller(api=None, list=None):
# api is the connection to the Trading API
# list is a dict containing required information for ebay to search for (http://developer.ebay.com/DevZone/XML/docs/Reference/ebay/GetSellerEvents.html)
# - The datapoints that MUST be defined are EndTimeFrom and EndTimeTo - These select the dateRange that items end on ebay
# Set our error conditions
res = { 'error': { 'code': None, 'msg': None, 'fnc': 'getSeller' }, 'apiResponse': {} }
if api == None:
res['error']['code'] = '1'
res['error']['msg'] = 'api is not set'
return res
if (list == None) | (isinstance(list, dict) != True):
res['error']['code'] = '2'
res['error']['msg'] = 'list doesn\'t exist or is of wrong type, must be dict'
return res
if (('EndTimeFrom' not in list) | ('EndTimeTo' not in list)) & (('StartTimeFrom' not in list) | ('StartTimeTo' not in list)):
res['error']['code'] = '3'
res['error']['msg'] = 'either "StartTime" or "EndTime" is not set in list'
return res
res['apiResponse'] = api.execute('GetSellerEvents', list).dict()
# Verify that the search returned information
if res['apiResponse']['ItemArray'] != None:
# Check if the itemArray is setup the way we want it (list containing one or more dicts)
try:
for k in res['apiResponse']['ItemArray']['Item']:
itemid = k['ItemID']
except TypeError:
# The itemArray is not, force it to be then
res['apiResponse'] = [ res['apiResponse']['ItemArray']['Item'] ]
else:
# No need to encase the list in another list
res['apiResponse'] = res['apiResponse']['ItemArray']['Item']
# Yay no errors
if res['error']['code'] == None:
res['error']['code'] = '0'
else:
# drop the response as it contains no useful information anymore
res['apiResponse'] = {}
res['error']['code'] = '5'
res['error']['msg'] = 'no items found - maybe the dateRange is too narrow?'
return res
# getItems uses the list of ItemIDs provided by getSeller to get specific information about each ItemID
def getItems(api=None, itemList=None, itemArgs=None):
# api is the connection to the Trading API
# itemList is a dict containing ItemIDs and other info as a result of getSeller
# itemArgs is an optional dict that contains extra details to refine the search returned by ebay (http://developer.ebay.com/DevZone/XML/docs/Reference/ebay/GetItem.html)
# - The two required datapoints in itemArgs are IncludeItemSpecifics and ItemID, both are defined below in the loop
# Set our error conditions
res = { 'error': { 'code': None, 'msg': None, 'fnc': 'getItems' }, 'apiResponse': {} }
if api == None:
res['error']['code'] = '1'
res['error']['msg'] = 'api is not set'
return res
if (itemList == None) | (type(itemList) != type([])):
res['error']['code'] = '2'
res['error']['msg'] = 'itemList doesn\'t exist or is of wrong type, must be list containing one or more dicts'
return res
if isinstance(itemArgs, dict) != True:
res['error']['code'] = '2'
res['error']['msg'] = 'itemArgs doesn\'t exist or is of wrong type, must be dict'
return res
# For each ItemID
for k in itemList:
# Extra arguments that should be applied anyways
itemArgs['IncludeItemSpecifics'] = 'True'
# Search for specific ItemID
itemArgs['ItemID'] = k['ItemID']
# If the Item is not active, we don't want it - Prevents sending extra API resquests for data we don't want
if(k['SellingStatus']['ListingStatus'] != 'Active'):
continue
res['apiResponse'][k['ItemID']] = api.execute('GetItem', itemArgs).dict()
# We want the error code to only be changed on the first successful iteration
if res['error']['code'] == None:
res['error']['code'] = '0'
# After the loop is complete, return the whole res
return res
# Use the modTime range and only return data for items that have been modified in the timerange
# Args we want are the modTime and NewItemFilter=True to get only items that have changed in this timerange
def checkRevisedItems(api=None, itemArgs=None, dateRange=None):
# api is the connection to the Trading API
# itemList is a list containing itemIDs to check for updates
# itemArgs is an optional dict that contains extra details to refine the search returned by ebay
res = { 'error': { 'code': None, 'msg': None, 'fnc': 'checkRevisedItems' }, 'apiResponse': { 'itemIDs': [] } }
if api == None:
res['error']['code'] = '1'
res['error']['msg'] = 'api is not set'
return res
if (itemArgs == None) | (isinstance(itemArgs, dict) != True):
res['error']['code'] = '2'
res['error']['msg'] = 'itemArgs doesn\'t exist or is of wrong type, must be dict'
return res
if (dateRange == None) | (isinstance(dateRange, dict) != True):
dateRange = setDateRange()
itemArgs['NewItemFilter'] = 'True'
# Switch statement to select the proper dateRange
itemArgs = switchDateRange(itemArgs, dateRange)
response = api.execute('GetSellerEvents', itemArgs).dict()
try:
items = response['ItemArray']['Item']
# Store the ItemIDs that need to be updated again with getItem
for i in items:
res['apiResponse']['itemIDs'].append(i['ItemID'])
except TypeError:
res['apiResponse'] = {'code': '1', 'msg': 'No items were revised in the selected dateRange'}
response = None
# No Errors found
if res['error']['code'] == None:
res['error']['code'] = '0'
return res
# storeItems uses the dict of Items provided by getItems and stores the information we want
def storeItems(itemList=None):
# itemList is the apiRequest value presented as a result of getItems
# - This contains every item that matches getItems criteria with the ItemID as the key of further dicts
# Set our error conditions
res = { 'error': { 'code': None, 'msg': None, 'fnc': 'storeItems' }, 'apiResponse': {} }
if (itemList == None) | (isinstance(itemList, dict) != True):
res['error']['code'] = '2'
res['error']['msg'] = 'itemList doesn\'t exist or is of wrong type, must be dict'
return res
# Now that we've stored all the data in a really big dictionary, lets pull only the information we want out of it
for k in itemList:
res['apiResponse'][k] = { 'price': {}, 'condition': {}, 'quantity': {} }
condition = '0'
# Switch statement - Sets condition.msg based on conditionID - based on table from http://developer.ebay.com/devzone/finding/callref/Enums/conditionIdList.html
def conCheck(condition):
# Cast the condition to a string
condition = str(condition)
switch = {
'1000': 'New',
'1500': 'New Other',
'1750': 'New with defects',
'2000': 'Manufacturer Refurbished',
'2500': 'Seller Refurbished',
'3000': 'Used',
'4000': 'Used/Very Good Condition',
'5000': 'Used/Good Condition',
'6000': 'Used/Acceptable Condition',
'7000': 'For Parts/Not Working'
}
# Return the approiate text or N/A if no ID matches
return switch.get(condition, 'N/A')
res['apiResponse'][k]['title'] = itemList[k]['Item']['Title']
if 'ConditionID' in itemList[k]['Item']:
# Override the condition defined at the start of the loop
condition = itemList[k]['Item']['ConditionID']
if 'SellingStatus' in itemList[k]['Item']:
# Store current price + currency the price is in
res['apiResponse'][k]['price']['cur'] = itemList[k]['Item']['SellingStatus']['CurrentPrice']['_currencyID']
res['apiResponse'][k]['price']['val'] = itemList[k]['Item']['SellingStatus']['CurrentPrice']['value']
# Store the quantity - Subtract sold from total to get current value
res['apiResponse'][k]['quantity']['sold'] = itemList[k]['Item']['SellingStatus']['QuantitySold']
res['apiResponse'][k]['quantity']['total'] = itemList[k]['Item']['Quantity']
else:
res['apiRequest'][k]['price']['cur'] = None
res['apiRequest'][k]['price']['val'] = None
res['apiRequest'][k]['quantity']['sold'] = None
res['apiRequest'][k]['quantity']['total'] = None
# Get the item specifics - Special fields pertaining to the item, such as manufacturer and part number
if 'ItemSpecifics' in itemList[k]['Item']:
# Try to for loop our data, if that fails, assume it's a dict with only one ItemSpecific
try:
for i in itemList[k]['Item']['ItemSpecifics']['NameValueList']:
name = i['Name']
if(name == 'Brand'):
res['apiResponse'][k]['mfg'] = i['Value']
if(name == 'MPN'):
res['apiResponse'][k]['mpn'] = i['Value']
except TypeError:
if itemList[k]['Item']['ItemSpecifics']['NameValueList']['Name'] == 'Brand':
res['apiResponse'][k]['mfg'] = itemList[k]['Item']['ItemSpecifics']['NameValueList']['Value']
res['apiResponse'][k]['mpn'] = None
elif itemList[k]['Item']['ItemSpecifics']['NameValueList']['Name'] == 'MPN':
res['apiResponse'][k]['mpn'] = itemList[k]['Item']['ItemSpecifics']['NameValueList']['Value']
res['apiResponse'][k]['mfg'] = None
else:
res['apiResponse'][k]['mpn'] = None
res['apiResponse'][k]['mfg'] = None
# Store the condition
res['apiResponse'][k]['condition']['code'] = condition
res['apiResponse'][k]['condition']['msg'] = conCheck(condition)
# We want the error code to only be changed on the first successful iteration
if res['error']['code'] == None:
res['error']['code'] = '0'
# After the loop is complete, return the whole res
return res
# Glue logic to run all the functions above properly
def glue(api=None, sellerList=None, dateRange=None):
# api is the connection to the api, this is passed to the functions as called
# sellerList is the options that you can present ebay for searching (http://developer.ebay.com/DevZone/XML/docs/Reference/ebay/GetSellerEvents.html)
# dateRange overrides EndTimeFrom and EndTimeTo from sellerList for ease of passing these required data with nothing else
# Set our error conditions
res = { 'error': { 'code': '0', 'msg': None, 'fnc': 'glue' }, 'apiResponse': {} }
if api == None:
res['error']['code'] = '1'
res['error']['msg'] = 'api is not set'
return res
if (sellerList == None) | (isinstance(sellerList, dict) != True):
res['error']['code'] = '2'
res['error']['msg'] = 'itemList doesn\'t exist or is of wrong type, must be dict'
return res
if (dateRange == None) | (isinstance(dateRange, dict) != True):
dateRange = setDateRange()
# Switch statement to select the proper dateRange
sellerList = switchDateRange(sellerList, dateRange)
seller = getSeller(api, sellerList)
if seller['error']['code'] == '0':
sellerList = seller['apiResponse']
seller = None # Unset any data we no-longer need - Saves on memory
items = getItems(api, sellerList, {})
if items['error']['code'] == '0':
sellerList = items['apiResponse']
items = storeItems(sellerList)
storedItems = items['apiResponse']
items = None
return storedItems
else:
res['error'] = items['error']
return res
else:
res['error'] = seller['error']
return res
try:
p = Parallel()
api = Trading(domain=domain, appid=app_id, devid=dev_id, certid=crt_id, token=usr_token, config_file=None, debug=False, parellel=p)
# Example usage, returns a dict containing all items of interst (based on the functions above)
# To import a whole lot of data from ebay, we need to pull in dateRange increments of ~20 days
# Easiest way would be to have a starting date and then look forward 20 days with the delta
itemData = glue(api=api, sellerList={}, dateRange={'from':'2016-03-01T00:00:00.000Z', 'to': '2016-03-28T23:59:59.999Z', 'type':'start'})
itemlist = []
# Write a CSV file containing some of the data we're interested in
with open(csv_path, 'wb') as csvfile:
fieldnames = ['Part Number', 'Manufacturer', 'Condition', 'Price', 'Quantity', 'Description']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=csv_delimiter, quotechar=csv_quote, quoting=csv.QUOTE_MINIMAL)
writer.writerow({'Part Number':'Part Number', 'Manufacturer':'Manufacturer', 'Condition':'Condition', 'Price':'Price', 'Quantity':'Quantity', 'Description':'Description'})
for i in itemData:
def keyCheck(key):
try:
data = itemData[i][key]
return data
except KeyError:
data = 'N/a'
return data
try:
price = itemData[i]['price']['val']
except KeyError:
price = 'N/a'
try:
quantity = str(int(itemData[i]['quantity']['total'])-int(itemData[i]['quantity']['sold']))
except KeyError:
quantity = 'N/a'
data = {
'Part Number': keyCheck('mpn'),
'Manufacturer': keyCheck('mfg'),
'Condition': itemData[i]['condition']['msg'],
'Price': price,
'Quantity': quantity,
'Description': keyCheck('title').encode('utf-8')
}
writer.writerow(data)
# Store the itemIDs so that we can use them to check which ones were modified
itemlist.append(i)
except ConnectionError as e:
print(e)