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/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)
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/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/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/eos/saveddata/price.py b/eos/saveddata/price.py
index 5f89f260e..4c7de879c 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,6 @@ 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):
+ return self.time >= time.time()
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/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])
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/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 61%
rename from gui/builtinPreferenceViews/pyfaProxyPreferences.py
rename to gui/builtinPreferenceViews/pyfaNetworkPreferences.py
index 40c2286f1..059a505de 100644
--- a/gui/builtinPreferenceViews/pyfaProxyPreferences.py
+++ b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py
@@ -1,29 +1,21 @@
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.settings = service.settings.NetworkSettings.getInstance()
+ self.network = service.Network.getInstance()
self.dirtySettings = False
- self.nMode = self.proxySettings.getMode()
- self.nAddr = self.proxySettings.getAddress()
- self.nPort = self.proxySettings.getPort()
- self.nType = self.proxySettings.getType()
-
mainSizer = wx.BoxSizer( wx.VERTICAL )
self.stTitle = wx.StaticText( panel, wx.ID_ANY, self.title, wx.DefaultPosition, wx.DefaultSize, 0 )
@@ -35,6 +27,49 @@ class PFProxyPref ( 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 )
+
+ 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))
+ 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 )
@@ -83,13 +118,13 @@ class PFProxyPref ( 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.proxySettings.autodetect()
+ proxy = self.settings.autodetect()
if proxy is not None:
addr,port = proxy
@@ -117,6 +152,24 @@ class PFProxyPref ( 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
@@ -133,10 +186,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.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:
@@ -172,4 +225,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/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/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]
diff --git a/gui/characterEditor.py b/gui/characterEditor.py
index 8cf298f0d..be77c8d21 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("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.message)
+ 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/gui/itemStats.py b/gui/itemStats.py
index 0851d5bf5..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)
@@ -67,10 +79,13 @@ 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))
+ 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)
@@ -281,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)
@@ -326,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
@@ -612,7 +632,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 +639,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]
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
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/__init__.py b/service/__init__.py
index e7581c675..8ded8a66a 100644
--- a/service/__init__.py
+++ b/service/__init__.py
@@ -6,3 +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/character.py b/service/character.py
index 7a8a6cbc6..e78ec990d 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)
@@ -47,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)
@@ -67,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:
@@ -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/eos/eveapi.py b/service/eveapi.py
similarity index 82%
rename from eos/eveapi.py
rename to service/eveapi.py
index db0888c10..4b19340b0 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)
@@ -108,26 +147,68 @@
#
#-----------------------------------------------------------------------------
-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
from xml.parsers import expat
from time import strptime
from calendar import timegm
+import service
+
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 +218,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 +256,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 +281,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 +395,11 @@ class _RootContext(_Context):
response = None
if response is None:
- if self._scheme == "https":
- connectionclass = httplib.HTTPSConnection
- else:
- connectionclass = httplib.HTTPConnection
+ network = service.Network.getInstance()
- if self._proxy is None:
- if self._scheme == "https":
- connectionclass = httplib.HTTPSConnection
- else:
- connectionclass = httplib.HTTPConnection
+ req = self._scheme+'://'+self._host+path
- 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)
- 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"})
- else:
- http.request("GET", self._scheme+'://'+self._host+path)
-
- response = http.getresponse()
- if response.status != 200:
- if response.status == httplib.NOT_FOUND:
- raise AttributeError("'%s' not available on API server (404 Not Found)" % path)
- else:
- raise RuntimeError("'%s' request failed (%d %s)" % (path, response.status, response.reason))
+ response = network.request(req, network.EVE, kw)
if cache:
store = True
@@ -348,7 +414,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 +452,7 @@ def _autocast(key, value):
# couldn't cast. return string unchanged.
return value
+_castfunc = _autocast
class _Parser(object):
@@ -460,20 +527,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 +570,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 +583,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 +608,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 +644,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 +672,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 +685,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 +734,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)]
diff --git a/service/market.py b/service/market.py
index 94b25e023..79495478b 100644
--- a/service/market.py
+++ b/service/market.py
@@ -25,7 +25,8 @@ import Queue
import eos.db
import eos.types
-from service.settings import SettingsProvider, ProxySettings
+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 = ProxySettings.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
new file mode 100644
index 000000000..04ba49875
--- /dev/null
+++ b/service/network.py
@@ -0,0 +1,96 @@
+#===============================================================================
+# 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
+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, msg=None):
+ self.message = msg
+
+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
+ # 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, 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)
+
+ # Make sure request is enabled
+ access = NetworkSettings.getInstance().getAccess()
+
+ if not self.ENABLED & access or not type & access:
+ 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)
+ headers={"User-Agent" : "pyfa {0} (Python-urllib2)".format(versionString)}
+
+ proxy = NetworkSettings.getInstance().getProxySettings()
+ if proxy is not None:
+ 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:
+ return urllib2.urlopen(request)
+ except urllib2.HTTPError, error:
+ if error.code == 404:
+ raise RequestError()
+ elif error.code == 403:
+ raise AuthenticationError()
+ elif error.code >= 500:
+ raise ServerError()
+ except urllib2.URLError, error:
+ if "timed out" in error.reason:
+ raise TimeoutError()
+ else:
+ raise Error(error)
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/price.py b/service/price.py
new file mode 100644
index 000000000..fafdf3297
--- /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"""
+
+ # 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:
+ 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
diff --git a/service/settings.py b/service/settings.py
index ed7d0225e..dde48f71d 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,52 @@ 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 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.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):
@@ -233,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):
@@ -245,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 5ec6ffabf..14a528b42 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,14 @@ 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,8 +67,8 @@ class CheckUpdateThread(threading.Thread):
else:
if release['prerelease'] and rVersion > config.expansionVersion:
wx.CallAfter(self.callback, release) # Singularity -> Singularity
- break;
- except: # for when there is no internet connection
+ break
+ except:
pass
def versiontuple(self, v):