Change the way we work with prices

This commit is contained in:
DarkPhoenix
2019-02-19 19:02:50 +03:00
parent 21dd9bb6e4
commit 704042b4b7
7 changed files with 102 additions and 115 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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