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,372 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
import ijson
|
|
9
|
+
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
CommandLineError, FleetCarrierError, OdysseyError,
|
|
12
|
+
PadSizeError, PlanetaryError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from tradedangerous import TradeEnv
|
|
16
|
+
from tradedangerous.tradedb import AmbiguityError, Station
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if typing.TYPE_CHECKING:
|
|
20
|
+
from argparse import Namespace
|
|
21
|
+
from typing import Any, ModuleType
|
|
22
|
+
|
|
23
|
+
from tradedangerous import TradeDB, TradeORM
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# See: https://espterm.github.io/docs/VT100%20escape%20codes.html
|
|
27
|
+
# or : https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
|
|
28
|
+
#
|
|
29
|
+
# ANSI-compliant "terminal" streams support changing the color (including boldness) of text
|
|
30
|
+
# with 'Color Sequence' codes, consisting of an initializer (CS), one or more semicolon-separated (;)
|
|
31
|
+
# parameters, and a command code.
|
|
32
|
+
#
|
|
33
|
+
# The CSI is ESC '[' where esc is 1b in hex or 033 in octal.
|
|
34
|
+
# For color-changes, the command is 'm'.
|
|
35
|
+
# To clear all color-code/effect changes, the sequence is : [escape, '[', '0', 'm'].
|
|
36
|
+
#
|
|
37
|
+
ANSI_CSI = "\033["
|
|
38
|
+
ANSI_COLOR_CMD = "m"
|
|
39
|
+
ANSI_COLOR = {
|
|
40
|
+
"CLEAR": "0",
|
|
41
|
+
"red": "31",
|
|
42
|
+
"green": "32",
|
|
43
|
+
"yellow": "33",
|
|
44
|
+
"blue": "34",
|
|
45
|
+
"magenta": "35",
|
|
46
|
+
"cyan": "36",
|
|
47
|
+
"lightGray": "37",
|
|
48
|
+
"darkGray": "90",
|
|
49
|
+
"lightRed": "91",
|
|
50
|
+
"lightGreen": "92",
|
|
51
|
+
"lightYellow": "93",
|
|
52
|
+
"lightBlue": "94",
|
|
53
|
+
"lightMagenta": "95",
|
|
54
|
+
"lightCyan": "96",
|
|
55
|
+
"white": "97",
|
|
56
|
+
}
|
|
57
|
+
ANSI_CLEAR = f"{ANSI_CSI}{ANSI_COLOR['CLEAR']}{ANSI_COLOR_CMD}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ResultRow:
|
|
61
|
+
""" ResultRow captures a data item returned by a command. It's really an abstract namespace. """
|
|
62
|
+
def __init__(self, **kwargs: typing.Any) -> None:
|
|
63
|
+
for k, v in kwargs.items():
|
|
64
|
+
setattr(self, k, v)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CommandResults:
|
|
68
|
+
""" Encapsulates the results returned by running a command. """
|
|
69
|
+
cmdenv: 'CommandEnv'
|
|
70
|
+
summary: ResultRow
|
|
71
|
+
rows: list[ResultRow]
|
|
72
|
+
|
|
73
|
+
def __init__(self, cmdenv: 'CommandEnv') -> None:
|
|
74
|
+
self.cmdenv = cmdenv
|
|
75
|
+
self.summary = ResultRow()
|
|
76
|
+
self.rows = []
|
|
77
|
+
|
|
78
|
+
def render(self, cmdenv: 'CommandEnv' = None, tdb: TradeDB | TradeORM | None = None) -> None:
|
|
79
|
+
cmdenv = cmdenv or self.cmdenv
|
|
80
|
+
tdb = tdb or cmdenv.tdb
|
|
81
|
+
cmdenv._cmd.render(self, cmdenv, tdb)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CommandEnv(TradeEnv):
|
|
85
|
+
"""
|
|
86
|
+
Base class for a TradeDangerous sub-command which has auxilliary
|
|
87
|
+
"environment" data in the form of command line options.
|
|
88
|
+
"""
|
|
89
|
+
def __init__(self, properties: dict[str, Any] | Namespace | None, argv: list[str] | None, cmdModule: ModuleType | None) -> None:
|
|
90
|
+
super().__init__(properties = properties)
|
|
91
|
+
|
|
92
|
+
self.tdb = None
|
|
93
|
+
self.mfd = None
|
|
94
|
+
self.argv = argv or sys.argv
|
|
95
|
+
self._preflight_done = False
|
|
96
|
+
|
|
97
|
+
if self.detail and self.quiet:
|
|
98
|
+
raise CommandLineError("'--detail' (-v) and '--quiet' (-q) are mutually exclusive.")
|
|
99
|
+
|
|
100
|
+
self._cmd = cmdModule
|
|
101
|
+
self.wantsTradeDB = getattr(cmdModule, 'wantsTradeDB', True)
|
|
102
|
+
self.usesTradeData = getattr(cmdModule, 'usesTradeData', False)
|
|
103
|
+
|
|
104
|
+
# We need to relocate to the working directory so that
|
|
105
|
+
# we can load a TradeDB after this without things going
|
|
106
|
+
# pear-shaped
|
|
107
|
+
if not self.cwd and argv[0]:
|
|
108
|
+
cwdPath = Path('.').resolve()
|
|
109
|
+
exePath = Path(argv[0]).parent.resolve()
|
|
110
|
+
if cwdPath != exePath:
|
|
111
|
+
self.cwd = str(exePath)
|
|
112
|
+
self.DEBUG1("cwd at launch was: {}, changing to {} to match trade.py", cwdPath, self.cwd)
|
|
113
|
+
if self.cwd:
|
|
114
|
+
os.chdir(self.cwd)
|
|
115
|
+
|
|
116
|
+
def preflight(self) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Phase A: quick validation that must be able to short-circuit before any
|
|
119
|
+
heavy TradeDB(load=True) path is invoked.
|
|
120
|
+
|
|
121
|
+
Commands may optionally implement validateRunArgumentsFast(cmdenv).
|
|
122
|
+
"""
|
|
123
|
+
if self._preflight_done:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
self._preflight_done = True
|
|
127
|
+
|
|
128
|
+
fast_validator = getattr(self._cmd, "validateRunArgumentsFast", None)
|
|
129
|
+
if fast_validator:
|
|
130
|
+
fast_validator(self)
|
|
131
|
+
|
|
132
|
+
def run(self, tdb: TradeDB | TradeORM) -> CommandResults | bool | None:
|
|
133
|
+
""" Try and execute the business logic of the command. Query commands
|
|
134
|
+
will return a result set for us to render, whereas operational
|
|
135
|
+
commands will likely do their own rendering as they work. """
|
|
136
|
+
# Ensure fast validation is executed for non-CLI call paths too.
|
|
137
|
+
self.preflight()
|
|
138
|
+
|
|
139
|
+
# Set the current database context for this env and check that
|
|
140
|
+
# the properties we have are valid.
|
|
141
|
+
self.tdb = tdb
|
|
142
|
+
update_database_schema(self.tdb)
|
|
143
|
+
|
|
144
|
+
if self.wantsTradeDB:
|
|
145
|
+
self.checkFromToNear()
|
|
146
|
+
self.checkAvoids()
|
|
147
|
+
self.checkVias()
|
|
148
|
+
|
|
149
|
+
self.checkPlanetary()
|
|
150
|
+
self.checkFleet()
|
|
151
|
+
self.checkOdyssey()
|
|
152
|
+
self.checkPadSize()
|
|
153
|
+
self.checkMFD()
|
|
154
|
+
|
|
155
|
+
results = CommandResults(self)
|
|
156
|
+
return self._cmd.run(results, self, tdb)
|
|
157
|
+
|
|
158
|
+
def render(self, results: CommandResults) -> None:
|
|
159
|
+
self._cmd.render(self, results, self, self.tdb)
|
|
160
|
+
|
|
161
|
+
def checkMFD(self) -> None:
|
|
162
|
+
self.mfd = None
|
|
163
|
+
try:
|
|
164
|
+
if not self.x52pro:
|
|
165
|
+
return
|
|
166
|
+
except AttributeError:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# The x52 module throws some hard errors, so we really only want to
|
|
170
|
+
# import it as a last resort when the user has asked. We can't do a
|
|
171
|
+
# soft "try and import and tell the user later".
|
|
172
|
+
from tradedangerous.mfd import X52ProMFD # noqa
|
|
173
|
+
self.mfd = X52ProMFD()
|
|
174
|
+
|
|
175
|
+
def checkFromToNear(self) -> None:
|
|
176
|
+
if not self.wantsTradeDB:
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
def check(label, fieldName, wantStation):
|
|
180
|
+
key = getattr(self, fieldName, None)
|
|
181
|
+
if not key:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
place = self.tdb.lookupPlace(key)
|
|
186
|
+
except LookupError:
|
|
187
|
+
raise CommandLineError(
|
|
188
|
+
"Unrecognized {}: {}"
|
|
189
|
+
.format(label, key))
|
|
190
|
+
if not wantStation:
|
|
191
|
+
if isinstance(place, Station):
|
|
192
|
+
return place.system
|
|
193
|
+
return place
|
|
194
|
+
|
|
195
|
+
if isinstance(place, Station):
|
|
196
|
+
return place
|
|
197
|
+
|
|
198
|
+
# it's a system, we want a station
|
|
199
|
+
if not place.stations:
|
|
200
|
+
raise CommandLineError(
|
|
201
|
+
"Station name required for {}: "
|
|
202
|
+
"{} is a SYSTEM but has no stations.".format(
|
|
203
|
+
label, key
|
|
204
|
+
))
|
|
205
|
+
if len(place.stations) > 1:
|
|
206
|
+
raise AmbiguityError(
|
|
207
|
+
label,
|
|
208
|
+
key,
|
|
209
|
+
place.stations,
|
|
210
|
+
key=lambda st: (
|
|
211
|
+
f"{st.text()} — "
|
|
212
|
+
f"({st.system.posX:.1f}, {st.system.posY:.1f}, {st.system.posZ:.1f})"
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return place.stations[0]
|
|
217
|
+
|
|
218
|
+
def lookupPlace(label, fieldName):
|
|
219
|
+
key = getattr(self, fieldName, None)
|
|
220
|
+
if key:
|
|
221
|
+
return self.tdb.lookupPlace(key)
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
self.startStation = check('origin station', 'origin', True)
|
|
225
|
+
self.stopStation = check('destination station', 'dest', True)
|
|
226
|
+
self.origPlace = lookupPlace('origin', 'starting')
|
|
227
|
+
self.destPlace = lookupPlace('destination', 'ending')
|
|
228
|
+
self.nearSystem = check('system', 'near', False)
|
|
229
|
+
|
|
230
|
+
def checkAvoids(self) -> None:
|
|
231
|
+
"""
|
|
232
|
+
Process a list of avoidances.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
avoidItems = self.avoidItems = []
|
|
236
|
+
avoidPlaces = self.avoidPlaces = []
|
|
237
|
+
avoidances = self.avoid
|
|
238
|
+
if not self.avoid:
|
|
239
|
+
return
|
|
240
|
+
avoidances = self.avoid
|
|
241
|
+
|
|
242
|
+
tdb = self.tdb
|
|
243
|
+
|
|
244
|
+
# You can use --avoid to specify an item, system or station.
|
|
245
|
+
# and you can group them together with commas or list them
|
|
246
|
+
# individually.
|
|
247
|
+
for avoid in ','.join(avoidances).split(','):
|
|
248
|
+
# Is it an item?
|
|
249
|
+
item, place = None, None
|
|
250
|
+
try:
|
|
251
|
+
item = tdb.lookupItem(avoid)
|
|
252
|
+
avoidItems.append(item)
|
|
253
|
+
if tdb.normalizedStr(item.name()) == tdb.normalizedStr(avoid):
|
|
254
|
+
continue
|
|
255
|
+
except LookupError:
|
|
256
|
+
pass
|
|
257
|
+
# Or is it a place?
|
|
258
|
+
try:
|
|
259
|
+
place = tdb.lookupPlace(avoid)
|
|
260
|
+
avoidPlaces.append(place)
|
|
261
|
+
if tdb.normalizedStr(place.name()) == tdb.normalizedStr(avoid):
|
|
262
|
+
continue
|
|
263
|
+
continue
|
|
264
|
+
except LookupError:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
# If it was none of the above, whine about it
|
|
268
|
+
if not (item or place):
|
|
269
|
+
raise CommandLineError("Unknown item/system/station: {}".format(avoid))
|
|
270
|
+
|
|
271
|
+
# But if it matched more than once, whine about ambiguity
|
|
272
|
+
if item and place:
|
|
273
|
+
raise AmbiguityError('Avoidance', avoid, [ item, place.text() ])
|
|
274
|
+
|
|
275
|
+
self.DEBUG0("Avoiding items {}, places {}",
|
|
276
|
+
[ item.name() for item in avoidItems ],
|
|
277
|
+
[ place.name() for place in avoidPlaces ],
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def checkVias(self) -> None:
|
|
281
|
+
""" Process a list of station names and build them into a list of waypoints. """
|
|
282
|
+
viaPlaceNames = getattr(self, 'via', None)
|
|
283
|
+
viaPlaces = self.viaPlaces = []
|
|
284
|
+
# accept [ "a", "b,c", "d" ] by joining everything and then splitting it.
|
|
285
|
+
if viaPlaceNames:
|
|
286
|
+
for via in ",".join(viaPlaceNames).split(","):
|
|
287
|
+
viaPlaces.append(self.tdb.lookupPlace(via))
|
|
288
|
+
|
|
289
|
+
def checkPadSize(self) -> None:
|
|
290
|
+
padSize = getattr(self, 'padSize', None)
|
|
291
|
+
if not padSize:
|
|
292
|
+
return
|
|
293
|
+
padSize = ''.join(sorted(set(padSize))).upper()
|
|
294
|
+
if padSize == '?LMS':
|
|
295
|
+
self.padSize = None
|
|
296
|
+
return
|
|
297
|
+
self.padSize = padSize = padSize.upper()
|
|
298
|
+
for value in padSize:
|
|
299
|
+
if value not in 'SML?':
|
|
300
|
+
raise PadSizeError(padSize)
|
|
301
|
+
self.padSize = padSize
|
|
302
|
+
|
|
303
|
+
def checkPlanetary(self) -> None:
|
|
304
|
+
planetary = getattr(self, 'planetary', None)
|
|
305
|
+
if not planetary:
|
|
306
|
+
return
|
|
307
|
+
planetary = ''.join(sorted(set(planetary))).upper()
|
|
308
|
+
if planetary == '?NY':
|
|
309
|
+
self.planetary = None
|
|
310
|
+
return
|
|
311
|
+
self.planetary = planetary = planetary.upper()
|
|
312
|
+
for value in planetary:
|
|
313
|
+
if value not in 'YN?':
|
|
314
|
+
raise PlanetaryError(planetary)
|
|
315
|
+
self.planetary = planetary
|
|
316
|
+
|
|
317
|
+
def checkFleet(self) -> None:
|
|
318
|
+
fleet = getattr(self, 'fleet', None)
|
|
319
|
+
if not fleet:
|
|
320
|
+
return
|
|
321
|
+
fleet = ''.join(sorted(set(fleet))).upper()
|
|
322
|
+
for value in fleet:
|
|
323
|
+
if value not in 'YN?':
|
|
324
|
+
raise FleetCarrierError(fleet)
|
|
325
|
+
if fleet == '?NY':
|
|
326
|
+
self.fleet = None
|
|
327
|
+
return
|
|
328
|
+
self.fleet = fleet = fleet.upper()
|
|
329
|
+
|
|
330
|
+
def checkOdyssey(self) -> None:
|
|
331
|
+
odyssey = getattr(self, 'odyssey', None)
|
|
332
|
+
if not odyssey:
|
|
333
|
+
return
|
|
334
|
+
odyssey = ''.join(sorted(set(odyssey))).upper()
|
|
335
|
+
for value in odyssey:
|
|
336
|
+
if value not in 'YN?':
|
|
337
|
+
raise OdysseyError(odyssey)
|
|
338
|
+
if odyssey == '?NY':
|
|
339
|
+
self.odyssey = None
|
|
340
|
+
return
|
|
341
|
+
self.odyssey = odyssey.upper()
|
|
342
|
+
|
|
343
|
+
def colorize(self, color: str, raw_text: str) -> str:
|
|
344
|
+
"""
|
|
345
|
+
Set up some coloring for readability.
|
|
346
|
+
TODO: Rich already does this, use it instead?
|
|
347
|
+
"""
|
|
348
|
+
if (code := ANSI_COLOR.get(color)):
|
|
349
|
+
# Only do anything if there's a code for that.
|
|
350
|
+
return f"{ANSI_CSI}{code}{ANSI_COLOR_CMD}{raw_text}{ANSI_CLEAR}"
|
|
351
|
+
# Otherwise, keep it raw.
|
|
352
|
+
return raw_text
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def update_database_schema(tdb: TradeDB | TradeORM) -> None:
|
|
356
|
+
""" Check if there are database changes to be made, and if so, execute them. """
|
|
357
|
+
# TODO: This should really be a function of the DB itself and not something
|
|
358
|
+
# the caller has to ask the database to do for it.
|
|
359
|
+
template_folder = getattr(tdb, "templatePath", None)
|
|
360
|
+
if not template_folder:
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
db_change = Path(template_folder, "database_changes.json")
|
|
364
|
+
if not db_change.exists():
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
with db_change.open("r", encoding="utf-8") as file:
|
|
369
|
+
for change in ijson.items(file, 'item'):
|
|
370
|
+
tdb.getDB().execute(change)
|
|
371
|
+
finally:
|
|
372
|
+
db_change.unlink()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from ..tradeexcept import TradeException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
######################################################################
|
|
5
|
+
# Exceptions
|
|
6
|
+
|
|
7
|
+
class UsageError(TradeException):
|
|
8
|
+
def __init__(self, title, usage):
|
|
9
|
+
self.title, self.usage = title, usage
|
|
10
|
+
|
|
11
|
+
def __str__(self):
|
|
12
|
+
return f"{self.title}\n\n{self.usage}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CommandLineError(TradeException):
|
|
16
|
+
"""
|
|
17
|
+
Raised when you provide invalid input on the command line.
|
|
18
|
+
Attributes:
|
|
19
|
+
errorstr What to tell the user.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, errorStr, usage=None):
|
|
22
|
+
self.errorStr, self.usage = errorStr, usage
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
if self.usage:
|
|
26
|
+
return f"ERROR: {self.errorStr}\n\n{self.usage}"
|
|
27
|
+
return f"ERROR: {self.errorStr}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NoDataError(TradeException):
|
|
31
|
+
"""
|
|
32
|
+
Raised when a request is made for which no data can be found.
|
|
33
|
+
Attributes:
|
|
34
|
+
errorStr Describe the problem to the user.
|
|
35
|
+
"""
|
|
36
|
+
def __init__(self, errorStr):
|
|
37
|
+
self.errorStr = errorStr
|
|
38
|
+
|
|
39
|
+
def __str__(self):
|
|
40
|
+
return f"""Error: {self.errorStr}
|
|
41
|
+
Possible causes:
|
|
42
|
+
- No profitable runs or routes meet your criteria,
|
|
43
|
+
- Missing Systems or Stations along the route (see "local -vv"),
|
|
44
|
+
- Missing price data (see "market -vv" or "update -h"),
|
|
45
|
+
|
|
46
|
+
If you are not sure where to get data from, consider using a crowd-sourcing
|
|
47
|
+
project such as EDDBlink (https://github.com/eyeonus/EDDBlink).
|
|
48
|
+
|
|
49
|
+
For more help, see the TradeDangerous Wiki:
|
|
50
|
+
https://github.com/eyeonus/Trade-Dangerous/wiki
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PadSizeError(CommandLineError):
|
|
55
|
+
""" Raised when an invalid pad-size option is given. """
|
|
56
|
+
def __init__(self, value):
|
|
57
|
+
super().__init__(
|
|
58
|
+
f"Invalid --pad-size '{value}': Use a combination of one or more "
|
|
59
|
+
"from 'S' for Small, 'M' for Medium, 'L' for Large or "
|
|
60
|
+
"'?' for unknown, e.g. 'SML?' matches any pad size while "
|
|
61
|
+
"'M?' matches medium or unknown or 'L' matches only large."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PlanetaryError(CommandLineError):
|
|
66
|
+
""" Raised when an invalid planetary option is given. """
|
|
67
|
+
def __init__(self, value):
|
|
68
|
+
super().__init__(
|
|
69
|
+
f"Invalid --planetary '{value}': Use a combination of one or more "
|
|
70
|
+
"from 'Y' for Yes, 'N' for No or '?' for unknown, "
|
|
71
|
+
"e.g. 'YN?' matches any station while 'Y?' matches "
|
|
72
|
+
"yes or unknown, or 'N' matches only non-planetary stations."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class FleetCarrierError(CommandLineError):
|
|
77
|
+
""" Raised when an invalid fleet-carrier option is given. """
|
|
78
|
+
def __init__(self, value):
|
|
79
|
+
super().__init__(
|
|
80
|
+
f"Invalid --fleet-carrier '{value}': Use a combination of one or more "
|
|
81
|
+
"from 'Y' for Yes, 'N' for No or '?' for unknown, "
|
|
82
|
+
"e.g. 'YN?' matches any station while 'Y?' matches "
|
|
83
|
+
"yes or unknown, or 'N' matches only non-fleet-carrier stations."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
class OdysseyError(CommandLineError):
|
|
87
|
+
""" Raised when an invalid odyssey option is given. """
|
|
88
|
+
def __init__(self, value):
|
|
89
|
+
super().__init__(
|
|
90
|
+
f"Invalid --odyssey '{value}': Use a combination of one or more "
|
|
91
|
+
"from 'Y' for Yes, 'N' for No or '?' for unknown, "
|
|
92
|
+
"e.g. 'YN?' matches any station while 'Y?' matches "
|
|
93
|
+
"yes or unknown, or 'N' matches only non-odyssey stations."
|
|
94
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from ..csvexport import exportTableToFile
|
|
2
|
+
from .parsing import ParseArgument, MutuallyExclusiveGroup
|
|
3
|
+
from .exceptions import CommandLineError
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
######################################################################
|
|
7
|
+
# TradeDangerous :: Commands :: Export
|
|
8
|
+
#
|
|
9
|
+
# Generate the CSV files for the master data of the database.
|
|
10
|
+
#
|
|
11
|
+
######################################################################
|
|
12
|
+
# CAUTION: If the database structure gets changed this script might
|
|
13
|
+
# need some corrections.
|
|
14
|
+
######################################################################
|
|
15
|
+
|
|
16
|
+
######################################################################
|
|
17
|
+
# Parser config
|
|
18
|
+
|
|
19
|
+
help='CSV exporter for TradeDangerous database.'
|
|
20
|
+
name='export'
|
|
21
|
+
epilog=(
|
|
22
|
+
"CAUTION: If you don't specify a different path, the current "
|
|
23
|
+
"CSV files in the data directory will be overwritten with "
|
|
24
|
+
"the current content of the database.\n "
|
|
25
|
+
"If you have changed any CSV file and didn't rebuild the "
|
|
26
|
+
"database, they will be lost.\n "
|
|
27
|
+
"Use the 'buildcache' command first to rebuild the database."
|
|
28
|
+
)
|
|
29
|
+
wantsTradeDB=False # because we don't want the DB to be rebuild
|
|
30
|
+
arguments = [
|
|
31
|
+
]
|
|
32
|
+
switches = [
|
|
33
|
+
ParseArgument('--path',
|
|
34
|
+
help="Specify a different save location of the CSV files than the default.",
|
|
35
|
+
type=str,
|
|
36
|
+
default=None
|
|
37
|
+
),
|
|
38
|
+
MutuallyExclusiveGroup(
|
|
39
|
+
ParseArgument('--tables', "-T",
|
|
40
|
+
help='Specify comma separated tablenames to export.',
|
|
41
|
+
metavar='TABLE[,TABLE,...]',
|
|
42
|
+
type=str,
|
|
43
|
+
default=None
|
|
44
|
+
),
|
|
45
|
+
ParseArgument('--all-tables',
|
|
46
|
+
help='Include the price tables for export.',
|
|
47
|
+
dest='allTables',
|
|
48
|
+
action='store_true',
|
|
49
|
+
default=False
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
ParseArgument('--delete-empty',
|
|
53
|
+
help='Delete CSV files without content.',
|
|
54
|
+
dest='deleteEmpty',
|
|
55
|
+
action='store_true',
|
|
56
|
+
default=False
|
|
57
|
+
),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
######################################################################
|
|
61
|
+
# Perform query and populate result set
|
|
62
|
+
|
|
63
|
+
def run(results, cmdenv, tdb):
|
|
64
|
+
"""
|
|
65
|
+
Backend-neutral export of DB tables to CSV.
|
|
66
|
+
|
|
67
|
+
Changes:
|
|
68
|
+
* Use tradedangerous.db.lifecycle.ensure_fresh_db(rebuild=False) to verify a usable DB
|
|
69
|
+
without rebuilding (works for SQLite and MariaDB).
|
|
70
|
+
* Backend-aware announcement of the source DB (file path for SQLite, DSN for others).
|
|
71
|
+
* Table enumeration via SQLAlchemy inspector (no sqlite_master, no COLLATE quirks).
|
|
72
|
+
"""
|
|
73
|
+
# --- Sanity check the database without rebuilding (works for both backends) ---
|
|
74
|
+
from tradedangerous.db.lifecycle import ensure_fresh_db # local import avoids import-time tangles
|
|
75
|
+
summary = ensure_fresh_db(
|
|
76
|
+
backend=getattr(tdb.engine, "dialect", None).name if getattr(tdb, "engine", None) else "unknown",
|
|
77
|
+
engine=getattr(tdb, "engine", None),
|
|
78
|
+
data_dir=tdb.dataPath,
|
|
79
|
+
metadata=None,
|
|
80
|
+
mode="auto",
|
|
81
|
+
rebuild=False, # IMPORTANT: never rebuild from here; just report health
|
|
82
|
+
)
|
|
83
|
+
if summary.get("sane") != "Y":
|
|
84
|
+
reason = summary.get("reason", "unknown")
|
|
85
|
+
raise CommandLineError(
|
|
86
|
+
f"Database is not initialized/healthy (reason: {reason}). "
|
|
87
|
+
"Use 'buildcache' or an importer to (re)build it."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# --- Determine export target directory (same behavior as before) ---
|
|
91
|
+
exportPath = Path(cmdenv.path) if cmdenv.path else Path(tdb.dataDir)
|
|
92
|
+
if not exportPath.is_dir():
|
|
93
|
+
raise CommandLineError("Save location '{}' not found.".format(str(exportPath)))
|
|
94
|
+
|
|
95
|
+
# --- Announce which DB we will read from, backend-aware ---
|
|
96
|
+
try:
|
|
97
|
+
dialect = tdb.engine.dialect.name
|
|
98
|
+
if dialect == "sqlite":
|
|
99
|
+
source_label = f"SQLite file '{tdb.dbPath}'"
|
|
100
|
+
else:
|
|
101
|
+
# Hide password in DSN
|
|
102
|
+
source_label = f"{dialect} @ {tdb.engine.url.render_as_string(hide_password=True)}"
|
|
103
|
+
except Exception:
|
|
104
|
+
source_label = str(getattr(tdb, "dbPath", "Unknown DB"))
|
|
105
|
+
cmdenv.NOTE("Using database {}", source_label)
|
|
106
|
+
|
|
107
|
+
# --- Enumerate tables using SQLAlchemy inspector (backend-neutral) ---
|
|
108
|
+
from sqlalchemy import inspect
|
|
109
|
+
inspector = inspect(tdb.engine)
|
|
110
|
+
all_tables = inspector.get_table_names() # current schema / database
|
|
111
|
+
|
|
112
|
+
# Optional ignore list (preserve legacy default: skip StationItem unless --all-tables)
|
|
113
|
+
ignoreList = []
|
|
114
|
+
if not getattr(cmdenv, "allTables", False):
|
|
115
|
+
ignoreList.append("StationItem")
|
|
116
|
+
|
|
117
|
+
# --tables filtering (case-insensitive, like old COLLATE NOCASE)
|
|
118
|
+
if getattr(cmdenv, "tables", None):
|
|
119
|
+
requested = [t.strip() for t in cmdenv.tables.split(",") if t.strip()]
|
|
120
|
+
lower_map = {t.lower(): t for t in all_tables}
|
|
121
|
+
resolved = []
|
|
122
|
+
for name in requested:
|
|
123
|
+
found = lower_map.get(name.lower())
|
|
124
|
+
if found:
|
|
125
|
+
resolved.append(found)
|
|
126
|
+
else:
|
|
127
|
+
cmdenv.NOTE("Requested table '{}' not found; skipping", name)
|
|
128
|
+
table_list = sorted(set(resolved))
|
|
129
|
+
else:
|
|
130
|
+
table_list = sorted(set(all_tables))
|
|
131
|
+
|
|
132
|
+
# --- Export each table via csvexport (already refactored elsewhere) ---
|
|
133
|
+
for tableName in table_list:
|
|
134
|
+
if tableName in ignoreList:
|
|
135
|
+
cmdenv.NOTE("Ignore Table '{table}'", table=tableName)
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
cmdenv.NOTE("Export Table '{table}'", table=tableName)
|
|
139
|
+
|
|
140
|
+
lineCount, filePath = exportTableToFile(tdb, cmdenv, tableName, exportPath)
|
|
141
|
+
|
|
142
|
+
# Optionally delete empty CSVs
|
|
143
|
+
if getattr(cmdenv, "deleteEmpty", False) and lineCount == 0:
|
|
144
|
+
try:
|
|
145
|
+
filePath.unlink(missing_ok=True)
|
|
146
|
+
cmdenv.DEBUG0("Delete empty file {file}", file=filePath)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
cmdenv.DEBUG0("Failed to delete empty file {file}: {err}", file=filePath, err=e)
|
|
149
|
+
|
|
150
|
+
return False # we've handled everything
|