Skip to content

Commit

Permalink
'buy' command now accepts multiple item names
Browse files Browse the repository at this point in the history
  • Loading branch information
kfsone committed Feb 27, 2015
1 parent 017698b commit d14c55e
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 72 deletions.
12 changes: 12 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
224 changes: 152 additions & 72 deletions commands/buy_cmd.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -16,7 +22,7 @@
ParseArgument(
'name',
help='Items or Ships to look for.',
type=str
nargs='+',
),
]
switches = [
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
))
2 changes: 2 additions & 0 deletions formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(). \
Expand Down

0 comments on commit d14c55e

Please sign in to comment.