237 lines
7.4 KiB
Python
237 lines
7.4 KiB
Python
# =============================================================================
|
|
# 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 queue
|
|
import threading
|
|
import time
|
|
|
|
import wx
|
|
from logbook import Logger
|
|
|
|
from eos import db
|
|
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):
|
|
instance = None
|
|
|
|
systemsList = {
|
|
"Jita": 30000142,
|
|
"Amarr": 30002187,
|
|
"Dodixie": 30002659,
|
|
"Rens": 30002510,
|
|
"Hek": 30002053
|
|
}
|
|
|
|
sources = {}
|
|
|
|
def __init__(self):
|
|
# Start price fetcher
|
|
self.priceWorkerThread = PriceWorkerThread()
|
|
self.priceWorkerThread.daemon = True
|
|
self.priceWorkerThread.start()
|
|
|
|
@classmethod
|
|
def register(cls, source):
|
|
cls.sources[source.name] = source
|
|
|
|
@classmethod
|
|
def getInstance(cls):
|
|
if cls.instance is None:
|
|
cls.instance = Price()
|
|
return cls.instance
|
|
|
|
@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 = 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 is not None and item.marketGroupID:
|
|
toRequest.add(typeID)
|
|
|
|
# Do not waste our time if all items are not on the market
|
|
if len(toRequest) == 0:
|
|
return
|
|
|
|
sFit = Fit.getInstance()
|
|
|
|
if len(cls.sources.keys()) == 0:
|
|
pyfalog.warn('No price source can be found')
|
|
return
|
|
|
|
# attempt to find user's selected price source, otherwise get first one
|
|
sourcesToTry = list(cls.sources.keys())
|
|
curr = sFit.serviceFittingOptions["priceSource"] if sFit.serviceFittingOptions["priceSource"] in sourcesToTry else sourcesToTry[0]
|
|
|
|
while len(sourcesToTry) > 0:
|
|
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
|
|
except TimeoutError:
|
|
# Timeout error deserves special treatment
|
|
pyfalog.warning("Price fetch timout")
|
|
for typeID in priceMap.keys():
|
|
priceobj = priceMap[typeID]
|
|
priceobj.time = time.time() + TIMEOUT
|
|
priceobj.failed = True
|
|
|
|
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]
|
|
|
|
# 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.failed = True
|
|
|
|
@classmethod
|
|
def fitItemsList(cls, fit):
|
|
# Compose a list of all the data we need & request it
|
|
fit_items = [fit.ship.item]
|
|
|
|
for mod in fit.modules:
|
|
if not mod.isEmpty:
|
|
fit_items.append(mod.item)
|
|
|
|
for drone in fit.drones:
|
|
fit_items.append(drone.item)
|
|
|
|
for fighter in fit.fighters:
|
|
fit_items.append(fighter.item)
|
|
|
|
for cargo in fit.cargo:
|
|
fit_items.append(cargo.item)
|
|
|
|
for boosters in fit.boosters:
|
|
fit_items.append(boosters.item)
|
|
|
|
for implants in fit.implants:
|
|
fit_items.append(implants.item)
|
|
|
|
return list(set(fit_items))
|
|
|
|
def getPriceNow(self, objitem):
|
|
"""Get price for provided typeID"""
|
|
sMkt = Market.getInstance()
|
|
item = sMkt.getItem(objitem)
|
|
|
|
return item.price.price
|
|
|
|
def getPrices(self, objitems, callback, waitforthread=False):
|
|
"""Get prices for multiple typeIDs"""
|
|
requests = []
|
|
for objitem in objitems:
|
|
sMkt = Market.getInstance()
|
|
item = sMkt.getItem(objitem)
|
|
requests.append(item.price)
|
|
|
|
def cb():
|
|
try:
|
|
callback(requests)
|
|
except Exception as e:
|
|
pyfalog.critical("Callback failed.")
|
|
pyfalog.critical(e)
|
|
|
|
db.commit()
|
|
|
|
if waitforthread:
|
|
self.priceWorkerThread.setToWait(requests, cb)
|
|
else:
|
|
self.priceWorkerThread.trigger(requests, cb)
|
|
|
|
def clearPriceCache(self):
|
|
pyfalog.debug("Clearing Prices")
|
|
db.clearPrices()
|
|
|
|
|
|
class PriceWorkerThread(threading.Thread):
|
|
def __init__(self):
|
|
threading.Thread.__init__(self)
|
|
self.name = "PriceWorker"
|
|
self.queue = queue.Queue()
|
|
self.wait = {}
|
|
pyfalog.debug("Initialize PriceWorkerThread.")
|
|
|
|
def run(self):
|
|
queue = self.queue
|
|
while True:
|
|
# Grab our data
|
|
callback, requests = queue.get()
|
|
|
|
# Grab prices, this is the time-consuming part
|
|
if len(requests) > 0:
|
|
Price.fetchPrices(requests)
|
|
|
|
wx.CallAfter(callback)
|
|
queue.task_done()
|
|
|
|
# After we fetch prices, go through the list of waiting items and call their callbacks
|
|
for price in requests:
|
|
callbacks = self.wait.pop(price.typeID, None)
|
|
if callbacks:
|
|
for callback in callbacks:
|
|
wx.CallAfter(callback)
|
|
|
|
def trigger(self, prices, callbacks):
|
|
self.queue.put((callbacks, prices))
|
|
|
|
def setToWait(self, prices, callback):
|
|
for x in prices:
|
|
if x.typeID not in self.wait:
|
|
self.wait[x.typeID] = []
|
|
self.wait[x.typeID].append(callback)
|
|
|
|
|