tradedangerous 12.7.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py.typed +1 -0
- trade.py +49 -0
- tradedangerous/__init__.py +43 -0
- tradedangerous/cache.py +1381 -0
- tradedangerous/cli.py +136 -0
- tradedangerous/commands/TEMPLATE.py +74 -0
- tradedangerous/commands/__init__.py +244 -0
- tradedangerous/commands/buildcache_cmd.py +102 -0
- tradedangerous/commands/buy_cmd.py +427 -0
- tradedangerous/commands/commandenv.py +372 -0
- tradedangerous/commands/exceptions.py +94 -0
- tradedangerous/commands/export_cmd.py +150 -0
- tradedangerous/commands/import_cmd.py +222 -0
- tradedangerous/commands/local_cmd.py +243 -0
- tradedangerous/commands/market_cmd.py +207 -0
- tradedangerous/commands/nav_cmd.py +252 -0
- tradedangerous/commands/olddata_cmd.py +270 -0
- tradedangerous/commands/parsing.py +221 -0
- tradedangerous/commands/rares_cmd.py +298 -0
- tradedangerous/commands/run_cmd.py +1521 -0
- tradedangerous/commands/sell_cmd.py +262 -0
- tradedangerous/commands/shipvendor_cmd.py +60 -0
- tradedangerous/commands/station_cmd.py +68 -0
- tradedangerous/commands/trade_cmd.py +181 -0
- tradedangerous/commands/update_cmd.py +67 -0
- tradedangerous/corrections.py +55 -0
- tradedangerous/csvexport.py +234 -0
- tradedangerous/db/__init__.py +27 -0
- tradedangerous/db/adapter.py +192 -0
- tradedangerous/db/config.py +107 -0
- tradedangerous/db/engine.py +259 -0
- tradedangerous/db/lifecycle.py +332 -0
- tradedangerous/db/locks.py +208 -0
- tradedangerous/db/orm_models.py +500 -0
- tradedangerous/db/paths.py +113 -0
- tradedangerous/db/utils.py +661 -0
- tradedangerous/edscupdate.py +565 -0
- tradedangerous/edsmupdate.py +474 -0
- tradedangerous/formatting.py +210 -0
- tradedangerous/fs.py +156 -0
- tradedangerous/gui.py +1146 -0
- tradedangerous/mapping.py +133 -0
- tradedangerous/mfd/__init__.py +103 -0
- tradedangerous/mfd/saitek/__init__.py +3 -0
- tradedangerous/mfd/saitek/directoutput.py +678 -0
- tradedangerous/mfd/saitek/x52pro.py +195 -0
- tradedangerous/misc/checkpricebounds.py +287 -0
- tradedangerous/misc/clipboard.py +49 -0
- tradedangerous/misc/coord64.py +83 -0
- tradedangerous/misc/csvdialect.py +57 -0
- tradedangerous/misc/derp-sentinel.py +35 -0
- tradedangerous/misc/diff-system-csvs.py +159 -0
- tradedangerous/misc/eddb.py +81 -0
- tradedangerous/misc/eddn.py +349 -0
- tradedangerous/misc/edsc.py +437 -0
- tradedangerous/misc/edsm.py +121 -0
- tradedangerous/misc/importeddbstats.py +54 -0
- tradedangerous/misc/prices-json-exp.py +179 -0
- tradedangerous/misc/progress.py +194 -0
- tradedangerous/plugins/__init__.py +249 -0
- tradedangerous/plugins/edcd_plug.py +371 -0
- tradedangerous/plugins/eddblink_plug.py +861 -0
- tradedangerous/plugins/edmc_batch_plug.py +133 -0
- tradedangerous/plugins/spansh_plug.py +2647 -0
- tradedangerous/prices.py +211 -0
- tradedangerous/submit-distances.py +422 -0
- tradedangerous/templates/Added.csv +37 -0
- tradedangerous/templates/Category.csv +17 -0
- tradedangerous/templates/RareItem.csv +143 -0
- tradedangerous/templates/TradeDangerous.sql +338 -0
- tradedangerous/tools.py +40 -0
- tradedangerous/tradecalc.py +1302 -0
- tradedangerous/tradedb.py +2320 -0
- tradedangerous/tradeenv.py +313 -0
- tradedangerous/tradeenv.pyi +109 -0
- tradedangerous/tradeexcept.py +131 -0
- tradedangerous/tradeorm.py +183 -0
- tradedangerous/transfers.py +192 -0
- tradedangerous/utils.py +243 -0
- tradedangerous/version.py +16 -0
- tradedangerous-12.7.6.dist-info/METADATA +106 -0
- tradedangerous-12.7.6.dist-info/RECORD +87 -0
- tradedangerous-12.7.6.dist-info/WHEEL +5 -0
- tradedangerous-12.7.6.dist-info/entry_points.txt +3 -0
- tradedangerous-12.7.6.dist-info/licenses/LICENSE +373 -0
- tradedangerous-12.7.6.dist-info/top_level.txt +2 -0
- tradegui.py +24 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
from .commandenv import ResultRow
|
|
2
|
+
from .exceptions import CommandLineError, NoDataError
|
|
3
|
+
from .parsing import (
|
|
4
|
+
AvoidPlacesArgument, BlackMarketSwitch, FleetCarrierArgument,
|
|
5
|
+
MutuallyExclusiveGroup, NoPlanetSwitch, OdysseyArgument,
|
|
6
|
+
PadSizeArgument, ParseArgument, PlanetaryArgument,
|
|
7
|
+
)
|
|
8
|
+
from ..tradedb import TradeDB, System, Station
|
|
9
|
+
from ..formatting import RowFormat
|
|
10
|
+
from sqlalchemy import text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
######################################################################
|
|
14
|
+
# Parser config
|
|
15
|
+
|
|
16
|
+
help='Find places to sell a given item within range of a given station.'
|
|
17
|
+
name='sell'
|
|
18
|
+
epilog=None
|
|
19
|
+
wantsTradeDB=True
|
|
20
|
+
arguments = [
|
|
21
|
+
ParseArgument('item', help='Name of item you want to sell.', type=str),
|
|
22
|
+
]
|
|
23
|
+
switches = [
|
|
24
|
+
ParseArgument(
|
|
25
|
+
'--demand', '--quantity',
|
|
26
|
+
help='Limit to stations known to have at least this much demand.',
|
|
27
|
+
default=0,
|
|
28
|
+
type=int,
|
|
29
|
+
),
|
|
30
|
+
ParseArgument('--near',
|
|
31
|
+
help='Find buyers within jump range of this system.',
|
|
32
|
+
type=str
|
|
33
|
+
),
|
|
34
|
+
ParseArgument('--ly-per',
|
|
35
|
+
help='Maximum light years per jump.',
|
|
36
|
+
default=None,
|
|
37
|
+
dest='maxLyPer',
|
|
38
|
+
metavar='N.NN',
|
|
39
|
+
type=float,
|
|
40
|
+
),
|
|
41
|
+
ParseArgument('--age', '--max-days-old', '-MD',
|
|
42
|
+
help = 'Maximum age (in days) of trade data to use.',
|
|
43
|
+
metavar = 'DAYS',
|
|
44
|
+
type = float,
|
|
45
|
+
dest = 'maxAge',
|
|
46
|
+
),
|
|
47
|
+
AvoidPlacesArgument(),
|
|
48
|
+
PadSizeArgument(),
|
|
49
|
+
MutuallyExclusiveGroup(
|
|
50
|
+
NoPlanetSwitch(),
|
|
51
|
+
PlanetaryArgument(),
|
|
52
|
+
),
|
|
53
|
+
FleetCarrierArgument(),
|
|
54
|
+
OdysseyArgument(),
|
|
55
|
+
BlackMarketSwitch(),
|
|
56
|
+
ParseArgument('--limit',
|
|
57
|
+
help='Maximum number of results to list.',
|
|
58
|
+
default=None,
|
|
59
|
+
type=int,
|
|
60
|
+
),
|
|
61
|
+
ParseArgument('--price-sort', '-P',
|
|
62
|
+
help='(When using --near) Sort by price not distance',
|
|
63
|
+
action='store_true',
|
|
64
|
+
default=False,
|
|
65
|
+
dest='sortByPrice',
|
|
66
|
+
),
|
|
67
|
+
ParseArgument('--gt',
|
|
68
|
+
help='Limit to prices above Ncr',
|
|
69
|
+
metavar='N',
|
|
70
|
+
dest='gt',
|
|
71
|
+
type="credits",
|
|
72
|
+
),
|
|
73
|
+
ParseArgument('--lt',
|
|
74
|
+
help='Limit to prices below Ncr',
|
|
75
|
+
metavar='N',
|
|
76
|
+
dest='lt',
|
|
77
|
+
type="credits",
|
|
78
|
+
),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
######################################################################
|
|
82
|
+
# Perform query and populate result set
|
|
83
|
+
|
|
84
|
+
def run(results, cmdenv, tdb: TradeDB):
|
|
85
|
+
"""
|
|
86
|
+
Backend-neutral implementation:
|
|
87
|
+
- Use SQLAlchemy text() with NAMED binds (':param') instead of '?'.
|
|
88
|
+
- Eagerly materialize rows via engine.connect().execute(...).fetchall()
|
|
89
|
+
to avoid closed-cursor errors when iterating later.
|
|
90
|
+
- Preserve all existing filters, sorting, and output fields.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
if cmdenv.lt and cmdenv.gt:
|
|
94
|
+
if cmdenv.lt <= cmdenv.gt:
|
|
95
|
+
raise CommandLineError("--gt must be lower than --lt")
|
|
96
|
+
|
|
97
|
+
item = tdb.lookupItem(cmdenv.item)
|
|
98
|
+
cmdenv.DEBUG0("Looking up item {} (#{})", item.name(), item.ID)
|
|
99
|
+
|
|
100
|
+
avoidSystems = {s for s in cmdenv.avoidPlaces if isinstance(s, System)}
|
|
101
|
+
avoidStations = {s for s in cmdenv.avoidPlaces if isinstance(s, Station)}
|
|
102
|
+
|
|
103
|
+
results.summary = ResultRow()
|
|
104
|
+
results.summary.item = item
|
|
105
|
+
results.summary.avoidSystems = avoidSystems
|
|
106
|
+
results.summary.avoidStations = avoidStations
|
|
107
|
+
|
|
108
|
+
# Detail: average demand price for this item (portable named bind)
|
|
109
|
+
if cmdenv.detail:
|
|
110
|
+
with tdb.engine.connect() as conn:
|
|
111
|
+
avg_val = conn.execute(
|
|
112
|
+
text("""
|
|
113
|
+
SELECT AVG(si.demand_price)
|
|
114
|
+
FROM StationItem AS si
|
|
115
|
+
WHERE si.item_id = :item_id AND si.demand_price > 0
|
|
116
|
+
"""),
|
|
117
|
+
{"item_id": item.ID},
|
|
118
|
+
).scalar()
|
|
119
|
+
results.summary.avg = int(avg_val or 0)
|
|
120
|
+
|
|
121
|
+
# Build the main query with named binds
|
|
122
|
+
columns = "si.station_id, si.demand_price, si.demand_units"
|
|
123
|
+
where = ["si.item_id = :item_id", "si.demand_price > 0"]
|
|
124
|
+
params = {"item_id": item.ID}
|
|
125
|
+
|
|
126
|
+
if cmdenv.demand:
|
|
127
|
+
where.append("si.demand_units >= :demand")
|
|
128
|
+
params["demand"] = cmdenv.demand
|
|
129
|
+
if cmdenv.lt:
|
|
130
|
+
where.append("si.demand_price < :lt")
|
|
131
|
+
params["lt"] = cmdenv.lt
|
|
132
|
+
if cmdenv.gt:
|
|
133
|
+
where.append("si.demand_price > :gt")
|
|
134
|
+
params["gt"] = cmdenv.gt
|
|
135
|
+
|
|
136
|
+
stmt = f"""
|
|
137
|
+
SELECT DISTINCT {columns}
|
|
138
|
+
FROM StationItem AS si
|
|
139
|
+
WHERE {' AND '.join(where)}
|
|
140
|
+
"""
|
|
141
|
+
cmdenv.DEBUG0('SQL: {} ; params={}', stmt, params)
|
|
142
|
+
|
|
143
|
+
# Execute and eagerly fetch rows
|
|
144
|
+
with tdb.engine.connect() as conn:
|
|
145
|
+
cur_rows = conn.execute(text(stmt), params).fetchall()
|
|
146
|
+
|
|
147
|
+
stationByID = tdb.stationByID
|
|
148
|
+
padSize = cmdenv.padSize
|
|
149
|
+
planetary = cmdenv.planetary
|
|
150
|
+
fleet = cmdenv.fleet
|
|
151
|
+
odyssey = cmdenv.odyssey
|
|
152
|
+
wantNoPlanet = cmdenv.noPlanet
|
|
153
|
+
wantBlackMarket = cmdenv.blackMarket
|
|
154
|
+
|
|
155
|
+
# System-based search
|
|
156
|
+
nearSystem = cmdenv.nearSystem
|
|
157
|
+
if nearSystem:
|
|
158
|
+
maxLy = cmdenv.maxLyPer or cmdenv.maxSystemLinkLy
|
|
159
|
+
results.summary.near = nearSystem
|
|
160
|
+
results.summary.ly = maxLy
|
|
161
|
+
distanceFn = nearSystem.distanceTo
|
|
162
|
+
else:
|
|
163
|
+
distanceFn = None
|
|
164
|
+
|
|
165
|
+
for (stationID, priceCr, demand) in cur_rows:
|
|
166
|
+
station = stationByID[stationID]
|
|
167
|
+
if padSize and not station.checkPadSize(padSize):
|
|
168
|
+
continue
|
|
169
|
+
if planetary and not station.checkPlanetary(planetary):
|
|
170
|
+
continue
|
|
171
|
+
if fleet and not station.checkFleet(fleet):
|
|
172
|
+
continue
|
|
173
|
+
if odyssey and not station.checkOdyssey(odyssey):
|
|
174
|
+
continue
|
|
175
|
+
if wantNoPlanet and station.planetary != 'N':
|
|
176
|
+
continue
|
|
177
|
+
if wantBlackMarket and station.blackMarket != 'Y':
|
|
178
|
+
continue
|
|
179
|
+
if station in avoidStations:
|
|
180
|
+
continue
|
|
181
|
+
if station.system in avoidSystems:
|
|
182
|
+
continue
|
|
183
|
+
maxAge, stnAge = cmdenv.maxAge, station.dataAge or float("inf")
|
|
184
|
+
if maxAge and stnAge > maxAge:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
row = ResultRow()
|
|
188
|
+
row.station = station
|
|
189
|
+
if distanceFn:
|
|
190
|
+
distance = distanceFn(row.station.system)
|
|
191
|
+
if distance > maxLy:
|
|
192
|
+
continue
|
|
193
|
+
row.dist = distance
|
|
194
|
+
row.price = priceCr
|
|
195
|
+
row.demand = demand
|
|
196
|
+
row.age = station.itemDataAgeStr
|
|
197
|
+
results.rows.append(row)
|
|
198
|
+
|
|
199
|
+
if not results.rows:
|
|
200
|
+
raise NoDataError("No available items found")
|
|
201
|
+
|
|
202
|
+
results.summary.sort = "Price"
|
|
203
|
+
results.rows.sort(key=lambda result: result.demand, reverse=True)
|
|
204
|
+
results.rows.sort(key=lambda result: result.price, reverse=True)
|
|
205
|
+
if nearSystem and not cmdenv.sortByPrice:
|
|
206
|
+
results.summary.sort = "Dist"
|
|
207
|
+
results.rows.sort(key=lambda result: result.dist)
|
|
208
|
+
|
|
209
|
+
limit = cmdenv.limit or 0
|
|
210
|
+
if limit > 0:
|
|
211
|
+
results.rows = results.rows[:limit]
|
|
212
|
+
|
|
213
|
+
return results
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
#######################################################################
|
|
217
|
+
## Transform result set into output
|
|
218
|
+
|
|
219
|
+
def render(results, cmdenv, tdb):
|
|
220
|
+
longestNamed = max(results.rows, key=lambda result: len(result.station.name()))
|
|
221
|
+
longestNameLen = len(longestNamed.station.name())
|
|
222
|
+
|
|
223
|
+
stnRowFmt = RowFormat()
|
|
224
|
+
stnRowFmt.addColumn('Station', '<', longestNameLen,
|
|
225
|
+
key=lambda row: row.station.name())
|
|
226
|
+
stnRowFmt.addColumn('Cost', '>', 10, 'n',
|
|
227
|
+
key=lambda row: row.price)
|
|
228
|
+
if cmdenv.detail:
|
|
229
|
+
stnRowFmt.addColumn('Demand', '>', 10,
|
|
230
|
+
key=lambda row: '{:n}'.format(row.demand) if row.demand >= 0 else '?')
|
|
231
|
+
if cmdenv.nearSystem:
|
|
232
|
+
stnRowFmt.addColumn('DistLy', '>', 6, '.2f',
|
|
233
|
+
key=lambda row: row.dist)
|
|
234
|
+
|
|
235
|
+
stnRowFmt.addColumn('Age/days', '>', 7,
|
|
236
|
+
key=lambda row: row.age)
|
|
237
|
+
stnRowFmt.addColumn('StnLs', '>', 10,
|
|
238
|
+
key=lambda row: row.station.distFromStar())
|
|
239
|
+
stnRowFmt.addColumn('B/mkt', '>', 4,
|
|
240
|
+
key=lambda row: TradeDB.marketStates[row.station.blackMarket])
|
|
241
|
+
stnRowFmt.addColumn("Pad", '>', '3',
|
|
242
|
+
key=lambda row: TradeDB.padSizes[row.station.maxPadSize])
|
|
243
|
+
stnRowFmt.addColumn("Plt", '>', '3',
|
|
244
|
+
key=lambda row: TradeDB.planetStates[row.station.planetary])
|
|
245
|
+
stnRowFmt.addColumn("Flc", '>', '3',
|
|
246
|
+
key=lambda row: TradeDB.fleetStates[row.station.fleet])
|
|
247
|
+
stnRowFmt.addColumn("Ody", '>', '3',
|
|
248
|
+
key=lambda row: TradeDB.odysseyStates[row.station.odyssey])
|
|
249
|
+
|
|
250
|
+
if not cmdenv.quiet:
|
|
251
|
+
heading, underline = stnRowFmt.heading()
|
|
252
|
+
print(heading, underline, sep='\n')
|
|
253
|
+
|
|
254
|
+
for row in results.rows:
|
|
255
|
+
print(stnRowFmt.format(row))
|
|
256
|
+
|
|
257
|
+
if cmdenv.detail:
|
|
258
|
+
print("{:{lnl}} {:>10n}".format(
|
|
259
|
+
"-- Average",
|
|
260
|
+
results.summary.avg,
|
|
261
|
+
lnl=longestNameLen,
|
|
262
|
+
))
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# tradedangerous/commands/shipvendor_cmd.py — DEPRECATED (no-op)
|
|
2
|
+
# This command no longer maintains ship lists directly.
|
|
3
|
+
# Guidance:
|
|
4
|
+
# • Import authoritative data via: trade import -P spansh | trade import -P eddblink
|
|
5
|
+
# • Search for ships via: trade buy --near "<place>" --ly N "<ship>"
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from .parsing import ParseArgument # to swallow arbitrary positional args
|
|
9
|
+
|
|
10
|
+
# ---- Command metadata ----
|
|
11
|
+
help = "DEPRECATED: no longer used. See deprecation banner when run."
|
|
12
|
+
name = "shipvendor"
|
|
13
|
+
epilog = None
|
|
14
|
+
acceptUnknown = True
|
|
15
|
+
|
|
16
|
+
# No DB access needed.
|
|
17
|
+
wantsTradeDB = False
|
|
18
|
+
usesTradeData = False
|
|
19
|
+
|
|
20
|
+
# Accept ANY number of positional args and ignore them (prevents parser errors).
|
|
21
|
+
arguments = (
|
|
22
|
+
ParseArgument(
|
|
23
|
+
'args',
|
|
24
|
+
help="(deprecated) ignored",
|
|
25
|
+
nargs='*',
|
|
26
|
+
type=str,
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
# No switches; unknown switches will still be rejected by the global parser.
|
|
30
|
+
switches = (
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _banner() -> str:
|
|
35
|
+
return (
|
|
36
|
+
"\n"
|
|
37
|
+
"=== DEPRECATION NOTICE: shipvendor ==================================\n"
|
|
38
|
+
"This command is no longer used and does not modify the database.\n"
|
|
39
|
+
"• Import authoritative data with: trade import -P eddblink | -P spansh\n"
|
|
40
|
+
"• Search for ships using: trade buy --near \"<place>\" --ly N \"<ship>\"\n"
|
|
41
|
+
"======================================================================\n"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run(results, cmdenv, tdb=None):
|
|
46
|
+
"""
|
|
47
|
+
No-op implementation: print banner and exit immediately.
|
|
48
|
+
All arguments/switches are ignored by design.
|
|
49
|
+
"""
|
|
50
|
+
banner = _banner()
|
|
51
|
+
try:
|
|
52
|
+
cmdenv.NOTE("{}", banner)
|
|
53
|
+
except Exception:
|
|
54
|
+
print(banner)
|
|
55
|
+
return False # Nothing to render
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def render(results, cmdenv, tdb=None):
|
|
59
|
+
# No output beyond the banner emitted in run().
|
|
60
|
+
return False # command is done
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# tradedangerous/commands/station_cmd.py — DEPRECATED (no-op)
|
|
2
|
+
# This command no longer edits station records directly.
|
|
3
|
+
#
|
|
4
|
+
# How to maintain station data going forward:
|
|
5
|
+
# • Import authoritative data via: trade import -P spansh | trade import -P eddblink
|
|
6
|
+
# • Solo/offline players (manual capture via EDMC plugin):
|
|
7
|
+
# TradeDangerous DB-Update for EDMC → https://github.com/bgol/UpdateTD
|
|
8
|
+
#
|
|
9
|
+
# To inspect a station’s data, use read-only commands like:
|
|
10
|
+
# trade station <name> ← (legacy pattern; now deprecated)
|
|
11
|
+
# trade local --near "<system>" (discoverability)
|
|
12
|
+
# trade market --origin "<station>" (market view)
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
from .parsing import ParseArgument
|
|
16
|
+
|
|
17
|
+
# ---- Command metadata ----
|
|
18
|
+
help = "DEPRECATED: no longer used. See deprecation banner when run."
|
|
19
|
+
name = "station"
|
|
20
|
+
epilog = None
|
|
21
|
+
acceptUnknown = True
|
|
22
|
+
|
|
23
|
+
# No DB access is needed for this no-op command.
|
|
24
|
+
wantsTradeDB = False
|
|
25
|
+
usesTradeData = False
|
|
26
|
+
|
|
27
|
+
# Accept ANY number of positional args and ignore them (prevents parser errors).
|
|
28
|
+
arguments = (
|
|
29
|
+
ParseArgument(
|
|
30
|
+
'args',
|
|
31
|
+
help="(deprecated) ignored",
|
|
32
|
+
nargs='*',
|
|
33
|
+
type=str,
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
# No switches; unknown switches will still be rejected by the global parser.
|
|
37
|
+
switches = (
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _banner() -> str:
|
|
42
|
+
return (
|
|
43
|
+
"\n"
|
|
44
|
+
"=== DEPRECATION NOTICE: station ====================================\n"
|
|
45
|
+
"This command no longer edits station records and does not modify the DB.\n"
|
|
46
|
+
"• Import station data via: trade import -P eddblink | -P spansh\n"
|
|
47
|
+
"• Solo/offline capture via EDMC plugin:\n"
|
|
48
|
+
" TradeDangerous DB-Update → https://github.com/bgol/UpdateTD\n"
|
|
49
|
+
"=====================================================================\n"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run(results, cmdenv, tdb=None):
|
|
54
|
+
"""
|
|
55
|
+
No-op implementation: print banner and exit immediately.
|
|
56
|
+
All arguments/switches are ignored by design.
|
|
57
|
+
"""
|
|
58
|
+
banner = _banner()
|
|
59
|
+
try:
|
|
60
|
+
cmdenv.NOTE("{}", banner)
|
|
61
|
+
except Exception:
|
|
62
|
+
print(banner)
|
|
63
|
+
return False # Nothing to render
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def render(results, cmdenv, tdb=None):
|
|
67
|
+
# No output beyond the banner emitted in run().
|
|
68
|
+
return
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# tradedangerous/commands/trade_cmd.py
|
|
2
|
+
import datetime
|
|
3
|
+
|
|
4
|
+
from .commandenv import ResultRow
|
|
5
|
+
from .exceptions import CommandLineError
|
|
6
|
+
from .parsing import ParseArgument
|
|
7
|
+
from tradedangerous import TradeORM
|
|
8
|
+
from tradedangerous.db import orm_models as models
|
|
9
|
+
from tradedangerous.formatting import RowFormat, max_len
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import select
|
|
12
|
+
from sqlalchemy.orm import aliased
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
######################################################################
|
|
16
|
+
# Parser config
|
|
17
|
+
|
|
18
|
+
help='Find potential trades between two given stations.'
|
|
19
|
+
name='trade'
|
|
20
|
+
epilog=None
|
|
21
|
+
wantsTradeDB=False
|
|
22
|
+
arguments = [
|
|
23
|
+
ParseArgument(
|
|
24
|
+
'origin',
|
|
25
|
+
help='Station you are purchasing from.',
|
|
26
|
+
type=str,
|
|
27
|
+
),
|
|
28
|
+
ParseArgument(
|
|
29
|
+
'dest',
|
|
30
|
+
help='Station you are selling to.',
|
|
31
|
+
type=str,
|
|
32
|
+
),
|
|
33
|
+
]
|
|
34
|
+
switches = [
|
|
35
|
+
ParseArgument('--gain-per-ton', '--gpt',
|
|
36
|
+
help = 'Specify the minimum gain per ton of cargo',
|
|
37
|
+
dest = 'minGainPerTon',
|
|
38
|
+
type = "credits",
|
|
39
|
+
default = 1,
|
|
40
|
+
),
|
|
41
|
+
ParseArgument('--limit', '-n',
|
|
42
|
+
help = 'Limit output to the top N results',
|
|
43
|
+
dest = 'limit',
|
|
44
|
+
type = int,
|
|
45
|
+
default = 0,
|
|
46
|
+
),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def age(now: datetime, modified: datetime) -> float:
|
|
51
|
+
""" Return age in hours between now and modified timestamp. """
|
|
52
|
+
delta = (now - modified).total_seconds() / 60.0
|
|
53
|
+
if delta < 90:
|
|
54
|
+
return f"{delta:.1f}M"
|
|
55
|
+
delta /= 60.0
|
|
56
|
+
if delta < 25:
|
|
57
|
+
return f"{delta:.1f}H"
|
|
58
|
+
delta /= 7.0
|
|
59
|
+
if delta < 7:
|
|
60
|
+
return f"{delta:.1f}D"
|
|
61
|
+
return f"{int(delta):n}D"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
######################################################################
|
|
65
|
+
# Perform query and populate result set
|
|
66
|
+
|
|
67
|
+
def run(results, cmdenv, tdb):
|
|
68
|
+
# IMPORTANT: resolve stations BEFORE constructing TradeCalc
|
|
69
|
+
tdb = TradeORM(tdenv=cmdenv)
|
|
70
|
+
|
|
71
|
+
lhs = tdb.lookup_station(cmdenv.origin)
|
|
72
|
+
if not lhs:
|
|
73
|
+
raise CommandLineError(f"Unknown origin station: {cmdenv.origin}")
|
|
74
|
+
cmdenv.DEBUG0("from id: system={}, station={}", lhs.system_id, lhs.station_id)
|
|
75
|
+
rhs = tdb.lookup_station(cmdenv.dest)
|
|
76
|
+
if not rhs:
|
|
77
|
+
raise CommandLineError(f"Unknown destination station: {cmdenv.dest}")
|
|
78
|
+
cmdenv.DEBUG0("to id..: system={}, station={}", rhs.system_id, rhs.station_id)
|
|
79
|
+
|
|
80
|
+
if lhs == rhs:
|
|
81
|
+
raise CommandLineError("Must specify two different stations.")
|
|
82
|
+
|
|
83
|
+
seller = aliased(models.StationItem, name="seller")
|
|
84
|
+
buyer = aliased(models.StationItem, name="buyer")
|
|
85
|
+
stmt = (
|
|
86
|
+
select(
|
|
87
|
+
models.Item,
|
|
88
|
+
seller.supply_price, seller.supply_units, seller.supply_level,
|
|
89
|
+
buyer.demand_price, buyer.demand_units, buyer.demand_level,
|
|
90
|
+
seller.modified, buyer.modified,
|
|
91
|
+
)
|
|
92
|
+
.where(
|
|
93
|
+
seller.station_id == lhs.station_id,
|
|
94
|
+
buyer.station_id == rhs.station_id,
|
|
95
|
+
seller.item_id == buyer.item_id,
|
|
96
|
+
seller.supply_price > 0,
|
|
97
|
+
buyer.demand_price > 0, # (optional extra guard)
|
|
98
|
+
buyer.demand_price >= seller.supply_price,
|
|
99
|
+
seller.item_id == models.Item.item_id,
|
|
100
|
+
)
|
|
101
|
+
.order_by((buyer.demand_price - seller.supply_price).desc())
|
|
102
|
+
)
|
|
103
|
+
compiled = stmt.compile(
|
|
104
|
+
dialect=tdb.session.bind.dialect,
|
|
105
|
+
compile_kwargs={"literal_binds": True}
|
|
106
|
+
)
|
|
107
|
+
cmdenv.DEBUG1("query: {}", compiled)
|
|
108
|
+
trades = tdb.session.execute(stmt).unique().all()
|
|
109
|
+
cmdenv.DEBUG0("Raw result count: {}", len(trades))
|
|
110
|
+
if not trades:
|
|
111
|
+
raise CommandLineError(f"No profitable trades {lhs.name} -> {rhs.name}")
|
|
112
|
+
|
|
113
|
+
results.summary = ResultRow(color=cmdenv.color)
|
|
114
|
+
results.summary.fromStation = lhs
|
|
115
|
+
results.summary.toStation = rhs
|
|
116
|
+
|
|
117
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
118
|
+
|
|
119
|
+
if cmdenv.limit > 0:
|
|
120
|
+
trades = trades[:cmdenv.limit]
|
|
121
|
+
|
|
122
|
+
for item, sup_price, sup_units, sup_level, dem_price, dem_units, dem_level, sup_age, dem_age in trades:
|
|
123
|
+
gain = dem_price - sup_price
|
|
124
|
+
if gain < cmdenv.minGainPerTon:
|
|
125
|
+
continue
|
|
126
|
+
results.rows.append({
|
|
127
|
+
"item": item.dbname(cmdenv.detail),
|
|
128
|
+
"sup_price": sup_price,
|
|
129
|
+
"sup_units": sup_units,
|
|
130
|
+
"sup_level": sup_level,
|
|
131
|
+
"dem_price": dem_price,
|
|
132
|
+
"dem_units": dem_units,
|
|
133
|
+
"dem_level": dem_level,
|
|
134
|
+
"sup_age": age(now, sup_age),
|
|
135
|
+
"dem_age": age(now, dem_age),
|
|
136
|
+
"gain": gain,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return results
|
|
140
|
+
|
|
141
|
+
#######################################################################
|
|
142
|
+
## Transform result set into output
|
|
143
|
+
|
|
144
|
+
def render(results, cmdenv, tdb):
|
|
145
|
+
longestNameLen = max_len(results.rows, key=lambda row: row["item"])
|
|
146
|
+
|
|
147
|
+
rowFmt = RowFormat()
|
|
148
|
+
rowFmt.addColumn('Item', '<', longestNameLen,
|
|
149
|
+
key=lambda row: row["item"])
|
|
150
|
+
rowFmt.addColumn('Profit', '>', 10, 'n',
|
|
151
|
+
key=lambda row: row["gain"])
|
|
152
|
+
rowFmt.addColumn('Cost', '>', 10, 'n',
|
|
153
|
+
key=lambda row: row["sup_price"])
|
|
154
|
+
# if cmdenv.detail > 1:
|
|
155
|
+
# rowFmt.addColumn('AvgCost', '>', 10,
|
|
156
|
+
# key=lambda row: tdb.avgSelling.get(row.item.ID, 0)
|
|
157
|
+
# )
|
|
158
|
+
rowFmt.addColumn('Buying', '>', 10, 'n',
|
|
159
|
+
key=lambda row: row["dem_price"])
|
|
160
|
+
# rowFmt.addColumn('AvgBuy', '>', 10,
|
|
161
|
+
# key=lambda row: tdb.avgBuying.get(row.item.ID, 0)
|
|
162
|
+
# )
|
|
163
|
+
|
|
164
|
+
if cmdenv.detail > 1:
|
|
165
|
+
rowFmt.addColumn('Supply', '>', 10,
|
|
166
|
+
key=lambda row: f'{row["sup_units"]:n}' if row["sup_units"] >= 0 else '?')
|
|
167
|
+
rowFmt.addColumn('Demand', '>', 10,
|
|
168
|
+
key=lambda row: f'{row["dem_units"]:n}' if row["dem_units"] >= 0 else '?')
|
|
169
|
+
if cmdenv.detail:
|
|
170
|
+
rowFmt.addColumn('SrcAge', '>', 9, 's',
|
|
171
|
+
key=lambda row: row["sup_age"])
|
|
172
|
+
rowFmt.addColumn('DstAge', '>', 9, 's',
|
|
173
|
+
key=lambda row: row["dem_age"])
|
|
174
|
+
|
|
175
|
+
if not cmdenv.quiet:
|
|
176
|
+
print(f"{len(results.rows)} trades found between {results.summary.fromStation.dbname()} and {results.summary.toStation.dbname()}.")
|
|
177
|
+
heading, underline = rowFmt.heading()
|
|
178
|
+
print(heading, underline, sep='\n')
|
|
179
|
+
|
|
180
|
+
for row in results.rows:
|
|
181
|
+
print(rowFmt.format(row))
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# tradedangerous/commands/update_cmd.py — DEPRECATED (no-op)
|
|
2
|
+
# This command no longer edits station price lists directly.
|
|
3
|
+
#
|
|
4
|
+
# How to maintain price/station data going forward:
|
|
5
|
+
# • Import authoritative data via: trade import -P spansh | trade import -P eddblink
|
|
6
|
+
# • Solo/offline players (manual capture via EDMC plugin):
|
|
7
|
+
# TradeDangerous DB-Update for EDMC → https://github.com/bgol/UpdateTD
|
|
8
|
+
#
|
|
9
|
+
# This module accepts and ignores any arguments/switches and exits immediately.
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
from .parsing import ParseArgument
|
|
13
|
+
|
|
14
|
+
# ---- Command metadata ----
|
|
15
|
+
help = "DEPRECATED: no longer used. See deprecation banner when run."
|
|
16
|
+
name = "update"
|
|
17
|
+
epilog = None
|
|
18
|
+
|
|
19
|
+
# Swallow unknown switches/args (loader must honor this flag).
|
|
20
|
+
acceptUnknown = True
|
|
21
|
+
|
|
22
|
+
# No DB access is needed for this no-op command.
|
|
23
|
+
wantsTradeDB = False
|
|
24
|
+
usesTradeData = False
|
|
25
|
+
|
|
26
|
+
# Accept ANY number of positional args and ignore them (prevents parser errors).
|
|
27
|
+
arguments = (
|
|
28
|
+
ParseArgument(
|
|
29
|
+
"args",
|
|
30
|
+
help="(deprecated) ignored",
|
|
31
|
+
nargs="*",
|
|
32
|
+
type=str,
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
# No switches; unknown switches will be ignored via acceptUnknown.
|
|
36
|
+
switches = (
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _banner() -> str:
|
|
41
|
+
return (
|
|
42
|
+
"\n"
|
|
43
|
+
"=== DEPRECATION NOTICE: update =====================================\n"
|
|
44
|
+
"This command no longer edits station price lists and does not modify the DB.\n"
|
|
45
|
+
"• Import data via: trade import -P eddblink | -P spansh\n"
|
|
46
|
+
"• Solo/offline capture via EDMC: TradeDangerous DB-Update → https://github.com/bgol/UpdateTD\n"
|
|
47
|
+
"=====================================================================\n"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run(results, cmdenv, tdb=None):
|
|
52
|
+
"""
|
|
53
|
+
No-op implementation: print banner and exit immediately.
|
|
54
|
+
All arguments/switches are ignored by design.
|
|
55
|
+
"""
|
|
56
|
+
banner = _banner()
|
|
57
|
+
try:
|
|
58
|
+
cmdenv.NOTE("{}", banner)
|
|
59
|
+
except Exception:
|
|
60
|
+
print(banner)
|
|
61
|
+
|
|
62
|
+
return False # all done
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def render(results, cmdenv, tdb=None):
|
|
66
|
+
# No output beyond the banner emitted in run().
|
|
67
|
+
return False # all done
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Provides an interface for correcting names that
|
|
2
|
+
# have changed in recent versions.
|
|
3
|
+
|
|
4
|
+
# Arbitrary, negative value to denote something that's been removed.
|
|
5
|
+
DELETED = -111
|
|
6
|
+
|
|
7
|
+
systems = {
|
|
8
|
+
"PANDAMONIUM": "PANDEMONIUM",
|
|
9
|
+
"ARGETLÁMH": "ARGETLAMH",
|
|
10
|
+
"LíFTHRUTI": "LIFTHRUTI",
|
|
11
|
+
"MANTóAC": "MANTOAC",
|
|
12
|
+
"NANTóAC": "NANTOAC",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
stations = {
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
categories = {
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
items = {
|
|
22
|
+
'POWER TRANSFER CONDUITS': 'Power Transfer Bus',
|
|
23
|
+
'LOW TEMPERATURE DIAMOND': 'Low Temperature Diamonds',
|
|
24
|
+
'COOLING HOSES': 'Micro-weave Cooling Hoses',
|
|
25
|
+
'METHANOL MONOHYDRATE': 'Methanol Monohydrate Crystals',
|
|
26
|
+
'OCCUPIED CRYOPOD': 'Occupied Escape Pod',
|
|
27
|
+
'SALVAGEABLE WRECKAGE': 'Wreckage Components',
|
|
28
|
+
'POLITICAL PRISONER': 'Political Prisoners',
|
|
29
|
+
'HOSTAGE': 'Hostages',
|
|
30
|
+
"VOID OPALS": "Void Opal",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def correctSystem(oldName):
|
|
34
|
+
try:
|
|
35
|
+
return systems[oldName.upper()]
|
|
36
|
+
except KeyError:
|
|
37
|
+
return oldName
|
|
38
|
+
|
|
39
|
+
def correctStation(systemName, oldName):
|
|
40
|
+
try:
|
|
41
|
+
return stations[systemName.upper() + "/" + oldName.upper()]
|
|
42
|
+
except KeyError:
|
|
43
|
+
return oldName
|
|
44
|
+
|
|
45
|
+
def correctCategory(oldName):
|
|
46
|
+
try:
|
|
47
|
+
return categories[oldName.upper()]
|
|
48
|
+
except KeyError:
|
|
49
|
+
return oldName
|
|
50
|
+
|
|
51
|
+
def correctItem(oldName):
|
|
52
|
+
try:
|
|
53
|
+
return items[oldName.upper()]
|
|
54
|
+
except KeyError:
|
|
55
|
+
return oldName
|