tradedangerous 10.16.17__py3-none-any.whl → 11.0.0__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 +183 -148
  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 +14 -17
  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 +393 -176
  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.16.17.dist-info → tradedangerous-11.0.0.dist-info}/METADATA +2 -2
  69. tradedangerous-11.0.0.dist-info/RECORD +79 -0
  70. tradedangerous-10.16.17.dist-info/RECORD +0 -79
  71. {tradedangerous-10.16.17.dist-info → tradedangerous-11.0.0.dist-info}/LICENSE +0 -0
  72. {tradedangerous-10.16.17.dist-info → tradedangerous-11.0.0.dist-info}/WHEEL +0 -0
  73. {tradedangerous-10.16.17.dist-info → tradedangerous-11.0.0.dist-info}/entry_points.txt +0 -0
  74. {tradedangerous-10.16.17.dist-info → tradedangerous-11.0.0.dist-info}/top_level.txt +0 -0
@@ -1,84 +1,215 @@
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
- 'listener': 'For use by TD-listener, prevents updating cache from generated prices file',
69
201
  }
70
-
202
+
71
203
  def __init__(self, *args, **kwargs):
72
204
  super().__init__(*args, **kwargs)
73
205
  self.url = self.getOption('url')
74
206
  self.file = self.getOption('file')
75
207
  self.maxage = float(self.getOption('maxage')) if self.getOption('maxage') else None
76
- self.listener = self.getOption('listener')
77
208
  assert not (self.url and self.file), 'Provide either url or file, not both'
78
209
  if self.file and (self.file != '-'):
79
- self.file = (Path(self.tdenv.cwDir) / self.file).resolve()
80
- if not (self.tdb.dataPath / Path("TradeDangerous.prices")).exists():
81
- 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")
82
213
  rib_path = ri_path.with_suffix(".tmp")
83
214
  if ri_path.exists():
84
215
  if rib_path.exists():
@@ -90,52 +221,110 @@ class ImportPlugin(plugins.ImportPluginBase):
90
221
  if rib_path.exists():
91
222
  rib_path.rename(ri_path)
92
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
+
93
229
  self.known_systems = self.load_known_systems()
94
230
  self.known_stations = self.load_known_stations()
95
231
  self.known_commodities = self.load_known_commodities()
96
-
97
- def print(self, *args, **kwargs):
98
- return self.tdenv.uprint(*args, **kwargs)
99
-
100
- 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:
101
254
  if not self.tdenv.detail:
102
255
  self.print('This will take at least several minutes...')
103
256
  self.print('You can increase verbosity (-v) to get a sense of progress')
104
- 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:
105
271
  system_count = 0
106
272
  total_station_count = 0
107
273
  total_commodity_count = 0
108
- for system, stations in self.data_stream():
109
- self.ensure_system(system)
110
- station_count = 0
111
- commodity_count = 0
112
- for station, commodities in stations:
113
- fq_station_name = f'@{system.name.upper()}/{station.name}'
114
- if self.maxage and (datetime.now() - station.modified) > timedelta(days=self.maxage):
115
- if self.tdenv.detail:
116
- self.print(f' | {fq_station_name:50s} | Skipping station due to age: {(datetime.now() - station.modified) / timedelta (days=1):.2f} days old')
117
- continue
118
- 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)
288
+
289
+ station_count = 0
290
+ commodity_count = 0
119
291
 
120
- items = []
121
- for commodity in commodities:
122
- commodity = self.ensure_commodity(commodity)
123
- result = self.execute("""SELECT modified FROM StationItem
124
- WHERE station_id = ? AND item_id = ?""",
125
- station.id, commodity.id, ).fetchone()
126
- modified = parse_ts(result[0]) if result else None
127
- if modified and commodity.modified <= modified:
128
- # All commodities in a station will have the same modified time,
129
- # so no need to check the rest if the fist is older.
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:
130
295
  if self.tdenv.detail:
131
- self.print(f' | {fq_station_name:50s} | Skipping older commodity data')
132
- break
133
- items.append((station.id, commodity.id, commodity.modified,
134
- commodity.sell, commodity.demand, -1,
135
- commodity.buy, commodity.supply, -1, 0))
136
- if items:
137
- for item in items:
138
- 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 (
139
328
  station_id, item_id, modified,
140
329
  demand_price, demand_units, demand_level,
141
330
  supply_price, supply_units, supply_level, from_live
@@ -143,70 +332,97 @@ class ImportPlugin(plugins.ImportPluginBase):
143
332
  ?, ?, IFNULL(?, CURRENT_TIMESTAMP),
144
333
  ?, ?, ?,
145
334
  ?, ?, ?, ?
146
- )""", *item )
147
- commodity_count += 1
148
- 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)
343
+
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()
149
354
 
150
- if commodity_count:
151
- station_count += 1
152
- if station_count:
153
- system_count += 1
154
- total_station_count += station_count
155
- total_commodity_count += commodity_count
156
- if self.tdenv.detail:
157
- self.print(
158
- f'{system_count:6d} | {system.name.upper():50s} | '
159
- f'{station_count:3d} st {commodity_count:6d} co'
160
- )
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 )
161
365
 
162
- self.execute('COMMIT')
163
366
  self.tdb.close()
367
+
164
368
  # Need to make sure cached tables are updated
165
- for table in [ "Item", "Station", "System" ]:
166
- _, path = csvexport.exportTableToFile( self.tdb, self.tdenv, table )
167
-
369
+ for table in ("Item", "Station", "System", "StationItem"):
370
+ # _, path =
371
+ csvexport.exportTableToFile(self.tdb, self.tdenv, table)
372
+
168
373
  self.print(
169
374
  f'{timedelta(seconds=int(timing.elapsed))!s} Done '
170
375
  f'{total_station_count} st {total_commodity_count} co'
171
376
  )
172
-
173
- with Timing() as timing:
174
- self.print('Exporting to cache...')
175
- cache.regeneratePricesFile(self.tdb, self.tdenv)
176
- self.print(f'Cache export completed in {timedelta(seconds=int(timing.elapsed))!s}')
177
377
 
178
378
  return False
179
-
379
+
180
380
  def data_stream(self):
181
- if not self.file:
182
- url = self.url or SOURCE_URL
183
- self.print(f'Downloading prices from remote URL: {url}')
184
- self.file = self.tdenv.tmpDir / Path("galaxy_stations.json")
185
- transfers.download(self.tdenv, url, self.file)
186
- self.print(f'Download complete, saved to local file: {self.file}')
187
-
188
381
  if self.file == '-':
189
382
  self.print('Reading prices from stdin')
190
383
  stream = sys.stdin
191
384
  elif self.file:
192
- self.print(f'Reading prices from local file: {self.file}')
385
+ self.print(f'Reading prices from local file: "{self.file}"')
193
386
  stream = open(self.file, 'r', encoding='utf8')
194
387
  return ingest_stream(stream)
195
-
388
+
196
389
  def categorise_commodities(self, commodities):
197
390
  categories = {}
198
391
  for commodity in commodities:
199
392
  categories.setdefault(commodity.category, []).append(commodity)
200
393
  return categories
201
-
202
- 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
203
419
  attempts = 5
204
- cursor = self.tdb.getDB().cursor()
205
420
  while True:
206
421
  try:
207
- return cursor.execute(query, params or kwparams)
422
+ return self.cursor.executemany(query, data)
208
423
  except sqlite3.OperationalError as ex:
209
424
  if "no transaction is active" in str(ex):
425
+ self.print(f"no transaction for {query}")
210
426
  return
211
427
  if not attempts:
212
428
  raise
@@ -214,71 +430,70 @@ class ImportPlugin(plugins.ImportPluginBase):
214
430
  self.print(f'Retrying query \'{query}\': {ex!s}')
215
431
  time.sleep(1)
216
432
 
217
- 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. """
218
435
  try:
219
- return dict(self.execute('SELECT system_id, name FROM System').fetchall())
220
- except:
221
- 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 {}
222
441
 
223
- 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. """
224
444
  try:
225
- return dict(self.execute('SELECT station_id, name FROM Station').fetchall())
226
- except:
227
- 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 {}
228
450
 
229
451
  def load_known_commodities(self):
452
+ """ Returns a dictionary of {fdev_id -> name} for all current commodities in the database. """
230
453
  try:
231
- return dict(self.execute('SELECT fdev_id, name FROM Item').fetchall())
232
- except:
233
- return dict()
234
-
235
- def ensure_system(self, system):
236
- if system.id in self.known_systems:
237
- 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. """
238
462
  self.execute(
239
463
  '''
240
464
  INSERT INTO System (system_id, name, pos_x, pos_y, pos_z, modified) VALUES (?, ?, ?, ?, ?, ?)
241
465
  ''',
242
466
  system.id, system.name, system.pos_x, system.pos_y, system.pos_z, system.modified,
467
+ commitable=True,
243
468
  )
244
- self.execute('COMMIT')
245
469
  if self.tdenv.detail > 1:
246
- self.print(f' | {system.name.upper():50s} | Added missing system')
470
+ self.print(f' | {upper_name:50s} | Added missing system :glowing_star:')
247
471
  self.known_systems[system.id] = system.name
248
-
249
- def ensure_station(self, system, station):
250
- if station.id in self.known_stations:
251
- system_id = self.execute('SELECT system_id FROM Station WHERE station_id = ?', station.id, ).fetchone()[0]
252
- if system_id != system.id:
253
- self.print(f' | {station.name:50s} | Megaship station moved, updating system')
254
- self.execute("UPDATE Station SET system_id = ? WHERE station_id = ?", system.id, station.id, )
255
- self.execute('COMMIT')
256
- 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. """
257
475
  self.execute(
258
476
  '''
259
477
  INSERT INTO Station (
260
- system_id,
261
- station_id,
262
- name,
263
- ls_from_star,
264
- max_pad_size,
265
- market,
266
- blackmarket,
267
- shipyard,
268
- outfitting,
269
- rearm,
270
- refuel,
271
- repair,
478
+ system_id, station_id, name,
479
+ ls_from_star, max_pad_size,
480
+ market, blackmarket, shipyard, outfitting,
481
+ rearm, refuel, repair,
272
482
  planetary,
273
483
  modified,
274
484
  type_id
275
485
  )
276
486
  VALUES (
277
- (SELECT system_id FROM System WHERE upper(name) = ?),
278
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
487
+ ?, ?, ?,
488
+ ?, ?,
489
+ ?, ?, ?, ?,
490
+ ?, ?, ?,
491
+ ?,
492
+ ?,
493
+ ?
279
494
  )
280
495
  ''',
281
- system.name.upper(),
496
+ station.system_id,
282
497
  station.id,
283
498
  station.name,
284
499
  station.distance,
@@ -293,15 +508,14 @@ class ImportPlugin(plugins.ImportPluginBase):
293
508
  self.bool_yn(station.planetary),
294
509
  station.modified,
295
510
  station.type,
511
+ commitable=True,
296
512
  )
297
- self.execute('COMMIT')
298
513
  if self.tdenv.detail > 1:
299
514
  self.print(f' | {station.name:50s} | Added missing station')
300
- self.known_stations[station.id]= station.name
301
-
302
- def ensure_commodity(self, commodity):
303
- if commodity.id in self.known_commodities:
304
- 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. """
305
519
  self.execute(
306
520
  '''
307
521
  INSERT INTO Item (item_id, category_id, name, fdev_id)
@@ -311,51 +525,51 @@ class ImportPlugin(plugins.ImportPluginBase):
311
525
  commodity.category.upper(),
312
526
  corrections.correctItem(commodity.name),
313
527
  commodity.id,
528
+ commitable=True,
314
529
  )
315
530
 
316
531
  # Need to update ui_order
317
- temp = self.execute("""SELECT
318
- name, category_id, fdev_id
532
+ temp = self.execute("""SELECT name, category_id, fdev_id, ui_order
319
533
  FROM Item
320
534
  ORDER BY category_id, name
321
535
  """)
322
536
  cat_id = 0
323
537
  ui_order = 1
324
538
  self.tdenv.DEBUG0("Updating ui_order data for items.")
325
- for line in temp:
326
- if line[1] != cat_id:
539
+ changes = []
540
+ for name, db_cat, fdev_id, db_order in temp:
541
+ if db_cat != cat_id:
327
542
  ui_order = 1
328
- cat_id = line[1]
543
+ cat_id = db_cat
329
544
  else:
330
545
  ui_order += 1
331
- self.execute("""UPDATE Item
332
- set ui_order = ?
333
- WHERE fdev_id = ?""",
334
- 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
+ )
335
556
 
336
- self.execute('COMMIT')
337
- if self.tdenv.detail > 1:
338
- self.print(f' | {commodity.name:50s} | Added missing commodity')
339
557
  self.known_commodities[commodity.id] = commodity.name
558
+
340
559
  return commodity
341
-
342
- 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 """
343
563
  return '?' if value is None else ('Y' if value else 'N')
344
564
 
345
565
 
346
566
  def ingest_stream(stream):
347
567
  """Ingest a spansh-style galaxy dump, yielding system-level data."""
348
- line = next(stream)
349
- assert line.rstrip(' \n,') == '['
350
- for line in stream:
351
- line = line.rstrip().rstrip(',')
352
- if line == ']':
353
- break
354
- system_data = simdjson.Parser().parse(line)
568
+ for system_data in ijson.items(stream, 'item', use_float=True):
355
569
  coords = system_data.get('coords', {})
356
570
  yield (
357
571
  System(
358
- id = system_data.get('id64'),
572
+ id=system_data.get('id64'),
359
573
  name=system_data.get('name', 'Unnamed').strip(),
360
574
  pos_x=coords.get('x', 999999),
361
575
  pos_y=coords.get('y', 999999),
@@ -368,6 +582,7 @@ def ingest_stream(stream):
368
582
 
369
583
  def ingest_stations(system_data):
370
584
  """Ingest system-level data, yielding station-level data."""
585
+ sys_id = system_data.get('id64')
371
586
  targets = [system_data, *system_data.get('bodies', ())]
372
587
  for target in targets:
373
588
  for station_data in target.get('stations', ()):
@@ -385,9 +600,11 @@ def ingest_stations(system_data):
385
600
  max_pad_size = 'M'
386
601
  elif landing_pads.get('small'):
387
602
  max_pad_size = 'S'
603
+ station_type = STATION_TYPE_MAP.get(station_data.get('type'))
388
604
  yield (
389
605
  Station(
390
- id = station_data.get('id'),
606
+ id=station_data.get('id'),
607
+ system_id=sys_id,
391
608
  name=station_data.get('name', 'Unnamed').strip(),
392
609
  distance=station_data.get('distanceToArrival', 999999),
393
610
  max_pad_size=max_pad_size,
@@ -398,8 +615,8 @@ def ingest_stations(system_data):
398
615
  rearm='Restock' in services,
399
616
  refuel='Refuel' in services,
400
617
  repair='Repair' in services,
401
- planetary=STATION_TYPE_MAP.get(station_data.get('type'))[1] or False,
402
- 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,
403
620
  modified=parse_ts(station_data.get('updateTime')),
404
621
  ),
405
622
  ingest_market(market),