def check(label, fieldName, wantStation): key = getattr(self, fieldName, None) if not key: return None try: place = self.tdb.lookupPlace(key) except LookupError: raise CommandLineError("Unrecognized {}: {}".format( label, key)) if not wantStation: if isinstance(place, Station): return place.system return place if isinstance(place, Station): return place # it's a system, we want a station if not place.stations: raise CommandLineError( "Station name required for {}: " "{} is a SYSTEM but has no stations.".format(label, key)) if len(place.stations) > 1: raise AmbiguityError(label, key, place.stations, key=lambda key: key.name()) return place.stations[0]
def checkStationDoesNotExist(tdb, cmdenv, system, stationName): if not system.stations: return upperName = stationName.upper() similarities = set() try: station = tdb.lookupStation(stationName, system) if station.dbname.upper() == upperName: raise CommandLineError("Station \"{}\" " "in system \"{}\" " "already exists.".format( stationName, system.name(), )) similarities.add(station.dbname.upper()) except LookupError: pass except AmbiguityError as e: for cand in e.anyMatch: similarities.add(e.key(cand).upper()) # Check to see if there are stations with somewhat # similar names, but allow the user to get around # cases where difflib matches 'X Port' to 'Y Port'. stationNames = [stn.dbname.upper() for stn in system.stations] cmdenv.DEBUG0("Comparing {} to {}".format( upperName, list(stationNames), )) candidates = difflib.get_close_matches( upperName, stationNames, cutoff=0.6, ) for cand in candidates: similarities.add(cand) if not similarities: return confCode = makeConfirmationCode(system.ID, similarities) if not cmdenv.confirm: raise CommandLineError( "\"{}\" contains similar station names:\n" " {}\n" "\n" "If you want to add this station anyway, re-run the " "command and add:\n" " --conf {}".format(system.name(), ', '.join(candidates), confCode)) if cmdenv.confirm.upper() != confCode: raise CommandLineError("Wrong confirmation code.") cmdenv.NOTE("Confirmation code accepted.")
def run(results, cmdenv, tdb): station = cmdenv.startStation if not isinstance(station, Station): raise CommandLineError("{} is a system, not a station".format( station.name())) if station.shipyard == 'N' and not cmdenv.remove: raise CommandLineError("{} is flagged as having no shipyard.".format( station.name())) if cmdenv.add: action = addShipVendor elif cmdenv.remove: action = removeShipVendor else: return listShipsPresent(tdb, cmdenv, station, results) if not cmdenv.ship: raise CommandLineError("No ship names specified.") cmdenv.NOTE("Using local database ({})", tdb.dbPath) ships = {} shipNames = chain.from_iterable(name.split(",") for name in cmdenv.ship) for shipName in shipNames: try: ship = tdb.lookupShip(shipName) except LookupError: raise CommandLineError("Unrecognized Ship: {}".format(shipName)) # Lets see if that ship sails from the specified port. shipPresent = checkShipPresent(tdb, station, ship) if cmdenv.add: if shipPresent: cmdenv.DEBUG0("{} is already listed at {}", ship.name(), station.name()) else: ships[ship.ID] = ship else: if not shipPresent: cmdenv.DEBUG0("{} is not listed at {}", ship.name(), station.name()) else: ships[ship.ID] = ship if len(ships) == 0: cmdenv.NOTE("Nothing to do.") return None # We've checked that everything should be good. dataToExport = False for ship in ships.values(): if action(tdb, cmdenv, station, ship): dataToExport = True maybeExportToCSV(tdb, cmdenv) return None
def run(results, cmdenv, tdb): place = cmdenv.origPlace if isinstance(place, System): system = place if len(system.stations) != 1: raise CommandLineError("'{}' is a system, not a station.".format( system.name())) cmdenv.startStation = system.stations[0] else: cmdenv.startStation = place if cmdenv.gui or (not cmdenv.editor and not cmdenv.editing): if not cmdenv.quiet: print( "NOTE:\n" ". Press CTRL-C here to abort edit, otherwise " "just close the update window to save.\n" ". '-F' makes the window show in-front of the " "E:D Window.\n" ". '-A' forces all items to be listed.\n", file=sys.stderr) guidedUpdate(tdb, cmdenv) else: # User specified one of the options to use an editor. editUpdate(tdb, cmdenv, cmdenv.startStation.ID) return None
def __init__(self, properties, argv, cmdModule): super().__init__(properties=properties) self.tdb = None self.mfd = None self.argv = argv or sys.argv if self.detail and self.quiet: raise CommandLineError( "'--detail' (-v) and '--quiet' (-q) are mutually exclusive.") self._cmd = cmdModule or __main__ self.wantsTradeDB = getattr(cmdModule, 'wantsTradeDB', True) self.usesTradeData = getattr(cmdModule, 'usesTradeData', False) # We need to relocate to the working directory so that # we can load a TradeDB after this without things going # pear-shaped if not self.cwd and argv[0]: cwdPath = pathlib.Path('.').resolve() exePath = pathlib.Path(argv[0]).parent.resolve() if cwdPath != exePath: self.cwd = str(exePath) self.DEBUG1( "cwd at launch was: {}, changing to {} to match trade.py", cwdPath, self.cwd) if self.cwd: os.chdir(self.cwd)
def checkSystemAndStation(tdb, cmdenv): # In add mode, the user has to be more specific. stnName = ' '.join(cmdenv.station).strip() if not cmdenv.add: try: station = tdb.lookupPlace(stnName) except LookupError: raise CommandLineError("Unrecognized Station: {}".format( cmdenv.station)) if not isinstance(station, Station): raise CommandLineError( "Expecting a STATION, got {}".format(stnName)) cmdenv.system = station.system.name() cmdenv.station = station.dbname return station.system, station # Clean up the station name and potentially lift the system # name out of it. stnName = re.sub(r" +", " ", stnName) stnName = re.sub(r"[ /]*/[ /]*", "/", stnName) while stnName.startswith('/'): stnName = stnName[1:] slashPos = stnName.find('/') if slashPos > 0: sysName, stnName = stnName[:slashPos], stnName[slashPos + 1:] sysName = sysName.upper() else: sysName = None if not stnName: raise CommandLineError("Invalid station name: {}".format(envStnName)) if not sysName: raise CommandLineError("No system name specified") cmdenv.system, cmdenv.station = sysName, TradeDB.titleFixup(stnName) try: system = tdb.lookupSystem(sysName) except LookupError: raise CommandLineError("Unknown SYSTEM name: \"{}\"".format(sysName)) # check the station does not exist checkStationDoesNotExist(tdb, cmdenv, system, stnName) return system, None
def run(results, cmdenv, tdb): from tradedb import TradeDB # Check that the file doesn't already exist. if not cmdenv.force: if tdb.dbPath.exists(): raise CommandLineError( "SQLite3 database '{}' already exists.\n" "Either remove the file first or use the '-f' option." .format(tdb.dbFilename)) if not tdb.sqlPath.exists(): raise CommandLineError( "SQL File does not exist: {}" .format(tdb.sqlFilename)) from cache import buildCache buildCache(tdb, cmdenv) return None
def run(results, cmdenv, tdb): if cmdenv.lsFromStar and cmdenv.lsFromStar < 0: raise CommandLineError("Invalid (negative) --ls option") system, station = checkSystemAndStation(tdb, cmdenv) systemName = cmdenv.system stationName = cmdenv.station if cmdenv.add: result = addStation(tdb, cmdenv, system, stationName) return checkResultAndExportStations(tdb, cmdenv, result) elif cmdenv.update: result = updateStation(tdb, cmdenv, station) return checkResultAndExportStations(tdb, cmdenv, result) elif cmdenv.remove: result = removeStation(tdb, cmdenv, station) return checkResultAndExportStations(tdb, cmdenv, result) # Otherwise, it's just a query results.summary = ResultRow() results.summary.system = station.system results.summary.station = station avgSell = results.summary.avgSelling = tdb.getAverageSelling() avgBuy = results.summary.avgBuying = tdb.getAverageBuying() class ItemTrade(object): def __init__(self, ID, price, avgAgainst): self.ID, self.item = ID, tdb.itemByID[ID] self.price = int(price) self.avgTrade = avgAgainst.get(ID, 0) # Look up all selling and buying by the station selling, buying = [], [] cur = tdb.query( """ SELECT item_id, demand_price, supply_price FROM StationItem WHERE station_id = ? AND (demand_price > 10 or supply_price > 10) """, [station.ID]) for ID, demand_price, supply_price in cur: if demand_price > 10 and avgSell.get(ID, 0) > 10: buying.append(ItemTrade(ID, demand_price, avgSell)) if supply_price > 10 and avgBuy.get(ID, 0) > 10: selling.append(ItemTrade(ID, supply_price, avgBuy)) selling.sort(key=lambda item: item.price - item.avgTrade, ) results.summary.selling = selling[:5] buying.sort(key=lambda item: item.avgTrade - item.price, ) results.summary.buying = buying[:5] return results
def checkAvoids(self): """ Process a list of avoidances. """ avoidItems = self.avoidItems = [] avoidPlaces = self.avoidPlaces = [] avoidances = self.avoid if not self.avoid: return avoidances = self.avoid tdb = self.tdb # You can use --avoid to specify an item, system or station. # and you can group them together with commas or list them # individually. for avoid in ','.join(avoidances).split(','): # Is it an item? item, place = None, None try: item = tdb.lookupItem(avoid) avoidItems.append(item) if tdb.normalizedStr(item.name()) == tdb.normalizedStr(avoid): continue except LookupError: pass # Or is it a place? try: place = tdb.lookupPlace(avoid) avoidPlaces.append(place) if tdb.normalizedStr(place.name()) == tdb.normalizedStr(avoid): continue continue except LookupError: pass # If it was none of the above, whine about it if not (item or place): raise CommandLineError( "Unknown item/system/station: {}".format(avoid)) # But if it matched more than once, whine about ambiguity if item and place: raise AmbiguityError('Avoidance', avoid, [item, place.str()]) self.DEBUG0( "Avoiding items {}, places {}", [item.name() for item in avoidItems], [place.name() for place in avoidPlaces], )
def render(results, cmdenv, tdb): if not results or not results.rows: raise CommandLineError("No ships available at {}".format( results.summary.station.name())) maxShipLen = max_len(results.rows, key=lambda row: row.ship.name()) rowFmt = RowFormat().append( ColumnFormat("Ship", '<', maxShipLen, key=lambda row: row.ship.name())).append( ColumnFormat("Cost", '>', 12, 'n', key=lambda row: row.ship.cost)) if not cmdenv.quiet: heading, underline = rowFmt.heading() print(heading, underline, sep='\n') for row in results.rows: print(rowFmt.format(row))
def getEditorPaths(cmdenv, editorName, envVar, windowsFolders, winExe, nixExe): cmdenv.DEBUG0("Locating {} editor", editorName) try: return os.environ[envVar] except KeyError: pass paths = [] import platform system = platform.system() if system == 'Windows': binary = winExe for folder in ["Program Files", "Program Files (x86)"]: for version in windowsFolders: paths.append("{}\\{}\\{}".format(os.environ['SystemDrive'], folder, version)) else: binary = nixExe try: paths += os.environ['PATH'].split(os.pathsep) except KeyError: pass for path in paths: candidate = os.path.join(path, binary) try: if pathlib.Path(candidate).exists(): return candidate except OSError: pass raise CommandLineError( "ERROR: Unable to locate {} editor.\n" "Either specify the path to your editor with --editor " "or set the {} environment variable to point to it.".format( editorName, envVar))
def run(results, cmdenv, tdb): # check database exists if not tdb.dbPath.is_file(): raise CommandLineError("Database '{}' not found.".format(tdb.dbPath)) # check export path exists if cmdenv.path: # the "--path" overwrites the default path of TD exportPath = Path(cmdenv.path) else: exportPath = Path(cmdenv.dataDir) if not exportPath.is_dir(): raise CommandLineError("Save location '{}' not found.".format( str(exportPath))) # connect to the database cmdenv.NOTE("Using database '{}'", tdb.dbPath) conn = tdb.getDB() conn.row_factory = sqlite3.Row # some tables might be ignored ignoreList = [] # extract tables from command line if cmdenv.tables: bindValues = cmdenv.tables.split(',') tableStmt = " AND name COLLATE NOCASE IN ({})".format(",".join( "?" * len(bindValues))) cmdenv.DEBUG0(tableStmt) else: bindValues = [] tableStmt = '' if not cmdenv.allTables: ignoreList.append("StationItem") tableCursor = conn.cursor() for row in tableCursor.execute( """ SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' {cmdTables} ORDER BY name """.format(cmdTables=tableStmt), bindValues): tableName = row['name'] if tableName in ignoreList: # ignore the table cmdenv.NOTE("Ignore Table '{table}'", table=tableName) continue cmdenv.NOTE("Export Table '{table}'", table=tableName) # create CSV files lineCount, filePath = exportTableToFile(tdb, cmdenv, tableName, exportPath) if cmdenv.deleteEmpty and lineCount == 0: # delete file if emtpy filePath.unlink() cmdenv.DEBUG0("Delete empty file {file}'".format(file=filePath)) return None
def editUpdate(tdb, cmdenv, stationID): """ Dump the price data for a specific station to a file and launch the user's text editor to let them make changes to the file. If the user makes changes, re-load the file, update the database and regenerate the master .prices file. """ cmdenv.DEBUG0("'update' mode with editor. editor:{} station:{}", cmdenv.editor, cmdenv.origin) editor, editorArgs = cmdenv.editor, [] if cmdenv.editing == 'sublime': cmdenv.DEBUG0("Sublime mode") editor = editor or \ getEditorPaths(cmdenv, "sublime", "SUBLIME_EDITOR", ["Sublime Text 3", "Sublime Text 2"], "sublime_text.exe", "subl" ) editorArgs += ["--wait"] elif cmdenv.editing == 'npp': cmdenv.DEBUG0("Notepad++ mode") editor = editor or \ getEditorPaths(cmdenv, "notepad++", "NOTEPADPP_EDITOR", ["Notepad++"], "notepad++.exe", "notepad++" ) if not cmdenv.quiet: print("NOTE: " "You'll need to exit Notepad++ when you are done.") elif cmdenv.editing == "vim": cmdenv.DEBUG0("VI iMproved mode") if not editor: # Hack... Hackity hack, hack, hack. # This has a disadvantage in that: we don't check for just "vim" (no .exe) under Windows vimDirs = [ "Git\\share\\vim\\vim{}".format(vimVer) for vimVer in range(70, 75) ] editor = getEditorPaths(cmdenv, "vim", "EDITOR", vimDirs, "vim.exe", "vim") elif cmdenv.editing == "notepad": cmdenv.DEBUG0("Notepad mode") editor = editor or "notepad.exe" # herp try: envArgs = os.environ["EDITOR_ARGS"] if envArgs: editorArgs += envArgs.split(' ') except KeyError: pass # Create a temporary text file with a list of the price data. tmpPath = getTemporaryPath(cmdenv) absoluteFilename = None dbFilename = tdb.dbFilename try: elementMask = prices.Element.basic | prices.Element.supply if cmdenv.timestamps: elementMask |= prices.Element.timestamp if cmdenv.all: elementMask |= prices.Element.blanks # Open the file and dump data to it. with tmpPath.open("w", encoding='utf-8') as tmpFile: # Remember the filename so we know we need to delete it. absoluteFilename = str(tmpPath.resolve()) prices.dumpPrices(dbFilename, elementMask, file=tmpFile, stationID=stationID, defaultZero=cmdenv.forceNa, debug=cmdenv.debug) # Stat the file so we can determine if the user writes to it. # Use the most recent create/modified timestamp. preStat = tmpPath.stat() preStamp = max(preStat.st_mtime, preStat.st_ctime) # Launch the editor editorCommandLine = [editor] + editorArgs + [absoluteFilename] cmdenv.DEBUG0("Invoking [{}]", ' '.join(editorCommandLine)) try: result = subprocess.call(editorCommandLine) except FileNotFoundError: raise CommandLineError( "Unable to launch requested editor: {}".format( editorCommandLine)) if result != 0: raise CommandLineError( "NO DATA IMPORTED: " "Your editor exited with a 'failed' exit code ({})".format( result)) # Did they update the file? Some editors destroy the file and rewrite it, # other files just write back to it, and some OSes do weird things with # these timestamps. That's why we have to use both mtime and ctime. postStat = tmpPath.stat() postStamp = max(postStat.st_mtime, postStat.st_ctime) if postStamp == preStamp: import random print("- No changes detected - doing nothing. {}".format( random.choice([ "Brilliant!", "I'll get my coat.", "I ain't seen you.", "You ain't seen me", "... which was nice", "Bingo!", "Scorchio!", "Boutros, boutros, ghali!", "I'm Ed Winchester!", "Suit you, sir! Oh!" ]))) else: cache.importDataFromFile(tdb, cmdenv, tmpPath) saveCopyOfChanges(cmdenv, dbFilename, stationID) tmpPath.unlink() tmpPath = None except Exception as e: print("ERROR:", e) print() print("*** YOUR UPDATES WILL BE SAVED AS {} ***".format("prices.last")) # Save a copy if absoluteFilename and tmpPath: saveTemporaryFile(tmpPath) if "EXCEPTIONS" in os.environ: raise e