tradedangerous 12.0.5__py3-none-any.whl → 12.0.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.

Potentially problematic release.


This version of tradedangerous might be problematic. Click here for more details.

Files changed (30) hide show
  1. tradedangerous/cache.py +135 -133
  2. tradedangerous/commands/buildcache_cmd.py +7 -7
  3. tradedangerous/commands/buy_cmd.py +4 -4
  4. tradedangerous/commands/export_cmd.py +11 -11
  5. tradedangerous/commands/import_cmd.py +12 -12
  6. tradedangerous/commands/market_cmd.py +17 -17
  7. tradedangerous/commands/olddata_cmd.py +18 -18
  8. tradedangerous/commands/rares_cmd.py +30 -30
  9. tradedangerous/commands/run_cmd.py +21 -21
  10. tradedangerous/commands/sell_cmd.py +5 -5
  11. tradedangerous/corrections.py +1 -1
  12. tradedangerous/csvexport.py +20 -20
  13. tradedangerous/db/adapter.py +9 -9
  14. tradedangerous/db/config.py +4 -4
  15. tradedangerous/db/engine.py +12 -12
  16. tradedangerous/db/lifecycle.py +28 -28
  17. tradedangerous/db/orm_models.py +42 -42
  18. tradedangerous/db/paths.py +3 -3
  19. tradedangerous/plugins/eddblink_plug.py +106 -251
  20. tradedangerous/plugins/spansh_plug.py +253 -253
  21. tradedangerous/prices.py +21 -21
  22. tradedangerous/tradedb.py +85 -85
  23. tradedangerous/tradeenv.py +2 -2
  24. tradedangerous/version.py +1 -1
  25. {tradedangerous-12.0.5.dist-info → tradedangerous-12.0.6.dist-info}/METADATA +1 -1
  26. {tradedangerous-12.0.5.dist-info → tradedangerous-12.0.6.dist-info}/RECORD +30 -30
  27. {tradedangerous-12.0.5.dist-info → tradedangerous-12.0.6.dist-info}/WHEEL +0 -0
  28. {tradedangerous-12.0.5.dist-info → tradedangerous-12.0.6.dist-info}/entry_points.txt +0 -0
  29. {tradedangerous-12.0.5.dist-info → tradedangerous-12.0.6.dist-info}/licenses/LICENSE +0 -0
  30. {tradedangerous-12.0.5.dist-info → tradedangerous-12.0.6.dist-info}/top_level.txt +0 -0
@@ -55,12 +55,12 @@ switches = [
55
55
  def run(results, cmdenv, tdb: TradeDB):
56
56
  """
57
57
  BRUTE-FORCE rebuild of the cache/database.
58
-
58
+
59
59
  Semantics preserved:
60
60
  - If DB exists and --force not given => error
61
61
  - SQL file must exist
62
62
  - Performs a full destructive rebuild
63
-
63
+
64
64
  Implementation change:
65
65
  - Delegates to tradedangerous.db.lifecycle.ensure_fresh_db with mode='force'
66
66
  so all backend-specific checks and rebuild steps run via the central path.
@@ -68,21 +68,21 @@ def run(results, cmdenv, tdb: TradeDB):
68
68
  # Deprecation note: keep short and visible but non-fatal.
69
69
  print("NOTE: 'buildcache' is deprecated. Prefer 'update' or importer plugins. "
70
70
  "Proceeding with a forced rebuild via db.lifecycle.ensure_fresh_db().")
71
-
71
+
72
72
  # Honor legacy safety: require --force to overwrite an existing DB file.
73
73
  if not cmdenv.force and tdb.dbPath.exists():
74
74
  raise CommandLineError(
75
75
  f"SQLite3 database '{tdb.dbFilename}' already exists.\n"
76
76
  "Either remove the file first or use the '-f/--force' option."
77
77
  )
78
-
78
+
79
79
  # Ensure the SQL source exists (buildCache ultimately relies on this path).
80
80
  if not tdb.sqlPath.exists():
81
81
  raise CommandLineError(f"SQL File does not exist: {tdb.sqlFilename}")
82
-
82
+
83
83
  # Force a rebuild through the lifecycle helper (works for both backends).
84
84
  from tradedangerous.db.lifecycle import ensure_fresh_db
85
-
85
+
86
86
  ensure_fresh_db(
87
87
  backend=tdb.engine.dialect.name if getattr(tdb, "engine", None) else "sqlite",
88
88
  engine=getattr(tdb, "engine", None),
@@ -93,6 +93,6 @@ def run(results, cmdenv, tdb: TradeDB):
93
93
  tdenv=cmdenv,
94
94
  rebuild=True,
95
95
  )
96
-
96
+
97
97
  return None
98
98
 
@@ -186,7 +186,7 @@ def sql_query(cmdenv, tdb, queries, mode):
186
186
  * Item: (item_id, station_id, supply_price, supply_units)
187
187
  """
188
188
  ids = list(queries.keys())
189
-
189
+
190
190
  # Build a stable, named-parameter IN(...) list
191
191
  params = {}
192
192
  placeholders = []
@@ -195,7 +195,7 @@ def sql_query(cmdenv, tdb, queries, mode):
195
195
  placeholders.append(f":{key}")
196
196
  params[key] = val
197
197
  id_list_sql = ",".join(placeholders)
198
-
198
+
199
199
  if mode is SHIP_MODE:
200
200
  columns = "s.ship_id, s.station_id, sh.cost, 1"
201
201
  tables = "ShipVendor AS s JOIN Ship AS sh ON sh.ship_id = s.ship_id"
@@ -216,11 +216,11 @@ def sql_query(cmdenv, tdb, queries, mode):
216
216
  if cmdenv.gt:
217
217
  constraints.append("(s.supply_price > :gt)")
218
218
  params["gt"] = cmdenv.gt
219
-
219
+
220
220
  where_clause = " AND ".join(constraints)
221
221
  stmt = f"SELECT DISTINCT {columns} FROM {tables} WHERE {where_clause}"
222
222
  cmdenv.DEBUG0('SQL: {} ; params={}', stmt, params)
223
-
223
+
224
224
  # Eagerly fetch to avoid closed cursor when iterating later.
225
225
  with tdb.engine.connect() as conn:
226
226
  result = conn.execute(text(stmt), params)
@@ -63,7 +63,7 @@ switches = [
63
63
  def run(results, cmdenv, tdb):
64
64
  """
65
65
  Backend-neutral export of DB tables to CSV.
66
-
66
+
67
67
  Changes:
68
68
  * Use tradedangerous.db.lifecycle.ensure_fresh_db(rebuild=False) to verify a usable DB
69
69
  without rebuilding (works for SQLite and MariaDB).
@@ -86,13 +86,13 @@ def run(results, cmdenv, tdb):
86
86
  f"Database is not initialized/healthy (reason: {reason}). "
87
87
  "Use 'buildcache' or an importer to (re)build it."
88
88
  )
89
-
89
+
90
90
  # --- Determine export target directory (same behavior as before) ---
91
91
  from pathlib import Path
92
92
  exportPath = Path(cmdenv.path) if cmdenv.path else Path(tdb.dataDir)
93
93
  if not exportPath.is_dir():
94
94
  raise CommandLineError("Save location '{}' not found.".format(str(exportPath)))
95
-
95
+
96
96
  # --- Announce which DB we will read from, backend-aware ---
97
97
  try:
98
98
  dialect = tdb.engine.dialect.name
@@ -104,17 +104,17 @@ def run(results, cmdenv, tdb):
104
104
  except Exception:
105
105
  source_label = str(getattr(tdb, "dbPath", "Unknown DB"))
106
106
  cmdenv.NOTE("Using database {}", source_label)
107
-
107
+
108
108
  # --- Enumerate tables using SQLAlchemy inspector (backend-neutral) ---
109
109
  from sqlalchemy import inspect
110
110
  inspector = inspect(tdb.engine)
111
111
  all_tables = inspector.get_table_names() # current schema / database
112
-
112
+
113
113
  # Optional ignore list (preserve legacy default: skip StationItem unless --all-tables)
114
114
  ignoreList = []
115
115
  if not getattr(cmdenv, "allTables", False):
116
116
  ignoreList.append("StationItem")
117
-
117
+
118
118
  # --tables filtering (case-insensitive, like old COLLATE NOCASE)
119
119
  if getattr(cmdenv, "tables", None):
120
120
  requested = [t.strip() for t in cmdenv.tables.split(",") if t.strip()]
@@ -129,17 +129,17 @@ def run(results, cmdenv, tdb):
129
129
  table_list = sorted(set(resolved))
130
130
  else:
131
131
  table_list = sorted(set(all_tables))
132
-
132
+
133
133
  # --- Export each table via csvexport (already refactored elsewhere) ---
134
134
  for tableName in table_list:
135
135
  if tableName in ignoreList:
136
136
  cmdenv.NOTE("Ignore Table '{table}'", table=tableName)
137
137
  continue
138
-
138
+
139
139
  cmdenv.NOTE("Export Table '{table}'", table=tableName)
140
-
140
+
141
141
  lineCount, filePath = exportTableToFile(tdb, cmdenv, tableName, exportPath)
142
-
142
+
143
143
  # Optionally delete empty CSVs
144
144
  if getattr(cmdenv, "deleteEmpty", False) and lineCount == 0:
145
145
  try:
@@ -147,5 +147,5 @@ def run(results, cmdenv, tdb):
147
147
  cmdenv.DEBUG0("Delete empty file {file}", file=filePath)
148
148
  except Exception as e:
149
149
  cmdenv.DEBUG0("Failed to delete empty file {file}: {err}", file=filePath, err=e)
150
-
150
+
151
151
  return None
@@ -121,7 +121,7 @@ def run(results, cmdenv, tdb):
121
121
  • If a plugin (-P) is specified: load it and run it (no deprecation banner).
122
122
  • Otherwise: proceed with legacy .prices/.url flow and show a deprecation notice.
123
123
  """
124
-
124
+
125
125
  # --- Plugin path (preferred; no banner) ---
126
126
  if cmdenv.plug:
127
127
  if cmdenv.pluginOptions:
@@ -132,16 +132,16 @@ def run(results, cmdenv, tdb):
132
132
  pluginClass = plugins.load(cmdenv.plug, "ImportPlugin")
133
133
  except plugins.PluginException as e:
134
134
  raise CommandLineError("Plugin Error: " + str(e))
135
-
135
+
136
136
  plugin = pluginClass(tdb, cmdenv)
137
-
137
+
138
138
  # If plugin returns False, it fully handled the run → stop here.
139
139
  if not plugin.run():
140
140
  return None
141
-
141
+
142
142
  # If plugin returns True, it’s handing control back to legacy flow below.
143
143
  # Fall through intentionally (still no banner, as user invoked a plugin).
144
-
144
+
145
145
  # --- Legacy .prices path (deprecated; show banner once) ---
146
146
  # Only warn when the user is *not* using a plugin. Keep functionality intact.
147
147
  if not cmdenv.plug:
@@ -155,23 +155,23 @@ def run(results, cmdenv, tdb):
155
155
  "Solo/offline: TradeDangerous DB-Update for EDMC → https://github.com/bgol/UpdateTD\n"
156
156
  "===================================================================\n"
157
157
  )
158
-
158
+
159
159
  # Refresh/close any cached handles before file ops (kept from original)
160
160
  tdb.reloadCache()
161
161
  tdb.close()
162
-
162
+
163
163
  # Treat a bare http(s) string in 'filename' as a URL
164
164
  if cmdenv.filename:
165
165
  if re.match(r"^https?://", cmdenv.filename, re.IGNORECASE):
166
166
  cmdenv.url, cmdenv.filename = cmdenv.filename, None
167
-
167
+
168
168
  # Optional download step
169
169
  if cmdenv.url:
170
170
  cmdenv.filename = cmdenv.filename or "import.prices"
171
171
  transfers.download(cmdenv, cmdenv.url, cmdenv.filename)
172
172
  if cmdenv.download:
173
173
  return None
174
-
174
+
175
175
  # No filename? If Tk is available, prompt user (legacy behavior)
176
176
  fh = None
177
177
  if not cmdenv.filename and hasTkInter:
@@ -190,7 +190,7 @@ def run(results, cmdenv, tdb):
190
190
  if not filename:
191
191
  raise SystemExit("Aborted")
192
192
  cmdenv.filename = filename
193
-
193
+
194
194
  # Validate path or use stdin
195
195
  if cmdenv.filename != "-":
196
196
  filePath = Path(cmdenv.filename)
@@ -199,7 +199,7 @@ def run(results, cmdenv, tdb):
199
199
  else:
200
200
  filePath = "stdin"
201
201
  fh = sys.stdin
202
-
202
+
203
203
  # If a plugin was also involved and wants to finish with default flow,
204
204
  # honour that (unchanged behavior).
205
205
  if cmdenv.plug:
@@ -208,7 +208,7 @@ def run(results, cmdenv, tdb):
208
208
  if not plugin.finish():
209
209
  cache.regeneratePricesFile()
210
210
  return None
211
-
211
+
212
212
  # Legacy .prices import
213
213
  cache.importDataFromFile(tdb, cmdenv, filePath, pricesFh=fh, reset=cmdenv.reset)
214
214
  return None
@@ -54,24 +54,24 @@ def render_units(units, level):
54
54
  def run(results, cmdenv, tdb):
55
55
  # Lazy import to avoid any import-time tangles elsewhere.
56
56
  from tradedangerous.db.utils import age_in_days
57
-
57
+
58
58
  origin = cmdenv.startStation
59
59
  if not origin.itemCount:
60
60
  raise CommandLineError(
61
61
  "No trade data available for {}".format(origin.name())
62
62
  )
63
-
63
+
64
64
  buying, selling = cmdenv.buying, cmdenv.selling
65
-
65
+
66
66
  results.summary = ResultRow()
67
67
  results.summary.origin = origin
68
68
  results.summary.buying = cmdenv.buying
69
69
  results.summary.selling = cmdenv.selling
70
-
70
+
71
71
  # Precompute averages (unchanged)
72
72
  tdb.getAverageSelling()
73
73
  tdb.getAverageBuying()
74
-
74
+
75
75
  # --- Backend-neutral query using SQLAlchemy Core + age_in_days ---
76
76
  si = table(
77
77
  "StationItem",
@@ -85,10 +85,10 @@ def run(results, cmdenv, tdb):
85
85
  column("supply_level"),
86
86
  column("modified"),
87
87
  )
88
-
88
+
89
89
  # Build session bound to current engine (needed by age_in_days)
90
90
  session = Session(bind=tdb.engine)
91
-
91
+
92
92
  stmt = (
93
93
  select(
94
94
  si.c.item_id,
@@ -98,17 +98,17 @@ def run(results, cmdenv, tdb):
98
98
  )
99
99
  .where(si.c.station_id == origin.ID)
100
100
  )
101
-
101
+
102
102
  rows = session.execute(stmt).fetchall()
103
103
  session.close()
104
-
104
+
105
105
  for r in rows:
106
106
  it = iter(r)
107
107
  item = tdb.itemByID[next(it)]
108
-
108
+
109
109
  row = ResultRow()
110
110
  row.item = item
111
-
111
+
112
112
  row.buyCr = int(next(it) or 0)
113
113
  row.avgBuy = tdb.avgBuying.get(item.ID, 0)
114
114
  units, level = int(next(it) or 0), int(next(it) or 0)
@@ -119,7 +119,7 @@ def run(results, cmdenv, tdb):
119
119
  hasBuy = (row.buyCr or units or level)
120
120
  else:
121
121
  hasBuy = False
122
-
122
+
123
123
  row.sellCr = int(next(it) or 0)
124
124
  row.avgSell = tdb.avgSelling.get(item.ID, 0)
125
125
  units, level = int(next(it) or 0), int(next(it) or 0)
@@ -130,19 +130,19 @@ def run(results, cmdenv, tdb):
130
130
  hasSell = (row.sellCr or units or level)
131
131
  else:
132
132
  hasSell = False
133
-
133
+
134
134
  age_days = next(it)
135
135
  row.age = float(age_days or 0.0)
136
-
136
+
137
137
  if hasBuy or hasSell:
138
138
  results.rows.append(row)
139
-
139
+
140
140
  if not results.rows:
141
141
  raise CommandLineError("No items found")
142
-
142
+
143
143
  results.rows.sort(key=lambda row: row.item.dbname)
144
144
  results.rows.sort(key=lambda row: row.item.category.dbname)
145
-
145
+
146
146
  return results
147
147
 
148
148
  #######################################################################
@@ -73,13 +73,13 @@ def run(results, cmdenv, tdb):
73
73
  """
74
74
  from .commandenv import ResultRow
75
75
  from tradedangerous.db.utils import age_in_days
76
-
76
+
77
77
  cmdenv = results.cmdenv
78
78
  tdb = cmdenv.tdb
79
-
79
+
80
80
  results.summary = ResultRow()
81
81
  results.limit = cmdenv.limit
82
-
82
+
83
83
  # SQLAlchemy lightweight table defs
84
84
  si = table(
85
85
  "StationItem",
@@ -100,13 +100,13 @@ def run(results, cmdenv, tdb):
100
100
  column("pos_y"),
101
101
  column("pos_z"),
102
102
  )
103
-
103
+
104
104
  # Build session bound to current engine (age_in_days needs the session)
105
105
  session = Session(bind=tdb.engine)
106
-
106
+
107
107
  # Base SELECT: station_id, ls_from_star, age_days
108
108
  age_expr = age_in_days(session, func.max(si.c.modified)).label("age_days")
109
-
109
+
110
110
  # Optional near-system distance²
111
111
  nearSys = cmdenv.nearSystem
112
112
  join_sys = False
@@ -117,7 +117,7 @@ def run(results, cmdenv, tdb):
117
117
  dist2_expr = (dx * dx + dy * dy + dz * dz).label("d2")
118
118
  else:
119
119
  dist2_expr = literal(0.0).label("d2")
120
-
120
+
121
121
  stmt = (
122
122
  select(
123
123
  si.c.station_id,
@@ -133,7 +133,7 @@ def run(results, cmdenv, tdb):
133
133
  .group_by(si.c.station_id, stn.c.ls_from_star, dist2_expr)
134
134
  .order_by(age_expr.desc())
135
135
  )
136
-
136
+
137
137
  # Bounding box for near (keeps scan small, mirrors original)
138
138
  if nearSys:
139
139
  maxLy = cmdenv.maxLyPer or tdb.maxSystemLinkLy
@@ -145,15 +145,15 @@ def run(results, cmdenv, tdb):
145
145
  )
146
146
  # Radius filter: HAVING dist2 <= maxLy^2
147
147
  stmt = stmt.having(dist2_expr <= (maxLy * maxLy))
148
-
148
+
149
149
  # Min-age filter (apply to aggregated age of MAX(modified))
150
150
  if cmdenv.minAge:
151
151
  stmt = stmt.having(age_expr >= float(cmdenv.minAge))
152
-
152
+
153
153
  # Execute and materialize rows
154
154
  rows = session.execute(stmt).fetchall()
155
155
  session.close()
156
-
156
+
157
157
  # Downstream filters (unchanged)
158
158
  padSize = cmdenv.padSize
159
159
  planetary = cmdenv.planetary
@@ -161,7 +161,7 @@ def run(results, cmdenv, tdb):
161
161
  odyssey = cmdenv.odyssey
162
162
  noPlanet = cmdenv.noPlanet
163
163
  mls = cmdenv.maxLs
164
-
164
+
165
165
  for (stnID, age, ls, dist2) in rows:
166
166
  cmdenv.DEBUG2("{}:{}:{}", stnID, age, ls)
167
167
  row = ResultRow()
@@ -169,7 +169,7 @@ def run(results, cmdenv, tdb):
169
169
  row.age = float(age or 0.0)
170
170
  row.ls = "{:n}".format(ls) if ls else "?"
171
171
  row.dist = (float(dist2) ** 0.5) if dist2 else 0.0
172
-
172
+
173
173
  if padSize and not row.station.checkPadSize(padSize):
174
174
  continue
175
175
  if planetary and not row.station.checkPlanetary(planetary):
@@ -182,9 +182,9 @@ def run(results, cmdenv, tdb):
182
182
  continue
183
183
  if mls and row.station.lsFromStar > mls:
184
184
  continue
185
-
185
+
186
186
  results.rows.append(row)
187
-
187
+
188
188
  # Route optimization and limiting (unchanged)
189
189
  if cmdenv.route and len(results.rows) > 1:
190
190
  def walk(start_idx, dist):
@@ -201,7 +201,7 @@ def run(results, cmdenv, tdb):
201
201
  path.append(nearest)
202
202
  dist += distFn(nearest.station.system)
203
203
  return (path, dist)
204
-
204
+
205
205
  if cmdenv.near:
206
206
  bestPath = walk(0, results.rows[0].dist)
207
207
  else:
@@ -211,10 +211,10 @@ def run(results, cmdenv, tdb):
211
211
  if candidate[1] < bestPath[1]:
212
212
  bestPath = candidate
213
213
  results.rows[:] = bestPath[0]
214
-
214
+
215
215
  if cmdenv.limit:
216
216
  results.rows[:] = results.rows[:cmdenv.limit]
217
-
217
+
218
218
  return results
219
219
 
220
220
 
@@ -103,14 +103,14 @@ def run(results, cmdenv, tdb):
103
103
  odyssey = cmdenv.odyssey
104
104
  # How far we're want to cast our net.
105
105
  maxLy = float(cmdenv.maxLyPer or 0.0)
106
-
106
+
107
107
  if cmdenv.illegal:
108
108
  wantIllegality = 'Y'
109
109
  elif cmdenv.legal:
110
110
  wantIllegality = 'N'
111
111
  else:
112
112
  wantIllegality = 'YN?'
113
-
113
+
114
114
  awaySystems = set()
115
115
  if cmdenv.away or cmdenv.awayFrom:
116
116
  if not cmdenv.away or not cmdenv.awayFrom:
@@ -119,15 +119,15 @@ def run(results, cmdenv, tdb):
119
119
  for sysName in cmdenv.awayFrom:
120
120
  system = tdb.lookupPlace(sysName).system
121
121
  awaySystems.add(system)
122
-
122
+
123
123
  # Start to build up the results data.
124
124
  results.summary = ResultRow()
125
125
  results.summary.near = start
126
126
  results.summary.ly = maxLy
127
127
  results.summary.awaySystems = awaySystems
128
-
128
+
129
129
  distCheckFn = start.distanceTo
130
-
130
+
131
131
  # Look through the rares list.
132
132
  for rare in tdb.rareItemByID.values():
133
133
  if rare.illegal not in wantIllegality:
@@ -143,45 +143,45 @@ def run(results, cmdenv, tdb):
143
143
  continue
144
144
  if noPlanet and stn.planetary != 'N':
145
145
  continue
146
-
146
+
147
147
  rareSys = stn.system
148
148
  dist = distCheckFn(rareSys)
149
149
  if maxLy > 0.0 and dist > maxLy:
150
150
  continue
151
-
151
+
152
152
  if awaySystems:
153
153
  awayCheck = rareSys.distanceTo
154
154
  if any(awayCheck(away) < minAwayDist for away in awaySystems):
155
155
  continue
156
-
156
+
157
157
  row = ResultRow()
158
158
  row.rare = rare
159
159
  row.station = stn # <-- IMPORTANT: used by render()
160
160
  row.dist = dist
161
161
  results.rows.append(row)
162
-
162
+
163
163
  # Was anything matched?
164
164
  if not results.rows:
165
165
  print("No matches found.")
166
166
  return None
167
-
167
+
168
168
  # Sort safely even if rare.costCr is None (treat None as 0)
169
169
  price_key = lambda row: (row.rare.costCr or 0)
170
-
170
+
171
171
  if cmdenv.sortByPrice:
172
172
  results.rows.sort(key=lambda row: row.dist)
173
173
  results.rows.sort(key=price_key, reverse=True)
174
174
  else:
175
175
  results.rows.sort(key=price_key, reverse=True)
176
176
  results.rows.sort(key=lambda row: row.dist)
177
-
177
+
178
178
  if cmdenv.reverse:
179
179
  results.rows.reverse()
180
-
180
+
181
181
  limit = cmdenv.limit or 0
182
182
  if limit > 0:
183
183
  results.rows = results.rows[:limit]
184
-
184
+
185
185
  return results
186
186
 
187
187
 
@@ -196,11 +196,11 @@ def render(results, cmdenv, tdb):
196
196
  Keeps existing column order/labels.
197
197
  """
198
198
  from ..formatting import RowFormat, max_len
199
-
199
+
200
200
  rows = results.rows
201
201
  if not rows:
202
202
  return
203
-
203
+
204
204
  # Helpers to coalesce possibly-missing attributes
205
205
  def _cost(row):
206
206
  try:
@@ -208,59 +208,59 @@ def render(results, cmdenv, tdb):
208
208
  return int(v) if v is not None else 0
209
209
  except Exception:
210
210
  return 0
211
-
211
+
212
212
  def _rare_name(row):
213
213
  try:
214
214
  n = row.rare.name()
215
215
  return n or "?"
216
216
  except Exception:
217
217
  return "?"
218
-
218
+
219
219
  def _alloc(row):
220
220
  val = getattr(row.rare, "allocation", None)
221
221
  return str(val) if val not in (None, "") else "?"
222
-
222
+
223
223
  def _rare_illegal(row):
224
224
  val = getattr(row.rare, "illegal", None)
225
225
  return val if val in ("Y", "N", "?") else "?"
226
-
226
+
227
227
  def _stn_ls(row):
228
228
  try:
229
229
  v = row.station.distFromStar()
230
230
  return v if v is not None else "?"
231
231
  except Exception:
232
232
  return "?"
233
-
233
+
234
234
  def _dist(row):
235
235
  try:
236
236
  return float(getattr(row, "dist", 0.0) or 0.0)
237
237
  except Exception:
238
238
  return 0.0
239
-
239
+
240
240
  def _stn_bm(row):
241
241
  key = getattr(row.station, "blackMarket", "?")
242
242
  return TradeDB.marketStates.get(key, key or "?")
243
-
243
+
244
244
  def _pad(row):
245
245
  key = getattr(row.station, "maxPadSize", "?")
246
246
  return TradeDB.padSizes.get(key, key or "?")
247
-
247
+
248
248
  def _plt(row):
249
249
  key = getattr(row.station, "planetary", "?")
250
250
  return TradeDB.planetStates.get(key, key or "?")
251
-
251
+
252
252
  def _flc(row):
253
253
  key = getattr(row.station, "fleet", "?")
254
254
  return TradeDB.fleetStates.get(key, key or "?")
255
-
255
+
256
256
  def _ody(row):
257
257
  key = getattr(row.station, "odyssey", "?")
258
258
  return TradeDB.odysseyStates.get(key, key or "?")
259
-
259
+
260
260
  # Column widths based on safe key functions
261
261
  max_stn = max_len(rows, key=lambda r: r.station.name())
262
262
  max_rare = max_len(rows, key=lambda r: _rare_name(r))
263
-
263
+
264
264
  rowFmt = RowFormat()
265
265
  rowFmt.addColumn('Station', '<', max_stn, key=lambda r: r.station.name())
266
266
  rowFmt.addColumn('Rare', '<', max_rare, key=lambda r: _rare_name(r))
@@ -276,10 +276,10 @@ def render(results, cmdenv, tdb):
276
276
  rowFmt.addColumn('Plt', '>', 3, key=lambda r: _plt(r))
277
277
  rowFmt.addColumn('Flc', '>', 3, key=lambda r: _flc(r))
278
278
  rowFmt.addColumn('Ody', '>', 3, key=lambda r: _ody(r))
279
-
279
+
280
280
  if not cmdenv.quiet:
281
281
  heading, underline = rowFmt.heading()
282
282
  print(heading, underline, sep='\n')
283
-
283
+
284
284
  for row in rows:
285
285
  print(rowFmt.format(row))