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.
- 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 +14 -17
- 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 +393 -176
- 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.16.17.dist-info → tradedangerous-11.0.0.dist-info}/METADATA +2 -2
- tradedangerous-11.0.0.dist-info/RECORD +79 -0
- tradedangerous-10.16.17.dist-info/RECORD +0 -79
- {tradedangerous-10.16.17.dist-info → tradedangerous-11.0.0.dist-info}/LICENSE +0 -0
- {tradedangerous-10.16.17.dist-info → tradedangerous-11.0.0.dist-info}/WHEEL +0 -0
- {tradedangerous-10.16.17.dist-info → tradedangerous-11.0.0.dist-info}/entry_points.txt +0 -0
- {tradedangerous-10.16.17.dist-info → tradedangerous-11.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,84 +1,215 @@
|
|
|
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
|
-
'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
|
|
80
|
-
if not (self.tdb.dataPath
|
|
81
|
-
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")
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
)""",
|
|
147
|
-
commodity_count +=
|
|
148
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
166
|
-
_, path =
|
|
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,
|
|
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.
|
|
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')
|
|
220
|
-
except:
|
|
221
|
-
|
|
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
|
|
226
|
-
except:
|
|
227
|
-
|
|
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')
|
|
232
|
-
except:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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' | {
|
|
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,
|
|
250
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
278
|
-
?, ?,
|
|
487
|
+
?, ?, ?,
|
|
488
|
+
?, ?,
|
|
489
|
+
?, ?, ?, ?,
|
|
490
|
+
?, ?, ?,
|
|
491
|
+
?,
|
|
492
|
+
?,
|
|
493
|
+
?
|
|
279
494
|
)
|
|
280
495
|
''',
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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 =
|
|
543
|
+
cat_id = db_cat
|
|
329
544
|
else:
|
|
330
545
|
ui_order += 1
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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=
|
|
402
|
-
type=
|
|
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),
|