Change the way we work with prices
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -17,30 +17,30 @@
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
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]
|
||||
|
||||
|
||||
|
||||
@@ -17,33 +17,31 @@
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
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]
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user