diff --git a/eos/saveddata/ship.py b/eos/saveddata/ship.py index b2b8e1d9e..90586dbd7 100644 --- a/eos/saveddata/ship.py +++ b/eos/saveddata/ship.py @@ -71,6 +71,11 @@ class Ship(ItemAttrShortcut, HandledItem): def item(self): return self.__item + @property + def name(self): + # NOTE: add name property + return self.__item.name + @property def itemModifiedAttributes(self): return self.__itemModifiedAttributes diff --git a/gui/mainFrame.py b/gui/mainFrame.py index b7c925497..77d0c9744 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -73,7 +73,7 @@ from service.update import Update 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 +from service.port import Port, IPortUser from service.settings import HTMLExportSettings from time import gmtime, strftime @@ -138,7 +138,7 @@ class OpenFitsThread(threading.Thread): wx.CallAfter(self.callback) -class MainFrame(wx.Frame): +class MainFrame(wx.Frame, IPortUser): __instance = None @classmethod @@ -422,8 +422,7 @@ class MainFrame(wx.Frame): format_ = dlg.GetFilterIndex() path = dlg.GetPath() if format_ == 0: - sPort = Port.getInstance() - output = sPort.exportXml(None, fit) + output = Port.exportXml(None, fit) if '.' not in os.path.basename(path): path += ".xml" else: @@ -814,7 +813,6 @@ class MainFrame(wx.Frame): def fileImportDialog(self, event): """Handles importing single/multiple EVE XML / EFT cfg fit files""" - sPort = Port.getInstance() dlg = wx.FileDialog( self, "Open One Or More Fitting Files", @@ -828,10 +826,10 @@ class MainFrame(wx.Frame): "Importing fits", " " * 100, # set some arbitrary spacing to create width in window parent=self, - style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME + style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL ) - self.progressDialog.message = None - sPort.importFitsThreaded(dlg.GetPaths(), self.fileImportCallback) + # self.progressDialog.message = None + Port.importFitsThreaded(dlg.GetPaths(), self) self.progressDialog.ShowModal() try: dlg.Destroy() @@ -863,9 +861,9 @@ class MainFrame(wx.Frame): "Backing up %d fits to: %s" % (max_, filePath), maximum=max_, parent=self, - style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME, + style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL ) - Port().backupFits(filePath, self.backupCallback) + Port.backupFits(filePath, self) self.progressDialog.ShowModal() def exportHtml(self, event): @@ -903,7 +901,19 @@ class MainFrame(wx.Frame): else: self.progressDialog.Update(info) - def fileImportCallback(self, action, data=None): + def on_port_process_start(self): + # flag for progress dialog. + self.__progress_flag = True + + def on_port_processing(self, action, data=None): + # 2017/03/29 NOTE: implementation like interface + wx.CallAfter( + self._on_port_processing, action, data + ) + + return self.__progress_flag + + def _on_port_processing(self, action, data): """ While importing fits from file, the logic calls back to this function to update progress bar to show activity. XML files can contain multiple @@ -917,22 +927,38 @@ class MainFrame(wx.Frame): 1: Replace message with data other: Close dialog and handle based on :action (-1 open fits, -2 display error) """ - - if action is None: - self.progressDialog.Pulse() - elif action == 1 and data != self.progressDialog.message: - self.progressDialog.message = data - self.progressDialog.Pulse(data) - else: + _message = None + if action & IPortUser.ID_ERROR: self.closeProgressDialog() - if action == -1: - self._openAfterImport(data) - elif action == -2: - dlg = wx.MessageDialog(self, - "The following error was generated\n\n%s\n\nBe aware that already processed fits were not saved" % data, - "Import Error", wx.OK | wx.ICON_ERROR) - if dlg.ShowModal() == wx.ID_OK: - return + _message = "Import Error" if action & IPortUser.PROCESS_IMPORT else "Export Error" + dlg = wx.MessageDialog(self, + "The following error was generated\n\n%s\n\nBe aware that already processed fits were not saved" % data, + _message, wx.OK | wx.ICON_ERROR) + # if dlg.ShowModal() == wx.ID_OK: + # return + dlg.ShowModal() + return + + # data is str + if action & IPortUser.PROCESS_IMPORT: + if action & IPortUser.ID_PULSE: + _message = () + # update message + elif action & IPortUser.ID_UPDATE: # and data != self.progressDialog.message: + _message = data + + if _message is not None: + self.__progress_flag, _unuse = self.progressDialog.Pulse(_message) + else: + self.closeProgressDialog() + if action & IPortUser.ID_DONE: + self._openAfterImport(data) + # data is tuple(int, str) + elif action & IPortUser.PROCESS_EXPORT: + if action & IPortUser.ID_DONE: + self.closeProgressDialog() + else: + self.__progress_flag, _unuse = self.progressDialog.Update(data[0], data[1]) def _openAfterImport(self, fits): if len(fits) > 0: diff --git a/service/port.py b/service/port.py index 3e923ed0f..3e8a98330 100644 --- a/service/port.py +++ b/service/port.py @@ -46,6 +46,8 @@ from eos.saveddata.ship import Ship from eos.saveddata.citadel import Citadel from eos.saveddata.fit import Fit from service.market import Market +from utils.strfunctions import sequential_rep, replace_ltgt +from abc import ABCMeta, abstractmethod if 'wxMac' not in wx.PlatformInfo or ('wxMac' in wx.PlatformInfo and wx.VERSION >= (3, 0)): from service.crest import Crest @@ -70,9 +72,152 @@ INV_FLAG_CARGOBAY = 5 INV_FLAG_DRONEBAY = 87 INV_FLAG_FIGHTER = 158 +# 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 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 + class Port(object): + """ + 2017/03/31 NOTE: About change + 1. want to keep the description recorded in fit + 2. i think should not write wx.CallAfter in here + """ instance = None + __tag_replace_flag = True @classmethod def getInstance(cls): @@ -81,20 +226,44 @@ class Port(object): 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, callback): + def backupFits(path, iportuser): pyfalog.debug("Starting backup fits thread.") - thread = FitBackupThread(path, callback) - thread.start() +# thread = FitBackupThread(path, callback) +# thread.start() + threading.Thread( + target=PortProcessing.backupFits, + args=(path, iportuser) + ).start() @staticmethod - def importFitsThreaded(paths, callback): + 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.") - thread = FitImportThread(paths, callback) - thread.start() +# thread = FitImportThread(paths, iportuser) +# thread.start() + threading.Thread( + target=PortProcessing.importFitsFromFile, + args=(paths, iportuser) + ).start() @staticmethod - def importFitFromFiles(paths, callback=None): + 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 @@ -104,99 +273,109 @@ class Port(object): defcodepage = locale.getpreferredencoding() sFit = svcFit.getInstance() - fits = [] - for path in paths: - if callback: # Pulse - pyfalog.debug("Processing file:\n{0}", path) - wx.CallAfter(callback, 1, "Processing file:\n%s" % path) + fit_list = [] + try: + for path in paths: + if iportuser: # Pulse + msg = "Processing file:\n%s" % path + pyfalog.debug(msg) + PortProcessing.notify(iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, msg) + # wx.CallAfter(callback, 1, msg) - file_ = open(path, "r") - srcString = file_.read() + with open(path, "r") as file_: + srcString = file_.read() - if len(srcString) == 0: # ignore blank files - pyfalog.debug("File is blank.") - continue + if len(srcString) == 0: # ignore blank files + pyfalog.debug("File is blank.") + continue - codec_found = None - # If file had ANSI encoding, decode it to unicode using detection - # of BOM header or if there is no header try default - # codepage then fallback to utf-16, cp1252 + codec_found = None + # If file had ANSI encoding, decode it to unicode using detection + # of BOM header or if there is no header try default + # codepage then fallback to utf-16, cp1252 - if isinstance(srcString, str): - savebom = None + if isinstance(srcString, str): + savebom = None - encoding_map = ( - ('\xef\xbb\xbf', 'utf-8'), - ('\xff\xfe\0\0', 'utf-32'), - ('\0\0\xfe\xff', 'UTF-32BE'), - ('\xff\xfe', 'utf-16'), - ('\xfe\xff', 'UTF-16BE')) + encoding_map = ( + ('\xef\xbb\xbf', 'utf-8'), + ('\xff\xfe\0\0', 'utf-32'), + ('\0\0\xfe\xff', 'UTF-32BE'), + ('\xff\xfe', 'utf-16'), + ('\xfe\xff', 'UTF-16BE')) - for bom, encoding in encoding_map: - if srcString.startswith(bom): - codec_found = encoding - savebom = bom + for bom, encoding in encoding_map: + if srcString.startswith(bom): + codec_found = encoding + savebom = bom + + if codec_found is None: + pyfalog.info("Unicode BOM not found in file {0}.", path) + attempt_codecs = (defcodepage, "utf-8", "utf-16", "cp1252") + + for page in attempt_codecs: + try: + pyfalog.info("Attempting to decode file {0} using {1} page.", path, page) + srcString = unicode(srcString, page) + codec_found = page + pyfalog.info("File {0} decoded using {1} page.", path, page) + except UnicodeDecodeError: + pyfalog.info("Error unicode decoding {0} from page {1}, trying next codec", path, page) + else: + break + else: + pyfalog.info("Unicode BOM detected in {0}, using {1} page.", path, codec_found) + srcString = unicode(srcString[len(savebom):], codec_found) + + else: + # nasty hack to detect other transparent utf-16 loading + if srcString[0] == '<' and 'utf-16' in srcString[:128].lower(): + codec_found = "utf-16" + else: + codec_found = "utf-8" if codec_found is None: - pyfalog.info("Unicode BOM not found in file {0}.", path) - attempt_codecs = (defcodepage, "utf-8", "utf-16", "cp1252") + return False, "Proper codec could not be established for %s" % path - for page in attempt_codecs: - try: - pyfalog.info("Attempting to decode file {0} using {1} page.", path, page) - srcString = unicode(srcString, page) - codec_found = page - pyfalog.info("File {0} decoded using {1} page.", path, page) - except UnicodeDecodeError: - pyfalog.info("Error unicode decoding {0} from page {1}, trying next codec", path, page) - else: - break - else: - pyfalog.info("Unicode BOM detected in {0}, using {1} page.", path, codec_found) - srcString = unicode(srcString[len(savebom):], codec_found) + try: + _, fitsImport = Port.importAuto(srcString, path, iportuser=iportuser, encoding=codec_found) + fit_list += fitsImport + except xml.parsers.expat.ExpatError: + pyfalog.warning("Malformed XML in:\n{0}", path) + return False, "Malformed XML in %s" % path - else: - # nasty hack to detect other transparent utf-16 loading - if srcString[0] == '<' and 'utf-16' in srcString[:128].lower(): - codec_found = "utf-16" - else: - codec_found = "utf-8" + # 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 + db.save(fit) + # IDs.append(fit.ID) + if iportuser: # Pulse + pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", idx + 1, numFits) + PortProcessing.notify( + iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, + "Processing complete, saving fits to database\n(%d/%d) %s" % (idx + 1, numFits, fit.ship.name) + ) - if codec_found is None: - return False, "Proper codec could not be established for %s" % path + 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 message: %s" % (path, e.message) - try: - _, fitsImport = Port.importAuto(srcString, path, callback=callback, encoding=codec_found) - fits += fitsImport - except xml.parsers.expat.ExpatError: - pyfalog.warning("Malformed XML in:\n{0}", path) - return False, "Malformed XML in %s" % path - except Exception as e: - pyfalog.critical("Unknown exception processing: {0}", path) - pyfalog.critical(e) - return False, "Unknown Error while processing {0}" % path - - IDs = [] - numFits = len(fits) - for i, fit in enumerate(fits): - # Set some more fit attributes and save - fit.character = sFit.character - fit.damagePattern = sFit.pattern - fit.targetResists = sFit.targetResists - db.save(fit) - IDs.append(fit.ID) - if callback: # Pulse - pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", i + 1, numFits) - wx.CallAfter( - callback, 1, - "Processing complete, saving fits to database\n(%d/%d)" % - (i + 1, numFits) - ) - - return True, fits + return True, fit_list @staticmethod def importFitFromBuffer(bufferStr, activeFit=None): + # type: (basestring, 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: @@ -228,7 +407,9 @@ class Port(object): fit['ship']['id'] = ofit.ship.item.ID fit['ship']['name'] = '' - fit['description'] = "" % ofit.ID + # 2017/03/29 NOTE: "<" or "<" is Ignored + # fit['description'] = "" % ofit.ID + fit['description'] = ofit.notes if ofit.notes is not None else "" fit['items'] = [] slotNum = {} @@ -302,17 +483,18 @@ class Port(object): return json.dumps(fit) @classmethod - def importAuto(cls, string, path=None, activeFit=None, callback=None, encoding=None): + def importAuto(cls, string, path=None, activeFit=None, iportuser=None, encoding=None): + # type: (basestring, basestring, object, IPortUser, basestring) -> 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.match("<", firstLine): + if re.search(RE_XML_START, firstLine): if encoding: - return "XML", cls.importXml(string, callback, encoding) + return "XML", cls.importXml(string, iportuser, encoding) else: - return "XML", cls.importXml(string, callback) + return "XML", cls.importXml(string, iportuser) # If JSON-style start, parse as CREST/JSON if firstLine[0] == '{': @@ -323,7 +505,7 @@ class Port(object): 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, callback) + 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 @@ -335,22 +517,26 @@ class Port(object): @staticmethod def importCrest(str_): - fit = json.loads(str_) - sMkt = Market.getInstance() - f = Fit() - f.name = fit['name'] + sMkt = Market.getInstance() + fitobj = Fit() + refobj = json.loads(str_) + items = refobj['items'] + # "<" and ">" is replace to "<", ">" by EVE client + fitobj.name = refobj['name'] + # 2017/03/29: read description + fitobj.notes = refobj['description'] try: + refobj = refobj['ship']['id'] try: - f.ship = Ship(sMkt.getItem(fit['ship']['id'])) + fitobj.ship = Ship(sMkt.getItem(refobj)) except ValueError: - f.ship = Citadel(sMkt.getItem(fit['ship']['id'])) + fitobj.ship = Citadel(sMkt.getItem(refobj)) except: pyfalog.warning("Caught exception in importCrest") return None - items = fit['items'] items.sort(key=lambda k: k['flag']) moduleList = [] @@ -360,14 +546,14 @@ class Port(object): if module['flag'] == INV_FLAG_DRONEBAY: d = Drone(item) d.amount = module['quantity'] - f.drones.append(d) + fitobj.drones.append(d) elif module['flag'] == INV_FLAG_CARGOBAY: c = Cargo(item) c.amount = module['quantity'] - f.cargo.append(c) + fitobj.cargo.append(c) elif module['flag'] == INV_FLAG_FIGHTER: fighter = Fighter(item) - f.fighters.append(fighter) + fitobj.fighters.append(fighter) else: try: m = Module(item) @@ -377,8 +563,8 @@ class Port(object): 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) + if m.fits(fitobj): + fitobj.modules.append(m) else: if m.isValidState(State.ACTIVE): m.state = State.ACTIVE @@ -390,13 +576,13 @@ class Port(object): continue # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(f) + svcFit.getInstance().recalc(fitobj) for module in moduleList: - if module.fits(f): - f.modules.append(module) + if module.fits(fitobj): + fitobj.modules.append(module) - return f + return fitobj @staticmethod def importDna(string): @@ -635,7 +821,7 @@ class Port(object): return fit @staticmethod - def importEftCfg(shipname, contents, callback=None): + def importEftCfg(shipname, contents, iportuser=None): """Handle import from EFT config store file""" # Check if we have such ship in database, bail if we don't @@ -648,7 +834,7 @@ class Port(object): # If client didn't take care of encoding file contents into Unicode, # do it using fallback encoding ourselves if isinstance(contents, str): - contents = unicode(contents, "cp1252") + contents = unicode(contents, locale.getpreferredencoding()) fits = [] # List for fits fitIndices = [] # List for starting line numbers for each fit @@ -671,14 +857,14 @@ class Port(object): try: # Create fit object - f = Fit() + fitobj = Fit() # Strip square brackets and pull out a fit name - f.name = fitLines[0][1:-1] + fitobj.name = fitLines[0][1:-1] # Assign ship to fitting try: - f.ship = Ship(sMkt.getItem(shipname)) + fitobj.ship = Ship(sMkt.getItem(shipname)) except ValueError: - f.ship = Citadel(sMkt.getItem(shipname)) + fitobj.ship = Citadel(sMkt.getItem(shipname)) moduleList = [] for x in range(1, len(fitLines)): @@ -689,6 +875,8 @@ class Port(object): # 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) @@ -713,11 +901,11 @@ class Port(object): d.amountActive = droneAmount elif entityState == "Inactive": d.amountActive = 0 - f.drones.append(d) + 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 - f.fighters.append(ft) + fitobj.fighters.append(ft) else: continue elif entityType == "Implant": @@ -735,7 +923,7 @@ class Port(object): imp.active = True elif entityState == "Inactive": imp.active = False - f.implants.append(imp) + fitobj.implants.append(imp) elif entityType == "Booster": # Bail if we can't get item or it's not from implant category try: @@ -752,7 +940,7 @@ class Port(object): b.active = True elif entityState == "Inactive": b.active = False - f.boosters.append(b) + 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)) @@ -767,7 +955,10 @@ class Port(object): # Add Cargo to the fitting c = Cargo(item) c.amount = cargoAmount - f.cargo.append(c) + 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 @@ -784,10 +975,10 @@ class Port(object): # Add subsystems before modules to make sure T3 cruisers have subsystems installed if modItem.category.name == "Subsystem": - if m.fits(f): - f.modules.append(m) + if m.fits(fitobj): + fitobj.modules.append(m) else: - m.owner = f + m.owner = fitobj # Activate mod if it is activable if m.isValidState(State.ACTIVE): m.state = State.ACTIVE @@ -804,17 +995,21 @@ class Port(object): moduleList.append(m) # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(f) + svcFit.getInstance().recalc(fitobj) for module in moduleList: - if module.fits(f): - f.modules.append(module) + if module.fits(fitobj): + fitobj.modules.append(module) # Append fit to list of fits - fits.append(f) + fits.append(fitobj) + + if iportuser: # NOTE: Send current processing status + PortProcessing.notify( + iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, + "%s:\n%s" % (fitobj.ship.name, fitobj.name) + ) - if callback: - wx.CallAfter(callback, None) # Skip fit silently if we get an exception except Exception as e: pyfalog.error("Caught exception on fit.") @@ -824,93 +1019,100 @@ class Port(object): return fits @staticmethod - def importXml(text, callback=None, encoding="utf-8"): + def importXml(text, iportuser=None, encoding="utf-8"): + # type: (basestring, IPortUser, basestring) -> list[eos.saveddata.fit.Fit] sMkt = Market.getInstance() - doc = xml.dom.minidom.parseString(text.encode(encoding)) + # 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") - fits = [] + fit_list = [] + + for fitting in fittings: + fitobj = _resolve_ship(fitting, sMkt, b_localized) + # -- 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 - for i, fitting in enumerate(fittings): - f = Fit() - f.name = fitting.getAttribute("name") - # Maelstrom - shipType = fitting.getElementsByTagName("shipType").item(0).getAttribute("value") - try: - try: - f.ship = Ship(sMkt.getItem(shipType)) - except ValueError: - f.ship = Citadel(sMkt.getItem(shipType)) - except Exception as e: - pyfalog.warning("Caught exception on importXml") - pyfalog.error(e) - continue hardwares = fitting.getElementsByTagName("hardware") moduleList = [] for hardware in hardwares: try: - moduleName = hardware.getAttribute("type") - try: - item = sMkt.getItem(moduleName, eager="group.category") - except Exception as e: - pyfalog.warning("Caught exception on importXml") - pyfalog.error(e) + item = _resolve_module(hardware, sMkt, b_localized) + if not item: continue - if item: - if item.category.name == "Drone": - d = Drone(item) - d.amount = int(hardware.getAttribute("qty")) - f.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 - f.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")) - f.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(f): - m.owner = f - f.modules.append(m) - else: - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE - moduleList.append(m) + 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(f) + svcFit.getInstance().recalc(fitobj) for module in moduleList: - if module.fits(f): - module.owner = f - f.modules.append(module) + if module.fits(fitobj): + module.owner = fitobj + fitobj.modules.append(module) - fits.append(f) - if callback: - wx.CallAfter(callback, None) + fit_list.append(fitobj) + if iportuser: # NOTE: Send current processing status + PortProcessing.notify( + iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, + "analysis :%s\n%s" % (fitobj.ship.name, fitobj.name) + ) - return fits + return fit_list @staticmethod def _exportEftBase(fit): + """Basically EFT format does not require blank lines + also, it's OK to arrange modules randomly? + """ offineSuffix = " /OFFLINE" export = "[%s, %s]\n" % (fit.ship.item.name, fit.name) stuff = {} @@ -1038,10 +1240,13 @@ class Port(object): return dna + "::" - @classmethod - def exportXml(cls, callback=None, *fits): + @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() @@ -1051,10 +1256,18 @@ class Port(object): fitting.setAttribute("name", fit.name) fittings.appendChild(fitting) description = doc.createElement("description") - description.setAttribute("value", "") + # -- 170327 Ignored description -- + try: + notes = fit.notes # unicode + 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.item.name) + shipType.setAttribute("value", fit.ship.name) fitting.appendChild(shipType) charges = {} @@ -1113,12 +1326,17 @@ class Port(object): hardware.setAttribute("slot", "cargo") hardware.setAttribute("type", name) fitting.appendChild(hardware) - except: - print("Failed on fitID: %d" % fit.ID) + except Exception as e: + # print("Failed on fitID: %d" % fit.ID) + pyfalog.error("Failed on fitID: %d, message: %s" % e.message) continue finally: - if callback: - wx.CallAfter(callback, i) + if iportuser: + PortProcessing.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() @@ -1169,37 +1387,33 @@ class Port(object): return export -class FitBackupThread(threading.Thread): - def __init__(self, path, callback): - threading.Thread.__init__(self) - self.path = path - self.callback = callback - - def run(self): - path = self.path - sFit = svcFit.getInstance() - sPort = Port.getInstance() - backedUpFits = sPort.exportXml(self.callback, *sFit.getAllFits()) - backupFile = open(path, "w", encoding="utf-8") - backupFile.write(backedUpFits) - backupFile.close() - +class PortProcessing(object): + """Port Processing class """ + @staticmethod + def backupFits(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(self.callback, -1) +# 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.") + @staticmethod + def importFitsFromFile(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) -class FitImportThread(threading.Thread): - def __init__(self, paths, callback): - threading.Thread.__init__(self) - self.paths = paths - self.callback = callback - - def run(self): - sPort = Port.getInstance() - success, result = sPort.importFitFromFiles(self.paths, self.callback) - - if not success: # there was an error during processing - pyfalog.error("Error while processing file import: {0}", result) - wx.CallAfter(self.callback, -2, result) - else: # Send done signal to GUI - wx.CallAfter(self.callback, -1, result) + @staticmethod + def notify(iportuser, flag, data): + if not iportuser.on_port_processing(flag, data): + raise UserCancelException diff --git a/service/settings.py b/service/settings.py index cdf84ea6e..0197564c1 100644 --- a/service/settings.py +++ b/service/settings.py @@ -46,36 +46,61 @@ class SettingsProvider(object): if not os.path.exists(self.BASE_PATH): os.mkdir(self.BASE_PATH) + # def getSettings(self, area, defaults=None): + # # type: (basestring, dict) -> service.Settings + # # NOTE: needed to change for tests + # settings_obj = self.settings.get(area) + # + # if settings_obj is None and hasattr(self, 'BASE_PATH'): + # canonical_path = os.path.join(self.BASE_PATH, area) + # + # if not os.path.exists(canonical_path): + # info = {} + # if defaults: + # for item in defaults: + # info[item] = defaults[item] + # + # else: + # try: + # f = open(canonical_path, "rb") + # info = cPickle.load(f) + # for item in defaults: + # if item not in info: + # info[item] = defaults[item] + # + # except: + # info = {} + # if defaults: + # for item in defaults: + # info[item] = defaults[item] + # + # self.settings[area] = settings_obj = Settings(canonical_path, info) + # + # return settings_obj def getSettings(self, area, defaults=None): - - s = self.settings.get(area) - - if s is None and hasattr(self, 'BASE_PATH'): - p = os.path.join(self.BASE_PATH, area) - - if not os.path.exists(p): + # type: (basestring, dict) -> service.Settings + # NOTE: needed to change for tests + # TODO: Write to memory with mmap -> https://docs.python.org/2/library/mmap.html + settings_obj = self.settings.get(area) + if settings_obj is None: # and hasattr(self, 'BASE_PATH'): + canonical_path = os.path.join(self.BASE_PATH, area) if hasattr(self, 'BASE_PATH') else "" + if not os.path.exists(canonical_path): # path string or empty string. info = {} if defaults: - for item in defaults: - info[item] = defaults[item] - + info.update(defaults) else: try: - f = open(p, "rb") - info = cPickle.load(f) + with open(canonical_path, "rb") as f: + info = cPickle.load(f) for item in defaults: if item not in info: info[item] = defaults[item] - except: info = {} - if defaults: - for item in defaults: - info[item] = defaults[item] + info.update(defaults) - self.settings[area] = s = Settings(p, info) - - return s + self.settings[area] = settings_obj = Settings(canonical_path, info) + return settings_obj def saveAll(self): for settings in self.settings.itervalues(): @@ -84,12 +109,22 @@ class SettingsProvider(object): class Settings(object): def __init__(self, location, info): + # type: (basestring, dict) -> None + # path string or empty string. self.location = location self.info = info + # def save(self): + # f = open(self.location, "wb") + # cPickle.dump(self.info, f, cPickle.HIGHEST_PROTOCOL) + def save(self): - f = open(self.location, "wb") - cPickle.dump(self.info, f, cPickle.HIGHEST_PROTOCOL) + # NOTE: needed to change for tests + if self.location is None or not self.location: + return + # NOTE: with + open -> file handle auto close + with open(self.location, "wb") as f: + cPickle.dump(self.info, f, cPickle.HIGHEST_PROTOCOL) def __getitem__(self, k): try: diff --git a/tests/jeffy_ja-en[99].xml b/tests/jeffy_ja-en[99].xml new file mode 100644 index 000000000..8ec178b16 --- /dev/null +++ b/tests/jeffy_ja-en[99].xml @@ -0,0 +1,2116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_unread_desc.py b/tests/test_unread_desc.py new file mode 100644 index 000000000..abe30af4f --- /dev/null +++ b/tests/test_unread_desc.py @@ -0,0 +1,88 @@ +""" + 2017/04/05: unread description tests module. +""" +# noinspection PyPackageRequirements +import pytest +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +# nopep8 +import re +# from utils.strfunctions import sequential_rep, replace_ltgt +from utils.stopwatch import Stopwatch + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..'))) +sys._called_from_test = True # need db open for tests. (see eos/config.py#17 +# noinspection PyPep8 +from service.port import Port, IPortUser +# +# noinspection PyPackageRequirements +# from _development.helpers import DBInMemory as DB + +""" +NOTE: + description character length is restricted 4hundred by EVE client. + these things apply to multi byte environment too. + + + o read xml fit data (and encode to utf-8 if need. + + o construct xml dom object, and extract "fitting" elements. + + o apply _resolve_ship method to each "fitting" elements. (time measurement + + o extract "hardware" elements from "fitting" element. + + o apply _resolve_module method to each "hardware" elements. (time measurement + +xml files: + "jeffy_ja-en[99].xml" + +NOTE of @decorator: + o Function to receive arguments of function to be decorated + o A function that accepts the decorate target function itself as an argument + o A function that accepts arguments of the decorator itself + +for local coverage: + py.test --cov=./ --cov-report=html +""" + +class PortUser(IPortUser): + + def on_port_processing(self, action, data=None): + print(data) + return True + + +stpw = Stopwatch('test measurementer') + +@pytest.fixture() +def print_db_info(): + # Output debug info + import eos + print + print "------------ data base connection info ------------" + print(eos.db.saveddata_engine) + print(eos.db.gamedata_engine) + print + + +# noinspection PyUnusedLocal +def test_import_xml(print_db_info): + usr = PortUser() +# for path in XML_FILES: + xml_file = "jeffy_ja-en[99].xml" + fit_count = int(re.search(r"\[(\d+)\]", xml_file).group(1)) + fits = None + with open(os.path.join(script_dir, xml_file), "r") as file_: + srcString = file_.read() + srcString = unicode(srcString, "utf-8") + # (basestring, IPortUser, basestring) -> list[eos.saveddata.fit.Fit] + usr.on_port_process_start() + stpw.reset() + with stpw: + fits = Port.importXml(srcString, usr) + + assert fits is not None and len(fits) is fit_count diff --git a/utils/stopwatch.py b/utils/stopwatch.py new file mode 100644 index 000000000..adf7e5832 --- /dev/null +++ b/utils/stopwatch.py @@ -0,0 +1,122 @@ +# coding: utf-8 + +import time +import os + + +class Stopwatch(object): + """ + --- on python console --- +import re +from utils.stopwatch import Stopwatch + +# measurementor +stpw = Stopwatch("test") +# measurement re.sub +def m_re_sub(t, set_count, executes, texts): + t.reset() + while set_count: + set_count -= 1 + with t: + while executes: + executes -= 1 + ret = re.sub("[a|s]+", "-", texts) + # stat string + return str(t) + +# statistics loop: 1000(exec re.sub: 100000) +m_re_sub(stpw, 1000, 100000, "asdfadsasdaasdfadsasda") + +----------- records ----------- + text: "asdfadsasda" + 'elapsed record(ms): min=0.000602411446948, max=220.85578571' + 'elapsed record(ms): min=0.000602411446948, max=217.331377504' + + text: "asdfadsasdaasdfadsasda" + 'elapsed record(ms): min=0.000602411446948, max=287.784902967' + 'elapsed record(ms): min=0.000602411432737, max=283.653264016' + + NOTE: about max + The value is large only at the first execution, + Will it be optimized, after that it will be significantly smaller + """ + + # time.clock() is μs? 1/1000ms + # https://docs.python.jp/2.7/library/time.html#time.clock + _tfunc = time.clock if os.name == "nt" else time.time + + def __init__(self, name='', logger=None): + self.name = name + self.start = Stopwatch._tfunc() + self.__last = self.start + # __last field is means last checkpoint system clock value? + self.logger = logger + self.min = 0.0 + self.max = 0.0 + self.__first = True + + @property + def stat(self): + # :return: (float, float) + return self.min, self.max + + @property + def elapsed(self): + # :return: time as ms + return (Stopwatch._tfunc() - self.start) * 1000 + + @property + def last(self): + return self.__last * 1000 + + def __update_stat(self, v): + # :param v: float unit of ms + if self.__first: + self.__first = False + return + if self.min == 0.0 or self.min > v: + self.min = v + if self.max < v: + self.max = v + + def checkpoint(self, name=''): + span = self.elapsed + self.__update_stat(span) + text = u'Stopwatch("{tname}") - {checkpoint} - {last:.6f}ms ({elapsed:.12f}ms elapsed)'.format( + tname=self.name, + checkpoint=unicode(name, "utf-8"), + last=self.last, + elapsed=span + ).strip() + self.__last = Stopwatch._tfunc() + if self.logger: + self.logger.debug(text) + else: + print(text) + + @staticmethod + def CpuClock(): + start = Stopwatch._tfunc() + time.sleep(1) + return Stopwatch._tfunc() - start + + def reset(self): + # clear stat + self.min = 0.0 + self.max = 0.0 + self.__first = True + + def __enter__(self): + self.start = Stopwatch._tfunc() + return self + + def __exit__(self, type_, value, traceback): + # https://docs.python.org/2.7/reference/datamodel.html?highlight=__enter__#object.__exit__ + # If the context was exited without an exception, all three arguments will be None + self.checkpoint('finished') + # ex: "type=None, value=None, traceback=None" + # print "type=%s, value=%s, traceback=%s" % (type, value, traceback) + return True + + def __repr__(self): + return "elapsed record(ms): min=%s, max=%s" % self.stat diff --git a/utils/strfunctions.py b/utils/strfunctions.py new file mode 100644 index 000000000..26bf5f59a --- /dev/null +++ b/utils/strfunctions.py @@ -0,0 +1,30 @@ +''' + string manipulation module +''' +import re + + +def sequential_rep(text_, *args): + # type: (basestring, tuple) -> basestring + """ + :param text_: string content + :param args: like , , , , ... + :return: if text_ length was zero or invalid parameters then no manipulation to text_ + """ + arg_len = len(args) + if arg_len % 2 == 0 and isinstance(text_, basestring) and len(text_) > 0: + i = 0 + while i < arg_len: + text_ = re.sub(args[i], args[i + 1], text_) + i += 2 + + return text_ + + +def replace_ltgt(text_): + # type: (basestring) -> basestring + """if fit name contained "<" or ">" then reprace to named html entity by EVE client. + :param text_: string content of fit name from exported by EVE client. + :return: if text_ is not instance of basestring then no manipulation to text_. + """ + return text_.replace("<", "<").replace(">", ">") if isinstance(text_, basestring) else text_