tradedangerous 10.17.0__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.
- tradedangerous/__init__.py +4 -4
- tradedangerous/cache.py +183 -148
- tradedangerous/cli.py +2 -7
- tradedangerous/commands/TEMPLATE.py +1 -2
- tradedangerous/commands/__init__.py +2 -4
- tradedangerous/commands/buildcache_cmd.py +6 -11
- tradedangerous/commands/buy_cmd.py +11 -12
- tradedangerous/commands/commandenv.py +16 -15
- tradedangerous/commands/exceptions.py +6 -4
- tradedangerous/commands/export_cmd.py +2 -4
- tradedangerous/commands/import_cmd.py +3 -5
- tradedangerous/commands/local_cmd.py +16 -25
- tradedangerous/commands/market_cmd.py +9 -8
- tradedangerous/commands/nav_cmd.py +17 -25
- tradedangerous/commands/olddata_cmd.py +9 -15
- tradedangerous/commands/parsing.py +9 -6
- tradedangerous/commands/rares_cmd.py +9 -10
- tradedangerous/commands/run_cmd.py +25 -26
- tradedangerous/commands/sell_cmd.py +9 -9
- tradedangerous/commands/shipvendor_cmd.py +4 -7
- tradedangerous/commands/station_cmd.py +8 -14
- tradedangerous/commands/trade_cmd.py +5 -10
- tradedangerous/commands/update_cmd.py +10 -7
- tradedangerous/commands/update_gui.py +1 -3
- tradedangerous/corrections.py +1 -3
- tradedangerous/csvexport.py +8 -8
- tradedangerous/edscupdate.py +4 -6
- tradedangerous/edsmupdate.py +4 -4
- tradedangerous/formatting.py +53 -40
- tradedangerous/fs.py +6 -6
- tradedangerous/gui.py +53 -62
- tradedangerous/jsonprices.py +8 -16
- tradedangerous/mapping.py +4 -3
- tradedangerous/mfd/__init__.py +2 -4
- tradedangerous/mfd/saitek/__init__.py +0 -1
- tradedangerous/mfd/saitek/directoutput.py +8 -11
- tradedangerous/mfd/saitek/x52pro.py +5 -7
- tradedangerous/misc/checkpricebounds.py +2 -3
- tradedangerous/misc/clipboard.py +2 -3
- tradedangerous/misc/coord64.py +2 -1
- tradedangerous/misc/derp-sentinel.py +1 -1
- tradedangerous/misc/diff-system-csvs.py +3 -0
- tradedangerous/misc/eddb.py +1 -3
- tradedangerous/misc/eddn.py +2 -2
- tradedangerous/misc/edsc.py +7 -14
- tradedangerous/misc/edsm.py +1 -8
- tradedangerous/misc/importeddbstats.py +2 -1
- tradedangerous/misc/prices-json-exp.py +7 -5
- tradedangerous/misc/progress.py +2 -2
- tradedangerous/plugins/__init__.py +2 -2
- tradedangerous/plugins/edapi_plug.py +13 -19
- tradedangerous/plugins/edcd_plug.py +4 -5
- tradedangerous/plugins/eddblink_plug.py +11 -15
- tradedangerous/plugins/edmc_batch_plug.py +3 -5
- tradedangerous/plugins/journal_plug.py +2 -1
- tradedangerous/plugins/netlog_plug.py +5 -5
- tradedangerous/plugins/spansh_plug.py +394 -170
- tradedangerous/prices.py +19 -20
- tradedangerous/submit-distances.py +3 -8
- tradedangerous/templates/TradeDangerous.sql +305 -306
- tradedangerous/trade.py +12 -5
- tradedangerous/tradecalc.py +30 -34
- tradedangerous/tradedb.py +140 -206
- tradedangerous/tradeenv.py +143 -69
- tradedangerous/tradegui.py +4 -2
- tradedangerous/transfers.py +23 -20
- tradedangerous/version.py +1 -1
- {tradedangerous-10.17.0.dist-info → tradedangerous-11.0.0.dist-info}/METADATA +2 -2
- tradedangerous-11.0.0.dist-info/RECORD +79 -0
- tradedangerous-10.17.0.dist-info/RECORD +0 -79
- {tradedangerous-10.17.0.dist-info → tradedangerous-11.0.0.dist-info}/LICENSE +0 -0
- {tradedangerous-10.17.0.dist-info → tradedangerous-11.0.0.dist-info}/WHEEL +0 -0
- {tradedangerous-10.17.0.dist-info → tradedangerous-11.0.0.dist-info}/entry_points.txt +0 -0
- {tradedangerous-10.17.0.dist-info → tradedangerous-11.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,72 +1,205 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
9
|
+
import typing
|
|
5
10
|
from collections import namedtuple
|
|
6
|
-
|
|
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
|
|
9
|
-
import
|
|
16
|
+
from rich.progress import Progress
|
|
17
|
+
import ijson
|
|
10
18
|
import sqlite3
|
|
11
19
|
|
|
12
|
-
from .. import plugins, cache,
|
|
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'
|
|
18
|
-
'Outpost'
|
|
19
|
-
'Coriolis Starport'
|
|
20
|
-
'Ocellus Starport'
|
|
21
|
-
'Orbis Starport'
|
|
22
|
-
'Planetary Outpost'
|
|
23
|
-
'Planetary Port'
|
|
24
|
-
'Mega ship'
|
|
25
|
-
'Asteroid base'
|
|
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
|
|
78
|
-
if not (self.tdb.dataPath
|
|
79
|
-
ri_path = self.tdb.dataPath
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
)""",
|
|
145
|
-
commodity_count +=
|
|
146
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
164
|
-
_, path =
|
|
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,
|
|
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.
|
|
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')
|
|
213
|
-
except:
|
|
214
|
-
|
|
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
|
|
219
|
-
except:
|
|
220
|
-
|
|
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')
|
|
225
|
-
except:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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' | {
|
|
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,
|
|
243
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
271
|
-
?, ?,
|
|
487
|
+
?, ?, ?,
|
|
488
|
+
?, ?,
|
|
489
|
+
?, ?, ?, ?,
|
|
490
|
+
?, ?, ?,
|
|
491
|
+
?,
|
|
492
|
+
?,
|
|
493
|
+
?
|
|
272
494
|
)
|
|
273
495
|
''',
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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 =
|
|
543
|
+
cat_id = db_cat
|
|
322
544
|
else:
|
|
323
545
|
ui_order += 1
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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=
|
|
395
|
-
type=
|
|
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),
|