From d29b4d91e9f6622a8c0d8c80e8291a2391787107 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 7 Aug 2014 22:47:59 -0400 Subject: [PATCH 01/16] Adds "Chance to Jam" to Sensor Strength tooltip --- eos/effects/ewtesteffectjam.py | 8 ++++++-- eos/saveddata/fit.py | 13 ++++++++++--- gui/builtinStatsViews/targetingMiscViewFull.py | 15 +++++++++++++-- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/eos/effects/ewtesteffectjam.py b/eos/effects/ewtesteffectjam.py index 7c18b5682..de80978a4 100644 --- a/eos/effects/ewtesteffectjam.py +++ b/eos/effects/ewtesteffectjam.py @@ -2,5 +2,9 @@ # Modules from group: ECM (44 of 44) # Drones named like: EC (3 of 3) type = "projected", "active" -def handler(fit, container, context): - pass +def handler(fit, module, context): + if "projected" in context: + # jam formula: 1 - (1- (jammer str/ship str))^(# of jam mods with same str)) + strModifier = 1 - module.getModifiedItemAttr("scan{0}StrengthBonus".format(fit.scanType))/fit.scanStrength + + fit.ecmProjectedStr *= strModifier diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index acafbcfb7..e2aaaa847 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -65,6 +65,7 @@ class Fit(object): self.boostsFits = set() self.gangBoosts = None self.timestamp = time.time() + self.ecmProjectedStr = 1 self.build() @reconstructor @@ -93,6 +94,7 @@ class Fit(object): self.fleet = None self.boostsFits = set() self.gangBoosts = None + self.ecmProjectedStr = 1 self.extraAttributes = ModifiedAttributeDict(self) self.extraAttributes.original = self.EXTRA_ATTRIBUTES self.ship = Ship(db.getItem(self.shipID)) if self.shipID is not None else None @@ -226,6 +228,10 @@ class Fit(object): return type + @property + def jamChance(self): + return (1-self.ecmProjectedStr)*100 + @property def alignTime(self): agility = self.ship.getModifiedItemAttr("agility") @@ -269,6 +275,7 @@ class Fit(object): self.__capState = None self.__capUsed = None self.__capRecharge = None + self.ecmProjectedStr = 1 del self.__calculatedTargets[:] del self.__extraDrains[:] @@ -341,7 +348,7 @@ class Fit(object): else: c = chain((self.character, self.ship), self.drones, self.boosters, self.appliedImplants, self.modules, self.projectedDrones, self.projectedModules) - + if self.gangBoosts is not None: contextMap = {Skill: "skill", Ship: "ship", @@ -366,7 +373,7 @@ class Fit(object): effect.handler(self, thing, context) except: pass - + for item in c: # Registering the item about to affect the fit allows us to track "Affected By" relations correctly if item is not None: @@ -375,7 +382,7 @@ class Fit(object): if forceProjected is True: targetFit.register(item) item.calculateModifiedAttributes(targetFit, runTime, True) - + for fit in self.projectedFits: fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage) diff --git a/gui/builtinStatsViews/targetingMiscViewFull.py b/gui/builtinStatsViews/targetingMiscViewFull.py index 6b61ace4b..78a7285b4 100644 --- a/gui/builtinStatsViews/targetingMiscViewFull.py +++ b/gui/builtinStatsViews/targetingMiscViewFull.py @@ -189,12 +189,15 @@ class TargetingMiscViewFull(StatsView): right = "%s [%d]" % (size, radius) lockTime += "%5s\t%s\n" % (left,right) label.SetToolTip(wx.ToolTip(lockTime)) - elif labelName == "labelSensorStr": - label.SetToolTip(wx.ToolTip("Type: %s - %.1f" % (fit.scanType, mainValue))) elif labelName == "labelFullSigRadius": label.SetToolTip(wx.ToolTip("Probe Size: %.3f" % (fit.probeSize or 0) )) elif labelName == "labelFullWarpSpeed": label.SetToolTip(wx.ToolTip("Max Warp Distance: %.1f AU" % fit.maxWarpDistance)) + elif labelName == "labelSensorStr": + if fit.jamChance > 0: + label.SetToolTip(wx.ToolTip("Type: %s\n%.1f%% Chance of Jam" % (fit.scanType, fit.jamChance))) + else: + label.SetToolTip(wx.ToolTip("Type: %s" % (fit.scanType))) elif labelName == "labelFullAlignTime": label.SetToolTip(wx.ToolTip("%.3f" % mainValue)) elif labelName == "labelFullCargo": @@ -214,6 +217,14 @@ class TargetingMiscViewFull(StatsView): label.SetToolTip(wx.ToolTip("Max Warp Distance: %.1f AU" % fit.maxWarpDistance)) else: label.SetToolTip(wx.ToolTip("")) + elif labelName == "labelSensorStr": + if fit: + if fit.jamChance > 0: + label.SetToolTip(wx.ToolTip("Type: %s\n%.1f%% Chance of Jam" % (fit.scanType, fit.jamChance))) + else: + label.SetToolTip(wx.ToolTip("Type: %s" % (fit.scanType))) + else: + label.SetToolTip(wx.ToolTip("")) elif labelName == "labelFullCargo": if fit: cachedCargo = self._cachedValues[counter] From bec61e43ae324d9bb8dcb335165e06fdbcf41b0d Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 7 Aug 2014 23:09:03 -0400 Subject: [PATCH 02/16] Only do overload effect if not projecting (otherwise overload bonus is applied twice) --- eos/effects/overloadselfecmstrenghtbonus.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/eos/effects/overloadselfecmstrenghtbonus.py b/eos/effects/overloadselfecmstrenghtbonus.py index 1a475aba7..c7906e010 100644 --- a/eos/effects/overloadselfecmstrenghtbonus.py +++ b/eos/effects/overloadselfecmstrenghtbonus.py @@ -3,7 +3,8 @@ # Modules from group: ECM Burst (7 of 7) type = "overheat" def handler(fit, module, context): - for scanType in ("Gravimetric", "Magnetometric", "Radar", "Ladar"): - module.boostItemAttr("scan{0}StrengthBonus".format(scanType), - module.getModifiedItemAttr("overloadECMStrengthBonus"), - stackingPenalties = True) + if "projected" not in context: + for scanType in ("Gravimetric", "Magnetometric", "Radar", "Ladar"): + module.boostItemAttr("scan{0}StrengthBonus".format(scanType), + module.getModifiedItemAttr("overloadECMStrengthBonus"), + stackingPenalties = True) From a5773a3fd6409527c076dfedea2f5b94d42641b9 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 14 Aug 2014 01:59:03 -0400 Subject: [PATCH 03/16] Update to eveapi 1.3.0, and move to service. --- {eos => service}/eveapi.py | 205 ++++++++++++++++++++++++++++++------- 1 file changed, 167 insertions(+), 38 deletions(-) rename {eos => service}/eveapi.py (80%) diff --git a/eos/eveapi.py b/service/eveapi.py similarity index 80% rename from eos/eveapi.py rename to service/eveapi.py index db0888c10..087934ce5 100644 --- a/eos/eveapi.py +++ b/service/eveapi.py @@ -1,7 +1,7 @@ #----------------------------------------------------------------------------- # eveapi - EVE Online API access # -# Copyright (c)2007 Jamie "Entity" van den Berge +# Copyright (c)2007-2014 Jamie "Entity" van den Berge # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -25,8 +25,47 @@ # OTHER DEALINGS IN THE SOFTWARE # #----------------------------------------------------------------------------- -# Version: 1.1.9-2 - 30 September 2011 -# - merge workaround provided by Entity to make it work with http proxies +# +# Version: 1.3.0 - 27 May 2014 +# - Added set_user_agent() module-level function to set the User-Agent header +# to be used for any requests by the library. If this function is not used, +# a warning will be thrown for every API request. +# +# Version: 1.2.9 - 14 September 2013 +# - Updated error handling: Raise an AuthenticationError in case +# the API returns HTTP Status Code 403 - Forbidden +# +# Version: 1.2.8 - 9 August 2013 +# - the XML value cast function (_autocast) can now be changed globally to a +# custom one using the set_cast_func(func) module-level function. +# +# Version: 1.2.7 - 3 September 2012 +# - Added get() method to Row object. +# +# Version: 1.2.6 - 29 August 2012 +# - Added finer error handling + added setup.py to allow distributing eveapi +# through pypi. +# +# Version: 1.2.5 - 1 August 2012 +# - Row objects now have __hasattr__ and __contains__ methods +# +# Version: 1.2.4 - 12 April 2012 +# - API version of XML response now available as _meta.version +# +# Version: 1.2.3 - 10 April 2012 +# - fix for tags of the form +# +# Version: 1.2.2 - 27 February 2012 +# - fix for the workaround in 1.2.1. +# +# Version: 1.2.1 - 23 February 2012 +# - added workaround for row tags missing attributes that were defined +# in their rowset (this should fix ContractItems) +# +# Version: 1.2.0 - 18 February 2012 +# - fix handling of empty XML tags. +# - improved proxy support a bit. +# # Version: 1.1.9 - 2 September 2011 # - added workaround for row tags with attributes that were not defined # in their rowset (this should fix AssetList) @@ -112,22 +151,53 @@ import httplib import urlparse import urllib import copy +import warnings from xml.parsers import expat from time import strptime from calendar import timegm proxy = None +proxySSL = False + +_default_useragent = "eveapi.py/1.3" +_useragent = None # use set_user_agent() to set this. #----------------------------------------------------------------------------- +def set_cast_func(func): + """Sets an alternative value casting function for the XML parser. + The function must have 2 arguments; key and value. It should return a + value or object of the type appropriate for the given attribute name/key. + func may be None and will cause the default _autocast function to be used. + """ + global _castfunc + _castfunc = _autocast if func is None else func + +def set_user_agent(user_agent_string): + """Sets a User-Agent for any requests sent by the library.""" + global _useragent + _useragent = user_agent_string + + class Error(StandardError): def __init__(self, code, message): self.code = code self.args = (message.rstrip("."),) + def __unicode__(self): + return u'%s [code=%s]' % (self.args[0], self.code) + +class RequestError(Error): + pass + +class AuthenticationError(Error): + pass + +class ServerError(Error): + pass -def EVEAPIConnection(url="api.eveonline.com", cacheHandler=None, proxy=None): +def EVEAPIConnection(url="api.eveonline.com", cacheHandler=None, proxy=None, proxySSL=False): # Creates an API object through which you can call remote functions. # # The following optional arguments may be provided: @@ -137,6 +207,8 @@ def EVEAPIConnection(url="api.eveonline.com", cacheHandler=None, proxy=None): # proxy - (host,port) specifying a proxy server through which to request # the API pages. Specifying a proxy overrides default proxy. # + # proxySSL - True if the proxy requires SSL, False otherwise. + # # cacheHandler - an object which must support the following interface: # # retrieve(host, path, params) @@ -173,6 +245,7 @@ def EVEAPIConnection(url="api.eveonline.com", cacheHandler=None, proxy=None): ctx._scheme = p.scheme ctx._host = p.netloc ctx._proxy = proxy or globals()["proxy"] + ctx._proxySSL = proxySSL or globals()["proxySSL"] return ctx @@ -197,7 +270,14 @@ def _ParseXML(response, fromContext, storeFunc): error = getattr(obj, "error", False) if error: - raise Error(error.code, error.data) + if error.code >= 500: + raise ServerError(error.code, error.data) + elif error.code >= 200: + raise AuthenticationError(error.code, error.data) + elif error.code >= 100: + raise RequestError(error.code, error.data) + else: + raise Error(error.code, error.data) result = getattr(obj, "result", False) if not result: @@ -304,36 +384,35 @@ class _RootContext(_Context): response = None if response is None: - if self._scheme == "https": - connectionclass = httplib.HTTPSConnection - else: - connectionclass = httplib.HTTPConnection + if not _useragent: + warnings.warn("No User-Agent set! Please use the set_user_agent() module-level function before accessing the EVE API.", stacklevel=3) if self._proxy is None: + req = path if self._scheme == "https": - connectionclass = httplib.HTTPSConnection + conn = httplib.HTTPSConnection(self._host) else: - connectionclass = httplib.HTTPConnection - - http = connectionclass(self._host) - if kw: - http.request("POST", path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"}) - else: - http.request("GET", path) + conn = httplib.HTTPConnection(self._host) else: - connectionclass = httplib.HTTPConnection - http = connectionclass(*self._proxy) - if kw: - http.request("POST", self._scheme+'://'+self._host+path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"}) + req = self._scheme+'://'+self._host+path + if self._proxySSL: + conn = httplib.HTTPSConnection(*self._proxy) else: - http.request("GET", self._scheme+'://'+self._host+path) + conn = httplib.HTTPConnection(*self._proxy) - response = http.getresponse() + if kw: + conn.request("POST", req, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": _useragent or _default_useragent}) + else: + conn.request("GET", req, "", {"User-Agent": _useragent or _default_useragent}) + + response = conn.getresponse() if response.status != 200: if response.status == httplib.NOT_FOUND: raise AttributeError("'%s' not available on API server (404 Not Found)" % path) + elif response.status == httplib.FORBIDDEN: + raise AuthenticationError(response.status, 'HTTP 403 - Forbidden') else: - raise RuntimeError("'%s' request failed (%d %s)" % (path, response.status, response.reason)) + raise ServerError(response.status, "'%s' request failed (%s)" % (path, response.reason)) if cache: store = True @@ -348,7 +427,7 @@ class _RootContext(_Context): # implementor is handling fallbacks... try: return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj))) - except Error, reason: + except Error, e: response = retrieve_fallback(self._host, path, kw, reason=e) if response is not None: return response @@ -386,6 +465,7 @@ def _autocast(key, value): # couldn't cast. return string unchanged. return value +_castfunc = _autocast class _Parser(object): @@ -460,20 +540,42 @@ class _Parser(object): # really assume the rest of the xml is going to be what we expect. if name != "eveapi": raise RuntimeError("Invalid API response") + try: + this.version = attributes[attributes.index("version")+1] + except KeyError: + raise RuntimeError("Invalid API response") self.root = this if isinstance(self.container, Rowset) and (self.container.__catch == this._name): # - # - check for missing columns attribute (see above) + # - check for missing columns attribute (see above). + # - check for missing row attributes. # - check for extra attributes that were not defined in the rowset, # such as rawQuantity in the assets lists. # In either case the tag is assumed to be correct and the rowset's - # columns are overwritten with the tag's version. - if not self.container._cols or (len(attributes)/2 > len(self.container._cols)): - self.container._cols = attributes[0::2] + # columns are overwritten with the tag's version, if required. + numAttr = len(attributes)/2 + numCols = len(self.container._cols) + if numAttr < numCols and (attributes[-2] == self.container._cols[-1]): + # the row data is missing attributes that were defined in the rowset. + # missing attributes' values will be set to None. + fixed = [] + row_idx = 0; hdr_idx = 0; numAttr*=2 + for col in self.container._cols: + if col == attributes[row_idx]: + fixed.append(_castfunc(col, attributes[row_idx+1])) + row_idx += 2 + else: + fixed.append(None) + hdr_idx += 1 + self.container.append(fixed) + else: + if not self.container._cols or (numAttr > numCols): + # the row data contains more attributes than were defined. + self.container._cols = attributes[0::2] + self.container.append([_castfunc(attributes[i], attributes[i+1]) for i in xrange(0, len(attributes), 2)]) # - self.container.append([_autocast(attributes[i], attributes[i+1]) for i in xrange(0, len(attributes), 2)]) this._isrow = True this._attributes = this._attributes2 = None else: @@ -481,10 +583,11 @@ class _Parser(object): this._attributes = attributes this._attributes2 = [] - self.container = this - + self.container = self._last = this + self.has_cdata = False def tag_cdata(self, data): + self.has_cdata = True if self._cdata: # unset cdata flag to indicate it's been handled. self._cdata = False @@ -493,7 +596,7 @@ class _Parser(object): return this = self.container - data = _autocast(this._name, data) + data = _castfunc(this._name, data) if this._isrow: # sigh. anonymous data inside rows makes Entity cry. @@ -518,6 +621,7 @@ class _Parser(object): def tag_end(self, name): this = self.container + if this is self.root: del this._attributes #this.__dict__.pop("_attributes", None) @@ -553,13 +657,26 @@ class _Parser(object): # really happen, but it doesn't hurt to handle this case! sibling = getattr(self.container, this._name, None) if sibling is None: - self.container._attributes2.append(this._name) - setattr(self.container, this._name, this) + if (not self.has_cdata) and (self._last is this) and (name != "rowset"): + if attributes: + # tag of the form + e = Element() + e._name = this._name + setattr(self.container, this._name, e) + for i in xrange(0, len(attributes), 2): + setattr(e, attributes[i], attributes[i+1]) + else: + # tag of the form: , treat as empty string. + setattr(self.container, this._name, "") + else: + self.container._attributes2.append(this._name) + setattr(self.container, this._name, this) + # Note: there aren't supposed to be any NON-rowset tags containing # multiples of some tag or attribute. Code below handles this case. elif isinstance(sibling, Rowset): # its doppelganger is a rowset, append this as a row to that. - row = [_autocast(attributes[i], attributes[i+1]) for i in xrange(0, len(attributes), 2)] + row = [_castfunc(attributes[i], attributes[i+1]) for i in xrange(0, len(attributes), 2)] row.extend([getattr(this, col) for col in attributes2]) sibling.append(row) elif isinstance(sibling, Element): @@ -568,7 +685,7 @@ class _Parser(object): # into a Rowset, adding the sibling element and this one. rs = Rowset() rs.__catch = rs._name = this._name - row = [_autocast(attributes[i], attributes[i+1]) for i in xrange(0, len(attributes), 2)]+[getattr(this, col) for col in attributes2] + row = [_castfunc(attributes[i], attributes[i+1]) for i in xrange(0, len(attributes), 2)]+[getattr(this, col) for col in attributes2] rs.append(row) row = [getattr(sibling, attributes[i]) for i in xrange(0, len(attributes), 2)]+[getattr(sibling, col) for col in attributes2] rs.append(row) @@ -581,7 +698,7 @@ class _Parser(object): # Now fix up the attributes and be done with it. for i in xrange(0, len(attributes), 2): - this.__dict__[attributes[i]] = _autocast(attributes[i], attributes[i+1]) + this.__dict__[attributes[i]] = _castfunc(attributes[i], attributes[i+1]) return @@ -630,6 +747,18 @@ class Row(object): raise TypeError("Incompatible comparison type") return cmp(self._cols, other._cols) or cmp(self._row, other._row) + def __hasattr__(self, this): + if this in self._cols: + return self._cols.index(this) < len(self._row) + return False + + __contains__ = __hasattr__ + + def get(self, this, default=None): + if (this in self._cols) and (self._cols.index(this) < len(self._row)): + return self._row[self._cols.index(this)] + return default + def __getattr__(self, this): try: return self._row[self._cols.index(this)] From 4fe80b755424e89b4532a12110599b29fc8eb7b1 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 14 Aug 2014 11:43:25 -0400 Subject: [PATCH 04/16] Implement new network service for: Updates, CREST. --- gui/builtinPreferenceViews/__init__.py | 2 +- .../pyfaGeneralPreferences.py | 2 - .../pyfaHTMLExportPreferences.py | 2 - ...eferences.py => pyfaNetworkPreferences.py} | 30 ++++----- service/__init__.py | 2 + service/market.py | 4 +- service/network.py | 67 +++++++++++++++++++ service/port.py | 13 ++-- service/settings.py | 31 +++++---- service/update.py | 11 +-- 10 files changed, 116 insertions(+), 48 deletions(-) rename gui/builtinPreferenceViews/{pyfaProxyPreferences.py => pyfaNetworkPreferences.py} (89%) create mode 100644 service/network.py diff --git a/gui/builtinPreferenceViews/__init__.py b/gui/builtinPreferenceViews/__init__.py index ac253597d..923e011ab 100644 --- a/gui/builtinPreferenceViews/__init__.py +++ b/gui/builtinPreferenceViews/__init__.py @@ -1 +1 @@ -__all__ = ["pyfaGeneralPreferences","pyfaHTMLExportPreferences","pyfaUpdatePreferences","pyfaProxyPreferences"] \ No newline at end of file +__all__ = ["pyfaGeneralPreferences","pyfaHTMLExportPreferences","pyfaUpdatePreferences","pyfaNetworkPreferences"] diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py index a2f6c193c..98190fcde 100644 --- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py +++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py @@ -1,6 +1,4 @@ import wx -import service -import urllib2 from gui.preferenceView import PreferenceView from gui import bitmapLoader diff --git a/gui/builtinPreferenceViews/pyfaHTMLExportPreferences.py b/gui/builtinPreferenceViews/pyfaHTMLExportPreferences.py index e160ae0bc..992fff1a3 100644 --- a/gui/builtinPreferenceViews/pyfaHTMLExportPreferences.py +++ b/gui/builtinPreferenceViews/pyfaHTMLExportPreferences.py @@ -1,6 +1,4 @@ import wx -import service -import urllib2 import os from gui.preferenceView import PreferenceView diff --git a/gui/builtinPreferenceViews/pyfaProxyPreferences.py b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py similarity index 89% rename from gui/builtinPreferenceViews/pyfaProxyPreferences.py rename to gui/builtinPreferenceViews/pyfaNetworkPreferences.py index 40c2286f1..d929e74cf 100644 --- a/gui/builtinPreferenceViews/pyfaProxyPreferences.py +++ b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py @@ -1,28 +1,24 @@ import wx -import service -import urllib2 from gui.preferenceView import PreferenceView from gui import bitmapLoader import gui.mainFrame import service -import gui.globalEvents as GE - -class PFProxyPref ( PreferenceView): - title = "Proxy" +class PFNetworkPref ( PreferenceView): + title = "Network" def populatePanel( self, panel ): self.mainFrame = gui.mainFrame.MainFrame.getInstance() - self.proxySettings = service.settings.ProxySettings.getInstance() + self.networkSettings = service.settings.NetworkSettings.getInstance() self.dirtySettings = False - self.nMode = self.proxySettings.getMode() - self.nAddr = self.proxySettings.getAddress() - self.nPort = self.proxySettings.getPort() - self.nType = self.proxySettings.getType() + self.nMode = self.networkSettings.getMode() + self.nAddr = self.networkSettings.getAddress() + self.nPort = self.networkSettings.getPort() + self.nType = self.networkSettings.getType() mainSizer = wx.BoxSizer( wx.VERTICAL ) @@ -89,7 +85,7 @@ class PFProxyPref ( PreferenceView): mainSizer.Add(btnSizer, 0, wx.EXPAND,5) - proxy = self.proxySettings.autodetect() + proxy = self.networkSettings.autodetect() if proxy is not None: addr,port = proxy @@ -133,10 +129,10 @@ class PFProxyPref ( PreferenceView): self.SaveSettings() def SaveSettings(self): - self.proxySettings.setMode(self.nMode) - self.proxySettings.setAddress(self.nAddr) - self.proxySettings.setPort(self.nPort) - self.proxySettings.setType(self.nType) + self.networkSettings.setMode(self.nMode) + self.networkSettings.setAddress(self.nAddr) + self.networkSettings.setPort(self.nPort) + self.networkSettings.setType(self.nType) def UpdateApplyButtonState(self): if self.dirtySettings: @@ -172,4 +168,4 @@ class PFProxyPref ( PreferenceView): def getImage(self): return bitmapLoader.getBitmap("prefs_proxy", "icons") -PFProxyPref.register() \ No newline at end of file +PFNetworkPref.register() diff --git a/service/__init__.py b/service/__init__.py index e7581c675..b257d5100 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -6,3 +6,5 @@ from service.damagePattern import DamagePattern from service.settings import SettingsProvider from service.fleet import Fleet from service.update import Update +from service.network import Network +from service.eveapi import EVEAPIConnection, ParseXML diff --git a/service/market.py b/service/market.py index 94b25e023..397e3e89d 100644 --- a/service/market.py +++ b/service/market.py @@ -25,7 +25,7 @@ import Queue import eos.db import eos.types -from service.settings import SettingsProvider, ProxySettings +from service.settings import SettingsProvider, NetworkSettings try: from collections import OrderedDict @@ -78,7 +78,7 @@ class PriceWorkerThread(threading.Thread): # Grab prices, this is the time-consuming part if len(requests) > 0: - proxy = ProxySettings.getInstance().getProxySettings() + proxy = NetworkSettings.getInstance().getProxySettings() if proxy is not None: proxy = "{0}:{1}".format(*proxy) eos.types.Price.fetchPrices(requests, proxy=proxy) diff --git a/service/network.py b/service/network.py new file mode 100644 index 000000000..c013ede7a --- /dev/null +++ b/service/network.py @@ -0,0 +1,67 @@ +#=============================================================================== +# Copyright (C) 2014 Ryan Holmes +# +# This file is part of pyfa. +# +# pyfa is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pyfa is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyfa. If not, see . +#=============================================================================== + +from service.settings import NetworkSettings +import urllib2 +import urllib +import config + +class Network(): + # Request constants - every request must supply this, as it is checked if + # enabled or not via settings + ENABLED = 1 + EVE = 2 # Mostly API, but also covers CREST requests + PRICES = 4 + UPDATE = 8 + + _instance = None + @classmethod + def getInstance(cls): + if cls._instance == None: + cls._instance = Network() + + return cls._instance + + def request(self, url, type=None, postData=None): + + # URL is required to be https as of right now + print "Starting request: %s\n\tType: %s\n\tPost Data: %s"%(url,type,postData) + + # Make sure request is enabled + access = NetworkSettings.getInstance().getAccess() + + if not type or not type & access: # @todo: check if enabled + print "\tType not enabled" + return # @todo: throw exception + + # Set up some things for the request + versionString = "{0} {1} - {2} {3}".format(config.version, config.tag, config.expansionName, config.expansionVersion) + headers={"User-Agent" : "pyfa {0} (Python-urllib2)".format(versionString)} + + proxy = NetworkSettings.getInstance().getProxySettings() + if proxy is not None: + print "\tUsing a proxy" + proxy = urllib2.ProxyHandler({'https': "{0}:{1}".format(*proxy)}) + opener = urllib2.build_opener(proxy) + urllib2.install_opener(opener) + + request = urllib2.Request(url, headers=headers, data=urllib.urlencode(postData) if postData else None) + data = urllib2.urlopen(request) + print "\tReturning data" + return data diff --git a/service/port.py b/service/port.py index e4b40d2d6..87c992143 100644 --- a/service/port.py +++ b/service/port.py @@ -1,5 +1,5 @@ #=============================================================================== -# Copyright (C) 2010 Diego Duclos +# Copyright (C) 2014 Ryan Holmes # # This file is part of pyfa. # @@ -19,7 +19,6 @@ import re import xml.dom -import urllib2 import json from eos.types import State, Slot, Module, Cargo, Fit, Ship, Drone, Implant, Booster @@ -166,9 +165,9 @@ class Port(object): @staticmethod def importCrest(info): sMkt = service.Market.getInstance() + network = service.Network.getInstance() try: - # @todo: proxy - response = urllib2.urlopen("https://public-crest.eveonline.com/killmails/%s/%s/" % info) + response = network.request("https://public-crest.eveonline.com/killmails/%s/%s/" % info, network.EVE) except: return @@ -638,7 +637,7 @@ class Port(object): for subsystem in sorted(subsystems, key=lambda mod: mod.getModifiedItemAttr("subSystemSlot")): dna += ":{0};1".format(subsystem.itemID) - + for drone in fit.drones: dna += ":{0};{1}".format(drone.itemID, drone.amount) @@ -688,10 +687,10 @@ class Port(object): else: if not slot in slotNum: slotNum[slot] = 0 - + slotId = slotNum[slot] slotNum[slot] += 1 - + hardware = doc.createElement("hardware") hardware.setAttribute("type", module.item.name) slotName = Slot.getName(slot).lower() diff --git a/service/settings.py b/service/settings.py index ed7d0225e..f64caa5d1 100644 --- a/service/settings.py +++ b/service/settings.py @@ -111,13 +111,12 @@ class Settings(): return self.info.items() - -class ProxySettings(): +class NetworkSettings(): _instance = None @classmethod def getInstance(cls): if cls._instance == None: - cls._instance = ProxySettings() + cls._instance = NetworkSettings() return cls._instance @@ -127,33 +126,39 @@ class ProxySettings(): # 0 - No proxy # 1 - Auto-detected proxy settings # 2 - Manual proxy settings - serviceProxyDefaultSettings = {"mode": 1, "type": "https", "address": "", "port": ""} + serviceNetworkDefaultSettings = {"mode": 1, "type": "https", "address": "", "port": "", "access": 15} - self.serviceProxySettings = SettingsProvider.getInstance().getSettings("pyfaServiceProxySettings", serviceProxyDefaultSettings) + self.serviceNetworkSettings = SettingsProvider.getInstance().getSettings("pyfaServiceNetworkSettings", serviceNetworkDefaultSettings) def getMode(self): - return self.serviceProxySettings["mode"] + return self.serviceNetworkSettings["mode"] def getAddress(self): - return self.serviceProxySettings["address"] + return self.serviceNetworkSettings["address"] def getPort(self): - return self.serviceProxySettings["port"] + return self.serviceNetworkSettings["port"] def getType(self): - return self.serviceProxySettings["type"] + return self.serviceNetworkSettings["type"] + + def getAccess(self): + return self.serviceNetworkSettings["access"] def setMode(self, mode): - self.serviceProxySettings["mode"] = mode + self.serviceNetworkSettings["mode"] = mode def setAddress(self, addr): - self.serviceProxySettings["address"] = addr + self.serviceNetworkSettings["address"] = addr def setPort(self, port): - self.serviceProxySettings["port"] = port + self.serviceNetworkSettings["port"] = port def setType(self, type): - self.serviceProxySettings["type"] = type + self.serviceNetworkSettings["type"] = type + + def setAccess(self, access): + self.serviceNetworkSettings["access"] = access def autodetect(self): diff --git a/service/update.py b/service/update.py index 5ec6ffabf..84be89679 100644 --- a/service/update.py +++ b/service/update.py @@ -1,5 +1,5 @@ #=============================================================================== -# Copyright (C) 2010 Diego Duclos +# Copyright (C) 2014 Ryan Holmes # # This file is part of pyfa. # @@ -29,16 +29,19 @@ class CheckUpdateThread(threading.Thread): threading.Thread.__init__(self) self.callback = callback self.settings = service.settings.UpdateSettings.getInstance() + self.network = service.Network.getInstance() def run(self): # Suppress all if (self.settings.get('all')): return + network = service.Network.getInstance() + try: # @todo: use proxy settings? - response = urllib2.urlopen('https://api.github.com/repos/DarkFenX/Pyfa/releases') - jsonResponse = json.loads(response.read()); + response = network.request('https://api.github.com/repos/DarkFenX/Pyfa/releases', network.UPDATE) + jsonResponse = json.loads(response.read()) for release in jsonResponse: # Suppress pre releases @@ -69,7 +72,7 @@ class CheckUpdateThread(threading.Thread): else: if release['prerelease'] and rVersion > config.expansionVersion: wx.CallAfter(self.callback, release) # Singularity -> Singularity - break; + break except: # for when there is no internet connection pass From ed1b9854a0afdb1d611f8530577242df09030b32 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 14 Aug 2014 11:44:50 -0400 Subject: [PATCH 05/16] Modify eveapi to work with pyfa service, move all character API calls from EOS to Character service and use new network service --- eos/saveddata/character.py | 26 +---------------- service/character.py | 57 ++++++++++++++++++++++++-------------- service/eveapi.py | 47 ++++++++++++------------------- 3 files changed, 54 insertions(+), 76 deletions(-) diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index 281f27dbe..2e9d26456 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -18,12 +18,9 @@ #=============================================================================== -import urllib2 +from sqlalchemy.orm import validates, reconstructor from eos.effectHandlerHelpers import HandledItem -from sqlalchemy.orm import validates, reconstructor -import sqlalchemy.orm.exc as exc -from eos import eveapi import eos class Character(object): @@ -108,27 +105,6 @@ class Character(object): for skill in self.__skills: self.__skillIdMap[skill.itemID] = skill - def apiCharList(self, proxy=None): - api = eveapi.EVEAPIConnection(proxy=proxy) - auth = api.auth(keyID=self.apiID, vCode=self.apiKey) - apiResult = auth.account.Characters() - return map(lambda c: unicode(c.name), apiResult.characters) - - def apiFetch(self, charName, proxy=None): - api = eveapi.EVEAPIConnection(proxy=proxy) - auth = api.auth(keyID=self.apiID, vCode=self.apiKey) - apiResult = auth.account.Characters() - charID = None - for char in apiResult.characters: - if char.name == charName: - charID = char.characterID - - if charID == None: - return - - sheet = auth.character(charID).CharacterSheet() - self.apiUpdateCharSheet(sheet) - def apiUpdateCharSheet(self, sheet): del self.__skills[:] self.__skillIdMap.clear() diff --git a/service/character.py b/service/character.py index 7a8a6cbc6..f20beca10 100644 --- a/service/character.py +++ b/service/character.py @@ -17,25 +17,23 @@ # along with pyfa. If not, see . #=============================================================================== -import eos.db -import eos.types import copy -import service import itertools -from eos import eveapi -import config import json -import os.path -import locale import threading -import wx from codecs import open - from xml.etree import ElementTree from xml.dom import minidom - import gzip +import wx + +import eos.db +import eos.types +import service +import config + + class CharacterImportThread(threading.Thread): def __init__(self, paths, callback): threading.Thread.__init__(self) @@ -240,19 +238,36 @@ class Character(object): def charList(self, charID, userID, apiKey): char = eos.db.getCharacter(charID) - try: - char.apiID = userID - char.apiKey = apiKey - charList = char.apiCharList(proxy = service.settings.ProxySettings.getInstance().getProxySettings()) - char.chars = json.dumps(charList) - return charList - except: - return None + + char.apiID = userID + char.apiKey = apiKey + + api = service.EVEAPIConnection() + auth = api.auth(keyID=userID, vCode=apiKey) + apiResult = auth.account.Characters() + charList = map(lambda c: unicode(c.name), apiResult.characters) + + char.chars = json.dumps(charList) + return charList def apiFetch(self, charID, charName): - char = eos.db.getCharacter(charID) - char.defaultChar = charName - char.apiFetch(charName, proxy = service.settings.ProxySettings.getInstance().getProxySettings()) + dbChar = eos.db.getCharacter(charID) + dbChar.defaultChar = charName + + api = service.EVEAPIConnection() + auth = api.auth(keyID=dbChar.apiID, vCode=dbChar.apiKey) + apiResult = auth.account.Characters() + charID = None + for char in apiResult.characters: + if char.name == charName: + charID = char.characterID + + if charID == None: + return + + sheet = auth.character(charID).CharacterSheet() + + dbChar.apiUpdateCharSheet(sheet) eos.db.commit() def apiUpdateCharSheet(self, charID, sheet): diff --git a/service/eveapi.py b/service/eveapi.py index 087934ce5..4b19340b0 100644 --- a/service/eveapi.py +++ b/service/eveapi.py @@ -147,16 +147,27 @@ # #----------------------------------------------------------------------------- -import httplib + +#----------------------------------------------------------------------------- +# This eveapi has been modified for pyfa. +# +# Specifically, the entire network request/response has been substituted for +# pyfa's own implementation in service.network +# +# Additionally, various other parts have been changed to support urllib2 +# responses instead of httplib +#----------------------------------------------------------------------------- + + import urlparse -import urllib import copy -import warnings from xml.parsers import expat from time import strptime from calendar import timegm +import service + proxy = None proxySSL = False @@ -384,35 +395,11 @@ class _RootContext(_Context): response = None if response is None: - if not _useragent: - warnings.warn("No User-Agent set! Please use the set_user_agent() module-level function before accessing the EVE API.", stacklevel=3) + network = service.Network.getInstance() - if self._proxy is None: - req = path - if self._scheme == "https": - conn = httplib.HTTPSConnection(self._host) - else: - conn = httplib.HTTPConnection(self._host) - else: - req = self._scheme+'://'+self._host+path - if self._proxySSL: - conn = httplib.HTTPSConnection(*self._proxy) - else: - conn = httplib.HTTPConnection(*self._proxy) + req = self._scheme+'://'+self._host+path - if kw: - conn.request("POST", req, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": _useragent or _default_useragent}) - else: - conn.request("GET", req, "", {"User-Agent": _useragent or _default_useragent}) - - response = conn.getresponse() - if response.status != 200: - if response.status == httplib.NOT_FOUND: - raise AttributeError("'%s' not available on API server (404 Not Found)" % path) - elif response.status == httplib.FORBIDDEN: - raise AuthenticationError(response.status, 'HTTP 403 - Forbidden') - else: - raise ServerError(response.status, "'%s' request failed (%s)" % (path, response.reason)) + response = network.request(req, network.EVE, kw) if cache: store = True From 1279b20370df54cc2b06b77544f73f6eb2b0de85 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 14 Aug 2014 22:52:27 -0400 Subject: [PATCH 06/16] Some (bad) network error handling logic, borrowed from eveapi --- gui/characterEditor.py | 36 +++++++++++++++++++----------------- service/network.py | 40 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 8cf298f0d..071b784da 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -18,7 +18,7 @@ #=============================================================================== import wx -import bitmapLoader + import gui.mainFrame import wx.lib.newevent import wx.gizmos @@ -27,7 +27,6 @@ import service import gui.display as d from gui.contextMenu import ContextMenu from wx.lib.buttons import GenBitmapButton -import sys import gui.globalEvents as GE class CharacterEditor(wx.Frame): @@ -638,22 +637,25 @@ class APIView (wx.Panel): return sChar = service.Character.getInstance() - list = sChar.charList(self.Parent.Parent.getActiveCharacter(), self.inputID.GetLineText(0), self.inputKey.GetLineText(0)) + try: + list = sChar.charList(self.Parent.Parent.getActiveCharacter(), self.inputID.GetLineText(0), self.inputKey.GetLineText(0)) + except service.network.AuthenticationError, e: + self.stStatus.SetLabel("%s\nAuthentication failure. Please check keyID and vCode combination."%e) + except service.network.TimeoutError, e: + self.stStatus.SetLabel("Request timed out. Please check network connectivity and/or proxy settings.") + except Exception, e: + self.stStatus.SetLabel("Error:\n%s"%e) + else: + self.charChoice.Clear() + for charName in list: + i = self.charChoice.Append(charName) - if not list: - self.stStatus.SetLabel("Unable to fetch characters list from EVE API!") - return + self.btnFetchSkills.Enable(True) + self.charChoice.Enable(True) - self.charChoice.Clear() - for charName in list: - i = self.charChoice.Append(charName) + self.Layout() - self.btnFetchSkills.Enable(True) - self.charChoice.Enable(True) - - self.Layout() - - self.charChoice.SetSelection(0) + self.charChoice.SetSelection(0) def fetchSkills(self, event): charName = self.charChoice.GetString(self.charChoice.GetSelection()) @@ -662,5 +664,5 @@ class APIView (wx.Panel): sChar = service.Character.getInstance() sChar.apiFetch(self.Parent.Parent.getActiveCharacter(), charName) self.stStatus.SetLabel("Successfully fetched %s\'s skills from EVE API." % charName) - except: - self.stStatus.SetLabel("Unable to retrieve %s\'s skills!" % charName) + except Exception, e: + self.stStatus.SetLabel("Unable to retrieve %s\'s skills. Error message:\n%s" % (charName, e)) diff --git a/service/network.py b/service/network.py index c013ede7a..0a97ed6e9 100644 --- a/service/network.py +++ b/service/network.py @@ -21,6 +21,27 @@ from service.settings import NetworkSettings import urllib2 import urllib import config +import socket + +# network timeout, otherwise pyfa hangs for a long while if no internet connection +timeout = 3 +socket.setdefaulttimeout(timeout) + +class Error(StandardError): + def __init__(self, error): + self.error = error + +class RequestError(StandardError): + pass + +class AuthenticationError(StandardError): + pass + +class ServerError(StandardError): + pass + +class TimeoutError(StandardError): + pass class Network(): # Request constants - every request must supply this, as it is checked if @@ -62,6 +83,19 @@ class Network(): urllib2.install_opener(opener) request = urllib2.Request(url, headers=headers, data=urllib.urlencode(postData) if postData else None) - data = urllib2.urlopen(request) - print "\tReturning data" - return data + try: + data = urllib2.urlopen(request) + print "\tReturning data" + return data + except urllib2.HTTPError, error: + if error.code == 404: + raise RequestError(error) + elif error.code == 403: + raise AuthenticationError(error) + elif error.code >= 500: + raise ServerError(error) + except urllib2.URLError, error: + if "timed out" in error.reason: + raise TimeoutError(error) + else: + raise Error(error) From fa5edbb804ed1d3a53dcef0ec33764bef7482fd7 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 14 Aug 2014 22:52:55 -0400 Subject: [PATCH 07/16] Fix reference to old eveapi --- service/character.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/character.py b/service/character.py index f20beca10..e78ec990d 100644 --- a/service/character.py +++ b/service/character.py @@ -45,7 +45,7 @@ class CharacterImportThread(threading.Thread): sCharacter = Character.getInstance() for path in paths: with open(path, mode='r') as charFile: - sheet = eveapi.ParseXML(charFile) + sheet = service.ParseXML(charFile) charID = sCharacter.new() sCharacter.rename(charID, sheet.name+" (imported)") sCharacter.apiUpdateCharSheet(charID, sheet) @@ -65,7 +65,7 @@ class SkillBackupThread(threading.Thread): sCharacter = Character.getInstance() sFit = service.Fit.getInstance() fit = sFit.getFit(self.activeFit) - backupData = ""; + backupData = "" if self.saveFmt == "xml" or self.saveFmt == "emp": backupData = sCharacter.exportXml() else: From 0686b602c658414d7c5c1f44d33484c883ba28dc Mon Sep 17 00:00:00 2001 From: blitzmann Date: Fri, 15 Aug 2014 01:00:31 -0400 Subject: [PATCH 08/16] Adds network toggling to the preferences, as well as a few tweaks to network service --- .../pyfaNetworkPreferences.py | 74 ++++++++++++++++--- gui/characterEditor.py | 4 +- service/network.py | 18 ++--- service/settings.py | 13 ++++ 4 files changed, 86 insertions(+), 23 deletions(-) diff --git a/gui/builtinPreferenceViews/pyfaNetworkPreferences.py b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py index d929e74cf..6f936e22d 100644 --- a/gui/builtinPreferenceViews/pyfaNetworkPreferences.py +++ b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py @@ -12,14 +12,10 @@ class PFNetworkPref ( PreferenceView): def populatePanel( self, panel ): self.mainFrame = gui.mainFrame.MainFrame.getInstance() - self.networkSettings = service.settings.NetworkSettings.getInstance() + self.settings = service.settings.NetworkSettings.getInstance() + self.network = service.Network.getInstance() self.dirtySettings = False - self.nMode = self.networkSettings.getMode() - self.nAddr = self.networkSettings.getAddress() - self.nPort = self.networkSettings.getPort() - self.nType = self.networkSettings.getType() - mainSizer = wx.BoxSizer( wx.VERTICAL ) self.stTitle = wx.StaticText( panel, wx.ID_ANY, self.title, wx.DefaultPosition, wx.DefaultSize, 0 ) @@ -31,6 +27,42 @@ class PFNetworkPref ( PreferenceView): self.m_staticline1 = wx.StaticLine( panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) mainSizer.Add( self.m_staticline1, 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5 ) + self.cbEnableNetwork = wx.CheckBox( panel, wx.ID_ANY, u"Enable Network", wx.DefaultPosition, wx.DefaultSize, 0 ) + mainSizer.Add( self.cbEnableNetwork, 0, wx.ALL|wx.EXPAND, 5 ) + + subSizer = wx.BoxSizer( wx.VERTICAL ) + self.cbEve = wx.CheckBox( panel, wx.ID_ANY, u"EVE Servers (API && CREST import)", wx.DefaultPosition, wx.DefaultSize, 0 ) + subSizer.Add( self.cbEve, 0, wx.ALL|wx.EXPAND, 5 ) + + self.cbPricing = wx.CheckBox( panel, wx.ID_ANY, u"Pricing updates", wx.DefaultPosition, wx.DefaultSize, 0 ) + subSizer.Add( self.cbPricing, 0, wx.ALL|wx.EXPAND, 5 ) + + self.cbPyfaUpdate = wx.CheckBox( panel, wx.ID_ANY, u"Pyfa Update checks", wx.DefaultPosition, wx.DefaultSize, 0 ) + subSizer.Add( self.cbPyfaUpdate, 0, wx.ALL|wx.EXPAND, 5 ) + + mainSizer.Add( subSizer, 0, wx.LEFT|wx.EXPAND, 30 ) + + self.cbEnableNetwork.SetValue(self.settings.isEnabled(self.network.ENABLED)) + self.cbEve.SetValue(self.settings.isEnabled(self.network.EVE)) + self.cbPricing.SetValue(self.settings.isEnabled(self.network.PRICES)) + self.cbPyfaUpdate.SetValue(self.settings.isEnabled(self.network.UPDATE)) + + self.cbEnableNetwork.Bind(wx.EVT_CHECKBOX, self.OnCBEnableChange) + self.cbEve.Bind(wx.EVT_CHECKBOX, self.OnCBEveChange) + self.cbPricing.Bind(wx.EVT_CHECKBOX, self.OnCBPricingChange) + self.cbPyfaUpdate.Bind(wx.EVT_CHECKBOX, self.OnCBUpdateChange) + + self.toggleNetworks(self.cbEnableNetwork.GetValue()) + + #--------------- + # Proxy + #--------------- + + self.nMode = self.settings.getMode() + self.nAddr = self.settings.getAddress() + self.nPort = self.settings.getPort() + self.nType = self.settings.getType() + ptypeSizer = wx.BoxSizer( wx.HORIZONTAL ) self.stPType = wx.StaticText( panel, wx.ID_ANY, u"Mode:", wx.DefaultPosition, wx.DefaultSize, 0 ) @@ -79,13 +111,13 @@ class PFNetworkPref ( PreferenceView): btnSizer = wx.BoxSizer( wx.HORIZONTAL ) btnSizer.AddSpacer( ( 0, 0), 1, wx.EXPAND, 5 ) - self.btnApply = wx.Button( panel, wx.ID_ANY, u"Apply", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.btnApply = wx.Button( panel, wx.ID_ANY, u"Apply Proxy Settings", wx.DefaultPosition, wx.DefaultSize, 0 ) btnSizer.Add( self.btnApply, 0, wx.ALL, 5 ) mainSizer.Add(btnSizer, 0, wx.EXPAND,5) - proxy = self.networkSettings.autodetect() + proxy = self.settings.autodetect() if proxy is not None: addr,port = proxy @@ -113,6 +145,24 @@ class PFNetworkPref ( PreferenceView): panel.SetSizer( mainSizer ) panel.Layout() + def toggleNetworks(self, toggle): + self.cbEve.Enable(toggle) + self.cbPricing.Enable(toggle) + self.cbPyfaUpdate.Enable(toggle) + + def OnCBEnableChange(self, event): + self.settings.toggleAccess(self.network.ENABLED, self.cbEnableNetwork.GetValue()) + self.toggleNetworks(self.cbEnableNetwork.GetValue()) + + def OnCBUpdateChange(self, event): + self.settings.toggleAccess(self.network.UPDATE, self.cbPyfaUpdate.GetValue()) + + def OnCBPricingChange(self, event): + self.settings.toggleAccess(self.network.PRICES, self.cbPricing.GetValue()) + + def OnCBEveChange(self, event): + self.settings.toggleAccess(self.network.EVE, self.cbEve.GetValue()) + def OnEditPSAddrText(self, event): self.nAddr = self.editProxySettingsAddr.GetValue() self.dirtySettings = True @@ -129,10 +179,10 @@ class PFNetworkPref ( PreferenceView): self.SaveSettings() def SaveSettings(self): - self.networkSettings.setMode(self.nMode) - self.networkSettings.setAddress(self.nAddr) - self.networkSettings.setPort(self.nPort) - self.networkSettings.setType(self.nType) + self.settings.setMode(self.nMode) + self.settings.setAddress(self.nAddr) + self.settings.setPort(self.nPort) + self.settings.setType(self.nType) def UpdateApplyButtonState(self): if self.dirtySettings: diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 071b784da..be77c8d21 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -640,11 +640,11 @@ class APIView (wx.Panel): try: list = sChar.charList(self.Parent.Parent.getActiveCharacter(), self.inputID.GetLineText(0), self.inputKey.GetLineText(0)) except service.network.AuthenticationError, e: - self.stStatus.SetLabel("%s\nAuthentication failure. Please check keyID and vCode combination."%e) + self.stStatus.SetLabel("Authentication failure. Please check keyID and vCode combination.") except service.network.TimeoutError, e: self.stStatus.SetLabel("Request timed out. Please check network connectivity and/or proxy settings.") except Exception, e: - self.stStatus.SetLabel("Error:\n%s"%e) + self.stStatus.SetLabel("Error:\n%s"%e.message) else: self.charChoice.Clear() for charName in list: diff --git a/service/network.py b/service/network.py index 0a97ed6e9..2cc22d4e4 100644 --- a/service/network.py +++ b/service/network.py @@ -28,8 +28,8 @@ timeout = 3 socket.setdefaulttimeout(timeout) class Error(StandardError): - def __init__(self, error): - self.error = error + def __init__(self, msg=None): + self.message = msg class RequestError(StandardError): pass @@ -59,7 +59,7 @@ class Network(): return cls._instance - def request(self, url, type=None, postData=None): + def request(self, url, type, postData=None): # URL is required to be https as of right now print "Starting request: %s\n\tType: %s\n\tPost Data: %s"%(url,type,postData) @@ -67,9 +67,9 @@ class Network(): # Make sure request is enabled access = NetworkSettings.getInstance().getAccess() - if not type or not type & access: # @todo: check if enabled + if not self.ENABLED & access or not type & access: print "\tType not enabled" - return # @todo: throw exception + raise Error("Access not enabled - please enable in Preferences > Network") # Set up some things for the request versionString = "{0} {1} - {2} {3}".format(config.version, config.tag, config.expansionName, config.expansionVersion) @@ -89,13 +89,13 @@ class Network(): return data except urllib2.HTTPError, error: if error.code == 404: - raise RequestError(error) + raise RequestError() elif error.code == 403: - raise AuthenticationError(error) + raise AuthenticationError() elif error.code >= 500: - raise ServerError(error) + raise ServerError() except urllib2.URLError, error: if "timed out" in error.reason: - raise TimeoutError(error) + raise TimeoutError() else: raise Error(error) diff --git a/service/settings.py b/service/settings.py index f64caa5d1..51ab815f7 100644 --- a/service/settings.py +++ b/service/settings.py @@ -130,6 +130,19 @@ class NetworkSettings(): self.serviceNetworkSettings = SettingsProvider.getInstance().getSettings("pyfaServiceNetworkSettings", serviceNetworkDefaultSettings) + def isEnabled(self, type): + if type & self.serviceNetworkSettings["access"]: + return True + return False + + def toggleAccess(self, type, toggle=True): + bitfield = self.serviceNetworkSettings["access"] + + if toggle: # Turn bit on + self.serviceNetworkSettings["access"] = type | bitfield + else: # Turn bit off + self.serviceNetworkSettings["access"] = ~type & bitfield + def getMode(self): return self.serviceNetworkSettings["mode"] From 0881abae7b9ff577dc486728d85b5a621cf2b10a Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 17 Aug 2014 21:47:07 -0400 Subject: [PATCH 09/16] Moves price fetching to new service as well as removes old and defunct c0rporation price source (along with much of the price fetching logic used to support multiple sources) --- eos/saveddata/price.py | 317 +---------------------------------------- service/__init__.py | 1 + service/market.py | 6 +- service/network.py | 6 +- service/price.py | 116 +++++++++++++++ 5 files changed, 126 insertions(+), 320 deletions(-) create mode 100644 service/price.py diff --git a/eos/saveddata/price.py b/eos/saveddata/price.py index 5f89f260e..b3c7d8151 100644 --- a/eos/saveddata/price.py +++ b/eos/saveddata/price.py @@ -19,18 +19,9 @@ #=============================================================================== import time -import urllib2 -from xml.dom import minidom - from sqlalchemy.orm import reconstructor -import eos.db - class Price(object): - # Price validity period, 24 hours - VALIDITY = 24*60*60 - # Re-request delay for failed fetches, 4 hours - REREQUEST = 4*60*60 def __init__(self, typeID): self.typeID = typeID @@ -42,307 +33,7 @@ class Price(object): def init(self): self.__item = None - def isValid(self, rqtime=time.time()): - updateAge = rqtime - self.time - # Mark price as invalid if it is expired - validity = updateAge <= self.VALIDITY - # Price is considered as valid, if it's expired but we had failed - # fetch attempt recently - if validity is False and self.failed is not None: - failedAge = rqtime - self.failed - validity = failedAge <= self.REREQUEST - # If it's already invalid, it can't get any better - if validity is False: - return validity - # If failed timestamp refers to future relatively to current - # system clock, mark price as invalid - if self.failed > rqtime: - return False - # Do the same for last updated timestamp - if self.time > rqtime: - return False - return validity - - @classmethod - def fetchPrices(cls, prices, proxy=None): - """Fetch all prices passed to this method""" - # Set time of the request - # We have to pass this time to all of our used methods and validity checks - # Using time of check instead can make extremely rare edge-case bugs to appear - # (e.g. when item price is already considered as outdated, but c0rp fetch is still - # valid, just because their update time has been set using slightly older timestamp) - rqtime = time.time() - # Dictionary for our price objects - priceMap = {} - # Check all provided price objects, and add invalid ones to dictionary - for price in prices: - if not price.isValid(rqtime=rqtime): - priceMap[price.typeID] = price - # List our price service methods - services = ((cls.fetchEveCentral, (priceMap,), {"rqtime": rqtime, "proxy": proxy}), - (cls.fetchC0rporation, (priceMap,), {"rqtime": rqtime, "proxy": proxy})) - # Cycle through services - for svc, args, kwargs in services: - # Stop cycling if we don't need price data anymore - if len(priceMap) == 0: - break - # Request prices and get some feedback - noData, abortedData = svc(*args, **kwargs) - # Mark items with some failure occurred during fetching - for typeID in abortedData: - priceMap[typeID].failed = rqtime - # Clear map from the fetched and failed items, leaving only items - # for which we've got no data - toRemove = set() - for typeID in priceMap: - if typeID not in noData: - toRemove.add(typeID) - for typeID in toRemove: - del priceMap[typeID] - # After we've checked all possible services, assign zero price for items - # which were not found on any service to avoid re-fetches during validity - # period - for typeID in priceMap: - priceobj = priceMap[typeID] - priceobj.price = 0 - priceobj.time = rqtime - priceobj.failed = None - - @classmethod - def fetchEveCentral(cls, priceMap, rqtime=time.time(), proxy=None): - """Use Eve-Central price service provider""" - # This set will contain typeIDs which were requested but no data has been fetched for them - noData = set() - # This set will contain items for which data fetch was aborted due to technical reasons - abortedData = set() - # Set of items which are still to be requested from this service - toRequestSvc = set() - # Compose list of items we're going to request - for typeID in priceMap: - # Get item object - item = eos.db.getItem(typeID) - # We're not going to request items only with market group, as eve-central - # doesn't provide any data for items not on the market - # Items w/o market group will be added to noData in the very end - if item.marketGroupID: - toRequestSvc.add(typeID) - # Do not waste our time if all items are not on the market - if len(toRequestSvc) == 0: - noData.update(priceMap.iterkeys()) - return (noData, abortedData) - # This set will contain typeIDs for which we've got useful data - fetchedTypeIDs = set() - # Base request URL - baseurl = "http://api.eve-central.com/api/marketstat" - # Area limitation list - areas = ("usesystem=30000142", # Jita - None) # Global - # Fetch prices from Jita market, if no data was available - check global data - for area in areas: - # Append area limitations to base URL - areaurl = "{0}&{1}".format(baseurl, area) if area else baseurl - # Set which contains IDs of items which we will fetch for given area - toRequestArea = toRequestSvc.difference(fetchedTypeIDs).difference(abortedData) - # As length of URL is limited, make a loop to make sure we request all data - while(len(toRequestArea) > 0): - # Set of items we're requesting during this cycle - requestedThisUrl = set() - # Always start composing our URL from area-limited URL - requrl = areaurl - # Generate final URL, making sure it isn't longer than 255 characters - for typeID in toRequestArea: - # Try to add new typeID argument - newrequrl = "{0}&typeid={1}".format(requrl, typeID) - # If we didn't exceed our limits - if len(newrequrl) <= 255: - # Accept new URL - requrl = newrequrl - # Fill the set for the utility needs - requestedThisUrl.add(typeID) - # Use previously generated URL if new is out of bounds - else: - break - # Do not request same items from the same area - toRequestArea.difference_update(requestedThisUrl) - # Replace first ampersand with question mark to separate arguments - # from URL itself - requrl = requrl.replace("&", "?", 1) - # Make the request object - request = urllib2.Request(requrl, headers={"User-Agent" : "eos"}) - # Attempt to send request and process it - try: - if proxy is not None: - proxyHandler = urllib2.ProxyHandler({"http": proxy}) - opener = urllib2.build_opener(proxyHandler) - urllib2.install_opener(opener) - data = urllib2.urlopen(request) - xml = minidom.parse(data) - types = xml.getElementsByTagName("marketstat").item(0).getElementsByTagName("type") - # Cycle through all types we've got from request - for type in types: - # Get data out of each typeID details tree - typeID = int(type.getAttribute("id")) - sell = type.getElementsByTagName("sell").item(0) - # If price data wasn't there, set price to zero - try: - percprice = float(sell.getElementsByTagName("percentile").item(0).firstChild.data) - except (TypeError, ValueError): - percprice = 0 - # Eve-central returns zero price if there was no data, thus modify price - # object only if we've got non-zero price - if percprice: - # Add item id to list of fetched items - fetchedTypeIDs.add(typeID) - # Fill price data - priceobj = priceMap[typeID] - priceobj.price = percprice - priceobj.time = rqtime - priceobj.failed = None - # If getting or processing data returned any errors - except: - # Consider fetch as aborted - abortedData.update(requestedThisUrl) - # Get actual list of items for which we didn't get data; it includes all requested items - # (even those which didn't pass filter), excluding items which had problems during fetching - # and items for which we've successfully fetched price - noData.update(set(priceMap.iterkeys()).difference(fetchedTypeIDs).difference(abortedData)) - # And return it for future use - return (noData, abortedData) - - @classmethod - def fetchC0rporation(cls, priceMap, rqtime=time.time(), proxy=None): - """Use c0rporation.com price service provider""" - # it must be here, otherwise eos doesn't load miscData in time - from eos.types import MiscData - # Set-container for requested items w/o any data returned - noData = set() - # Container for items which had errors during fetching - abortedData = set() - # Set with types for which we've got data - fetchedTypeIDs = set() - # Container for prices we'll re-request from eve central. - eveCentralUpdate = {} - # Check when we updated prices last time - fieldName = "priceC0rpTime" - lastUpdatedField = eos.db.getMiscData(fieldName) - # If this field isn't available, create and add it to session - if lastUpdatedField is None: - lastUpdatedField = MiscData(fieldName) - eos.db.add(lastUpdatedField) - # Convert field value to float, assigning it zero on any errors - try: - lastUpdated = float(lastUpdatedField.fieldValue) - except (TypeError, ValueError): - lastUpdated = 0 - # Get age of price - updateAge = rqtime - lastUpdated - # Using timestamp we've got, check if fetch results are still valid and make - # sure system clock hasn't been changed to past - c0rpValidityUpd = updateAge <= cls.VALIDITY and lastUpdated <= rqtime - # If prices should be valid according to miscdata last update timestamp, - # but method was requested to provide prices for some items, we can - # safely assume that these items are not on the XML (to be more accurate, - # on its previously fetched version), because all items which are valid - # (and they are valid only when xml is valid) should be filtered out before - # passing them to this method - if c0rpValidityUpd is True: - noData.update(set(priceMap.iterkeys())) - return (noData, abortedData) - # Check when price fetching failed last time - fieldName = "priceC0rpFailed" - # If it doesn't exist, add this one to the session too - lastFailedField = eos.db.getMiscData(fieldName) - if lastFailedField is None: - lastFailedField = MiscData(fieldName) - eos.db.add(lastFailedField) - # Convert field value to float, assigning it none on any errors - try: - lastFailed = float(lastFailedField.fieldValue) - except (TypeError, ValueError): - lastFailed = None - # If we had failed fetch attempt at some point - if lastFailed is not None: - failedAge = rqtime - lastFailed - # Check if we should refetch data now or not (we do not want to do anything until - # refetch timeout is reached or we have failed timestamp referencing some future time) - c0rpValidityFail = failedAge <= cls.REREQUEST and lastFailed <= rqtime - # If it seems we're not willing to fetch any data - if c0rpValidityFail is True: - # Consider all requested items as aborted. As we don't store list of items - # provided by this service, this will include anything passed to this service, - # even items which are usually not included in xml - abortedData.update(set(priceMap.iterkeys())) - return (noData, abortedData) - # Our request URL - requrl = "http://prices.c0rporation.com/faction.xml" - # Generate request - request = urllib2.Request(requrl, headers={"User-Agent" : "eos"}) - # Attempt to send request and process returned data - try: - if proxy is not None: - proxyHandler = urllib2.ProxyHandler({"http": proxy}) - opener = urllib2.build_opener(proxyHandler) - urllib2.install_opener(opener) - data = urllib2.urlopen(request) - # Parse the data we've got - xml = minidom.parse(data) - rowsets = xml.getElementsByTagName("rowset") - for rowset in rowsets: - rows = rowset.getElementsByTagName("row") - # Go through all given data rows; as we don't want to request and process whole xml - # for each price request, we need to process it in one single run - for row in rows: - typeID = int(row.getAttribute("typeID")) - # Median price field may be absent or empty, assign 0 in this case - try: - medprice = float(row.getAttribute("median")) - except (TypeError, ValueError): - medprice = 0 - # Process price only if it's non-zero - if medprice: - # Add current typeID to the set of fetched types - fetchedTypeIDs.add(typeID) - # If we have given typeID in the map we've got, pull price object out of it - if typeID in priceMap: - priceobj = priceMap[typeID] - # If we don't, request it from database - else: - priceobj = eos.db.getPrice(typeID) - # If everything failed - if priceobj is None: - # Create price object ourselves - priceobj = Price(typeID) - # And let database know that we'd like to keep it - eos.db.add(priceobj) - # Finally, fill object with data - priceobj.price = medprice - priceobj.time = rqtime - priceobj.failed = None - # Check if item has market group assigned - item = eos.db.getItem(typeID) - if item is not None and item.marketGroupID: - eveCentralUpdate[typeID] = priceobj - # If any items need to be re-requested from EVE-Central, do so - # We need to do this because c0rp returns prices for lot of items; - # if returned price is one of requested, it's fetched from eve-central - # first, which is okay; if it's not, price from c0rp will be written - # which will prevent further updates from eve-central. As we consider - # eve-central as more accurate source, ask to update prices for all - # items we got - if eveCentralUpdate: - # We do not need any feedback from it, we just want it to update - # prices - cls.fetchEveCentral(eveCentralUpdate, rqtime=rqtime, proxy=proxy) - # Save current time for the future use - lastUpdatedField.fieldValue = rqtime - # Clear the last failed field - lastFailedField.fieldValue = None - # Find which items were requested but no data has been returned - noData.update(set(priceMap.iterkeys()).difference(fetchedTypeIDs)) - # If we failed somewhere during fetching or processing - except: - # Consider all items as aborted - abortedData.update(set(priceMap.iterkeys())) - # And whole fetch too - lastFailedField.fieldValue = rqtime - return (noData, abortedData) + @property + def isValid(self): + print self.time, time.time(), self.time >= time.time() + return self.time >= time.time() diff --git a/service/__init__.py b/service/__init__.py index b257d5100..8ded8a66a 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -6,5 +6,6 @@ from service.damagePattern import DamagePattern from service.settings import SettingsProvider from service.fleet import Fleet from service.update import Update +from service.price import Price from service.network import Network from service.eveapi import EVEAPIConnection, ParseXML diff --git a/service/market.py b/service/market.py index 397e3e89d..79495478b 100644 --- a/service/market.py +++ b/service/market.py @@ -26,6 +26,7 @@ import Queue import eos.db import eos.types from service.settings import SettingsProvider, NetworkSettings +import service try: from collections import OrderedDict @@ -78,10 +79,7 @@ class PriceWorkerThread(threading.Thread): # Grab prices, this is the time-consuming part if len(requests) > 0: - proxy = NetworkSettings.getInstance().getProxySettings() - if proxy is not None: - proxy = "{0}:{1}".format(*proxy) - eos.types.Price.fetchPrices(requests, proxy=proxy) + service.Price.fetchPrices(requests) wx.CallAfter(callback) queue.task_done() diff --git a/service/network.py b/service/network.py index 2cc22d4e4..8d8a62375 100644 --- a/service/network.py +++ b/service/network.py @@ -59,10 +59,10 @@ class Network(): return cls._instance - def request(self, url, type, postData=None): + def request(self, url, type, data=None): # URL is required to be https as of right now - print "Starting request: %s\n\tType: %s\n\tPost Data: %s"%(url,type,postData) + print "Starting request: %s\n\tType: %s\n\tPost Data: %s"%(url,type,data) # Make sure request is enabled access = NetworkSettings.getInstance().getAccess() @@ -82,7 +82,7 @@ class Network(): opener = urllib2.build_opener(proxy) urllib2.install_opener(opener) - request = urllib2.Request(url, headers=headers, data=urllib.urlencode(postData) if postData else None) + request = urllib2.Request(url, headers=headers, data=urllib.urlencode(data) if data else None) try: data = urllib2.urlopen(request) print "\tReturning data" diff --git a/service/price.py b/service/price.py new file mode 100644 index 000000000..eb1600c4b --- /dev/null +++ b/service/price.py @@ -0,0 +1,116 @@ +#=============================================================================== +# Copyright (C) 2010 Diego Duclos +# +# This file is part of pyfa. +# +# pyfa is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pyfa is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyfa. If not, see . +#=============================================================================== + +import service +import eos.db +import eos.types +import time +from xml.dom import minidom + +VALIDITY = 24*60*60 # Price validity period, 24 hours +REREQUEST = 4*60*60 # Re-request delay for failed fetches, 4 hours +TIMEOUT = 15*60 # Network timeout delay for connection issues, 15 minutes + +class Price(): + + @classmethod + def fetchPrices(cls, prices): + """Fetch all prices passed to this method""" + print "Fetch time: %s"%time.time() + # Dictionary for our price objects + priceMap = {} + # Check all provided price objects, and add invalid ones to dictionary + for price in prices: + if not price.isValid: + priceMap[price.typeID] = price + + if len(priceMap) == 0: + print "No price updates" + return + # Set of items which are still to be requested from this service + toRequest = set() + + # Compose list of items we're going to request + for typeID in priceMap: + # Get item object + item = eos.db.getItem(typeID) + # We're not going to request items only with market group, as eve-central + # doesn't provide any data for items not on the market + if item.marketGroupID: + toRequest.add(typeID) + + # Do not waste our time if all items are not on the market + if len(toRequest) == 0: + return + + # This will store POST data for eve-central + data = [] + + # Base request URL + baseurl = "https://api.eve-central.com/api/marketstat" + data.append(("usesystem", 30000142)) # Use Jita for market + + for typeID in toRequest: # Add all typeID arguments + data.append(("typeid", typeID)) + + # Attempt to send request and process it + try: + len(priceMap) + network = service.Network.getInstance() + data = network.request(baseurl, network.PRICES, data) + xml = minidom.parse(data) + types = xml.getElementsByTagName("marketstat").item(0).getElementsByTagName("type") + # Cycle through all types we've got from request + for type in types: + # Get data out of each typeID details tree + typeID = int(type.getAttribute("id")) + sell = type.getElementsByTagName("sell").item(0) + # If price data wasn't there, set price to zero + try: + percprice = float(sell.getElementsByTagName("percentile").item(0).firstChild.data) + except (TypeError, ValueError): + percprice = 0 + + # Fill price data + priceobj = priceMap[typeID] + priceobj.price = percprice + priceobj.time = time.time() + VALIDITY + priceobj.failed = None + + # delete price from working dict + del priceMap[typeID] + + # If getting or processing data returned any errors + except service.network.TimeoutError, e: + # Timeout error deserves special treatment + for typeID in priceMap.keys(): + priceobj = priceMap[typeID] + priceobj.time = time.time() + TIMEOUT + priceobj.failed = None + del priceMap[typeID] + except: + # all other errors will pass and continue onward to the REREQUEST delay + pass + + # if we get to this point, then we've got an error. Set to REREQUEST delay + for typeID in priceMap.keys(): + priceobj = priceMap[typeID] + priceobj.price = 0 + priceobj.time = time.time() + REREQUEST + priceobj.failed = None From 8928d394c028653b249f29c681e81ad1f9487fef Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 17 Aug 2014 23:27:07 -0400 Subject: [PATCH 10/16] Updates "update" prefs to reflect new changes --- .../pyfaNetworkPreferences.py | 7 +++++ .../pyfaUpdatePreferences.py | 30 ++++--------------- service/settings.py | 7 ++--- service/update.py | 7 +---- 4 files changed, 17 insertions(+), 34 deletions(-) diff --git a/gui/builtinPreferenceViews/pyfaNetworkPreferences.py b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py index 6f936e22d..059a505de 100644 --- a/gui/builtinPreferenceViews/pyfaNetworkPreferences.py +++ b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py @@ -42,6 +42,13 @@ class PFNetworkPref ( PreferenceView): mainSizer.Add( subSizer, 0, wx.LEFT|wx.EXPAND, 30 ) + proxyTitle = wx.StaticText( panel, wx.ID_ANY, "Proxy settings", wx.DefaultPosition, wx.DefaultSize, 0 ) + proxyTitle.Wrap( -1 ) + proxyTitle.SetFont( wx.Font( 12, 70, 90, 90, False, wx.EmptyString ) ) + + mainSizer.Add( proxyTitle, 0, wx.ALL, 5 ) + mainSizer.Add( wx.StaticLine( panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ), 0, wx.EXPAND, 5 ) + self.cbEnableNetwork.SetValue(self.settings.isEnabled(self.network.ENABLED)) self.cbEve.SetValue(self.settings.isEnabled(self.network.EVE)) self.cbPricing.SetValue(self.settings.isEnabled(self.network.PRICES)) diff --git a/gui/builtinPreferenceViews/pyfaUpdatePreferences.py b/gui/builtinPreferenceViews/pyfaUpdatePreferences.py index 94b49b759..f76542b39 100644 --- a/gui/builtinPreferenceViews/pyfaUpdatePreferences.py +++ b/gui/builtinPreferenceViews/pyfaUpdatePreferences.py @@ -12,8 +12,9 @@ import gui.globalEvents as GE class PFUpdatePref (PreferenceView): title = "Updates" desc = "Pyfa can automatically check and notify you of new releases. "+\ - "These options will allow you to choose what kind of updates, "+\ - "if any, you wish to receive notifications for." + "This feature is toggled in the Network settings. "+\ + "Here, you may allow pre-release notifications and view "+\ + "suppressed release notifications, if any." def populatePanel( self, panel ): self.UpdateSettings = service.settings.UpdateSettings.getInstance() @@ -35,18 +36,12 @@ class PFUpdatePref (PreferenceView): self.stDesc.Wrap(dlgWidth - 50) mainSizer.Add( self.stDesc, 0, wx.ALL, 5 ) - self.suppressAll = wx.CheckBox( panel, wx.ID_ANY, u"Don't check for updates", wx.DefaultPosition, wx.DefaultSize, 0 ) self.suppressPrerelease = wx.CheckBox( panel, wx.ID_ANY, u"Allow pre-release notifications", wx.DefaultPosition, wx.DefaultSize, 0 ) - - mainSizer.Add( self.suppressAll, 0, wx.ALL|wx.EXPAND, 5 ) - mainSizer.Add( self.suppressPrerelease, 0, wx.ALL|wx.EXPAND, 5 ) - - self.suppressAll.Bind(wx.EVT_CHECKBOX, self.OnSuppressAllStateChange) self.suppressPrerelease.Bind(wx.EVT_CHECKBOX, self.OnPrereleaseStateChange) - - self.suppressAll.SetValue(self.UpdateSettings.get('all')) self.suppressPrerelease.SetValue(not self.UpdateSettings.get('prerelease')) + mainSizer.Add( self.suppressPrerelease, 0, wx.ALL|wx.EXPAND, 5 ) + if (self.UpdateSettings.get('version')): self.versionSizer = wx.BoxSizer( wx.VERTICAL ) @@ -83,22 +78,9 @@ class PFUpdatePref (PreferenceView): self.versionSizer.Add( actionSizer, 0, wx.EXPAND, 5 ) mainSizer.Add( self.versionSizer, 0, wx.EXPAND, 5 ) - self.ToggleSuppressAll(self.suppressAll.IsChecked()) - panel.SetSizer( mainSizer ) panel.Layout() - def ToggleSuppressAll(self, bool): - ''' Toggles other inputs on/off depending on value of SuppressAll ''' - if bool: - self.suppressPrerelease.Disable() - else: - self.suppressPrerelease.Enable() - - def OnSuppressAllStateChange(self, event): - self.UpdateSettings.set('all', self.suppressAll.IsChecked()) - self.ToggleSuppressAll(self.suppressAll.IsChecked()) - def OnPrereleaseStateChange(self, event): self.UpdateSettings.set('prerelease', not self.suppressPrerelease.IsChecked()) @@ -119,4 +101,4 @@ class PFUpdatePref (PreferenceView): def getImage(self): return bitmapLoader.getBitmap("prefs_update", "icons") -PFUpdatePref.register() \ No newline at end of file +PFUpdatePref.register() diff --git a/service/settings.py b/service/settings.py index 51ab815f7..dde48f71d 100644 --- a/service/settings.py +++ b/service/settings.py @@ -251,10 +251,10 @@ class UpdateSettings(): def __init__(self): # Settings - # all - If True, suppress all update notifications - # prerelease - If True, suppress only prerelease notifications + # Updates are completely suppressed via network settings + # prerelease - If True, suppress prerelease notifications # version - Set to release tag that user does not want notifications for - serviceUpdateDefaultSettings = { "all": False, "prerelease": True, 'version': None } + serviceUpdateDefaultSettings = {"prerelease": True, 'version': None } self.serviceUpdateSettings = SettingsProvider.getInstance().getSettings("pyfaServiceUpdateSettings", serviceUpdateDefaultSettings) def get(self, type): @@ -263,5 +263,4 @@ class UpdateSettings(): def set(self, type, value): self.serviceUpdateSettings[type] = value -# @todo: "reopen fits" setting class # @todo: migrate fit settings (from fit service) here? diff --git a/service/update.py b/service/update.py index 84be89679..14a528b42 100644 --- a/service/update.py +++ b/service/update.py @@ -32,14 +32,9 @@ class CheckUpdateThread(threading.Thread): self.network = service.Network.getInstance() def run(self): - # Suppress all - if (self.settings.get('all')): - return - network = service.Network.getInstance() try: - # @todo: use proxy settings? response = network.request('https://api.github.com/repos/DarkFenX/Pyfa/releases', network.UPDATE) jsonResponse = json.loads(response.read()) @@ -73,7 +68,7 @@ class CheckUpdateThread(threading.Thread): if release['prerelease'] and rVersion > config.expansionVersion: wx.CallAfter(self.callback, release) # Singularity -> Singularity break - except: # for when there is no internet connection + except: pass def versiontuple(self, v): From e8041470c84975f121b36ac899f01ef31cf3cb34 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 17 Aug 2014 23:40:22 -0400 Subject: [PATCH 11/16] Remove / disable debugging prints --- eos/saveddata/price.py | 1 - gui/utils/exportHtml.py | 8 ++++---- service/network.py | 9 ++------- service/price.py | 4 ++-- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/eos/saveddata/price.py b/eos/saveddata/price.py index b3c7d8151..4c7de879c 100644 --- a/eos/saveddata/price.py +++ b/eos/saveddata/price.py @@ -35,5 +35,4 @@ class Price(object): @property def isValid(self): - print self.time, time.time(), self.time >= time.time() return self.time >= time.time() diff --git a/gui/utils/exportHtml.py b/gui/utils/exportHtml.py index 2dcc0be32..c61c2444a 100644 --- a/gui/utils/exportHtml.py +++ b/gui/utils/exportHtml.py @@ -35,9 +35,9 @@ class exportHtmlThread(threading.Thread): def run(self): # wait 1 second just in case a lot of modifications get made - time.sleep(1); + time.sleep(1) if self.stopRunning: - return; + return sMkt = service.Market.getInstance() sFit = service.Fit.getInstance() @@ -190,8 +190,8 @@ class exportHtmlThread(threading.Thread): try: FILE = open(settings.getPath(), "w") - FILE.write(HTML.encode('utf-8')); - FILE.close(); + FILE.write(HTML.encode('utf-8')) + FILE.close() except IOError: print "Failed to write to " + settings.getPath() pass diff --git a/service/network.py b/service/network.py index 8d8a62375..04ba49875 100644 --- a/service/network.py +++ b/service/network.py @@ -60,15 +60,13 @@ class Network(): return cls._instance def request(self, url, type, data=None): - # URL is required to be https as of right now - print "Starting request: %s\n\tType: %s\n\tPost Data: %s"%(url,type,data) + #print "Starting request: %s\n\tType: %s\n\tPost Data: %s"%(url,type,data) # Make sure request is enabled access = NetworkSettings.getInstance().getAccess() if not self.ENABLED & access or not type & access: - print "\tType not enabled" raise Error("Access not enabled - please enable in Preferences > Network") # Set up some things for the request @@ -77,16 +75,13 @@ class Network(): proxy = NetworkSettings.getInstance().getProxySettings() if proxy is not None: - print "\tUsing a proxy" proxy = urllib2.ProxyHandler({'https': "{0}:{1}".format(*proxy)}) opener = urllib2.build_opener(proxy) urllib2.install_opener(opener) request = urllib2.Request(url, headers=headers, data=urllib.urlencode(data) if data else None) try: - data = urllib2.urlopen(request) - print "\tReturning data" - return data + return urllib2.urlopen(request) except urllib2.HTTPError, error: if error.code == 404: raise RequestError() diff --git a/service/price.py b/service/price.py index eb1600c4b..fafdf3297 100644 --- a/service/price.py +++ b/service/price.py @@ -32,7 +32,7 @@ class Price(): @classmethod def fetchPrices(cls, prices): """Fetch all prices passed to this method""" - print "Fetch time: %s"%time.time() + # Dictionary for our price objects priceMap = {} # Check all provided price objects, and add invalid ones to dictionary @@ -41,8 +41,8 @@ class Price(): priceMap[price.typeID] = price if len(priceMap) == 0: - print "No price updates" return + # Set of items which are still to be requested from this service toRequest = set() From 84ac71f528e7617aefcb583e7e1b2764afca10fc Mon Sep 17 00:00:00 2001 From: blitzmann Date: Mon, 18 Aug 2014 01:10:49 -0400 Subject: [PATCH 12/16] Fixed bugs introduced from code consistency fix --- gui/builtinContextMenus/droneRemoveStack.py | 6 +++--- gui/builtinContextMenus/itemRemove.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/builtinContextMenus/droneRemoveStack.py b/gui/builtinContextMenus/droneRemoveStack.py index e465f4b2d..c3063ca52 100644 --- a/gui/builtinContextMenus/droneRemoveStack.py +++ b/gui/builtinContextMenus/droneRemoveStack.py @@ -18,10 +18,10 @@ class ItemRemove(ContextMenu): srcContext = fullContext[0] sFit = service.Fit.getInstance() fitID = self.mainFrame.getActiveFit() - sFit = sFit.getFit(fitID) + fit = sFit.getFit(fitID) - idx = sFit.drones.index(selection[0]) - sFit.removeDrone(fitID, idx, numDronesToRemove=sFit.drones[idx].amount) + idx = fit.drones.index(selection[0]) + sFit.removeDrone(fitID, idx, numDronesToRemove=fit.drones[idx].amount) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) diff --git a/gui/builtinContextMenus/itemRemove.py b/gui/builtinContextMenus/itemRemove.py index c291a1d37..a54c415b7 100644 --- a/gui/builtinContextMenus/itemRemove.py +++ b/gui/builtinContextMenus/itemRemove.py @@ -19,20 +19,20 @@ class ItemRemove(ContextMenu): srcContext = fullContext[0] sFit = service.Fit.getInstance() fitID = self.mainFrame.getActiveFit() - sFit = sFit.getFit(fitID) + fit = sFit.getFit(fitID) if srcContext == "fittingModule": for module in selection: if module is not None: - sFit.removeModule(fitID,sFit.modules.index(module)) + sFit.removeModule(fitID,fit.modules.index(module)) elif srcContext in ("fittingCharge" , "projectedCharge"): sFit.setAmmo(fitID, None, selection) elif srcContext == "droneItem": - sFit.removeDrone(fitID, sFit.drones.index(selection[0])) + sFit.removeDrone(fitID, fit.drones.index(selection[0])) elif srcContext == "implantItem": - sFit.removeImplant(fitID, sFit.implants.index(selection[0])) + sFit.removeImplant(fitID, fit.implants.index(selection[0])) elif srcContext == "boosterItem": - sFit.removeBooster(fitID, sFit.boosters.index(selection[0])) + sFit.removeBooster(fitID, fit.boosters.index(selection[0])) else: sFit.removeProjected(fitID, selection[0]) From 458e89a5346c30a7b22b07b9685ad1b750f563b9 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Mon, 18 Aug 2014 17:46:41 -0400 Subject: [PATCH 13/16] Fixes #154 --- eos/modifiedAttributeDict.py | 8 ++++---- gui/itemStats.py | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/eos/modifiedAttributeDict.py b/eos/modifiedAttributeDict.py index 62811b614..ca3615281 100644 --- a/eos/modifiedAttributeDict.py +++ b/eos/modifiedAttributeDict.py @@ -195,13 +195,13 @@ class ModifiedAttributeDict(collections.MutableMapping): affs = self.__affectedBy[attributeName] # If there's no set for current fit in dictionary, create it if self.fit not in affs: - affs[self.fit] = set() - # Reassign alias to set + affs[self.fit] = [] + # Reassign alias to list affs = affs[self.fit] # Get modifier which helps to compose 'Affected by' map modifier = self.fit.getModifier() - # Add current affliction to set - affs.add((modifier, operation, bonus, used)) + # Add current affliction to list + affs.append((modifier, operation, bonus, used)) def preAssign(self, attributeName, value): """Overwrites original value of the entity with given one, allowing further modification""" diff --git a/gui/itemStats.py b/gui/itemStats.py index 0851d5bf5..1ca83be55 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -67,7 +67,7 @@ class ItemStatsDialog(wx.Dialog): if itemImg is not None: self.SetIcon(wx.IconFromBitmap(itemImg)) self.SetTitle("%s: %s" % ("%s Stats" % itmContext if itmContext is not None else "Stats", item.name)) - + self.SetMinSize((300, 200)) self.SetSize((500, 300)) self.SetMaxSize((500, -1)) @@ -612,7 +612,6 @@ class ItemAffectedBy (wx.Panel): self.imageList = wx.ImageList(16, 16) self.affectedBy.SetImageList(self.imageList) - cont = self.stuff.itemModifiedAttributes if self.item == self.stuff.item else self.stuff.chargeModifiedAttributes things = {} @@ -620,20 +619,26 @@ class ItemAffectedBy (wx.Panel): # if value is 0 or there has been no change from original to modified, return if cont[attrName] == (cont.getOriginal(attrName) or 0): continue - for fit, afflictors in cont.getAfflictions(attrName).iteritems(): for afflictor, modifier, amount, used in afflictors: if not used or afflictor.item is None: continue + if afflictor.item.name not in things: - things[afflictor.item.name] = [type(afflictor), set(), set()] + things[afflictor.item.name] = [type(afflictor), set(), []] info = things[afflictor.item.name] info[1].add(afflictor) - info[2].add((attrName, modifier, amount)) + # If info[1] > 1, there are two separate modules working. + # Check to make sure we only include the modifier once + # See GH issue 154 + if len(info[1]) > 1 and (attrName, modifier, amount) in info[2]: + continue + info[2].append((attrName, modifier, amount)) order = things.keys() order.sort(key=lambda x: (self.ORDER.index(things[x][0]), x)) + for itemName in order: info = things[itemName] From 2dd8453bbba3ca2d2ff09cfa44e6210a7bf96f24 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Tue, 19 Aug 2014 16:22:37 -0400 Subject: [PATCH 14/16] Give itemStat width more room on GTK, resolves #113 --- gui/itemStats.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gui/itemStats.py b/gui/itemStats.py index 1ca83be55..6424fd305 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -69,8 +69,11 @@ class ItemStatsDialog(wx.Dialog): self.SetTitle("%s: %s" % ("%s Stats" % itmContext if itmContext is not None else "Stats", item.name)) self.SetMinSize((300, 200)) - self.SetSize((500, 300)) - self.SetMaxSize((500, -1)) + if "wxGTK" in wx.PlatformInfo: # GTK has huge tab widgets, give it a bit more room + self.SetSize((530, 300)) + else: + self.SetSize((500, 300)) + #self.SetMaxSize((500, -1)) self.mainSizer = wx.BoxSizer(wx.VERTICAL) self.container = ItemStatsContainer(self, victim, item, itmContext) self.mainSizer.Add(self.container, 1, wx.EXPAND) From 59be18506cd679d62f34db754791a25b75976540 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Tue, 19 Aug 2014 23:50:10 -0400 Subject: [PATCH 15/16] Added "Open in Ship Browser" to fitting context menu, and modified a bit of the history logic in shipBrowser. --- gui/builtinContextMenus/__init__.py | 2 +- gui/builtinContextMenus/shipJump.py | 29 +++++++++++++++++++++++++++++ gui/shipBrowser.py | 9 ++++++--- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 gui/builtinContextMenus/shipJump.py diff --git a/gui/builtinContextMenus/__init__.py b/gui/builtinContextMenus/__init__.py index edec6a9fc..4f93b8424 100644 --- a/gui/builtinContextMenus/__init__.py +++ b/gui/builtinContextMenus/__init__.py @@ -1,2 +1,2 @@ __all__ = ["moduleAmmoPicker", "itemStats", "damagePattern", "marketJump", "droneSplit", "itemRemove", - "droneRemoveStack", "ammoPattern", "project", "factorReload", "whProjector", "cargo"] + "droneRemoveStack", "ammoPattern", "project", "factorReload", "whProjector", "cargo", "shipJump"] diff --git a/gui/builtinContextMenus/shipJump.py b/gui/builtinContextMenus/shipJump.py new file mode 100644 index 000000000..7d8fc0fbc --- /dev/null +++ b/gui/builtinContextMenus/shipJump.py @@ -0,0 +1,29 @@ +import wx +from gui.contextMenu import ContextMenu +import gui.mainFrame +import service +from gui.shipBrowser import Stage3Selected + +class ShipJump(ContextMenu): + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + def display(self, srcContext, selection): + validContexts = ("fittingShip") + if not srcContext in validContexts: + return False + return True + + def getText(self, itmContext, selection): + return "Open in Ship Browser" + + def activate(self, fullContext, selection, i): + fitID = self.mainFrame.getActiveFit() + sFit = service.Fit.getInstance() + stuff = sFit.getFit(fitID).ship + groupID = stuff.item.group.ID + + self.mainFrame.notebookBrowsers.SetSelection(1) + wx.PostEvent(self.mainFrame.shipBrowser,Stage3Selected(shipID=stuff.item.ID, back=groupID)) + +ShipJump.register() diff --git a/gui/shipBrowser.py b/gui/shipBrowser.py index 6713bc892..4bbdb0855 100644 --- a/gui/shipBrowser.py +++ b/gui/shipBrowser.py @@ -454,7 +454,7 @@ class NavigationPanel(SFItem.SFBrowserItem): def OnHistoryReset(self): if self.shipBrowser.browseHist: self.shipBrowser.browseHist = [] - self.gotoStage(1,0) + self.gotoStage(1,0) def OnHistoryBack(self): if len(self.shipBrowser.browseHist) > 0: @@ -671,6 +671,7 @@ class ShipBrowser(wx.Panel): self._lastStage = self._activeStage self._activeStage = 1 self.lastdata = 0 + self.browseHist = [(1,0)] self.navpanel.ShowNewFitButton(False) self.navpanel.ShowSwitchEmptyGroupsButton(False) @@ -774,8 +775,8 @@ class ShipBrowser(wx.Panel): def stage2(self, event): back = event.back - if not back: - self.browseHist.append( (1,0) ) + #if not back: + # self.browseHist.append( (1,0) ) self._lastStage = self._activeStage self._activeStage = 2 @@ -807,6 +808,8 @@ class ShipBrowser(wx.Panel): elif event.back == -1: if len(self.navpanel.recentSearches)>0: self.browseHist.append((4, self.navpanel.lastSearch)) + elif event.back > 0: + self.browseHist.append( (2,event.back) ) shipID = event.shipID self.lastdata = shipID From 3e70a6fd7cb114b6ba6ccc34485dcaac9ce8b0d5 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 26 Aug 2014 18:19:39 +0400 Subject: [PATCH 16/16] Fix #139 (Compare Charges mismatch) --- gui/itemStats.py | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/gui/itemStats.py b/gui/itemStats.py index 6424fd305..80288f79a 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -35,12 +35,24 @@ except ImportError: class ItemStatsDialog(wx.Dialog): counter = 0 - def __init__(self, victim, fullContext=None, pos = wx.DefaultPosition, size = wx.DefaultSize, maximized = False): - wx.Dialog.__init__(self, - gui.mainFrame.MainFrame.getInstance(), - wx.ID_ANY, title="Item stats", pos = pos, size = size, - style = wx.CAPTION | wx.CLOSE_BOX | wx.MINIMIZE_BOX | - wx.MAXIMIZE_BOX | wx.RESIZE_BORDER| wx.SYSTEM_MENU) + def __init__( + self, + victim, + fullContext=None, + pos=wx.DefaultPosition, + size=wx.DefaultSize, + maximized = False + ): + + wx.Dialog.__init__( + self, + gui.mainFrame.MainFrame.getInstance(), + wx.ID_ANY, + title="Item stats", + pos=pos, + size=size, + style=wx.CAPTION | wx.CLOSE_BOX | wx.MINIMIZE_BOX | wx.MAXIMIZE_BOX | wx.RESIZE_BORDER| wx.SYSTEM_MENU + ) empty = getattr(victim, "isEmpty", False) @@ -284,6 +296,17 @@ class ItemParams (wx.Panel): self.toggleView = 1 self.stuff = stuff self.item = item + self.attrInfo = {} + self.attrValues = {} + if self.stuff is None: + self.attrInfo.update(self.item.attributes) + self.attrValues.update(self.item.attributes) + elif self.stuff.item == self.item: + self.attrInfo.update(self.stuff.item.attributes) + self.attrValues.update(self.stuff.itemModifiedAttributes) + else: + self.attrInfo.update(self.stuff.charge.attributes) + self.attrValues.update(self.stuff.chargeModifiedAttributes) self.m_staticline = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) mainSizer.Add( self.m_staticline, 0, wx.EXPAND) @@ -329,23 +352,17 @@ class ItemParams (wx.Panel): self.paramList.setResizeColumn(1) self.imageList = wx.ImageList(16, 16) self.paramList.SetImageList(self.imageList,wx.IMAGE_LIST_SMALL) - if self.stuff is None or self.stuff.item == self.item: - attrs = self.stuff.itemModifiedAttributes if self.stuff is not None else self.item.attributes - attrsInfo = self.item.attributes if self.stuff is None else self.stuff.item.attributes - else: - attrs = self.stuff.chargeModifiedAttributes if self.stuff is not None else self.item.attributes - attrsInfo = self.item.attributes if self.stuff is None else self.stuff.charge.attributes - names = list(attrs.iterkeys()) + names = list(self.attrValues.iterkeys()) names.sort() idNameMap = {} idCount = 0 for name in names: - info = attrsInfo.get(name) + info = self.attrInfo.get(name) - att = attrs[name] + att = self.attrValues[name] val = getattr(att, "value", None) value = val if val is not None else att