# ============================================================================= # 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 . # ============================================================================= import time import threading import queue from xml.dom import minidom from logbook import Logger import wx from eos import db from service.network import Network, TimeoutError from service.fit import Fit from service.market import Market 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) from service.marketSources import evemarketer, evemarketdata # noqa: E402