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