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.

Files changed (47) hide show
  1. tradedangerous/cache.py +567 -395
  2. tradedangerous/cli.py +2 -2
  3. tradedangerous/commands/TEMPLATE.py +25 -26
  4. tradedangerous/commands/__init__.py +8 -16
  5. tradedangerous/commands/buildcache_cmd.py +40 -10
  6. tradedangerous/commands/buy_cmd.py +57 -46
  7. tradedangerous/commands/commandenv.py +0 -2
  8. tradedangerous/commands/export_cmd.py +78 -50
  9. tradedangerous/commands/import_cmd.py +67 -31
  10. tradedangerous/commands/market_cmd.py +52 -19
  11. tradedangerous/commands/olddata_cmd.py +120 -107
  12. tradedangerous/commands/rares_cmd.py +122 -110
  13. tradedangerous/commands/run_cmd.py +118 -66
  14. tradedangerous/commands/sell_cmd.py +52 -45
  15. tradedangerous/commands/shipvendor_cmd.py +49 -234
  16. tradedangerous/commands/station_cmd.py +55 -485
  17. tradedangerous/commands/update_cmd.py +56 -420
  18. tradedangerous/csvexport.py +173 -162
  19. tradedangerous/db/__init__.py +27 -0
  20. tradedangerous/db/adapter.py +191 -0
  21. tradedangerous/db/config.py +95 -0
  22. tradedangerous/db/engine.py +246 -0
  23. tradedangerous/db/lifecycle.py +332 -0
  24. tradedangerous/db/locks.py +208 -0
  25. tradedangerous/db/orm_models.py +455 -0
  26. tradedangerous/db/paths.py +112 -0
  27. tradedangerous/db/utils.py +661 -0
  28. tradedangerous/gui.py +2 -2
  29. tradedangerous/plugins/eddblink_plug.py +387 -251
  30. tradedangerous/plugins/spansh_plug.py +2488 -821
  31. tradedangerous/prices.py +124 -142
  32. tradedangerous/templates/TradeDangerous.sql +6 -6
  33. tradedangerous/tradecalc.py +1227 -1109
  34. tradedangerous/tradedb.py +533 -384
  35. tradedangerous/tradeenv.py +12 -1
  36. tradedangerous/version.py +1 -1
  37. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/METADATA +11 -7
  38. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/RECORD +42 -38
  39. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/WHEEL +1 -1
  40. tradedangerous/commands/update_gui.py +0 -721
  41. tradedangerous/jsonprices.py +0 -254
  42. tradedangerous/plugins/edapi_plug.py +0 -1071
  43. tradedangerous/plugins/journal_plug.py +0 -537
  44. tradedangerous/plugins/netlog_plug.py +0 -316
  45. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/entry_points.txt +0 -0
  46. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info/licenses}/LICENSE +0 -0
  47. {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 < 0x03070000:
52
+ if sys.hexversion < 0x30813F0:
53
53
  raise SystemExit(
54
- "Sorry: TradeDangerous requires Python 3.7 or higher.\n"
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 commands.commandenv import ResultRow
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
- # Helpers
10
+ # Whether this command needs a TradeDB instance
11
+ wantsTradeDB = True
12
+ usesTradeData = False
27
13
 
28
- ######################################################################
29
- # Perform query and populate result set
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
- ### TODO: Implement
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
- parsed = CommandEnv(properties, argv, cmdModule)
250
- parsed.DEBUG0("Command line was: {}", argv)
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
- # Check that the file doesn't already exist.
57
- if not cmdenv.force:
58
- if tdb.dbPath.exists():
59
- raise CommandLineError(
60
- f"SQLite3 database '{tdb.dbFilename}' already exists.\n"
61
- "Either remove the file first or use the '-f' option.")
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
- buildCache(tdb, cmdenv)
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
- # Constraints
180
- idList = ','.join(str(ID) for ID in queries.keys())
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
- tables = "ShipVendor AS s INNER JOIN Ship AS sh USING (ship_id)"
183
- constraints = ["(ship_id IN ({}))".format(idList)]
184
- columns = [
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 ({}))".format(idList),
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
- bindValues.append(cmdenv.supply)
211
+ constraints.append("(s.supply_units >= :supply)")
212
+ params["supply"] = cmdenv.supply
210
213
  if cmdenv.lt:
211
- constraints.append("(supply_price < ?)")
212
- bindValues.append(cmdenv.lt)
214
+ constraints.append("(s.supply_price < :lt)")
215
+ params["lt"] = cmdenv.lt
213
216
  if cmdenv.gt:
214
- constraints.append("(supply_price > ?)")
215
- bindValues.append(cmdenv.gt)
216
-
217
- whereClause = ' AND '.join(constraints)
218
- stmt = """SELECT DISTINCT {columns} FROM {tables} WHERE {where}""".format(
219
- columns = ','.join(columns),
220
- tables = tables,
221
- where = whereClause
222
- )
223
- cmdenv.DEBUG0('SQL: {}', stmt)
224
- return tdb.query(stmt, bindValues)
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
- avgPrice = tdb.query("""
260
- SELECT AVG(si.supply_price)
261
- FROM StationItem AS si
262
- WHERE si.item_id = ? AND si.supply_price > 0
263
- """, [first.ID]).fetchone()[0]
264
- if not avgPrice:
265
- avgPrice = 0
266
- results.summary.avg = int(avgPrice)
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
 
@@ -1,5 +1,3 @@
1
- import sqlite3
2
-
3
1
  from .exceptions import (
4
2
  CommandLineError, FleetCarrierError, OdysseyError,
5
3
  PadSizeError, PlanetaryError,
@@ -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
- # check database exists
67
- if not tdb.dbPath.is_file():
68
- raise CommandLineError("Database '{}' not found.".format(tdb.dbPath))
69
-
70
- # check export path exists
71
- if cmdenv.path:
72
- # the "--path" overwrites the default path of TD
73
- exportPath = Path(cmdenv.path)
74
- else:
75
- exportPath = Path(cmdenv.dataDir)
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
- # connect to the database
80
- cmdenv.NOTE("Using database '{}'", tdb.dbPath)
81
- conn = tdb.getDB()
82
- conn.row_factory = sqlite3.Row
83
-
84
- # some tables might be ignored
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
- # extract tables from command line
88
- if cmdenv.tables:
89
- bindValues = cmdenv.tables.split(',')
90
- tableStmt = " AND name COLLATE NOCASE IN ({})".format(",".join("?" * len(bindValues)))
91
- cmdenv.DEBUG0(tableStmt)
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
- bindValues = []
94
- tableStmt = ''
95
- if not cmdenv.allTables:
96
- ignoreList.append("StationItem")
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
- if cmdenv.deleteEmpty and lineCount == 0:
119
- # delete file if emtpy
120
- filePath.unlink()
121
- cmdenv.DEBUG0("Delete empty file {file}'".format(file=filePath))
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
- # If we're using a plugin, initialize that first.
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
- # Run the plugin. If it returns False, then it did everything
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
- # If the filename specified was "-" or None, then go ahead
141
- # and present the user with an open file dialog.
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
- ("TradeDangerous '.prices' Files", "*.prices"),
147
- ("All Files", "*.*"),
148
- )
181
+ ("TradeDangerous '.prices' Files", "*.prices"),
182
+ ("All Files", "*.*"),
183
+ )
149
184
  filename = tkfd.askopenfilename(
150
- title = "Select the file to import",
151
- initialfile = "TradeDangerous.prices",
152
- filetypes = filetypes,
153
- initialdir = '.',
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
- # check the file exists.
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: {}".format(
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
- cache.importDataFromFile(tdb, cmdenv, filePath, pricesFh = fh, reset = cmdenv.reset)
177
-
211
+
212
+ # Legacy .prices import
213
+ cache.importDataFromFile(tdb, cmdenv, filePath, pricesFh=fh, reset=cmdenv.reset)
178
214
  return None