From 4e5a70993e661f5ae5693b0868ef11f7d66ffcee Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 16:15:23 +0300 Subject: [PATCH] Split XML facilities into their own file as well --- service/port/port.py | 331 +++---------------------------------------- service/port/xml.py | 325 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+), 314 deletions(-) create mode 100644 service/port/xml.py diff --git a/service/port/port.py b/service/port/port.py index 97acaba98..045f5561e 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -19,135 +19,29 @@ import re import os -import xml.dom -from logbook import Logger -import collections -import json import threading -from bs4 import UnicodeDammit - - +import xml.dom +import xml.parsers.expat from codecs import open -import xml.parsers.expat +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 - -# noinspection PyPackageRequirements -import wx - -from eos.saveddata.cargo import Cargo -from eos.saveddata.drone import Drone -from eos.saveddata.fighter import Fighter -from eos.saveddata.module import Module, State, Slot -from eos.saveddata.ship import Ship -from eos.saveddata.citadel import Citadel -from eos.saveddata.fit import Fit, ImplantLocation -from service.market import Market -from utils.strfunctions import sequential_rep, replace_ltgt - from service.port.dna import exportDna, importDna from service.port.eft import EftPort, SLOT_ORDER as EFT_SLOT_ORDER from service.port.esi import importESI, exportESI 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*\?>' -# -- 170327 Ignored description -- -RE_LTGT = "&(lt|gt);" -L_MARK = "<localized hint="" -# <localized hint="([^"]+)">([^\*]+)\*<\/localized> -LOCALIZED_PATTERN = re.compile(r'([^\*]+)\*') - - -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 - # Maelstrom - 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 "<" in anything or ">" 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 - class Port(object): """Service which houses all import/export format functions""" @@ -288,7 +182,7 @@ class Port(object): @staticmethod def importFitFromBuffer(bufferStr, activeFit=None): - # type: (basestring, object) -> object + # type: (str, object) -> object # TODO: catch the exception? # activeFit is reserved?, bufferStr is unicode? (assume only clipboard string? sFit = svcFit.getInstance() @@ -307,7 +201,7 @@ class Port(object): @classmethod def importAuto(cls, string, path=None, activeFit=None, iportuser=None): - # type: (basestring, basestring, object, IPortUser, basestring) -> object + # 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() @@ -335,206 +229,6 @@ class Port(object): # Use DNA format for all other cases return "DNA", (cls.importDna(string),) - @staticmethod - def importXml(text, iportuser=None): - # 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
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 - - @staticmethod - def exportXml(iportuser=None, *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)+", "
", 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: - # print("Failed on fitID: %d" % fit.ID) - 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)) - ) -# wx.CallAfter(callback, i, "(%s/%s) %s" % (i, fit_count, fit.ship.name)) - - return doc.toprettyxml() - @staticmethod def exportMultiBuy(fit): export = "%s\n" % fit.ship.item.name @@ -611,3 +305,12 @@ class Port(object): @staticmethod def exportESI(fit): exportESI(fit) + + # XML-related methods + @staticmethod + def importXml(text, iportuser=None): + return importXml(text, iportuser) + + @staticmethod + def exportXml(iportuser=None, *fits): + exportXml(iportuser, *fits) diff --git a/service/port/xml.py b/service/port/xml.py new file mode 100644 index 000000000..d5a5f08e0 --- /dev/null +++ b/service/port/xml.py @@ -0,0 +1,325 @@ +# ============================================================================= +# 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 . +# ============================================================================= + +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 = "<localized hint="" +# <localized hint="([^"]+)">([^\*]+)\*<\/localized> +LOCALIZED_PATTERN = re.compile(r'([^\*]+)\*') + + +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 + # Maelstrom + 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 "<" in anything or ">" 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): + # 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
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)+", "
", 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()