tradedangerous 10.17.0__py3-none-any.whl → 11.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 (74) hide show
  1. tradedangerous/__init__.py +4 -4
  2. tradedangerous/cache.py +178 -142
  3. tradedangerous/cli.py +2 -7
  4. tradedangerous/commands/TEMPLATE.py +1 -2
  5. tradedangerous/commands/__init__.py +2 -4
  6. tradedangerous/commands/buildcache_cmd.py +6 -11
  7. tradedangerous/commands/buy_cmd.py +11 -12
  8. tradedangerous/commands/commandenv.py +16 -15
  9. tradedangerous/commands/exceptions.py +6 -4
  10. tradedangerous/commands/export_cmd.py +2 -4
  11. tradedangerous/commands/import_cmd.py +3 -5
  12. tradedangerous/commands/local_cmd.py +16 -25
  13. tradedangerous/commands/market_cmd.py +9 -8
  14. tradedangerous/commands/nav_cmd.py +17 -25
  15. tradedangerous/commands/olddata_cmd.py +9 -15
  16. tradedangerous/commands/parsing.py +9 -6
  17. tradedangerous/commands/rares_cmd.py +9 -10
  18. tradedangerous/commands/run_cmd.py +25 -26
  19. tradedangerous/commands/sell_cmd.py +9 -9
  20. tradedangerous/commands/shipvendor_cmd.py +4 -7
  21. tradedangerous/commands/station_cmd.py +8 -14
  22. tradedangerous/commands/trade_cmd.py +5 -10
  23. tradedangerous/commands/update_cmd.py +10 -7
  24. tradedangerous/commands/update_gui.py +1 -3
  25. tradedangerous/corrections.py +1 -3
  26. tradedangerous/csvexport.py +8 -8
  27. tradedangerous/edscupdate.py +4 -6
  28. tradedangerous/edsmupdate.py +4 -4
  29. tradedangerous/formatting.py +53 -40
  30. tradedangerous/fs.py +6 -6
  31. tradedangerous/gui.py +53 -62
  32. tradedangerous/jsonprices.py +8 -16
  33. tradedangerous/mapping.py +4 -3
  34. tradedangerous/mfd/__init__.py +2 -4
  35. tradedangerous/mfd/saitek/__init__.py +0 -1
  36. tradedangerous/mfd/saitek/directoutput.py +8 -11
  37. tradedangerous/mfd/saitek/x52pro.py +5 -7
  38. tradedangerous/misc/checkpricebounds.py +2 -3
  39. tradedangerous/misc/clipboard.py +2 -3
  40. tradedangerous/misc/coord64.py +2 -1
  41. tradedangerous/misc/derp-sentinel.py +1 -1
  42. tradedangerous/misc/diff-system-csvs.py +3 -0
  43. tradedangerous/misc/eddb.py +1 -3
  44. tradedangerous/misc/eddn.py +2 -2
  45. tradedangerous/misc/edsc.py +7 -14
  46. tradedangerous/misc/edsm.py +1 -8
  47. tradedangerous/misc/importeddbstats.py +2 -1
  48. tradedangerous/misc/prices-json-exp.py +7 -5
  49. tradedangerous/misc/progress.py +2 -2
  50. tradedangerous/plugins/__init__.py +2 -2
  51. tradedangerous/plugins/edapi_plug.py +13 -19
  52. tradedangerous/plugins/edcd_plug.py +4 -5
  53. tradedangerous/plugins/eddblink_plug.py +11 -15
  54. tradedangerous/plugins/edmc_batch_plug.py +3 -5
  55. tradedangerous/plugins/journal_plug.py +2 -1
  56. tradedangerous/plugins/netlog_plug.py +5 -5
  57. tradedangerous/plugins/spansh_plug.py +394 -170
  58. tradedangerous/prices.py +19 -20
  59. tradedangerous/submit-distances.py +3 -8
  60. tradedangerous/templates/TradeDangerous.sql +305 -306
  61. tradedangerous/trade.py +12 -5
  62. tradedangerous/tradecalc.py +30 -34
  63. tradedangerous/tradedb.py +140 -206
  64. tradedangerous/tradeenv.py +143 -69
  65. tradedangerous/tradegui.py +4 -2
  66. tradedangerous/transfers.py +23 -20
  67. tradedangerous/version.py +1 -1
  68. {tradedangerous-10.17.0.dist-info → tradedangerous-11.0.1.dist-info}/METADATA +2 -2
  69. tradedangerous-11.0.1.dist-info/RECORD +79 -0
  70. tradedangerous-10.17.0.dist-info/RECORD +0 -79
  71. {tradedangerous-10.17.0.dist-info → tradedangerous-11.0.1.dist-info}/LICENSE +0 -0
  72. {tradedangerous-10.17.0.dist-info → tradedangerous-11.0.1.dist-info}/WHEEL +0 -0
  73. {tradedangerous-10.17.0.dist-info → tradedangerous-11.0.1.dist-info}/entry_points.txt +0 -0
  74. {tradedangerous-10.17.0.dist-info → tradedangerous-11.0.1.dist-info}/top_level.txt +0 -0
@@ -1,72 +1,205 @@
1
- import os
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from datetime import datetime, timedelta
5
+ from pathlib import Path
6
+
2
7
  import sys
3
8
  import time
4
- from datetime import datetime, timedelta
9
+ import typing
5
10
  from collections import namedtuple
6
- from pathlib import Path
11
+ if sys.version_info.major == 3 and sys.version_info.minor >= 10:
12
+ from dataclasses import dataclass
13
+ else:
14
+ dataclass = False # pylint: disable=invalid-name
7
15
 
8
- import requests
9
- import simdjson
16
+ from rich.progress import Progress
17
+ import ijson
10
18
  import sqlite3
11
19
 
12
- from .. import plugins, cache, fs, transfers, csvexport, corrections
20
+ from .. import plugins, cache, transfers, csvexport, corrections
21
+
22
+ if typing.TYPE_CHECKING:
23
+ from typing import Any, Iterable, Optional
24
+ from .. tradeenv import TradeEnv
13
25
 
14
26
  SOURCE_URL = 'https://downloads.spansh.co.uk/galaxy_stations.json'
15
27
 
16
28
  STATION_TYPE_MAP = {
17
- 'None' : [0, False],
18
- 'Outpost' : [1, False],
19
- 'Coriolis Starport' : [2, False],
20
- 'Ocellus Starport' : [3, False],
21
- 'Orbis Starport' : [4, False],
22
- 'Planetary Outpost' : [11, True],
23
- 'Planetary Port' : [12, True],
24
- 'Mega ship' : [13, False],
25
- 'Asteroid base' : [14, False],
29
+ 'None': [0, False],
30
+ 'Outpost': [1, False],
31
+ 'Coriolis Starport': [2, False],
32
+ 'Ocellus Starport': [3, False],
33
+ 'Orbis Starport': [4, False],
34
+ 'Planetary Outpost': [11, True],
35
+ 'Planetary Port': [12, True],
36
+ 'Mega ship': [13, False],
37
+ 'Asteroid base': [14, False],
26
38
  'Drake-Class Carrier': [24, False], # fleet carriers
27
39
  'Settlement': [25, True], # odyssey settlements
28
40
  }
29
41
 
30
- System = namedtuple('System', 'id,name,pos_x,pos_y,pos_z,modified')
31
- Station = namedtuple('Station', 'id,name,distance,max_pad_size,market,black_market,shipyard,outfitting,rearm,refuel,repair,planetary,type,modified')
32
- Commodity = namedtuple('Commodity', 'id,name,category,demand,supply,sell,buy,modified')
33
42
 
43
+ if dataclass:
44
+ # Dataclass with slots is considerably cheaper and faster than namedtuple
45
+ # but is only reliably introduced in 3.10+
46
+ @dataclass(slots=True)
47
+ class System:
48
+ id: int
49
+ name: str
50
+ pos_x: float
51
+ pos_y: float
52
+ pos_z: float
53
+ modified: float | None
54
+
55
+ @dataclass(slots=True)
56
+ class Station: # pylint: disable=too-many-instance-attributes
57
+ id: int
58
+ system_id: int
59
+ name: str
60
+ distance: float
61
+ max_pad_size: str
62
+ market: str # should be Optional[bool]
63
+ black_market: str # should be Optional[bool]
64
+ shipyard: str # should be Optional[bool]
65
+ outfitting: str # should be Optional[bool]
66
+ rearm: str # should be Optional[bool]
67
+ refuel: str # should be Optional[bool]
68
+ repair: str # should be Optional[bool]
69
+ planetary: str # should be Optional[bool]
70
+ type: int # station type
71
+ modified: float
72
+
73
+
74
+ @dataclass(slots=True)
75
+ class Commodity:
76
+ id: int
77
+ name: str
78
+ category: str
79
+ demand: int
80
+ supply: int
81
+ sell: int
82
+ buy: int
83
+ modified: float
84
+
85
+ else:
86
+ System = namedtuple('System', 'id,name,pos_x,pos_y,pos_z,modified')
87
+ Station = namedtuple('Station',
88
+ 'id,system_id,name,distance,max_pad_size,'
89
+ 'market,black_market,shipyard,outfitting,rearm,refuel,repair,planetary,type,modified')
90
+ Commodity = namedtuple('Commodity', 'id,name,category,demand,supply,sell,buy,modified')
34
91
 
35
- class Timing:
36
92
 
93
+ class Timing:
94
+ """ Helper that provides a context manager for timing code execution. """
95
+
37
96
  def __init__(self):
38
97
  self.start_ts = None
39
98
  self.end_ts = None
40
-
99
+
41
100
  def __enter__(self):
42
101
  self.start_ts = time.perf_counter()
43
102
  self.end_ts = None
44
103
  return self
45
-
104
+
46
105
  def __exit__(self, *args):
47
106
  self.end_ts = time.perf_counter()
48
-
107
+
49
108
  @property
50
- def elapsed(self):
109
+ def elapsed(self) -> Optional[float]:
110
+ """ If the timing has finish, calculates the elapsed time. """
51
111
  if self.start_ts is None:
52
112
  return None
53
113
  return (self.end_ts or time.perf_counter()) - self.start_ts
54
-
114
+
55
115
  @property
56
- def is_finished(self):
116
+ def is_finished(self) -> bool:
117
+ """ True if the timing has finished. """
57
118
  return self.end_ts is not None
58
119
 
59
120
 
121
+ class Progresser:
122
+ """ Encapsulates a potentially transient progress view for a given TradeEnv. """
123
+ def __init__(self, tdenv: 'TradeEnv', title: str, fancy: bool = True, total: Optional[int] = None):
124
+ self.started = time.time()
125
+ self.tdenv = tdenv
126
+ self.progress, self.main_task = None, None
127
+ self.title = title
128
+ self.fancy = fancy
129
+ self.total = total
130
+ self.main_task = None
131
+ if fancy:
132
+ self.progress = Progress(console=self.tdenv.console, transient=True, auto_refresh=True, refresh_per_second=2)
133
+ else:
134
+ self.progress = None
135
+
136
+ def __enter__(self):
137
+ if not self.fancy:
138
+ self.tdenv.uprint(self.title)
139
+ else:
140
+ self.progress.start()
141
+ self.main_task = self.progress.add_task(self.title, start=True, total=self.total)
142
+ return self
143
+
144
+ def __exit__(self, *args):
145
+ self.progress.stop()
146
+
147
+ def update(self, title: str) -> None:
148
+ if self.fancy:
149
+ self.progress.update(self.main_task, description=title)
150
+ else:
151
+ self.tdenv.DEBUG1(title)
152
+
153
+ @contextmanager
154
+ def task(self, title: str, total: Optional[int] = None, parent: Optional[str] = None):
155
+ parent = parent or self.main_task
156
+ if self.fancy:
157
+ task = self.progress.add_task(title, start=True, total=total, parent=parent)
158
+ else:
159
+ self.tdenv.DEBUG0(title)
160
+ task = None
161
+ try:
162
+ yield task
163
+ finally:
164
+ if self.fancy:
165
+ self.progress.remove_task(task)
166
+ if task is not None and parent is not None:
167
+ self.progress.update(parent, advance=1)
168
+
169
+ def bump(self, task, advance: int = 1, description: Optional[str] = None):
170
+ """ Advances the progress of a task by one mark. """
171
+ if self.fancy and task is not None:
172
+ self.progress.update(task, advance=advance, description=description)
173
+
174
+
175
+ def get_timings(started: float, system_count: int, total_station_count: int, *, min_count: int = 100) -> str:
176
+ """ describes how long it is taking to process each system and station """
177
+ elapsed = time.time() - started
178
+ timings = "sys="
179
+ if system_count >= min_count:
180
+ avg = elapsed / float(system_count) * 1000.0
181
+ timings += f"{avg:5.2f}ms"
182
+ else:
183
+ timings += "..."
184
+ timings += ", stn="
185
+ if total_station_count >= min_count:
186
+ avg = elapsed / float(total_station_count) * 1000.0
187
+ timings += f"{avg:5.2f}ms"
188
+ else:
189
+ timings += "..."
190
+ return elapsed, timings
191
+
192
+
60
193
  class ImportPlugin(plugins.ImportPluginBase):
61
194
  """Plugin that downloads data from https://spansh.co.uk/dumps.
62
195
  """
63
-
196
+
64
197
  pluginOptions = {
65
198
  'url': f'URL to download galaxy data from (defaults to {SOURCE_URL})',
66
199
  'file': 'Local filename to import galaxy data from; use "-" to load from stdin',
67
200
  'maxage': 'Skip all entries older than specified age in days, ex.: maxage=1.5',
68
201
  }
69
-
202
+
70
203
  def __init__(self, *args, **kwargs):
71
204
  super().__init__(*args, **kwargs)
72
205
  self.url = self.getOption('url')
@@ -74,9 +207,9 @@ class ImportPlugin(plugins.ImportPluginBase):
74
207
  self.maxage = float(self.getOption('maxage')) if self.getOption('maxage') else None
75
208
  assert not (self.url and self.file), 'Provide either url or file, not both'
76
209
  if self.file and (self.file != '-'):
77
- self.file = (Path(self.tdenv.cwDir) / self.file).resolve()
78
- if not (self.tdb.dataPath / Path("TradeDangerous.prices")).exists():
79
- ri_path = self.tdb.dataPath / Path("RareItem.csv")
210
+ self.file = (Path(self.tdenv.cwDir, self.file)).resolve()
211
+ if not Path(self.tdb.dataPath, "TradeDangerous.prices").exists():
212
+ ri_path = Path(self.tdb.dataPath, "RareItem.csv")
80
213
  rib_path = ri_path.with_suffix(".tmp")
81
214
  if ri_path.exists():
82
215
  if rib_path.exists():
@@ -88,52 +221,110 @@ class ImportPlugin(plugins.ImportPluginBase):
88
221
  if rib_path.exists():
89
222
  rib_path.rename(ri_path)
90
223
 
224
+ self.need_commit = False
225
+ self.cursor = self.tdb.getDB().cursor()
226
+ self.commit_rate = 200
227
+ self.commit_limit = self.commit_rate
228
+
91
229
  self.known_systems = self.load_known_systems()
92
230
  self.known_stations = self.load_known_stations()
93
231
  self.known_commodities = self.load_known_commodities()
94
-
95
- def print(self, *args, **kwargs):
96
- return self.tdenv.uprint(*args, **kwargs)
97
-
98
- def run(self):
232
+
233
+ def print(self, *args, **kwargs) -> None:
234
+ """ Shortcut to the TradeEnv uprint method. """
235
+ self.tdenv.uprint(*args, **kwargs)
236
+
237
+ def commit(self, *, force: bool = False) -> None:
238
+ """ Perform a commit if required, but try not to do a crazy amount of committing. """
239
+ if not force and not self.need_commit:
240
+ return self.cursor
241
+
242
+ if not force and self.commit_limit > 0:
243
+ self.commit_limit -= 1
244
+ return self.cursor
245
+
246
+ db = self.tdb.getDB()
247
+ db.commit()
248
+ self.cursor = db.cursor()
249
+
250
+ self.commit_limit = self.commit_rate
251
+ self.need_commit = False
252
+
253
+ def run(self) -> bool:
99
254
  if not self.tdenv.detail:
100
255
  self.print('This will take at least several minutes...')
101
256
  self.print('You can increase verbosity (-v) to get a sense of progress')
102
- with Timing() as timing:
257
+
258
+ theme = self.tdenv.theme
259
+ BOLD, CLOSE, DIM, ITALIC = theme.bold, theme.CLOSE, theme.dim, theme.italic # pylint: disable=invalid-name
260
+
261
+ if not self.file:
262
+ url = self.url or SOURCE_URL
263
+ self.print(f'Downloading prices from remote URL: {url}')
264
+ self.file = Path(self.tdenv.tmpDir, "galaxy_stations.json")
265
+ transfers.download(self.tdenv, url, self.file)
266
+ self.print(f'Download complete, saved to local file: "{self.file}"')
267
+
268
+
269
+ sys_desc = f"Importing {ITALIC}spansh{CLOSE} data"
270
+ with Timing() as timing, Progresser(self.tdenv, sys_desc, total=len(self.known_systems)) as progress:
103
271
  system_count = 0
104
272
  total_station_count = 0
105
273
  total_commodity_count = 0
106
- for system, stations in self.data_stream():
107
- self.ensure_system(system)
108
- station_count = 0
109
- commodity_count = 0
110
- for station, commodities in stations:
111
- fq_station_name = f'@{system.name.upper()}/{station.name}'
112
- if self.maxage and (datetime.now() - station.modified) > timedelta(days=self.maxage):
113
- if self.tdenv.detail:
114
- self.print(f' | {fq_station_name:50s} | Skipping station due to age: {(datetime.now() - station.modified) / timedelta (days=1):.2f} days old')
115
- continue
116
- self.ensure_station(system, station)
274
+
275
+ age_cutoff = timedelta(days=self.maxage) if self.maxage else None
276
+ now = datetime.now()
277
+ started = time.time()
278
+
279
+ for system, station_iter in self.data_stream():
280
+ upper_sys = system.name.upper()
281
+
282
+ elapsed, averages = get_timings(started, system_count, total_station_count)
283
+ label = f"{ITALIC}#{system_count:<5d}{CLOSE} {BOLD}{upper_sys:30s}{CLOSE} {DIM}({elapsed:.2f}s, avgs: {averages}){CLOSE}"
284
+ stations = list(station_iter)
285
+ with progress.task(label, total=len(stations)) as sys_task:
286
+ if system.id not in self.known_systems:
287
+ self.ensure_system(system, upper_sys)
117
288
 
118
- items = []
119
- for commodity in commodities:
120
- commodity = self.ensure_commodity(commodity)
121
- result = self.execute("""SELECT modified FROM StationItem
122
- WHERE station_id = ? AND item_id = ?""",
123
- station.id, commodity.id, ).fetchone()
124
- modified = parse_ts(result[0]) if result else None
125
- if modified and commodity.modified <= modified:
126
- # All commodities in a station will have the same modified time,
127
- # so no need to check the rest if the fist is older.
289
+ station_count = 0
290
+ commodity_count = 0
291
+
292
+ for station, commodities in stations:
293
+ fq_station_name = f'@{upper_sys}/{station.name}'
294
+ if age_cutoff and (now - station.modified) > age_cutoff:
128
295
  if self.tdenv.detail:
129
- self.print(f' | {fq_station_name:50s} | Skipping older commodity data')
130
- break
131
- items.append((station.id, commodity.id, commodity.modified,
132
- commodity.sell, commodity.demand, -1,
133
- commodity.buy, commodity.supply, -1, 0))
134
- if items:
135
- for item in items:
136
- self.execute("""INSERT OR REPLACE INTO StationItem (
296
+ self.print(f' | {fq_station_name:50s} | Skipping station due to age: {now - station.modified}, ts: {station.modified}')
297
+ progress.bump(sys_task)
298
+ continue
299
+
300
+ station_info = self.known_stations.get(station.id)
301
+ if not station_info:
302
+ self.ensure_station(station)
303
+ elif station_info[1] != station.system_id:
304
+ self.print(f' | {station.name:50s} | Megaship station moved, updating system')
305
+ self.execute("UPDATE Station SET system_id = ? WHERE station_id = ?", station.system_id, station.id, commitable=True)
306
+ self.known_stations[station.id] = (station.name, station.system_id)
307
+
308
+ items = []
309
+ db_times = dict(self.execute("SELECT item_id, modified FROM StationItem WHERE station_id = ?", station.id))
310
+
311
+ for commodity in commodities:
312
+ if commodity.id not in self.known_commodities:
313
+ commodity = self.ensure_commodity(commodity)
314
+
315
+ db_modified = db_times.get(commodity.id)
316
+ modified = parse_ts(db_modified) if db_modified else None
317
+ if modified and commodity.modified <= modified:
318
+ # All commodities in a station will have the same modified time,
319
+ # so no need to check the rest if the fist is older.
320
+ if self.tdenv.detail:
321
+ self.print(f' | {fq_station_name:50s} | Skipping older commodity data')
322
+ break
323
+ items.append((station.id, commodity.id, commodity.modified,
324
+ commodity.sell, commodity.demand, -1,
325
+ commodity.buy, commodity.supply, -1, 0))
326
+ if items:
327
+ self.executemany("""INSERT OR REPLACE INTO StationItem (
137
328
  station_id, item_id, modified,
138
329
  demand_price, demand_units, demand_level,
139
330
  supply_price, supply_units, supply_level, from_live
@@ -141,65 +332,97 @@ class ImportPlugin(plugins.ImportPluginBase):
141
332
  ?, ?, IFNULL(?, CURRENT_TIMESTAMP),
142
333
  ?, ?, ?,
143
334
  ?, ?, ?, ?
144
- )""", *item )
145
- commodity_count += 1
146
- self.execute('COMMIT')
335
+ )""", items, commitable=True)
336
+ commodity_count += len(items)
337
+ # Good time to save data and try to keep the transaction small
338
+ self.commit()
339
+
340
+ if commodity_count:
341
+ station_count += 1
342
+ progress.bump(sys_task)
147
343
 
148
- if commodity_count:
149
- station_count += 1
150
- if station_count:
151
- system_count += 1
152
- total_station_count += station_count
153
- total_commodity_count += commodity_count
154
- if self.tdenv.detail:
155
- self.print(
156
- f'{system_count:6d} | {system.name.upper():50s} | '
157
- f'{station_count:3d} st {commodity_count:6d} co'
158
- )
344
+ if station_count:
345
+ system_count += 1
346
+ total_station_count += station_count
347
+ total_commodity_count += commodity_count
348
+ if self.tdenv.detail:
349
+ self.print(
350
+ f'{system_count:6d} | {upper_sys:50s} | '
351
+ f'{station_count:3d} st {commodity_count:6d} co'
352
+ )
353
+ self.commit()
354
+
355
+ if system_count % 25 == 1:
356
+ avg_stations = total_station_count / (system_count or 1)
357
+ progress.update(f"{sys_desc}{DIM} ({total_station_count}:station:, {avg_stations:.1f}per:glowing_star:){CLOSE}")
358
+
359
+ self.commit()
360
+
361
+ # Need to make sure cached tables are updated, if changes were made
362
+ # if self.update_cache:
363
+ # for table in [ "Item", "Station", "System" ]:
364
+ # _, path = csvexport.exportTableToFile( self.tdb, self.tdenv, table )
159
365
 
160
- self.execute('COMMIT')
161
366
  self.tdb.close()
367
+
162
368
  # Need to make sure cached tables are updated
163
- for table in [ "Item", "Station", "System" ]:
164
- _, path = csvexport.exportTableToFile( self.tdb, self.tdenv, table )
165
-
369
+ for table in ("Item", "Station", "System", "StationItem"):
370
+ # _, path =
371
+ csvexport.exportTableToFile(self.tdb, self.tdenv, table)
372
+
166
373
  self.print(
167
374
  f'{timedelta(seconds=int(timing.elapsed))!s} Done '
168
375
  f'{total_station_count} st {total_commodity_count} co'
169
376
  )
170
-
377
+
171
378
  return False
172
-
379
+
173
380
  def data_stream(self):
174
- if not self.file:
175
- url = self.url or SOURCE_URL
176
- self.print(f'Downloading prices from remote URL: {url}')
177
- self.file = self.tdenv.tmpDir / Path("galaxy_stations.json")
178
- transfers.download(self.tdenv, url, self.file)
179
- self.print(f'Download complete, saved to local file: {self.file}')
180
-
181
381
  if self.file == '-':
182
382
  self.print('Reading prices from stdin')
183
383
  stream = sys.stdin
184
384
  elif self.file:
185
- self.print(f'Reading prices from local file: {self.file}')
385
+ self.print(f'Reading prices from local file: "{self.file}"')
186
386
  stream = open(self.file, 'r', encoding='utf8')
187
387
  return ingest_stream(stream)
188
-
388
+
189
389
  def categorise_commodities(self, commodities):
190
390
  categories = {}
191
391
  for commodity in commodities:
192
392
  categories.setdefault(commodity.category, []).append(commodity)
193
393
  return categories
194
-
195
- def execute(self, query, *params, **kwparams):
394
+
395
+ def execute(self, query: str, *params, commitable: bool = False) -> Optional[sqlite3.Cursor]:
396
+ """ helper method that performs retriable queries and marks the transaction as needing to commit
397
+ if the query is commitable."""
398
+ if commitable:
399
+ self.need_commit = True
400
+ attempts = 5
401
+ while True:
402
+ try:
403
+ return self.cursor.execute(query, params)
404
+ except sqlite3.OperationalError as ex:
405
+ if "no transaction is active" in str(ex):
406
+ self.print(f"no transaction for {query}")
407
+ return
408
+ if not attempts:
409
+ raise
410
+ attempts -= 1
411
+ self.print(f'Retrying query \'{query}\': {ex!s}')
412
+ time.sleep(1)
413
+
414
+ def executemany(self, query: str, data: Iterable[Any], *, commitable: bool = False) -> Optional[sqlite3.Cursor]:
415
+ """ helper method that performs retriable queries and marks the transaction as needing to commit
416
+ if the query is commitable."""
417
+ if commitable:
418
+ self.need_commit = True
196
419
  attempts = 5
197
- cursor = self.tdb.getDB().cursor()
198
420
  while True:
199
421
  try:
200
- return cursor.execute(query, params or kwparams)
422
+ return self.cursor.executemany(query, data)
201
423
  except sqlite3.OperationalError as ex:
202
424
  if "no transaction is active" in str(ex):
425
+ self.print(f"no transaction for {query}")
203
426
  return
204
427
  if not attempts:
205
428
  raise
@@ -207,71 +430,70 @@ class ImportPlugin(plugins.ImportPluginBase):
207
430
  self.print(f'Retrying query \'{query}\': {ex!s}')
208
431
  time.sleep(1)
209
432
 
210
- def load_known_systems(self):
433
+ def load_known_systems(self) -> dict[int, str]:
434
+ """ Returns a dictionary of {system_id -> system_name} for all current systems in the database. """
211
435
  try:
212
- return dict(self.execute('SELECT system_id, name FROM System').fetchall())
213
- except:
214
- return dict()
436
+ return dict(self.cursor.execute('SELECT system_id, name FROM System'))
437
+ except Exception as e: # pylint: disable=broad-except
438
+ self.print("[purple]:thinking_face:Assuming no system data yet")
439
+ self.tdenv.DEBUG0(f"load_known_systems query raised {e}")
440
+ return {}
215
441
 
216
- def load_known_stations(self):
442
+ def load_known_stations(self) -> dict[int, tuple[str, int]]:
443
+ """ Returns a dictionary of {station_id -> (station_name, system_id)} for all current stations in the database. """
217
444
  try:
218
- return dict(self.execute('SELECT station_id, name FROM Station').fetchall())
219
- except:
220
- return dict()
445
+ return {cols[0]: (cols[1], cols[2]) for cols in self.cursor.execute('SELECT station_id, name, system_id FROM Station')}
446
+ except Exception as e: # pylint: disable=broad-except
447
+ self.print("[purple]:thinking_face:Assuming no station data yet")
448
+ self.tdenv.DEBUG0(f"load_known_stations query raised {e}")
449
+ return {}
221
450
 
222
451
  def load_known_commodities(self):
452
+ """ Returns a dictionary of {fdev_id -> name} for all current commodities in the database. """
223
453
  try:
224
- return dict(self.execute('SELECT fdev_id, name FROM Item').fetchall())
225
- except:
226
- return dict()
227
-
228
- def ensure_system(self, system):
229
- if system.id in self.known_systems:
230
- return
454
+ return dict(self.cursor.execute('SELECT fdev_id, name FROM Item'))
455
+ except Exception as e: # pylint: disable=broad-except
456
+ self.print("[purple]:thinking_face:Assuming no commodity data yet")
457
+ self.tdenv.DEBUG0(f"load_known_commodities query raised {e}")
458
+ return {}
459
+
460
+ def ensure_system(self, system: System, upper_name: str) -> None:
461
+ """ Adds a record for a system, and registers the system in the known_systems dict. """
231
462
  self.execute(
232
463
  '''
233
464
  INSERT INTO System (system_id, name, pos_x, pos_y, pos_z, modified) VALUES (?, ?, ?, ?, ?, ?)
234
465
  ''',
235
466
  system.id, system.name, system.pos_x, system.pos_y, system.pos_z, system.modified,
467
+ commitable=True,
236
468
  )
237
- self.execute('COMMIT')
238
469
  if self.tdenv.detail > 1:
239
- self.print(f' | {system.name.upper():50s} | Added missing system')
470
+ self.print(f' | {upper_name:50s} | Added missing system :glowing_star:')
240
471
  self.known_systems[system.id] = system.name
241
-
242
- def ensure_station(self, system, station):
243
- if station.id in self.known_stations:
244
- system_id = self.execute('SELECT system_id FROM Station WHERE station_id = ?', station.id, ).fetchone()[0]
245
- if system_id != system.id:
246
- self.print(f' | {station.name:50s} | Megaship station moved, updating system')
247
- self.execute("UPDATE Station SET system_id = ? WHERE station_id = ?", system.id, station.id, )
248
- self.execute('COMMIT')
249
- return
472
+
473
+ def ensure_station(self, station: Station) -> None:
474
+ """ Adds a record for a station, and registers the station in the known_stations dict. """
250
475
  self.execute(
251
476
  '''
252
477
  INSERT INTO Station (
253
- system_id,
254
- station_id,
255
- name,
256
- ls_from_star,
257
- max_pad_size,
258
- market,
259
- blackmarket,
260
- shipyard,
261
- outfitting,
262
- rearm,
263
- refuel,
264
- repair,
478
+ system_id, station_id, name,
479
+ ls_from_star, max_pad_size,
480
+ market, blackmarket, shipyard, outfitting,
481
+ rearm, refuel, repair,
265
482
  planetary,
266
483
  modified,
267
484
  type_id
268
485
  )
269
486
  VALUES (
270
- (SELECT system_id FROM System WHERE upper(name) = ?),
271
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
487
+ ?, ?, ?,
488
+ ?, ?,
489
+ ?, ?, ?, ?,
490
+ ?, ?, ?,
491
+ ?,
492
+ ?,
493
+ ?
272
494
  )
273
495
  ''',
274
- system.name.upper(),
496
+ station.system_id,
275
497
  station.id,
276
498
  station.name,
277
499
  station.distance,
@@ -286,15 +508,14 @@ class ImportPlugin(plugins.ImportPluginBase):
286
508
  self.bool_yn(station.planetary),
287
509
  station.modified,
288
510
  station.type,
511
+ commitable=True,
289
512
  )
290
- self.execute('COMMIT')
291
513
  if self.tdenv.detail > 1:
292
514
  self.print(f' | {station.name:50s} | Added missing station')
293
- self.known_stations[station.id]= station.name
294
-
295
- def ensure_commodity(self, commodity):
296
- if commodity.id in self.known_commodities:
297
- return commodity
515
+ self.known_stations[station.id] = (station.name, station.system_id)
516
+
517
+ def ensure_commodity(self, commodity: Commodity):
518
+ """ Adds a record for a commodity and registers the commodity in the known_commodities dict. """
298
519
  self.execute(
299
520
  '''
300
521
  INSERT INTO Item (item_id, category_id, name, fdev_id)
@@ -304,51 +525,51 @@ class ImportPlugin(plugins.ImportPluginBase):
304
525
  commodity.category.upper(),
305
526
  corrections.correctItem(commodity.name),
306
527
  commodity.id,
528
+ commitable=True,
307
529
  )
308
530
 
309
531
  # Need to update ui_order
310
- temp = self.execute("""SELECT
311
- name, category_id, fdev_id
532
+ temp = self.execute("""SELECT name, category_id, fdev_id, ui_order
312
533
  FROM Item
313
534
  ORDER BY category_id, name
314
535
  """)
315
536
  cat_id = 0
316
537
  ui_order = 1
317
538
  self.tdenv.DEBUG0("Updating ui_order data for items.")
318
- for line in temp:
319
- if line[1] != cat_id:
539
+ changes = []
540
+ for name, db_cat, fdev_id, db_order in temp:
541
+ if db_cat != cat_id:
320
542
  ui_order = 1
321
- cat_id = line[1]
543
+ cat_id = db_cat
322
544
  else:
323
545
  ui_order += 1
324
- self.execute("""UPDATE Item
325
- set ui_order = ?
326
- WHERE fdev_id = ?""",
327
- ui_order, line[2],)
546
+ if ui_order != db_order:
547
+ self.tdenv.DEBUG0(f"UI order for {name} ({fdev_id}) needs correction.")
548
+ changes += [(ui_order, fdev_id)]
549
+
550
+ if changes:
551
+ self.executemany(
552
+ "UPDATE Item SET ui_order = ? WHERE fdev_id = ?",
553
+ changes,
554
+ commitable=True
555
+ )
328
556
 
329
- self.execute('COMMIT')
330
- if self.tdenv.detail > 1:
331
- self.print(f' | {commodity.name:50s} | Added missing commodity')
332
557
  self.known_commodities[commodity.id] = commodity.name
558
+
333
559
  return commodity
334
-
335
- def bool_yn(self, value):
560
+
561
+ def bool_yn(self, value: Optional[bool]) -> str:
562
+ """ translates a ternary (none, true, false) into the ?/Y/N representation """
336
563
  return '?' if value is None else ('Y' if value else 'N')
337
564
 
338
565
 
339
566
  def ingest_stream(stream):
340
567
  """Ingest a spansh-style galaxy dump, yielding system-level data."""
341
- line = next(stream)
342
- assert line.rstrip(' \n,') == '['
343
- for line in stream:
344
- line = line.rstrip().rstrip(',')
345
- if line == ']':
346
- break
347
- system_data = simdjson.Parser().parse(line)
568
+ for system_data in ijson.items(stream, 'item', use_float=True):
348
569
  coords = system_data.get('coords', {})
349
570
  yield (
350
571
  System(
351
- id = system_data.get('id64'),
572
+ id=system_data.get('id64'),
352
573
  name=system_data.get('name', 'Unnamed').strip(),
353
574
  pos_x=coords.get('x', 999999),
354
575
  pos_y=coords.get('y', 999999),
@@ -361,6 +582,7 @@ def ingest_stream(stream):
361
582
 
362
583
  def ingest_stations(system_data):
363
584
  """Ingest system-level data, yielding station-level data."""
585
+ sys_id = system_data.get('id64')
364
586
  targets = [system_data, *system_data.get('bodies', ())]
365
587
  for target in targets:
366
588
  for station_data in target.get('stations', ()):
@@ -378,9 +600,11 @@ def ingest_stations(system_data):
378
600
  max_pad_size = 'M'
379
601
  elif landing_pads.get('small'):
380
602
  max_pad_size = 'S'
603
+ station_type = STATION_TYPE_MAP.get(station_data.get('type'))
381
604
  yield (
382
605
  Station(
383
- id = station_data.get('id'),
606
+ id=station_data.get('id'),
607
+ system_id=sys_id,
384
608
  name=station_data.get('name', 'Unnamed').strip(),
385
609
  distance=station_data.get('distanceToArrival', 999999),
386
610
  max_pad_size=max_pad_size,
@@ -391,8 +615,8 @@ def ingest_stations(system_data):
391
615
  rearm='Restock' in services,
392
616
  refuel='Refuel' in services,
393
617
  repair='Repair' in services,
394
- planetary=STATION_TYPE_MAP.get(station_data.get('type'))[1] or False,
395
- type=STATION_TYPE_MAP.get(station_data.get('type'))[0] or 0,
618
+ planetary=station_type[1] if station_type else False,
619
+ type=station_type[0] if station_type else 0,
396
620
  modified=parse_ts(station_data.get('updateTime')),
397
621
  ),
398
622
  ingest_market(market),