From d14c55e5983d092a088b2edd869d3f791e2d7ed5 Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Thu, 26 Feb 2015 16:28:20 -0800 Subject: [PATCH] 'buy' command now accepts multiple item names --- CHANGES.txt | 12 +++ commands/buy_cmd.py | 224 ++++++++++++++++++++++++++++++-------------- formatting.py | 2 + 3 files changed, 166 insertions(+), 72 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index f0202665..dd00ab26 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,18 @@ TradeDangerous, Copyright (C) Oliver "kfsone" Smith, July 2014 ============================================================================== +v6.12.1 Feb 25 2015 +. (kfsone) "buy" command: + - now accepts multiple arguments (e.g. explosives,clothing), + - new "--one-stop" (-1) option only shows stations that carry the full list, + - mixing ships and items is not allowed, + e.g. + trade.py buy food clothing fish + trade.py buy --near achenar fish,food,grain --one-stop + trade.py buy --near achenar --ly 100 type6,type7 --one-stop + trade.py buy --near achenar --ly 100 type6,type7 -1 +. (kfsone) "submit-distances" now has proper argument parsing, see --help + v6.12.0 Feb 23 2015 . (kfsone) Added "market", "shipyard" and "modified" values to Station table . (kfsone) "submit-distances" now has proper argument parsing, see --help diff --git a/commands/buy_cmd.py b/commands/buy_cmd.py index f733605f..16f17a6d 100644 --- a/commands/buy_cmd.py +++ b/commands/buy_cmd.py @@ -1,10 +1,16 @@ from __future__ import absolute_import, with_statement, print_function, division, unicode_literals +from collections import defaultdict +from commands.commandenv import ResultRow from commands.exceptions import * from commands.parsing import MutuallyExclusiveGroup, ParseArgument -from tradedb import TradeDB +from formatting import RowFormat, ColumnFormat, max_len +from tradedb import TradeDB, AmbiguityError import math +ITEM_MODE = "Item" +SHIP_MODE = "Ship" + ###################################################################### # Parser config @@ -16,7 +22,7 @@ ParseArgument( 'name', help='Items or Ships to look for.', - type=str + nargs='+', ), ] switches = [ @@ -52,6 +58,12 @@ dest='padSize', ), MutuallyExclusiveGroup( + ParseArgument( + '--one-stop', '-1', + help='Only list stations that carry all items listed.', + action='store_true', + dest='oneStop', + ), ParseArgument( '--price-sort', '-P', help='(When using --near) Sort by price not distance', @@ -83,71 +95,133 @@ ), ] -###################################################################### -# Perform query and populate result set - -def run(results, cmdenv, tdb): - from commands.commandenv import ResultRow - - if cmdenv.lt and cmdenv.gt: - if cmdenv.lt <= cmdenv.gt: - raise CommandLineError("--gt must be lower than --lt") +def get_lookup_list(cmdenv, tdb): + # Credit: http://stackoverflow.com/a/952952/257645 + # Turns [['a'],['b','c']] => ['a', 'b', 'c'] + names = [ + name for names in cmdenv.name for name in names.split(',') + ] + queries, mode = {}, None + for name in names: + if mode is not SHIP_MODE: + try: + item = tdb.lookupItem(name) + cmdenv.DEBUG0("Looking up item {} (#{})", item.name(), item.ID) + queries[item.ID] = item + mode = ITEM_MODE + continue + except LookupError: + if mode is ITEM_MODE: + raise CommandLineError( + "Unrecognized item: {}".format(name) + ) + pass - try: - item = tdb.lookupItem(cmdenv.name) - cmdenv.DEBUG0("Looking up item {} (#{})", item.name(), item.ID) - except LookupError: - item = tdb.lookupShip(cmdenv.name) - cmdenv.DEBUG0("Looking up ship {} (#{})", item.name(), item.ID) - cmdenv.ship = True + try: + ship = tdb.lookupShip(name) + cmdenv.DEBUG0("Looking up ship {} (#{})", ship.name(), ship.ID) + queries[ship.ID] = ship + mode = SHIP_MODE + continue + except LookupError: + if not mode: + raise CommandLineError( + "Unrecognized item/ship: {}".format(name) + ) + raise CommandLineError( + "Unrecognized ship: {}".format(name) + ) - results.summary = ResultRow() - results.summary.item = item + return queries, mode - if cmdenv.detail: - if cmdenv.ship: - results.summary.avg = item.cost - else: - avgPrice = tdb.query(""" - SELECT CAST(AVG(ss.price) AS INT) - FROM StationSelling AS ss - WHERE ss.item_id = ? - """, [item.ID]).fetchone()[0] - results.summary.avg = avgPrice +def sql_query(cmdenv, tdb, queries, mode): # Constraints - if cmdenv.ship: + idList = ','.join(str(ID) for ID in queries.keys()) + if mode is SHIP_MODE: tables = "ShipVendor AS ss" - constraints = [ "(ship_id = {})".format(item.ID) ] + constraints = ["(ship_id IN ({}))".format(idList)] columns = [ + 'ss.ship_id', 'ss.station_id', '0', '1', "0", ] - bindValues = [ ] + bindValues = [] else: tables = "StationSelling AS ss" - constraints = [ "(item_id = {})".format(item.ID) ] + constraints = ["(item_id IN ({}))".format(idList)] columns = [ + 'ss.item_id', 'ss.station_id', 'ss.price', 'ss.units', "JULIANDAY('NOW') - JULIANDAY(ss.modified)", ] - bindValues = [ ] + bindValues = [] + + # Additional constraints in ITEM_MODE + if mode is ITEM_MODE: + if cmdenv.quantity: + constraints.append("(units = -1 or units >= ?)") + bindValues.append(cmdenv.quantity) + if cmdenv.lt: + constraints.append("(price < ?)") + bindValues.append(cmdenv.lt) + if cmdenv.gt: + constraints.append("(price > ?)") + bindValues.append(cmdenv.gt) + + whereClause = ' AND '.join(constraints) + stmt = """ + SELECT DISTINCT {columns} + FROM {tables} + WHERE {where} + """.format( + columns=','.join(columns), + tables=tables, + where=whereClause + ) + cmdenv.DEBUG0('SQL: {}', stmt) + return tdb.query(stmt, bindValues) + + +###################################################################### +# Perform query and populate result set - if cmdenv.quantity: - constraints.append("(units = -1 or units >= ?)") - bindValues.append(cmdenv.quantity) +def run(results, cmdenv, tdb): + if cmdenv.lt and cmdenv.gt: + if cmdenv.lt <= cmdenv.gt: + raise CommandLineError("--gt must be lower than --lt") - if cmdenv.lt: - constraints.append("(price < ?)") - bindValues.append(cmdenv.lt) - if cmdenv.gt: - constraints.append("(price > ?)") - bindValues.append(cmdenv.gt) + # Find out what we're looking for. + queries, mode = get_lookup_list(cmdenv, tdb) + cmdenv.DEBUG0("{} query: {}", mode, queries.values()) + + # Summarize + results.summary = ResultRow() + results.summary.mode = mode + results.summary.queries = queries + results.summary.oneStop = cmdenv.oneStop + + # In single mode with detail enabled, add average reports. + # Thus if you're looking up "algae" or the "asp", it'll + # tell you the average/ship cost. + singleMode = len(queries) == 1 + if singleMode and cmdenv.detail: + first = list(queries.values())[0] + if mode is SHIP_MODE: + results.summary.avg = first.cost + else: + avgPrice = tdb.query(""" + SELECT CAST(AVG(ss.price) AS INT) + FROM StationSelling AS ss + WHERE ss.item_id = ? + """, [first.ID]).fetchone()[0] + results.summary.avg = avgPrice + # System-based search nearSystem = cmdenv.nearSystem if nearSystem: maxLy = cmdenv.maxLyPer or tdb.maxSystemLinkLy @@ -157,23 +231,14 @@ def run(results, cmdenv, tdb): else: distanceFn = None - whereClause = ' AND '.join(constraints) - stmt = """ - SELECT DISTINCT {columns} - FROM {tables} - WHERE {where} - """.format( - columns=','.join(columns), - tables=tables, - where=whereClause - ) - cmdenv.DEBUG0('SQL: {}', stmt) - cur = tdb.query(stmt, bindValues) - + oneStopMode = cmdenv.oneStop padSize = cmdenv.padSize + stations = defaultdict(list) stationByID = tdb.stationByID - for (stationID, priceCr, stock, age) in cur: + + cur = sql_query(cmdenv, tdb, queries, mode) + for (ID, stationID, priceCr, stock, age) in cur: station = stationByID[stationID] if padSize and not station.checkPadSize(padSize): continue @@ -184,22 +249,33 @@ def run(results, cmdenv, tdb): if distance > maxLy: continue row.dist = distance + row.item = queries[ID] row.price = priceCr row.stock = stock row.age = age - results.rows.append(row) + stationRows = stations[stationID] + stationRows.append(row) + if oneStopMode and len(stationRows) < len(queries): + continue + results.rows.extend(stationRows) if not results.rows: + if oneStopMode and len(stations): + raise NoDataError("No one-stop stations found") raise NoDataError("No available items found") + if oneStopMode and singleMode: + results.rows.sort(key=lambda result: result.item.name()) + results.rows.sort(key=lambda result: result.station.name()) if cmdenv.sortByStock: results.summary.sort = "Stock" results.rows.sort(key=lambda result: result.price) results.rows.sort(key=lambda result: result.stock, reverse=True) else: - results.summary.sort = "Price" - results.rows.sort(key=lambda result: result.stock, reverse=True) - results.rows.sort(key=lambda result: result.price) + if not oneStopMode: + results.summary.sort = "Price" + results.rows.sort(key=lambda result: result.stock, reverse=True) + results.rows.sort(key=lambda result: result.price) if nearSystem and not cmdenv.sortByPrice: results.summary.sort = "Ly" results.rows.sort(key=lambda result: result.dist) @@ -214,15 +290,19 @@ def run(results, cmdenv, tdb): ## Transform result set into output def render(results, cmdenv, tdb): - from formatting import RowFormat, ColumnFormat - - longestNamed = max(results.rows, key=lambda result: len(result.station.name())) - longestNameLen = len(longestNamed.station.name()) + mode = results.summary.mode + singleMode = len(results.summary.queries) == 1 + maxStnLen = max_len(results.rows, key=lambda row: row.station.name()) stnRowFmt = RowFormat() - stnRowFmt.addColumn('Station', '<', longestNameLen, + stnRowFmt.addColumn('Station', '<', maxStnLen, key=lambda row: row.station.name()) - if not cmdenv.ship: + if not singleMode: + maxItmLen = max_len(results.rows, key=lambda row: row.item.name()) + stnRowFmt.addColumn(results.summary.mode, '<', maxItmLen, + key=lambda row: row.item.name() + ) + if mode is not SHIP_MODE: stnRowFmt.addColumn('Cost', '>', 10, 'n', key=lambda row: row.price) stnRowFmt.addColumn('Stock', '>', 10, @@ -232,7 +312,7 @@ def render(results, cmdenv, tdb): stnRowFmt.addColumn('DistLy', '>', 6, '.2f', key=lambda row: row.dist) - if not cmdenv.ship: + if mode is not SHIP_MODE: stnRowFmt.addColumn('Age/days', '>', 7, '.2f', key=lambda row: row.age) stnRowFmt.addColumn("StnLs", '>', 10, @@ -249,9 +329,9 @@ def render(results, cmdenv, tdb): for row in results.rows: print(stnRowFmt.format(row)) - if cmdenv.detail: + if singleMode and cmdenv.detail: print("{:{lnl}} {:>10n}".format( - "-- Average" if not cmdenv.ship else "-- Ship Cost", + "-- Ship Cost" if mode is SHIP_MODE else "-- Average", results.summary.avg, - lnl=longestNameLen, + lnl=maxStnLen, )) diff --git a/formatting.py b/formatting.py index a06671a9..78a4b358 100644 --- a/formatting.py +++ b/formatting.py @@ -164,6 +164,8 @@ def heading(self): def format(self, rowData): return self.prefix + ' '.join(col.format(rowData) for col in self.columns) +def max_len(iterable, key=lambda item: item): + return max(len(key(item)) for item in iterable) if __name__ == '__main__': rowFmt = RowFormat(). \