tradedangerous 11.5.3__py3-none-any.whl → 12.0.1__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.
Potentially problematic release.
This version of tradedangerous might be problematic. Click here for more details.
- tradedangerous/cache.py +567 -395
- tradedangerous/cli.py +2 -2
- tradedangerous/commands/TEMPLATE.py +25 -26
- tradedangerous/commands/__init__.py +8 -16
- tradedangerous/commands/buildcache_cmd.py +40 -10
- tradedangerous/commands/buy_cmd.py +57 -46
- tradedangerous/commands/commandenv.py +0 -2
- tradedangerous/commands/export_cmd.py +78 -50
- tradedangerous/commands/import_cmd.py +67 -31
- tradedangerous/commands/market_cmd.py +52 -19
- tradedangerous/commands/olddata_cmd.py +120 -107
- tradedangerous/commands/rares_cmd.py +122 -110
- tradedangerous/commands/run_cmd.py +118 -66
- tradedangerous/commands/sell_cmd.py +52 -45
- tradedangerous/commands/shipvendor_cmd.py +49 -234
- tradedangerous/commands/station_cmd.py +55 -485
- tradedangerous/commands/update_cmd.py +56 -420
- tradedangerous/csvexport.py +173 -162
- tradedangerous/db/__init__.py +27 -0
- tradedangerous/db/adapter.py +191 -0
- tradedangerous/db/config.py +95 -0
- tradedangerous/db/engine.py +246 -0
- tradedangerous/db/lifecycle.py +332 -0
- tradedangerous/db/locks.py +208 -0
- tradedangerous/db/orm_models.py +455 -0
- tradedangerous/db/paths.py +112 -0
- tradedangerous/db/utils.py +661 -0
- tradedangerous/gui.py +2 -2
- tradedangerous/plugins/eddblink_plug.py +387 -251
- tradedangerous/plugins/spansh_plug.py +2488 -821
- tradedangerous/prices.py +124 -142
- tradedangerous/templates/TradeDangerous.sql +6 -6
- tradedangerous/tradecalc.py +1227 -1109
- tradedangerous/tradedb.py +533 -384
- tradedangerous/tradeenv.py +12 -1
- tradedangerous/version.py +1 -1
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/METADATA +11 -7
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/RECORD +42 -38
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/WHEEL +1 -1
- tradedangerous/commands/update_gui.py +0 -721
- tradedangerous/jsonprices.py +0 -254
- tradedangerous/plugins/edapi_plug.py +0 -1071
- tradedangerous/plugins/journal_plug.py +0 -537
- tradedangerous/plugins/netlog_plug.py +0 -316
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/entry_points.txt +0 -0
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info/licenses}/LICENSE +0 -0
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/top_level.txt +0 -0
tradedangerous/cli.py
CHANGED
|
@@ -49,9 +49,9 @@ if "CPROF" in os.environ:
|
|
|
49
49
|
def main(argv = None):
|
|
50
50
|
if not argv:
|
|
51
51
|
argv = sys.argv
|
|
52
|
-
if sys.hexversion <
|
|
52
|
+
if sys.hexversion < 0x30813F0:
|
|
53
53
|
raise SystemExit(
|
|
54
|
-
"Sorry: TradeDangerous requires Python 3.
|
|
54
|
+
"Sorry: TradeDangerous requires Python 3.8.19 or higher.\n"
|
|
55
55
|
"For assistance, see:\n"
|
|
56
56
|
"\tBug Tracker: https://github.com/eyeonus/Trade-Dangerous/issues\n"
|
|
57
57
|
"\tDocumentation: https://github.com/eyeonus/Trade-Dangerous/wiki\n"
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
from commands.parsing import *
|
|
3
|
-
from formatting import RowFormat, ColumnFormat
|
|
1
|
+
from __future__ import annotations
|
|
4
2
|
|
|
5
3
|
######################################################################
|
|
6
4
|
# Parser config
|
|
@@ -8,25 +6,23 @@ from formatting import RowFormat, ColumnFormat
|
|
|
8
6
|
help = 'Describe your command briefly here for the top-level --help.'
|
|
9
7
|
name = 'TEMPLATE' # name of your .py file excluding the _cmd
|
|
10
8
|
epilog = None # text to print at the bottom of --help
|
|
11
|
-
wantsTradeDB = True # Should we try to load the cache at startup?
|
|
12
|
-
usesTradeData = True # Will we be needing trading data?
|
|
13
|
-
arguments = [
|
|
14
|
-
#ParseArgument('near', help='System to start from', type=str),
|
|
15
|
-
]
|
|
16
|
-
switches = [
|
|
17
|
-
#ParseArgument('--ly-per',
|
|
18
|
-
# help='Maximum light years per jump.',
|
|
19
|
-
# dest='maxLyPer',
|
|
20
|
-
# metavar='N.NN',
|
|
21
|
-
# type=float,
|
|
22
|
-
# ),
|
|
23
|
-
]
|
|
24
9
|
|
|
25
|
-
|
|
26
|
-
|
|
10
|
+
# Whether this command needs a TradeDB instance
|
|
11
|
+
wantsTradeDB = True
|
|
12
|
+
usesTradeData = False
|
|
27
13
|
|
|
28
|
-
|
|
29
|
-
#
|
|
14
|
+
# Parser wiring (keep tuples for consistency with loader)
|
|
15
|
+
from .parsing import ParseArgument # import specific helpers as needed
|
|
16
|
+
arguments = (
|
|
17
|
+
ParseArgument("name", help="Example positional(s).", type=str, nargs="*"),
|
|
18
|
+
)
|
|
19
|
+
switches = (
|
|
20
|
+
ParseArgument("--flag", help="Example flag.", action="store_true", default=False),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Runtime API
|
|
24
|
+
from .commandenv import ResultRow
|
|
25
|
+
from ..formatting import RowFormat, ColumnFormat # use package-relative imports
|
|
30
26
|
|
|
31
27
|
def run(results, cmdenv, tdb):
|
|
32
28
|
"""
|
|
@@ -44,12 +40,10 @@ def run(results, cmdenv, tdb):
|
|
|
44
40
|
"""
|
|
45
41
|
|
|
46
42
|
### TODO: Implement
|
|
47
|
-
|
|
43
|
+
row = ResultRow(example="ok")
|
|
44
|
+
results.rows.append(row)
|
|
48
45
|
return results
|
|
49
46
|
|
|
50
|
-
######################################################################
|
|
51
|
-
# Transform result set into output
|
|
52
|
-
|
|
53
47
|
def render(results, cmdenv, tdb):
|
|
54
48
|
"""
|
|
55
49
|
If run() returns a non-None value, the trade.py code will then
|
|
@@ -57,5 +51,10 @@ def render(results, cmdenv, tdb):
|
|
|
57
51
|
|
|
58
52
|
This is where you should generate any output from your command.
|
|
59
53
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
fmt = RowFormat()
|
|
55
|
+
fmt.addColumn("Example", "<", 10, key=lambda r: getattr(r, "example", ""))
|
|
56
|
+
if not cmdenv.quiet:
|
|
57
|
+
hdr, ul = fmt.heading()
|
|
58
|
+
print(hdr, ul, sep="\n")
|
|
59
|
+
for row in results.rows:
|
|
60
|
+
print(fmt.format(row))
|
|
@@ -101,12 +101,6 @@ class CommandIndex:
|
|
|
101
101
|
# Figure out the pre-indentation
|
|
102
102
|
cmdFmt = ' {:<12s} '
|
|
103
103
|
cmdFmtLen = len(cmdFmt.format(''))
|
|
104
|
-
# Generate a formatter which will produce nicely formatted text
|
|
105
|
-
# that wraps at column 78 but puts continuation text one character
|
|
106
|
-
# indented from where the previous text started, e.g
|
|
107
|
-
# cmd1 Cmd1 help text stuff
|
|
108
|
-
# continued cmd1 text
|
|
109
|
-
# cmd2 Cmd2 help text
|
|
110
104
|
tw = TextWrapper(
|
|
111
105
|
subsequent_indent = ' ' * (cmdFmtLen + 1),
|
|
112
106
|
width = 78,
|
|
@@ -117,14 +111,12 @@ class CommandIndex:
|
|
|
117
111
|
break_on_hyphens = True,
|
|
118
112
|
)
|
|
119
113
|
|
|
120
|
-
# List each command with its help text
|
|
121
114
|
lastCmdName = None
|
|
122
115
|
for cmdName, cmd in sorted(commandIndex.items()):
|
|
123
116
|
tw.initial_indent = cmdFmt.format(cmdName)
|
|
124
117
|
text += tw.fill(cmd.help) + "\n"
|
|
125
118
|
lastCmdName = cmdName
|
|
126
119
|
|
|
127
|
-
# Epilog
|
|
128
120
|
text += (
|
|
129
121
|
"\n"
|
|
130
122
|
"For additional help on a specific command, such as '{cmd}' use\n"
|
|
@@ -139,10 +131,6 @@ class CommandIndex:
|
|
|
139
131
|
"TradeDangerous provides a set of trade database "
|
|
140
132
|
"facilities for Elite:Dangerous.", self.usage(argv))
|
|
141
133
|
|
|
142
|
-
# ## TODO: Break this model up a bit more so that
|
|
143
|
-
# ## we just try and import the command you specify,
|
|
144
|
-
# ## and only worry about an index when that fails or
|
|
145
|
-
# ## the user requests usage.
|
|
146
134
|
cmdName, cmdModule = argv[1].casefold(), None
|
|
147
135
|
try:
|
|
148
136
|
cmdModule = commandIndex[cmdName]
|
|
@@ -172,7 +160,6 @@ class CommandIndex:
|
|
|
172
160
|
cmdModule = candidates[0][1]
|
|
173
161
|
|
|
174
162
|
class ArgParser(argparse.ArgumentParser):
|
|
175
|
-
|
|
176
163
|
def error(self, message):
|
|
177
164
|
raise exceptions.CommandLineError(message, self.format_usage())
|
|
178
165
|
|
|
@@ -244,9 +231,14 @@ class CommandIndex:
|
|
|
244
231
|
fromfilePath = _findFromFile(cmdModule.name)
|
|
245
232
|
if fromfilePath:
|
|
246
233
|
argv.insert(2, '{}{}'.format(fromfile_prefix, fromfilePath))
|
|
247
|
-
properties = parser.parse_args(argv[1:])
|
|
248
234
|
|
|
249
|
-
|
|
250
|
-
|
|
235
|
+
# Parse argv; optionally swallow unknown args/switches if the module allows it.
|
|
236
|
+
accept_unknown = getattr(cmdModule, 'acceptUnknown', False)
|
|
237
|
+
if accept_unknown:
|
|
238
|
+
properties, _unknown = parser.parse_known_args(argv[1:])
|
|
239
|
+
else:
|
|
240
|
+
properties = parser.parse_args(argv[1:])
|
|
251
241
|
|
|
242
|
+
parsed = CommandEnv(vars(properties), argv, cmdModule)
|
|
243
|
+
parsed.DEBUG0("Command line was: {}", argv)
|
|
252
244
|
return parsed
|
|
@@ -53,16 +53,46 @@ switches = [
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def run(results, cmdenv, tdb: TradeDB):
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
"""
|
|
57
|
+
BRUTE-FORCE rebuild of the cache/database.
|
|
58
|
+
|
|
59
|
+
Semantics preserved:
|
|
60
|
+
- If DB exists and --force not given => error
|
|
61
|
+
- SQL file must exist
|
|
62
|
+
- Performs a full destructive rebuild
|
|
63
|
+
|
|
64
|
+
Implementation change:
|
|
65
|
+
- Delegates to tradedangerous.db.lifecycle.ensure_fresh_db with mode='force'
|
|
66
|
+
so all backend-specific checks and rebuild steps run via the central path.
|
|
67
|
+
"""
|
|
68
|
+
# Deprecation note: keep short and visible but non-fatal.
|
|
69
|
+
print("NOTE: 'buildcache' is deprecated. Prefer 'update' or importer plugins. "
|
|
70
|
+
"Proceeding with a forced rebuild via db.lifecycle.ensure_fresh_db().")
|
|
71
|
+
|
|
72
|
+
# Honor legacy safety: require --force to overwrite an existing DB file.
|
|
73
|
+
if not cmdenv.force and tdb.dbPath.exists():
|
|
74
|
+
raise CommandLineError(
|
|
75
|
+
f"SQLite3 database '{tdb.dbFilename}' already exists.\n"
|
|
76
|
+
"Either remove the file first or use the '-f/--force' option."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Ensure the SQL source exists (buildCache ultimately relies on this path).
|
|
63
80
|
if not tdb.sqlPath.exists():
|
|
64
81
|
raise CommandLineError(f"SQL File does not exist: {tdb.sqlFilename}")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
|
|
83
|
+
# Force a rebuild through the lifecycle helper (works for both backends).
|
|
84
|
+
from tradedangerous.db.lifecycle import ensure_fresh_db
|
|
85
|
+
|
|
86
|
+
ensure_fresh_db(
|
|
87
|
+
backend=tdb.engine.dialect.name if getattr(tdb, "engine", None) else "sqlite",
|
|
88
|
+
engine=getattr(tdb, "engine", None),
|
|
89
|
+
data_dir=tdb.dataPath,
|
|
90
|
+
metadata=None,
|
|
91
|
+
mode="force",
|
|
92
|
+
tdb=tdb,
|
|
93
|
+
tdenv=cmdenv,
|
|
94
|
+
rebuild=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
68
97
|
return None
|
|
98
|
+
|
|
@@ -8,6 +8,7 @@ from .parsing import (
|
|
|
8
8
|
AvoidPlacesArgument, BlackMarketSwitch, FleetCarrierArgument, MutuallyExclusiveGroup,
|
|
9
9
|
NoPlanetSwitch, OdysseyArgument, PadSizeArgument, ParseArgument, PlanetaryArgument,
|
|
10
10
|
)
|
|
11
|
+
from sqlalchemy import text
|
|
11
12
|
|
|
12
13
|
# TODO: Add UPGRADE_MODE
|
|
13
14
|
ITEM_MODE = "Item"
|
|
@@ -176,52 +177,57 @@ def get_lookup_list(cmdenv, tdb):
|
|
|
176
177
|
|
|
177
178
|
|
|
178
179
|
def sql_query(cmdenv, tdb, queries, mode):
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
|
|
181
199
|
if mode is SHIP_MODE:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
's.ship_id',
|
|
186
|
-
's.station_id',
|
|
187
|
-
'sh.cost',
|
|
188
|
-
'1',
|
|
189
|
-
]
|
|
190
|
-
bindValues = []
|
|
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}))"]
|
|
191
203
|
else:
|
|
204
|
+
columns = "s.item_id, s.station_id, s.supply_price, s.supply_units"
|
|
192
205
|
tables = "StationItem AS s"
|
|
193
|
-
columns = [
|
|
194
|
-
's.item_id',
|
|
195
|
-
's.station_id',
|
|
196
|
-
's.supply_price',
|
|
197
|
-
's.supply_units',
|
|
198
|
-
]
|
|
199
206
|
constraints = [
|
|
200
|
-
"(s.item_id IN ({}))"
|
|
201
|
-
"(s.supply_price > 0)",
|
|
207
|
+
f"(s.item_id IN ({id_list_sql}))",
|
|
208
|
+
"(s.supply_price > 0)", # preserves index intent across backends
|
|
202
209
|
]
|
|
203
|
-
bindValues = []
|
|
204
|
-
|
|
205
|
-
# Additional constraints in ITEM_MODE
|
|
206
|
-
if mode is ITEM_MODE:
|
|
207
210
|
if cmdenv.supply:
|
|
208
|
-
constraints.append("(supply_units >=
|
|
209
|
-
|
|
211
|
+
constraints.append("(s.supply_units >= :supply)")
|
|
212
|
+
params["supply"] = cmdenv.supply
|
|
210
213
|
if cmdenv.lt:
|
|
211
|
-
constraints.append("(supply_price <
|
|
212
|
-
|
|
214
|
+
constraints.append("(s.supply_price < :lt)")
|
|
215
|
+
params["lt"] = cmdenv.lt
|
|
213
216
|
if cmdenv.gt:
|
|
214
|
-
constraints.append("(supply_price >
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
stmt = "
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
|
|
225
231
|
|
|
226
232
|
######################################################################
|
|
227
233
|
# Perform query and populate result set
|
|
@@ -256,14 +262,17 @@ def run(results, cmdenv, tdb):
|
|
|
256
262
|
if mode is SHIP_MODE:
|
|
257
263
|
results.summary.avg = first.cost
|
|
258
264
|
else:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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)
|
|
267
276
|
|
|
268
277
|
# System-based search
|
|
269
278
|
nearSystem = cmdenv.nearSystem
|
|
@@ -360,6 +369,8 @@ def run(results, cmdenv, tdb):
|
|
|
360
369
|
|
|
361
370
|
return results
|
|
362
371
|
|
|
372
|
+
|
|
373
|
+
|
|
363
374
|
#######################################################################
|
|
364
375
|
# # Transform result set into output
|
|
365
376
|
|
|
@@ -3,8 +3,6 @@ from .parsing import ParseArgument, MutuallyExclusiveGroup
|
|
|
3
3
|
from .exceptions import CommandLineError
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
import sqlite3
|
|
7
|
-
|
|
8
6
|
######################################################################
|
|
9
7
|
# TradeDangerous :: Commands :: Export
|
|
10
8
|
#
|
|
@@ -63,61 +61,91 @@ switches = [
|
|
|
63
61
|
# Perform query and populate result set
|
|
64
62
|
|
|
65
63
|
def run(results, cmdenv, tdb):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
from pathlib import Path
|
|
92
|
+
exportPath = Path(cmdenv.path) if cmdenv.path else Path(tdb.dataDir)
|
|
76
93
|
if not exportPath.is_dir():
|
|
77
94
|
raise CommandLineError("Save location '{}' not found.".format(str(exportPath)))
|
|
78
|
-
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
|
|
96
|
+
# --- Announce which DB we will read from, backend-aware ---
|
|
97
|
+
try:
|
|
98
|
+
dialect = tdb.engine.dialect.name
|
|
99
|
+
if dialect == "sqlite":
|
|
100
|
+
source_label = f"SQLite file '{tdb.dbPath}'"
|
|
101
|
+
else:
|
|
102
|
+
# Hide password in DSN
|
|
103
|
+
source_label = f"{dialect} @ {tdb.engine.url.render_as_string(hide_password=True)}"
|
|
104
|
+
except Exception:
|
|
105
|
+
source_label = str(getattr(tdb, "dbPath", "Unknown DB"))
|
|
106
|
+
cmdenv.NOTE("Using database {}", source_label)
|
|
107
|
+
|
|
108
|
+
# --- Enumerate tables using SQLAlchemy inspector (backend-neutral) ---
|
|
109
|
+
from sqlalchemy import inspect
|
|
110
|
+
inspector = inspect(tdb.engine)
|
|
111
|
+
all_tables = inspector.get_table_names() # current schema / database
|
|
112
|
+
|
|
113
|
+
# Optional ignore list (preserve legacy default: skip StationItem unless --all-tables)
|
|
85
114
|
ignoreList = []
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
cmdenv.
|
|
115
|
+
if not getattr(cmdenv, "allTables", False):
|
|
116
|
+
ignoreList.append("StationItem")
|
|
117
|
+
|
|
118
|
+
# --tables filtering (case-insensitive, like old COLLATE NOCASE)
|
|
119
|
+
if getattr(cmdenv, "tables", None):
|
|
120
|
+
requested = [t.strip() for t in cmdenv.tables.split(",") if t.strip()]
|
|
121
|
+
lower_map = {t.lower(): t for t in all_tables}
|
|
122
|
+
resolved = []
|
|
123
|
+
for name in requested:
|
|
124
|
+
found = lower_map.get(name.lower())
|
|
125
|
+
if found:
|
|
126
|
+
resolved.append(found)
|
|
127
|
+
else:
|
|
128
|
+
cmdenv.NOTE("Requested table '{}' not found; skipping", name)
|
|
129
|
+
table_list = sorted(set(resolved))
|
|
92
130
|
else:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
tableCursor = conn.cursor()
|
|
99
|
-
for row in tableCursor.execute("""
|
|
100
|
-
SELECT name
|
|
101
|
-
FROM sqlite_master
|
|
102
|
-
WHERE type = 'table'
|
|
103
|
-
AND name NOT LIKE 'sqlite_%'
|
|
104
|
-
{cmdTables}
|
|
105
|
-
ORDER BY name
|
|
106
|
-
""".format(cmdTables=tableStmt),
|
|
107
|
-
bindValues):
|
|
108
|
-
tableName = row['name']
|
|
131
|
+
table_list = sorted(set(all_tables))
|
|
132
|
+
|
|
133
|
+
# --- Export each table via csvexport (already refactored elsewhere) ---
|
|
134
|
+
for tableName in table_list:
|
|
109
135
|
if tableName in ignoreList:
|
|
110
|
-
# ignore the table
|
|
111
136
|
cmdenv.NOTE("Ignore Table '{table}'", table=tableName)
|
|
112
137
|
continue
|
|
113
|
-
|
|
138
|
+
|
|
114
139
|
cmdenv.NOTE("Export Table '{table}'", table=tableName)
|
|
115
|
-
|
|
116
|
-
# create CSV files
|
|
140
|
+
|
|
117
141
|
lineCount, filePath = exportTableToFile(tdb, cmdenv, tableName, exportPath)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
|
|
143
|
+
# Optionally delete empty CSVs
|
|
144
|
+
if getattr(cmdenv, "deleteEmpty", False) and lineCount == 0:
|
|
145
|
+
try:
|
|
146
|
+
filePath.unlink(missing_ok=True)
|
|
147
|
+
cmdenv.DEBUG0("Delete empty file {file}", file=filePath)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
cmdenv.DEBUG0("Failed to delete empty file {file}: {err}", file=filePath, err=e)
|
|
150
|
+
|
|
123
151
|
return None
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# tradedangerous/commands/import_cmd.py
|
|
2
|
+
# DEPRECATED: The legacy “.prices” import path is deprecated.
|
|
3
|
+
# Prefer supported import plugins:
|
|
4
|
+
# - trade import -P spansh (authoritative bulk dump)
|
|
5
|
+
# - trade import -P eddblink (server listings pipeline)
|
|
6
|
+
# Solo / offline players: use the TradeDangerous DB-Update plugin for EDMC:
|
|
7
|
+
# https://github.com/bgol/UpdateTD
|
|
8
|
+
#
|
|
9
|
+
# This module remains available for compatibility with old “.prices” files,
|
|
10
|
+
# but the format is being phased out and may be removed in a future release.
|
|
11
|
+
|
|
12
|
+
|
|
1
13
|
from .exceptions import CommandLineError
|
|
2
14
|
from .parsing import ParseArgument, MutuallyExclusiveGroup
|
|
3
15
|
from itertools import chain
|
|
@@ -104,7 +116,13 @@ switches = [
|
|
|
104
116
|
|
|
105
117
|
|
|
106
118
|
def run(results, cmdenv, tdb):
|
|
107
|
-
|
|
119
|
+
"""
|
|
120
|
+
Dispatch import work:
|
|
121
|
+
• If a plugin (-P) is specified: load it and run it (no deprecation banner).
|
|
122
|
+
• Otherwise: proceed with legacy .prices/.url flow and show a deprecation notice.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# --- Plugin path (preferred; no banner) ---
|
|
108
126
|
if cmdenv.plug:
|
|
109
127
|
if cmdenv.pluginOptions:
|
|
110
128
|
cmdenv.pluginOptions = chain.from_iterable(
|
|
@@ -114,65 +132,83 @@ def run(results, cmdenv, tdb):
|
|
|
114
132
|
pluginClass = plugins.load(cmdenv.plug, "ImportPlugin")
|
|
115
133
|
except plugins.PluginException as e:
|
|
116
134
|
raise CommandLineError("Plugin Error: " + str(e))
|
|
117
|
-
|
|
118
|
-
# Initialize the plugin
|
|
135
|
+
|
|
119
136
|
plugin = pluginClass(tdb, cmdenv)
|
|
120
|
-
|
|
121
|
-
#
|
|
122
|
-
# that needs doing and we can stop now.
|
|
123
|
-
# If it returns True, it is returning control to the module.
|
|
137
|
+
|
|
138
|
+
# If plugin returns False, it fully handled the run → stop here.
|
|
124
139
|
if not plugin.run():
|
|
125
140
|
return None
|
|
126
|
-
|
|
141
|
+
|
|
142
|
+
# If plugin returns True, it’s handing control back to legacy flow below.
|
|
143
|
+
# Fall through intentionally (still no banner, as user invoked a plugin).
|
|
144
|
+
|
|
145
|
+
# --- Legacy .prices path (deprecated; show banner once) ---
|
|
146
|
+
# Only warn when the user is *not* using a plugin. Keep functionality intact.
|
|
147
|
+
if not cmdenv.plug:
|
|
148
|
+
print(
|
|
149
|
+
"NOTE:\n"
|
|
150
|
+
"=== DEPRECATION NOTICE ============================================\n"
|
|
151
|
+
"The legacy '.prices' import is deprecated.\n"
|
|
152
|
+
"Use a supported plugin instead:\n"
|
|
153
|
+
" • trade import -P spansh\n"
|
|
154
|
+
" • trade import -P eddblink\n"
|
|
155
|
+
"Solo/offline: TradeDangerous DB-Update for EDMC → https://github.com/bgol/UpdateTD\n"
|
|
156
|
+
"===================================================================\n"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Refresh/close any cached handles before file ops (kept from original)
|
|
127
160
|
tdb.reloadCache()
|
|
128
161
|
tdb.close()
|
|
129
|
-
|
|
162
|
+
|
|
163
|
+
# Treat a bare http(s) string in 'filename' as a URL
|
|
130
164
|
if cmdenv.filename:
|
|
131
165
|
if re.match(r"^https?://", cmdenv.filename, re.IGNORECASE):
|
|
132
166
|
cmdenv.url, cmdenv.filename = cmdenv.filename, None
|
|
133
|
-
|
|
167
|
+
|
|
168
|
+
# Optional download step
|
|
134
169
|
if cmdenv.url:
|
|
135
170
|
cmdenv.filename = cmdenv.filename or "import.prices"
|
|
136
171
|
transfers.download(cmdenv, cmdenv.url, cmdenv.filename)
|
|
137
172
|
if cmdenv.download:
|
|
138
173
|
return None
|
|
139
|
-
|
|
140
|
-
#
|
|
141
|
-
|
|
174
|
+
|
|
175
|
+
# No filename? If Tk is available, prompt user (legacy behavior)
|
|
176
|
+
fh = None
|
|
142
177
|
if not cmdenv.filename and hasTkInter:
|
|
143
178
|
tk = tkinter.Tk()
|
|
144
179
|
tk.withdraw()
|
|
145
180
|
filetypes = (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
181
|
+
("TradeDangerous '.prices' Files", "*.prices"),
|
|
182
|
+
("All Files", "*.*"),
|
|
183
|
+
)
|
|
149
184
|
filename = tkfd.askopenfilename(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
185
|
+
title="Select the file to import",
|
|
186
|
+
initialfile="TradeDangerous.prices",
|
|
187
|
+
filetypes=filetypes,
|
|
188
|
+
initialdir='.',
|
|
189
|
+
)
|
|
155
190
|
if not filename:
|
|
156
191
|
raise SystemExit("Aborted")
|
|
157
192
|
cmdenv.filename = filename
|
|
158
|
-
|
|
159
|
-
#
|
|
193
|
+
|
|
194
|
+
# Validate path or use stdin
|
|
160
195
|
if cmdenv.filename != "-":
|
|
161
|
-
fh = None
|
|
162
196
|
filePath = Path(cmdenv.filename)
|
|
163
197
|
if not filePath.is_file():
|
|
164
|
-
raise CommandLineError("File not found: {}"
|
|
165
|
-
str(filePath)
|
|
166
|
-
))
|
|
198
|
+
raise CommandLineError(f"File not found: {str(filePath)}")
|
|
167
199
|
else:
|
|
168
200
|
filePath = "stdin"
|
|
169
201
|
fh = sys.stdin
|
|
170
|
-
|
|
202
|
+
|
|
203
|
+
# If a plugin was also involved and wants to finish with default flow,
|
|
204
|
+
# honour that (unchanged behavior).
|
|
171
205
|
if cmdenv.plug:
|
|
206
|
+
# Plugins returning True above chose to hand control back.
|
|
207
|
+
# finish() may return False to suppress default regeneration.
|
|
172
208
|
if not plugin.finish():
|
|
173
209
|
cache.regeneratePricesFile()
|
|
174
210
|
return None
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
211
|
+
|
|
212
|
+
# Legacy .prices import
|
|
213
|
+
cache.importDataFromFile(tdb, cmdenv, filePath, pricesFh=fh, reset=cmdenv.reset)
|
|
178
214
|
return None
|