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.
Files changed (87) hide show
  1. py.typed +1 -0
  2. trade.py +49 -0
  3. tradedangerous/__init__.py +43 -0
  4. tradedangerous/cache.py +1381 -0
  5. tradedangerous/cli.py +136 -0
  6. tradedangerous/commands/TEMPLATE.py +74 -0
  7. tradedangerous/commands/__init__.py +244 -0
  8. tradedangerous/commands/buildcache_cmd.py +102 -0
  9. tradedangerous/commands/buy_cmd.py +427 -0
  10. tradedangerous/commands/commandenv.py +372 -0
  11. tradedangerous/commands/exceptions.py +94 -0
  12. tradedangerous/commands/export_cmd.py +150 -0
  13. tradedangerous/commands/import_cmd.py +222 -0
  14. tradedangerous/commands/local_cmd.py +243 -0
  15. tradedangerous/commands/market_cmd.py +207 -0
  16. tradedangerous/commands/nav_cmd.py +252 -0
  17. tradedangerous/commands/olddata_cmd.py +270 -0
  18. tradedangerous/commands/parsing.py +221 -0
  19. tradedangerous/commands/rares_cmd.py +298 -0
  20. tradedangerous/commands/run_cmd.py +1521 -0
  21. tradedangerous/commands/sell_cmd.py +262 -0
  22. tradedangerous/commands/shipvendor_cmd.py +60 -0
  23. tradedangerous/commands/station_cmd.py +68 -0
  24. tradedangerous/commands/trade_cmd.py +181 -0
  25. tradedangerous/commands/update_cmd.py +67 -0
  26. tradedangerous/corrections.py +55 -0
  27. tradedangerous/csvexport.py +234 -0
  28. tradedangerous/db/__init__.py +27 -0
  29. tradedangerous/db/adapter.py +192 -0
  30. tradedangerous/db/config.py +107 -0
  31. tradedangerous/db/engine.py +259 -0
  32. tradedangerous/db/lifecycle.py +332 -0
  33. tradedangerous/db/locks.py +208 -0
  34. tradedangerous/db/orm_models.py +500 -0
  35. tradedangerous/db/paths.py +113 -0
  36. tradedangerous/db/utils.py +661 -0
  37. tradedangerous/edscupdate.py +565 -0
  38. tradedangerous/edsmupdate.py +474 -0
  39. tradedangerous/formatting.py +210 -0
  40. tradedangerous/fs.py +156 -0
  41. tradedangerous/gui.py +1146 -0
  42. tradedangerous/mapping.py +133 -0
  43. tradedangerous/mfd/__init__.py +103 -0
  44. tradedangerous/mfd/saitek/__init__.py +3 -0
  45. tradedangerous/mfd/saitek/directoutput.py +678 -0
  46. tradedangerous/mfd/saitek/x52pro.py +195 -0
  47. tradedangerous/misc/checkpricebounds.py +287 -0
  48. tradedangerous/misc/clipboard.py +49 -0
  49. tradedangerous/misc/coord64.py +83 -0
  50. tradedangerous/misc/csvdialect.py +57 -0
  51. tradedangerous/misc/derp-sentinel.py +35 -0
  52. tradedangerous/misc/diff-system-csvs.py +159 -0
  53. tradedangerous/misc/eddb.py +81 -0
  54. tradedangerous/misc/eddn.py +349 -0
  55. tradedangerous/misc/edsc.py +437 -0
  56. tradedangerous/misc/edsm.py +121 -0
  57. tradedangerous/misc/importeddbstats.py +54 -0
  58. tradedangerous/misc/prices-json-exp.py +179 -0
  59. tradedangerous/misc/progress.py +194 -0
  60. tradedangerous/plugins/__init__.py +249 -0
  61. tradedangerous/plugins/edcd_plug.py +371 -0
  62. tradedangerous/plugins/eddblink_plug.py +861 -0
  63. tradedangerous/plugins/edmc_batch_plug.py +133 -0
  64. tradedangerous/plugins/spansh_plug.py +2647 -0
  65. tradedangerous/prices.py +211 -0
  66. tradedangerous/submit-distances.py +422 -0
  67. tradedangerous/templates/Added.csv +37 -0
  68. tradedangerous/templates/Category.csv +17 -0
  69. tradedangerous/templates/RareItem.csv +143 -0
  70. tradedangerous/templates/TradeDangerous.sql +338 -0
  71. tradedangerous/tools.py +40 -0
  72. tradedangerous/tradecalc.py +1302 -0
  73. tradedangerous/tradedb.py +2320 -0
  74. tradedangerous/tradeenv.py +313 -0
  75. tradedangerous/tradeenv.pyi +109 -0
  76. tradedangerous/tradeexcept.py +131 -0
  77. tradedangerous/tradeorm.py +183 -0
  78. tradedangerous/transfers.py +192 -0
  79. tradedangerous/utils.py +243 -0
  80. tradedangerous/version.py +16 -0
  81. tradedangerous-12.7.6.dist-info/METADATA +106 -0
  82. tradedangerous-12.7.6.dist-info/RECORD +87 -0
  83. tradedangerous-12.7.6.dist-info/WHEEL +5 -0
  84. tradedangerous-12.7.6.dist-info/entry_points.txt +3 -0
  85. tradedangerous-12.7.6.dist-info/licenses/LICENSE +373 -0
  86. tradedangerous-12.7.6.dist-info/top_level.txt +2 -0
  87. 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