Merge branch 'crest_fitting' into singularity

This commit is contained in:
blitzmann
2015-10-31 15:02:24 -04:00
27 changed files with 1657 additions and 18 deletions

2
.gitignore vendored
View File

@@ -13,7 +13,7 @@
*.patch
#Personal
saveddata/
/saveddata/
#PyCharm
.idea/

View File

@@ -92,6 +92,9 @@ def defPaths():
__createDirs(savePath)
if isFrozen():
os.environ["REQUESTS_CA_BUNDLE"] = os.path.join(pyfaPath, "cacert.pem")
format = '%(asctime)s %(name)-24s %(levelname)-8s %(message)s'
logging.basicConfig(format=format, level=logLevel)
handler = logging.handlers.RotatingFileHandler(os.path.join(savePath, "log.txt"), maxBytes=1000000, backupCount=3)

View File

@@ -75,7 +75,7 @@ from eos.db.saveddata.queries import getUser, getCharacter, getFit, getFitsWithS
getFitList, getFleetList, getFleet, save, remove, commit, add, \
getCharactersForUser, getMiscData, getSquadsIDsWithFitID, getWing, \
getSquad, getBoosterFits, getProjectedFits, getTargetResistsList, getTargetResists,\
clearPrices, countAllFits
clearPrices, countAllFits, getCrestCharacters, getCrestCharacter
#If using in memory saveddata, you'll want to reflect it so the data structure is good.
if config.saveddata_connectionstring == "sqlite:///:memory:":

View File

@@ -1,3 +1,3 @@
__all__ = ["character", "fit", "module", "user", "skill", "price",
__all__ = ["character", "fit", "module", "user", "crest", "skill", "price",
"booster", "drone", "implant", "fleet", "damagePattern",
"miscData", "targetResists"]

31
eos/db/saveddata/crest.py Normal file
View File

@@ -0,0 +1,31 @@
#===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
from sqlalchemy import Table, Column, Integer, String, Boolean
from sqlalchemy.orm import mapper
from eos.db import saveddata_meta
from eos.types import CrestChar
crest_table = Table("crest", saveddata_meta,
Column("ID", Integer, primary_key = True),
Column("name", String, nullable = False, unique = True),
Column("refresh_token", String, nullable = False))
mapper(CrestChar, crest_table)

View File

@@ -19,7 +19,7 @@
from eos.db.util import processEager, processWhere
from eos.db import saveddata_session, sd_lock
from eos.types import User, Character, Fit, Price, DamagePattern, Fleet, MiscData, Wing, Squad, TargetResists
from eos.types import User, Character, Fit, Price, DamagePattern, Fleet, MiscData, Wing, Squad, TargetResists, CrestChar
from eos.db.saveddata.fleet import squadmembers_table
from eos.db.saveddata.fit import projectedFits_table
from sqlalchemy.sql import and_
@@ -416,6 +416,30 @@ def getProjectedFits(fitID):
else:
raise TypeError("Need integer as argument")
def getCrestCharacters(eager=None):
eager = processEager(eager)
with sd_lock:
characters = saveddata_session.query(CrestChar).options(*eager).all()
return characters
@cachedQuery(CrestChar, 1, "lookfor")
def getCrestCharacter(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
with sd_lock:
character = saveddata_session.query(CrestChar).get(lookfor)
else:
eager = processEager(eager)
with sd_lock:
character = saveddata_session.query(CrestChar).options(*eager).filter(CrestChar.ID == lookfor).first()
elif isinstance(lookfor, basestring):
eager = processEager(eager)
with sd_lock:
character = saveddata_session.query(CrestChar).options(*eager).filter(CrestChar.name == lookfor).first()
else:
raise TypeError("Need integer or string as argument")
return character
def removeInvalid(fits):
invalids = [f for f in fits if f.isInvalid]

View File

@@ -0,0 +1,46 @@
#===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
import urllib
from cStringIO import StringIO
from sqlalchemy.orm import reconstructor
#from tomorrow import threads
class CrestChar(object):
def __init__(self, id, name, refresh_token=None):
self.ID = id
self.name = name
self.refresh_token = refresh_token
@reconstructor
def init(self):
pass
'''
@threads(1)
def fetchImage(self):
url = 'https://image.eveonline.com/character/%d_128.jpg'%self.ID
fp = urllib.urlopen(url)
data = fp.read()
fp.close()
self.img = StringIO(data)
'''

View File

@@ -21,6 +21,7 @@ from eos.gamedata import Attribute, Category, Effect, Group, Icon, Item, MarketG
MetaGroup, AttributeInfo, Unit, EffectInfo, MetaType, MetaData, Traits
from eos.saveddata.price import Price
from eos.saveddata.user import User
from eos.saveddata.crestchar import CrestChar
from eos.saveddata.damagePattern import DamagePattern
from eos.saveddata.targetResists import TargetResists
from eos.saveddata.character import Character, Skill

View File

@@ -1 +1 @@
__all__ = ["pyfaGeneralPreferences","pyfaHTMLExportPreferences","pyfaUpdatePreferences","pyfaNetworkPreferences"]
__all__ = ["pyfaGeneralPreferences","pyfaHTMLExportPreferences","pyfaUpdatePreferences","pyfaNetworkPreferences","pyfaCrestPreferences"]

View File

@@ -0,0 +1,120 @@
import wx
from gui.preferenceView import PreferenceView
from gui.bitmapLoader import BitmapLoader
import gui.mainFrame
import service
from service.crest import CrestModes
class PFCrestPref ( PreferenceView):
title = "CREST"
def populatePanel( self, panel ):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = service.settings.CRESTSettings.getInstance()
self.dirtySettings = False
dlgWidth = panel.GetParent().GetParent().ClientSize.width
mainSizer = wx.BoxSizer( wx.VERTICAL )
self.stTitle = wx.StaticText( panel, wx.ID_ANY, self.title, wx.DefaultPosition, wx.DefaultSize, 0 )
self.stTitle.Wrap( -1 )
self.stTitle.SetFont( wx.Font( 12, 70, 90, 90, False, wx.EmptyString ) )
mainSizer.Add( self.stTitle, 0, wx.ALL, 5 )
self.m_staticline1 = wx.StaticLine( panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
mainSizer.Add( self.m_staticline1, 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5 )
self.stInfo = wx.StaticText( panel, wx.ID_ANY, u"Please see the pyfa wiki on GitHub for information regarding these options.", wx.DefaultPosition, wx.DefaultSize, 0 )
self.stInfo.Wrap(dlgWidth - 50)
mainSizer.Add( self.stInfo, 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5 )
rbSizer = wx.BoxSizer(wx.HORIZONTAL)
self.rbMode = wx.RadioBox(panel, -1, "Mode", wx.DefaultPosition, wx.DefaultSize, ['Implicit', 'User-supplied details'], 1, wx.RA_SPECIFY_COLS)
self.rbServer = wx.RadioBox(panel, -1, "Server", wx.DefaultPosition, wx.DefaultSize, ['Tranquility', 'Singularity'], 1, wx.RA_SPECIFY_COLS)
self.rbMode.SetSelection(self.settings.get('mode'))
self.rbServer.SetSelection(self.settings.get('server'))
rbSizer.Add(self.rbMode, 1, wx.TOP | wx.RIGHT, 5 )
rbSizer.Add(self.rbServer, 1, wx.ALL, 5 )
self.rbMode.Bind(wx.EVT_RADIOBOX, self.OnModeChange)
self.rbServer.Bind(wx.EVT_RADIOBOX, self.OnServerChange)
mainSizer.Add(rbSizer, 1, wx.ALL|wx.EXPAND, 0)
detailsTitle = wx.StaticText( panel, wx.ID_ANY, "CREST client details", wx.DefaultPosition, wx.DefaultSize, 0 )
detailsTitle.Wrap( -1 )
detailsTitle.SetFont( wx.Font( 12, 70, 90, 90, False, wx.EmptyString ) )
mainSizer.Add( detailsTitle, 0, wx.ALL, 5 )
mainSizer.Add( wx.StaticLine( panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ), 0, wx.EXPAND, 5 )
fgAddrSizer = wx.FlexGridSizer( 2, 2, 0, 0 )
fgAddrSizer.AddGrowableCol( 1 )
fgAddrSizer.SetFlexibleDirection( wx.BOTH )
fgAddrSizer.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED )
self.stSetID = wx.StaticText( panel, wx.ID_ANY, u"Client ID:", wx.DefaultPosition, wx.DefaultSize, 0 )
self.stSetID.Wrap( -1 )
fgAddrSizer.Add( self.stSetID, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
self.inputClientID = wx.TextCtrl( panel, wx.ID_ANY, self.settings.get('clientID'), wx.DefaultPosition, wx.DefaultSize, 0 )
fgAddrSizer.Add( self.inputClientID, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5 )
self.stSetSecret = wx.StaticText( panel, wx.ID_ANY, u"Client Secret:", wx.DefaultPosition, wx.DefaultSize, 0 )
self.stSetSecret.Wrap( -1 )
fgAddrSizer.Add( self.stSetSecret, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
self.inputClientSecret = wx.TextCtrl( panel, wx.ID_ANY, self.settings.get('clientSecret'), wx.DefaultPosition, wx.DefaultSize, 0 )
fgAddrSizer.Add( self.inputClientSecret, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5 )
self.btnApply = wx.Button( panel, wx.ID_ANY, u"Save Client Settings", wx.DefaultPosition, wx.DefaultSize, 0 )
self.btnApply.Bind(wx.EVT_BUTTON, self.OnBtnApply)
mainSizer.Add( fgAddrSizer, 0, wx.EXPAND, 5)
mainSizer.Add( self.btnApply, 0, wx.ALIGN_RIGHT, 5)
self.ToggleProxySettings(self.settings.get('mode'))
panel.SetSizer( mainSizer )
panel.Layout()
def OnModeChange(self, event):
self.settings.set('mode', event.GetInt())
self.ToggleProxySettings(self.settings.get('mode'))
service.Crest.restartService()
def OnServerChange(self, event):
self.settings.set('server', event.GetInt())
service.Crest.restartService()
def OnBtnApply(self, event):
self.settings.set('clientID', self.inputClientID.GetValue())
self.settings.set('clientSecret', self.inputClientSecret.GetValue())
sCrest = service.Crest.getInstance()
sCrest.delAllCharacters()
def ToggleProxySettings(self, mode):
if mode:
self.stSetID.Enable()
self.inputClientID.Enable()
self.stSetSecret.Enable()
self.inputClientSecret.Enable()
else:
self.stSetID.Disable()
self.inputClientID.Disable()
self.stSetSecret.Disable()
self.inputClientSecret.Disable()
def getImage(self):
return BitmapLoader.getBitmap("eve", "gui")
PFCrestPref.register()

View File

@@ -25,16 +25,18 @@ class CopySelectDialog(wx.Dialog):
copyFormatEftImps = 1
copyFormatXml = 2
copyFormatDna = 3
copyFormatCrest = 4
def __init__(self, parent):
wx.Dialog.__init__(self, parent, id = wx.ID_ANY, title = u"Select a format", size = (-1,-1), style = wx.DEFAULT_DIALOG_STYLE)
mainSizer = wx.BoxSizer(wx.VERTICAL)
copyFormats = [u"EFT", u"EFT (Implants)", u"XML", u"DNA"]
copyFormats = [u"EFT", u"EFT (Implants)", u"XML", u"DNA", u"CREST"]
copyFormatTooltips = {CopySelectDialog.copyFormatEft: u"EFT text format",
CopySelectDialog.copyFormatEftImps: u"EFT text format",
CopySelectDialog.copyFormatXml: u"EVE native XML format",
CopySelectDialog.copyFormatDna: u"A one-line text format"}
CopySelectDialog.copyFormatDna: u"A one-line text format",
CopySelectDialog.copyFormatCrest: u"A JSON format used for EVE CREST"}
selector = wx.RadioBox(self, wx.ID_ANY, label = u"Copy to the clipboard using:", choices = copyFormats, style = wx.RA_SPECIFY_ROWS)
selector.Bind(wx.EVT_RADIOBOX, self.Selected)
for format, tooltip in copyFormatTooltips.iteritems():

363
gui/crestFittings.py Normal file
View File

@@ -0,0 +1,363 @@
import time
import webbrowser
import json
import wx
from wx.lib.pubsub import setupkwargs
from wx.lib.pubsub import pub
import service
from service.crest import CrestModes
import gui.display as d
from eos.types import Cargo
from eos.db import getItem
class CrestFittings(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, id=wx.ID_ANY, title="Browse EVE Fittings", pos=wx.DefaultPosition, size=wx.Size( 550,450 ), style=wx.DEFAULT_FRAME_STYLE | wx.TAB_TRAVERSAL)
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
self.mainFrame = parent
mainSizer = wx.BoxSizer(wx.VERTICAL)
sCrest = service.Crest.getInstance()
characterSelectSizer = wx.BoxSizer( wx.HORIZONTAL )
if sCrest.settings.get('mode') == CrestModes.IMPLICIT:
self.stLogged = wx.StaticText(self, wx.ID_ANY, "Currently logged in as %s"%sCrest.implicitCharacter.name, wx.DefaultPosition, wx.DefaultSize)
self.stLogged.Wrap( -1 )
characterSelectSizer.Add( self.stLogged, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
else:
self.charChoice = wx.Choice(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, [])
characterSelectSizer.Add( self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
self.updateCharList()
self.fetchBtn = wx.Button( self, wx.ID_ANY, u"Fetch Fits", wx.DefaultPosition, wx.DefaultSize, 5 )
characterSelectSizer.Add( self.fetchBtn, 0, wx.ALL, 5 )
mainSizer.Add( characterSelectSizer, 0, wx.EXPAND, 5 )
self.sl = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
mainSizer.Add( self.sl, 0, wx.EXPAND |wx.ALL, 5 )
contentSizer = wx.BoxSizer( wx.HORIZONTAL )
browserSizer = wx.BoxSizer( wx.VERTICAL )
self.fitTree = FittingsTreeView(self)
browserSizer.Add( self.fitTree, 1, wx.ALL|wx.EXPAND, 5 )
contentSizer.Add( browserSizer, 1, wx.EXPAND, 0 )
fitSizer = wx.BoxSizer( wx.VERTICAL )
self.fitView = FitView(self)
fitSizer.Add( self.fitView, 1, wx.ALL|wx.EXPAND, 5 )
btnSizer = wx.BoxSizer( wx.HORIZONTAL )
self.importBtn = wx.Button( self, wx.ID_ANY, u"Import to pyfa", wx.DefaultPosition, wx.DefaultSize, 5 )
self.deleteBtn = wx.Button( self, wx.ID_ANY, u"Delete from EVE", wx.DefaultPosition, wx.DefaultSize, 5 )
btnSizer.Add( self.importBtn, 1, wx.ALL, 5 )
btnSizer.Add( self.deleteBtn, 1, wx.ALL, 5 )
fitSizer.Add( btnSizer, 0, wx.EXPAND )
contentSizer.Add(fitSizer, 1, wx.EXPAND, 0)
mainSizer.Add(contentSizer, 1, wx.EXPAND, 5)
self.fetchBtn.Bind(wx.EVT_BUTTON, self.fetchFittings)
self.importBtn.Bind(wx.EVT_BUTTON, self.importFitting)
self.deleteBtn.Bind(wx.EVT_BUTTON, self.deleteFitting)
pub.subscribe(self.ssoLogout, 'logout_success')
self.statusbar = wx.StatusBar(self)
self.statusbar.SetFieldsCount()
self.SetStatusBar(self.statusbar)
self.cacheTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.updateCacheStatus, self.cacheTimer)
self.SetSizer(mainSizer)
self.Layout()
self.Centre(wx.BOTH)
def updateCharList(self):
sCrest = service.Crest.getInstance()
chars = sCrest.getCrestCharacters()
if len(chars) == 0:
self.Close()
for char in chars:
self.charChoice.Append(char.name, char.ID)
self.charChoice.SetSelection(0)
def updateCacheStatus(self, event):
t = time.gmtime(self.cacheTime-time.time())
if t < 0:
self.cacheTimer.Stop()
self.statusbar.Hide()
else:
sTime = time.strftime("%H:%M:%S", t)
self.statusbar.SetStatusText("Cached for %s"%sTime, 0)
def ssoLogout(self, message):
self.Close()
def getActiveCharacter(self):
sCrest = service.Crest.getInstance()
if sCrest.settings.get('mode') == CrestModes.IMPLICIT:
return sCrest.implicitCharacter.ID
selection = self.charChoice.GetCurrentSelection()
return self.charChoice.GetClientData(selection) if selection is not None else None
def fetchFittings(self, event):
sCrest = service.Crest.getInstance()
waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self)
fittings = sCrest.getFittings(self.getActiveCharacter())
self.cacheTime = fittings.get('cached_until')
self.updateCacheStatus(None)
self.cacheTimer.Start(1000)
self.fitTree.populateSkillTree(fittings)
del waitDialog
def importFitting(self, event):
selection = self.fitView.fitSelection
if not selection:
return
data = self.fitTree.fittingsTreeCtrl.GetPyData(selection)
sFit = service.Fit.getInstance()
fits = sFit.importFitFromBuffer(data)
self.mainFrame._openAfterImport(fits)
def deleteFitting(self, event):
sCrest = service.Crest.getInstance()
selection = self.fitView.fitSelection
if not selection:
return
data = json.loads(self.fitTree.fittingsTreeCtrl.GetPyData(selection))
dlg = wx.MessageDialog(self,
"Do you really want to delete %s (%s) from EVE?"%(data['name'], data['ship']['name']),
"Confirm Delete", wx.YES | wx.NO | wx.ICON_QUESTION)
if dlg.ShowModal() == wx.ID_YES:
sCrest.delFitting(self.getActiveCharacter(), data['fittingID'])
class ExportToEve(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, id=wx.ID_ANY, title="Export fit to EVE", pos=wx.DefaultPosition, size=(wx.Size(350,100)), style=wx.DEFAULT_FRAME_STYLE | wx.TAB_TRAVERSAL)
self.mainFrame = parent
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
sCrest = service.Crest.getInstance()
mainSizer = wx.BoxSizer(wx.VERTICAL)
hSizer = wx.BoxSizer(wx.HORIZONTAL)
if sCrest.settings.get('mode') == CrestModes.IMPLICIT:
self.stLogged = wx.StaticText(self, wx.ID_ANY, "Currently logged in as %s"%sCrest.implicitCharacter.name, wx.DefaultPosition, wx.DefaultSize)
self.stLogged.Wrap( -1 )
hSizer.Add( self.stLogged, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
else:
self.charChoice = wx.Choice(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, [])
hSizer.Add( self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
self.updateCharList()
self.charChoice.SetSelection(0)
self.exportBtn = wx.Button( self, wx.ID_ANY, u"Export Fit", wx.DefaultPosition, wx.DefaultSize, 5 )
hSizer.Add( self.exportBtn, 0, wx.ALL, 5 )
mainSizer.Add( hSizer, 0, wx.EXPAND, 5 )
self.exportBtn.Bind(wx.EVT_BUTTON, self.exportFitting)
self.statusbar = wx.StatusBar(self)
self.statusbar.SetFieldsCount(2)
self.statusbar.SetStatusWidths([100, -1])
pub.subscribe(self.ssoLogout, 'logout_success')
self.SetSizer(hSizer)
self.SetStatusBar(self.statusbar)
self.Layout()
self.Centre(wx.BOTH)
def updateCharList(self):
sCrest = service.Crest.getInstance()
chars = sCrest.getCrestCharacters()
if len(chars) == 0:
self.Close()
for char in chars:
self.charChoice.Append(char.name, char.ID)
self.charChoice.SetSelection(0)
def ssoLogout(self, message):
self.Close()
def getActiveCharacter(self):
sCrest = service.Crest.getInstance()
if sCrest.settings.get('mode') == CrestModes.IMPLICIT:
return sCrest.implicitCharacter.ID
selection = self.charChoice.GetCurrentSelection()
return self.charChoice.GetClientData(selection) if selection is not None else None
def exportFitting(self, event):
self.statusbar.SetStatusText("", 0)
self.statusbar.SetStatusText("Sending request and awaiting response", 1)
sCrest = service.Crest.getInstance()
sFit = service.Fit.getInstance()
data = sFit.exportCrest(self.mainFrame.getActiveFit())
res = sCrest.postFitting(self.getActiveCharacter(), data)
self.statusbar.SetStatusText("%d: %s"%(res.status_code, res.reason), 0)
try:
text = json.loads(res.text)
self.statusbar.SetStatusText(text['message'], 1)
except ValueError:
self.statusbar.SetStatusText("", 1)
class CrestMgmt(wx.Dialog):
def __init__( self, parent ):
wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = "CREST Character Management", pos = wx.DefaultPosition, size = wx.Size( 550,250 ), style = wx.DEFAULT_DIALOG_STYLE )
mainSizer = wx.BoxSizer( wx.HORIZONTAL )
self.lcCharacters = wx.ListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LC_REPORT)
self.lcCharacters.InsertColumn(0, heading='Character')
self.lcCharacters.InsertColumn(1, heading='Refresh Token')
self.popCharList()
mainSizer.Add( self.lcCharacters, 1, wx.ALL|wx.EXPAND, 5 )
btnSizer = wx.BoxSizer( wx.VERTICAL )
self.addBtn = wx.Button( self, wx.ID_ANY, u"Add Character", wx.DefaultPosition, wx.DefaultSize, 0 )
btnSizer.Add( self.addBtn, 0, wx.ALL | wx.EXPAND, 5 )
self.deleteBtn = wx.Button( self, wx.ID_ANY, u"Revoke Character", wx.DefaultPosition, wx.DefaultSize, 0 )
btnSizer.Add( self.deleteBtn, 0, wx.ALL | wx.EXPAND, 5 )
mainSizer.Add( btnSizer, 0, wx.EXPAND, 5 )
self.addBtn.Bind(wx.EVT_BUTTON, self.addChar)
self.deleteBtn.Bind(wx.EVT_BUTTON, self.delChar)
pub.subscribe(self.ssoLogin, 'login_success')
self.SetSizer( mainSizer )
self.Layout()
self.Centre( wx.BOTH )
def ssoLogin(self, type):
self.popCharList()
def popCharList(self):
sCrest = service.Crest.getInstance()
chars = sCrest.getCrestCharacters()
self.lcCharacters.DeleteAllItems()
for index, char in enumerate(chars):
self.lcCharacters.InsertStringItem(index, char.name)
self.lcCharacters.SetStringItem(index, 1, char.refresh_token)
self.lcCharacters.SetItemData(index, char.ID)
self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE)
self.lcCharacters.SetColumnWidth(1, wx.LIST_AUTOSIZE)
def addChar(self, event):
sCrest = service.Crest.getInstance()
uri = sCrest.startServer()
webbrowser.open(uri)
def delChar(self, event):
item = self.lcCharacters.GetFirstSelected()
charID = self.lcCharacters.GetItemData(item)
sCrest = service.Crest.getInstance()
sCrest.delCrestCharacter(charID)
self.popCharList()
class FittingsTreeView(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, id=wx.ID_ANY)
self.parent = parent
pmainSizer = wx.BoxSizer(wx.VERTICAL)
tree = self.fittingsTreeCtrl = wx.TreeCtrl(self, wx.ID_ANY, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT)
pmainSizer.Add(tree, 1, wx.EXPAND | wx.ALL, 0)
self.root = tree.AddRoot("Fits")
self.populateSkillTree(None)
self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.displayFit)
self.SetSizer(pmainSizer)
self.Layout()
def populateSkillTree(self, data):
if data is None:
return
root = self.root
tree = self.fittingsTreeCtrl
tree.DeleteChildren(root)
dict = {}
fits = data['items']
for fit in fits:
if fit['ship']['name'] not in dict:
dict[fit['ship']['name']] = []
dict[fit['ship']['name']].append(fit)
for name, fits in dict.iteritems():
shipID = tree.AppendItem(root, name)
for fit in fits:
fitId = tree.AppendItem(shipID, fit['name'])
tree.SetPyData(fitId, json.dumps(fit))
tree.SortChildren(root)
def displayFit(self, event):
selection = self.fittingsTreeCtrl.GetSelection()
fit = json.loads(self.fittingsTreeCtrl.GetPyData(selection))
list = []
for item in fit['items']:
try:
cargo = Cargo(getItem(item['type']['id']))
cargo.amount = item['quantity']
list.append(cargo)
except:
pass
self.parent.fitView.fitSelection = selection
self.parent.fitView.update(list)
class FitView(d.Display):
DEFAULT_COLS = ["Base Icon",
"Base Name"]
def __init__(self, parent):
d.Display.__init__(self, parent, style=wx.LC_SINGLE_SEL)
self.fitSelection = None

View File

@@ -30,6 +30,7 @@ from wx.lib.wordwrap import wordwrap
import service
import config
import threading
import webbrowser
import gui.aboutData
import gui.chromeTabs
@@ -44,6 +45,7 @@ from gui.multiSwitch import MultiSwitch
from gui.statsPane import StatsPane
from gui.shipBrowser import ShipBrowser, FitSelected, ImportSelected, Stage3Selected
from gui.characterEditor import CharacterEditor, SaveCharacterAs
from gui.crestFittings import CrestFittings, ExportToEve, CrestMgmt
from gui.characterSelection import CharacterSelection
from gui.patternEditor import DmgPatternEditorDlg
from gui.resistsEditor import ResistsEditorDlg
@@ -54,9 +56,12 @@ from gui.utils.clipboard import toClipboard, fromClipboard
from gui.fleetBrowser import FleetBrowser
from gui.updateDialog import UpdateDialog
from gui.builtinViews import *
from time import gmtime, strftime
from service.crest import CrestModes
from wx.lib.pubsub import setupkwargs
from wx.lib.pubsub import pub
#dummy panel(no paint no erasebk)
class PFPanel(wx.Panel):
@@ -101,8 +106,8 @@ class MainFrame(wx.Frame):
return cls.__instance if cls.__instance is not None else MainFrame()
def __init__(self):
title="pyfa %s%s - Python Fitting Assistant"%(config.version, "" if config.tag.lower() != 'git' else " (git)")
wx.Frame.__init__(self, None, wx.ID_ANY, title)
self.title="pyfa %s%s - Python Fitting Assistant"%(config.version, "" if config.tag.lower() != 'git' else " (git)")
wx.Frame.__init__(self, None, wx.ID_ANY, self.title)
MainFrame.__instance = self
@@ -193,6 +198,12 @@ class MainFrame(wx.Frame):
self.sUpdate = service.Update.getInstance()
self.sUpdate.CheckUpdate(self.ShowUpdateBox)
pub.subscribe(self.onSSOLogin, 'login_success')
pub.subscribe(self.onSSOLogout, 'logout_success')
self.titleTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.updateTitle, self.titleTimer)
def ShowUpdateBox(self, release):
dlg = UpdateDialog(self, release)
dlg.ShowModal()
@@ -370,10 +381,10 @@ class MainFrame(wx.Frame):
dlg.ShowModal()
def goWiki(self, event):
wx.LaunchDefaultBrowser('https://github.com/DarkFenX/Pyfa/wiki')
webbrowser.open('https://github.com/DarkFenX/Pyfa/wiki')
def goForums(self, event):
wx.LaunchDefaultBrowser('https://forums.eveonline.com/default.aspx?g=posts&t=247609')
webbrowser.open('https://forums.eveonline.com/default.aspx?g=posts&t=247609')
def registerMenu(self):
menuBar = self.GetMenuBar()
@@ -416,6 +427,12 @@ class MainFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.saveCharAs, id = menuBar.saveCharAsId)
# Save current character
self.Bind(wx.EVT_MENU, self.revertChar, id = menuBar.revertCharId)
# Browse fittings
self.Bind(wx.EVT_MENU, self.eveFittings, id = menuBar.eveFittingsId)
# Export to EVE
self.Bind(wx.EVT_MENU, self.exportToEve, id = menuBar.exportToEveId)
# Login to EVE
self.Bind(wx.EVT_MENU, self.ssoLogin, id = menuBar.ssoLoginId)
#Clipboard exports
self.Bind(wx.EVT_MENU, self.exportToClipboard, id=wx.ID_COPY)
@@ -480,6 +497,43 @@ class MainFrame(wx.Frame):
atable = wx.AcceleratorTable(actb)
self.SetAcceleratorTable(atable)
def eveFittings(self, event):
dlg=CrestFittings(self)
dlg.Show()
def updateTitle(self, event):
sCrest = service.Crest.getInstance()
char = sCrest.implicitCharacter
if char:
t = time.gmtime(char.eve.expires-time.time())
sTime = time.strftime("%H:%M:%S", t if t >= 0 else 0)
newTitle = "%s | %s - %s"%(self.title, char.name, sTime)
self.SetTitle(newTitle)
def onSSOLogin(self, type):
if type == 0:
self.titleTimer.Start(1000)
def onSSOLogout(self, message):
self.titleTimer.Stop()
self.SetTitle(self.title)
def ssoLogin(self, event):
sCrest = service.Crest.getInstance()
if sCrest.settings.get('mode') == CrestModes.IMPLICIT:
if sCrest.implicitCharacter is not None:
sCrest.logout()
else:
uri = sCrest.startServer()
webbrowser.open(uri)
else:
dlg=CrestMgmt(self)
dlg.Show()
def exportToEve(self, event):
dlg=ExportToEve(self)
dlg.Show()
def saveChar(self, event):
sChr = service.Character.getInstance()
charID = self.charSelection.getActiveCharacter()
@@ -542,6 +596,10 @@ class MainFrame(wx.Frame):
sFit = service.Fit.getInstance()
toClipboard(sFit.exportDna(self.getActiveFit()))
def clipboardCrest(self):
sFit = service.Fit.getInstance()
toClipboard(sFit.exportCrest(self.getActiveFit()))
def clipboardXml(self):
sFit = service.Fit.getInstance()
toClipboard(sFit.exportXml(None, self.getActiveFit()))
@@ -559,14 +617,15 @@ class MainFrame(wx.Frame):
CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft,
CopySelectDialog.copyFormatEftImps: self.clipboardEftImps,
CopySelectDialog.copyFormatXml: self.clipboardXml,
CopySelectDialog.copyFormatDna: self.clipboardDna}
CopySelectDialog.copyFormatDna: self.clipboardDna,
CopySelectDialog.copyFormatCrest: self.clipboardCrest}
dlg = CopySelectDialog(self)
dlg.ShowModal()
selected = dlg.GetSelected()
try:
CopySelectDict[selected]()
except:
pass
CopySelectDict[selected]()
dlg.Destroy()
def exportSkillsNeeded(self, event):

View File

@@ -24,6 +24,10 @@ import gui.mainFrame
import gui.graphFrame
import gui.globalEvents as GE
import service
from service.crest import CrestModes
from wx.lib.pubsub import setupkwargs
from wx.lib.pubsub import pub
class MainMenuBar(wx.MenuBar):
def __init__(self):
@@ -40,9 +44,14 @@ class MainMenuBar(wx.MenuBar):
self.saveCharId = wx.NewId()
self.saveCharAsId = wx.NewId()
self.revertCharId = wx.NewId()
self.eveFittingsId = wx.NewId()
self.exportToEveId = wx.NewId()
self.ssoLoginId = wx.NewId()
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sCrest = service.Crest.getInstance()
wx.MenuBar.__init__(self)
# File menu
@@ -102,6 +111,20 @@ class MainMenuBar(wx.MenuBar):
preferencesItem.SetBitmap(BitmapLoader.getBitmap("preferences_small", "gui"))
windowMenu.AppendItem(preferencesItem)
# CREST Menu
crestMenu = wx.Menu()
self.Append(crestMenu, "&CREST")
if self.sCrest.settings.get('mode') != CrestModes.IMPLICIT:
crestMenu.Append(self.ssoLoginId, "Manage Characters")
else:
crestMenu.Append(self.ssoLoginId, "Login to EVE")
crestMenu.Append(self.eveFittingsId, "Browse EVE Fittings")
crestMenu.Append(self.exportToEveId, "Export To EVE")
if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT or len(self.sCrest.getCrestCharacters()) == 0:
self.Enable(self.eveFittingsId, False)
self.Enable(self.exportToEveId, False)
# Help menu
helpMenu = wx.Menu()
self.Append(helpMenu, "&Help")
@@ -114,6 +137,9 @@ class MainMenuBar(wx.MenuBar):
helpMenu.Append( self.mainFrame.widgetInspectMenuID, "Open Widgets Inspect tool", "Open Widgets Inspect tool")
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
pub.subscribe(self.ssoLogin, 'login_success')
pub.subscribe(self.ssoLogout, 'logout_success')
pub.subscribe(self.updateCrest, 'crest_changed')
def fitChanged(self, event):
enable = event.fitID is not None
@@ -131,3 +157,25 @@ class MainMenuBar(wx.MenuBar):
self.Enable(self.revertCharId, char.isDirty)
event.Skip()
def ssoLogin(self, type):
if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT:
self.SetLabel(self.ssoLoginId, "Logout Character")
self.Enable(self.eveFittingsId, True)
self.Enable(self.exportToEveId, True)
def ssoLogout(self, message):
if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT:
self.SetLabel(self.ssoLoginId, "Login to EVE")
self.Enable(self.eveFittingsId, False)
self.Enable(self.exportToEveId, False)
def updateCrest(self, message):
bool = self.sCrest.settings.get('mode') == CrestModes.IMPLICIT or len(self.sCrest.getCrestCharacters()) == 0
self.Enable(self.eveFittingsId, not bool)
self.Enable(self.exportToEveId, not bool)
if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT:
self.SetLabel(self.ssoLoginId, "Login to EVE")
else:
self.SetLabel(self.ssoLoginId, "Manage Characters")

BIN
imgs/gui/eve.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -10,3 +10,6 @@ from service.update import Update
from service.price import Price
from service.network import Network
from service.eveapi import EVEAPIConnection, ParseXML
from service.crest import Crest
from service.server import StoppableHTTPServer, AuthHandler
from service.pycrest import EVE

204
service/crest.py Normal file
View File

@@ -0,0 +1,204 @@
import thread
import config
import logging
import threading
import copy
import uuid
import wx
import time
from wx.lib.pubsub import pub
import eos.db
from eos.enum import Enum
from eos.types import CrestChar
import service
logger = logging.getLogger(__name__)
class Servers(Enum):
TQ = 0
SISI = 1
class CrestModes(Enum):
IMPLICIT = 0
USER = 1
class Crest():
clientIDs = {
Servers.TQ: 'f9be379951c046339dc13a00e6be7704',
Servers.SISI: 'af87365240d644f7950af563b8418bad'
}
# @todo: move this to settings
clientCallback = 'http://localhost:6461'
clientTest = True
_instance = None
@classmethod
def getInstance(cls):
if cls._instance == None:
cls._instance = Crest()
return cls._instance
@classmethod
def restartService(cls):
# This is here to reseed pycrest values when changing preferences
# We first stop the server n case one is running, as creating a new
# instance doesn't do this.
if cls._instance.httpd:
cls._instance.stopServer()
cls._instance = Crest()
wx.CallAfter(pub.sendMessage, 'crest_changed', message=None)
return cls._instance
def __init__(self):
print "init crest"
self.settings = service.settings.CRESTSettings.getInstance()
self.scopes = ['characterFittingsRead', 'characterFittingsWrite']
# these will be set when needed
self.httpd = None
self.state = None
self.ssoTimer = None
# Base EVE connection that is copied to all characters
self.eve = service.pycrest.EVE(
client_id=self.settings.get('clientID') if self.settings.get('mode') == CrestModes.USER else self.clientIDs.get(self.settings.get('server')),
api_key=self.settings.get('clientSecret') if self.settings.get('mode') == CrestModes.USER else None,
redirect_uri=self.clientCallback,
testing=self.isTestServer
)
self.implicitCharacter = None
# The database cache does not seem to be working for some reason. Use
# this as a temporary measure
self.charCache = {}
pub.subscribe(self.handleLogin, 'sso_login')
@property
def isTestServer(self):
return self.settings.get('server') == Servers.SISI
def delCrestCharacter(self, charID):
char = eos.db.getCrestCharacter(charID)
eos.db.remove(char)
wx.CallAfter(pub.sendMessage, 'crest_delete', message=None)
def delAllCharacters(self):
chars = eos.db.getCrestCharacters()
for char in chars:
eos.db.remove(char)
self.charCache = {}
wx.CallAfter(pub.sendMessage, 'crest_delete', message=None)
def getCrestCharacters(self):
chars = eos.db.getCrestCharacters()
return chars
def getCrestCharacter(self, charID):
'''
Get character, and modify to include the eve connection
'''
if self.settings.get('mode') == CrestModes.IMPLICIT:
if self.implicitCharacter.ID != charID:
raise ValueError("CharacterID does not match currently logged in character.")
return self.implicitCharacter
if charID in self.charCache:
return self.charCache.get(charID)
char = eos.db.getCrestCharacter(charID)
if char and not hasattr(char, "eve"):
char.eve = copy.deepcopy(self.eve)
char.eve.temptoken_authorize(refresh_token=char.refresh_token)
self.charCache[charID] = char
return char
def getFittings(self, charID):
char = self.getCrestCharacter(charID)
return char.eve.get('https://api-sisi.testeveonline.com/characters/%d/fittings/'%char.ID)
def postFitting(self, charID, json):
#@todo: new fitting ID can be recovered from Location header, ie: Location -> https://api-sisi.testeveonline.com/characters/1611853631/fittings/37486494/
char = self.getCrestCharacter(charID)
return char.eve.post('https://api-sisi.testeveonline.com/characters/%d/fittings/'%char.ID, data=json)
def delFitting(self, charID, fittingID):
char = self.getCrestCharacter(charID)
return char.eve.delete('https://api-sisi.testeveonline.com/characters/%d/fittings/%d/'%(char.ID, fittingID))
def logout(self):
logging.debug("Character logout")
self.implicitCharacter = None
wx.CallAfter(pub.sendMessage, 'logout_success', message=None)
def stopServer(self):
logging.debug("Stopping Server")
self.httpd.stop()
self.httpd = None
def startServer(self):
logging.debug("Starting server")
if self.httpd:
self.stopServer()
time.sleep(1) # we need this to ensure that the previous get_request finishes, and then the socket will close
self.httpd = service.StoppableHTTPServer(('', 6461), service.AuthHandler)
thread.start_new_thread(self.httpd.serve, ())
self.state = str(uuid.uuid4())
return self.eve.auth_uri(scopes=self.scopes, state=self.state)
def handleLogin(self, message):
if not message:
return
if message['state'][0] != self.state:
logger.warn("OAUTH state mismatch")
return
logger.debug("Handling CREST login with: %s"%message)
if 'access_token' in message: # implicit
eve = copy.deepcopy(self.eve)
eve.temptoken_authorize(
access_token=message['access_token'][0],
expires_in=int(message['expires_in'][0])
)
self.ssoTimer = threading.Timer(int(message['expires_in'][0]), self.logout)
self.ssoTimer.start()
eve()
info = eve.whoami()
logger.debug("Got character info: %s" % info)
self.implicitCharacter = CrestChar(info['CharacterID'], info['CharacterName'])
self.implicitCharacter.eve = eve
#self.implicitCharacter.fetchImage()
wx.CallAfter(pub.sendMessage, 'login_success', type=CrestModes.IMPLICIT)
elif 'code' in message:
eve = copy.deepcopy(self.eve)
eve.authorize(message['code'][0])
eve()
info = eve.whoami()
logger.debug("Got character info: %s" % info)
# check if we have character already. If so, simply replace refresh_token
char = self.getCrestCharacter(int(info['CharacterID']))
if char:
char.refresh_token = eve.refresh_token
else:
char = CrestChar(info['CharacterID'], info['CharacterName'], eve.refresh_token)
char.eve = eve
self.charCache[int(info['CharacterID'])] = char
eos.db.save(char)
wx.CallAfter(pub.sendMessage, 'login_success', type=CrestModes.USER)
self.stopServer()

View File

@@ -820,6 +820,10 @@ class Fit(object):
fit = eos.db.getFit(fitID)
return Port.exportDna(fit)
def exportCrest(self, fitID, callback=None):
fit = eos.db.getFit(fitID)
return Port.exportCrest(fit, callback)
def exportXml(self, callback=None, *fitIDs):
fits = map(lambda fitID: eos.db.getFit(fitID), fitIDs)
return Port.exportXml(callback, *fits)

View File

@@ -25,6 +25,9 @@ from eos.types import State, Slot, Module, Cargo, Fit, Ship, Drone, Implant, Boo
import service
import wx
import logging
import config
import collections
import json
logger = logging.getLogger("pyfa.service.port")
@@ -34,9 +37,63 @@ except ImportError:
from utils.compat import OrderedDict
EFT_SLOT_ORDER = [Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM]
INV_FLAGS = {
Slot.LOW: 11,
Slot.MED: 19,
Slot.HIGH: 27,
Slot.RIG: 92,
Slot.SUBSYSTEM: 125}
class Port(object):
"""Service which houses all import/export format functions"""
@classmethod
def exportCrest(cls, ofit, callback=None):
# A few notes:
# max fit name length is 50 characters
# Most keys are created simply because they are required, but bogus data is okay
nested_dict = lambda: collections.defaultdict(nested_dict)
fit = nested_dict()
sCrest = service.Crest.getInstance()
eve = sCrest.eve
# max length is 50 characters
name = ofit.name[:47] + '...' if len(ofit.name) > 50 else ofit.name
fit['name'] = name
fit['ship']['href'] = "%stypes/%d/"%(eve._authed_endpoint, ofit.ship.item.ID)
fit['ship']['id'] = ofit.ship.item.ID
fit['ship']['name'] = ''
fit['description'] = "<pyfa:%d />"%ofit.ID
fit['items'] = []
slotNum = {}
for module in ofit.modules:
if module.isEmpty:
continue
item = nested_dict()
slot = module.slot
if slot == Slot.SUBSYSTEM:
# Order of subsystem matters based on this attr. See GH issue #130
slot = int(module.getModifiedItemAttr("subSystemSlot"))
item['flag'] = slot
else:
if not slot in slotNum:
slotNum[slot] = INV_FLAGS[slot]
item['flag'] = slotNum[slot]
slotNum[slot] += 1
item['quantity'] = 1
item['type']['href'] = "%stypes/%d/"%(eve._authed_endpoint, module.item.ID)
item['type']['id'] = module.item.ID
item['type']['name'] = ''
fit['items'].append(item)
return json.dumps(fit)
@classmethod
def importAuto(cls, string, path=None, activeFit=None, callback=None, encoding=None):
@@ -48,6 +105,10 @@ class Port(object):
if re.match("<", firstLine):
return "XML", cls.importXml(string, callback, encoding)
# If JSON-style start, parse os CREST/JSON
if firstLine[0] == '{':
return "JSON", (cls.importCrest(string),)
# If we've got source file name which is used to describe ship name
# and first line contains something like [setup name], detect as eft config file
if re.match("\[.*\]", firstLine) and path is not None:
@@ -63,6 +124,47 @@ class Port(object):
# Use DNA format for all other cases
return "DNA", (cls.importDna(string),)
@staticmethod
def importCrest(str):
fit = json.loads(str)
sMkt = service.Market.getInstance()
f = Fit()
f.name = fit['name']
try:
f.ship = Ship(sMkt.getItem(fit['ship']['id']))
except:
return None
items = fit['items']
items.sort(key=lambda k: k['flag'])
for module in items:
try:
item = sMkt.getItem(module['type']['id'], eager="group.category")
if item.category.name == "Drone":
d = Drone(item)
d.amount = module['quantity']
f.drones.append(d)
elif item.category.name == "Charge":
c = Cargo(item)
c.amount = module['quantity']
f.cargo.append(c)
else:
try:
m = Module(item)
# When item can't be added to any slot (unknown item or just charge), ignore it
except ValueError:
continue
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
f.modules.append(m)
except:
continue
return f
@staticmethod
def importDna(string):
sMkt = service.Market.getInstance()

View File

@@ -0,0 +1,13 @@
import logging
class NullHandler(logging.Handler):
def emit(self, record):
pass
logger = logging.getLogger('pycrest')
logger.addHandler(NullHandler())
version = "0.0.1"
from .eve import EVE

24
service/pycrest/compat.py Normal file
View File

@@ -0,0 +1,24 @@
import sys
PY3 = sys.version_info[0] == 3
if PY3: # pragma: no cover
string_types = str,
text_type = str
binary_type = bytes
else: # pragma: no cover
string_types = basestring,
text_type = unicode
binary_type = str
def text_(s, encoding='latin-1', errors='strict'): # pragma: no cover
if isinstance(s, binary_type):
return s.decode(encoding, errors)
return s
def bytes_(s, encoding='latin-1', errors='strict'): # pragma: no cover
if isinstance(s, text_type):
return s.encode(encoding, errors)
return s

View File

@@ -0,0 +1,2 @@
class APIException(Exception):
pass

322
service/pycrest/eve.py Normal file
View File

@@ -0,0 +1,322 @@
import os
import base64
import time
import zlib
import requests
from . import version
from compat import bytes_, text_
from errors import APIException
from weak_ciphers import WeakCiphersAdapter
try:
from urllib.parse import urlparse, urlunparse, parse_qsl
except ImportError: # pragma: no cover
from urlparse import urlparse, urlunparse, parse_qsl
try:
import pickle
except ImportError: # pragma: no cover
import cPickle as pickle
try:
from urllib.parse import quote
except ImportError: # pragma: no cover
from urllib import quote
import logging
import re
logger = logging.getLogger("pycrest.eve")
cache_re = re.compile(r'max-age=([0-9]+)')
class APICache(object):
def put(self, key, value):
raise NotImplementedError
def get(self, key):
raise NotImplementedError
def invalidate(self, key):
raise NotImplementedError
class FileCache(APICache):
def __init__(self, path):
self._cache = {}
self.path = path
if not os.path.isdir(self.path):
os.mkdir(self.path, 0o700)
def _getpath(self, key):
return os.path.join(self.path, str(hash(key)) + '.cache')
def put(self, key, value):
with open(self._getpath(key), 'wb') as f:
f.write(zlib.compress(pickle.dumps(value, -1)))
self._cache[key] = value
def get(self, key):
if key in self._cache:
return self._cache[key]
try:
with open(self._getpath(key), 'rb') as f:
return pickle.loads(zlib.decompress(f.read()))
except IOError as ex:
if ex.errno == 2: # file does not exist (yet)
return None
else:
raise
def invalidate(self, key):
self._cache.pop(key, None)
try:
os.unlink(self._getpath(key))
except OSError as ex:
if ex.errno == 2: # does not exist
pass
else:
raise
class DictCache(APICache):
def __init__(self):
self._dict = {}
def get(self, key):
return self._dict.get(key, None)
def put(self, key, value):
self._dict[key] = value
def invalidate(self, key):
self._dict.pop(key, None)
class APIConnection(object):
def __init__(self, additional_headers=None, user_agent=None, cache_dir=None, cache=None):
# Set up a Requests Session
session = requests.Session()
if additional_headers is None:
additional_headers = {}
if user_agent is None:
user_agent = "PyCrest/{0}".format(version)
session.headers.update({
"User-Agent": user_agent,
"Accept": "application/json",
})
session.headers.update(additional_headers)
session.mount('https://public-crest.eveonline.com',
WeakCiphersAdapter())
self._session = session
if cache:
if isinstance(cache, APICache):
self.cache = cache # Inherit from parents
elif isinstance(cache, type):
self.cache = cache() # Instantiate a new cache
elif cache_dir:
self.cache_dir = cache_dir
self.cache = FileCache(self.cache_dir)
else:
self.cache = DictCache()
def get(self, resource, params=None):
logger.debug('Getting resource %s', resource)
if params is None:
params = {}
# remove params from resource URI (needed for paginated stuff)
parsed_uri = urlparse(resource)
qs = parsed_uri.query
resource = urlunparse(parsed_uri._replace(query=''))
prms = {}
for tup in parse_qsl(qs):
prms[tup[0]] = tup[1]
# params supplied to self.get() override parsed params
for key in params:
prms[key] = params[key]
# check cache
key = (resource, frozenset(self._session.headers.items()), frozenset(prms.items()))
cached = self.cache.get(key)
if cached and cached['cached_until'] > time.time():
logger.debug('Cache hit for resource %s (params=%s)', resource, prms)
return cached
elif cached:
logger.debug('Cache stale for resource %s (params=%s)', resource, prms)
self.cache.invalidate(key)
else:
logger.debug('Cache miss for resource %s (params=%s', resource, prms)
logger.debug('Getting resource %s (params=%s)', resource, prms)
res = self._session.get(resource, params=prms)
if res.status_code != 200:
raise APIException("Got unexpected status code from server: %i" % res.status_code)
ret = res.json()
# cache result
expires = self._get_expires(res)
if expires > 0:
ret.update({'cached_until': time.time() + expires})
self.cache.put(key, ret)
return ret
def _get_expires(self, response):
if 'Cache-Control' not in response.headers:
return 0
if any([s in response.headers['Cache-Control'] for s in ['no-cache', 'no-store']]):
return 0
match = cache_re.search(response.headers['Cache-Control'])
if match:
return int(match.group(1))
return 0
class EVE(APIConnection):
def __init__(self, **kwargs):
self.api_key = kwargs.pop('api_key', None)
self.client_id = kwargs.pop('client_id', None)
self.redirect_uri = kwargs.pop('redirect_uri', None)
if kwargs.pop('testing', False):
self._public_endpoint = "http://public-crest-sisi.testeveonline.com/"
self._authed_endpoint = "https://api-sisi.testeveonline.com/"
self._image_server = "https://image.testeveonline.com/"
self._oauth_endpoint = "https://sisilogin.testeveonline.com/oauth"
else:
self._public_endpoint = "https://public-crest.eveonline.com/"
self._authed_endpoint = "https://crest-tq.eveonline.com/"
self._image_server = "https://image.eveonline.com/"
self._oauth_endpoint = "https://login.eveonline.com/oauth"
self._endpoint = self._public_endpoint
self._cache = {}
self._data = None
self.token = None
self.refresh_token = None
self.expires = None
APIConnection.__init__(self, **kwargs)
def __call__(self):
if not self._data:
self._data = APIObject(self.get(self._endpoint), self)
return self._data
def __getattr__(self, item):
return self._data.__getattr__(item)
def auth_uri(self, scopes=None, state=None):
s = [] if not scopes else scopes
grant_type = "token" if self.api_key is None else "code"
return "%s/authorize?response_type=%s&redirect_uri=%s&client_id=%s%s%s" % (
self._oauth_endpoint,
grant_type,
self.redirect_uri,
self.client_id,
"&scope=%s" % '+'.join(s) if scopes else '',
"&state=%s" % state if state else ''
)
def _authorize(self, params):
auth = text_(base64.b64encode(bytes_("%s:%s" % (self.client_id, self.api_key))))
headers = {"Authorization": "Basic %s" % auth}
res = self._session.post("%s/token" % self._oauth_endpoint, params=params, headers=headers)
if res.status_code != 200:
raise APIException("Got unexpected status code from API: %i" % res.status_code)
return res.json()
def set_auth_values(self, res):
self.__class__ = AuthedConnection
self.token = res['access_token']
self.refresh_token = res['refresh_token']
self.expires = int(time.time()) + res['expires_in']
self._endpoint = self._authed_endpoint
self._session.headers.update({"Authorization": "Bearer %s" % self.token})
def authorize(self, code):
res = self._authorize(params={"grant_type": "authorization_code", "code": code})
self.set_auth_values(res)
def refr_authorize(self, refresh_token):
res = self._authorize(params={"grant_type": "refresh_token", "refresh_token": refresh_token})
self.set_auth_values(res)
def temptoken_authorize(self, access_token=None, expires_in=0, refresh_token=None):
self.set_auth_values({'access_token': access_token,
'refresh_token': refresh_token,
'expires_in': expires_in})
class AuthedConnection(EVE):
def __call__(self):
if not self._data:
self._data = APIObject(self.get(self._endpoint), self)
return self._data
def whoami(self):
#if 'whoami' not in self._cache:
# print "Setting this whoami cache"
# self._cache['whoami'] = self.get("%s/verify" % self._oauth_endpoint)
return self.get("%s/verify" % self._oauth_endpoint)
def get(self, resource, params=None):
if self.refresh_token and int(time.time()) >= self.expires:
self.refr_authorize(self.refresh_token)
return super(self.__class__, self).get(resource, params)
def post(self, resource, data, params=None):
if self.refresh_token and int(time.time()) >= self.expires:
self.refr_authorize(self.refresh_token)
return self._session.post(resource, data=data, params=params)
def delete(self, resource, params=None):
if self.refresh_token and int(time.time()) >= self.expires:
self.refr_authorize(self.refresh_token)
return self._session.delete(resource, params=params)
class APIObject(object):
def __init__(self, parent, connection):
self._dict = {}
self.connection = connection
for k, v in parent.items():
if type(v) is dict:
self._dict[k] = APIObject(v, connection)
elif type(v) is list:
self._dict[k] = self._wrap_list(v)
else:
self._dict[k] = v
def _wrap_list(self, list_):
new = []
for item in list_:
if type(item) is dict:
new.append(APIObject(item, self.connection))
elif type(item) is list:
new.append(self._wrap_list(item))
else:
new.append(item)
return new
def __getattr__(self, item):
if item in self._dict:
return self._dict[item]
raise AttributeError(item)
def __call__(self, **kwargs):
# Caching is now handled by APIConnection
if 'href' in self._dict:
return APIObject(self.connection.get(self._dict['href'], params=kwargs), self.connection)
else:
return self
def __str__(self): # pragma: no cover
return self._dict.__str__()
def __repr__(self): # pragma: no cover
return self._dict.__repr__()

View File

@@ -0,0 +1,140 @@
import datetime
import ssl
import sys
import warnings
from requests.adapters import HTTPAdapter
try:
from requests.packages import urllib3
from requests.packages.urllib3.util import ssl_
from requests.packages.urllib3.exceptions import (
SystemTimeWarning,
SecurityWarning,
)
from requests.packages.urllib3.packages.ssl_match_hostname import \
match_hostname
except:
import urllib3
from urllib3.util import ssl_
from urllib3.exceptions import (
SystemTimeWarning,
SecurityWarning,
)
from urllib3.packages.ssl_match_hostname import \
match_hostname
class WeakCiphersHTTPSConnection(
urllib3.connection.VerifiedHTTPSConnection): # pragma: no cover
# Python versions >=2.7.9 and >=3.4.1 do not (by default) allow ciphers
# with MD5. Unfortunately, the CREST public server _only_ supports
# TLS_RSA_WITH_RC4_128_MD5 (as of 5 Jan 2015). The cipher list below is
# nearly identical except for allowing that cipher as a last resort (and
# excluding export versions of ciphers).
DEFAULT_CIPHERS = (
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:'
'ECDH+HIGH:DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:'
'RSA+3DES:ECDH+RC4:DH+RC4:RSA+RC4:!aNULL:!eNULL:!EXP:-MD5:RSA+RC4+MD5'
)
def __init__(self, host, port, ciphers=None, **kwargs):
self.ciphers = ciphers if ciphers is not None else self.DEFAULT_CIPHERS
super(WeakCiphersHTTPSConnection, self).__init__(host, port, **kwargs)
def connect(self):
# Yup, copied in VerifiedHTTPSConnection.connect just to change the
# default cipher list.
# Add certificate verification
conn = self._new_conn()
resolved_cert_reqs = ssl_.resolve_cert_reqs(self.cert_reqs)
resolved_ssl_version = ssl_.resolve_ssl_version(self.ssl_version)
hostname = self.host
if getattr(self, '_tunnel_host', None):
# _tunnel_host was added in Python 2.6.3
# (See: http://hg.python.org/cpython/rev/0f57b30a152f)
self.sock = conn
# Calls self._set_hostport(), so self.host is
# self._tunnel_host below.
self._tunnel()
# Mark this connection as not reusable
self.auto_open = 0
# Override the host with the one we're requesting data from.
hostname = self._tunnel_host
is_time_off = datetime.date.today() < urllib3.connection.RECENT_DATE
if is_time_off:
warnings.warn((
'System time is way off (before {0}). This will probably '
'lead to SSL verification errors').format(
urllib3.connection.RECENT_DATE),
SystemTimeWarning
)
# Wrap socket using verification with the root certs in
# trusted_root_certs
self.sock = ssl_.ssl_wrap_socket(conn, self.key_file, self.cert_file,
cert_reqs=resolved_cert_reqs,
ca_certs=self.ca_certs,
server_hostname=hostname,
ssl_version=resolved_ssl_version,
ciphers=self.ciphers)
if self.assert_fingerprint:
ssl_.assert_fingerprint(self.sock.getpeercert(binary_form=True),
self.assert_fingerprint)
elif resolved_cert_reqs != ssl.CERT_NONE \
and self.assert_hostname is not False:
cert = self.sock.getpeercert()
if not cert.get('subjectAltName', ()):
warnings.warn((
'Certificate has no `subjectAltName`, falling back to check for a `commonName` for now. '
'This feature is being removed by major browsers and deprecated by RFC 2818. '
'(See https://github.com/shazow/urllib3/issues/497 for details.)'),
SecurityWarning
)
match_hostname(cert, self.assert_hostname or hostname)
self.is_verified = (resolved_cert_reqs == ssl.CERT_REQUIRED
or self.assert_fingerprint is not None)
class WeakCiphersHTTPSConnectionPool(
urllib3.connectionpool.HTTPSConnectionPool):
ConnectionCls = WeakCiphersHTTPSConnection
class WeakCiphersPoolManager(urllib3.poolmanager.PoolManager):
def _new_pool(self, scheme, host, port):
if scheme == 'https':
return WeakCiphersHTTPSConnectionPool(host, port,
**(self.connection_pool_kw))
return super(WeakCiphersPoolManager, self)._new_pool(scheme, host,
port)
class WeakCiphersAdapter(HTTPAdapter):
""""Transport adapter" that allows us to use TLS_RSA_WITH_RC4_128_MD5."""
def init_poolmanager(self, connections, maxsize, block=False,
**pool_kwargs):
# Rewrite of the requests.adapters.HTTPAdapter.init_poolmanager method
# to use WeakCiphersPoolManager instead of urllib3's PoolManager
self._pool_connections = connections
self._pool_maxsize = maxsize
self._pool_block = block
self.poolmanager = WeakCiphersPoolManager(num_pools=connections,
maxsize=maxsize, block=block, strict=True, **pool_kwargs)

101
service/server.py Normal file
View File

@@ -0,0 +1,101 @@
import BaseHTTPServer
import urlparse
import socket
import thread
import wx
from wx.lib.pubsub import setupkwargs
from wx.lib.pubsub import pub
import logging
logger = logging.getLogger(__name__)
HTML = '''
<!DOCTYPE html>
<html>
<body>
Done. Please close this window.
<script type="text/javascript">
function extractFromHash(name, hash) {
var match = hash.match(new RegExp(name + "=([^&]+)"));
return !!match && match[1];
}
var hash = window.location.hash;
var token = extractFromHash("access_token", hash);
if (token){
var redirect = window.location.origin.concat('/?', window.location.hash.substr(1));
window.location = redirect;
}
else {
console.log("do nothing");
}
</script>
</body>
</html>
'''
# https://github.com/fuzzysteve/CREST-Market-Downloader/
class AuthHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/favicon.ico":
return
parsed_path = urlparse.urlparse(self.path)
parts = urlparse.parse_qs(parsed_path.query)
self.send_response(200)
self.end_headers()
self.wfile.write(HTML)
wx.CallAfter(pub.sendMessage, 'sso_login', message=parts)
def log_message(self, format, *args):
return
# http://code.activestate.com/recipes/425210-simple-stoppable-server-using-socket-timeout/
class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
def server_bind(self):
BaseHTTPServer.HTTPServer.server_bind(self)
# Allow listening for 60 seconds
sec = 60
self.socket.settimeout(0.5)
self.max_tries = sec / self.socket.gettimeout()
self.tries = 0
self.run = True
def get_request(self):
while self.run:
try:
sock, addr = self.socket.accept()
sock.settimeout(None)
return (sock, addr)
except socket.timeout:
pass
def stop(self):
self.run = False
self.server_close()
def handle_timeout(self):
logger.debug("Number of tries: %d"%self.tries)
self.tries += 1
if self.tries == self.max_tries:
logger.debug("Server timed out waiting for connection")
self.stop()
def serve(self):
while self.run:
try:
self.handle_request()
except TypeError:
pass
if __name__ == "__main__":
httpd = StoppableHTTPServer(('', 6461), AuthHandler)
thread.start_new_thread(httpd.serve, ())
raw_input("Press <RETURN> to stop server\n")
httpd.stop()

View File

@@ -263,4 +263,30 @@ class UpdateSettings():
def set(self, type, value):
self.serviceUpdateSettings[type] = value
class CRESTSettings():
_instance = None
@classmethod
def getInstance(cls):
if cls._instance is None:
cls._instance = CRESTSettings()
return cls._instance
def __init__(self):
# mode
# 0 - Implicit authentication
# 1 - User-supplied client details
serviceCRESTDefaultSettings = {"mode": 0, "server": 0, "clientID": "", "clientSecret": ""}
self.serviceCRESTSettings = SettingsProvider.getInstance().getSettings("pyfaServiceCRESTSettings", serviceCRESTDefaultSettings)
def get(self, type):
return self.serviceCRESTSettings[type]
def set(self, type, value):
self.serviceCRESTSettings[type] = value
# @todo: migrate fit settings (from fit service) here?

View File

@@ -4,11 +4,12 @@ Distribution builder for pyfa.
Windows executable: python setup.py build
Windows executable + installer: python setup.py bdist_msi
"""
import requests.certs
# The modules that contain the bulk of teh source
packages = ['eos', 'gui', 'service', 'utils']
# Extra files that will be copied into the root directory
include_files = ['eve.db', 'LICENSE', 'README.md']
include_files = ['eve.db', 'LICENSE', 'README.md', (requests.certs.where(),'cacert.pem')]
# this is read by dist.py to package the icons
icon_dirs = ['gui', 'icons', 'renders']