Merge branch 'master' into py3EFFS

This commit is contained in:
Maru Maru
2018-05-20 00:39:08 -04:00
56 changed files with 875 additions and 973 deletions

View File

@@ -15,12 +15,8 @@ The latest version along with release notes can always be found on the project's
Windows and OS X users are supplied self-contained builds of pyfa on the [latest releases](https://github.com/pyfa-org/Pyfa/releases/latest) page. An `.exe` installer is also available for Windows builds. Linux users can run pyfa using their distribution's Python interpreter. There is no official self-contained package for Linux, however, there are a number of third-party packages available through distribution-specific repositories. Windows and OS X users are supplied self-contained builds of pyfa on the [latest releases](https://github.com/pyfa-org/Pyfa/releases/latest) page. An `.exe` installer is also available for Windows builds. Linux users can run pyfa using their distribution's Python interpreter. There is no official self-contained package for Linux, however, there are a number of third-party packages available through distribution-specific repositories.
#### OS X #### OS X
There are two different distributives for OS X: `-mac` and `-mac-deprecated`.
* `-mac`: based on wxPython 3.0.2.0 and has updated libraries. This is the recommended build. Apart from the official release, there is also a [Homebrew](http://brew.sh) option for installing pyfa on OS X. Please note this is maintained by a third-party and is not tested by pyfa developers. Simply fire up in terminal:
* `-mac-deprecated`: utilizes older binaries running on wxPython 2.8; because of this, some features are not available (currently CREST support and Attribute Overrides). Additionally, as development happens primarily on wxPython 3.0, a few GUI bugs may pop up as `-mac-deprecated` is not actively tested. However, due to some general issues with wxPython 3.0, especially on some newer OS X versions, `-mac-deprecated` is still offered for those that need it.
There is also a [Homebrew](http://brew.sh) option for installing pyfa on OS X. Please note this is maintained by a third-party and is not tested by pyfa developers. Simply fire up in terminal:
``` ```
$ brew install Caskroom/cask/pyfa $ brew install Caskroom/cask/pyfa
``` ```

View File

@@ -24,8 +24,8 @@ saveInRoot = False
# Version data # Version data
version = "2.0.0b5" version = "2.0.2"
tag = "git" tag = "Stable"
expansionName = "YC120.3" expansionName = "YC120.3"
expansionVersion = "1.8" expansionVersion = "1.8"
evemonMinVersion = "4081" evemonMinVersion = "4081"
@@ -42,7 +42,6 @@ logging_setup = None
cipher = None cipher = None
clientHash = None clientHash = None
ESI_AUTH_PROXY = "https://www.pyfa.io" # "http://localhost:5015"
ESI_CACHE = 'esi_cache' ESI_CACHE = 'esi_cache'
LOGLEVEL_MAP = { LOGLEVEL_MAP = {
@@ -141,7 +140,7 @@ def defPaths(customSavePath=None):
# os.environ["SSL_CERT_FILE"] = os.path.join(pyfaPath, "cacert.pem") # os.environ["SSL_CERT_FILE"] = os.path.join(pyfaPath, "cacert.pem")
# The database where we store all the fits etc # The database where we store all the fits etc
saveDB = os.path.join(savePath, "saveddata-py3-dev.db") saveDB = os.path.join(savePath, "saveddata.db")
# The database where the static EVE data from the datadump is kept. # The database where the static EVE data from the datadump is kept.
# This is not the standard sqlite datadump but a modified version created by eos # This is not the standard sqlite datadump but a modified version created by eos

View File

@@ -30,6 +30,8 @@ added_files = [
import_these = [] import_these = []
icon = os.path.join(os.getcwd(), "dist_assets", "mac", "pyfa.icns")
# Walk directories that do dynamic importing # Walk directories that do dynamic importing
paths = ('eos/effects', 'eos/db/migrations', 'service/conversions') paths = ('eos/effects', 'eos/db/migrations', 'service/conversions')
for root, folders, files in chain.from_iterable(os.walk(path) for path in paths): for root, folders, files in chain.from_iterable(os.walk(path) for path in paths):
@@ -65,10 +67,10 @@ exe = EXE(pyz,
upx=True, upx=True,
runtime_tmpdir=None, runtime_tmpdir=None,
console=False , console=False ,
icon='dist_assets/mac/pyfa.icns', icon=icon,
) )
app = BUNDLE(exe, app = BUNDLE(exe,
name='pyfa.app', name='pyfa.app',
icon=None, icon=icon,
bundle_identifier=None) bundle_identifier=None)

46
dist_assets/win/dist.py Normal file
View File

@@ -0,0 +1,46 @@
# helper script to zip up pyinstaller distribution and create installer file
import os.path
from subprocess import call
import zipfile
def zipdir(path, zip):
for root, dirs, files in os.walk(path):
for file in files:
zip.write(os.path.join(root, file))
config = {}
exec(compile(open("config.py").read(), "config.py", 'exec'), config)
iscc = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" # inno script location via wine
print("Creating archive")
source = os.path.join(os.getcwd(), "dist", "pyfa")
fileName = "pyfa-{}-{}-{}-win".format(
config['version'],
config['expansionName'].lower(),
config['expansionVersion']
)
archive = zipfile.ZipFile(os.path.join(os.getcwd(), "dist", fileName + ".zip"), 'w', compression=zipfile.ZIP_DEFLATED)
zipdir(source, archive)
archive.close()
print("Compiling EXE")
expansion = "%s %s" % (config['expansionName'], config['expansionVersion']),
call([
iscc,
os.path.join(os.getcwd(), "dist_assets", "win", "pyfa-setup.iss"),
"/dMyAppVersion=%s" % (config['version']),
"/dMyAppExpansion=%s" % (expansion),
"/dMyAppDir=%s" % source,
"/dMyOutputDir=%s" % os.path.join(os.getcwd(), "dist"),
"/dMyOutputFile=%s" % fileName]) # stdout=devnull, stderr=devnull
print("Done")

View File

@@ -19,7 +19,8 @@
#define MyAppExeName "pyfa.exe" #define MyAppExeName "pyfa.exe"
; What version starts with the new structure (1.x.0). This is used to determine if we run directory structure cleanup ; What version starts with the new structure (1.x.0). This is used to determine if we run directory structure cleanup
#define VersionFlag 16 #define MajorVersionFlag 2
#define MinorVersionFlag 0
#ifndef MyOutputFile #ifndef MyOutputFile
#define MyOutputFile LowerCase(StringChange(MyAppName+'-'+MyAppVersion+'-'+MyAppExpansion+'-win-wx3', " ", "-")) #define MyOutputFile LowerCase(StringChange(MyAppName+'-'+MyAppVersion+'-'+MyAppExpansion+'-win-wx3', " ", "-"))
@@ -138,15 +139,19 @@ var
V: Integer; V: Integer;
iResultCode: Integer; iResultCode: Integer;
sUnInstallString: string; sUnInstallString: string;
iOldVersion: Cardinal; iOldVersionMajor: Cardinal;
iOldVersionMinor: Cardinal;
begin begin
Result := True; // in case when no previous version is found Result := True; // in case when no previous version is found
if RegValueExists(HKEY_LOCAL_MACHINE,'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1', 'UninstallString') then //Your App GUID/ID if RegValueExists(HKEY_LOCAL_MACHINE,'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1', 'UninstallString') then //Your App GUID/ID
begin begin
RegQueryDWordValue(HKEY_LOCAL_MACHINE, RegQueryDWordValue(HKEY_LOCAL_MACHINE,
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1', 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1',
'MinorVersion', iOldVersion); 'MajorVersion', iOldVersionMajor);
if iOldVersion < {#VersionFlag} then // If old version with old structure is installed. RegQueryDWordValue(HKEY_LOCAL_MACHINE,
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1',
'MinorVersion', iOldVersionMinor);
if (iOldVersionMajor < {#MajorVersionFlag}) or ((iOldVersionMajor = {#MajorVersionFlag}) and (iOldVersionMinor < {#MinorVersionFlag})) then // If old version with old structure is installed.
begin begin
V := MsgBox(ExpandConstant('An old version of pyfa was detected. Due to recent changes in the application structure, you must uninstall the previous version first. This will not affect your user data (saved fittings, characters, etc.). Do you want to uninstall now?'), mbInformation, MB_YESNO); //Custom Message if App installed V := MsgBox(ExpandConstant('An old version of pyfa was detected. Due to recent changes in the application structure, you must uninstall the previous version first. This will not affect your user data (saved fittings, characters, etc.). Do you want to uninstall now?'), mbInformation, MB_YESNO); //Custom Message if App installed
if V = IDYES then if V = IDYES then

View File

@@ -80,3 +80,4 @@ coll = COLLECT(
name='pyfa', name='pyfa',
icon='dist_assets/win/pyfa.ico', icon='dist_assets/win/pyfa.ico',
) )

View File

@@ -29,9 +29,6 @@ from eos.saveddata.user import User
from eos.saveddata.character import Character, Skill from eos.saveddata.character import Character, Skill
from eos.saveddata.ssocharacter import SsoCharacter from eos.saveddata.ssocharacter import SsoCharacter
characters_table = Table("characters", saveddata_meta, characters_table = Table("characters", saveddata_meta,
Column("ID", Integer, primary_key=True), Column("ID", Integer, primary_key=True),
Column("name", String, nullable=False), Column("name", String, nullable=False),

View File

@@ -5,6 +5,6 @@ type = "passive"
def handler(fit, src, context): def handler(fit, src, context):
groups = ("Structure Anti-Subcapital Missile", "Structure Anti-Capital Missile") groups = ("Structure Anti-Subcapital Missile", "Structure Anti-Capital Missile")
fit.modules.filteredItemBoost(lambda mod: mod.charge.group.name in groups, fit.modules.filteredChargeBoost(lambda mod: mod.charge.group.name in groups,
"aoeVelocity", src.getModifiedItemAttr("structureRigMissileExploVeloBonus"), "aoeVelocity", src.getModifiedItemAttr("structureRigMissileExploVeloBonus"),
stackingPenalties=True) stackingPenalties=True)

View File

@@ -4,6 +4,6 @@ type = "passive"
def handler(fit, src, context): def handler(fit, src, context):
groups = ("Structure Anti-Subcapital Missile", "Structure Anti-Capital Missile") groups = ("Structure Anti-Subcapital Missile", "Structure Anti-Capital Missile")
fit.modules.filteredItemBoost(lambda mod: mod.charge.group.name in groups, fit.modules.filteredChargeBoost(lambda mod: mod.charge.group.name in groups,
"maxVelocity", src.getModifiedItemAttr("structureRigMissileVelocityBonus"), "maxVelocity", src.getModifiedItemAttr("structureRigMissileVelocityBonus"),
stackingPenalties=True) stackingPenalties=True)

View File

@@ -17,8 +17,8 @@ def handler(fit, module, context):
# this is such a dirty hack # this is such a dirty hack
for mod in fit.modules: for mod in fit.modules:
if not mod.isEmpty and mod.state > State.ONLINE and ( if not mod.isEmpty and mod.state > State.ONLINE and (
mod.item.requiresSkill("Micro Jump Drive Operation") mod.item.requiresSkill("Micro Jump Drive Operation") or
or mod.item.requiresSkill("High Speed Maneuvering") mod.item.requiresSkill("High Speed Maneuvering")
): ):
mod.state = State.ONLINE mod.state = State.ONLINE
if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > State.ONLINE: if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > State.ONLINE:

View File

@@ -52,7 +52,6 @@ class Character(object):
self.addSkill(Skill(self, item.ID, self.defaultLevel)) self.addSkill(Skill(self, item.ID, self.defaultLevel))
self.__implants = HandledImplantBoosterList() self.__implants = HandledImplantBoosterList()
self.apiKey = None
@reconstructor @reconstructor
def init(self): def init(self):
@@ -171,7 +170,6 @@ class Character(object):
if x.client == clientHash: if x.client == clientHash:
self.__ssoCharacters.remove(x) self.__ssoCharacters.remove(x)
def getSsoCharacter(self, clientHash): def getSsoCharacter(self, clientHash):
return next((x for x in self.__ssoCharacters if x.client == clientHash), None) return next((x for x in self.__ssoCharacters if x.client == clientHash), None)
@@ -274,20 +272,17 @@ class Character(object):
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
copy = Character("%s copy" % self.name, initSkills=False) copy = Character("%s copy" % self.name, initSkills=False)
copy.apiKey = self.apiKey
copy.apiID = self.apiID
for skill in self.skills: for skill in self.skills:
copy.addSkill(Skill(copy, skill.itemID, skill.level, False, skill.learned)) copy.addSkill(Skill(copy, skill.itemID, skill.level, False, skill.learned))
return copy return copy
@validates("ID", "name", "apiKey", "ownerID") @validates("ID", "name", "ownerID")
def validator(self, key, val): def validator(self, key, val):
map = { map = {
"ID" : lambda _val: isinstance(_val, int), "ID" : lambda _val: isinstance(_val, int),
"name" : lambda _val: True, "name" : lambda _val: True,
"apiKey" : lambda _val: _val is None or (isinstance(_val, str) and len(_val) > 0),
"ownerID": lambda _val: isinstance(_val, int) or _val is None "ownerID": lambda _val: isinstance(_val, int) or _val is None
} }

View File

@@ -1143,14 +1143,16 @@ class Fit(object):
except AttributeError: except AttributeError:
usesCap = False usesCap = False
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
# Normal Repairers # Normal Repairers
if usesCap and not mod.charge: if usesCap and not mod.charge:
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
sustainable[attr] -= amount / (cycleTime / 1000.0) sustainable[attr] -= amount / (cycleTime / 1000.0)
repairers.append(mod) repairers.append(mod)
# Ancillary Armor reps etc # Ancillary Armor reps etc
elif usesCap and mod.charge: elif usesCap and mod.charge:
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
if mod.charge.name == "Nanite Repair Paste": if mod.charge.name == "Nanite Repair Paste":
multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1 multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
else: else:
@@ -1158,7 +1160,9 @@ class Fit(object):
sustainable[attr] -= amount * multiplier / (cycleTime / 1000.0) sustainable[attr] -= amount * multiplier / (cycleTime / 1000.0)
repairers.append(mod) repairers.append(mod)
# Ancillary Shield boosters etc # Ancillary Shield boosters etc
elif not usesCap: elif not usesCap and mod.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"):
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
if self.factorReload and mod.charge: if self.factorReload and mod.charge:
reloadtime = mod.reloadTime reloadtime = mod.reloadTime
else: else:

View File

@@ -144,10 +144,11 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return empty return empty
@classmethod @classmethod
def buildRack(cls, slot): def buildRack(cls, slot, num=None):
empty = Rack(None) empty = Rack(None)
empty.__slot = slot empty.__slot = slot
empty.dummySlot = slot empty.dummySlot = slot
empty.num = num
return empty return empty
@property @property
@@ -799,6 +800,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
class Rack(Module): class Rack(Module):
""" """
This is simply the Module class named something else to differentiate This is simply the Module class named something else to differentiate
it for app logic. This class does not do anything special it for app logic. The only thing interesting about it is the num property,
which is the number of slots for this rack
""" """
pass num = None

View File

@@ -32,24 +32,15 @@ class SsoCharacter(object):
self.accessToken = accessToken self.accessToken = accessToken
self.refreshToken = refreshToken self.refreshToken = refreshToken
self.accessTokenExpires = None self.accessTokenExpires = None
self.esi_client = None
@reconstructor @reconstructor
def init(self): def init(self):
self.esi_client = None pass
def get_sso_data(self):
""" Little "helper" function to get formated data for esipy security
"""
return {
'access_token': self.accessToken,
'refresh_token': self.refreshToken,
'expires_in': (
self.accessTokenExpires - datetime.datetime.utcnow()
).total_seconds()
}
def is_token_expired(self):
if self.accessTokenExpires is None:
return True
return datetime.datetime.now() >= self.accessTokenExpires
def __repr__(self): def __repr__(self):
return "SsoCharacter(ID={}, name={}, client={}) at {}".format( return "SsoCharacter(ID={}, name={}, client={}) at {}".format(

View File

@@ -58,9 +58,9 @@ class ModuleAmmoPicker(ContextMenu):
def turretSorter(self, charge): def turretSorter(self, charge):
damage = 0 damage = 0
range_ = (self.module.getModifiedItemAttr("maxRange")) * \ range_ = (self.module.item.getAttribute("maxRange")) * \
(charge.getAttribute("weaponRangeMultiplier") or 1) (charge.getAttribute("weaponRangeMultiplier") or 1)
falloff = (self.module.getModifiedItemAttr("falloff")) * \ falloff = (self.module.item.getAttribute("falloff")) * \
(charge.getAttribute("fallofMultiplier") or 1) (charge.getAttribute("fallofMultiplier") or 1)
for type_ in self.DAMAGE_TYPES: for type_ in self.DAMAGE_TYPES:
d = charge.getAttribute("%sDamage" % type_) d = charge.getAttribute("%sDamage" % type_)

View File

@@ -43,23 +43,65 @@ class PFEsiPref(PreferenceView):
rbSizer = wx.BoxSizer(wx.HORIZONTAL) rbSizer = wx.BoxSizer(wx.HORIZONTAL)
self.rbMode = wx.RadioBox(panel, -1, "Login Authentication Method", wx.DefaultPosition, wx.DefaultSize, self.rbMode = wx.RadioBox(panel, -1, "Login Authentication Method", wx.DefaultPosition, wx.DefaultSize,
['Local Server', 'Manual'], 1, wx.RA_SPECIFY_COLS) ['Local Server', 'Manual'], 1, wx.RA_SPECIFY_COLS)
self.rbMode.SetItemToolTip(0, "This options starts a local webserver that the web application will call back to with information about the character login.") self.rbMode.SetItemToolTip(0, "This options starts a local webserver that the web application will call back to"
self.rbMode.SetItemToolTip(1, "This option prompts users to copy and paste information from the web application to allow for character login. Use this if having issues with the local server.") " with information about the character login.")
# self.rbServer = wx.RadioBox(panel, -1, "Server", wx.DefaultPosition, wx.DefaultSize, self.rbMode.SetItemToolTip(1, "This option prompts users to copy and paste information from the web application "
# ['Tranquility', 'Singularity'], 1, wx.RA_SPECIFY_COLS) "to allow for character login. Use this if having issues with the local server.")
self.rbSsoMode = wx.RadioBox(panel, -1, "SSO Mode", wx.DefaultPosition, wx.DefaultSize,
['pyfa.io', 'Custom application'], 1, wx.RA_SPECIFY_COLS)
self.rbSsoMode.SetItemToolTip(0, "This options routes SSO Logins through pyfa.io, allowing you to easily login "
"without any configuration. When in doubt, use this option.")
self.rbSsoMode.SetItemToolTip(1, "This option goes through EVE SSO directly, but requires more configuration. Use "
"this is pyfa.io is blocked for some reason, or if you do not wish to route data throguh pyfa.io.")
self.rbMode.SetSelection(self.settings.get('loginMode')) self.rbMode.SetSelection(self.settings.get('loginMode'))
# self.rbServer.SetSelection(self.settings.get('server')) self.rbSsoMode.SetSelection(self.settings.get('ssoMode'))
rbSizer.Add(self.rbSsoMode, 1, wx.ALL, 5)
rbSizer.Add(self.rbMode, 1, wx.TOP | wx.RIGHT, 5) 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.rbMode.Bind(wx.EVT_RADIOBOX, self.OnModeChange)
# self.rbServer.Bind(wx.EVT_RADIOBOX, self.OnServerChange) self.rbSsoMode.Bind(wx.EVT_RADIOBOX, self.OnSSOChange)
mainSizer.Add(rbSizer, 1, wx.ALL | wx.EXPAND, 0) mainSizer.Add(rbSizer, 1, wx.ALL | wx.EXPAND, 0)
timeoutSizer = wx.BoxSizer(wx.HORIZONTAL) detailsTitle = wx.StaticText(panel, wx.ID_ANY, "Custom Application", 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.inputClientID.Bind(wx.EVT_TEXT, self.OnClientDetailChange)
self.inputClientSecret.Bind(wx.EVT_TEXT, self.OnClientDetailChange)
mainSizer.Add(fgAddrSizer, 0, wx.EXPAND, 5)
# self.stTimout = wx.StaticText(panel, wx.ID_ANY, "Timeout (seconds):", wx.DefaultPosition, wx.DefaultSize, 0) # self.stTimout = wx.StaticText(panel, wx.ID_ANY, "Timeout (seconds):", wx.DefaultPosition, wx.DefaultSize, 0)
# self.stTimout.Wrap(-1) # self.stTimout.Wrap(-1)
@@ -112,6 +154,7 @@ class PFEsiPref(PreferenceView):
# self.ToggleProxySettings(self.settings.get('loginMode')) # self.ToggleProxySettings(self.settings.get('loginMode'))
self.ToggleSSOMode(self.settings.get('ssoMode'))
panel.SetSizer(mainSizer) panel.SetSizer(mainSizer)
panel.Layout() panel.Layout()
@@ -121,14 +164,31 @@ class PFEsiPref(PreferenceView):
def OnModeChange(self, event): def OnModeChange(self, event):
self.settings.set('loginMode', event.GetInt()) self.settings.set('loginMode', event.GetInt())
def OnServerChange(self, event): def OnSSOChange(self, event):
self.settings.set('server', event.GetInt()) self.settings.set('ssoMode', event.GetInt())
self.ToggleSSOMode(event.GetInt())
def OnBtnApply(self, event): def ToggleSSOMode(self, mode):
if mode:
self.stSetID.Enable()
self.inputClientID.Enable()
self.stSetSecret.Enable()
self.inputClientSecret.Enable()
self.rbMode.Disable()
else:
self.stSetID.Disable()
self.inputClientID.Disable()
self.stSetSecret.Disable()
self.inputClientSecret.Disable()
self.rbMode.Enable()
def OnClientDetailChange(self, evt):
self.settings.set('clientID', self.inputClientID.GetValue().strip()) self.settings.set('clientID', self.inputClientID.GetValue().strip())
self.settings.set('clientSecret', self.inputClientSecret.GetValue().strip()) self.settings.set('clientSecret', self.inputClientSecret.GetValue().strip())
sEsi = Esi.getInstance()
sEsi.delAllCharacters() # sEsi = Esi.getInstance()
# sEsi.delAllCharacters()
#
def getImage(self): def getImage(self):
return BitmapLoader.getBitmap("eve", "gui") return BitmapLoader.getBitmap("eve", "gui")

View File

@@ -150,10 +150,10 @@ class PFNetworkPref(PreferenceView):
proxy = self.settings.autodetect() proxy = self.settings.autodetect()
if proxy is not None: if proxy is not None:
addr, port = proxy addr, port = proxy
txt = addr + ":" + str(port) txt = addr + ":" + str(port)
else: else:
txt = "None" txt = "None"
self.stPSAutoDetected.SetLabel("Auto-detected: " + txt) self.stPSAutoDetected.SetLabel("Auto-detected: " + txt)
self.stPSAutoDetected.Disable() self.stPSAutoDetected.Disable()

View File

@@ -129,7 +129,7 @@ class FirepowerViewFull(StatsView):
# Remove effective label # Remove effective label
hsizer = self.headerPanel.GetSizer() hsizer = self.headerPanel.GetSizer()
hsizer.Hide(self.stEff) hsizer.Hide(self.stEff)
#self.stEff.Destroy() # self.stEff.Destroy()
# Get the new view # Get the new view
view = StatsView.getView("miningyieldViewFull")(self.parent) view = StatsView.getView("miningyieldViewFull")(self.parent)

View File

@@ -73,7 +73,7 @@ class BaseName(ViewColumn):
if stuff.slot == Slot.MODE: if stuff.slot == Slot.MODE:
return '─ Tactical Mode ─' return '─ Tactical Mode ─'
else: else:
return '{} Slots '.format(Slot.getName(stuff.slot).capitalize()) return '{} {} Slot{}'.format(stuff.num, Slot.getName(stuff.slot).capitalize(), '' if stuff.num == 1 else 's')
else: else:
return "" return ""
elif isinstance(stuff, Module): elif isinstance(stuff, Module):

View File

@@ -88,7 +88,7 @@ class FitSpawner(gui.multiSwitch.TabSpawner):
def handleDrag(self, type, fitID): def handleDrag(self, type, fitID):
if type == "fit": if type == "fit":
for page in self.multiSwitch.pages: for page in self.multiSwitch._pages:
if isinstance(page, FittingView) and page.activeFitID == fitID: if isinstance(page, FittingView) and page.activeFitID == fitID:
index = self.multiSwitch.GetPageIndex(page) index = self.multiSwitch.GetPageIndex(page)
self.multiSwitch.SetSelection(index) self.multiSwitch.SetSelection(index)
@@ -222,12 +222,15 @@ class FittingView(d.Display):
wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID)) wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID))
def Destroy(self): def Destroy(self):
# @todo: when wxPython 4.0.2 is release, https://github.com/pyfa-org/Pyfa/issues/1586#issuecomment-390074915
# Make sure to remove the shitty checks that I have to put in place for these handlers to ignore when self is None
print("+++++ Destroy " + repr(self)) print("+++++ Destroy " + repr(self))
print(self.parent.Unbind(EVT_NOTEBOOK_PAGE_CHANGED))
print(self.mainFrame.Unbind(GE.FIT_CHANGED)) # print(self.parent.Unbind(EVT_NOTEBOOK_PAGE_CHANGED))
print(self.mainFrame.Unbind(EVT_FIT_RENAMED)) # print(self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.fitChanged))
print(self.mainFrame.Unbind(EVT_FIT_REMOVED)) # print(self.mainFrame.Unbind(EVT_FIT_RENAMED, handler=self.fitRenamed ))
print(self.mainFrame.Unbind(ITEM_SELECTED)) # print(self.mainFrame.Unbind(EVT_FIT_REMOVED, handler=self.fitRemoved))
# print(self.mainFrame.Unbind(ITEM_SELECTED, handler=self.appendItem))
d.Display.Destroy(self) d.Display.Destroy(self)
@@ -291,6 +294,9 @@ class FittingView(d.Display):
""" """
print('_+_+_+_+_+_ Fit Removed: {} {} activeFitID: {}, eventFitID: {}'.format(repr(self), str(bool(self)), self.activeFitID, event.fitID)) print('_+_+_+_+_+_ Fit Removed: {} {} activeFitID: {}, eventFitID: {}'.format(repr(self), str(bool(self)), self.activeFitID, event.fitID))
pyfalog.debug("FittingView::fitRemoved") pyfalog.debug("FittingView::fitRemoved")
if not self:
event.Skip()
return
if event.fitID == self.getActiveFit(): if event.fitID == self.getActiveFit():
pyfalog.debug(" Deleted fit is currently active") pyfalog.debug(" Deleted fit is currently active")
self.parent.DeletePage(self.parent.GetPageIndex(self)) self.parent.DeletePage(self.parent.GetPageIndex(self))
@@ -298,8 +304,12 @@ class FittingView(d.Display):
try: try:
# Sometimes there is no active page after deletion, hence the try block # Sometimes there is no active page after deletion, hence the try block
sFit = Fit.getInstance() sFit = Fit.getInstance()
sFit.refreshFit(self.getActiveFit())
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID)) # stopgap for #1384
fit = sFit.getFit(self.getActiveFit())
if fit:
sFit.refreshFit(self.getActiveFit())
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID))
except RuntimeError: except RuntimeError:
pyfalog.warning("Caught dead object") pyfalog.warning("Caught dead object")
pass pass
@@ -307,6 +317,9 @@ class FittingView(d.Display):
event.Skip() event.Skip()
def fitRenamed(self, event): def fitRenamed(self, event):
if not self:
event.Skip()
return
fitID = event.fitID fitID = event.fitID
if fitID == self.getActiveFit(): if fitID == self.getActiveFit():
self.updateTab() self.updateTab()
@@ -343,6 +356,9 @@ class FittingView(d.Display):
self.parent.SetPageTextIcon(pageIndex, text, bitmap) self.parent.SetPageTextIcon(pageIndex, text, bitmap)
def appendItem(self, event): def appendItem(self, event):
if not self:
event.Skip()
return
if self.parent.IsActive(self): if self.parent.IsActive(self):
itemID = event.itemID itemID = event.itemID
fitID = self.activeFitID fitID = self.activeFitID
@@ -504,7 +520,7 @@ class FittingView(d.Display):
# second loop modifies self.mods, rewrites self.blanks to represent actual index of blanks # second loop modifies self.mods, rewrites self.blanks to represent actual index of blanks
for i, (x, slot) in enumerate(self.blanks): for i, (x, slot) in enumerate(self.blanks):
self.blanks[i] = x + i # modify blanks with actual index self.blanks[i] = x + i # modify blanks with actual index
self.mods.insert(x + i, Rack.buildRack(slot)) self.mods.insert(x + i, Rack.buildRack(slot, sum(m.slot == slot for m in self.mods)))
if fit.mode: if fit.mode:
# Modes are special snowflakes and need a little manual loving # Modes are special snowflakes and need a little manual loving
@@ -512,7 +528,7 @@ class FittingView(d.Display):
# while also marking the mode header position in the Blanks list # while also marking the mode header position in the Blanks list
if sFit.serviceFittingOptions["rackSlots"]: if sFit.serviceFittingOptions["rackSlots"]:
self.blanks.append(len(self.mods)) self.blanks.append(len(self.mods))
self.mods.append(Rack.buildRack(Slot.MODE)) self.mods.append(Rack.buildRack(Slot.MODE, None))
self.mods.append(fit.mode) self.mods.append(fit.mode)
else: else:
@@ -524,7 +540,9 @@ class FittingView(d.Display):
def fitChanged(self, event): def fitChanged(self, event):
print('====== Fit Changed: {} {} activeFitID: {}, eventFitID: {}'.format(repr(self), str(bool(self)), self.activeFitID, event.fitID)) print('====== Fit Changed: {} {} activeFitID: {}, eventFitID: {}'.format(repr(self), str(bool(self)), self.activeFitID, event.fitID))
if not self:
event.Skip()
return
try: try:
if self.activeFitID is not None and self.activeFitID == event.fitID: if self.activeFitID is not None and self.activeFitID == event.fitID:
self.generateMods() self.generateMods()

View File

@@ -734,17 +734,14 @@ class APIView(wx.Panel):
self.stDisabledTip.Wrap(-1) self.stDisabledTip.Wrap(-1)
hintSizer.Add(self.stDisabledTip, 0, wx.TOP | wx.BOTTOM, 10) hintSizer.Add(self.stDisabledTip, 0, wx.TOP | wx.BOTTOM, 10)
self.noCharactersTip = wx.StaticText(self, wx.ID_ANY,
"You haven't logging into EVE SSO with any characters yet. Please use the "
"button below to log into EVE.", style=wx.ALIGN_CENTER)
self.noCharactersTip.Wrap(-1)
hintSizer.Add(self.noCharactersTip, 0, wx.TOP | wx.BOTTOM, 10)
self.stDisabledTip.Hide() self.stDisabledTip.Hide()
hintSizer.AddStretchSpacer() hintSizer.AddStretchSpacer()
pmainSizer.Add(hintSizer, 0, wx.EXPAND, 5) pmainSizer.Add(hintSizer, 0, wx.EXPAND, 5)
fgSizerInput = wx.FlexGridSizer(3, 2, 0, 0) fgSizerInput = wx.FlexGridSizer(1, 3, 0, 0)
fgSizerInput.AddGrowableCol(1) fgSizerInput.AddGrowableCol(1)
fgSizerInput.SetFlexibleDirection(wx.BOTH) fgSizerInput.SetFlexibleDirection(wx.BOTH)
fgSizerInput.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED) fgSizerInput.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED)
@@ -754,15 +751,28 @@ class APIView(wx.Panel):
fgSizerInput.Add(self.m_staticCharText, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) fgSizerInput.Add(self.m_staticCharText, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 10)
self.charChoice = wx.Choice(self, wx.ID_ANY, style=0) self.charChoice = wx.Choice(self, wx.ID_ANY, style=0)
fgSizerInput.Add(self.charChoice, 1, wx.ALL | wx.EXPAND, 10) fgSizerInput.Add(self.charChoice, 1, wx.TOP | wx.BOTTOM | wx.EXPAND, 10)
self.fetchButton = wx.Button(self, wx.ID_ANY, "Get Skills", wx.DefaultPosition, wx.DefaultSize, 0)
self.fetchButton.Bind(wx.EVT_BUTTON, self.fetchSkills)
fgSizerInput.Add(self.fetchButton, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 10)
pmainSizer.Add(fgSizerInput, 0, wx.EXPAND, 5) pmainSizer.Add(fgSizerInput, 0, wx.EXPAND, 5)
pmainSizer.AddStretchSpacer()
self.m_staticline1 = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL)
pmainSizer.Add(self.m_staticline1, 0, wx.EXPAND | wx.ALL, 10)
self.noCharactersTip = wx.StaticText(self, wx.ID_ANY, "Don't see your EVE character in the list?", style=wx.ALIGN_CENTER)
self.noCharactersTip.Wrap(-1)
pmainSizer.Add(self.noCharactersTip, 0, wx.CENTER | wx.TOP | wx.BOTTOM, 0)
self.addButton = wx.Button(self, wx.ID_ANY, "Log In with EVE SSO", wx.DefaultPosition, wx.DefaultSize, 0) self.addButton = wx.Button(self, wx.ID_ANY, "Log In with EVE SSO", wx.DefaultPosition, wx.DefaultSize, 0)
self.addButton.Bind(wx.EVT_BUTTON, self.addCharacter) self.addButton.Bind(wx.EVT_BUTTON, self.addCharacter)
pmainSizer.Add(self.addButton, 0, wx.ALL | wx.ALIGN_CENTER, 5) pmainSizer.Add(self.addButton, 0, wx.ALL | wx.ALIGN_CENTER, 10)
self.stStatus = wx.StaticText(self, wx.ID_ANY, wx.EmptyString)
pmainSizer.Add(self.stStatus, 0, wx.ALL, 5)
self.charEditor.mainFrame.Bind(GE.EVT_SSO_LOGOUT, self.ssoListChanged) self.charEditor.mainFrame.Bind(GE.EVT_SSO_LOGOUT, self.ssoListChanged)
self.charEditor.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.ssoListChanged) self.charEditor.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.ssoListChanged)
self.charEditor.entityEditor.Bind(wx.EVT_CHOICE, self.charChanged) self.charEditor.entityEditor.Bind(wx.EVT_CHOICE, self.charChanged)
@@ -776,9 +786,18 @@ class APIView(wx.Panel):
def ssoCharChanged(self, event): def ssoCharChanged(self, event):
sChar = Character.getInstance() sChar = Character.getInstance()
activeChar = self.charEditor.entityEditor.getActiveEntity() activeChar = self.charEditor.entityEditor.getActiveEntity()
sChar.setSsoCharacter(activeChar.ID, self.getActiveCharacter()) ssoChar = self.getActiveCharacter()
sChar.setSsoCharacter(activeChar.ID, ssoChar)
self.fetchButton.Enable(ssoChar is not None)
event.Skip() event.Skip()
def fetchSkills(self, evt):
sChar = Character.getInstance()
char = self.charEditor.entityEditor.getActiveEntity()
sChar.apiFetch(char.ID, self.__fetchCallback)
def addCharacter(self, event): def addCharacter(self, event):
sEsi = Esi.getInstance() sEsi = Esi.getInstance()
sEsi.login() sEsi.login()
@@ -788,17 +807,8 @@ class APIView(wx.Panel):
return self.charChoice.GetClientData(selection) if selection is not -1 else None return self.charChoice.GetClientData(selection) if selection is not -1 else None
def ssoListChanged(self, event): def ssoListChanged(self, event):
sEsi = Esi.getInstance() if not self: # todo: fix event not unbinding properly
ssoChars = sEsi.getSsoCharacters() return
if len(ssoChars) == 0:
self.charChoice.Hide()
self.m_staticCharText.Hide()
self.noCharactersTip.Show()
else:
self.noCharactersTip.Hide()
self.m_staticCharText.Show()
self.charChoice.Show()
self.charChanged(event) self.charChanged(event)
@@ -814,6 +824,8 @@ class APIView(wx.Panel):
sso = sChar.getSsoCharacter(activeChar.ID) sso = sChar.getSsoCharacter(activeChar.ID)
self.fetchButton.Enable(sso is not None)
ssoChars = sEsi.getSsoCharacters() ssoChars = sEsi.getSsoCharacters()
self.charChoice.Clear() self.charChoice.Clear()
@@ -825,9 +837,9 @@ class APIView(wx.Panel):
if sso is not None and char.ID == sso.ID: if sso is not None and char.ID == sso.ID:
self.charChoice.SetSelection(currId) self.charChoice.SetSelection(currId)
if sso is None:
self.charChoice.SetSelection(noneID)
if sso is None:
self.charChoice.SetSelection(noneID)
# #
# if chars: # if chars:
@@ -851,13 +863,17 @@ class APIView(wx.Panel):
event.Skip() event.Skip()
def __fetchCallback(self, e=None): def __fetchCallback(self, e=None):
charName = self.charChoice.GetString(self.charChoice.GetSelection()) if e:
if e is None:
self.stStatus.SetLabel("Successfully fetched {}\'s skills from EVE API.".format(charName))
else:
exc_type, exc_obj, exc_trace = e exc_type, exc_obj, exc_trace = e
pyfalog.error("Unable to retrieve {0}\'s skills. Error message:\n{1}".format(charName, exc_obj)) pyfalog.warn("Error fetching skill information for character")
self.stStatus.SetLabel("Unable to retrieve {}\'s skills. Error message:\n{}".format(charName, exc_obj)) pyfalog.warn(exc_obj)
wx.MessageBox(
"Error fetching skill information",
"Error", wx.ICON_ERROR | wx.STAY_ON_TOP)
else:
wx.MessageBox(
"Successfully fetched skills", "Success", wx.ICON_INFORMATION | wx.STAY_ON_TOP)
class SecStatusDialog(wx.Dialog): class SecStatusDialog(wx.Dialog):

View File

@@ -170,7 +170,6 @@ class CharacterSelection(wx.Panel):
def charChanged(self, event): def charChanged(self, event):
fitID = self.mainFrame.getActiveFit() fitID = self.mainFrame.getActiveFit()
charID = self.getActiveCharacter() charID = self.getActiveCharacter()
sChar = Character.getInstance()
if charID == -1: if charID == -1:
# revert to previous character # revert to previous character

View File

@@ -1,4 +1,4 @@
#=============================================================================== # ===============================================================================
# #
# ToDo: Bug - when selecting close on a tab, sometimes the tab to the right is # ToDo: Bug - when selecting close on a tab, sometimes the tab to the right is
# selected, most likely due to determination of mouse position # selected, most likely due to determination of mouse position
@@ -11,7 +11,7 @@
# tab index?). This will also help with finding close buttons. # tab index?). This will also help with finding close buttons.
# ToDo: Fix page preview code (PFNotebookPagePreview) # ToDo: Fix page preview code (PFNotebookPagePreview)
# #
#= ============================================================================== # ===============================================================================
import wx import wx
import wx.lib.newevent import wx.lib.newevent
@@ -413,7 +413,7 @@ class _TabRenderer:
mdc.SelectObject(ebmp) mdc.SelectObject(ebmp)
mdc.SetFont(self.font) mdc.SetFont(self.font)
textSizeX, textSizeY = mdc.GetTextExtent(self.text) textSizeX, textSizeY = mdc.GetTextExtent(self.text)
totalSize = self.left_width + self.right_width + textSizeX + self.close_btn_width / 2 + 16 + self.padding* 2 totalSize = self.left_width + self.right_width + textSizeX + self.close_btn_width / 2 + 16 + self.padding * 2
mdc.SelectObject(wx.NullBitmap) mdc.SelectObject(wx.NullBitmap)
return totalSize, self.tab_height return totalSize, self.tab_height
@@ -1478,4 +1478,3 @@ if __name__ == "__main__":
top = Frame("Test Chrome Tabs") top = Frame("Test Chrome Tabs")
top.Show() top.Show()
app.MainLoop() app.MainLoop()

View File

@@ -1,5 +1,3 @@
import time
import webbrowser
import json import json
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import wx import wx
@@ -15,9 +13,8 @@ from gui.display import Display
import gui.globalEvents as GE import gui.globalEvents as GE
from logbook import Logger from logbook import Logger
import calendar
from service.esi import Esi from service.esi import Esi
from esipy.exceptions import APIException from service.esiAccess import APIException
from service.port import ESIExportException from service.port import ESIExportException
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -32,7 +29,6 @@ class EveFittings(wx.Frame):
self.mainFrame = parent self.mainFrame = parent
mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer = wx.BoxSizer(wx.VERTICAL)
sEsi = Esi.getInstance()
characterSelectSizer = wx.BoxSizer(wx.HORIZONTAL) characterSelectSizer = wx.BoxSizer(wx.HORIZONTAL)
@@ -72,8 +68,6 @@ class EveFittings(wx.Frame):
self.importBtn.Bind(wx.EVT_BUTTON, self.importFitting) self.importBtn.Bind(wx.EVT_BUTTON, self.importFitting)
self.deleteBtn.Bind(wx.EVT_BUTTON, self.deleteFitting) self.deleteBtn.Bind(wx.EVT_BUTTON, self.deleteFitting)
self.mainFrame.Bind(GE.EVT_SSO_LOGOUT, self.ssoLogout)
self.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.ssoLogin)
self.Bind(wx.EVT_CLOSE, self.OnClose) self.Bind(wx.EVT_CLOSE, self.OnClose)
self.statusbar = wx.StatusBar(self) self.statusbar = wx.StatusBar(self)
@@ -85,10 +79,6 @@ class EveFittings(wx.Frame):
self.Centre(wx.BOTH) self.Centre(wx.BOTH)
def ssoLogin(self, event):
self.updateCharList()
event.Skip()
def updateCharList(self): def updateCharList(self):
sEsi = Esi.getInstance() sEsi = Esi.getInstance()
chars = sEsi.getSsoCharacters() chars = sEsi.getSsoCharacters()
@@ -102,10 +92,6 @@ class EveFittings(wx.Frame):
self.charChoice.SetSelection(0) self.charChoice.SetSelection(0)
def ssoLogout(self, event):
self.updateCharList()
event.Skip() # continue event
def OnClose(self, event): def OnClose(self, event):
self.mainFrame.Unbind(GE.EVT_SSO_LOGOUT) self.mainFrame.Unbind(GE.EVT_SSO_LOGOUT)
self.mainFrame.Unbind(GE.EVT_SSO_LOGIN) self.mainFrame.Unbind(GE.EVT_SSO_LOGIN)
@@ -121,21 +107,23 @@ class EveFittings(wx.Frame):
waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self) waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self)
try: try:
fittings = sEsi.getFittings(self.getActiveCharacter()) self.fittings = sEsi.getFittings(self.getActiveCharacter())
# self.cacheTime = fittings.get('cached_until') # self.cacheTime = fittings.get('cached_until')
# self.updateCacheStatus(None) # self.updateCacheStatus(None)
# self.cacheTimer.Start(1000) # self.cacheTimer.Start(1000)
self.fitTree.populateSkillTree(fittings) self.fitTree.populateSkillTree(self.fittings)
del waitDialog del waitDialog
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
msg = "Connection error, please check your internet connection" msg = "Connection error, please check your internet connection"
pyfalog.error(msg) pyfalog.error(msg)
self.statusbar.SetStatusText(msg) self.statusbar.SetStatusText(msg)
except APIException as ex: except APIException as ex:
del waitDialog # Can't do this in a finally because then it obscures the message dialog # Can't do this in a finally because then it obscures the message dialog
del waitDialog # noqa: F821
ESIExceptionHandler(self, ex) ESIExceptionHandler(self, ex)
except Exception as ex: except Exception as ex:
del waitDialog del waitDialog # noqa: F821
raise ex
def importFitting(self, event): def importFitting(self, event):
selection = self.fitView.fitSelection selection = self.fitView.fitSelection
@@ -160,6 +148,9 @@ class EveFittings(wx.Frame):
if dlg.ShowModal() == wx.ID_YES: if dlg.ShowModal() == wx.ID_YES:
try: try:
sEsi.delFitting(self.getActiveCharacter(), data['fitting_id']) sEsi.delFitting(self.getActiveCharacter(), data['fitting_id'])
# repopulate the fitting list
self.fitTree.populateSkillTree(self.fittings)
self.fitView.update([])
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
msg = "Connection error, please check your internet connection" msg = "Connection error, please check your internet connection"
pyfalog.error(msg) pyfalog.error(msg)
@@ -167,8 +158,9 @@ class EveFittings(wx.Frame):
class ESIExceptionHandler(object): class ESIExceptionHandler(object):
# todo: make this a generate excetpion handler for all calls
def __init__(self, parentWindow, ex): def __init__(self, parentWindow, ex):
if ex.response['error'] == "invalid_token": if ex.response['error'].startswith('Token is not valid') or ex.response['error'] == 'invalid_token': # todo: this seems messy, figure out a better response
dlg = wx.MessageDialog(parentWindow, dlg = wx.MessageDialog(parentWindow,
"There was an error validating characters' SSO token. Please try " "There was an error validating characters' SSO token. Please try "
"logging into the character again to reset the token.", "Invalid Token", "logging into the character again to reset the token.", "Invalid Token",
@@ -188,7 +180,6 @@ class ExportToEve(wx.Frame):
self.mainFrame = parent self.mainFrame = parent
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
sEsi = Esi.getInstance()
mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer = wx.BoxSizer(wx.VERTICAL)
hSizer = wx.BoxSizer(wx.HORIZONTAL) hSizer = wx.BoxSizer(wx.HORIZONTAL)
@@ -208,8 +199,6 @@ class ExportToEve(wx.Frame):
self.statusbar.SetFieldsCount(2) self.statusbar.SetFieldsCount(2)
self.statusbar.SetStatusWidths([100, -1]) self.statusbar.SetStatusWidths([100, -1])
self.mainFrame.Bind(GE.EVT_SSO_LOGOUT, self.ssoLogout)
self.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.ssoLogin)
self.Bind(wx.EVT_CLOSE, self.OnClose) self.Bind(wx.EVT_CLOSE, self.OnClose)
self.SetSizer(mainSizer) self.SetSizer(mainSizer)
@@ -231,14 +220,6 @@ class ExportToEve(wx.Frame):
self.charChoice.SetSelection(0) self.charChoice.SetSelection(0)
def ssoLogin(self, event):
self.updateCharList()
event.Skip()
def ssoLogout(self, event):
self.updateCharList()
event.Skip() # continue event
def OnClose(self, event): def OnClose(self, event):
self.mainFrame.Unbind(GE.EVT_SSO_LOGOUT) self.mainFrame.Unbind(GE.EVT_SSO_LOGOUT)
self.mainFrame.Unbind(GE.EVT_SSO_LOGIN) self.mainFrame.Unbind(GE.EVT_SSO_LOGIN)
@@ -250,7 +231,6 @@ class ExportToEve(wx.Frame):
return self.charChoice.GetClientData(selection) if selection is not None else None return self.charChoice.GetClientData(selection) if selection is not None else None
def exportFitting(self, event): def exportFitting(self, event):
sPort = Port.getInstance()
fitID = self.mainFrame.getActiveFit() fitID = self.mainFrame.getActiveFit()
self.statusbar.SetStatusText("", 0) self.statusbar.SetStatusText("", 0)
@@ -260,12 +240,8 @@ class ExportToEve(wx.Frame):
return return
self.statusbar.SetStatusText("Sending request and awaiting response", 1) self.statusbar.SetStatusText("Sending request and awaiting response", 1)
sEsi = Esi.getInstance()
try: try:
sFit = Fit.getInstance()
data = sPort.exportESI(sFit.getFit(fitID))
res = sEsi.postFitting(self.getActiveCharacter(), data)
self.statusbar.SetStatusText("", 0) self.statusbar.SetStatusText("", 0)
self.statusbar.SetStatusText("", 1) self.statusbar.SetStatusText("", 1)
@@ -324,8 +300,10 @@ class SsoCharacterMgmt(wx.Dialog):
self.Centre(wx.BOTH) self.Centre(wx.BOTH)
def ssoLogin(self, event): def ssoLogin(self, event):
self.popCharList() if (self):
event.Skip() # todo: these events don't unbind properly when window is closed (?), hence the `if`. Figure out better way of doing this.
self.popCharList()
event.Skip()
def popCharList(self): def popCharList(self):
sEsi = Esi.getInstance() sEsi = Esi.getInstance()
@@ -380,10 +358,17 @@ class FittingsTreeView(wx.Panel):
tree = self.fittingsTreeCtrl tree = self.fittingsTreeCtrl
tree.DeleteChildren(root) tree.DeleteChildren(root)
sEsi = Esi.getInstance()
dict = {} dict = {}
fits = data fits = data
for fit in fits: for fit in fits:
if (fit['fitting_id'] in sEsi.fittings_deleted):
continue
ship = getItem(fit['ship_type_id']) ship = getItem(fit['ship_type_id'])
if ship is None:
pyfalog.debug('Cannot find ship type id: {}'.format(fit['ship_type_id']))
continue
if ship.name not in dict: if ship.name not in dict:
dict[ship.name] = [] dict[ship.name] = []
dict[ship.name].append(fit) dict[ship.name].append(fit)

View File

@@ -69,6 +69,7 @@ from service.settings import SettingsProvider
from service.fit import Fit from service.fit import Fit
from service.character import Character from service.character import Character
from service.update import Update from service.update import Update
from service.esiAccess import SsoMode
# import this to access override setting # import this to access override setting
from eos.modifiedAttributeDict import ModifiedAttributeDict from eos.modifiedAttributeDict import ModifiedAttributeDict
@@ -243,12 +244,12 @@ class MainFrame(wx.Frame):
self.Bind(GE.EVT_SSO_LOGGING_IN, self.ShowSsoLogin) self.Bind(GE.EVT_SSO_LOGGING_IN, self.ShowSsoLogin)
def ShowSsoLogin(self, event): def ShowSsoLogin(self, event):
if getattr(event, "login_mode", LoginMethod.SERVER) == LoginMethod.MANUAL: if getattr(event, "login_mode", LoginMethod.SERVER) == LoginMethod.MANUAL and getattr(event, "sso_mode", SsoMode.AUTO) == SsoMode.AUTO:
dlg = SsoLogin(self) dlg = SsoLogin(self)
if dlg.ShowModal() == wx.ID_OK: if dlg.ShowModal() == wx.ID_OK:
sEsi = Esi.getInstance() sEsi = Esi.getInstance()
# todo: verify that this is a correct SSO Info block # todo: verify that this is a correct SSO Info block
sEsi.handleLogin(dlg.ssoInfoCtrl.Value.strip()) sEsi.handleLogin({'SSOInfo': [dlg.ssoInfoCtrl.Value.strip()]})
def ShowUpdateBox(self, release, version): def ShowUpdateBox(self, release, version):
dlg = UpdateDialog(self, release, version) dlg = UpdateDialog(self, release, version)

View File

@@ -1,5 +1,6 @@
import wx import wx
class SsoLogin(wx.Dialog): class SsoLogin(wx.Dialog):
def __init__(self, parent): def __init__(self, parent):
wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="SSO Login", size=wx.Size(400, 240)) wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="SSO Login", size=wx.Size(400, 240))

View File

@@ -62,7 +62,6 @@ class UpdateDialog(wx.Dialog):
self.browser.Bind(wx.html2.EVT_WEBVIEW_NEWWINDOW, self.OnNewWindow) self.browser.Bind(wx.html2.EVT_WEBVIEW_NEWWINDOW, self.OnNewWindow)
link_patterns = [ link_patterns = [
(re.compile("([0-9a-f]{6,40})", re.I), r"https://github.com/pyfa-org/Pyfa/commit/\1"),
(re.compile("#(\d+)", re.I), r"https://github.com/pyfa-org/Pyfa/issues/\1"), (re.compile("#(\d+)", re.I), r"https://github.com/pyfa-org/Pyfa/issues/\1"),
(re.compile("@(\w+)", re.I), r"https://github.com/\1") (re.compile("@(\w+)", re.I), r"https://github.com/\1")
] ]
@@ -71,12 +70,27 @@ class UpdateDialog(wx.Dialog):
extras=['cuddled-lists', 'fenced-code-blocks', 'target-blank-links', 'toc', 'link-patterns'], extras=['cuddled-lists', 'fenced-code-blocks', 'target-blank-links', 'toc', 'link-patterns'],
link_patterns=link_patterns) link_patterns=link_patterns)
release_markup = markdowner.convert(self.releaseInfo['body'])
# run the text through markup again, this time with the hashing pattern. This is required due to bugs in markdown2:
# https://github.com/trentm/python-markdown2/issues/287
link_patterns = [
(re.compile("([0-9a-f]{6,40})", re.I), r"https://github.com/pyfa-org/Pyfa/commit/\1"),
]
markdowner = markdown2.Markdown(
extras=['cuddled-lists', 'fenced-code-blocks', 'target-blank-links', 'toc', 'link-patterns'],
link_patterns=link_patterns)
# The space here is required, again, due to bug. Again, see https://github.com/trentm/python-markdown2/issues/287
release_markup = markdowner.convert(' ' + release_markup)
self.browser.SetPage(html_tmpl.format( self.browser.SetPage(html_tmpl.format(
self.releaseInfo['tag_name'], self.releaseInfo['tag_name'],
releaseDate.strftime('%B %d, %Y'), releaseDate.strftime('%B %d, %Y'),
"<p class='text-danger'><b>This is a pre-release, be prepared for unstable features</b></p>" if version.is_prerelease else "", "<p class='text-danger'><b>This is a pre-release, be prepared for unstable features</b></p>" if version.is_prerelease else "",
markdowner.convert(self.releaseInfo['body']) release_markup
),"") ), "")
notesSizer.Add(self.browser, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) notesSizer.Add(self.browser, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5)
mainSizer.Add(notesSizer, 1, wx.EXPAND, 5) mainSizer.Add(notesSizer, 1, wx.EXPAND, 5)

34
pyfa.py
View File

@@ -26,23 +26,23 @@ from optparse import AmbiguousOptionError, BadOptionError, OptionParser
from service.prereqsCheck import PreCheckException, PreCheckMessage, version_precheck, version_block from service.prereqsCheck import PreCheckException, PreCheckMessage, version_precheck, version_block
import config import config
ascii_text = ''' # ascii_text = '''
++++++++++++++++++++++++++++++++++++++++++++++++++ # ++++++++++++++++++++++++++++++++++++++++++++++++++
#
/ _| # / _|
_ __ _ _ | | # _ __ _ _ | |
| '_ \ | | | || _|/ _` | # | '_ \ | | | || _|/ _` |
| |_) || |_| || | | (_| | # | |_) || |_| || | | (_| |
| .__/ \__, ||_| \__,_| # | .__/ \__, ||_| \__,_|
| | __/ | # | | __/ |
|_| |___/ # |_| |___/
#
You are running a alpha/beta version of pyfa. # You are running a alpha/beta version of pyfa.
#
++++++++++++++++++++++++++++++++++++++++++++++++++ # ++++++++++++++++++++++++++++++++++++++++++++++++++
''' # '''
#
print(ascii_text) # print(ascii_text)
class PassThroughOptionParser(OptionParser): class PassThroughOptionParser(OptionParser):

View File

@@ -4,7 +4,6 @@ matplotlib >= 2.0.0
python-dateutil python-dateutil
requests >= 2.0.0 requests >= 2.0.0
sqlalchemy >= 1.0.5 sqlalchemy >= 1.0.5
esipy == 0.3.3
cryptography cryptography
diskcache diskcache
markdown2 markdown2

63
scripts/compile_data.py Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python
"""
This script bootstraps Phobos from a supplied path and feeds it
information regarding EVE data paths and where to dump data. It then imports
some other scripts and uses them to convert the json data into a SQLite
database and then compare the new database to the existing one, producing a
diff which can then be used to assist in the updating.
"""
import sys
import os
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--dump", dest="dump_path", help="Location of Phobos JSON dump directory", required=True)
args = parser.parse_args()
dump_path = os.path.expanduser(args.dump_path)
script_path = os.path.dirname(__file__)
def header(text, subtext=None):
print()
print("* "*30)
print(text.center(60))
if subtext:
print(subtext.center(60))
print("* "*30)
print()
### SQL Convert
import jsonToSql
db_file = os.path.join(dump_path, "eve.db")
header("Converting Data to SQL", db_file)
if os.path.isfile(db_file):
os.remove(db_file)
jsonToSql.main("sqlite:///" + db_file, dump_path)
### Diff generation
import itemDiff
diff_file = os.path.join(dump_path, "diff.txt")
old_db = os.path.join(script_path, "..", "eve.db")
header("Generating DIFF", diff_file)
old_stdout = sys.stdout
sys.stdout = open(diff_file, 'w')
itemDiff.main(old=old_db, new=db_file)
sys.stdout = old_stdout
header("Commiting changes for ", diff_file)
from subprocess import call
os.chdir(dump_path)
call(["git.exe", "add", "."])
call(["git.exe", "commit", "-m", "Commit"])
print("\nAll done.")

View File

@@ -1,257 +0,0 @@
#!/usr/bin/env python
"""
Script for generating distributables based on platform skeletons.
User supplies path for pyfa code base, root skeleton directory, and where the
builds go. The builds are automatically named depending on the pyfa config
values of `version` and `tag`. If it's a Stable release, the naming
convention is:
pyfa-pyfaversion-expansion-expversion-platform
If it is not Stable (tag=git), we determine if the pyfa code base includes
the git repo to use as an ID. If not, uses randomly generated 6-character ID.
The unstable naming convention:
pyfa-YYYMMDD-id-platform
dist.py can also build the Windows installer provided that it has a path to
Inno Setup (and, for generating on non-Windows platforms, that WINE is
installed). To build the EXE file, `win` must be included in the platforms to
be built.
"""
#@todo: ensure build directory can be written to
# todo: default build and dist directories
from optparse import OptionParser
import os.path
import shutil
import sys
import tarfile
import datetime
import random
import string
import zipfile
import errno
from subprocess import call
class FileStub():
def write(self, *args):
pass
def flush(self, *args):
pass
i = 0
def loginfo(path, names):
# Print out a "progress" and return directories / files to ignore
global i
i += 1
if i % 10 == 0:
sys.stdout.write(".")
sys.stdout.flush()
return ()
def copyanything(src, dst):
try:
shutil.copytree(src, dst, ignore=loginfo)
except: # python >2.5
try:
shutil.copy(src, dst)
except:
raise
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size))
def zipdir(path, zip):
for root, dirs, files in os.walk(path):
for file in files:
zip.write(os.path.join(root, file))
skels = ['win', 'src', 'mac', 'mac-deprecated']
iscc = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" # inno script location via wine
if __name__ == "__main__":
oldstd = sys.stdout
parser = OptionParser()
parser.add_option("-s", "--skeleton", dest="skeleton", help="Location of Pyfa-skel directory")
parser.add_option("-b", "--base", dest="base", help="Location of cleaned read-only base directory")
parser.add_option("-d", "--destination", dest="destination", help="Where to copy our distributable")
parser.add_option("-p", "--platforms", dest="platforms", help="Comma-separated list of platforms to build", default=','.join(skels))
parser.add_option("-q", "--quiet", dest="silent", action="store_true")
parser.add_option("-w", "--winexe", dest="winexe", action="store_true", help="Build the Windows installer file (needs Inno Setup). Must include 'win' in platform options")
parser.add_option("-z", "--zip", dest="zip", action="store_true", help="zip archive instead of tar")
options, args = parser.parse_args()
if options.skeleton is None or options.base is None or options.destination is None:
print("Need --skeleton argument as well as --base and --destination argument")
parser.print_help()
sys.exit()
if options.silent:
sys.stdout = FileStub()
options.platforms = options.platforms.split(",")
for skel in skels:
if skel not in options.platforms:
continue
print("\n======== %s ========"%skel)
info = {}
config = {}
setup = {}
skeleton = os.path.expanduser(os.path.join(options.skeleton, skel))
exec(compile(open(os.path.join(options.base, "config.py")).read(), os.path.join(options.base, "config.py"), 'exec'), config)
exec(compile(open(os.path.join(skeleton, "info.py")).read(), os.path.join(skeleton, "info.py"), 'exec'), info)
exec(compile(open(os.path.join(options.base, "setup.py")).read(), os.path.join(options.base, "setup.py"), 'exec'), setup)
destination = os.path.expanduser(options.destination)
if not os.path.isdir(destination) or not os.access(destination, os.W_OK | os.X_OK):
print("Destination directory does not exist or is not writable: {}".format(destination))
sys.exit()
dirName = info["arcname"]
nowdt = datetime.datetime.now()
now = "%04d%02d%02d" % (nowdt.year, nowdt.month, nowdt.day)
git = False
if config['tag'].lower() == "git":
try: # if there is a git repo associated with base, use master commit
with open(os.path.join(options.base, ".git", "refs", "heads", "master"), 'r') as f:
id = f.readline()[0:6]
git = True
except: # else, use custom ID
id = id_generator()
fileName = "pyfa-{}-{}-{}".format(now, id, info["os"])
else:
fileName = "pyfa-{}-{}-{}-{}".format(
config['version'],
config['expansionName'].lower(),
config['expansionVersion'],
info["os"]
)
archiveName = "{}.{}".format(fileName, "zip" if options.zip else "tar.bz2")
tmpDir = os.path.join(os.getcwd(), dirName) # tmp directory where files are copied
tmpFile = os.path.join(os.getcwd(), archiveName)
try:
print("Copying skeleton to ", tmpDir)
shutil.copytree(skeleton, tmpDir, ignore=loginfo)
print()
source = os.path.expanduser(options.base)
root = os.path.join(tmpDir, info["base"])
# it is easier to work from the source directory
oldcwd = os.getcwd()
os.chdir(source)
if info["library"]:
print("Injecting files into", info["library"])
libraryFile = os.path.join(root, info["library"])
with zipfile.ZipFile(libraryFile, 'a') as library:
for dir in setup['packages']:
zipdir(dir, library)
library.write('pyfa.py', 'pyfa__main__.py')
library.write('config.py')
else: # platforms where we don't have a packaged library
print("Copying modules into", root)
for dir in setup['packages']:
copyanything(dir, os.path.join(root, dir))
# add some additional files to root dir for these platforms
# (hopefully can figure out a way later for OS X to use the one in
# it's library)
if skel == 'mac':
setup['include_files'] += ['pyfa.py']
if skel in ('src', 'mac-deprecated'):
setup['include_files'] += ['pyfa.py', 'config.py']
print()
print("Copying included files:", end=' ')
for file in setup['include_files']:
if isinstance(file, str):
print(file, end=' ')
copyanything(file, os.path.join(root, file))
print()
print("Creating images zipfile:", end=' ')
os.chdir('imgs')
imagesFile = os.path.join(root, "imgs.zip")
with zipfile.ZipFile(imagesFile, 'w') as images:
for dir in setup['icon_dirs']:
print(dir, end=' ')
zipdir(dir, images)
os.chdir(oldcwd)
print()
print("Creating archive")
if options.zip:
archive = zipfile.ZipFile(tmpFile, 'w', compression=zipfile.ZIP_DEFLATED)
zipdir(dirName, archive)
archive.close()
else:
archive = tarfile.open(tmpFile, "w:bz2")
archive.add(tmpDir, arcname=info["arcname"])
archive.close()
print("Moving archive to ", destination)
shutil.move(tmpFile, destination)
if "win" in skel and options.winexe:
print("Compiling EXE")
if config['tag'].lower() == "git":
if git: # if git repo info available, use git commit
expansion = "git-%s"%(id)
else: # if there is no git repo, use timestamp
expansion = now
else: # if code is Stable, use expansion name
expansion = "%s %s"%(config['expansionName'], config['expansionVersion']),
calllist = ["wine"] if 'win' not in sys.platform else []
call(calllist + [
iscc,
"pyfa-setup.iss",
"/dMyAppVersion=%s"%(config['version']),
"/dMyAppExpansion=%s"%(expansion),
"/dMyAppDir=pyfa",
"/dMyOutputDir=%s"%destination,
"/dMyOutputFile=%s"%fileName]) #stdout=devnull, stderr=devnull
print("EXE completed")
except Exception as e:
print("Encountered an error: \n\t", e)
raise
finally:
print("Deleting tmp files\n")
try:
shutil.rmtree("dist") # Inno dir
except:
pass
try:
shutil.rmtree(tmpDir)
except:
pass
try:
os.unlink(tmpFile)
except:
pass
sys.stdout = oldstd
if os.path.isdir(destination):
print(os.path.join(destination, os.path.split(tmpFile)[1]))
else:
print(destination)

81
scripts/dump_data.py Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python
"""
This script bootstraps Phobos from a supplied path and feeds it
information regarding EVE data paths and where to dump data. It then imports
some other scripts and uses them to convert the json data into a SQLite
database and then compare the new database to the existing one, producing a
diff which can then be used to assist in the updating.
"""
import sys
import os
# Phobos location
phb_path = os.path.expanduser("path/to/phobos")
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-e", "--eve", dest="eve_path", help="Location of EVE directory", required=True)
parser.add_argument("-c", "--cache", dest="cache_path", help="Location of EVE cache directory. If not specified, an attempt will be make to automatically determine path.")
parser.add_argument("-r", "--res", dest="res_path", help="Location of EVE shared resource cache. If not specified, an attempt will be make to automatically determine path.")
parser.add_argument("-d", "--dump", dest="dump_path", help="Location of Phobos JSON dump directory", required=True)
parser.add_argument("-p", "--phobos", dest="phb_path", help="Location of Phobos, defaults to path noted in script", default=phb_path)
parser.add_argument("-s", "--singularity", action="store_true", help="Singularity build")
args = parser.parse_args()
eve_path = os.path.expanduser(args.eve_path)
cache_path = os.path.expanduser(args.cache_path) if args.cache_path else None
res_path = os.path.expanduser(args.res_path) if args.res_path else None
dump_path = os.path.expanduser(args.dump_path)
script_path = os.path.dirname(__file__)
### Append Phobos to path
sys.path.append(os.path.expanduser(args.phb_path))
def header(text, subtext=None):
print()
print("* "*30)
print(text.center(60))
if subtext:
print(subtext.center(60))
print("* "*30)
print()
header("Dumping Phobos Data", dump_path)
import reverence
from flow import FlowManager
from miner import *
from translator import Translator
from writer import *
rvr = reverence.blue.EVE(eve_path, cachepath=args.cache_path, sharedcachepath=res_path, server="singularity" if args.singularity else "tranquility")
print("EVE Directory: {}".format(rvr.paths.root))
print("Cache Directory: {}".format(rvr.paths.cache))
print("Shared Resource Directory: {}".format(rvr.paths.sharedcache))
pickle_miner = ResourcePickleMiner(rvr)
trans = Translator(pickle_miner)
bulkdata_miner = BulkdataMiner(rvr, trans)
staticcache_miner = ResourceStaticCacheMiner(rvr, trans)
miners = (
MetadataMiner(eve_path),
bulkdata_miner,
staticcache_miner,
TraitMiner(staticcache_miner, bulkdata_miner, trans),
SqliteMiner(rvr.paths.root, trans),
CachedCallsMiner(rvr, trans),
pickle_miner
)
writers = (
JsonWriter(dump_path, indent=2),
)
list = "dgmexpressions,dgmattribs,dgmeffects,dgmtypeattribs,dgmtypeeffects,"\
"dgmunits,invcategories,invgroups,invmetagroups,invmetatypes,"\
"invtypes,mapbulk_marketGroups,phbmetadata,phbtraits,fsdTypeOverrides,"\
"evegroups,evetypes,evecategories,mapbulk_marketGroups,clonegrades"
FlowManager(miners, writers).run(list, "en-us")

View File

@@ -216,7 +216,7 @@ def main(old, new, groups=True, effects=True, attributes=True, renames=True):
# Initialize container for the data for each item with empty stuff besides groupID # Initialize container for the data for each item with empty stuff besides groupID
dictionary[itemid] = [groupID, set(), {}] dictionary[itemid] = [groupID, set(), {}]
# Add items filtered by group # Add items filtered by group
query = 'SELECT it.typeID, it.groupID FROM invtypes AS it INNER JOIN invgroups AS ig ON it.groupID = ig.groupID WHERE it.published = 1 AND ig.groupName IN ("Effect Beacon", "Ship Modifiers")' query = 'SELECT it.typeID, it.groupID FROM invtypes AS it INNER JOIN invgroups AS ig ON it.groupID = ig.groupID WHERE it.published = 1 AND ig.groupName IN ("Effect Beacon", "Ship Modifiers", "Mutaplasmids")'
cursor.execute(query) cursor.execute(query)
for row in cursor: for row in cursor:
itemid = row[0] itemid = row[0]

View File

@@ -20,16 +20,19 @@
import os import os
import sys import sys
import functools
import re import re
# Add eos root path to sys.path so we can import ourselves # Add eos root path to sys.path so we can import ourselves
path = os.path.dirname(str(__file__, sys.getfilesystemencoding())) path = os.path.dirname(__file__)
sys.path.append(os.path.realpath(os.path.join(path, ".."))) sys.path.append(os.path.realpath(os.path.join(path, "..")))
import json import json
import argparse import argparse
def main(db, json_path): def main(db, json_path):
if os.path.isfile(db):
os.remove(db)
jsonPath = os.path.expanduser(json_path) jsonPath = os.path.expanduser(json_path)
@@ -130,7 +133,7 @@ def main(db, json_path):
check[ID] = {} check[ID] = {}
check[ID][int(skill["typeID"])] = int(skill["level"]) check[ID][int(skill["typeID"])] = int(skill["level"])
if not reduce(lambda a, b: a if a == b else False, [v for _, v in check.iteritems()]): if not functools.reduce(lambda a, b: a if a == b else False, [v for _, v in check.items()]):
raise Exception("Alpha Clones not all equal") raise Exception("Alpha Clones not all equal")
newData = [x for x in newData if x['alphaCloneID'] == 1] newData = [x for x in newData if x['alphaCloneID'] == 1]
@@ -188,7 +191,7 @@ def main(db, json_path):
# Dump all data to memory so we can easely cross check ignored rows # Dump all data to memory so we can easely cross check ignored rows
for jsonName, cls in tables.items(): for jsonName, cls in tables.items():
with open(os.path.join(jsonPath, "{}.json".format(jsonName))) as f: with open(os.path.join(jsonPath, "{}.json".format(jsonName)), encoding="utf-8") as f:
tableData = json.load(f) tableData = json.load(f)
if jsonName in rowsInValues: if jsonName in rowsInValues:
tableData = list(tableData.values()) tableData = list(tableData.values())

View File

@@ -1,109 +0,0 @@
#!/usr/bin/env python
"""
This script bootstraps Phobos from a supplied path and feeds it
information regarding EVE data paths and where to dump data. It then imports
some other scripts and uses them to convert the json data into a SQLite
database and then compare the new database to the existing one, producing a
diff which can then be used to assist in the updating.
"""
import sys
import os
# Phobos location
phb_path = os.path.expanduser("path/to/phobos")
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-e", "--eve", dest="eve_path", help="Location of EVE directory", required=True)
parser.add_argument("-c", "--cache", dest="cache_path", help="Location of EVE cache directory. If not specified, an attempt will be make to automatically determine path.")
parser.add_argument("-r", "--res", dest="res_path", help="Location of EVE shared resource cache. If not specified, an attempt will be make to automatically determine path.")
parser.add_argument("-d", "--dump", dest="dump_path", help="Location of Phobos JSON dump directory", required=True)
parser.add_argument("-p", "--phobos", dest="phb_path", help="Location of Phobos, defaults to path noted in script", default=phb_path)
parser.add_argument("-s", "--singularity", action="store_true", help="Singularity build")
parser.add_argument("-j", "--nojson", dest="nojson", action="store_true", help="Skip Phobos JSON data dump.")
args = parser.parse_args()
eve_path = os.path.expanduser(str(args.eve_path, sys.getfilesystemencoding()))
cache_path = os.path.expanduser(str(args.cache_path, sys.getfilesystemencoding())) if args.cache_path else None
res_path = os.path.expanduser(str(args.res_path, sys.getfilesystemencoding())) if args.res_path else None
dump_path = os.path.expanduser(str(args.dump_path, sys.getfilesystemencoding()))
script_path = os.path.dirname(str(__file__, sys.getfilesystemencoding()))
### Append Phobos to path
sys.path.append(os.path.expanduser(str(args.phb_path, sys.getfilesystemencoding())))
def header(text, subtext=None):
print()
print("* "*30)
print(text.center(60))
if subtext:
print(subtext.center(60))
print("* "*30)
print()
### Data dump
if not args.nojson:
header("Dumping Phobos Data", dump_path)
import reverence
from flow import FlowManager
from miner import *
from translator import Translator
from writer import *
rvr = reverence.blue.EVE(eve_path, cachepath=args.cache_path, sharedcachepath=res_path, server="singularity" if args.singularity else "tranquility")
print("EVE Directory: {}".format(rvr.paths.root))
print("Cache Directory: {}".format(rvr.paths.cache))
print("Shared Resource Directory: {}".format(rvr.paths.sharedcache))
print()
pickle_miner = ResourcePickleMiner(rvr)
trans = Translator(pickle_miner)
bulkdata_miner = BulkdataMiner(rvr, trans)
staticcache_miner = ResourceStaticCacheMiner(rvr, trans)
miners = (
MetadataMiner(eve_path),
bulkdata_miner,
staticcache_miner,
TraitMiner(staticcache_miner, bulkdata_miner, trans),
SqliteMiner(rvr.paths.root, trans),
CachedCallsMiner(rvr, trans),
pickle_miner
)
writers = (
JsonWriter(dump_path, indent=2),
)
list = "dgmexpressions,dgmattribs,dgmeffects,dgmtypeattribs,dgmtypeeffects,"\
"dgmunits,invcategories,invgroups,invmetagroups,invmetatypes,"\
"invtypes,mapbulk_marketGroups,phbmetadata,phbtraits,fsdTypeOverrides,"\
"evegroups,evetypes,evecategories,mapbulk_marketGroups,clonegrades"
FlowManager(miners, writers).run(list, "en-us")
### SQL Convert
import jsonToSql
db_file = os.path.join(dump_path, "eve.db")
header("Converting Data to SQL", db_file)
if os.path.isfile(db_file):
os.remove(db_file)
jsonToSql.main("sqlite:///"+db_file, dump_path)
### Diff generation
import itemDiff
diff_file = os.path.join(dump_path, "diff.txt")
old_db = os.path.join(script_path, "..", "eve.db")
header("Generating DIFF", diff_file)
old_stdout = sys.stdout
sys.stdout = open(diff_file, 'w')
itemDiff.main(old=old_db, new=db_file)
sys.stdout = old_stdout
print("\nAll done.")

View File

@@ -123,6 +123,7 @@ class SkillBackupThread(threading.Thread):
wx.CallAfter(self.callback) wx.CallAfter(self.callback)
class Character(object): class Character(object):
instance = None instance = None
skillReqsDict = {} skillReqsDict = {}

View File

@@ -2,75 +2,34 @@
import wx import wx
from logbook import Logger from logbook import Logger
import threading import threading
import uuid
import time import time
import config
import base64 import base64
import json import json
import os
import config import config
import webbrowser import webbrowser
import eos.db import eos.db
import datetime
from eos.enum import Enum from eos.enum import Enum
from eos.saveddata.ssocharacter import SsoCharacter from eos.saveddata.ssocharacter import SsoCharacter
from service.esiAccess import APIException, SsoMode
import gui.globalEvents as GE import gui.globalEvents as GE
from service.server import StoppableHTTPServer, AuthHandler from service.server import StoppableHTTPServer, AuthHandler
from service.settings import EsiSettings from service.settings import EsiSettings
from service.esiAccess import EsiAccess
from .esi_security_proxy import EsiSecurityProxy from requests import Session
from esipy import EsiClient, EsiApp
from esipy.cache import FileCache
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
cache_path = os.path.join(config.savePath, config.ESI_CACHE)
from esipy.events import AFTER_TOKEN_REFRESH
if not os.path.exists(cache_path):
os.mkdir(cache_path)
file_cache = FileCache(cache_path)
class Servers(Enum):
TQ = 0
SISI = 1
class LoginMethod(Enum): class LoginMethod(Enum):
SERVER = 0 SERVER = 0
MANUAL = 1 MANUAL = 1
class Esi(object): class Esi(EsiAccess):
esiapp = None
esi_v1 = None
esi_v4 = None
_initializing = None
_instance = None _instance = None
@classmethod
def initEsiApp(cls):
if cls._initializing is None:
cls._initializing = True
cls.esiapp = EsiApp(cache=file_cache, cache_time=None, cache_prefix='pyfa{0}-esipy-'.format(config.version))
cls.esi_v1 = cls.esiapp.get_v1_swagger
cls.esi_v4 = cls.esiapp.get_v4_swagger
cls._initializing = False
@classmethod
def genEsiClient(cls, security=None):
return EsiClient(
security=EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) if security is None else security,
cache=file_cache,
headers={'User-Agent': 'pyfa esipy'}
)
@classmethod @classmethod
def getInstance(cls): def getInstance(cls):
if cls._instance is None: if cls._instance is None:
@@ -79,11 +38,9 @@ class Esi(object):
return cls._instance return cls._instance
def __init__(self): def __init__(self):
Esi.initEsiApp()
self.settings = EsiSettings.getInstance() self.settings = EsiSettings.getInstance()
AFTER_TOKEN_REFRESH.add_receiver(self.tokenUpdate) super().__init__()
# these will be set when needed # these will be set when needed
self.httpd = None self.httpd = None
@@ -92,18 +49,14 @@ class Esi(object):
self.implicitCharacter = None self.implicitCharacter = None
# The database cache does not seem to be working for some reason. Use # until I can get around to making proper caching and modifications to said cache, storee deleted fittings here
# this as a temporary measure # so that we can easily hide them in the fitting browser
self.charCache = {} self.fittings_deleted = set()
# need these here to post events # need these here to post events
import gui.mainFrame # put this here to avoid loop import gui.mainFrame # put this here to avoid loop
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def tokenUpdate(self, **kwargs):
print(kwargs)
pass
def delSsoCharacter(self, id): def delSsoCharacter(self, id):
char = eos.db.getSsoCharacter(id, config.getClientSecret()) char = eos.db.getSsoCharacter(id, config.getClientSecret())
@@ -120,103 +73,52 @@ class Esi(object):
return chars return chars
def getSsoCharacter(self, id): def getSsoCharacter(self, id):
"""
Get character, and modify to include the eve connection
"""
char = eos.db.getSsoCharacter(id, config.getClientSecret()) char = eos.db.getSsoCharacter(id, config.getClientSecret())
if char is not None and char.esi_client is None:
char.esi_client = Esi.genEsiClient()
Esi.update_token(char, Esi.get_sso_data(char)) # don't use update_token on security directly, se still need to apply the values here
eos.db.commit() eos.db.commit()
return char return char
def getSkills(self, id): def getSkills(self, id):
char = self.getSsoCharacter(id) char = self.getSsoCharacter(id)
op = Esi.esi_v4.op['get_characters_character_id_skills'](character_id=char.characterID) resp = super().getSkills(char)
resp = char.esi_client.request(op) return resp.json()
return resp.data
def getSecStatus(self, id): def getSecStatus(self, id):
char = self.getSsoCharacter(id) char = self.getSsoCharacter(id)
op = Esi.esi_v4.op['get_characters_character_id'](character_id=char.characterID) resp = super().getSecStatus(char)
resp = char.esi_client.request(op) return resp.json()
return resp.data
def getFittings(self, id): def getFittings(self, id):
char = self.getSsoCharacter(id) char = self.getSsoCharacter(id)
op = Esi.esi_v1.op['get_characters_character_id_fittings'](character_id=char.characterID) resp = super().getFittings(char)
resp = char.esi_client.request(op) return resp.json()
return resp.data
def postFitting(self, id, json_str): def postFitting(self, id, json_str):
# @todo: new fitting ID can be recovered from resp.data, # @todo: new fitting ID can be recovered from resp.data,
char = self.getSsoCharacter(id) char = self.getSsoCharacter(id)
op = Esi.esi_v1.op['post_characters_character_id_fittings']( resp = super().postFitting(char, json_str)
character_id=char.characterID, return resp.json()
fitting=json.loads(json_str)
)
resp = char.esi_client.request(op)
return resp.data
def delFitting(self, id, fittingID): def delFitting(self, id, fittingID):
char = self.getSsoCharacter(id) char = self.getSsoCharacter(id)
op = Esi.esi_v1.op['delete_characters_character_id_fittings_fitting_id']( super().delFitting(char, fittingID)
character_id=char.characterID, self.fittings_deleted.add(fittingID)
fitting_id=fittingID
)
resp = char.esi_client.request(op)
return resp.data
@staticmethod
def get_sso_data(char):
""" Little "helper" function to get formated data for esipy security
"""
return {
'access_token': char.accessToken,
'refresh_token': config.cipher.decrypt(char.refreshToken).decode(),
'expires_in': (char.accessTokenExpires - datetime.datetime.utcnow()).total_seconds()
}
@staticmethod
def update_token(char, tokenResponse):
""" helper function to update token data from SSO response """
char.accessToken = tokenResponse['access_token']
char.accessTokenExpires = datetime.datetime.fromtimestamp(time.time() + tokenResponse['expires_in'])
if 'refresh_token' in tokenResponse:
char.refreshToken = config.cipher.encrypt(tokenResponse['refresh_token'].encode())
if char.esi_client is not None:
char.esi_client.security.update_token(tokenResponse)
def login(self): def login(self):
serverAddr = None serverAddr = None
if self.settings.get('loginMode') == LoginMethod.SERVER: # always start the local server if user is using client details. Otherwise, start only if they choose to do so.
serverAddr = self.startServer() if self.settings.get('ssoMode') == SsoMode.CUSTOM or self.settings.get('loginMode') == LoginMethod.SERVER:
# random port, or if it's custom application, use a defined port
serverAddr = self.startServer(6461 if self.settings.get('ssoMode') == SsoMode.CUSTOM else 0)
uri = self.getLoginURI(serverAddr) uri = self.getLoginURI(serverAddr)
webbrowser.open(uri) webbrowser.open(uri)
wx.PostEvent(self.mainFrame, GE.SsoLoggingIn(login_mode=self.settings.get('loginMode'))) wx.PostEvent(self.mainFrame, GE.SsoLoggingIn(sso_mode=self.settings.get('ssoMode'), login_mode=self.settings.get('loginMode')))
def stopServer(self): def stopServer(self):
pyfalog.debug("Stopping Server") pyfalog.debug("Stopping Server")
self.httpd.stop() self.httpd.stop()
self.httpd = None self.httpd = None
def getLoginURI(self, redirect=None): def startServer(self, port): # todo: break this out into two functions: starting the server, and getting the URI
self.state = str(uuid.uuid4())
esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY)
args = {
'state': self.state,
'pyfa_version': config.version,
'login_method': self.settings.get('loginMode')
}
if redirect is not None:
args['redirect'] = redirect
return esisecurity.get_auth_uri(**args)
def startServer(self): # todo: break this out into two functions: starting the server, and getting the URI
pyfalog.debug("Starting server") pyfalog.debug("Starting server")
# we need this to ensure that the previous get_request finishes, and then the socket will close # we need this to ensure that the previous get_request finishes, and then the socket will close
@@ -224,7 +126,7 @@ class Esi(object):
self.stopServer() self.stopServer()
time.sleep(1) time.sleep(1)
self.httpd = StoppableHTTPServer(('localhost', 0), AuthHandler) self.httpd = StoppableHTTPServer(('localhost', port), AuthHandler)
port = self.httpd.socket.getsockname()[1] port = self.httpd.socket.getsockname()[1]
self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerLogin,)) self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerLogin,))
self.serverThread.name = "SsoCallbackServer" self.serverThread.name = "SsoCallbackServer"
@@ -233,31 +135,41 @@ class Esi(object):
return 'http://localhost:{}'.format(port) return 'http://localhost:{}'.format(port)
def handleLogin(self, ssoInfo): def handleLogin(self, message):
auth_response = json.loads(base64.b64decode(ssoInfo))
# We need to preload the ESI Security object beforehand with the auth response so that we can use verify to # we already have authenticated stuff for the auto mode
# get character information if (self.settings.get('ssoMode') == SsoMode.AUTO):
# init the security object ssoInfo = message['SSOInfo'][0]
esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) auth_response = json.loads(base64.b64decode(ssoInfo))
else:
# otherwise, we need to fetch the information
auth_response = self.auth(message['code'][0])
esisecurity.update_token(auth_response) res = self._session.get(
self.oauth_verify,
# we get the character information headers=self.get_oauth_header(auth_response['access_token'])
cdata = esisecurity.verify() )
if res.status_code != 200:
raise APIException(
self.oauth_verify,
res.status_code,
res.json()
)
cdata = res.json()
print(cdata) print(cdata)
currentCharacter = self.getSsoCharacter(cdata['CharacterName']) currentCharacter = self.getSsoCharacter(cdata['CharacterName'])
if currentCharacter is None: if currentCharacter is None:
currentCharacter = SsoCharacter(cdata['CharacterID'], cdata['CharacterName'], config.getClientSecret()) currentCharacter = SsoCharacter(cdata['CharacterID'], cdata['CharacterName'], config.getClientSecret())
currentCharacter.esi_client = Esi.genEsiClient(esisecurity)
Esi.update_token(currentCharacter, auth_response) # this also sets the esi security token Esi.update_token(currentCharacter, auth_response)
eos.db.save(currentCharacter) eos.db.save(currentCharacter)
wx.PostEvent(self.mainFrame, GE.SsoLogin(character=currentCharacter)) wx.PostEvent(self.mainFrame, GE.SsoLogin(character=currentCharacter))
# get (endpoint, char, data?)
def handleServerLogin(self, message): def handleServerLogin(self, message):
if not message: if not message:
raise Exception("Could not parse out querystring parameters.") raise Exception("Could not parse out querystring parameters.")
@@ -268,4 +180,4 @@ class Esi(object):
pyfalog.debug("Handling SSO login with: {0}", message) pyfalog.debug("Handling SSO login with: {0}", message)
self.handleLogin(message['SSOInfo'][0]) self.handleLogin(message)

284
service/esiAccess.py Normal file
View File

@@ -0,0 +1,284 @@
'''
A lot of the inspiration (and straight up code copying!) for this class comes from EsiPy <https://github.com/Kyria/EsiPy>
Much of the credit goes to the maintainer of that package, Kyria <tweetfleet slack: @althalus>. The reasoning for no
longer using EsiPy was due to it's reliance on pyswagger, which has caused a bit of a headache in how it operates on a
low level.
Eventually I'll rewrite this to be a bit cleaner and a bit more generic, but for now, it works!
'''
# noinspection PyPackageRequirements
from logbook import Logger
import uuid
import time
import config
import base64
import datetime
from eos.enum import Enum
from service.settings import EsiSettings, NetworkSettings
from requests import Session
from urllib.parse import urlencode, quote
pyfalog = Logger(__name__)
# todo: reimplement Caching for calls
# from esipy.cache import FileCache
# file_cache = FileCache(cache_path)
# cache_path = os.path.join(config.savePath, config.ESI_CACHE)
#
# if not os.path.exists(cache_path):
# os.mkdir(cache_path)
#
scopes = [
'esi-skills.read_skills.v1',
'esi-fittings.read_fittings.v1',
'esi-fittings.write_fittings.v1'
]
class SsoMode(Enum):
AUTO = 0
CUSTOM = 1
class APIException(Exception):
""" Exception for SSO related errors """
def __init__(self, url, code, json_response):
self.url = url
self.status_code = code
self.response = json_response
super(APIException, self).__init__(str(self))
def __str__(self):
if 'error' in self.response:
return 'HTTP Error %s: %s' % (self.status_code,
self.response['error'])
elif 'message' in self.response:
return 'HTTP Error %s: %s' % (self.status_code,
self.response['message'])
return 'HTTP Error %s' % (self.status_code)
class ESIEndpoints(Enum):
CHAR = "/v4/characters/{character_id}/"
CHAR_SKILLS = "/v4/characters/{character_id}/skills/"
CHAR_FITTINGS = "/v1/characters/{character_id}/fittings/"
CHAR_DEL_FIT = "/v1/characters/{character_id}/fittings/{fitting_id}/"
class EsiAccess(object):
def __init__(self):
self.settings = EsiSettings.getInstance()
# session request stuff
self._session = Session()
self._session.headers.update({
'Accept': 'application/json',
'User-Agent': (
'pyfa v{}'.format(config.version)
)
})
self._session.proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat()
@property
def sso_url(self):
if (self.settings.get("ssoMode") == SsoMode.CUSTOM):
return "https://login.eveonline.com"
return "https://www.pyfa.io"
@property
def esi_url(self):
return "https://esi.tech.ccp.is"
@property
def oauth_verify(self):
return '%s/verify/' % self.esi_url
@property
def oauth_authorize(self):
return '%s/oauth/authorize' % self.sso_url
@property
def oauth_token(self):
return '%s/oauth/token' % self.sso_url
def getSkills(self, char):
return self.get(char, ESIEndpoints.CHAR_SKILLS, character_id=char.characterID)
def getSecStatus(self, char):
return self.get(char, ESIEndpoints.CHAR, character_id=char.characterID)
def getFittings(self, char):
return self.get(char, ESIEndpoints.CHAR_FITTINGS, character_id=char.characterID)
def postFitting(self, char, json_str):
# @todo: new fitting ID can be recovered from resp.data,
return self.post(char, ESIEndpoints.CHAR_FITTINGS, json_str, character_id=char.characterID)
def delFitting(self, char, fittingID):
return self.delete(char, ESIEndpoints.CHAR_DEL_FIT, character_id=char.characterID, fitting_id=fittingID)
@staticmethod
def update_token(char, tokenResponse):
""" helper function to update token data from SSO response """
char.accessToken = tokenResponse['access_token']
char.accessTokenExpires = datetime.datetime.fromtimestamp(time.time() + tokenResponse['expires_in'])
if 'refresh_token' in tokenResponse:
char.refreshToken = config.cipher.encrypt(tokenResponse['refresh_token'].encode())
def getLoginURI(self, redirect=None):
self.state = str(uuid.uuid4())
if (self.settings.get("ssoMode") == SsoMode.AUTO):
args = {
'state': self.state,
'pyfa_version': config.version,
'login_method': self.settings.get('loginMode'),
'client_hash': config.getClientSecret()
}
if redirect is not None:
args['redirect'] = redirect
return '%s?%s' % (
self.oauth_authorize,
urlencode(args)
)
else:
return '%s?response_type=%s&redirect_uri=%s&client_id=%s%s%s' % (
self.oauth_authorize,
'code',
quote('http://localhost:6461', safe=''),
self.settings.get('clientID'),
'&scope=%s' % '+'.join(scopes) if scopes else '',
'&state=%s' % self.state
)
def get_oauth_header(self, token):
""" Return the Bearer Authorization header required in oauth calls
:return: a dict with the authorization header
"""
return {'Authorization': 'Bearer %s' % token}
def get_refresh_token_params(self, refreshToken):
""" Return the param object for the post() call to get the access_token
from the refresh_token
:param code: the refresh token
:return: a dict with the url, params and header
"""
if refreshToken is None:
raise AttributeError('No refresh token is defined.')
data = {
'grant_type': 'refresh_token',
'refresh_token': refreshToken,
}
if self.settings.get('ssoMode') == SsoMode.AUTO:
# data is all we really need, the rest is handled automatically by pyfa.io
return {
'data': data,
'url': self.oauth_token,
}
# otherwise, we need to make the token with the client keys
return self.__make_token_request_parameters(data)
def __get_token_auth_header(self):
""" Return the Basic Authorization header required to get the tokens
:return: a dict with the headers
"""
# encode/decode for py2/py3 compatibility
auth_b64 = "%s:%s" % (self.settings.get('clientID'), self.settings.get('clientSecret'))
auth_b64 = base64.b64encode(auth_b64.encode('latin-1'))
auth_b64 = auth_b64.decode('latin-1')
return {'Authorization': 'Basic %s' % auth_b64}
def __make_token_request_parameters(self, params):
request_params = {
'headers': self.__get_token_auth_header(),
'data': params,
'url': self.oauth_token,
}
return request_params
def get_access_token_request_params(self, code):
return self.__make_token_request_parameters(
{
'grant_type': 'authorization_code',
'code': code,
}
)
def auth(self, code):
request_data = self.get_access_token_request_params(code)
res = self._session.post(**request_data)
if res.status_code != 200:
raise Exception(
request_data['url'],
res.status_code,
res.json()
)
json_res = res.json()
return json_res
def refresh(self, ssoChar):
request_data = self.get_refresh_token_params(config.cipher.decrypt(ssoChar.refreshToken).decode())
res = self._session.post(**request_data)
if res.status_code != 200:
raise APIException(
request_data['url'],
res.status_code,
res.json()
)
json_res = res.json()
self.update_token(ssoChar, json_res)
return json_res
def _before_request(self, ssoChar):
if ssoChar.is_token_expired():
pyfalog.info("Refreshing token for {}".format(ssoChar.characterName))
self.refresh(ssoChar)
if ssoChar.accessToken is not None:
self._session.headers.update(self.get_oauth_header(ssoChar.accessToken))
def _after_request(self, resp):
if ("warning" in resp.headers):
pyfalog.warn("{} - {}".format(resp.headers["warning"], resp.url))
if resp.status_code >= 400:
raise APIException(
resp.url,
resp.status_code,
resp.json()
)
return resp
def get(self, ssoChar, endpoint, *args, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.get("{}{}".format(self.esi_url, endpoint)))
def post(self, ssoChar, endpoint, json, *args, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.post("{}{}".format(self.esi_url, endpoint), data=json))
def delete(self, ssoChar, endpoint, *args, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.delete("{}{}".format(self.esi_url, endpoint)))

View File

@@ -1,235 +0,0 @@
# -*- encoding: utf-8 -*-
""" EsiPy Security Proxy - An ESI Security class that directs authentication towards a third-party service.
Client key/secret not needed.
"""
from __future__ import absolute_import
import base64
import logging
import time
from requests import Session
from requests.utils import quote
from six.moves.urllib.parse import urlparse
from urllib.parse import urlencode
from esipy.events import AFTER_TOKEN_REFRESH
from esipy.exceptions import APIException
LOGGER = logging.getLogger(__name__)
class EsiSecurityProxy(object):
""" Contains all the OAuth2 knowledge for ESI use.
Based on pyswagger Security object, to be used with pyswagger BaseClient
implementation.
"""
def __init__(
self,
**kwargs):
""" Init the ESI Security Object
:param sso_url: the default sso URL used when no "app" is provided
:param esi_url: the default esi URL used for verify endpoint
:param app: (optionnal) the pyswagger app object
:param security_name: (optionnal) the name of the object holding the
informations in the securityDefinitions, used to check authed endpoint
"""
app = kwargs.pop('app', None)
sso_url = kwargs.pop('sso_url', "https://login.eveonline.com")
esi_url = kwargs.pop('esi_url', "https://esi.tech.ccp.is")
self.security_name = kwargs.pop('security_name', 'evesso')
# we provide app object, so we don't use sso_url
if app is not None:
# check if the security_name exists in the securityDefinition
security = app.root.securityDefinitions.get(
self.security_name,
None
)
if security is None:
raise NameError(
"%s is not defined in the securityDefinitions" %
self.security_name
)
self.oauth_authorize = security.authorizationUrl
# some URL we still need to "manually" define... sadly
# we parse the authUrl so we don't care if it's TQ or SISI.
# https://github.com/ccpgames/esi-issues/issues/92
parsed_uri = urlparse(security.authorizationUrl)
self.oauth_token = '%s://%s/oauth/token' % (
parsed_uri.scheme,
parsed_uri.netloc
)
# no app object is provided, so we use direct URLs
else:
if sso_url is None or sso_url == "":
raise AttributeError("sso_url cannot be None or empty "
"without app parameter")
self.oauth_authorize = '%s/oauth/authorize' % sso_url
self.oauth_token = '%s/oauth/token' % sso_url
# use ESI url for verify, since it's better for caching
if esi_url is None or esi_url == "":
raise AttributeError("esi_url cannot be None or empty")
self.oauth_verify = '%s/verify/' % esi_url
# session request stuff
self._session = Session()
self._session.headers.update({
'Accept': 'application/json',
'User-Agent': (
'EsiPy/Security/ - '
'https://github.com/Kyria/EsiPy'
)
})
# token data
self.refresh_token = None
self.access_token = None
self.token_expiry = None
def __get_oauth_header(self):
""" Return the Bearer Authorization header required in oauth calls
:return: a dict with the authorization header
"""
return {'Authorization': 'Bearer %s' % self.access_token}
def __make_token_request_parameters(self, params):
""" Return the token uri from the securityDefinition
:param params: the data given to the request
:return: the oauth/token uri
"""
request_params = {
'data': params,
'url': self.oauth_token,
}
return request_params
def get_auth_uri(self, *args, **kwargs):
""" Constructs the full auth uri and returns it.
:param state: The state to pass through the auth process
:param redirect: The URI that the proxy server will redirect to
:return: the authorizationUrl with the correct parameters.
"""
return '%s?%s' % (
self.oauth_authorize,
urlencode(kwargs)
)
def get_refresh_token_params(self):
""" Return the param object for the post() call to get the access_token
from the refresh_token
:param code: the refresh token
:return: a dict with the url, params and header
"""
if self.refresh_token is None:
raise AttributeError('No refresh token is defined.')
return self.__make_token_request_parameters(
{
'grant_type': 'refresh_token',
'refresh_token': self.refresh_token,
}
)
def update_token(self, response_json):
""" Update access_token, refresh_token and token_expiry from the
response body.
The response must be converted to a json object before being passed as
a parameter
:param response_json: the response body to use.
"""
self.access_token = response_json['access_token']
self.token_expiry = int(time.time()) + response_json['expires_in']
if 'refresh_token' in response_json:
self.refresh_token = response_json['refresh_token']
def is_token_expired(self, offset=0):
""" Return true if the token is expired.
The offset can be used to change the expiry time:
- positive value decrease the time (sooner)
- negative value increase the time (later)
If the expiry is not set, always return True. This case allow the users
to define a security object, only knowing the refresh_token and get
a new access_token / expiry_time without errors.
:param offset: the expiry offset (in seconds) [default: 0]
:return: boolean true if expired, else false.
"""
if self.token_expiry is None:
return True
return int(time.time()) >= (self.token_expiry - offset)
def refresh(self):
""" Update the auth data (tokens) using the refresh token in auth.
"""
request_data = self.get_refresh_token_params()
res = self._session.post(**request_data)
if res.status_code != 200:
raise APIException(
request_data['url'],
res.status_code,
res.json()
)
json_res = res.json()
self.update_token(json_res)
return json_res
def verify(self):
""" Make a get call to the oauth/verify endpoint to get the user data
:return: the json with the data.
"""
res = self._session.get(
self.oauth_verify,
headers=self.__get_oauth_header()
)
if res.status_code != 200:
raise APIException(
self.oauth_verify,
res.status_code,
res.json()
)
return res.json()
def __call__(self, request):
""" Check if the request need security header and apply them.
Required for pyswagger.core.BaseClient.request().
:param request: the pyswagger request object to check
:return: the updated request.
"""
if not request._security:
return request
if self.is_token_expired():
json_response = self.refresh()
AFTER_TOKEN_REFRESH.send(**json_response)
for security in request._security:
if self.security_name not in security:
LOGGER.warning(
"Missing Securities: [%s]" % ", ".join(security.keys())
)
continue
if self.access_token is not None:
request._p['header'].update(self.__get_oauth_header())
return request

View File

@@ -74,7 +74,7 @@ class Fit(object):
"exportCharges": True, "exportCharges": True,
"openFitInNew": False, "openFitInNew": False,
"priceSystem": "Jita", "priceSystem": "Jita",
"priceSource": "eve-central.com", "priceSource": "eve-marketdata.com",
"showShipBrowserTooltip": True, "showShipBrowserTooltip": True,
"marketSearchDelay": 250 "marketSearchDelay": 250
} }
@@ -1209,11 +1209,9 @@ class Fit(object):
start_time = time() start_time = time()
pyfalog.info("=" * 10 + "recalc: {0}" + "=" * 10, fit.name) pyfalog.info("=" * 10 + "recalc: {0}" + "=" * 10, fit.name)
fit.factorReload = self.serviceFittingOptions["useGlobalForceReload"] fit.factorReload = self.serviceFittingOptions["useGlobalForceReload"]
fit.clear() fit.clear()
fit.calculateModifiedAttributes() fit.calculateModifiedAttributes()
pyfalog.info("=" * 10 + "recalc time: " + str(time() - start_time) + "=" * 10) pyfalog.info("=" * 10 + "recalc time: " + str(time() - start_time) + "=" * 10)

View File

@@ -19,6 +19,7 @@ cpu: Co-Processor
coproc: Co-Processor coproc: Co-Processor
dc: Damage Control dc: Damage Control
dcu: Damage Control dcu: Damage Control
dda: Drone Damage Amplifier
disco: Smartbomb disco: Smartbomb
eanm: Energized Adaptive Nano Membrane eanm: Energized Adaptive Nano Membrane
enam: Energized Adaptive Nano Membrane enam: Energized Adaptive Nano Membrane

View File

@@ -12,3 +12,10 @@
# Syntax: # Syntax:
# #
# abbreviation: full name # abbreviation: full name
#
# The default jargon definitions are stored in Pyfa itself as well, and are
# listed here for convenience overriding them. To disable a jargon definition,
# set it as an empty string. For example, if you do not want "web" to return
# anything containing "stasis":
#
# web: ""

View File

@@ -20,17 +20,18 @@
import config import config
import pkg_resources import pkg_resources
class Jargon(object): class Jargon(object):
def __init__(self, rawdata: dict): def __init__(self, rawdata: dict):
self._rawdata = rawdata self._rawdata = rawdata
# copy the data to lowercase keys, ignore blank keys # copy the data to lowercase keys, ignore blank keys
self._data = {str(k).lower():v for k,v in rawdata.items() if k} self._data = {str(k).lower(): v for k, v in rawdata.items() if k}
def get(self, term: str) -> str: def get(self, term: str) -> str:
return self._data.get(term.lower()) return self._data.get(term.lower())
def get_rawdata() -> dict: def get_rawdata(self) -> dict:
return self._rawdata return self._rawdata
def apply(self, query): def apply(self, query):

View File

@@ -26,16 +26,12 @@ from .resources import DEFAULT_DATA, DEFAULT_HEADER
JARGON_PATH = os.path.join(config.savePath, 'jargon.yaml') JARGON_PATH = os.path.join(config.savePath, 'jargon.yaml')
class JargonLoader(object): class JargonLoader(object):
def __init__(self, jargon_path: str): def __init__(self, jargon_path: str):
self.jargon_path = jargon_path self.jargon_path = jargon_path
self._jargon_mtime = 0 # type: int self._jargon_mtime = 0 # type: int
self._jargon = None # type: Jargon self._jargon = None # type: Jargon
def save_jargon(self, data: Jargon):
rawdata = data.get_rawdata()
with open(JARGON_PATH, 'w') as f:
yaml.dump(rawdata, stream=f, default_flow_style=False)
def get_jargon(self) -> Jargon: def get_jargon(self) -> Jargon:
if self._is_stale(): if self._is_stale():
@@ -47,10 +43,12 @@ class JargonLoader(object):
self.jargon_mtime != self._get_jargon_file_mtime()) self.jargon_mtime != self._get_jargon_file_mtime())
def _load_jargon(self): def _load_jargon(self):
jargondata = yaml.load(DEFAULT_DATA)
with open(JARGON_PATH) as f: with open(JARGON_PATH) as f:
rawdata = yaml.load(f) userdata = yaml.load(f)
jargondata.update(userdata)
self.jargon_mtime = self._get_jargon_file_mtime() self.jargon_mtime = self._get_jargon_file_mtime()
self._jargon = Jargon(rawdata) self._jargon = Jargon(jargondata)
def _get_jargon_file_mtime(self) -> int: def _get_jargon_file_mtime(self) -> int:
if not os.path.exists(self.jargon_path): if not os.path.exists(self.jargon_path):
@@ -60,17 +58,22 @@ class JargonLoader(object):
@staticmethod @staticmethod
def init_user_jargon(jargon_path): def init_user_jargon(jargon_path):
values = yaml.load(DEFAULT_DATA) values = yaml.load(DEFAULT_DATA)
if os.path.exists(jargon_path):
with open(jargon_path) as f: # Disabled for issue/1533; do not overwrite existing user config
custom_values = yaml.load(f) # if os.path.exists(jargon_path):
if custom_values: # with open(jargon_path) as f:
values.update(custom_values) # custom_values = yaml.load(f)
with open(jargon_path, 'w') as f: # if custom_values:
f.write(DEFAULT_HEADER) # values.update(custom_values)
f.write('\n\n')
yaml.dump(values, stream=f, default_flow_style=False) if not os.path.exists(jargon_path):
with open(jargon_path, 'w') as f:
f.write(DEFAULT_HEADER)
f.write('\n\n')
yaml.dump(values, stream=f, default_flow_style=False)
_instance = None _instance = None
@staticmethod @staticmethod
def instance(jargon_path=None): def instance(jargon_path=None):
if not JargonLoader._instance: if not JargonLoader._instance:
@@ -78,4 +81,5 @@ class JargonLoader(object):
JargonLoader._instance = JargonLoader(jargon_path) JargonLoader._instance = JargonLoader(jargon_path)
return JargonLoader._instance return JargonLoader._instance
JargonLoader.init_user_jargon(JARGON_PATH) JargonLoader.init_user_jargon(JARGON_PATH)

View File

@@ -41,6 +41,7 @@ pyfalog = Logger(__name__)
# Event which tells threads dependent on Market that it's initialized # Event which tells threads dependent on Market that it's initialized
mktRdy = threading.Event() mktRdy = threading.Event()
class ShipBrowserWorkerThread(threading.Thread): class ShipBrowserWorkerThread(threading.Thread):
def __init__(self): def __init__(self):
threading.Thread.__init__(self) threading.Thread.__init__(self)
@@ -113,10 +114,8 @@ class SearchWorkerThread(threading.Thread):
else: else:
filter_ = None filter_ = None
jargon_request = self.jargonLoader.get_jargon().apply(request) jargon_request = self.jargonLoader.get_jargon().apply(request)
results = [] results = []
if len(request) >= config.minItemSearchLength: if len(request) >= config.minItemSearchLength:
results = eos.db.searchItems(request, where=filter_, results = eos.db.searchItems(request, where=filter_,

View File

@@ -1 +1 @@
__all__ = ['evecentral', 'evemarketdata'] __all__ = ['evemarketer', 'evemarketdata']

View File

@@ -35,7 +35,7 @@ class EveMarketData(object):
def __init__(self, types, system, priceMap): def __init__(self, types, system, priceMap):
data = {} data = {}
baseurl = "https://eve-marketdata.com/api/item_prices.xml" baseurl = "https://eve-marketdata.com/api/item_prices.xml"
data["system_id"] = system # Use Jita for market data["system_id"] = system # Use Jita for market
data["type_ids"] = ','.join(str(x) for x in types) data["type_ids"] = ','.join(str(x) for x in types)
network = Network.getInstance() network = Network.getInstance()

View File

@@ -30,11 +30,11 @@ pyfalog = Logger(__name__)
class EveCentral(object): class EveCentral(object):
name = "eve-central.com" name = "evemarketer"
def __init__(self, types, system, priceMap): def __init__(self, types, system, priceMap):
data = {} data = {}
baseurl = "https://eve-central.com/api/marketstat" baseurl = "https://api.evemarketer.com/ec/marketstat"
data["usesystem"] = system # Use Jita for market data["usesystem"] = system # Use Jita for market
data["typeid"] = set() data["typeid"] = set()

View File

@@ -93,25 +93,7 @@ class Network(object):
# or with HTTP Basic auth support: proxies = {'http': 'http://user:pass@10.10.1.10:3128/'} # or with HTTP Basic auth support: proxies = {'http': 'http://user:pass@10.10.1.10:3128/'}
# then you do: requests.get('http://example.org', proxies=proxies) # then you do: requests.get('http://example.org', proxies=proxies)
proxies = None proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat()
proxy_settings = NetworkSettings.getInstance().getProxySettings()
# proxy_settings is a tuple of (host, port), like ('192.168.20.1', 3128), or None
if proxy_settings is not None:
# form proxy address in format "http://host:port
proxy_host_port = '{}:{}'.format(proxy_settings[0], proxy_settings[1])
proxy_auth_details = NetworkSettings.getInstance().getProxyAuthDetails()
# proxy_auth_details is a tuple of (login, password), or None
user_pass = ''
if proxy_auth_details is not None:
# construct prefix in form "user:password@"
user_pass = '{}:{}@'.format(proxy_auth_details[0], proxy_auth_details[1])
proxies = {
'http': 'http://' + user_pass + proxy_host_port,
'https': 'http://' + user_pass + proxy_host_port
}
# final form: { 'http': 'http://user:password@host:port', ... }, or
# { 'http': 'http://host:port', ... } if no auth info.
try: try:
resp = requests.get(url, headers=headers, proxies=proxies, **kwargs) resp = requests.get(url, headers=headers, proxies=proxies, **kwargs)

View File

@@ -58,6 +58,7 @@ from collections import OrderedDict
class ESIExportException(Exception): class ESIExportException(Exception):
pass pass
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
EFT_SLOT_ORDER = [Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE] EFT_SLOT_ORDER = [Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE]
@@ -349,7 +350,6 @@ class Port(object):
nested_dict = lambda: collections.defaultdict(nested_dict) nested_dict = lambda: collections.defaultdict(nested_dict)
fit = nested_dict() fit = nested_dict()
sEsi = Esi.getInstance()
sFit = svcFit.getInstance() sFit = svcFit.getInstance()
# max length is 50 characters # max length is 50 characters

View File

@@ -234,4 +234,4 @@ class PriceWorkerThread(threading.Thread):
self.wait[itemID].append(callback) self.wait[itemID].append(callback)
from service.marketSources import evecentral, evemarketdata # noqa: E402 from service.marketSources import evemarketer, evemarketdata # noqa: E402

View File

@@ -103,11 +103,18 @@ class AuthHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, format, *args): def log_message(self, format, *args):
return return
import socketserver
# http://code.activestate.com/recipes/425210-simple-stoppable-server-using-socket-timeout/ # http://code.activestate.com/recipes/425210-simple-stoppable-server-using-socket-timeout/
class StoppableHTTPServer(http.server.HTTPServer): class StoppableHTTPServer(socketserver.TCPServer):
def server_bind(self): def server_bind(self):
http.server.HTTPServer.server_bind(self) # Can't use HTTPServer due to reliance on socket.getfqdn() which seems to be bugged.
# See https://github.com/pyfa-org/Pyfa/issues/1560#issuecomment-390095101
socketserver.TCPServer.server_bind(self)
host, port = self.server_address[:2]
self.server_name = host
self.server_port = port
# self.settings = CRESTSettings.getInstance() # self.settings = CRESTSettings.getInstance()
# Allow listening for x seconds # Allow listening for x seconds

View File

@@ -284,6 +284,23 @@ class NetworkSettings(object):
self.serviceNetworkSettings["login"] = login self.serviceNetworkSettings["login"] = login
self.serviceNetworkSettings["password"] = password self.serviceNetworkSettings["password"] = password
def getProxySettingsInRequestsFormat(self) -> dict:
proxies = {}
proxy_settings = self.getProxySettings()
if proxy_settings is not None:
# form proxy address in format "http://host:port
proxy_host_port = '{}:{}'.format(proxy_settings[0], proxy_settings[1])
proxy_auth_details = self.getProxyAuthDetails()
user_pass = ''
if proxy_auth_details is not None:
# construct prefix in form "user:password@"
user_pass = '{}:{}@'.format(proxy_auth_details[0], proxy_auth_details[1])
proxies = {
'http': 'http://' + user_pass + proxy_host_port,
'https': 'http://' + user_pass + proxy_host_port
}
return proxies
class HTMLExportSettings(object): class HTMLExportSettings(object):
""" """
@@ -363,10 +380,18 @@ class EsiSettings(object):
return cls._instance return cls._instance
def __init__(self): def __init__(self):
# SSO Mode:
# 0 - pyfa.io
# 1 - custom application
# LoginMode: # LoginMode:
# 0 - Server Start Up # 0 - Server Start Up
# 1 - User copy and paste data from website to pyfa # 1 - User copy and paste data from website to pyfa
defaults = {"loginMode": 0, "clientID": "", "clientSecret": "", "timeout": 60} defaults = {
"ssoMode": 0,
"loginMode": 0,
"clientID": "",
"clientSecret": "",
"timeout": 60}
self.settings = SettingsProvider.getInstance().getSettings( self.settings = SettingsProvider.getInstance().getSettings(
"pyfaServiceEsiSettings", "pyfaServiceEsiSettings",

View File

@@ -48,7 +48,12 @@ class CheckUpdateThread(threading.Thread):
network = Network.getInstance() network = Network.getInstance()
try: try:
response = network.request('https://api.github.com/repos/pyfa-org/Pyfa/releases', network.UPDATE) try:
response = network.request('https://www.pyfa.io/update_check?pyfa_version={}&client_hash={}'.format(
config.version, config.getClientSecret()), network.UPDATE)
except Exception as e:
response = network.request('https://api.github.com/repos/pyfa-org/Pyfa/releases', network.UPDATE)
jsonResponse = response.json() jsonResponse = response.json()
jsonResponse.sort( jsonResponse.sort(
key=lambda x: calendar.timegm(dateutil.parser.parse(x['published_at']).utctimetuple()), key=lambda x: calendar.timegm(dateutil.parser.parse(x['published_at']).utctimetuple()),

View File

@@ -7,7 +7,7 @@ passenv = CI TRAVIS TRAVIS_*
deps = deps =
-rrequirements.txt -rrequirements.txt
-rrequirements_test.txt -rrequirements_test.txt
basepython = python2.7 basepython = python3.6
commands = py.test -vv --cov Pyfa tests2/ commands = py.test -vv --cov Pyfa tests2/
[testenv:pep8] [testenv:pep8]