@@ -95,7 +95,7 @@ def defPaths(customSavePath=None):
|
||||
global pyfaPath
|
||||
global savePath
|
||||
global saveDB
|
||||
global gameDB
|
||||
global gameDB
|
||||
global saveInRoot
|
||||
global logPath
|
||||
global cipher
|
||||
|
||||
@@ -41,8 +41,7 @@ items_table = Table("invtypes", gamedata_meta,
|
||||
Column("iconID", Integer),
|
||||
Column("graphicID", Integer),
|
||||
Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True),
|
||||
Column("replaceSame", String),
|
||||
Column("replaceBetter", String))
|
||||
Column("replacements", String))
|
||||
|
||||
from .metaGroup import metatypes_table # noqa
|
||||
from .traits import traits_table # noqa
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -30,6 +30,7 @@ pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class Booster(HandledItem, ItemAttrShortcut):
|
||||
|
||||
def __init__(self, item):
|
||||
self.__item = item
|
||||
|
||||
@@ -147,3 +148,17 @@ class Booster(HandledItem, ItemAttrShortcut):
|
||||
copyEffect.active = sideEffect.active
|
||||
|
||||
return copy
|
||||
|
||||
def rebase(self, item):
|
||||
active = self.active
|
||||
sideEffectStates = {se.effectID: se.active for se in self.sideEffects}
|
||||
Booster.__init__(self, item)
|
||||
self.active = active
|
||||
for sideEffect in self.sideEffects:
|
||||
if sideEffect.effectID in sideEffectStates:
|
||||
sideEffect.active = sideEffectStates[sideEffect.effectID]
|
||||
|
||||
def __repr__(self):
|
||||
return "Booster(ID={}, name={}) at {}".format(
|
||||
self.item.ID, self.item.name, hex(id(self))
|
||||
)
|
||||
|
||||
@@ -89,3 +89,13 @@ class Cargo(HandledItem, ItemAttrShortcut):
|
||||
copy = Cargo(self.item)
|
||||
copy.amount = self.amount
|
||||
return copy
|
||||
|
||||
def rebase(self, item):
|
||||
amount = self.amount
|
||||
Cargo.__init__(self, item)
|
||||
self.amount = amount
|
||||
|
||||
def __repr__(self):
|
||||
return "Cargo(ID={}, name={}) at {}".format(
|
||||
self.item.ID, self.item.name, hex(id(self))
|
||||
)
|
||||
|
||||
@@ -296,6 +296,13 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
copy.amountActive = self.amountActive
|
||||
return copy
|
||||
|
||||
def rebase(self, item):
|
||||
amount = self.amount
|
||||
amountActive = self.amountActive
|
||||
Drone.__init__(self, item)
|
||||
self.amount = amount
|
||||
self.amountActive = amountActive
|
||||
|
||||
def fits(self, fit):
|
||||
fitDroneGroupLimits = set()
|
||||
for i in range(1, 3):
|
||||
|
||||
@@ -355,6 +355,17 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
copyAbility.active = ability.active
|
||||
return copy
|
||||
|
||||
def rebase(self, item):
|
||||
amount = self.amount
|
||||
active = self.active
|
||||
abilityEffectStates = {a.effectID: a.active for a in self.abilities}
|
||||
Fighter.__init__(self, item)
|
||||
self.amount = amount
|
||||
self.active = active
|
||||
for ability in self.abilities:
|
||||
if ability.effectID in abilityEffectStates:
|
||||
ability.active = abilityEffectStates[ability.effectID]
|
||||
|
||||
def fits(self, fit):
|
||||
# If ships doesn't support this type of fighter, don't add it
|
||||
if fit.getNumSlots(self.slot) == 0:
|
||||
|
||||
@@ -115,6 +115,11 @@ class Implant(HandledItem, ItemAttrShortcut):
|
||||
copy.active = self.active
|
||||
return copy
|
||||
|
||||
def rebase(self, item):
|
||||
active = self.active
|
||||
Implant.__init__(self, item)
|
||||
self.active = active
|
||||
|
||||
def __repr__(self):
|
||||
return "Implant(ID={}, name={}) at {}".format(
|
||||
self.item.ID, self.item.name, hex(id(self))
|
||||
|
||||
@@ -941,6 +941,16 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
|
||||
return copy
|
||||
|
||||
def rebase(self, item):
|
||||
state = self.state
|
||||
charge = self.charge
|
||||
Module.__init__(self, item, self.baseItem, self.mutaplasmid)
|
||||
self.state = state
|
||||
if self.isValidCharge(charge):
|
||||
self.charge = charge
|
||||
for x in self.mutators.values():
|
||||
Mutator(self, x.attribute, x.value)
|
||||
|
||||
def __repr__(self):
|
||||
if self.item:
|
||||
return "Module(ID={}, name={}) at {}".format(
|
||||
|
||||
@@ -19,41 +19,56 @@
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
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
|
||||
def isValid(self, validityOverride=None):
|
||||
# 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 + (validityOverride if validityOverride is not None else 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
|
||||
|
||||
@@ -122,8 +122,8 @@ class FighterDisplay(d.Display):
|
||||
# "Max Range",
|
||||
# "Miscellanea",
|
||||
"attr:maxVelocity",
|
||||
"Fighter Abilities"
|
||||
# "Price",
|
||||
"Fighter Abilities",
|
||||
"Price",
|
||||
]
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
@@ -16,7 +16,7 @@ class ItemCompare(wx.Panel):
|
||||
def __init__(self, parent, stuff, item, items, context=None):
|
||||
# Start dealing with Price stuff to get that thread going
|
||||
sPrice = ServicePrice.getInstance()
|
||||
sPrice.getPrices(items, self.UpdateList)
|
||||
sPrice.getPrices(items, self.UpdateList, fetchTimeout=90)
|
||||
|
||||
wx.Panel.__init__(self, parent)
|
||||
mainSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
@@ -22,7 +22,7 @@ import wx
|
||||
from gui.statsView import StatsView
|
||||
from gui.bitmap_loader import BitmapLoader
|
||||
from gui.utils.numberFormatter import formatAmount
|
||||
from service.price import Price
|
||||
from service.price import Fit, Price
|
||||
from service.settings import PriceMenuSettings
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class PriceViewFull(StatsView):
|
||||
|
||||
gridPrice = wx.GridSizer(2, 3, 0, 0)
|
||||
contentSizer.Add(gridPrice, 0, wx.EXPAND | wx.ALL, 0)
|
||||
for _type in ("ship", "fittings", "total", "drones", "cargoBay", "character"):
|
||||
for _type in ("ship", "fittings", "character", "drones", "cargoBay", "total"):
|
||||
if _type in "ship":
|
||||
image = "ship_big"
|
||||
elif _type in ("fittings", "total"):
|
||||
@@ -79,11 +79,8 @@ class PriceViewFull(StatsView):
|
||||
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)
|
||||
fit_items = set(Fit.fitItemIter(fit))
|
||||
Price.getInstance().getPrices(fit_items, self.processPrices, fetchTimeout=30)
|
||||
self.labelEMStatus.SetLabel("Updating prices...")
|
||||
|
||||
self.refreshPanelPrices(fit)
|
||||
|
||||
@@ -22,7 +22,7 @@ import wx
|
||||
from gui.statsView import StatsView
|
||||
from gui.bitmap_loader import BitmapLoader
|
||||
from gui.utils.numberFormatter import formatAmount
|
||||
from service.price import Price
|
||||
from service.price import Fit, Price
|
||||
from service.settings import PriceMenuSettings
|
||||
|
||||
|
||||
@@ -73,11 +73,8 @@ class PriceViewMinimal(StatsView):
|
||||
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)
|
||||
fit_items = set(Fit.fitItemIter(fit))
|
||||
Price.getInstance().getPrices(fit_items, self.processPrices, fetchTimeout=30)
|
||||
self.labelEMStatus.SetLabel("Updating prices...")
|
||||
|
||||
self.refreshPanelPrices(fit)
|
||||
|
||||
@@ -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,20 @@ from gui.bitmap_loader import BitmapLoader
|
||||
from gui.utils.numberFormatter import formatAmount
|
||||
|
||||
|
||||
def formatPrice(stuff, priceObj):
|
||||
textItems = []
|
||||
if priceObj.price:
|
||||
mult = 1
|
||||
if isinstance(stuff, (Drone, Cargo)):
|
||||
mult = stuff.amount
|
||||
elif isinstance(stuff, Fighter):
|
||||
mult = stuff.amountActive
|
||||
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"
|
||||
|
||||
@@ -48,35 +63,21 @@ class Price(ViewColumn):
|
||||
|
||||
priceObj = stuff.item.price
|
||||
|
||||
if not priceObj.isValid:
|
||||
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)
|
||||
|
||||
sPrice.getPrices([mod.item], callback, True)
|
||||
sPrice.getPrices([mod.item], callback, waitforthread=True)
|
||||
|
||||
def getImageId(self, mod):
|
||||
return -1
|
||||
|
||||
@@ -26,6 +26,10 @@ import wx
|
||||
from service.port.eft import EFT_OPTIONS
|
||||
from service.port.multibuy import MULTIBUY_OPTIONS
|
||||
from service.settings import SettingsProvider
|
||||
from service.port import EfsPort, Port
|
||||
from service.const import PortMultiBuyOptions
|
||||
from eos.db import getFit
|
||||
from gui.utils.clipboard import toClipboard
|
||||
|
||||
|
||||
class CopySelectDialog(wx.Dialog):
|
||||
@@ -39,6 +43,17 @@ class CopySelectDialog(wx.Dialog):
|
||||
def __init__(self, parent):
|
||||
wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="Select a format", size=(-1, -1),
|
||||
style=wx.DEFAULT_DIALOG_STYLE)
|
||||
|
||||
self.CopySelectDict = {
|
||||
CopySelectDialog.copyFormatEft : self.exportEft,
|
||||
CopySelectDialog.copyFormatXml : self.exportXml,
|
||||
CopySelectDialog.copyFormatDna : self.exportDna,
|
||||
CopySelectDialog.copyFormatEsi : self.exportEsi,
|
||||
CopySelectDialog.copyFormatMultiBuy: self.exportMultiBuy,
|
||||
CopySelectDialog.copyFormatEfs : self.exportEfs
|
||||
}
|
||||
|
||||
self.mainFrame = parent
|
||||
mainSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
self.copyFormats = OrderedDict((
|
||||
@@ -87,8 +102,8 @@ class CopySelectDialog(wx.Dialog):
|
||||
self.options[formatId][optId] = checkbox
|
||||
if self.settings['options'].get(formatId, {}).get(optId, defaultFormatOptions.get(formatId, {}).get(optId)):
|
||||
checkbox.SetValue(True)
|
||||
bsizer.Add(checkbox, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 3)
|
||||
mainSizer.Add(bsizer, 1, wx.EXPAND | wx.LEFT, 20)
|
||||
bsizer.Add(checkbox, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 3)
|
||||
mainSizer.Add(bsizer, 0, wx.EXPAND | wx.LEFT, 20)
|
||||
|
||||
buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
|
||||
if buttonSizer:
|
||||
@@ -99,6 +114,31 @@ class CopySelectDialog(wx.Dialog):
|
||||
self.Fit()
|
||||
self.Center()
|
||||
|
||||
def Validate(self):
|
||||
# Since this dialog is shown through aa ShowModal(), we hook into the Validate function to veto the closing of the dialog until we're ready.
|
||||
# This always returns False, and when we're ready will EndModal()
|
||||
selected = self.GetSelected()
|
||||
options = self.GetOptions()
|
||||
|
||||
settings = SettingsProvider.getInstance().getSettings("pyfaExport")
|
||||
settings["format"] = selected
|
||||
settings["options"] = options
|
||||
self.waitDialog = None
|
||||
|
||||
def cb(text):
|
||||
if self.waitDialog:
|
||||
del self.waitDialog
|
||||
toClipboard(text)
|
||||
self.EndModal(wx.ID_OK)
|
||||
|
||||
export_options = options.get(selected)
|
||||
if selected == CopySelectDialog.copyFormatMultiBuy and export_options.get(PortMultiBuyOptions.OPTIMIZE_PRICES, False):
|
||||
self.waitDialog = wx.BusyInfo("Optimizing Prices", parent=self)
|
||||
|
||||
self.CopySelectDict[selected](export_options, callback=cb)
|
||||
|
||||
return False
|
||||
|
||||
def Selected(self, event):
|
||||
obj = event.GetEventObject()
|
||||
formatName = obj.GetLabel()
|
||||
@@ -119,3 +159,27 @@ class CopySelectDialog(wx.Dialog):
|
||||
for formatId in self.options:
|
||||
options[formatId] = {optId: ch.IsChecked() for optId, ch in self.options[formatId].items()}
|
||||
return options
|
||||
|
||||
def exportEft(self, options, callback):
|
||||
fit = getFit(self.mainFrame.getActiveFit())
|
||||
Port.exportEft(fit, options, callback)
|
||||
|
||||
def exportDna(self, options, callback):
|
||||
fit = getFit(self.mainFrame.getActiveFit())
|
||||
Port.exportDna(fit, callback)
|
||||
|
||||
def exportEsi(self, options, callback):
|
||||
fit = getFit(self.mainFrame.getActiveFit())
|
||||
Port.exportESI(fit, callback)
|
||||
|
||||
def exportXml(self, options, callback):
|
||||
fit = getFit(self.mainFrame.getActiveFit())
|
||||
Port.exportXml(None, fit, callback)
|
||||
|
||||
def exportMultiBuy(self, options, callback):
|
||||
fit = getFit(self.mainFrame.getActiveFit())
|
||||
Port.exportMultiBuy(fit, options, callback)
|
||||
|
||||
def exportEfs(self, options, callback):
|
||||
fit = getFit(self.mainFrame.getActiveFit())
|
||||
EfsPort.exportEfs(fit, 0, callback)
|
||||
@@ -36,3 +36,4 @@ from .guiFitRename import GuiFitRenameCommand
|
||||
from .guiChangeImplantLocation import GuiChangeImplantLocation
|
||||
from .guiImportMutatedModule import GuiImportMutatedModuleCommand
|
||||
from .guiSetSpoolup import GuiSetSpoolup
|
||||
from .guiRebaseItems import GuiRebaseItemsCommand
|
||||
|
||||
31
gui/fitCommands/calc/fitRebaseItem.py
Normal file
31
gui/fitCommands/calc/fitRebaseItem.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import wx
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
import eos.db
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitRebaseItemCommand(wx.Command):
|
||||
|
||||
def __init__(self, fitID, containerName, position, newTypeID):
|
||||
wx.Command.__init__(self, True, "Rebase Item")
|
||||
self.fitID = fitID
|
||||
self.containerName = containerName
|
||||
self.position = position
|
||||
self.newTypeID = newTypeID
|
||||
self.oldTypeID = None
|
||||
|
||||
def Do(self):
|
||||
fit = eos.db.getFit(self.fitID)
|
||||
obj = getattr(fit, self.containerName)[self.position]
|
||||
self.oldTypeID = getattr(obj.item, "ID", None)
|
||||
newItem = eos.db.getItem(self.newTypeID)
|
||||
obj.rebase(newItem)
|
||||
return True
|
||||
|
||||
def Undo(self):
|
||||
cmd = FitRebaseItemCommand(self.fitID, self.containerName, self.position, self.oldTypeID)
|
||||
return cmd.Do()
|
||||
@@ -8,7 +8,7 @@ from .calc.fitAddDrone import FitAddDroneCommand
|
||||
|
||||
class GuiAddDroneCommand(wx.Command):
|
||||
def __init__(self, fitID, itemID):
|
||||
wx.Command.__init__(self, True, "Cargo Add")
|
||||
wx.Command.__init__(self, True, "Drone Add")
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.sFit = Fit.getInstance()
|
||||
self.internal_history = wx.CommandProcessor()
|
||||
|
||||
@@ -8,7 +8,7 @@ from .calc.fitAddFighter import FitAddFighterCommand
|
||||
|
||||
class GuiAddFighterCommand(wx.Command):
|
||||
def __init__(self, fitID, itemID):
|
||||
wx.Command.__init__(self, True, "Cargo Add")
|
||||
wx.Command.__init__(self, True, "Fighter Add")
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.sFit = Fit.getInstance()
|
||||
self.internal_history = wx.CommandProcessor()
|
||||
|
||||
@@ -21,7 +21,7 @@ class GuiCargoToModuleCommand(wx.Command):
|
||||
"""
|
||||
|
||||
def __init__(self, fitID, moduleIdx, cargoIdx, copy=False):
|
||||
wx.Command.__init__(self, True, "Module State Change")
|
||||
wx.Command.__init__(self, True, "Cargo to Module")
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.sFit = Fit.getInstance()
|
||||
self.fitID = fitID
|
||||
|
||||
@@ -17,7 +17,7 @@ class GuiFillWithModuleCommand(wx.Command):
|
||||
set the charge on the underlying module (requires position)
|
||||
:param position: Optional. The position in fit.modules that we are attempting to set the item to
|
||||
"""
|
||||
wx.Command.__init__(self, True, "Module Add: {}".format(itemID))
|
||||
wx.Command.__init__(self, True, "Module Fill: {}".format(itemID))
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.sFit = Fit.getInstance()
|
||||
self.fitID = fitID
|
||||
|
||||
@@ -14,7 +14,7 @@ pyfalog = Logger(__name__)
|
||||
|
||||
class GuiModuleToCargoCommand(wx.Command):
|
||||
def __init__(self, fitID, moduleIdx, cargoIdx, copy=False):
|
||||
wx.Command.__init__(self, True, "Module State Change")
|
||||
wx.Command.__init__(self, True, "Module to Cargo")
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.sFit = Fit.getInstance()
|
||||
self.fitID = fitID
|
||||
|
||||
46
gui/fitCommands/guiRebaseItems.py
Normal file
46
gui/fitCommands/guiRebaseItems.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import wx
|
||||
|
||||
import eos.db
|
||||
import gui.mainFrame
|
||||
from gui import globalEvents as GE
|
||||
from service.fit import Fit
|
||||
from .calc.fitRebaseItem import FitRebaseItemCommand
|
||||
from .calc.fitSetCharge import FitSetChargeCommand
|
||||
|
||||
|
||||
class GuiRebaseItemsCommand(wx.Command):
|
||||
|
||||
def __init__(self, fitID, rebaseMap):
|
||||
wx.Command.__init__(self, True, "Mass Rebase Item")
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.fitID = fitID
|
||||
self.rebaseMap = rebaseMap
|
||||
self.internal_history = wx.CommandProcessor()
|
||||
|
||||
def Do(self):
|
||||
fit = eos.db.getFit(self.fitID)
|
||||
for mod in fit.modules:
|
||||
if mod.item is not None and mod.item.ID in self.rebaseMap:
|
||||
self.internal_history.Submit(FitRebaseItemCommand(self.fitID, "modules", mod.modPosition, self.rebaseMap[mod.item.ID]))
|
||||
if mod.charge is not None and mod.charge.ID in self.rebaseMap:
|
||||
self.internal_history.Submit(FitSetChargeCommand(self.fitID, [mod.modPosition], self.rebaseMap[mod.charge.ID]))
|
||||
for containerName in ("drones", "fighters", "implants", "boosters", "cargo"):
|
||||
container = getattr(fit, containerName)
|
||||
for obj in container:
|
||||
if obj.item is not None and obj.item.ID in self.rebaseMap:
|
||||
self.internal_history.Submit(FitRebaseItemCommand(self.fitID, containerName, container.index(obj), self.rebaseMap[obj.item.ID]))
|
||||
if self.internal_history.Commands:
|
||||
eos.db.commit()
|
||||
Fit.getInstance().recalc(self.fitID)
|
||||
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID))
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def Undo(self):
|
||||
for _ in self.internal_history.Commands:
|
||||
self.internal_history.Undo()
|
||||
eos.db.commit()
|
||||
Fit.getInstance().recalc(self.fitID)
|
||||
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID))
|
||||
return True
|
||||
@@ -8,7 +8,7 @@ from .calc.fitRemoveCargo import FitRemoveCargoCommand
|
||||
|
||||
class GuiRemoveCargoCommand(wx.Command):
|
||||
def __init__(self, fitID, itemID):
|
||||
wx.Command.__init__(self, True, "Module Charge Add")
|
||||
wx.Command.__init__(self, True, "Cargo Remove")
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.sFit = Fit.getInstance()
|
||||
self.internal_history = wx.CommandProcessor()
|
||||
|
||||
@@ -8,7 +8,7 @@ from .calc.fitRemoveDrone import FitRemoveDroneCommand
|
||||
|
||||
class GuiRemoveDroneCommand(wx.Command):
|
||||
def __init__(self, fitID, position, amount=1):
|
||||
wx.Command.__init__(self, True, "Cargo Add")
|
||||
wx.Command.__init__(self, True, "Drone Remove")
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.sFit = Fit.getInstance()
|
||||
self.internal_history = wx.CommandProcessor()
|
||||
|
||||
@@ -8,7 +8,7 @@ from .calc.fitRemoveFighter import FitRemoveFighterCommand
|
||||
|
||||
class GuiRemoveFighterCommand(wx.Command):
|
||||
def __init__(self, fitID, position):
|
||||
wx.Command.__init__(self, True, "Module Remove")
|
||||
wx.Command.__init__(self, True, "Fighter Remove")
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.sFit = Fit.getInstance()
|
||||
self.fitID = fitID
|
||||
|
||||
@@ -27,7 +27,7 @@ class GuiRemoveProjectedCommand(wx.Command):
|
||||
}
|
||||
|
||||
def __init__(self, fitID, thing):
|
||||
wx.Command.__init__(self, True, "Projected Add")
|
||||
wx.Command.__init__(self, True, "Projected Remove")
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.sFit = Fit.getInstance()
|
||||
self.internal_history = wx.CommandProcessor()
|
||||
|
||||
@@ -8,7 +8,7 @@ from .calc.fitSetMode import FitSetModeCommand
|
||||
|
||||
class GuiSetModeCommand(wx.Command):
|
||||
def __init__(self, fitID, mode):
|
||||
wx.Command.__init__(self, True, "Cargo Add")
|
||||
wx.Command.__init__(self, True, "Mode Set")
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
self.sFit = Fit.getInstance()
|
||||
self.internal_history = wx.CommandProcessor()
|
||||
|
||||
@@ -37,7 +37,6 @@ import config
|
||||
import gui.globalEvents as GE
|
||||
from eos.config import gamedata_date, gamedata_version
|
||||
from eos.db.saveddata.loadDefaultDatabaseValues import DefaultDatabaseValues
|
||||
from eos.db.saveddata.queries import getFit as db_getFit
|
||||
# import this to access override setting
|
||||
from eos.modifiedAttributeDict import ModifiedAttributeDict
|
||||
from gui import graphFrame
|
||||
@@ -64,11 +63,12 @@ from gui.setEditor import ImplantSetEditorDlg
|
||||
from gui.shipBrowser import ShipBrowser
|
||||
from gui.statsPane import StatsPane
|
||||
from gui.updateDialog import UpdateDialog
|
||||
from gui.utils.clipboard import fromClipboard, toClipboard
|
||||
from gui.utils.clipboard import fromClipboard
|
||||
from service.character import Character
|
||||
from service.esi import Esi
|
||||
from service.fit import Fit
|
||||
from service.port import EfsPort, IPortUser, Port
|
||||
from service.port import IPortUser, Port
|
||||
from service.price import Price
|
||||
from service.settings import HTMLExportSettings, SettingsProvider
|
||||
from service.update import Update
|
||||
import gui.fitCommands as cmd
|
||||
@@ -508,6 +508,8 @@ class MainFrame(wx.Frame):
|
||||
self.Bind(wx.EVT_MENU, self.saveCharAs, id=menuBar.saveCharAsId)
|
||||
# Save current character
|
||||
self.Bind(wx.EVT_MENU, self.revertChar, id=menuBar.revertCharId)
|
||||
# Optimize fit price
|
||||
self.Bind(wx.EVT_MENU, self.optimizeFitPrice, id=menuBar.optimizeFitPrice)
|
||||
|
||||
# Browse fittings
|
||||
self.Bind(wx.EVT_MENU, self.eveFittings, id=menuBar.eveFittingsId)
|
||||
@@ -655,6 +657,23 @@ class MainFrame(wx.Frame):
|
||||
sChr.revertCharacter(charID)
|
||||
wx.PostEvent(self, GE.CharListUpdated())
|
||||
|
||||
def optimizeFitPrice(self, event):
|
||||
fitID = self.getActiveFit()
|
||||
sFit = Fit.getInstance()
|
||||
fit = sFit.getFit(fitID)
|
||||
|
||||
if fit:
|
||||
def updateFitCb(replacementsCheaper):
|
||||
del self.waitDialog
|
||||
del self.disablerAll
|
||||
rebaseMap = {k.ID: v.ID for k, v in replacementsCheaper.items()}
|
||||
self.command.Submit(cmd.GuiRebaseItemsCommand(fitID, rebaseMap))
|
||||
|
||||
fitItems = {i for i in Fit.fitItemIter(fit) if i is not fit.ship.item}
|
||||
self.disablerAll = wx.WindowDisabler()
|
||||
self.waitDialog = wx.BusyInfo("Please Wait...", parent=self)
|
||||
Price.getInstance().findCheaperReplacements(fitItems, updateFitCb, fetchTimeout=10)
|
||||
|
||||
def AdditionsTabSelect(self, event):
|
||||
selTab = self.additionsSelect.index(event.GetId())
|
||||
|
||||
@@ -688,30 +707,6 @@ class MainFrame(wx.Frame):
|
||||
else:
|
||||
self.marketBrowser.search.Focus()
|
||||
|
||||
def clipboardEft(self, options):
|
||||
fit = db_getFit(self.getActiveFit())
|
||||
toClipboard(Port.exportEft(fit, options))
|
||||
|
||||
def clipboardDna(self, options):
|
||||
fit = db_getFit(self.getActiveFit())
|
||||
toClipboard(Port.exportDna(fit))
|
||||
|
||||
def clipboardEsi(self, options):
|
||||
fit = db_getFit(self.getActiveFit())
|
||||
toClipboard(Port.exportESI(fit))
|
||||
|
||||
def clipboardXml(self, options):
|
||||
fit = db_getFit(self.getActiveFit())
|
||||
toClipboard(Port.exportXml(None, fit))
|
||||
|
||||
def clipboardMultiBuy(self, options):
|
||||
fit = db_getFit(self.getActiveFit())
|
||||
toClipboard(Port.exportMultiBuy(fit, options))
|
||||
|
||||
def clipboardEfs(self, options):
|
||||
fit = db_getFit(self.getActiveFit())
|
||||
toClipboard(EfsPort.exportEfs(fit, 0))
|
||||
|
||||
def importFromClipboard(self, event):
|
||||
clipboard = fromClipboard()
|
||||
activeFit = self.getActiveFit()
|
||||
@@ -728,28 +723,8 @@ class MainFrame(wx.Frame):
|
||||
self._openAfterImport(importData)
|
||||
|
||||
def exportToClipboard(self, event):
|
||||
CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft,
|
||||
CopySelectDialog.copyFormatXml: self.clipboardXml,
|
||||
CopySelectDialog.copyFormatDna: self.clipboardDna,
|
||||
CopySelectDialog.copyFormatEsi: self.clipboardEsi,
|
||||
CopySelectDialog.copyFormatMultiBuy: self.clipboardMultiBuy,
|
||||
CopySelectDialog.copyFormatEfs: self.clipboardEfs}
|
||||
dlg = CopySelectDialog(self)
|
||||
btnPressed = dlg.ShowModal()
|
||||
|
||||
if btnPressed == wx.ID_OK:
|
||||
selected = dlg.GetSelected()
|
||||
options = dlg.GetOptions()
|
||||
|
||||
settings = SettingsProvider.getInstance().getSettings("pyfaExport")
|
||||
settings["format"] = selected
|
||||
settings["options"] = options
|
||||
CopySelectDict[selected](options.get(selected))
|
||||
|
||||
try:
|
||||
dlg.Destroy()
|
||||
except RuntimeError:
|
||||
pyfalog.error("Tried to destroy an object that doesn't exist in <exportToClipboard>.")
|
||||
with CopySelectDialog(self) as dlg:
|
||||
dlg.ShowModal()
|
||||
|
||||
def exportSkillsNeeded(self, event):
|
||||
""" Exports skills needed for active fit and active character """
|
||||
|
||||
@@ -28,8 +28,6 @@ import gui.globalEvents as GE
|
||||
from gui.bitmap_loader import BitmapLoader
|
||||
|
||||
from logbook import Logger
|
||||
# from service.crest import Crest
|
||||
# from service.crest import CrestModes
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
@@ -59,6 +57,7 @@ class MainMenuBar(wx.MenuBar):
|
||||
self.importDatabaseDefaultsId = wx.NewId()
|
||||
self.toggleIgnoreRestrictionID = wx.NewId()
|
||||
self.devToolsId = wx.NewId()
|
||||
self.optimizeFitPrice = wx.NewId()
|
||||
|
||||
# pheonix: evaluate if this is needed
|
||||
if 'wxMac' in wx.PlatformInfo and wx.VERSION >= (3, 0):
|
||||
@@ -101,6 +100,9 @@ class MainMenuBar(wx.MenuBar):
|
||||
editMenu.Append(self.revertCharId, "Revert Character")
|
||||
editMenu.AppendSeparator()
|
||||
self.ignoreRestrictionItem = editMenu.Append(self.toggleIgnoreRestrictionID, "Ignore Fitting Restrictions")
|
||||
editMenu.AppendSeparator()
|
||||
editMenu.Append(self.optimizeFitPrice, "Optimize Fit Price")
|
||||
|
||||
|
||||
# Character menu
|
||||
windowMenu = wx.Menu()
|
||||
@@ -134,8 +136,6 @@ class MainMenuBar(wx.MenuBar):
|
||||
preferencesItem.SetBitmap(BitmapLoader.getBitmap("preferences_small", "gui"))
|
||||
windowMenu.Append(preferencesItem)
|
||||
|
||||
# self.sEsi = Crest.getInstance()
|
||||
|
||||
# CREST Menu
|
||||
esiMMenu = wx.Menu()
|
||||
self.Append(esiMMenu, "EVE &SSO")
|
||||
|
||||
@@ -178,32 +178,17 @@ def main(db, json_path):
|
||||
|
||||
def fillReplacements(tables):
|
||||
|
||||
def compareAttrs(attrs1, attrs2, attrHig):
|
||||
"""
|
||||
Compares received attribute sets. Returns:
|
||||
- 0 if sets are different
|
||||
- 1 if sets are exactly the same
|
||||
- 2 if first set is strictly better
|
||||
- 3 if second set is strictly better
|
||||
"""
|
||||
def compareAttrs(attrs1, attrs2):
|
||||
# Consider items as different if they have no attrs
|
||||
if len(attrs1) == 0 and len(attrs2) == 0:
|
||||
return False
|
||||
if set(attrs1) != set(attrs2):
|
||||
return 0
|
||||
return False
|
||||
if all(attrs1[aid] == attrs2[aid] for aid in attrs1):
|
||||
return 1
|
||||
if all(
|
||||
(attrs1[aid] >= attrs2[aid] and attrHig[aid]) or
|
||||
(attrs1[aid] <= attrs2[aid] and not attrHig[aid])
|
||||
for aid in attrs1
|
||||
):
|
||||
return 2
|
||||
if all(
|
||||
(attrs2[aid] >= attrs1[aid] and attrHig[aid]) or
|
||||
(attrs2[aid] <= attrs1[aid] and not attrHig[aid])
|
||||
for aid in attrs1
|
||||
):
|
||||
return 3
|
||||
return 0
|
||||
return True
|
||||
return False
|
||||
|
||||
print('finding replacements')
|
||||
skillReqAttribs = {
|
||||
182: 277,
|
||||
183: 278,
|
||||
@@ -213,10 +198,17 @@ def main(db, json_path):
|
||||
1290: 1288}
|
||||
skillReqAttribsFlat = set(skillReqAttribs.keys()).union(skillReqAttribs.values())
|
||||
# Get data on type groups
|
||||
# Format: {type ID: group ID}
|
||||
typesGroups = {}
|
||||
for row in tables['evetypes']:
|
||||
typesGroups[row['typeID']] = row['groupID']
|
||||
# Get data on item effects
|
||||
# Format: {type ID: set(effect, IDs)}
|
||||
typesEffects = {}
|
||||
for row in tables['dgmtypeeffects']:
|
||||
typesEffects.setdefault(row['typeID'], set()).add(row['effectID'])
|
||||
# Get data on type attributes
|
||||
# Format: {type ID: {attribute ID: attribute value}}
|
||||
typesNormalAttribs = {}
|
||||
typesSkillAttribs = {}
|
||||
for row in tables['dgmtypeattribs']:
|
||||
@@ -226,15 +218,23 @@ def main(db, json_path):
|
||||
typeSkillAttribs[row['attributeID']] = row['value']
|
||||
# Ignore these attributes for comparison purposes
|
||||
elif attributeID in (
|
||||
# We do not need mass as it affects final ship stats only when carried by ship itself
|
||||
# (and we're not going to replace ships), but it's wildly inconsistent for other items,
|
||||
# which otherwise would be the same
|
||||
4, # mass
|
||||
124, # mainColor
|
||||
162, # radius
|
||||
422, # techLevel
|
||||
633, # metaLevel
|
||||
1692 # metaGroupID
|
||||
1692, # metaGroupID
|
||||
1768 # typeColorScheme
|
||||
):
|
||||
continue
|
||||
else:
|
||||
typeNormalAttribs = typesNormalAttribs.setdefault(row['typeID'], {})
|
||||
typeNormalAttribs[row['attributeID']] = row['value']
|
||||
# Get data on skill requirements
|
||||
# Format: {type ID: {skill type ID: skill level}}
|
||||
typesSkillReqs = {}
|
||||
for typeID, typeAttribs in typesSkillAttribs.items():
|
||||
typeSkillAttribs = typesSkillAttribs.get(typeID, {})
|
||||
@@ -248,46 +248,54 @@ def main(db, json_path):
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
typeSkillReqs[skillType] = skillLevel
|
||||
# Get data on attribute highIsGood flag
|
||||
attrHig = {}
|
||||
for row in tables['dgmattribs']:
|
||||
attrHig[row['attributeID']] = bool(row['highIsGood'])
|
||||
# Format: {group ID: category ID}
|
||||
groupCategories = {}
|
||||
for row in tables['evegroups']:
|
||||
groupCategories[row['groupID']] = row['categoryID']
|
||||
# As EVE affects various types mostly depending on their group or skill requirements,
|
||||
# we're going to group various types up this way
|
||||
# Format: {(group ID, frozenset(skillreq, type, IDs), frozenset(type, effect, IDs): [type ID, {attribute ID: attribute value}]}
|
||||
groupedData = {}
|
||||
for row in tables['evetypes']:
|
||||
typeID = row['typeID']
|
||||
# Ignore items outside of categories we need
|
||||
if groupCategories[typesGroups[typeID]] not in (
|
||||
6, # Ship
|
||||
7, # Module
|
||||
8, # Charge
|
||||
18, # Drone
|
||||
20, # Implant
|
||||
22, # Deployable
|
||||
23, # Starbase
|
||||
32, # Subsystem
|
||||
35, # Decryptors
|
||||
65, # Structure
|
||||
66, # Structure Module
|
||||
87, # Fighter
|
||||
):
|
||||
continue
|
||||
typeAttribs = typesNormalAttribs.get(typeID, {})
|
||||
# Ignore stuff w/o attributes
|
||||
# Ignore items w/o attributes
|
||||
if not typeAttribs:
|
||||
continue
|
||||
# We need only skill types, not levels for keys
|
||||
typeSkillreqs = frozenset(typesSkillReqs.get(typeID, {}))
|
||||
typeGroup = typesGroups[typeID]
|
||||
groupData = groupedData.setdefault((typeGroup, typeSkillreqs), [])
|
||||
typeEffects = frozenset(typesEffects.get(typeID, ()))
|
||||
groupData = groupedData.setdefault((typeGroup, typeSkillreqs, typeEffects), [])
|
||||
groupData.append((typeID, typeAttribs))
|
||||
same = {}
|
||||
better = {}
|
||||
# Now, go through composed groups and for every item within it find items which are
|
||||
# the same and which are better
|
||||
# Format: {type ID: set(type IDs)}
|
||||
replacements = {}
|
||||
# Now, go through composed groups and for every item within it
|
||||
# find items which are the same
|
||||
for groupData in groupedData.values():
|
||||
for type1, type2 in itertools.combinations(groupData, 2):
|
||||
comparisonResult = compareAttrs(type1[1], type2[1], attrHig)
|
||||
# Equal
|
||||
if comparisonResult == 1:
|
||||
same.setdefault(type1[0], set()).add(type2[0])
|
||||
same.setdefault(type2[0], set()).add(type1[0])
|
||||
# First is better
|
||||
elif comparisonResult == 2:
|
||||
better.setdefault(type2[0], set()).add(type1[0])
|
||||
# Second is better
|
||||
elif comparisonResult == 3:
|
||||
better.setdefault(type1[0], set()).add(type2[0])
|
||||
if compareAttrs(type1[1], type2[1]):
|
||||
replacements.setdefault(type1[0], set()).add(type2[0])
|
||||
replacements.setdefault(type2[0], set()).add(type1[0])
|
||||
# Put this data into types table so that normal process hooks it up
|
||||
for row in tables['evetypes']:
|
||||
typeID = row['typeID']
|
||||
row['replaceSame'] = ','.join('{}'.format(tid) for tid in sorted(same.get(typeID, ())))
|
||||
row['replaceBetter'] = ','.join('{}'.format(tid) for tid in sorted(better.get(typeID, ())))
|
||||
row['replacements'] = ','.join('{}'.format(tid) for tid in sorted(replacements.get(row['typeID'], ())))
|
||||
|
||||
data = {}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ class PortMultiBuyOptions(IntEnum):
|
||||
IMPLANTS = 1
|
||||
CARGO = 2
|
||||
LOADED_CHARGES = 3
|
||||
OPTIMIZE_PRICES = 4
|
||||
|
||||
|
||||
@unique
|
||||
@@ -99,4 +100,4 @@ class GuiAttrGroup(IntEnum):
|
||||
ON_DEATH = auto()
|
||||
JUMP_SYSTEMS = auto()
|
||||
PROPULSIONS = auto()
|
||||
FIGHTERS = auto()
|
||||
FIGHTERS = auto()
|
||||
|
||||
@@ -555,9 +555,26 @@ class Fit(FitDeprecated):
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
# If any state was changed, recalculate attributes again
|
||||
# if changed:
|
||||
# self.recalc(fit)
|
||||
|
||||
@classmethod
|
||||
def fitObjectIter(cls, fit):
|
||||
yield fit.ship
|
||||
|
||||
for mod in fit.modules:
|
||||
if not mod.isEmpty:
|
||||
yield mod
|
||||
|
||||
for container in (fit.drones, fit.fighters, fit.implants, fit.boosters, fit.cargo):
|
||||
for obj in container:
|
||||
yield obj
|
||||
|
||||
@classmethod
|
||||
def fitItemIter(cls, fit):
|
||||
for fitobj in cls.fitObjectIter(fit):
|
||||
yield fitobj.item
|
||||
charge = getattr(fitobj, 'charge', None)
|
||||
if charge:
|
||||
yield charge
|
||||
|
||||
def refreshFit(self, fitID):
|
||||
pyfalog.debug("Refresh fit for fit ID: {0}", fitID)
|
||||
|
||||
@@ -795,3 +795,20 @@ class Market(object):
|
||||
"""Filter items by meta lvl"""
|
||||
filtered = set([item for item in items if self.getMetaGroupIdByItem(item) in metas])
|
||||
return filtered
|
||||
|
||||
def getReplacements(self, identity):
|
||||
item = self.getItem(identity)
|
||||
# We already store needed type IDs in database
|
||||
replTypeIDs = {int(i) for i in item.replacements.split(",") if i}
|
||||
if not replTypeIDs:
|
||||
return ()
|
||||
# As replacements were generated without keeping track which items were published,
|
||||
# filter them out here
|
||||
items = []
|
||||
for typeID in replTypeIDs:
|
||||
item = self.getItem(typeID)
|
||||
if not item:
|
||||
continue
|
||||
if self.getPublicityByItem(item):
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
@@ -17,30 +17,37 @@
|
||||
# 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):
|
||||
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)
|
||||
def __init__(self, priceMap, system, fetchTimeout):
|
||||
# Try selected system first
|
||||
self.fetchPrices(priceMap, max(2 * fetchTimeout / 3, 2), system)
|
||||
# If price was not available - try globally
|
||||
if priceMap:
|
||||
self.fetchPrices(priceMap, max(fetchTimeout / 3, 2))
|
||||
|
||||
@staticmethod
|
||||
def fetchPrices(priceMap, fetchTimeout, system=None):
|
||||
params = {"type_ids": ','.join(str(typeID) for typeID in priceMap)}
|
||||
if system is not None:
|
||||
params["system_id"] = system
|
||||
baseurl = "https://eve-marketdata.com/api/item_prices.xml"
|
||||
network = Network.getInstance()
|
||||
data = network.request(baseurl, network.PRICES, params=data)
|
||||
data = network.request(baseurl, network.PRICES, params=params, timeout=fetchTimeout)
|
||||
xml = minidom.parseString(data.text)
|
||||
types = xml.getElementsByTagName("eve").item(0).getElementsByTagName("price")
|
||||
|
||||
@@ -55,19 +62,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,37 @@
|
||||
# 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):
|
||||
data = {}
|
||||
def __init__(self, priceMap, system, fetchTimeout):
|
||||
# Try selected system first
|
||||
self.fetchPrices(priceMap, max(2 * fetchTimeout / 3, 2), system)
|
||||
# If price was not available - try globally
|
||||
if priceMap:
|
||||
self.fetchPrices(priceMap, max(fetchTimeout / 3, 2))
|
||||
|
||||
@staticmethod
|
||||
def fetchPrices(priceMap, fetchTimeout, system=None):
|
||||
params = {"typeid": {typeID for typeID in priceMap}}
|
||||
if system is not None:
|
||||
params["usesystem"] = system
|
||||
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)
|
||||
|
||||
network = Network.getInstance()
|
||||
data = network.request(baseurl, network.PRICES, params=data)
|
||||
data = network.request(baseurl, network.PRICES, params=params, timeout=fetchTimeout)
|
||||
xml = minidom.parseString(data.text)
|
||||
types = xml.getElementsByTagName("marketstat").item(0).getElementsByTagName("type")
|
||||
# Cycle through all types we've got from request
|
||||
@@ -56,15 +60,15 @@ 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
|
||||
# Price is 0 if evemarketer has info on this item, but it is not available
|
||||
# for current scope limit. If we provided scope limit - make sure to skip
|
||||
# such items to check globally, and do not skip if requested globally
|
||||
if percprice == 0 and system is not None:
|
||||
continue
|
||||
|
||||
# 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
|
||||
|
||||
@@ -124,7 +124,7 @@ def importDna(string):
|
||||
return f
|
||||
|
||||
|
||||
def exportDna(fit):
|
||||
def exportDna(fit, callback):
|
||||
dna = str(fit.shipID)
|
||||
subsystems = [] # EVE cares which order you put these in
|
||||
mods = OrderedDict()
|
||||
@@ -174,4 +174,9 @@ def exportDna(fit):
|
||||
for charge in charges:
|
||||
dna += ":{0};{1}".format(charge, charges[charge])
|
||||
|
||||
return dna + "::"
|
||||
text = dna + "::"
|
||||
|
||||
if callback:
|
||||
callback(text)
|
||||
else:
|
||||
return text
|
||||
|
||||
@@ -576,7 +576,7 @@ class EfsPort:
|
||||
return sizeNotFoundMsg
|
||||
|
||||
@staticmethod
|
||||
def exportEfs(fit, typeNotFitFlag):
|
||||
def exportEfs(fit, typeNotFitFlag, callback):
|
||||
sFit = Fit.getInstance()
|
||||
includeShipTypeData = typeNotFitFlag > 0
|
||||
if includeShipTypeData:
|
||||
@@ -673,4 +673,8 @@ class EfsPort:
|
||||
pyfalog.error(e)
|
||||
dataDict = {"name": fitName + "Fit could not be correctly parsed"}
|
||||
export = json.dumps(dataDict, skipkeys=True)
|
||||
return export
|
||||
|
||||
if callback:
|
||||
callback(export)
|
||||
else:
|
||||
return export
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
|
||||
import re
|
||||
from service.const import PortEftOptions, PortEftRigSize
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
@@ -34,6 +33,7 @@ from eos.saveddata.module import Module
|
||||
from eos.saveddata.ship import Ship
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.const import FittingSlot, FittingModuleState
|
||||
from service.const import PortEftOptions, PortEftRigSize
|
||||
from service.fit import Fit as svcFit
|
||||
from service.market import Market
|
||||
from service.port.muta import parseMutant, renderMutant
|
||||
@@ -54,7 +54,7 @@ SLOT_ORDER = (FittingSlot.LOW, FittingSlot.MED, FittingSlot.HIGH, FittingSlot.RI
|
||||
OFFLINE_SUFFIX = '/OFFLINE'
|
||||
|
||||
|
||||
def exportEft(fit, options):
|
||||
def exportEft(fit, options, callback):
|
||||
# EFT formatted export is split in several sections, each section is
|
||||
# separated from another using 2 blank lines. Sections might have several
|
||||
# sub-sections, which are separated by 1 blank line
|
||||
@@ -150,7 +150,12 @@ def exportEft(fit, options):
|
||||
if mutationLines:
|
||||
sections.append('\n'.join(mutationLines))
|
||||
|
||||
return '{}\n\n{}'.format(header, '\n\n\n'.join(sections))
|
||||
text = '{}\n\n{}'.format(header, '\n\n\n'.join(sections))
|
||||
|
||||
if callback:
|
||||
callback(text)
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
def importEft(lines):
|
||||
|
||||
@@ -55,7 +55,7 @@ INV_FLAG_DRONEBAY = 87
|
||||
INV_FLAG_FIGHTER = 158
|
||||
|
||||
|
||||
def exportESI(ofit):
|
||||
def exportESI(ofit, callback):
|
||||
# A few notes:
|
||||
# max fit name length is 50 characters
|
||||
# Most keys are created simply because they are required, but bogus data is okay
|
||||
@@ -135,7 +135,12 @@ def exportESI(ofit):
|
||||
if len(fit['items']) == 0:
|
||||
raise ESIExportException("Cannot export fitting: module list cannot be empty.")
|
||||
|
||||
return json.dumps(fit)
|
||||
text = json.dumps(fit)
|
||||
|
||||
if callback:
|
||||
callback(text)
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
def importESI(string):
|
||||
|
||||
@@ -19,52 +19,76 @@
|
||||
|
||||
|
||||
from service.const import PortMultiBuyOptions
|
||||
from service.price import Price as sPrc
|
||||
|
||||
|
||||
MULTIBUY_OPTIONS = (
|
||||
(PortMultiBuyOptions.LOADED_CHARGES.value, 'Loaded Charges', 'Export charges loaded into modules', True),
|
||||
(PortMultiBuyOptions.IMPLANTS.value, 'Implants && Boosters', 'Export implants and boosters', False),
|
||||
(PortMultiBuyOptions.CARGO.value, 'Cargo', 'Export cargo contents', True),
|
||||
(PortMultiBuyOptions.OPTIMIZE_PRICES.value, 'Optimize Prices', 'Replace items by cheaper alternatives', False),
|
||||
)
|
||||
|
||||
|
||||
def exportMultiBuy(fit, options):
|
||||
itemCounts = {}
|
||||
|
||||
def addItem(item, quantity=1):
|
||||
if item not in itemCounts:
|
||||
itemCounts[item] = 0
|
||||
itemCounts[item] += quantity
|
||||
def exportMultiBuy(fit, options, callback):
|
||||
itemAmounts = {}
|
||||
|
||||
for module in fit.modules:
|
||||
if module.item:
|
||||
# Mutated items are of no use for multibuy
|
||||
if module.isMutated:
|
||||
continue
|
||||
addItem(module.item)
|
||||
_addItem(itemAmounts, module.item)
|
||||
if module.charge and options[PortMultiBuyOptions.LOADED_CHARGES.value]:
|
||||
addItem(module.charge, module.numCharges)
|
||||
_addItem(itemAmounts, module.charge, module.numCharges)
|
||||
|
||||
for drone in fit.drones:
|
||||
addItem(drone.item, drone.amount)
|
||||
_addItem(itemAmounts, drone.item, drone.amount)
|
||||
|
||||
for fighter in fit.fighters:
|
||||
addItem(fighter.item, fighter.amountActive)
|
||||
_addItem(itemAmounts, fighter.item, fighter.amountActive)
|
||||
|
||||
if options[PortMultiBuyOptions.CARGO.value]:
|
||||
for cargo in fit.cargo:
|
||||
addItem(cargo.item, cargo.amount)
|
||||
_addItem(itemAmounts, cargo.item, cargo.amount)
|
||||
|
||||
if options[PortMultiBuyOptions.IMPLANTS.value]:
|
||||
for implant in fit.implants:
|
||||
addItem(implant.item)
|
||||
_addItem(itemAmounts, implant.item)
|
||||
|
||||
for booster in fit.boosters:
|
||||
addItem(booster.item)
|
||||
_addItem(itemAmounts, booster.item)
|
||||
|
||||
if options[PortMultiBuyOptions.OPTIMIZE_PRICES.value]:
|
||||
|
||||
def formatCheaperExportCb(replacementsCheaper):
|
||||
updatedAmounts = {}
|
||||
for item, itemAmount in itemAmounts.items():
|
||||
_addItem(updatedAmounts, replacementsCheaper.get(item, item), itemAmount)
|
||||
string = _prepareString(fit.ship.item, updatedAmounts)
|
||||
callback(string)
|
||||
|
||||
priceSvc = sPrc.getInstance()
|
||||
priceSvc.findCheaperReplacements(itemAmounts, formatCheaperExportCb)
|
||||
else:
|
||||
string = _prepareString(fit.ship.item, itemAmounts)
|
||||
if callback:
|
||||
callback(string)
|
||||
else:
|
||||
return string
|
||||
|
||||
|
||||
def _addItem(container, item, quantity=1):
|
||||
if item not in container:
|
||||
container[item] = 0
|
||||
container[item] += quantity
|
||||
|
||||
|
||||
def _prepareString(shipItem, itemAmounts):
|
||||
exportLines = []
|
||||
exportLines.append(fit.ship.item.name)
|
||||
for item in sorted(itemCounts, key=lambda i: (i.group.category.name, i.group.name, i.name)):
|
||||
count = itemCounts[item]
|
||||
exportLines.append(shipItem.name)
|
||||
for item in sorted(itemAmounts, key=lambda i: (i.group.category.name, i.group.name, i.name)):
|
||||
count = itemAmounts[item]
|
||||
if count == 1:
|
||||
exportLines.append(item.name)
|
||||
else:
|
||||
|
||||
@@ -257,8 +257,8 @@ class Port(object):
|
||||
return importEftCfg(shipname, lines, iportuser)
|
||||
|
||||
@classmethod
|
||||
def exportEft(cls, fit, options):
|
||||
return exportEft(fit, options)
|
||||
def exportEft(cls, fit, options, callback=None):
|
||||
return exportEft(fit, options, callback=callback)
|
||||
|
||||
# DNA-related methods
|
||||
@staticmethod
|
||||
@@ -266,8 +266,8 @@ class Port(object):
|
||||
return importDna(string)
|
||||
|
||||
@staticmethod
|
||||
def exportDna(fit):
|
||||
return exportDna(fit)
|
||||
def exportDna(fit, callback=None):
|
||||
return exportDna(fit, callback=callback)
|
||||
|
||||
# ESI-related methods
|
||||
@staticmethod
|
||||
@@ -275,8 +275,8 @@ class Port(object):
|
||||
return importESI(string)
|
||||
|
||||
@staticmethod
|
||||
def exportESI(fit):
|
||||
return exportESI(fit)
|
||||
def exportESI(fit, callback=None):
|
||||
return exportESI(fit, callback=callback)
|
||||
|
||||
# XML-related methods
|
||||
@staticmethod
|
||||
@@ -284,10 +284,10 @@ class Port(object):
|
||||
return importXml(text, iportuser)
|
||||
|
||||
@staticmethod
|
||||
def exportXml(iportuser=None, *fits):
|
||||
return exportXml(iportuser, *fits)
|
||||
def exportXml(iportuser=None, callback=None, *fits):
|
||||
return exportXml(iportuser, callback=callback, *fits)
|
||||
|
||||
# Multibuy-related methods
|
||||
@staticmethod
|
||||
def exportMultiBuy(fit, options):
|
||||
return exportMultiBuy(fit, options)
|
||||
def exportMultiBuy(fit, options, callback=None):
|
||||
return exportMultiBuy(fit, options, callback=callback)
|
||||
|
||||
@@ -226,14 +226,13 @@ def importXml(text, iportuser):
|
||||
return fit_list
|
||||
|
||||
|
||||
def exportXml(iportuser, *fits):
|
||||
def exportXml(iportuser, callback, *fits):
|
||||
doc = xml.dom.minidom.Document()
|
||||
fittings = doc.createElement("fittings")
|
||||
# fit count
|
||||
fit_count = len(fits)
|
||||
fittings.setAttribute("count", "%s" % fit_count)
|
||||
doc.appendChild(fittings)
|
||||
sFit = svcFit.getInstance()
|
||||
|
||||
for i, fit in enumerate(fits):
|
||||
try:
|
||||
@@ -324,4 +323,9 @@ def exportXml(iportuser, *fits):
|
||||
iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE,
|
||||
(i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name))
|
||||
)
|
||||
return doc.toprettyxml()
|
||||
text = doc.toprettyxml()
|
||||
|
||||
if callback:
|
||||
callback(text)
|
||||
else:
|
||||
return text
|
||||
|
||||
174
service/price.py
174
service/price.py
@@ -18,28 +18,26 @@
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from itertools import chain
|
||||
|
||||
import wx
|
||||
from logbook import Logger
|
||||
|
||||
from eos import db
|
||||
from eos.saveddata.price import PriceStatus
|
||||
from gui.fitCommands.guiRebaseItems import GuiRebaseItemsCommand
|
||||
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 +67,34 @@ class Price(object):
|
||||
return cls.instance
|
||||
|
||||
@classmethod
|
||||
def fetchPrices(cls, prices):
|
||||
def fetchPrices(cls, prices, fetchTimeout, validityOverride):
|
||||
"""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:
|
||||
if not price.isValid(validityOverride):
|
||||
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()
|
||||
@@ -110,62 +104,46 @@ class Price(object):
|
||||
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]
|
||||
sourceAll = list(cls.sources.keys())
|
||||
sourcePrimary = sFit.serviceFittingOptions["priceSource"] if sFit.serviceFittingOptions["priceSource"] in sourceAll else sourceAll[0]
|
||||
|
||||
while len(sourcesToTry) > 0:
|
||||
sourcesToTry.remove(curr)
|
||||
# Format: {source name: timeout weight}
|
||||
sources = {sourcePrimary: len(sourceAll)}
|
||||
for source in sourceAll:
|
||||
if source == sourcePrimary:
|
||||
continue
|
||||
sources[source] = min(sources.values()) - 1
|
||||
timeoutWeightMult = fetchTimeout / sum(sources.values())
|
||||
|
||||
# Record timeouts as it will affect our final decision
|
||||
timedOutSources = {}
|
||||
|
||||
for source, timeoutWeight in sources.items():
|
||||
pyfalog.info('Trying {}'.format(source))
|
||||
timedOutSources[source] = False
|
||||
sourceFetchTimeout = timeoutWeight * timeoutWeightMult
|
||||
try:
|
||||
sourceCls = cls.sources.get(curr)
|
||||
sourceCls(toRequest, cls.systemsList[sFit.serviceFittingOptions["priceSystem"]], priceMap)
|
||||
break
|
||||
# If getting or processing data returned any errors
|
||||
sourceCls = cls.sources.get(source)
|
||||
sourceCls(priceMap, cls.systemsList[sFit.serviceFittingOptions["priceSystem"]], sourceFetchTimeout)
|
||||
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(source))
|
||||
timedOutSources[source] = True
|
||||
except Exception as e:
|
||||
pyfalog.warn('Failed to fetch prices from price source {}: {}'.format(source, e))
|
||||
# Sources remove price map items as they fetch info, if none remain then we're done
|
||||
if not priceMap:
|
||||
break
|
||||
|
||||
# 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
|
||||
|
||||
@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))
|
||||
# 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 timedOutSources.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)
|
||||
|
||||
def getPriceNow(self, objitem):
|
||||
"""Get price for provided typeID"""
|
||||
@@ -174,7 +152,7 @@ class Price(object):
|
||||
|
||||
return item.price.price
|
||||
|
||||
def getPrices(self, objitems, callback, waitforthread=False):
|
||||
def getPrices(self, objitems, callback, fetchTimeout=30, waitforthread=False, validityOverride=None):
|
||||
"""Get prices for multiple typeIDs"""
|
||||
requests = []
|
||||
sMkt = Market.getInstance()
|
||||
@@ -186,7 +164,7 @@ class Price(object):
|
||||
try:
|
||||
callback(requests)
|
||||
except Exception as e:
|
||||
pyfalog.critical("Callback failed.")
|
||||
pyfalog.critical("Execution of callback from getPrices failed.")
|
||||
pyfalog.critical(e)
|
||||
|
||||
db.commit()
|
||||
@@ -194,12 +172,43 @@ class Price(object):
|
||||
if waitforthread:
|
||||
self.priceWorkerThread.setToWait(requests, cb)
|
||||
else:
|
||||
self.priceWorkerThread.trigger(requests, cb)
|
||||
self.priceWorkerThread.trigger(requests, cb, fetchTimeout, validityOverride)
|
||||
|
||||
def clearPriceCache(self):
|
||||
pyfalog.debug("Clearing Prices")
|
||||
db.clearPrices()
|
||||
|
||||
def findCheaperReplacements(self, items, callback, fetchTimeout=10):
|
||||
sMkt = Market.getInstance()
|
||||
|
||||
replacementsAll = {} # All possible item replacements
|
||||
for item in items:
|
||||
if item in replacementsAll:
|
||||
continue
|
||||
itemRepls = sMkt.getReplacements(item)
|
||||
if itemRepls:
|
||||
replacementsAll[item] = itemRepls
|
||||
itemsToFetch = {i for i in chain(replacementsAll.keys(), *replacementsAll.values())}
|
||||
|
||||
def makeCheapMapCb(requests):
|
||||
# Decide what we are going to replace
|
||||
replacementsCheaper = {} # Items which should be replaced
|
||||
for replacee, replacers in replacementsAll.items():
|
||||
replacer = min(replacers, key=lambda i: i.price.price or math.inf)
|
||||
if (replacer.price.price or math.inf) < (replacee.price.price or math.inf):
|
||||
replacementsCheaper[replacee] = replacer
|
||||
try:
|
||||
callback(replacementsCheaper)
|
||||
except Exception as e:
|
||||
pyfalog.critical("Execution of callback from findCheaperReplacements failed.")
|
||||
pyfalog.critical(e)
|
||||
|
||||
# Prices older than 2 hours have to be refetched
|
||||
validityOverride = 2 * 60 * 60
|
||||
self.getPrices(itemsToFetch, makeCheapMapCb, fetchTimeout=fetchTimeout, validityOverride=validityOverride)
|
||||
|
||||
|
||||
|
||||
|
||||
class PriceWorkerThread(threading.Thread):
|
||||
|
||||
@@ -214,11 +223,11 @@ class PriceWorkerThread(threading.Thread):
|
||||
queue = self.queue
|
||||
while True:
|
||||
# Grab our data
|
||||
callback, requests = queue.get()
|
||||
callback, requests, fetchTimeout, validityOverride = queue.get()
|
||||
|
||||
# Grab prices, this is the time-consuming part
|
||||
if len(requests) > 0:
|
||||
Price.fetchPrices(requests)
|
||||
Price.fetchPrices(requests, fetchTimeout, validityOverride)
|
||||
|
||||
wx.CallAfter(callback)
|
||||
queue.task_done()
|
||||
@@ -230,14 +239,13 @@ class PriceWorkerThread(threading.Thread):
|
||||
for callback in callbacks:
|
||||
wx.CallAfter(callback)
|
||||
|
||||
def trigger(self, prices, callbacks):
|
||||
self.queue.put((callbacks, prices))
|
||||
def trigger(self, prices, callbacks, fetchTimeout, validityOverride):
|
||||
self.queue.put((callbacks, prices, fetchTimeout, validityOverride))
|
||||
|
||||
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)
|
||||
for price in prices:
|
||||
callbacks = self.wait.setdefault(price.typeID, [])
|
||||
callbacks.append(callback)
|
||||
|
||||
|
||||
# Import market sources only to initialize price source modules, they register on their own
|
||||
|
||||
Reference in New Issue
Block a user