Merge remote-tracking branch 'origin/development' into commandRefactor

This commit is contained in:
blitzmann
2018-09-03 17:28:35 -04:00
15 changed files with 2097 additions and 1435 deletions

View File

@@ -396,6 +396,21 @@ def getAbyssalTypes():
return set([r.resultingTypeID for r in gamedata_session.query(DynamicItem.resultingTypeID).distinct()])
@cachedQuery(1, "itemID")
def getDynamicItem(itemID, eager=None):
try:
if isinstance(itemID, int):
if eager is None:
result = gamedata_session.query(DynamicItem).filter(DynamicItem.ID == itemID).one()
else:
result = gamedata_session.query(DynamicItem).options(*processEager(eager)).filter(DynamicItem.ID == itemID).one()
else:
raise TypeError("Need integer as argument")
except exc.NoResultFound:
result = None
return result
def getRequiredFor(itemID, attrMapping):
Attribute1 = aliased(Attribute)
Attribute2 = aliased(Attribute)

View File

@@ -114,6 +114,7 @@ class HandledList(list):
class HandledModuleList(HandledList):
def append(self, mod):
emptyPosition = float("Inf")
for i in range(len(self)):
@@ -131,6 +132,9 @@ class HandledModuleList(HandledList):
self.remove(mod)
return
self.appendIgnoreEmpty(mod)
def appendIgnoreEmpty(self, mod):
mod.position = len(self)
HandledList.append(self, mod)
if mod.isInvalid:

View File

@@ -20,51 +20,84 @@
# noinspection PyPackageRequirements
import wx
from service.port.eft import EFT_OPTIONS
from service.settings import SettingsProvider
class CopySelectDialog(wx.Dialog):
copyFormatEft = 0
copyFormatEftImps = 1
copyFormatXml = 2
copyFormatDna = 3
copyFormatEsi = 4
copyFormatMultiBuy = 5
copyFormatEfs = 6
copyFormatXml = 1
copyFormatDna = 2
copyFormatEsi = 3
copyFormatMultiBuy = 4
copyFormatEfs = 5
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)
mainSizer = wx.BoxSizer(wx.VERTICAL)
copyFormats = ["EFT", "EFT (Implants)", "XML", "DNA", "ESI", "MultiBuy", "EFS"]
copyFormatTooltips = {CopySelectDialog.copyFormatEft: "EFT text format",
CopySelectDialog.copyFormatEftImps: "EFT text format",
CopySelectDialog.copyFormatXml: "EVE native XML format",
CopySelectDialog.copyFormatDna: "A one-line text format",
CopySelectDialog.copyFormatEsi: "A JSON format used for ESI",
CopySelectDialog.copyFormatMultiBuy: "MultiBuy text format",
CopySelectDialog.copyFormatEfs: "JSON data format used by EFS"}
selector = wx.RadioBox(self, wx.ID_ANY, label="Copy to the clipboard using:", choices=copyFormats,
style=wx.RA_SPECIFY_ROWS)
selector.Bind(wx.EVT_RADIOBOX, self.Selected)
for format, tooltip in copyFormatTooltips.items():
selector.SetItemToolTip(format, tooltip)
self.settings = SettingsProvider.getInstance().getSettings("pyfaExport", {"format": 0, "options": 0})
self.copyFormat = CopySelectDialog.copyFormatEft
selector.SetSelection(self.copyFormat)
self.copyFormats = {
"EFT": CopySelectDialog.copyFormatEft,
"XML": CopySelectDialog.copyFormatXml,
"DNA": CopySelectDialog.copyFormatDna,
"ESI": CopySelectDialog.copyFormatEsi,
"MultiBuy": CopySelectDialog.copyFormatMultiBuy,
"EFS": CopySelectDialog.copyFormatEfs
}
mainSizer.Add(selector, 0, wx.EXPAND | wx.ALL, 5)
self.options = {}
for i, format in enumerate(self.copyFormats.keys()):
if i == 0:
rdo = wx.RadioButton(self, wx.ID_ANY, format, style=wx.RB_GROUP)
else:
rdo = wx.RadioButton(self, wx.ID_ANY, format)
rdo.Bind(wx.EVT_RADIOBUTTON, self.Selected)
if self.settings['format'] == self.copyFormats[format]:
rdo.SetValue(True)
self.copyFormat = self.copyFormats[format]
mainSizer.Add(rdo, 0, wx.EXPAND | wx.ALL, 5)
if format == "EFT":
bsizer = wx.BoxSizer(wx.VERTICAL)
for x, v in EFT_OPTIONS.items():
ch = wx.CheckBox(self, -1, v['name'])
self.options[x] = ch
if self.settings['options'] & x:
ch.SetValue(True)
bsizer.Add(ch, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 3)
mainSizer.Add(bsizer, 1, wx.EXPAND | wx.LEFT, 20)
buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
if buttonSizer:
mainSizer.Add(buttonSizer, 0, wx.EXPAND | wx.ALL, 5)
self.toggleOptions()
self.SetSizer(mainSizer)
self.Fit()
self.Center()
def Selected(self, event):
self.copyFormat = event.GetSelection()
obj = event.GetEventObject()
format = obj.GetLabel()
self.copyFormat = self.copyFormats[format]
self.toggleOptions()
self.Fit()
def toggleOptions(self):
for ch in self.options.values():
ch.Enable(self.GetSelected() == CopySelectDialog.copyFormatEft)
def GetSelected(self):
return self.copyFormat
def GetOptions(self):
i = 0
for x, v in self.options.items():
if v.IsChecked():
i = i ^ x
return i

View File

@@ -15,7 +15,7 @@ import gui.globalEvents as GE
from logbook import Logger
from service.esi import Esi
from service.esiAccess import APIException
from service.port import ESIExportException
from service.port.esi import ESIExportException
pyfalog = Logger(__name__)

View File

@@ -76,8 +76,7 @@ from service.esiAccess import SsoMode
from eos.modifiedAttributeDict import ModifiedAttributeDict
from eos.db.saveddata.loadDefaultDatabaseValues import DefaultDatabaseValues
from eos.db.saveddata.queries import getFit as db_getFit
from service.port import Port, IPortUser
from service.efsPort import EfsPort
from service.port import Port, IPortUser, EfsPort
from service.settings import HTMLExportSettings
from time import gmtime, strftime
@@ -711,31 +710,31 @@ class MainFrame(wx.Frame):
else:
self.marketBrowser.search.Focus()
def clipboardEft(self):
def clipboardEft(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportEft(fit))
toClipboard(Port.exportEft(fit, options))
def clipboardEftImps(self):
def clipboardEftImps(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportEftImps(fit))
def clipboardDna(self):
def clipboardDna(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportDna(fit))
def clipboardEsi(self):
def clipboardEsi(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportESI(fit))
def clipboardXml(self):
def clipboardXml(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportXml(None, fit))
def clipboardMultiBuy(self):
def clipboardMultiBuy(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportMultiBuy(fit))
def clipboardEfs(self):
def clipboardEfs(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(EfsPort.exportEfs(fit, 0))
@@ -750,7 +749,7 @@ class MainFrame(wx.Frame):
def exportToClipboard(self, event):
CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft,
CopySelectDialog.copyFormatEftImps: self.clipboardEftImps,
# CopySelectDialog.copyFormatEftImps: self.clipboardEftImps,
CopySelectDialog.copyFormatXml: self.clipboardXml,
CopySelectDialog.copyFormatDna: self.clipboardDna,
CopySelectDialog.copyFormatEsi: self.clipboardEsi,
@@ -759,8 +758,13 @@ class MainFrame(wx.Frame):
dlg = CopySelectDialog(self)
dlg.ShowModal()
selected = dlg.GetSelected()
options = dlg.GetOptions()
CopySelectDict[selected]()
settings = SettingsProvider.getInstance().getSettings("pyfaExport")
settings["format"] = selected
settings["options"] = options
CopySelectDict[selected](options)
try:
dlg.Destroy()

File diff suppressed because it is too large Load Diff

2
service/port/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .efs import EfsPort
from .port import Port, IPortUser

176
service/port/dna.py Normal file
View File

@@ -0,0 +1,176 @@
# =============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import re
from collections import OrderedDict
from logbook import Logger
from eos.saveddata.cargo import Cargo
from eos.saveddata.citadel import Citadel
from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter
from eos.saveddata.fit import Fit
from eos.saveddata.module import Module, State, Slot
from eos.saveddata.ship import Ship
from service.fit import Fit as svcFit
from service.market import Market
pyfalog = Logger(__name__)
def importDna(string):
sMkt = Market.getInstance()
ids = list(map(int, re.findall(r'\d+', string)))
for id_ in ids:
try:
try:
try:
Ship(sMkt.getItem(sMkt.getItem(id_)))
except ValueError:
Citadel(sMkt.getItem(sMkt.getItem(id_)))
except ValueError:
Citadel(sMkt.getItem(id_))
string = string[string.index(str(id_)):]
break
except:
pyfalog.warning("Exception caught in importDna")
pass
string = string[:string.index("::") + 2]
info = string.split(":")
f = Fit()
try:
try:
f.ship = Ship(sMkt.getItem(int(info[0])))
except ValueError:
f.ship = Citadel(sMkt.getItem(int(info[0])))
f.name = "{0} - DNA Imported".format(f.ship.item.name)
except UnicodeEncodeError:
def logtransform(s_):
if len(s_) > 10:
return s_[:10] + "..."
return s_
pyfalog.exception("Couldn't import ship data {0}", [logtransform(s) for s in info])
return None
moduleList = []
for itemInfo in info[1:]:
if itemInfo:
itemID, amount = itemInfo.split(";")
item = sMkt.getItem(int(itemID), eager="group.category")
if item.category.name == "Drone":
d = Drone(item)
d.amount = int(amount)
f.drones.append(d)
elif item.category.name == "Fighter":
ft = Fighter(item)
ft.amount = int(amount) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize
if ft.fits(f):
f.fighters.append(ft)
elif item.category.name == "Charge":
c = Cargo(item)
c.amount = int(amount)
f.cargo.append(c)
else:
for i in range(int(amount)):
try:
m = Module(item)
except:
pyfalog.warning("Exception caught in importDna")
continue
# Add subsystems before modules to make sure T3 cruisers have subsystems installed
if item.category.name == "Subsystem":
if m.fits(f):
f.modules.append(m)
else:
m.owner = f
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
moduleList.append(m)
# Recalc to get slot numbers correct for T3 cruisers
svcFit.getInstance().recalc(f)
for module in moduleList:
if module.fits(f):
module.owner = f
if module.isValidState(State.ACTIVE):
module.state = State.ACTIVE
f.modules.append(module)
return f
def exportDna(fit):
dna = str(fit.shipID)
subsystems = [] # EVE cares which order you put these in
mods = OrderedDict()
charges = OrderedDict()
sFit = svcFit.getInstance()
for mod in fit.modules:
if not mod.isEmpty:
if mod.slot == Slot.SUBSYSTEM:
subsystems.append(mod)
continue
if mod.itemID not in mods:
mods[mod.itemID] = 0
mods[mod.itemID] += 1
if mod.charge and sFit.serviceFittingOptions["exportCharges"]:
if mod.chargeID not in charges:
charges[mod.chargeID] = 0
# `or 1` because some charges (ie scripts) are without qty
charges[mod.chargeID] += mod.numCharges or 1
for subsystem in sorted(subsystems, key=lambda mod_: mod_.getModifiedItemAttr("subSystemSlot")):
dna += ":{0};1".format(subsystem.itemID)
for mod in mods:
dna += ":{0};{1}".format(mod, mods[mod])
for drone in fit.drones:
dna += ":{0};{1}".format(drone.itemID, drone.amount)
for fighter in fit.fighters:
dna += ":{0};{1}".format(fighter.itemID, fighter.amountActive)
for fighter in fit.fighters:
dna += ":{0};{1}".format(fighter.itemID, fighter.amountActive)
for cargo in fit.cargo:
# DNA format is a simple/dumb format. As CCP uses the slot information of the item itself
# without designating slots in the DNA standard, we need to make sure we only include
# charges in the DNA export. If modules were included, the EVE Client will interpret these
# as being "Fitted" to whatever slot they are for, and it causes an corruption error in the
# client when trying to save the fit
if cargo.item.category.name == "Charge":
if cargo.item.ID not in charges:
charges[cargo.item.ID] = 0
charges[cargo.item.ID] += cargo.amount
for charge in charges:
dna += ":{0};{1}".format(charge, charges[charge])
return dna + "::"

878
service/port/eft.py Normal file
View File

@@ -0,0 +1,878 @@
# =============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import re
from logbook import Logger
from eos.db.gamedata.queries import getAttributeInfo, getDynamicItem
from eos.saveddata.cargo import Cargo
from eos.saveddata.citadel import Citadel
from eos.saveddata.booster import Booster
from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter
from eos.saveddata.implant import Implant
from eos.saveddata.module import Module, State, Slot
from eos.saveddata.ship import Ship
from eos.saveddata.fit import Fit
from gui.utils.numberFormatter import roundToPrec
from service.fit import Fit as svcFit
from service.market import Market
from service.port.shared import IPortUser, processing_notify
from enum import Enum
pyfalog = Logger(__name__)
class Options(Enum):
IMPLANTS = 1
MUTATIONS = 2
MODULE_CATS = ('Module', 'Subsystem', 'Structure Module')
SLOT_ORDER = (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE)
OFFLINE_SUFFIX = '/OFFLINE'
EFT_OPTIONS = {
Options.IMPLANTS.value: {
"name": "Implants",
"description": "Exports implants"
},
Options.MUTATIONS.value: {
"name": "Mutated Attributes",
"description": "Exports Abyssal stats"
}
}
def exportEft(fit, options):
# 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
sections = []
header = '[{}, {}]'.format(fit.ship.item.name, fit.name)
# Section 1: modules, rigs, subsystems, services
modsBySlotType = {}
sFit = svcFit.getInstance()
for module in fit.modules:
modsBySlotType.setdefault(module.slot, []).append(module)
modSection = []
mutants = {} # Format: {reference number: module}
mutantReference = 1
for slotType in SLOT_ORDER:
rackLines = []
modules = modsBySlotType.get(slotType, ())
for module in modules:
if module.item:
mutated = bool(module.mutators)
# if module was mutated, use base item name for export
if mutated:
modName = module.baseItem.name
else:
modName = module.item.name
if mutated and options & Options.MUTATIONS.value:
mutants[mutantReference] = module
mutationSuffix = ' [{}]'.format(mutantReference)
mutantReference += 1
else:
mutationSuffix = ''
modOfflineSuffix = ' {}'.format(OFFLINE_SUFFIX) if module.state == State.OFFLINE else ''
if module.charge and sFit.serviceFittingOptions['exportCharges']:
rackLines.append('{}, {}{}{}'.format(
modName, module.charge.name, modOfflineSuffix, mutationSuffix))
else:
rackLines.append('{}{}{}'.format(modName, modOfflineSuffix, mutationSuffix))
else:
rackLines.append('[Empty {} slot]'.format(
Slot.getName(slotType).capitalize() if slotType is not None else ''))
if rackLines:
modSection.append('\n'.join(rackLines))
if modSection:
sections.append('\n\n'.join(modSection))
# Section 2: drones, fighters
minionSection = []
droneLines = []
for drone in sorted(fit.drones, key=lambda d: d.item.name):
droneLines.append('{} x{}'.format(drone.item.name, drone.amount))
if droneLines:
minionSection.append('\n'.join(droneLines))
fighterLines = []
for fighter in sorted(fit.fighters, key=lambda f: f.item.name):
fighterLines.append('{} x{}'.format(fighter.item.name, fighter.amountActive))
if fighterLines:
minionSection.append('\n'.join(fighterLines))
if minionSection:
sections.append('\n\n'.join(minionSection))
# Section 3: implants, boosters
if options & Options.IMPLANTS.value:
charSection = []
implantLines = []
for implant in fit.implants:
implantLines.append(implant.item.name)
if implantLines:
charSection.append('\n'.join(implantLines))
boosterLines = []
for booster in fit.boosters:
boosterLines.append(booster.item.name)
if boosterLines:
charSection.append('\n'.join(boosterLines))
if charSection:
sections.append('\n\n'.join(charSection))
# Section 4: cargo
cargoLines = []
for cargo in sorted(
fit.cargo,
key=lambda c: (c.item.group.category.name, c.item.group.name, c.item.name)
):
cargoLines.append('{} x{}'.format(cargo.item.name, cargo.amount))
if cargoLines:
sections.append('\n'.join(cargoLines))
# Section 5: mutated modules' details
mutationLines = []
if mutants and options & Options.MUTATIONS.value:
for mutantReference in sorted(mutants):
mutant = mutants[mutantReference]
mutatedAttrs = {}
for attrID, mutator in mutant.mutators.items():
attrName = getAttributeInfo(attrID).name
mutatedAttrs[attrName] = mutator.value
mutationLines.append('[{}] {}'.format(mutantReference, mutant.baseItem.name))
mutationLines.append(' {}'.format(mutant.mutaplasmid.item.name))
# Round to 7th significant number to avoid exporting float errors
customAttrsLine = ', '.join(
'{} {}'.format(a, roundToPrec(mutatedAttrs[a], 7))
for a in sorted(mutatedAttrs))
mutationLines.append(' {}'.format(customAttrsLine))
if mutationLines:
sections.append('\n'.join(mutationLines))
return '{}\n\n{}'.format(header, '\n\n\n'.join(sections))
def importEft(eftString):
lines = _importPrepareString(eftString)
try:
fit = _importCreateFit(lines)
except EftImportError:
return
aFit = AbstractFit()
aFit.mutations = _importGetMutationData(lines)
nameChars = '[^,/\[\]]' # Characters which are allowed to be used in name
stubPattern = '^\[.+?\]$'
modulePattern = '^(?P<typeName>{0}+?)(,\s*(?P<chargeName>{0}+?))?(?P<offline>\s*{1})?(\s*\[(?P<mutation>\d+?)\])?$'.format(nameChars, OFFLINE_SUFFIX)
droneCargoPattern = '^(?P<typeName>{}+?) x(?P<amount>\d+?)$'.format(nameChars)
sections = []
for section in _importSectionIter(lines):
for line in section.lines:
# Stub line
if re.match(stubPattern, line):
section.itemSpecs.append(None)
continue
# Items with quantity specifier
m = re.match(droneCargoPattern, line)
if m:
try:
itemSpec = MultiItemSpec(m.group('typeName'))
# Items which cannot be fetched are considered as stubs
except EftImportError:
section.itemSpecs.append(None)
else:
itemSpec.amount = int(m.group('amount'))
section.itemSpecs.append(itemSpec)
continue
# All other items
m = re.match(modulePattern, line)
if m:
try:
itemSpec = RegularItemSpec(m.group('typeName'), chargeName=m.group('chargeName'))
# Items which cannot be fetched are considered as stubs
except EftImportError:
section.itemSpecs.append(None)
else:
if m.group('offline'):
itemSpec.offline = True
if m.group('mutation'):
itemSpec.mutationIdx = int(m.group('mutation'))
section.itemSpecs.append(itemSpec)
continue
_clearTail(section.itemSpecs)
sections.append(section)
hasDroneBay = any(s.isDroneBay for s in sections)
hasFighterBay = any(s.isFighterBay for s in sections)
for section in sections:
if section.isModuleRack:
aFit.addModules(section.itemSpecs)
elif section.isImplantRack:
for itemSpec in section.itemSpecs:
aFit.addImplant(itemSpec)
elif section.isDroneBay:
for itemSpec in section.itemSpecs:
aFit.addDrone(itemSpec)
elif section.isFighterBay:
for itemSpec in section.itemSpecs:
aFit.addFighter(itemSpec)
elif section.isCargoHold:
for itemSpec in section.itemSpecs:
aFit.addCargo(itemSpec)
# Mix between different kinds of item specs (can happen when some
# blank lines are removed)
else:
for itemSpec in section.itemSpecs:
if itemSpec is None:
continue
if itemSpec.isModule:
aFit.addModule(itemSpec)
elif itemSpec.isImplant:
aFit.addImplant(itemSpec)
elif itemSpec.isDrone and not hasDroneBay:
aFit.addDrone(itemSpec)
elif itemSpec.isFighter and not hasFighterBay:
aFit.addFighter(itemSpec)
elif itemSpec.isCargo:
aFit.addCargo(itemSpec)
# Subsystems first because they modify slot amount
for m in aFit.subsystems:
if m is None:
dummy = Module.buildEmpty(aFit.getSlotByContainer(aFit.subsystems))
dummy.owner = fit
fit.modules.appendIgnoreEmpty(dummy)
elif m.fits(fit):
m.owner = fit
fit.modules.appendIgnoreEmpty(m)
svcFit.getInstance().recalc(fit)
# Other stuff
for modRack in (
aFit.rigs,
aFit.services,
aFit.modulesHigh,
aFit.modulesMed,
aFit.modulesLow,
):
for m in modRack:
if m is None:
dummy = Module.buildEmpty(aFit.getSlotByContainer(modRack))
dummy.owner = fit
fit.modules.appendIgnoreEmpty(dummy)
elif m.fits(fit):
m.owner = fit
if not m.isValidState(m.state):
pyfalog.warning('service.port.eft.importEft: module {} cannot have state {}', m, m.state)
fit.modules.appendIgnoreEmpty(m)
for implant in aFit.implants:
fit.implants.append(implant)
for booster in aFit.boosters:
fit.boosters.append(booster)
for drone in aFit.drones.values():
fit.drones.append(drone)
for fighter in aFit.fighters:
fit.fighters.append(fighter)
for cargo in aFit.cargo.values():
fit.cargo.append(cargo)
return fit
def importEftCfg(shipname, contents, iportuser):
"""Handle import from EFT config store file"""
# Check if we have such ship in database, bail if we don't
sMkt = Market.getInstance()
try:
sMkt.getItem(shipname)
except:
return [] # empty list is expected
fits = [] # List for fits
fitIndices = [] # List for starting line numbers for each fit
lines = re.split('[\n\r]+', contents) # Separate string into lines
for line in lines:
# Detect fit header
if line[:1] == "[" and line[-1:] == "]":
# Line index where current fit starts
startPos = lines.index(line)
fitIndices.append(startPos)
for i, startPos in enumerate(fitIndices):
# End position is last file line if we're trying to get it for last fit,
# or start position of next fit minus 1
endPos = len(lines) if i == len(fitIndices) - 1 else fitIndices[i + 1]
# Finally, get lines for current fitting
fitLines = lines[startPos:endPos]
try:
# Create fit object
fitobj = Fit()
# Strip square brackets and pull out a fit name
fitobj.name = fitLines[0][1:-1]
# Assign ship to fitting
try:
fitobj.ship = Ship(sMkt.getItem(shipname))
except ValueError:
fitobj.ship = Citadel(sMkt.getItem(shipname))
moduleList = []
for x in range(1, len(fitLines)):
line = fitLines[x]
if not line:
continue
# Parse line into some data we will need
misc = re.match("(Drones|Implant|Booster)_(Active|Inactive)=(.+)", line)
cargo = re.match("Cargohold=(.+)", line)
# 2017/03/27 NOTE: store description from EFT
description = re.match("Description=(.+)", line)
if misc:
entityType = misc.group(1)
entityState = misc.group(2)
entityData = misc.group(3)
if entityType == "Drones":
droneData = re.match("(.+),([0-9]+)", entityData)
# Get drone name and attempt to detect drone number
droneName = droneData.group(1) if droneData else entityData
droneAmount = int(droneData.group(2)) if droneData else 1
# Bail if we can't get item or it's not from drone category
try:
droneItem = sMkt.getItem(droneName, eager="group.category")
except:
pyfalog.warning("Cannot get item.")
continue
if droneItem.category.name == "Drone":
# Add drone to the fitting
d = Drone(droneItem)
d.amount = droneAmount
if entityState == "Active":
d.amountActive = droneAmount
elif entityState == "Inactive":
d.amountActive = 0
fitobj.drones.append(d)
elif droneItem.category.name == "Fighter": # EFT saves fighter as drones
ft = Fighter(droneItem)
ft.amount = int(droneAmount) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize
fitobj.fighters.append(ft)
else:
continue
elif entityType == "Implant":
# Bail if we can't get item or it's not from implant category
try:
implantItem = sMkt.getItem(entityData, eager="group.category")
except:
pyfalog.warning("Cannot get item.")
continue
if implantItem.category.name != "Implant":
continue
# Add implant to the fitting
imp = Implant(implantItem)
if entityState == "Active":
imp.active = True
elif entityState == "Inactive":
imp.active = False
fitobj.implants.append(imp)
elif entityType == "Booster":
# Bail if we can't get item or it's not from implant category
try:
boosterItem = sMkt.getItem(entityData, eager="group.category")
except:
pyfalog.warning("Cannot get item.")
continue
# All boosters have implant category
if boosterItem.category.name != "Implant":
continue
# Add booster to the fitting
b = Booster(boosterItem)
if entityState == "Active":
b.active = True
elif entityState == "Inactive":
b.active = False
fitobj.boosters.append(b)
# If we don't have any prefixes, then it's a module
elif cargo:
cargoData = re.match("(.+),([0-9]+)", cargo.group(1))
cargoName = cargoData.group(1) if cargoData else cargo.group(1)
cargoAmount = int(cargoData.group(2)) if cargoData else 1
# Bail if we can't get item
try:
item = sMkt.getItem(cargoName)
except:
pyfalog.warning("Cannot get item.")
continue
# Add Cargo to the fitting
c = Cargo(item)
c.amount = cargoAmount
fitobj.cargo.append(c)
# 2017/03/27 NOTE: store description from EFT
elif description:
fitobj.notes = description.group(1).replace("|", "\n")
else:
withCharge = re.match("(.+),(.+)", line)
modName = withCharge.group(1) if withCharge else line
chargeName = withCharge.group(2) if withCharge else None
# If we can't get module item, skip it
try:
modItem = sMkt.getItem(modName)
except:
pyfalog.warning("Cannot get item.")
continue
# Create module
m = Module(modItem)
# Add subsystems before modules to make sure T3 cruisers have subsystems installed
if modItem.category.name == "Subsystem":
if m.fits(fitobj):
fitobj.modules.append(m)
else:
m.owner = fitobj
# Activate mod if it is activable
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
# Add charge to mod if applicable, on any errors just don't add anything
if chargeName:
try:
chargeItem = sMkt.getItem(chargeName, eager="group.category")
if chargeItem.category.name == "Charge":
m.charge = chargeItem
except:
pyfalog.warning("Cannot get item.")
pass
# Append module to fit
moduleList.append(m)
# Recalc to get slot numbers correct for T3 cruisers
svcFit.getInstance().recalc(fitobj)
for module in moduleList:
if module.fits(fitobj):
fitobj.modules.append(module)
# Append fit to list of fits
fits.append(fitobj)
if iportuser: # NOTE: Send current processing status
processing_notify(
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
"%s:\n%s" % (fitobj.ship.name, fitobj.name)
)
# Skip fit silently if we get an exception
except Exception as e:
pyfalog.error("Caught exception on fit.")
pyfalog.error(e)
pass
return fits
def _importPrepareString(eftString):
lines = eftString.splitlines()
for i in range(len(lines)):
lines[i] = lines[i].strip()
while lines and not lines[0]:
del lines[0]
while lines and not lines[-1]:
del lines[-1]
return lines
def _importGetMutationData(lines):
data = {}
consumedIndices = set()
for i in range(len(lines)):
line = lines[i]
m = re.match('^\[(?P<ref>\d+)\]', line)
if m:
ref = int(m.group('ref'))
# Attempt to apply mutation is useless w/o mutaplasmid, so skip it
# altogether if we have no info on it
try:
mutaName = lines[i + 1]
except IndexError:
continue
else:
consumedIndices.add(i)
consumedIndices.add(i + 1)
# Get custom attribute values
mutaAttrs = {}
try:
mutaAttrsLine = lines[i + 2]
except IndexError:
pass
else:
consumedIndices.add(i + 2)
pairs = [p.strip() for p in mutaAttrsLine.split(',')]
for pair in pairs:
try:
attrName, value = pair.split(' ')
except ValueError:
continue
try:
value = float(value)
except (ValueError, TypeError):
continue
attrInfo = getAttributeInfo(attrName.strip())
if attrInfo is None:
continue
mutaAttrs[attrInfo.ID] = value
mutaItem = _fetchItem(mutaName)
if mutaItem is None:
continue
data[ref] = (mutaItem, mutaAttrs)
# If we got here, we have seen at least correct reference line and
# mutaplasmid name line
i += 2
# Bonus points for seeing correct attrs line. Worst case we
# will have to scan it once again
if mutaAttrs:
i += 1
# Cleanup the lines from mutaplasmid info
for i in sorted(consumedIndices, reverse=True):
del lines[i]
return data
def _importSectionIter(lines):
section = Section()
for line in lines:
if not line:
if section.lines:
yield section
section = Section()
else:
section.lines.append(line)
if section.lines:
yield section
def _importCreateFit(lines):
"""Create fit and set top-level entity (ship or citadel)."""
fit = Fit()
header = lines.pop(0)
m = re.match('\[(?P<shipType>[\w\s]+),\s*(?P<fitName>.+)\]', header)
if not m:
pyfalog.warning('service.port.eft.importEft: corrupted fit header')
raise EftImportError
shipType = m.group('shipType').strip()
fitName = m.group('fitName').strip()
try:
ship = _fetchItem(shipType)
try:
fit.ship = Ship(ship)
except ValueError:
fit.ship = Citadel(ship)
fit.name = fitName
except:
pyfalog.warning('service.port.eft.importEft: exception caught when parsing header')
raise EftImportError
return fit
def _fetchItem(typeName, eagerCat=False):
sMkt = Market.getInstance()
eager = 'group.category' if eagerCat else None
try:
item = sMkt.getItem(typeName, eager=eager)
except:
pyfalog.warning('service.port.eft: unable to fetch item "{}"'.format(typeName))
return None
if sMkt.getPublicityByItem(item):
return item
else:
return None
def _clearTail(lst):
while lst and lst[-1] is None:
del lst[-1]
class EftImportError(Exception):
"""Exception class emitted and consumed by EFT importer internally."""
...
class Section:
def __init__(self):
self.lines = []
self.itemSpecs = []
self.__itemDataCats = None
@property
def itemDataCats(self):
if self.__itemDataCats is None:
cats = set()
for itemSpec in self.itemSpecs:
if itemSpec is None:
continue
cats.add(itemSpec.item.category.name)
self.__itemDataCats = tuple(sorted(cats))
return self.__itemDataCats
@property
def isModuleRack(self):
return all(i is None or i.isModule for i in self.itemSpecs)
@property
def isImplantRack(self):
return all(i is not None and i.isImplant for i in self.itemSpecs)
@property
def isDroneBay(self):
return all(i is not None and i.isDrone for i in self.itemSpecs)
@property
def isFighterBay(self):
return all(i is not None and i.isFighter for i in self.itemSpecs)
@property
def isCargoHold(self):
return (
all(i is not None and i.isCargo for i in self.itemSpecs) and
not self.isDroneBay and not self.isFighterBay)
class BaseItemSpec:
def __init__(self, typeName):
item = _fetchItem(typeName, eagerCat=True)
if item is None:
raise EftImportError
self.typeName = typeName
self.item = item
@property
def isModule(self):
return False
@property
def isImplant(self):
return False
@property
def isDrone(self):
return False
@property
def isFighter(self):
return False
@property
def isCargo(self):
return False
class RegularItemSpec(BaseItemSpec):
def __init__(self, typeName, chargeName=None):
super().__init__(typeName)
self.charge = self.__fetchCharge(chargeName)
self.offline = False
self.mutationIdx = None
def __fetchCharge(self, chargeName):
if chargeName:
charge = _fetchItem(chargeName, eagerCat=True)
if not charge or charge.category.name != 'Charge':
charge = None
else:
charge = None
return charge
@property
def isModule(self):
return self.item.category.name in MODULE_CATS
@property
def isImplant(self):
return (
self.item.category.name == 'Implant' and (
'implantness' in self.item.attributes or
'boosterness' in self.item.attributes))
class MultiItemSpec(BaseItemSpec):
def __init__(self, typeName):
super().__init__(typeName)
self.amount = 0
@property
def isDrone(self):
return self.item.category.name == 'Drone'
@property
def isFighter(self):
return self.item.category.name == 'Fighter'
@property
def isCargo(self):
return True
class AbstractFit:
def __init__(self):
# Modules
self.modulesHigh = []
self.modulesMed = []
self.modulesLow = []
self.rigs = []
self.subsystems = []
self.services = []
# Non-modules
self.implants = []
self.boosters = []
self.drones = {} # Format: {item: Drone}
self.fighters = []
self.cargo = {} # Format: {item: Cargo}
# Other stuff
self.mutations = {} # Format: {reference: (mutaplamid item, {attr ID: attr value})}
@property
def __slotContainerMap(self):
return {
Slot.HIGH: self.modulesHigh,
Slot.MED: self.modulesMed,
Slot.LOW: self.modulesLow,
Slot.RIG: self.rigs,
Slot.SUBSYSTEM: self.subsystems,
Slot.SERVICE: self.services}
def getContainerBySlot(self, slotType):
return self.__slotContainerMap.get(slotType)
def getSlotByContainer(self, container):
slotType = None
for k, v in self.__slotContainerMap.items():
if v is container:
slotType = k
break
return slotType
def addModules(self, itemSpecs):
modules = []
slotTypes = set()
for itemSpec in itemSpecs:
if itemSpec is None:
modules.append(None)
continue
m = self.__makeModule(itemSpec)
if m is None:
modules.append(None)
continue
modules.append(m)
slotTypes.add(m.slot)
_clearTail(modules)
# If all the modules have same slot type, put them to appropriate
# container with stubs
if len(slotTypes) == 1:
slotType = tuple(slotTypes)[0]
self.getContainerBySlot(slotType).extend(modules)
# Otherwise, put just modules
else:
for m in modules:
if m is None:
continue
self.getContainerBySlot(m.slot).append(m)
def addModule(self, itemSpec):
if itemSpec is None:
return
m = self.__makeModule(itemSpec)
if m is not None:
self.getContainerBySlot(m.slot).append(m)
def __makeModule(self, itemSpec):
# Mutate item if needed
m = None
if itemSpec.mutationIdx in self.mutations:
mutaItem, mutaAttrs = self.mutations[itemSpec.mutationIdx]
mutaplasmid = getDynamicItem(mutaItem.ID)
if mutaplasmid:
try:
m = Module(mutaplasmid.resultingItem, itemSpec.item, mutaplasmid)
except ValueError:
pass
else:
for attrID, mutator in m.mutators.items():
if attrID in mutaAttrs:
mutator.value = mutaAttrs[attrID]
# If we still don't have item (item is not mutated or we
# failed to construct mutated item), try to make regular item
if m is None:
try:
m = Module(itemSpec.item)
except ValueError:
return None
if itemSpec.charge is not None and m.isValidCharge(itemSpec.charge):
m.charge = itemSpec.charge
if itemSpec.offline and m.isValidState(State.OFFLINE):
m.state = State.OFFLINE
elif m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
return m
def addImplant(self, itemSpec):
if itemSpec is None:
return
if 'implantness' in itemSpec.item.attributes:
self.implants.append(Implant(itemSpec.item))
elif 'boosterness' in itemSpec.item.attributes:
self.boosters.append(Booster(itemSpec.item))
else:
pyfalog.error('Failed to import implant: {}', itemSpec.typeName)
def addDrone(self, itemSpec):
if itemSpec is None:
return
if itemSpec.item not in self.drones:
self.drones[itemSpec.item] = Drone(itemSpec.item)
self.drones[itemSpec.item].amount += itemSpec.amount
def addFighter(self, itemSpec):
if itemSpec is None:
return
fighter = Fighter(itemSpec.item)
fighter.amount = itemSpec.amount
self.fighters.append(fighter)
def addCargo(self, itemSpec):
if itemSpec is None:
return
if itemSpec.item not in self.cargo:
self.cargo[itemSpec.item] = Cargo(itemSpec.item)
self.cargo[itemSpec.item].amount += itemSpec.amount

208
service/port/esi.py Normal file
View File

@@ -0,0 +1,208 @@
# =============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import collections
import json
from logbook import Logger
from eos.saveddata.cargo import Cargo
from eos.saveddata.citadel import Citadel
from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter
from eos.saveddata.fit import Fit
from eos.saveddata.module import Module, State, Slot
from eos.saveddata.ship import Ship
from service.fit import Fit as svcFit
from service.market import Market
class ESIExportException(Exception):
pass
pyfalog = Logger(__name__)
INV_FLAGS = {
Slot.LOW: 11,
Slot.MED: 19,
Slot.HIGH: 27,
Slot.RIG: 92,
Slot.SUBSYSTEM: 125,
Slot.SERVICE: 164
}
INV_FLAG_CARGOBAY = 5
INV_FLAG_DRONEBAY = 87
INV_FLAG_FIGHTER = 158
def exportESI(ofit):
# A few notes:
# max fit name length is 50 characters
# Most keys are created simply because they are required, but bogus data is okay
nested_dict = lambda: collections.defaultdict(nested_dict)
fit = nested_dict()
sFit = svcFit.getInstance()
# max length is 50 characters
name = ofit.name[:47] + '...' if len(ofit.name) > 50 else ofit.name
fit['name'] = name
fit['ship_type_id'] = ofit.ship.item.ID
# 2017/03/29 NOTE: "<" or "&lt;" is Ignored
# fit['description'] = "<pyfa:%d />" % ofit.ID
fit['description'] = ofit.notes[:397] + '...' if len(ofit.notes) > 400 else ofit.notes if ofit.notes is not None else ""
fit['items'] = []
slotNum = {}
charges = {}
for module in ofit.modules:
if module.isEmpty:
continue
item = nested_dict()
slot = module.slot
if slot == Slot.SUBSYSTEM:
# Order of subsystem matters based on this attr. See GH issue #130
slot = int(module.getModifiedItemAttr("subSystemSlot"))
item['flag'] = slot
else:
if slot not in slotNum:
slotNum[slot] = INV_FLAGS[slot]
item['flag'] = slotNum[slot]
slotNum[slot] += 1
item['quantity'] = 1
item['type_id'] = module.item.ID
fit['items'].append(item)
if module.charge and sFit.serviceFittingOptions["exportCharges"]:
if module.chargeID not in charges:
charges[module.chargeID] = 0
# `or 1` because some charges (ie scripts) are without qty
charges[module.chargeID] += module.numCharges or 1
for cargo in ofit.cargo:
item = nested_dict()
item['flag'] = INV_FLAG_CARGOBAY
item['quantity'] = cargo.amount
item['type_id'] = cargo.item.ID
fit['items'].append(item)
for chargeID, amount in list(charges.items()):
item = nested_dict()
item['flag'] = INV_FLAG_CARGOBAY
item['quantity'] = amount
item['type_id'] = chargeID
fit['items'].append(item)
for drone in ofit.drones:
item = nested_dict()
item['flag'] = INV_FLAG_DRONEBAY
item['quantity'] = drone.amount
item['type_id'] = drone.item.ID
fit['items'].append(item)
for fighter in ofit.fighters:
item = nested_dict()
item['flag'] = INV_FLAG_FIGHTER
item['quantity'] = fighter.amountActive
item['type_id'] = fighter.item.ID
fit['items'].append(item)
if len(fit['items']) == 0:
raise ESIExportException("Cannot export fitting: module list cannot be empty.")
return json.dumps(fit)
def importESI(str_):
sMkt = Market.getInstance()
fitobj = Fit()
refobj = json.loads(str_)
items = refobj['items']
# "<" and ">" is replace to "&lt;", "&gt;" by EVE client
fitobj.name = refobj['name']
# 2017/03/29: read description
fitobj.notes = refobj['description']
try:
ship = refobj['ship_type_id']
try:
fitobj.ship = Ship(sMkt.getItem(ship))
except ValueError:
fitobj.ship = Citadel(sMkt.getItem(ship))
except:
pyfalog.warning("Caught exception in importESI")
return None
items.sort(key=lambda k: k['flag'])
moduleList = []
for module in items:
try:
item = sMkt.getItem(module['type_id'], eager="group.category")
if not item.published:
continue
if module['flag'] == INV_FLAG_DRONEBAY:
d = Drone(item)
d.amount = module['quantity']
fitobj.drones.append(d)
elif module['flag'] == INV_FLAG_CARGOBAY:
c = Cargo(item)
c.amount = module['quantity']
fitobj.cargo.append(c)
elif module['flag'] == INV_FLAG_FIGHTER:
fighter = Fighter(item)
fitobj.fighters.append(fighter)
else:
try:
m = Module(item)
# When item can't be added to any slot (unknown item or just charge), ignore it
except ValueError:
pyfalog.debug("Item can't be added to any slot (unknown item or just charge)")
continue
# Add subsystems before modules to make sure T3 cruisers have subsystems installed
if item.category.name == "Subsystem":
if m.fits(fitobj):
fitobj.modules.append(m)
else:
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
moduleList.append(m)
except:
pyfalog.warning("Could not process module.")
continue
# Recalc to get slot numbers correct for T3 cruisers
svcFit.getInstance().recalc(fitobj)
for module in moduleList:
if module.fits(fitobj):
fitobj.modules.append(module)
return fitobj

68
service/port/multibuy.py Normal file
View File

@@ -0,0 +1,68 @@
# =============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
from service.fit import Fit as svcFit
from service.port.eft import SLOT_ORDER as EFT_SLOT_ORDER
def exportMultiBuy(fit):
export = "%s\n" % fit.ship.item.name
stuff = {}
sFit = svcFit.getInstance()
for module in fit.modules:
slot = module.slot
if slot not in stuff:
stuff[slot] = []
curr = "%s\n" % module.item.name if module.item else ""
if module.charge and sFit.serviceFittingOptions["exportCharges"]:
curr += "%s x%s\n" % (module.charge.name, module.numCharges)
stuff[slot].append(curr)
for slotType in EFT_SLOT_ORDER:
data = stuff.get(slotType)
if data is not None:
# export += "\n"
for curr in data:
export += curr
if len(fit.drones) > 0:
for drone in fit.drones:
export += "%s x%s\n" % (drone.item.name, drone.amount)
if len(fit.cargo) > 0:
for cargo in fit.cargo:
export += "%s x%s\n" % (cargo.item.name, cargo.amount)
if len(fit.implants) > 0:
for implant in fit.implants:
export += "%s\n" % implant.item.name
if len(fit.boosters) > 0:
for booster in fit.boosters:
export += "%s\n" % booster.item.name
if len(fit.fighters) > 0:
for fighter in fit.fighters:
export += "%s x%s\n" % (fighter.item.name, fighter.amountActive)
if export[-1] == "\n":
export = export[:-1]
return export

277
service/port/port.py Normal file
View File

@@ -0,0 +1,277 @@
# =============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import re
import os
import threading
import xml.dom
import xml.parsers.expat
from codecs import open
from bs4 import UnicodeDammit
from logbook import Logger
from eos import db
from eos.saveddata.fit import ImplantLocation
from service.fit import Fit as svcFit
from service.port.dna import exportDna, importDna
from service.port.eft import exportEft, importEft, importEftCfg
from service.port.esi import exportESI, importESI
from service.port.multibuy import exportMultiBuy
from service.port.shared import IPortUser, UserCancelException, processing_notify
from service.port.xml import importXml, exportXml
pyfalog = Logger(__name__)
# 2017/04/05 NOTE: simple validation, for xml file
RE_XML_START = r'<\?xml\s+version="1.0"\s*\?>'
class Port(object):
"""Service which houses all import/export format functions"""
instance = None
__tag_replace_flag = True
@classmethod
def getInstance(cls):
if cls.instance is None:
cls.instance = Port()
return cls.instance
@classmethod
def set_tag_replace(cls, b):
cls.__tag_replace_flag = b
@classmethod
def is_tag_replace(cls):
# might there is a person who wants to hold tags.
# (item link in EVE client etc. When importing again to EVE)
return cls.__tag_replace_flag
@staticmethod
def backupFits(path, iportuser):
pyfalog.debug("Starting backup fits thread.")
def backupFitsWorkerFunc(path, iportuser):
success = True
try:
iportuser.on_port_process_start()
backedUpFits = Port.exportXml(iportuser,
*svcFit.getInstance().getAllFits())
backupFile = open(path, "w", encoding="utf-8")
backupFile.write(backedUpFits)
backupFile.close()
except UserCancelException:
success = False
# Send done signal to GUI
# wx.CallAfter(callback, -1, "Done.")
flag = IPortUser.ID_ERROR if not success else IPortUser.ID_DONE
iportuser.on_port_processing(IPortUser.PROCESS_EXPORT | flag,
"User canceled or some error occurrence." if not success else "Done.")
threading.Thread(
target=backupFitsWorkerFunc,
args=(path, iportuser)
).start()
@staticmethod
def importFitsThreaded(paths, iportuser):
# type: (tuple, IPortUser) -> None
"""
:param paths: fits data file path list.
:param iportuser: IPortUser implemented class.
:rtype: None
"""
pyfalog.debug("Starting import fits thread.")
def importFitsFromFileWorkerFunc(paths, iportuser):
iportuser.on_port_process_start()
success, result = Port.importFitFromFiles(paths, iportuser)
flag = IPortUser.ID_ERROR if not success else IPortUser.ID_DONE
iportuser.on_port_processing(IPortUser.PROCESS_IMPORT | flag, result)
threading.Thread(
target=importFitsFromFileWorkerFunc,
args=(paths, iportuser)
).start()
@staticmethod
def importFitFromFiles(paths, iportuser=None):
"""
Imports fits from file(s). First processes all provided paths and stores
assembled fits into a list. This allows us to call back to the GUI as
fits are processed as well as when fits are being saved.
returns
"""
sFit = svcFit.getInstance()
fit_list = []
try:
for path in paths:
if iportuser: # Pulse
msg = "Processing file:\n%s" % path
pyfalog.debug(msg)
processing_notify(iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, msg)
# wx.CallAfter(callback, 1, msg)
with open(path, "rb") as file_:
srcString = file_.read()
dammit = UnicodeDammit(srcString)
srcString = dammit.unicode_markup
if len(srcString) == 0: # ignore blank files
pyfalog.debug("File is blank.")
continue
try:
_, fitsImport = Port.importAuto(srcString, path, iportuser=iportuser)
fit_list += fitsImport
except xml.parsers.expat.ExpatError:
pyfalog.warning("Malformed XML in:\n{0}", path)
return False, "Malformed XML in %s" % path
# IDs = [] # NOTE: what use for IDs?
numFits = len(fit_list)
for idx, fit in enumerate(fit_list):
# Set some more fit attributes and save
fit.character = sFit.character
fit.damagePattern = sFit.pattern
fit.targetResists = sFit.targetResists
if len(fit.implants) > 0:
fit.implantLocation = ImplantLocation.FIT
else:
useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"]
fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT
db.save(fit)
# IDs.append(fit.ID)
if iportuser: # Pulse
pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", idx + 1, numFits)
processing_notify(
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
"Processing complete, saving fits to database\n(%d/%d) %s" % (idx + 1, numFits, fit.ship.name)
)
except UserCancelException:
return False, "Processing has been canceled.\n"
except Exception as e:
pyfalog.critical("Unknown exception processing: {0}", path)
pyfalog.critical(e)
# TypeError: not all arguments converted during string formatting
# return False, "Unknown Error while processing {0}" % path
return False, "Unknown error while processing %s\n\n Error: %s" % (path, e.message)
return True, fit_list
@staticmethod
def importFitFromBuffer(bufferStr, activeFit=None):
# type: (str, object) -> object
# TODO: catch the exception?
# activeFit is reserved?, bufferStr is unicode? (assume only clipboard string?
sFit = svcFit.getInstance()
_, fits = Port.importAuto(bufferStr, activeFit=activeFit)
for fit in fits:
fit.character = sFit.character
fit.damagePattern = sFit.pattern
fit.targetResists = sFit.targetResists
if len(fit.implants) > 0:
fit.implantLocation = ImplantLocation.FIT
else:
useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"]
fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT
db.save(fit)
return fits
@classmethod
def importAuto(cls, string, path=None, activeFit=None, iportuser=None):
# type: (Port, str, str, object, IPortUser) -> object
# Get first line and strip space symbols of it to avoid possible detection errors
firstLine = re.split("[\n\r]+", string.strip(), maxsplit=1)[0]
firstLine = firstLine.strip()
# If XML-style start of tag encountered, detect as XML
if re.search(RE_XML_START, firstLine):
return "XML", cls.importXml(string, iportuser)
# If JSON-style start, parse as CREST/JSON
if firstLine[0] == '{':
return "JSON", (cls.importESI(string),)
# If we've got source file name which is used to describe ship name
# and first line contains something like [setup name], detect as eft config file
if re.match("\[.*\]", firstLine) and path is not None:
filename = os.path.split(path)[1]
shipName = filename.rsplit('.')[0]
return "EFT Config", cls.importEftCfg(shipName, string, iportuser)
# If no file is specified and there's comma between brackets,
# consider that we have [ship, setup name] and detect like eft export format
if re.match("\[.*,.*\]", firstLine):
return "EFT", (cls.importEft(string),)
# Use DNA format for all other cases
return "DNA", (cls.importDna(string),)
# EFT-related methods
@staticmethod
def importEft(eftString):
return importEft(eftString)
@staticmethod
def importEftCfg(shipname, contents, iportuser=None):
return importEftCfg(shipname, contents, iportuser)
@classmethod
def exportEft(cls, fit, options):
return exportEft(fit, options)
# DNA-related methods
@staticmethod
def importDna(string):
return importDna(string)
@staticmethod
def exportDna(fit):
return exportDna(fit)
# ESI-related methods
@staticmethod
def importESI(string):
return importESI(string)
@staticmethod
def exportESI(fit):
return exportESI(fit)
# XML-related methods
@staticmethod
def importXml(text, iportuser=None):
return importXml(text, iportuser)
@staticmethod
def exportXml(iportuser=None, *fits):
return exportXml(iportuser, *fits)
# Multibuy-related methods
@staticmethod
def exportMultiBuy(fit):
return exportMultiBuy(fit)

70
service/port/shared.py Normal file
View File

@@ -0,0 +1,70 @@
# =============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
from abc import ABCMeta, abstractmethod
class UserCancelException(Exception):
"""when user cancel on port processing."""
pass
class IPortUser(metaclass=ABCMeta):
ID_PULSE = 1
# Pulse the progress bar
ID_UPDATE = ID_PULSE << 1
# Replace message with data: update messate
ID_DONE = ID_PULSE << 2
# open fits: import process done
ID_ERROR = ID_PULSE << 3
# display error: raise some error
PROCESS_IMPORT = ID_PULSE << 4
# means import process.
PROCESS_EXPORT = ID_PULSE << 5
# means import process.
@abstractmethod
def on_port_processing(self, action, data=None):
"""
While importing fits from file, the logic calls back to this function to
update progress bar to show activity. XML files can contain multiple
ships with multiple fits, whereas EFT cfg files contain many fits of
a single ship. When iterating through the files, we update the message
when we start a new file, and then Pulse the progress bar with every fit
that is processed.
action : a flag that lets us know how to deal with :data
None: Pulse the progress bar
1: Replace message with data
other: Close dialog and handle based on :action (-1 open fits, -2 display error)
"""
"""return: True is continue process, False is cancel."""
pass
def on_port_process_start(self):
pass
def processing_notify(iportuser, flag, data):
if not iportuser.on_port_processing(flag, data):
raise UserCancelException

326
service/port/xml.py Normal file
View File

@@ -0,0 +1,326 @@
# =============================================================================
# Copyright (C) 2014 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import re
import xml.dom
import xml.parsers.expat
from logbook import Logger
from eos.saveddata.cargo import Cargo
from eos.saveddata.citadel import Citadel
from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter
from eos.saveddata.fit import Fit
from eos.saveddata.module import Module, State, Slot
from eos.saveddata.ship import Ship
from service.fit import Fit as svcFit
from service.market import Market
from utils.strfunctions import sequential_rep, replace_ltgt
from service.port.shared import IPortUser, processing_notify
pyfalog = Logger(__name__)
# -- 170327 Ignored description --
RE_LTGT = "&(lt|gt);"
L_MARK = "&lt;localized hint=&quot;"
# &lt;localized hint=&quot;([^"]+)&quot;&gt;([^\*]+)\*&lt;\/localized&gt;
LOCALIZED_PATTERN = re.compile(r'<localized hint="([^"]+)">([^\*]+)\*</localized>')
def _extract_match(t):
m = LOCALIZED_PATTERN.match(t)
# hint attribute, text content
return m.group(1), m.group(2)
def _resolve_ship(fitting, sMkt, b_localized):
# type: (xml.dom.minidom.Element, service.market.Market, bool) -> eos.saveddata.fit.Fit
""" NOTE: Since it is meaningless unless a correct ship object can be constructed,
process flow changed
"""
# ------ Confirm ship
# <localized hint="Maelstrom">Maelstrom</localized>
shipType = fitting.getElementsByTagName("shipType").item(0).getAttribute("value")
anything = None
if b_localized:
# expect an official name, emergency cache
shipType, anything = _extract_match(shipType)
limit = 2
ship = None
while True:
must_retry = False
try:
try:
ship = Ship(sMkt.getItem(shipType))
except ValueError:
ship = Citadel(sMkt.getItem(shipType))
except Exception as e:
pyfalog.warning("Caught exception on _resolve_ship")
pyfalog.error(e)
limit -= 1
if limit is 0:
break
shipType = anything
must_retry = True
if not must_retry:
break
if ship is None:
raise Exception("cannot resolve ship type.")
fitobj = Fit(ship=ship)
# ------ Confirm fit name
anything = fitting.getAttribute("name")
# 2017/03/29 NOTE:
# if fit name contained "<" or ">" then reprace to named html entity by EVE client
# if re.search(RE_LTGT, anything):
if "&lt;" in anything or "&gt;" in anything:
anything = replace_ltgt(anything)
fitobj.name = anything
return fitobj
def _resolve_module(hardware, sMkt, b_localized):
# type: (xml.dom.minidom.Element, service.market.Market, bool) -> eos.saveddata.module.Module
moduleName = hardware.getAttribute("type")
emergency = None
if b_localized:
# expect an official name, emergency cache
moduleName, emergency = _extract_match(moduleName)
item = None
limit = 2
while True:
must_retry = False
try:
item = sMkt.getItem(moduleName, eager="group.category")
except Exception as e:
pyfalog.warning("Caught exception on _resolve_module")
pyfalog.error(e)
limit -= 1
if limit is 0:
break
moduleName = emergency
must_retry = True
if not must_retry:
break
return item
def importXml(text, iportuser):
from .port import Port
# type: (str, IPortUser) -> list[eos.saveddata.fit.Fit]
sMkt = Market.getInstance()
doc = xml.dom.minidom.parseString(text)
# NOTE:
# When L_MARK is included at this point,
# Decided to be localized data
b_localized = L_MARK in text
fittings = doc.getElementsByTagName("fittings").item(0)
fittings = fittings.getElementsByTagName("fitting")
fit_list = []
failed = 0
for fitting in fittings:
try:
fitobj = _resolve_ship(fitting, sMkt, b_localized)
except:
failed += 1
continue
# -- 170327 Ignored description --
# read description from exported xml. (EVE client, EFT)
description = fitting.getElementsByTagName("description").item(0).getAttribute("value")
if description is None:
description = ""
elif len(description):
# convert <br> to "\n" and remove html tags.
if Port.is_tag_replace():
description = replace_ltgt(
sequential_rep(description, r"<(br|BR)>", "\n", r"<[^<>]+>", "")
)
fitobj.notes = description
hardwares = fitting.getElementsByTagName("hardware")
moduleList = []
for hardware in hardwares:
try:
item = _resolve_module(hardware, sMkt, b_localized)
if not item or not item.published:
continue
if item.category.name == "Drone":
d = Drone(item)
d.amount = int(hardware.getAttribute("qty"))
fitobj.drones.append(d)
elif item.category.name == "Fighter":
ft = Fighter(item)
ft.amount = int(hardware.getAttribute("qty")) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize
fitobj.fighters.append(ft)
elif hardware.getAttribute("slot").lower() == "cargo":
# although the eve client only support charges in cargo, third-party programs
# may support items or "refits" in cargo. Support these by blindly adding all
# cargo, not just charges
c = Cargo(item)
c.amount = int(hardware.getAttribute("qty"))
fitobj.cargo.append(c)
else:
try:
m = Module(item)
# When item can't be added to any slot (unknown item or just charge), ignore it
except ValueError:
pyfalog.warning("item can't be added to any slot (unknown item or just charge), ignore it")
continue
# Add subsystems before modules to make sure T3 cruisers have subsystems installed
if item.category.name == "Subsystem":
if m.fits(fitobj):
m.owner = fitobj
fitobj.modules.append(m)
else:
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
moduleList.append(m)
except KeyboardInterrupt:
pyfalog.warning("Keyboard Interrupt")
continue
# Recalc to get slot numbers correct for T3 cruisers
svcFit.getInstance().recalc(fitobj)
for module in moduleList:
if module.fits(fitobj):
module.owner = fitobj
fitobj.modules.append(module)
fit_list.append(fitobj)
if iportuser: # NOTE: Send current processing status
processing_notify(
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
"Processing %s\n%s" % (fitobj.ship.name, fitobj.name)
)
return fit_list
def exportXml(iportuser, *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:
fitting = doc.createElement("fitting")
fitting.setAttribute("name", fit.name)
fittings.appendChild(fitting)
description = doc.createElement("description")
# -- 170327 Ignored description --
try:
notes = fit.notes # unicode
if notes:
notes = notes[:397] + '...' if len(notes) > 400 else notes
description.setAttribute(
"value", re.sub("(\r|\n|\r\n)+", "<br>", notes) if notes is not None else ""
)
except Exception as e:
pyfalog.warning("read description is failed, msg=%s\n" % e.args)
fitting.appendChild(description)
shipType = doc.createElement("shipType")
shipType.setAttribute("value", fit.ship.name)
fitting.appendChild(shipType)
charges = {}
slotNum = {}
for module in fit.modules:
if module.isEmpty:
continue
slot = module.slot
if slot == Slot.SUBSYSTEM:
# Order of subsystem matters based on this attr. See GH issue #130
slotId = module.getModifiedItemAttr("subSystemSlot") - 125
else:
if slot not in slotNum:
slotNum[slot] = 0
slotId = slotNum[slot]
slotNum[slot] += 1
hardware = doc.createElement("hardware")
hardware.setAttribute("type", module.item.name)
slotName = Slot.getName(slot).lower()
slotName = slotName if slotName != "high" else "hi"
hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId))
fitting.appendChild(hardware)
if module.charge and sFit.serviceFittingOptions["exportCharges"]:
if module.charge.name not in charges:
charges[module.charge.name] = 0
# `or 1` because some charges (ie scripts) are without qty
charges[module.charge.name] += module.numCharges or 1
for drone in fit.drones:
hardware = doc.createElement("hardware")
hardware.setAttribute("qty", "%d" % drone.amount)
hardware.setAttribute("slot", "drone bay")
hardware.setAttribute("type", drone.item.name)
fitting.appendChild(hardware)
for fighter in fit.fighters:
hardware = doc.createElement("hardware")
hardware.setAttribute("qty", "%d" % fighter.amountActive)
hardware.setAttribute("slot", "fighter bay")
hardware.setAttribute("type", fighter.item.name)
fitting.appendChild(hardware)
for cargo in fit.cargo:
if cargo.item.name not in charges:
charges[cargo.item.name] = 0
charges[cargo.item.name] += cargo.amount
for name, qty in list(charges.items()):
hardware = doc.createElement("hardware")
hardware.setAttribute("qty", "%d" % qty)
hardware.setAttribute("slot", "cargo")
hardware.setAttribute("type", name)
fitting.appendChild(hardware)
except Exception as e:
pyfalog.error("Failed on fitID: %d, message: %s" % e.message)
continue
finally:
if iportuser:
processing_notify(
iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE,
(i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name))
)
return doc.toprettyxml()