Skip to content

Instantly share code, notes, and snippets.

@bitoffdev
Last active January 2, 2018 12:47
Show Gist options
  • Save bitoffdev/d71e567167f5a52e1f11 to your computer and use it in GitHub Desktop.
Save bitoffdev/d71e567167f5a52e1f11 to your computer and use it in GitHub Desktop.
import requests, datetime, pytz
class AssetStoreException (Exception):
pass
class AssetStoreClient(object):
LOGIN_URL = 'https://publisher.assetstore.unity3d.com/login'
LOGOUT_URL = 'https://publisher.assetstore.unity3d.com/logout'
SALES_URL = 'https://publisher.assetstore.unity3d.com/sales.html'
USER_OVERVIEW_JSON_URL = 'https://publisher.assetstore.unity3d.com/api/user/overview.json'
PUBLISHER_OVERVIEW_JSON_URL = 'https://publisher.assetstore.unity3d.com/api/publisher/overview.json'
SALES_PERIODS_JSON_URL = 'https://publisher.assetstore.unity3d.com/api/publisher-info/months/{publisher_id}.json'
SALES_JSON_URL = 'https://publisher.assetstore.unity3d.com/api/publisher-info/sales/{publisher_id}/{year}{month}.json'
DOWNLOADS_JSON_URL = 'https://publisher.assetstore.unity3d.com/api/publisher-info/downloads/{publisher_id}/{year}{month}.json'
INVOICE_VERIFY_JSON_URL = 'https://publisher.assetstore.unity3d.com/api/publisher-info/verify-invoice/{publisher_id}/{invoice_id}.json'
REVENUE_JSON_URL = 'https://publisher.assetstore.unity3d.com/api/publisher-info/revenue/{publisher_id}.json'
PENDING_JSON_URL = 'https://publisher.assetstore.unity3d.com/api/publisher-info/pending/{publisher_id}.json'
API_KEY_JSON_URL = 'https://publisher.assetstore.unity3d.com/api/publisher-info/api-key/{publisher_id}.json'
LOGIN_TOKEN = '26c4202eb475d02864b40827dfff11a14657aa41'
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.3; rv:27.0) Gecko/20100101 Firefox/27.0'
def __init__(self):
self.loginToken = '';
self.isLoggedIn = False;
self.cookies = {};
self.userInfoOverview = None;
self.publisherInfoOverview = None;
def LoginWithToken(self, token):
self.AssertIsNotLoggedIn()
self.loginToken = token
self.isLoggedIn = True
self.cookies['xunitysession'] = self.GetXUnitySessionCookie()
def Login(self, username, password):
self.AssertIsNotLoggedIn()
token = self.GetLoginToken(username, password)
self.LoginWithToken(token)
return token
def Logout(self):
self.AssertIsLoggedIn()
result = self.GetSimpleData({'url':self.LOGOUT_URL})
self.AssertHttpCode('Logout failed, error code {code}', result.status_code);
self.__init__() #resets the variables
def IsLoggedIn(self):
return self.isLoggedIn
def GetUserInfo(self):
self.AssertIsLoggedIn()
if self.userInfoOverview == None:
result = self.GetSimpleData({'url':self.USER_OVERVIEW_JSON_URL})
self.AssertHttpCode('Fetching user data failed, error code {code}', result.status_code)
self.userInfoOverview = result.json()
return self.userInfoOverview
def GetPublisherInfo(self):
self.AssertIsLoggedIn()
if self.publisherInfoOverview == None:
result = self.GetSimpleData({'url':self.PUBLISHER_OVERVIEW_JSON_URL});
self.AssertHttpCode('Fetching publisher data failed, error code {code}', result.status_code)
publisherInfoObject = result.json()
self.publisherInfoOverview = PublisherInfo(publisherInfoObject)
return self.publisherInfoOverview
def FetchApiKey(self):
url = self.API_KEY_JSON_URL.format(publisher_id = self.GetPublisherInfo().GetId())
result = self.GetSimpleData({'url':url})
self.AssertHttpCode('Fetching API key failed, error code {code}', result.status_code)
keyDataObject = result.json()
return keyDataObject['api_key']
def FetchSalesPeriods(self):
self.AssertIsLoggedIn()
url = self.SALES_PERIODS_JSON_URL.format(publisher_id=self.GetPublisherInfo().GetId())
result = self.GetSimpleData({'url':url})
self.AssertHttpCode('Fetching sales periods failed, error code {code}', result.status_code)
salesPeriods = result.json()
infoArray = []
for value in salesPeriods['periods']:
infoArray.append(SalesPeriod(value))
return infoArray
def FetchRevenue(self):
self.AssertIsLoggedIn()
url = self.REVENUE_JSON_URL.format(publisher_id=self.GetPublisherInfo().GetId())
result = self.GetSimpleData({'url':url})
self.AssertHttpCode('Fetching sales periods failed, error code {code}', result.status_code)
infoObject = result.json()
infoArray = []
for value in infoObject['aaData']:
infoArray.append(RevenueInfo(value))
return infoArray
def FetchPending(self):
self.AssertIsLoggedIn()
url = self.PENDING_JSON_URL.format(publisher_id=self.GetPublisherInfo().GetId())
result = self.GetSimpleData({'url':url})
self.AssertHttpCode('Fetching pending packages failed, error code {code}', result.status_code)
infoObject = result.json()
infoArray = []
for value in infoObject['data']:
infoArray.append(RevenueInfo(value))
return infoArray
def VerifyInvoice(self, invoiceNumbers):
pass
def FetchSales(self, year, month):
self.AssertIsLoggedIn()
year = int(year)
month = int(month)
if year<2010:
raise AssetStoreException('Year must be after 2009')
if month>12 or month<1:
raise AssetStoreException('Month must be an integer between 1 and 12')
month = str(month).zfill(2)
year = str(year)
url = self.SALES_JSON_URL.format(publisher_id=self.GetPublisherInfo().GetId(), year=year, month=month)
result = self.GetSimpleData({'url':url})
self.AssertHttpCode('Fetching sales failed, error code {code}', result.status_code)
salesInfoObject = result.json()
salesInfo = []
key = 0
for value in salesInfoObject['aaData']:
temp = {i:j for i,j in enumerate(value)}
temp['shortLink'] = salesInfoObject['result'][key]['short_url']
salesInfo.append(AssetSalesInfo(temp))
key+=1
return PeriodSalesInfo(salesInfo, self.GetPublisherInfo().GetPayoutCut())
def FetchDownloads(year, month):
self.AssertIsLoggedIn()
year = int(year)
month = int(month)
if year<2010:
raise AssetStoreException('Year must be after 2009')
if month>12 or month<1:
raise AssetStoreException('Month must be an integer between 1 and 12')
month = str(month).zfill(2)
year = str(year)
self.SALES_JSON_URL.format(publisher_id=self.GetPublisherInfo().GetId(), year=year, month=month)
result = self.GetSimpleData({'url':url})
self.AssertHttpCode('Fetching downloads failed, error code {code}', result.status_code)
downloadsInfoObject = result.json()
downloadsInfo = []
for key, value in downloadsInfoObject['aaData']:
value['shortLink'] = downloadsInfoObject.result[key].short_url
downloadsInfo.append(AssetDownloadsInfo(value))
return PeriodDownloadsInfo(downloadsInfo)
def SetupCurlQuery(params):
pass
def GetSimpleData(self, url):
#Returns a requests.Response() instead of a dict
self.AssertIsLoggedIn()
if type(url)==dict: url=url['url']
headers = {
'User-Agent':self.USER_AGENT,
'referer':self.LOGIN_URL
}
r = requests.get(url, headers=headers, cookies=self.cookies)
return r
def GetXUnitySessionCookie(self):
if self.isLoggedIn:
return self.loginToken + self.LOGIN_TOKEN*2
else:
return self.LOGIN_TOKEN*3
def GetLoginToken(self, username, password):
query = {'user':username, 'pass':password, 'skip_terms':True}
headers = {
'User-Agent':self.USER_AGENT,
'X-Unity-Session':self.GetXUnitySessionCookie(),
'referer':self.LOGIN_URL
}
r = requests.post(self.LOGIN_URL, headers=headers, data=query)
self.AssertHttpCode('Login failed, error code {code}', r.status_code)
return r.text
def AssertHttpCode(self, message, code):
if HttpUtilities.IsErrorCode(code):
raise AssetStoreException(message.format(code=code))
def AssertIsLoggedIn(self):
if not self.IsLoggedIn():
raise AssetStoreException('Can\'t execute operation when not logged in')
def AssertIsNotLoggedIn(self):
if self.IsLoggedIn():
raise AssetStoreException('Login already performed')
class ParsedData (object):
def __init__(self):
self.data = Array()
def ParseDate(self, date):
if date=='':
return None
import time, datetime
timestamp = time.mktime(datetime.datetime.strptime(date, '%Y-%m-%d').timetuple())
return timestamp
def ParseCurrency(self, value):
if value=='':
return None
return float(filter(lambda x: x.isdigit() or x in ',.-', value))
class PublisherInfo(object):
def __init__(self, data):
data = data['overview']
self.data = {'id':int(data['id']),
'name':data['name'],
'description':data['description'],
'rating':int(data['rating']['average']),
'ratingCount':int(data['rating']['count']),
'payoutCut':data['payout_cut'],
'publisherUrl':data['long_url'],
'publisherShortUrl':data['short_url'],
'siteUrl':data['url'],
'supportUrl':data['support_url'],
'supportEmail':data['support_email']
}
def GetId(self):
return self.data['id']
def GetName(self):
return self.data['name']
def GetDescription(self):
return self.data['description']
def GetRating(self):
return self.data['rating']
def GetRatingCount(self):
return self.data['ratingCount']
def GetPayoutCut(self):
return self.data['payoutCut']
def GetPublisherUrl(self):
return self.data['publisherUrl']
def GetPublisherShortUrl(self):
return self.data['publisherShortUrl']
def GetSiteUrl(self):
return self.data['siteUrl']
def GetSupportUrl(self):
return self.data['supportUrl']
def GetSupportEmail(self):
return self.data['supportEmail']
#Added method
def __str__(self):
return '\n'.join(k+': '+str(self.data[k]) for k in self.data.keys())
class RevenueInfo (ParsedData):
TypeUnknown = -1;
TypeRevenue = 1;
TypePayout = 2;
def __init__(self, data):
infoType = self.TypeUnknown;
if 'revenue' in data[1]:
infoType = self.TypeRevenue
elif 'payout' in data[1]:
infoType = self.TypePayout
self.data = {
'date':self.ParseDate(data[0]),
'description':data[1],
'debet':self.ParseCurrency(data[2]),
'credit':self.ParseCurrency(data[3]),
'balance':self.ParseCurrency(data[4]),
'infoType':infoType
}
def GetDate(self):
return self.data['date']
def GetDescription(self):
return self.data['description']
def GetDebet(self):
return self.data['debet']
def GetCredit(self):
return self.data['credit']
def GetBalance(self):
return self.data['balance']
def GetInfoType(self):
return self.data['infoType']
class InvoiceInfo (object):
def __init__(self, data):
self.data = {
'id':data[0],
'assetName':data[1],
'date':self.ParseDate(data[2]),
'isRefunded':data[3] == 'Yes',
}
def GetInvoiceNumber(self):
return self.data['id']
def GetAssetName(self):
return self.data['assetName']
def GetPurchaseDate(self):
return self.data['date']
def IsRefunded(self):
return self.data['isRefunded']
class SalesPeriod(object):
def __init__(self, data):
self.year = int(data['value'][0:4])
self.month = int(data['value'][4:6])
def GetYear(self):
return self.year
def GetMonth(self):
return self.month
def GetDate(self):
import datetime, time
return time.mktime(datetime.datetime(self.year, self.month, 1, 0, 0, 0, 0).timetuple())
class PeriodSalesInfo(object):
def __init__(self, assetSales, payoutCut = 0.7):
self.assetSales = assetSales
self.payoutCut = float(payoutCut)
self.revenueGross = 0
for value in self.assetSales:
self.revenueGross += value.GetPrice() * (value.GetQuantity() - value.GetRefunds() - value.GetChargebacks())
self.revenueNet = self.revenueGross * self.payoutCut
def GetAssetSales(self):
return self.assetSales
def GetRevenueGross(self):
return self.revenueGross
def GetRevenueNet(self):
return self.revenueNet
def GetPayoutCut(self):
return self.payoutCut
class PeriodDownloadsInfo(object):
def __init__(self, assetDownloads):
self.assetDownloads = assetDownloads
def GetAssetDownloads(self):
return self.assetDownloads
class AssetSalesInfo (ParsedData):
def __init__(self, data):
self.data = {
'name':data[0],
'price':self.ParseCurrency(data[1]),
'quantity': None if data[2] == None else int(data[2]),
'refunds': None if data[3]==None else abs(int(data[3])),
'chargebacks': None if data[4]==None else abs(int(data[4])),
'gross': None if data[5]=='' else self.ParseCurrency(data[5]),
'firstPurchase': None if data[6]=='' else self.ParseDate(data[6]),
'lastPurchase': None if data[7] == None else self.ParseDate(data[7]),
'shortLink':data['shortLink'],
}
def GetAssetName(self):
return self.data['name']
def GetPrice(self):
return self.data['price']
def GetQuantity(self):
return self.data['quantity']
def GetRefunds(self):
return self.data['refunds']
def GetChargebacks(self):
return self.data['chargebacks']
def GetGross(self):
return self.data['gross']
def GetFirstPurchaseDate(self):
return self.data['firstPurchase']
def GetLastPurchaseDate(self):
return self.data['lastPurchase']
def GetShortUrl(self):
return self.data['shortLink']
def FetchAssetId(self):
redirect = HttpUtilities.GetRedirectUrl(self.data['shortLink'])
redirect = end(explode('/', redirect))
return redirect
class AssetDownloadsInfo(object):
def __init__(self, data):
self.data = {
'name':data[0],
'quantity':int(data[1]) if data[1] != None else None,
'firstDownload':self.ParseDate(data[2]) if data[2] != None else None,
'lastDownload':self.ParseDate(data[3]) if data[3] != None else None,
'shortLink':data['shortLink'],
}
def GetAssetName(self):
return self.data['name']
def GetQuantity(self):
return self.data['quantity']
def GetFirstDownloadDate(self):
return self.data['firstDownload']
def GetLastDownloadDate(self):
return self.data['lastDownload']
def GetShortUrl(self):
return self.data['shortLink']
def FetchAssetId(self):
redirect = HttpUtilities.GetRedirectUrl(self.data['shortLink'])
redirect = end(explode('/', redirect))
return redirect
class PendingInfo(object):
StatusUnknown = -1
StatusError = 1
StatusDraft = 2
StatusPending = 3
StatusDeclined = 4
def __init__(self, data):
status = self.StatusUnknown
size = data[1]
status = data[2]
# Parse status
if stripos(status, 'pending') != False:
status = self.StatusPending
elif stripos(status, 'declined') != False:
status = self.StatusDeclined
elif stripos(status, 'draft') != False:
status = self.StatusDraft
elif stripos(status, 'error') != False:
status = self.StatusError
# Parse size
sizeExplode = explode(' ', size)
size = float(sizeExplode[0])
if sizeExplode[1] == 'GB':
size *= 1000 * 1000
elif sizeExplode[1] == 'MB':
size *= 1000
size = int(size * 1000) # to bytes
self.data = {
'name':data[0],
'packageSize':size,
'status':status,
'updateDate':self.ParseDate(data[3])
}
def GetAssetName(self):
return self.data['name']
def GetPackageSize(self):
return self.data['packageSize']
def GetStatus(self):
return self.data['status']
def GetUpdateDate(self):
return self.data['updateDate']
class HttpUtilities (object):
errorMessages = {
# [Informational 1xx]
100:'Continue',
101:'Switching Protocols',
# [Successful 2xx]
200:'OK',
201:'Created',
202:'Accepted',
203:'Non-Authoritative Information',
204:'No Content',
205:'Reset Content',
206:'Partial Content',
# [Redirection 3xx]
300:'Multiple Choices',
301:'Moved Permanently',
302:'Found',
303:'See Other',
304:'Not Modified',
305:'Use Proxy',
306:'(Unused)',
307:'Temporary Redirect',
# [Client Error 4xx]
400:'Bad Request',
401:'Unauthorized',
402:'Payment Required',
403:'Forbidden',
404:'Not Found',
405:'Method Not Allowed',
406:'Not Acceptable',
407:'Proxy Authentication Required',
408:'Request Timeout',
409:'Conflict',
410:'Gone',
411:'Length Required',
412:'Precondition Failed',
413:'Request Entity Too Large',
414:'Request-URI Too Long',
415:'Unsupported Media Type',
416:'Requested Range Not Satisfiable',
417:'Expectation Failed',
# [Server Error 5xx]
500:'Internal Server Error',
501:'Not Implemented',
502:'Bad Gateway',
503:'Service Unavailable',
504:'Gateway Timeout',
505:'HTTP Version Not Supported'
}
@classmethod
def IsErrorCode(self, code):
# Error codes begin at 400
return type(code)==int and code >= 400
@classmethod
def GetStatusMessage(self, code):
return self.errorMessages[code]
@classmethod
def GetRedirectUrl(self, url):
return requests.get(url).url
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from datetime import datetime
import cgi
import webbrowser, AssetStoreAPI, getpass
class myHandler(BaseHTTPRequestHandler):
STYLE = 'table{border-collapse:collapse;}td,th{border:1px gray solid;padding:5px;}th{background:#EEE;}'
TEMPLATE = '<html><style>{style}</style><head></head><body>{body}</body></html>'
def GenPubInfoHtml(self, publisherInfo):
html = '<h2>Publisher info</h2><ul>'
namekeyset = {'Id':'id'}
for k, v in publisherInfo.data.iteritems():
html += '<li>%s: %s</li>'%(k,v)
html += '</ul>'
return html
def GenSalesPeriodsHtml(self, salesPeriods):
html = '<h2>Sales periods</h2><ul>'
for value in salesPeriods:
html += '<li>Month: %d, year: %d, formatted: %s</li>' %(
value.GetMonth(),
value.GetYear(),
datetime.fromtimestamp(value.GetDate()).strftime('%B %Y'))
html += '</ul>'
return html
def GenSalesHtml(self, sales):
html = '<h2>Sales</h2><table><tr><th>Paid Package</th><th>Price ($)</th><th>Qty</th><th>Refunds</th><th>Chargebacks</th><th>Gross ($)</th><th>First</th><th>Last</th></tr>'
for value in sales.GetAssetSales():
html += '<tr><td><a href="%s">%s</a></td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
value.GetShortUrl(),
value.GetAssetName(),
value.GetPrice(),
value.GetQuantity(),
value.GetRefunds(),
value.GetChargebacks(),
None if value.GetGross() == 0 else value.GetGross(),
None if value.GetFirstPurchaseDate() == None else datetime.fromtimestamp(value.GetFirstPurchaseDate()).strftime('%d %B %Y'),
None if value.GetLastPurchaseDate() == None else datetime.fromtimestamp(value.GetLastPurchaseDate()).strftime('%d %B %Y'))
html += '</table>'
return html
def GenRevenueHtml(self, revenue):
html = '<h2>Revenue</h2><table><tr><th>Date</th><th>Type</th><th>Description</th><th>Debit ($)</th><th>Credit ($)</th><th>Balance ($)</th></tr>'
for value in revenue:
infoType = 'Unknown'
if value.GetInfoType()==AssetStoreAPI.RevenueInfo.TypeRevenue:
infoType = 'Revenue'
elif value.GetInfoType()==AssetStoreAPI.RevenueInfo.TypePayout:
infoType = 'Payout'
html += '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
datetime.fromtimestamp(value.GetDate()).strftime('%B %Y'),
infoType,
value.GetDescription(),
value.GetDebet() or '',
value.GetCredit() or '',
None if value.GetBalance()==0 else value.GetBalance())
html += '</table'
return html
def get_asset_store_data(self):
#login
store = AssetStoreAPI.AssetStoreClient()
store.Login('user@example.com', 'password')
#Load HTML
body = self.GenPubInfoHtml(store.GetPublisherInfo())
salesperiods = store.FetchSalesPeriods()
salesyear = salesperiods[-1].GetYear()
salesmonth = salesperiods[-1].GetMonth()
salesPeriodsHtml = self.GenSalesPeriodsHtml(salesperiods)
saleshtml = self.GenSalesHtml(store.FetchSales(salesyear, salesmonth))
revenuehtml = self.GenRevenueHtml(store.FetchRevenue())
html = self.TEMPLATE.format(style=self.STYLE, body=body+salesPeriodsHtml+saleshtml+revenuehtml)
return html
def do_GET(self):
self.send_response(200)
self.send_header('Content-type','text/html')
self.end_headers()
# Send the html message
self.wfile.write(self.get_asset_store_data())
return
def do_POST(self):
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={'REQUEST_METHOD':'POST',
'CONTENT_TYPE':self.headers['Content-Type'],
})
return
#Serve HTML
PORT_NUMBER = 8080
try:
#Create a web server and define the handler to manage the incoming request
server = HTTPServer(('', PORT_NUMBER), myHandler)
webbrowser.open("http://127.0.0.1:%d"%PORT_NUMBER)
#Wait forever for incoming http requests
server.serve_forever()
except KeyboardInterrupt:
print '^C received, shutting down the web server'
server.socket.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment