diff --git a/eos/db/saveddata/price.py b/eos/db/saveddata/price.py
index 8abd07132..e0e0f530a 100644
--- a/eos/db/saveddata/price.py
+++ b/eos/db/saveddata/price.py
@@ -32,6 +32,4 @@ prices_table = Table("prices", saveddata_meta,
Column("status", Integer, nullable=False))
-mapper(Price, prices_table, properties={
- "_Price__price": prices_table.c.price,
-})
+mapper(Price, prices_table)
diff --git a/eos/saveddata/price.py b/eos/saveddata/price.py
index a2a630c30..79f8cd590 100644
--- a/eos/saveddata/price.py
+++ b/eos/saveddata/price.py
@@ -19,41 +19,57 @@
# ===============================================================================
-import time
from enum import IntEnum, unique
+from time import time
from logbook import Logger
+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
+
+
pyfalog = Logger(__name__)
@unique
class PriceStatus(IntEnum):
- notFetched = 0
- success = 1
- fail = 2
- notSupported = 3
+ initialized = 0
+ notSupported = 1
+ fetchSuccess = 2
+ fetchFail = 3
+ fetchTimeout = 4
class Price(object):
def __init__(self, typeID):
self.typeID = typeID
self.time = 0
- self.__price = 0
- self.status = PriceStatus.notFetched
+ self.price = 0
+ self.status = PriceStatus.initialized
@property
def isValid(self):
- return self.time >= time.time()
-
- @property
- def price(self):
- if self.status != PriceStatus.success:
- return 0
+ # Always attempt to update prices which were just initialized, and prices
+ # of unsupported items (maybe we start supporting them at some point)
+ if self.status in (PriceStatus.initialized, PriceStatus.notSupported):
+ return False
+ elif self.status == PriceStatus.fetchSuccess:
+ return time() <= self.time + VALIDITY
+ elif self.status == PriceStatus.fetchFail:
+ return time() <= self.time + REREQUEST
+ elif self.status == PriceStatus.fetchTimeout:
+ return time() <= self.time + TIMEOUT
else:
- return self.__price or 0
+ return False
- @price.setter
- def price(self, price):
- self.__price = price
+ def update(self, status, price=0):
+ # Keep old price if we failed to fetch new one
+ if status in (PriceStatus.fetchFail, PriceStatus.fetchTimeout):
+ price = self.price
+ elif status != PriceStatus.fetchSuccess:
+ price = 0
+ self.time = time()
+ self.price = price
+ self.status = status
diff --git a/gui/builtinViewColumns/price.py b/gui/builtinViewColumns/price.py
index 56901c172..f4083438e 100644
--- a/gui/builtinViewColumns/price.py
+++ b/gui/builtinViewColumns/price.py
@@ -22,6 +22,7 @@ import wx
from eos.saveddata.cargo import Cargo
from eos.saveddata.drone import Drone
+from eos.saveddata.fighter import Fighter
from eos.saveddata.price import PriceStatus
from service.price import Price as ServicePrice
from gui.viewColumn import ViewColumn
@@ -29,6 +30,16 @@ from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount
+def formatPrice(stuff, priceObj):
+ textItems = []
+ if priceObj.price:
+ mult = stuff.amount if isinstance(stuff, (Drone, Cargo, Fighter)) else 1
+ textItems.append(formatAmount(priceObj.price * mult, 3, 3, 9, currency=True))
+ if priceObj.status in (PriceStatus.fetchFail, PriceStatus.fetchTimeout):
+ textItems.append("(!)")
+ return " ".join(textItems)
+
+
class Price(ViewColumn):
name = "Price"
@@ -51,28 +62,14 @@ class Price(ViewColumn):
if not priceObj.isValid:
return False
- # Fetch actual price as float to not modify its value on Price object
- price = priceObj.price
-
- if price == 0:
- return ""
-
- if isinstance(stuff, Drone) or isinstance(stuff, Cargo):
- price *= stuff.amount
-
- return formatAmount(price, 3, 3, 9, currency=True)
+ return formatPrice(stuff, priceObj)
def delayedText(self, mod, display, colItem):
sPrice = ServicePrice.getInstance()
def callback(item):
- price = item[0]
- textItems = []
- if price.price:
- textItems.append(formatAmount(price.price, 3, 3, 9, currency=True))
- if price.status == PriceStatus.fail:
- textItems.append("(!)")
- colItem.SetText(" ".join(textItems))
+ priceObj = item[0]
+ colItem.SetText(formatPrice(mod, priceObj))
display.SetItem(colItem)
diff --git a/service/marketSources/evemarketdata.py b/service/marketSources/evemarketdata.py
index 15edc92ef..af8a83991 100644
--- a/service/marketSources/evemarketdata.py
+++ b/service/marketSources/evemarketdata.py
@@ -17,30 +17,30 @@
# along with pyfa. If not, see .
# =============================================================================
-import time
+
from xml.dom import minidom
from logbook import Logger
from eos.saveddata.price import PriceStatus
from service.network import Network
-from service.price import Price, TIMEOUT, VALIDITY
+from service.price import Price
pyfalog = Logger(__name__)
-class EveMarketData(object):
+class EveMarketData:
name = "eve-marketdata.com"
- def __init__(self, types, system, priceMap):
+ def __init__(self, priceMap, system, timeout):
data = {}
baseurl = "https://eve-marketdata.com/api/item_prices.xml"
- data["system_id"] = system # Use Jita for market
- data["type_ids"] = ','.join(str(x) for x in types)
+ data["system_id"] = system
+ data["type_ids"] = ','.join(str(typeID) for typeID in priceMap)
network = Network.getInstance()
- data = network.request(baseurl, network.PRICES, params=data)
+ data = network.request(baseurl, network.PRICES, params=data, timeout=timeout)
xml = minidom.parseString(data.text)
types = xml.getElementsByTagName("eve").item(0).getElementsByTagName("price")
@@ -55,19 +55,10 @@ class EveMarketData(object):
pyfalog.warning("Failed to get price for: {0}", type_)
continue
- # Fill price data
- priceobj = priceMap[typeID]
-
- # eve-marketdata returns 0 if price data doesn't even exist for the item. In this case, don't reset the
- # cached price, and set the price timeout to TIMEOUT (every 15 minutes currently). Se GH issue #1334
- if price != 0:
- priceobj.price = price
- priceobj.time = time.time() + VALIDITY
- priceobj.status = PriceStatus.success
- else:
- priceobj.time = time.time() + TIMEOUT
-
- # delete price from working dict
+ # eve-marketdata returns 0 if price data doesn't even exist for the item
+ if price == 0:
+ continue
+ priceMap[typeID].update(PriceStatus.fetchSuccess, price)
del priceMap[typeID]
diff --git a/service/marketSources/evemarketer.py b/service/marketSources/evemarketer.py
index 478de4371..07a0b2dc0 100644
--- a/service/marketSources/evemarketer.py
+++ b/service/marketSources/evemarketer.py
@@ -17,33 +17,31 @@
# along with pyfa. If not, see .
# =============================================================================
-import time
+
from xml.dom import minidom
from logbook import Logger
from eos.saveddata.price import PriceStatus
from service.network import Network
-from service.price import Price, VALIDITY
+from service.price import Price
pyfalog = Logger(__name__)
-class EveMarketer(object):
+class EveMarketer:
name = "evemarketer"
- def __init__(self, types, system, priceMap):
+ def __init__(self, priceMap, system, timeout):
data = {}
baseurl = "https://api.evemarketer.com/ec/marketstat"
- data["usesystem"] = system # Use Jita for market
- data["typeid"] = set()
- for typeID in types: # Add all typeID arguments
- data["typeid"].add(typeID)
+ data["usesystem"] = system
+ data["typeid"] = {typeID for typeID in priceMap}
network = Network.getInstance()
- data = network.request(baseurl, network.PRICES, params=data)
+ data = network.request(baseurl, network.PRICES, params=data, timeout=timeout)
xml = minidom.parseString(data.text)
types = xml.getElementsByTagName("marketstat").item(0).getElementsByTagName("type")
# Cycle through all types we've got from request
@@ -56,15 +54,10 @@ class EveMarketer(object):
percprice = float(sell.getElementsByTagName("percentile").item(0).firstChild.data)
except (TypeError, ValueError):
pyfalog.warning("Failed to get price for: {0}", type_)
- percprice = 0
+ continue
# Fill price data
- priceobj = priceMap[typeID]
- priceobj.price = percprice
- priceobj.time = time.time() + VALIDITY
- priceobj.status = PriceStatus.success
-
- # delete price from working dict
+ priceMap[typeID].update(PriceStatus.fetchSuccess, percprice)
del priceMap[typeID]
diff --git a/service/network.py b/service/network.py
index 5554eadec..5ce413445 100644
--- a/service/network.py
+++ b/service/network.py
@@ -53,7 +53,7 @@ class TimeoutError(Exception):
pass
-class Network(object):
+class Network:
# Request constants - every request must supply this, as it is checked if
# enabled or not via settings
ENABLED = 1
diff --git a/service/price.py b/service/price.py
index 3028b74ad..039a5a2d4 100644
--- a/service/price.py
+++ b/service/price.py
@@ -20,7 +20,6 @@
import queue
import threading
-import time
import wx
from logbook import Logger
@@ -31,15 +30,11 @@ from service.fit import Fit
from service.market import Market
from service.network import TimeoutError
+
pyfalog = Logger(__name__)
-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(object):
+class Price:
instance = None
systemsList = {
@@ -69,38 +64,34 @@ class Price(object):
return cls.instance
@classmethod
- def fetchPrices(cls, prices):
+ def fetchPrices(cls, prices, timeout=5):
"""Fetch all prices passed to this method"""
# Dictionary for our price objects
priceMap = {}
- # Check all provided price objects, and add invalid ones to dictionary
+ # Check all provided price objects, and add those we want to update to
+ # dictionary
for price in prices:
if not price.isValid:
priceMap[price.typeID] = price
- if len(priceMap) == 0:
+ if not priceMap:
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 tuple(priceMap):
# Get item object
item = 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
+ # We're not going to request items only with market group, as our current market
+ # sources do not provide any data for items not on the market
if item is None:
continue
if not item.marketGroupID:
- priceMap[typeID].status = PriceStatus.notSupported
+ priceMap[typeID].update(PriceStatus.notSupported)
del priceMap[typeID]
continue
- toRequest.add(typeID)
- # Do not waste our time if all items are not on the market
- if len(toRequest) == 0:
+ if not priceMap:
return
sFit = Fit.getInstance()
@@ -113,33 +104,34 @@ class Price(object):
sourcesToTry = list(cls.sources.keys())
curr = sFit.serviceFittingOptions["priceSource"] if sFit.serviceFittingOptions["priceSource"] in sourcesToTry else sourcesToTry[0]
- while len(sourcesToTry) > 0:
+ # Record timeouts as it will affect our final decision
+ timeouts = {}
+
+ while priceMap and sourcesToTry:
+ timeouts[curr] = False
sourcesToTry.remove(curr)
try:
sourceCls = cls.sources.get(curr)
- sourceCls(toRequest, cls.systemsList[sFit.serviceFittingOptions["priceSystem"]], priceMap)
- break
- # If getting or processing data returned any errors
+ sourceCls(priceMap, cls.systemsList[sFit.serviceFittingOptions["priceSystem"]], timeout)
except TimeoutError:
- # Timeout error deserves special treatment
- pyfalog.warning("Price fetch timout")
- for typeID in tuple(priceMap):
- priceobj = priceMap[typeID]
- priceobj.time = time.time() + TIMEOUT
- priceobj.status = PriceStatus.fail
- del priceMap[typeID]
- except Exception as ex:
- # something happened, try another source
- pyfalog.warn('Failed to fetch prices from price source {}: {}'.format(curr, ex, sourcesToTry[0]))
- if len(sourcesToTry) > 0:
- pyfalog.warn('Trying {}'.format(sourcesToTry[0]))
- curr = sourcesToTry[0]
+ pyfalog.warning("Price fetch timeout for source {}".format(curr))
+ timeouts[curr] = True
+ except Exception as e:
+ pyfalog.warn('Failed to fetch prices from price source {}: {}'.format(curr, e))
+ if sourcesToTry:
+ curr = sourcesToTry[0]
+ pyfalog.warn('Trying {}'.format(curr))
- # if we get to this point, then we've got an error in all of our sources. Set to REREQUEST delay
- for typeID in priceMap.keys():
- priceobj = priceMap[typeID]
- priceobj.time = time.time() + REREQUEST
- priceobj.status = PriceStatus.fail
+ # If we get to this point, then we've failed to get price with all our sources
+ # If all sources failed due to timeouts, set one status
+ if all(to is True for to in timeouts.values()):
+ for typeID in priceMap.keys():
+ priceMap[typeID].update(PriceStatus.fetchTimeout)
+ # If some sources failed due to any other reason, then it's definitely not network
+ # timeout and we just set another status
+ else:
+ for typeID in priceMap.keys():
+ priceMap[typeID].update(PriceStatus.fetchFail)
@classmethod
def fitItemsList(cls, fit):