Merge branch 'master' into singularity

This commit is contained in:
DarkPhoenix
2014-08-26 22:47:50 +04:00
29 changed files with 703 additions and 566 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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"""

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -1,2 +1,2 @@
__all__ = ["moduleAmmoPicker", "itemStats", "damagePattern", "marketJump", "droneSplit", "itemRemove",
"droneRemoveStack", "ammoPattern", "project", "factorReload", "whProjector", "cargo"]
"droneRemoveStack", "ammoPattern", "project", "factorReload", "whProjector", "cargo", "shipJump"]

View File

@@ -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))

View File

@@ -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])

View File

@@ -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()

View File

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

View File

@@ -1,6 +1,4 @@
import wx
import service
import urllib2
from gui.preferenceView import PreferenceView
from gui import bitmapLoader

View File

@@ -1,6 +1,4 @@
import wx
import service
import urllib2
import os
from gui.preferenceView import PreferenceView

View File

@@ -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()
PFNetworkPref.register()

View File

@@ -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()
PFUpdatePref.register()

View File

@@ -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]

View File

@@ -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))

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -17,25 +17,23 @@
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
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):

View File

@@ -1,7 +1,7 @@
#-----------------------------------------------------------------------------
# eveapi - EVE Online API access
#
# Copyright (c)2007 Jamie "Entity" van den Berge <entity@vapor.com>
# Copyright (c)2007-2014 Jamie "Entity" van den Berge <jamie@hlekkir.com>
#
# 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 <tag attr=bla ... />
#
# 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):
# <hack>
# - 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)])
# </hack>
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 <tag attribute=bla ... />
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: <tag />, 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)]

View File

@@ -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()

96
service/network.py Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
#===============================================================================
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)

View File

@@ -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()

116
service/price.py Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
#===============================================================================
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

View File

@@ -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?

View File

@@ -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):