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,427 @@
1
+ from __future__ import annotations
2
+ from collections import defaultdict
3
+ from .commandenv import ResultRow
4
+ from .exceptions import CommandLineError, NoDataError
5
+ from ..formatting import RowFormat, max_len
6
+ from ..tradedb import Station, System, TradeDB
7
+ from .parsing import (
8
+ AvoidPlacesArgument, BlackMarketSwitch, FleetCarrierArgument, MutuallyExclusiveGroup,
9
+ NoPlanetSwitch, OdysseyArgument, PadSizeArgument, ParseArgument, PlanetaryArgument,
10
+ )
11
+ from sqlalchemy import text
12
+
13
+ # TODO: Add UPGRADE_MODE
14
+ ITEM_MODE = "Item"
15
+ SHIP_MODE = "Ship"
16
+
17
+ ######################################################################
18
+ # Parser config
19
+
20
+ help = 'Find places to buy a given item within range of a given station.'
21
+ name = 'buy'
22
+ epilog = None
23
+ wantsTradeDB = True
24
+ arguments = (
25
+ ParseArgument(
26
+ 'name',
27
+ help = 'Items or Ships to look for.',
28
+ type = str,
29
+ nargs = '+',
30
+ ),
31
+ )
32
+ switches = (
33
+ ParseArgument(
34
+ '--supply', '--quantity',
35
+ help = 'Limit to stations known to have at least this much supply.',
36
+ default = 0,
37
+ type = int,
38
+ ),
39
+ ParseArgument(
40
+ '--near',
41
+ help = 'Find sellers within jump range of this system.',
42
+ type = str
43
+ ),
44
+ ParseArgument(
45
+ '--ly',
46
+ help = '[Requires --near] Systems within this range of --near.',
47
+ default = None,
48
+ dest = 'maxLyPer',
49
+ metavar = 'N.NN',
50
+ type = float,
51
+ ),
52
+ ParseArgument(
53
+ '--limit',
54
+ help = 'Maximum number of results to list.',
55
+ default = None,
56
+ type = int,
57
+ ),
58
+ AvoidPlacesArgument(),
59
+ ParseArgument('--age', '--max-days-old', '-MD',
60
+ help = 'Maximum age (in days) of trade data to use.',
61
+ metavar = 'DAYS',
62
+ type = float,
63
+ dest = 'maxAge',
64
+ ),
65
+ PadSizeArgument(),
66
+ MutuallyExclusiveGroup(
67
+ NoPlanetSwitch(),
68
+ PlanetaryArgument(),
69
+ ),
70
+ FleetCarrierArgument(),
71
+ OdysseyArgument(),
72
+ BlackMarketSwitch(),
73
+ MutuallyExclusiveGroup(
74
+ ParseArgument(
75
+ '--one-stop', '-1',
76
+ help = 'Only list stations that carry all items listed.',
77
+ action = 'store_true',
78
+ dest = 'oneStop',
79
+ ),
80
+ ParseArgument(
81
+ '--price-sort', '-P',
82
+ help = '(When using --near) Sort by price not distance',
83
+ action = 'store_true',
84
+ default = False,
85
+ dest = 'sortByPrice',
86
+ ),
87
+ ParseArgument(
88
+ '--units-sort', '-S',
89
+ help = 'Sort by available units followed by price',
90
+ action = 'store_true',
91
+ default = False,
92
+ dest = 'sortByUnits',
93
+ ),
94
+ ),
95
+ ParseArgument(
96
+ '--gt',
97
+ help = 'Limit to prices above Ncr',
98
+ metavar = 'N',
99
+ dest = 'gt',
100
+ type = "credits",
101
+ ),
102
+ ParseArgument(
103
+ '--lt',
104
+ help = 'Limit to prices below Ncr',
105
+ metavar = 'N',
106
+ dest = 'lt',
107
+ type = "credits",
108
+ ),
109
+ ParseArgument('--ls-max',
110
+ help='Only consider stations up to this many ls from their star.',
111
+ metavar='LS',
112
+ dest='maxLs',
113
+ type=int,
114
+ default=0,
115
+ ),
116
+ )
117
+
118
+
119
+ def get_lookup_list(cmdenv, tdb):
120
+ # Credit: http://stackoverflow.com/a/952952/257645
121
+ # Turns [['a'],['b','c']] => ['a', 'b', 'c']
122
+ names = [
123
+ name for names in cmdenv.name for name in names.split(',')
124
+ ]
125
+ # We only support searching for one type of purchase a time: ship or item.
126
+ # Our first match is open-ended, but once we have matched one type of
127
+ # thing, the remaining arguments are all sourced from the same pool.
128
+ # Thus: [food, cobra, metals] is illegal but [metals, hydrogen] is legal.
129
+ mode = None
130
+
131
+ queries = {}
132
+ for name in names:
133
+ if mode is not SHIP_MODE:
134
+ # Either no mode selected yet or we are in ITEM_MODE.
135
+ # Consider categories first.
136
+ try:
137
+ category = tdb.lookupCategory(name)
138
+ for item in category.items:
139
+ names.append(item.name())
140
+ queries[item.ID] = item
141
+ mode = ITEM_MODE
142
+ continue
143
+ except LookupError:
144
+ pass
145
+
146
+ # Item names secondary.
147
+ try:
148
+ item = tdb.lookupItem(name)
149
+ cmdenv.DEBUG0("Looking up item {} (#{})", item.name(), item.ID)
150
+ queries[item.ID] = item
151
+ mode = ITEM_MODE
152
+ continue
153
+ except LookupError:
154
+ if mode is ITEM_MODE:
155
+ raise CommandLineError(
156
+ "Unrecognized item: {}".format(name)
157
+ )
158
+ pass
159
+
160
+ # Either no mode selected yet or we are in SHIP_MODE.
161
+ try:
162
+ ship = tdb.lookupShip(name)
163
+ cmdenv.DEBUG0("Looking up ship {} (#{})", ship.name(), ship.ID)
164
+ queries[ship.ID] = ship
165
+ mode = SHIP_MODE
166
+ continue
167
+ except LookupError:
168
+ if not mode:
169
+ raise CommandLineError(
170
+ "Unrecognized item/ship: {}".format(name)
171
+ )
172
+ raise CommandLineError(
173
+ "Unrecognized ship: {}".format(name)
174
+ )
175
+
176
+ return queries, mode
177
+
178
+
179
+ def sql_query(cmdenv, tdb, queries, mode):
180
+ """
181
+ Backend-portable query builder.
182
+ - Uses named binds (':param') instead of SQLite '?'.
183
+ - Materializes rows eagerly to avoid closed-cursor issues.
184
+ - Preserves return shapes:
185
+ * Ship: (ship_id, station_id, cost, 1)
186
+ * Item: (item_id, station_id, supply_price, supply_units)
187
+ """
188
+ ids = list(queries.keys())
189
+
190
+ # Build a stable, named-parameter IN(...) list
191
+ params = {}
192
+ placeholders = []
193
+ for i, val in enumerate(ids):
194
+ key = f"id{i}"
195
+ placeholders.append(f":{key}")
196
+ params[key] = val
197
+ id_list_sql = ",".join(placeholders)
198
+
199
+ if mode is SHIP_MODE:
200
+ columns = "s.ship_id, s.station_id, sh.cost, 1"
201
+ tables = "ShipVendor AS s JOIN Ship AS sh ON sh.ship_id = s.ship_id"
202
+ constraints = [f"(s.ship_id IN ({id_list_sql}))"]
203
+ else:
204
+ columns = "s.item_id, s.station_id, s.supply_price, s.supply_units"
205
+ tables = "StationItem AS s"
206
+ constraints = [
207
+ f"(s.item_id IN ({id_list_sql}))",
208
+ "(s.supply_price > 0)", # preserves index intent across backends
209
+ ]
210
+ if cmdenv.supply:
211
+ constraints.append("(s.supply_units >= :supply)")
212
+ params["supply"] = cmdenv.supply
213
+ if cmdenv.lt:
214
+ constraints.append("(s.supply_price < :lt)")
215
+ params["lt"] = cmdenv.lt
216
+ if cmdenv.gt:
217
+ constraints.append("(s.supply_price > :gt)")
218
+ params["gt"] = cmdenv.gt
219
+
220
+ where_clause = " AND ".join(constraints)
221
+ stmt = f"SELECT DISTINCT {columns} FROM {tables} WHERE {where_clause}"
222
+ cmdenv.DEBUG0('SQL: {} ; params={}', stmt, params)
223
+
224
+ # Eagerly fetch to avoid closed cursor when iterating later.
225
+ with tdb.engine.connect() as conn:
226
+ result = conn.execute(text(stmt), params)
227
+ rows = result.fetchall()
228
+ return rows
229
+
230
+
231
+
232
+ ######################################################################
233
+ # Perform query and populate result set
234
+
235
+
236
+ def run(results, cmdenv, tdb):
237
+ if cmdenv.lt and cmdenv.gt:
238
+ if cmdenv.lt <= cmdenv.gt:
239
+ raise CommandLineError("--gt must be lower than --lt")
240
+
241
+ # Find out what we're looking for.
242
+ queries, mode = get_lookup_list(cmdenv, tdb)
243
+ cmdenv.DEBUG0("{} query: {}", mode, queries.values())
244
+
245
+ avoidSystems = {s for s in cmdenv.avoidPlaces if isinstance(s, System)}
246
+ avoidStations = {s for s in cmdenv.avoidPlaces if isinstance(s, Station)}
247
+
248
+ # Summarize
249
+ results.summary = ResultRow()
250
+ results.summary.mode = mode
251
+ results.summary.queries = queries
252
+ results.summary.oneStop = cmdenv.oneStop
253
+ results.summary.avoidSystems = avoidSystems
254
+ results.summary.avoidStations = avoidStations
255
+
256
+ # In single mode with detail enabled, add average reports.
257
+ # Thus if you're looking up "algae" or the "asp", it'll
258
+ # tell you the average/ship cost.
259
+ singleMode = len(queries) == 1
260
+ if singleMode and cmdenv.detail:
261
+ first = list(queries.values())[0]
262
+ if mode is SHIP_MODE:
263
+ results.summary.avg = first.cost
264
+ else:
265
+ # Portable AVG with named bind; eager scalar fetch
266
+ with tdb.engine.connect() as conn:
267
+ avg_val = conn.execute(
268
+ text("""
269
+ SELECT AVG(si.supply_price) AS avg_price
270
+ FROM StationItem AS si
271
+ WHERE si.item_id = :item_id AND si.supply_price > 0
272
+ """),
273
+ {"item_id": first.ID},
274
+ ).scalar()
275
+ results.summary.avg = int(avg_val or 0)
276
+
277
+ # System-based search
278
+ nearSystem = cmdenv.nearSystem
279
+ if nearSystem:
280
+ maxLy = cmdenv.maxLyPer or cmdenv.maxSystemLinkLy
281
+ results.summary.near = nearSystem
282
+ results.summary.ly = maxLy
283
+ distanceFn = nearSystem.distanceTo
284
+ else:
285
+ distanceFn = None
286
+
287
+ oneStopMode = cmdenv.oneStop
288
+ padSize = cmdenv.padSize
289
+ planetary = cmdenv.planetary
290
+ fleet = cmdenv.fleet
291
+ odyssey = cmdenv.odyssey
292
+ wantNoPlanet = cmdenv.noPlanet
293
+ wantBlackMarket = cmdenv.blackMarket
294
+ mls = cmdenv.maxLs
295
+
296
+ stations = defaultdict(list)
297
+ stationByID = tdb.stationByID
298
+
299
+ cur = sql_query(cmdenv, tdb, queries, mode)
300
+ for (ID, stationID, price, units) in cur:
301
+ station = stationByID[stationID]
302
+ if padSize and not station.checkPadSize(padSize):
303
+ continue
304
+ if planetary and not station.checkPlanetary(planetary):
305
+ continue
306
+ if fleet and not station.checkFleet(fleet):
307
+ continue
308
+ if odyssey and not station.checkOdyssey(odyssey):
309
+ continue
310
+ if wantNoPlanet and station.planetary != 'N':
311
+ continue
312
+ if wantBlackMarket and station.blackMarket != 'Y':
313
+ continue
314
+ if station in avoidStations:
315
+ continue
316
+ if station.system in avoidSystems:
317
+ continue
318
+ maxAge, stnAge = cmdenv.maxAge, station.dataAge or float("inf")
319
+ if maxAge and stnAge > maxAge:
320
+ continue
321
+
322
+ row = ResultRow()
323
+ row.station = station
324
+ if mls:
325
+ distanceFromStar = station.lsFromStar
326
+ if distanceFromStar > mls:
327
+ continue
328
+ if distanceFn:
329
+ distance = distanceFn(row.station.system)
330
+ if distance > maxLy:
331
+ continue
332
+ row.dist = distance
333
+ row.item = queries[ID]
334
+ row.price = price
335
+ row.units = units
336
+ row.age = station.itemDataAgeStr
337
+ if oneStopMode:
338
+ stationRows = stations[stationID]
339
+ stationRows.append(row)
340
+ if len(stationRows) >= len(queries):
341
+ results.rows.extend(stationRows)
342
+ else:
343
+ results.rows.append(row)
344
+
345
+ if not results.rows:
346
+ if oneStopMode and len(stations):
347
+ raise NoDataError("No one-stop stations found")
348
+ raise NoDataError("No available items found")
349
+
350
+ if oneStopMode and not singleMode:
351
+ results.rows.sort(key = lambda result: result.item.name())
352
+ results.rows.sort(key = lambda result: result.station.name())
353
+ if cmdenv.sortByUnits:
354
+ results.summary.sort = "units"
355
+ results.rows.sort(key = lambda result: result.price)
356
+ results.rows.sort(key = lambda result: result.units, reverse = True)
357
+ else:
358
+ if not oneStopMode:
359
+ results.summary.sort = "Price"
360
+ results.rows.sort(key = lambda result: result.units, reverse = True)
361
+ results.rows.sort(key = lambda result: result.price)
362
+ if nearSystem and not cmdenv.sortByPrice:
363
+ results.summary.sort = "Ly"
364
+ results.rows.sort(key = lambda result: result.dist)
365
+
366
+ limit = cmdenv.limit or 0
367
+ if limit > 0:
368
+ results.rows = results.rows[:limit]
369
+
370
+ return results
371
+
372
+
373
+
374
+ #######################################################################
375
+ # # Transform result set into output
376
+
377
+
378
+ def render(results, cmdenv, tdb):
379
+ mode = results.summary.mode
380
+ singleMode = len(results.summary.queries) == 1
381
+ maxStnLen = max_len(results.rows, key = lambda row: row.station.name())
382
+
383
+ stnRowFmt = RowFormat()
384
+ stnRowFmt.addColumn('Station', '<', maxStnLen,
385
+ key = lambda row: row.station.name())
386
+ if not singleMode:
387
+ maxItmLen = max_len(results.rows, key = lambda row: row.item.name(cmdenv.detail))
388
+ stnRowFmt.addColumn(results.summary.mode, '<', maxItmLen,
389
+ key = lambda row: row.item.name(cmdenv.detail)
390
+ )
391
+ if mode is not SHIP_MODE or not singleMode:
392
+ stnRowFmt.addColumn('Cost', '>', 10, 'n',
393
+ key = lambda row: row.price)
394
+ if mode is not SHIP_MODE:
395
+ stnRowFmt.addColumn('Units', '>', 10,
396
+ key = lambda row: '{:n}'.format(row.units) if row.units >= 0 else '?')
397
+
398
+ if cmdenv.nearSystem:
399
+ stnRowFmt.addColumn('DistLy', '>', 6, '.2f',
400
+ key = lambda row: row.dist)
401
+
402
+ if mode is not SHIP_MODE:
403
+ stnRowFmt.addColumn('Age/days', '>', 7,
404
+ key = lambda row: row.age)
405
+ stnRowFmt.addColumn("StnLs", '>', 10,
406
+ key = lambda row: row.station.distFromStar())
407
+ stnRowFmt.addColumn('B/mkt', '>', 4,
408
+ key = lambda row: TradeDB.marketStates[row.station.blackMarket])
409
+ stnRowFmt.addColumn("Pad", '>', '3',
410
+ key = lambda row: TradeDB.padSizes[row.station.maxPadSize])
411
+ stnRowFmt.addColumn("Plt", '>', '3',
412
+ key = lambda row: TradeDB.planetStates[row.station.planetary])
413
+ stnRowFmt.addColumn("Flc", '>', '3',
414
+ key = lambda row: TradeDB.fleetStates[row.station.fleet])
415
+ stnRowFmt.addColumn("Ody", '>', '3',
416
+ key = lambda row: TradeDB.odysseyStates[row.station.odyssey])
417
+
418
+ if not cmdenv.quiet:
419
+ heading, underline = stnRowFmt.heading()
420
+ print(heading, underline, sep = '\n')
421
+
422
+ for row in results.rows:
423
+ print(stnRowFmt.format(row))
424
+
425
+ if singleMode and cmdenv.detail:
426
+ msg = "-- Ship Cost" if mode is SHIP_MODE else "-- Average"
427
+ print(f"{msg:{maxStnLen}} {results.summary.avg:>10n}")