diff --git a/eos/db/__init__.py b/eos/db/__init__.py index 2647d9d10..30f1102f0 100644 --- a/eos/db/__init__.py +++ b/eos/db/__init__.py @@ -68,7 +68,7 @@ else: saveddata_meta = None # Lock controlling any changes introduced to session -sd_lock = threading.Lock() +sd_lock = threading.RLock() # Import all the definitions for all our database stuff # noinspection PyPep8 diff --git a/eos/gamedata.py b/eos/gamedata.py index fe7eb2893..f97aaa58f 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -23,6 +23,7 @@ from sqlalchemy.orm import reconstructor import eos.db from eqBase import EqBase +from eos.saveddata.price import Price as types_Price try: from collections import OrderedDict @@ -438,6 +439,39 @@ class Item(EqBase): return False + @property + def price(self): + try: + if not hasattr(self, "__price"): + self.__price = types_Price(self.ID) + + # Get the price from the DB + price = eos.db.getPrice(self.ID) + + if price: + if self.__price.time > price.time: + # The object is newer than the DB, update the DB. + eos.db.add(self.__price) + eos.db.commit() + + if self.__price.time < price.time: + # DB object is newer than local object, update the local object + self.__price = price + else: + pyfalog.debug("Unable to fetch item price from database.") + + return self.__price + + except Exception as e: + # We want to catch our failure and log it, but don't bail out for a single missing price tag. + pyfalog.error("Failed to get price for typeID: {0}", self.ID) + pyfalog.error(e) + if not self.__price.price: + self.__price.price = 0 + self.__price.failed = True + + return self.__price + def __repr__(self): return "Item(ID={}, name={}) at {}".format( self.ID, self.name, hex(id(self)) diff --git a/eos/saveddata/price.py b/eos/saveddata/price.py index 8074373df..304d6fedd 100644 --- a/eos/saveddata/price.py +++ b/eos/saveddata/price.py @@ -21,6 +21,9 @@ import time from sqlalchemy.orm import reconstructor +from logbook import Logger + +pyfalog = Logger(__name__) class Price(object): @@ -29,7 +32,6 @@ class Price(object): self.time = 0 self.price = 0 self.failed = None - self.__item = None @reconstructor def init(self): diff --git a/gui/boosterView.py b/gui/boosterView.py index fe0d802d3..b90350bf8 100644 --- a/gui/boosterView.py +++ b/gui/boosterView.py @@ -43,9 +43,12 @@ class BoosterViewDrop(wx.PyDropTarget): class BoosterView(d.Display): - DEFAULT_COLS = ["State", - "attr:boosterness", - "Base Name"] + DEFAULT_COLS = [ + "State", + "attr:boosterness", + "Base Name", + "Price", + ] def __init__(self, parent): d.Display.__init__(self, parent, style=wx.LC_SINGLE_SEL | wx.BORDER_NONE) diff --git a/gui/builtinContextMenus/priceClear.py b/gui/builtinContextMenus/priceClear.py index 44093e662..0a2dfae63 100644 --- a/gui/builtinContextMenus/priceClear.py +++ b/gui/builtinContextMenus/priceClear.py @@ -3,7 +3,6 @@ import gui.mainFrame # noinspection PyPackageRequirements import wx import gui.globalEvents as GE -from service.market import Market from service.settings import ContextMenuSettings @@ -22,8 +21,6 @@ class PriceClear(ContextMenu): return "Reset Price Cache" def activate(self, fullContext, selection, i): - sMkt = Market.getInstance() - sMkt.clearPriceCache() wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit())) diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py index 0be3baddf..10699e8f0 100644 --- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py +++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py @@ -9,7 +9,6 @@ import gui.globalEvents as GE from service.settings import SettingsProvider from service.fit import Fit from service.price import Price -from service.market import Market class PFGeneralPref(PreferenceView): @@ -197,9 +196,6 @@ class PFGeneralPref(PreferenceView): fitID = self.mainFrame.getActiveFit() - sMkt = Market.getInstance() - sMkt.clearPriceCache() - self.sFit.refreshFit(fitID) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) event.Skip() diff --git a/gui/builtinPreferenceViews/pyfaStatViewPreferences.py b/gui/builtinPreferenceViews/pyfaStatViewPreferences.py index 64182a82e..800b64851 100644 --- a/gui/builtinPreferenceViews/pyfaStatViewPreferences.py +++ b/gui/builtinPreferenceViews/pyfaStatViewPreferences.py @@ -91,8 +91,6 @@ class PFStatViewPref(PreferenceView): rbSizerRow3 = wx.BoxSizer(wx.HORIZONTAL) self.rbPrice = wx.RadioBox(panel, -1, "Price", wx.DefaultPosition, wx.DefaultSize, ['None', 'Minimal', 'Full'], 1, wx.RA_SPECIFY_COLS) - # Disable minimal as we don't have a view for this yet - self.rbPrice.EnableItem(1, False) self.rbPrice.SetSelection(self.settings.get('price')) rbSizerRow3.Add(self.rbPrice, 1, wx.TOP | wx.RIGHT, 5) self.rbPrice.Bind(wx.EVT_RADIOBOX, self.OnPriceChange) diff --git a/gui/builtinStatsViews/__init__.py b/gui/builtinStatsViews/__init__.py index be310c2ac..8769c7aef 100644 --- a/gui/builtinStatsViews/__init__.py +++ b/gui/builtinStatsViews/__init__.py @@ -8,4 +8,5 @@ __all__ = [ "outgoingViewMinimal", "targetingMiscViewMinimal", "priceViewFull", + "priceViewMinimal", ] diff --git a/gui/builtinStatsViews/priceViewFull.py b/gui/builtinStatsViews/priceViewFull.py index 0fbd24ff0..0b7ea8df6 100644 --- a/gui/builtinStatsViews/priceViewFull.py +++ b/gui/builtinStatsViews/priceViewFull.py @@ -22,7 +22,6 @@ import wx from gui.statsView import StatsView from gui.bitmapLoader import BitmapLoader from gui.utils.numberFormatter import formatAmount -from service.market import Market from service.price import Price @@ -32,9 +31,6 @@ class PriceViewFull(StatsView): def __init__(self, parent): StatsView.__init__(self) self.parent = parent - self._cachedShip = 0 - self._cachedFittings = 0 - self._cachedTotal = 0 def getHeaderText(self, fit): return "Price" @@ -51,10 +47,16 @@ class PriceViewFull(StatsView): headerContentSizer.Add(self.labelEMStatus) headerPanel.GetParent().AddToggleItem(self.labelEMStatus) - gridPrice = wx.GridSizer(1, 3) + gridPrice = wx.GridSizer(2, 3) contentSizer.Add(gridPrice, 0, wx.EXPAND | wx.ALL, 0) - for type in ("ship", "fittings", "total"): - image = "%sPrice_big" % type if type != "ship" else "ship_big" + for _type in ("ship", "fittings", "drones", "cargoBay", "character", "total"): + if _type in "ship": + image = "ship_big" + elif _type in ("fittings", "total"): + image = "%sPrice_big" % _type + else: + image = "%s_big" % _type + box = wx.BoxSizer(wx.HORIZONTAL) gridPrice.Add(box, 0, wx.ALIGN_TOP) @@ -63,50 +65,91 @@ class PriceViewFull(StatsView): vbox = wx.BoxSizer(wx.VERTICAL) box.Add(vbox, 1, wx.EXPAND) - vbox.Add(wx.StaticText(contentPanel, wx.ID_ANY, type.capitalize()), 0, wx.ALIGN_LEFT) + vbox.Add(wx.StaticText(contentPanel, wx.ID_ANY, _type.capitalize()), 0, wx.ALIGN_LEFT) hbox = wx.BoxSizer(wx.HORIZONTAL) vbox.Add(hbox) lbl = wx.StaticText(contentPanel, wx.ID_ANY, "0.00 ISK") - setattr(self, "labelPrice%s" % type.capitalize(), lbl) + setattr(self, "labelPrice%s" % _type.capitalize(), lbl) hbox.Add(lbl, 0, wx.ALIGN_LEFT) def refreshPanel(self, fit): if fit is not None: self.fit = fit - typeIDs = Price.fitItemsList(fit) + fit_items = Price.fitItemsList(fit) - sMkt = Market.getInstance() - sMkt.getPrices(typeIDs, self.processPrices) + sPrice = Price.getInstance() + sPrice.getPrices(fit_items, self.processPrices) self.labelEMStatus.SetLabel("Updating prices...") - else: - self.labelEMStatus.SetLabel("") - self.labelPriceShip.SetLabel("0.0 ISK") - self.labelPriceFittings.SetLabel("0.0 ISK") - self.labelPriceTotal.SetLabel("0.0 ISK") - self._cachedFittings = self._cachedShip = self._cachedTotal = 0 - self.panel.Layout() + + self.refreshPanelPrices(fit) + self.panel.Layout() + + def refreshPanelPrices(self, fit=None): + + ship_price = 0 + module_price = 0 + drone_price = 0 + fighter_price = 0 + cargo_price = 0 + booster_price = 0 + implant_price = 0 + + if fit: + ship_price = fit.ship.item.price.price + + if fit.modules: + for module in fit.modules: + if not module.isEmpty: + module_price += module.item.price.price + + if fit.drones: + for drone in fit.drones: + drone_price += drone.item.price.price * drone.amount + + if fit.fighters: + for fighter in fit.fighters: + fighter_price += fighter.item.price.price * fighter.amount + + if fit.cargo: + for cargo in fit.cargo: + cargo_price += cargo.item.price.price * cargo.amount + + if fit.boosters: + for booster in fit.boosters: + booster_price += booster.item.price.price + + if fit.implants: + for implant in fit.implants: + implant_price += implant.item.price.price + + fitting_price = module_price + drone_price + fighter_price + cargo_price + booster_price + implant_price + total_price = ship_price + fitting_price + + self.labelPriceShip.SetLabel("%s ISK" % formatAmount(ship_price, 3, 3, 9, currency=True)) + self.labelPriceShip.SetToolTip(wx.ToolTip('{:,.2f}'.format(ship_price))) + + self.labelPriceFittings.SetLabel("%s ISK" % formatAmount(module_price, 3, 3, 9, currency=True)) + self.labelPriceFittings.SetToolTip(wx.ToolTip('{:,.2f}'.format(module_price))) + + self.labelPriceDrones.SetLabel("%s ISK" % formatAmount(drone_price + fighter_price, 3, 3, 9, currency=True)) + self.labelPriceDrones.SetToolTip(wx.ToolTip('{:,.2f}'.format(drone_price + fighter_price))) + + self.labelPriceCargobay.SetLabel("%s ISK" % formatAmount(cargo_price, 3, 3, 9, currency=True)) + self.labelPriceCargobay.SetToolTip(wx.ToolTip('{:,.2f}'.format(cargo_price))) + + self.labelPriceCharacter.SetLabel("%s ISK" % formatAmount(booster_price + implant_price, 3, 3, 9, currency=True)) + self.labelPriceCharacter.SetToolTip(wx.ToolTip('{:,.2f}'.format(booster_price + implant_price))) + + self.labelPriceTotal.SetLabel("%s ISK" % formatAmount(total_price, 3, 3, 9, currency=True)) + self.labelPriceTotal.SetToolTip(wx.ToolTip('{:,.2f}'.format(total_price))) def processPrices(self, prices): - shipPrice = prices[0].price - modPrice = sum(map(lambda p: p.price or 0, prices[1:])) + self.refreshPanelPrices(self.fit) self.labelEMStatus.SetLabel("") - - if self._cachedShip != shipPrice: - self.labelPriceShip.SetLabel("%s ISK" % formatAmount(shipPrice, 3, 3, 9, currency=True)) - self.labelPriceShip.SetToolTip(wx.ToolTip('{:,.2f}'.format(shipPrice))) - self._cachedShip = shipPrice - if self._cachedFittings != modPrice: - self.labelPriceFittings.SetLabel("%s ISK" % formatAmount(modPrice, 3, 3, 9, currency=True)) - self.labelPriceFittings.SetToolTip(wx.ToolTip('{:,.2f}'.format(modPrice))) - self._cachedFittings = modPrice - if self._cachedTotal != (shipPrice + modPrice): - self.labelPriceTotal.SetLabel("%s ISK" % formatAmount(shipPrice + modPrice, 3, 3, 9, currency=True)) - self.labelPriceTotal.SetToolTip(wx.ToolTip('{:,.2f}'.format(shipPrice + modPrice))) - self._cachedTotal = shipPrice + modPrice self.panel.Layout() diff --git a/gui/builtinStatsViews/priceViewMinimal.py b/gui/builtinStatsViews/priceViewMinimal.py new file mode 100644 index 000000000..421d3d983 --- /dev/null +++ b/gui/builtinStatsViews/priceViewMinimal.py @@ -0,0 +1,141 @@ +# ============================================================================= +# 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 . +# ============================================================================= + +# noinspection PyPackageRequirements +import wx +from gui.statsView import StatsView +from gui.bitmapLoader import BitmapLoader +from gui.utils.numberFormatter import formatAmount +from service.price import Price + + +class PriceViewMinimal(StatsView): + name = "priceViewMinimal" + + def __init__(self, parent): + StatsView.__init__(self) + self.parent = parent + + def getHeaderText(self, fit): + return "Price" + + def populatePanel(self, contentPanel, headerPanel): + contentSizer = contentPanel.GetSizer() + self.panel = contentPanel + self.headerPanel = headerPanel + + headerContentSizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer = headerPanel.GetSizer() + hsizer.Add(headerContentSizer, 0, 0, 0) + self.labelEMStatus = wx.StaticText(headerPanel, wx.ID_ANY, "") + headerContentSizer.Add(self.labelEMStatus) + headerPanel.GetParent().AddToggleItem(self.labelEMStatus) + + gridPrice = wx.GridSizer(1, 3) + contentSizer.Add(gridPrice, 0, wx.EXPAND | wx.ALL, 0) + for _type in ("ship", "fittings", "total"): + image = "%sPrice_big" % _type if _type != "ship" else "ship_big" + box = wx.BoxSizer(wx.HORIZONTAL) + gridPrice.Add(box, 0, wx.ALIGN_TOP) + + box.Add(BitmapLoader.getStaticBitmap(image, contentPanel, "gui"), 0, wx.ALIGN_CENTER) + + vbox = wx.BoxSizer(wx.VERTICAL) + box.Add(vbox, 1, wx.EXPAND) + + vbox.Add(wx.StaticText(contentPanel, wx.ID_ANY, _type.capitalize()), 0, wx.ALIGN_LEFT) + + hbox = wx.BoxSizer(wx.HORIZONTAL) + vbox.Add(hbox) + + lbl = wx.StaticText(contentPanel, wx.ID_ANY, "0.00 ISK") + setattr(self, "labelPrice%s" % _type.capitalize(), lbl) + hbox.Add(lbl, 0, wx.ALIGN_LEFT) + + def refreshPanel(self, fit): + if fit is not None: + self.fit = fit + + fit_items = Price.fitItemsList(fit) + + sPrice = Price.getInstance() + sPrice.getPrices(fit_items, self.processPrices) + self.labelEMStatus.SetLabel("Updating prices...") + + self.refreshPanelPrices(fit) + self.panel.Layout() + + def refreshPanelPrices(self, fit=None): + + ship_price = 0 + module_price = 0 + drone_price = 0 + fighter_price = 0 + cargo_price = 0 + booster_price = 0 + implant_price = 0 + + if fit: + ship_price = fit.ship.item.price.price + + if fit.modules: + for module in fit.modules: + if not module.isEmpty: + module_price += module.item.price.price + + if fit.drones: + for drone in fit.drones: + drone_price += drone.item.price.price * drone.amount + + if fit.fighters: + for fighter in fit.fighters: + fighter_price += fighter.item.price.price * fighter.amount + + if fit.cargo: + for cargo in fit.cargo: + cargo_price += cargo.item.price.price * cargo.amount + + if fit.boosters: + for booster in fit.boosters: + booster_price += booster.item.price.price + + if fit.implants: + for implant in fit.implants: + implant_price += implant.item.price.price + + fitting_price = module_price + drone_price + fighter_price + cargo_price + booster_price + implant_price + total_price = ship_price + fitting_price + + self.labelPriceShip.SetLabel("%s ISK" % formatAmount(ship_price, 3, 3, 9, currency=True)) + self.labelPriceShip.SetToolTip(wx.ToolTip('{:,.2f}'.format(ship_price))) + + self.labelPriceFittings.SetLabel("%s ISK" % formatAmount(fitting_price, 3, 3, 9, currency=True)) + self.labelPriceFittings.SetToolTip(wx.ToolTip('{:,.2f}'.format(fitting_price))) + + self.labelPriceTotal.SetLabel("%s ISK" % formatAmount(total_price, 3, 3, 9, currency=True)) + self.labelPriceTotal.SetToolTip(wx.ToolTip('{:,.2f}'.format(total_price))) + + def processPrices(self, prices): + self.refreshPanelPrices(self.fit) + + self.labelEMStatus.SetLabel("") + self.panel.Layout() + + +PriceViewMinimal.register() diff --git a/gui/builtinViewColumns/price.py b/gui/builtinViewColumns/price.py index 194aa594d..5d43dabdc 100644 --- a/gui/builtinViewColumns/price.py +++ b/gui/builtinViewColumns/price.py @@ -22,7 +22,7 @@ import wx from eos.saveddata.cargo import Cargo from eos.saveddata.drone import Drone -from service.market import Market +from service.price import Price as ServicePrice from gui.viewColumn import ViewColumn from gui.bitmapLoader import BitmapLoader from gui.utils.numberFormatter import formatAmount @@ -41,13 +41,15 @@ class Price(ViewColumn): if stuff.item is None or stuff.item.group.name == "Ship Modifiers": return "" - sMkt = Market.getInstance() - price = sMkt.getPriceNow(stuff.item.ID) + if hasattr(stuff, "isEmpty"): + if stuff.isEmpty: + return "" - if not price or not price.price or not price.isValid: - return False + sPrice = ServicePrice.getInstance() + price = sPrice.getPriceNow(stuff.item) - price = price.price # Set new price variable with what we need + if not price: + return "" if isinstance(stuff, Drone) or isinstance(stuff, Cargo): price *= stuff.amount @@ -55,10 +57,10 @@ class Price(ViewColumn): return formatAmount(price, 3, 3, 9, currency=True) def delayedText(self, mod, display, colItem): - sMkt = Market.getInstance() + sPrice = ServicePrice.getInstance() def callback(item): - price = sMkt.getPriceNow(item.ID) + price = sPrice.getPriceNow(item.ID) text = formatAmount(price.price, 3, 3, 9, currency=True) if price.price else "" if price.failed: text += " (!)" @@ -66,7 +68,7 @@ class Price(ViewColumn): display.SetItem(colItem) - sMkt.waitForPrice(mod.item, callback) + sPrice.getPrices([mod.item], callback, True) def getImageId(self, mod): return -1 diff --git a/gui/implantView.py b/gui/implantView.py index 86b9afd96..709528dde 100644 --- a/gui/implantView.py +++ b/gui/implantView.py @@ -79,10 +79,13 @@ class ImplantView(wx.Panel): class ImplantDisplay(d.Display): - DEFAULT_COLS = ["State", - "attr:implantness", - "Base Icon", - "Base Name"] + DEFAULT_COLS = [ + "State", + "attr:implantness", + "Base Icon", + "Base Name", + "Price", + ] def __init__(self, parent): d.Display.__init__(self, parent, style=wx.LC_SINGLE_SEL | wx.BORDER_NONE) diff --git a/gui/itemStats.py b/gui/itemStats.py index 3ac94ce0c..41226464c 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -43,6 +43,7 @@ from eos.saveddata.citadel import Citadel from eos.saveddata.fit import Fit from service.market import Market from service.attribute import Attribute +from service.price import Price as ServicePrice import gui.mainFrame from gui.bitmapLoader import BitmapLoader from gui.utils.numberFormatter import formatAmount @@ -623,7 +624,7 @@ class ItemCompare(wx.Panel): def processPrices(self, prices): for i, price in enumerate(prices): - self.paramList.SetStringItem(i, len(self.attrs) + 1, formatAmount(price.price, 3, 3, 9, currency=True)) + self.paramList.SetStringItem(i, len(self.attrs) + 1, formatAmount(price.value, 3, 3, 9, currency=True)) def PopulateList(self, sort=None): @@ -660,9 +661,6 @@ class ItemCompare(wx.Panel): self.paramList.InsertColumn(len(self.attrs) + 1, "Price") self.paramList.SetColumnWidth(len(self.attrs) + 1, 60) - sMkt = Market.getInstance() - sMkt.getPrices([x.ID for x in self.items], self.processPrices) - for item in self.items: i = self.paramList.InsertStringItem(sys.maxint, item.name) for x, attr in enumerate(self.attrs.keys()): @@ -678,6 +676,10 @@ class ItemCompare(wx.Panel): self.paramList.SetStringItem(i, x + 1, valueUnit) + # Add prices + sPrice = ServicePrice.getInstance() + self.paramList.SetStringItem(i, len(self.attrs) + 1, formatAmount(sPrice.getPriceNow(item), 3, 3, 9, currency=True)) + self.paramList.RefreshRows() self.Layout() diff --git a/gui/statsView.py b/gui/statsView.py index 464359695..dc4c0276b 100644 --- a/gui/statsView.py +++ b/gui/statsView.py @@ -52,6 +52,7 @@ from gui.builtinStatsViews import ( # noqa: E402, F401 rechargeViewFull, targetingMiscViewMinimal, priceViewFull, + priceViewMinimal, outgoingViewFull, outgoingViewMinimal, ) diff --git a/service/market.py b/service/market.py index 70dcd1e71..49f0d88d2 100644 --- a/service/market.py +++ b/service/market.py @@ -30,11 +30,10 @@ import config import eos.db from service import conversions from service.settings import SettingsProvider -from service.price import Price from eos.gamedata import Category as types_Category, Group as types_Group, Item as types_Item, MarketGroup as types_MarketGroup, \ MetaGroup as types_MetaGroup, MetaType as types_MetaType -from eos.saveddata.price import Price as types_Price + try: from collections import OrderedDict @@ -85,48 +84,6 @@ class ShipBrowserWorkerThread(threading.Thread): pyfalog.critical(e) -class PriceWorkerThread(threading.Thread): - def __init__(self): - threading.Thread.__init__(self) - self.name = "PriceWorker" - pyfalog.debug("Initialize PriceWorkerThread.") - - def run(self): - pyfalog.debug("Run start") - self.queue = Queue.Queue() - self.wait = {} - self.processUpdates() - pyfalog.debug("Run end") - - def processUpdates(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, itemID, callback): - if itemID not in self.wait: - self.wait[itemID] = [] - self.wait[itemID].append(callback) - - class SearchWorkerThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) @@ -180,7 +137,6 @@ class Market(object): instance = None def __init__(self): - self.priceCache = {} # Init recently used module storage serviceMarketRecentlyUsedModules = {"pyfaMarketRecentlyUsedModules": []} @@ -188,11 +144,6 @@ class Market(object): self.serviceMarketRecentlyUsedModules = SettingsProvider.getInstance().getSettings( "pyfaMarketRecentlyUsedModules", serviceMarketRecentlyUsedModules) - # Start price fetcher - self.priceWorkerThread = PriceWorkerThread() - self.priceWorkerThread.daemon = True - self.priceWorkerThread.start() - # Thread which handles search self.searchWorkerThread = SearchWorkerThread() self.searchWorkerThread.daemon = True @@ -855,60 +806,6 @@ class Market(object): filtered = set(filter(lambda item: self.getMetaGroupIdByItem(item) in metas, items)) return filtered - def getPriceNow(self, typeID): - """Get price for provided typeID""" - price = self.priceCache.get(typeID) - if price is None: - price = eos.db.getPrice(typeID) - if price is None: - price = types_Price(typeID) - eos.db.add(price) - - self.priceCache[typeID] = price - - return price - - def getPricesNow(self, typeIDs): - """Return map of calls to get price against list of typeIDs""" - return map(self.getPrice, typeIDs) - - def getPrices(self, typeIDs, callback): - """Get prices for multiple typeIDs""" - requests = [] - for typeID in typeIDs: - price = self.getPriceNow(typeID) - requests.append(price) - - def cb(): - try: - callback(requests) - except Exception as e: - pyfalog.critical("Callback failed.") - pyfalog.critical(e) - eos.db.commit() - - self.priceWorkerThread.trigger(requests, cb) - - def waitForPrice(self, item, callback): - """ - Wait for prices to be fetched and callback when finished. This is used with the column prices for modules. - Instead of calling them individually, we set them to wait until the entire fit price is called and calculated - (see GH #290) - """ - - def cb(): - try: - callback(item) - except Exception as e: - pyfalog.critical("Callback failed.") - pyfalog.critical(e) - - self.priceWorkerThread.setToWait(item.ID, cb) - - def clearPriceCache(self): - self.priceCache.clear() - eos.db.clearPrices() - def getSystemWideEffects(self): """ Get dictionary with system-wide effects diff --git a/service/price.py b/service/price.py index b2466c4c4..6572adea4 100644 --- a/service/price.py +++ b/service/price.py @@ -19,12 +19,17 @@ 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 logbook import Logger +from service.market import Market pyfalog = Logger(__name__) @@ -35,6 +40,8 @@ TIMEOUT = 15 * 60 # Network timeout delay for connection issues, 15 minutes class Price(object): + instance = None + systemsList = { "Jita": 30000142, "Amarr": 30002187, @@ -43,10 +50,17 @@ class Price(object): "Hek": 30002053 } + def __init__(self): + # Start price fetcher + self.priceWorkerThread = PriceWorkerThread() + self.priceWorkerThread.daemon = True + self.priceWorkerThread.start() + @classmethod - def invalidPrices(cls, prices): - for price in prices: - price.time = 0 + def getInstance(cls): + if cls.instance is None: + cls.instance = Price() + return cls.instance @classmethod def fetchPrices(cls, prices): @@ -113,6 +127,10 @@ class Price(object): priceobj.time = time.time() + VALIDITY priceobj.failed = None + # Update the DB. + db.add(priceobj) + db.commit() + # delete price from working dict del priceMap[typeID] @@ -124,6 +142,11 @@ class Price(object): priceobj = priceMap[typeID] priceobj.time = time.time() + TIMEOUT priceobj.failed = True + + # Update the DB. + db.add(priceobj) + db.commit() + del priceMap[typeID] except: # all other errors will pass and continue onward to the REREQUEST delay @@ -136,22 +159,102 @@ class Price(object): priceobj.time = time.time() + REREQUEST priceobj.failed = True + # Update the DB. + db.add(priceobj) + db.commit() + @classmethod def fitItemsList(cls, fit): # Compose a list of all the data we need & request it - typeIDs = [fit.ship.item.ID] + fit_items = [fit.ship.item] for mod in fit.modules: if not mod.isEmpty: - typeIDs.append(mod.itemID) + fit_items.append(mod.item) for drone in fit.drones: - typeIDs.append(drone.itemID) + fit_items.append(drone.item) for fighter in fit.fighters: - typeIDs.append(fighter.itemID) + fit_items.append(fighter.item) for cargo in fit.cargo: - typeIDs.append(cargo.itemID) + fit_items.append(cargo.item) - return typeIDs + 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) + + +class PriceWorkerThread(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + self.name = "PriceWorker" + pyfalog.debug("Initialize PriceWorkerThread.") + + def run(self): + pyfalog.debug("Run start") + self.queue = Queue.Queue() + self.wait = {} + self.processUpdates() + pyfalog.debug("Run end") + + def processUpdates(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, itemID, callback): + if itemID not in self.wait: + self.wait[itemID] = [] + self.wait[itemID].append(callback)