tradedangerous 11.5.3__py3-none-any.whl → 12.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (47) hide show
  1. tradedangerous/cache.py +567 -395
  2. tradedangerous/cli.py +2 -2
  3. tradedangerous/commands/TEMPLATE.py +25 -26
  4. tradedangerous/commands/__init__.py +8 -16
  5. tradedangerous/commands/buildcache_cmd.py +40 -10
  6. tradedangerous/commands/buy_cmd.py +57 -46
  7. tradedangerous/commands/commandenv.py +0 -2
  8. tradedangerous/commands/export_cmd.py +78 -50
  9. tradedangerous/commands/import_cmd.py +67 -31
  10. tradedangerous/commands/market_cmd.py +52 -19
  11. tradedangerous/commands/olddata_cmd.py +120 -107
  12. tradedangerous/commands/rares_cmd.py +122 -110
  13. tradedangerous/commands/run_cmd.py +118 -66
  14. tradedangerous/commands/sell_cmd.py +52 -45
  15. tradedangerous/commands/shipvendor_cmd.py +49 -234
  16. tradedangerous/commands/station_cmd.py +55 -485
  17. tradedangerous/commands/update_cmd.py +56 -420
  18. tradedangerous/csvexport.py +173 -162
  19. tradedangerous/db/__init__.py +27 -0
  20. tradedangerous/db/adapter.py +191 -0
  21. tradedangerous/db/config.py +95 -0
  22. tradedangerous/db/engine.py +246 -0
  23. tradedangerous/db/lifecycle.py +332 -0
  24. tradedangerous/db/locks.py +208 -0
  25. tradedangerous/db/orm_models.py +455 -0
  26. tradedangerous/db/paths.py +112 -0
  27. tradedangerous/db/utils.py +661 -0
  28. tradedangerous/gui.py +2 -2
  29. tradedangerous/plugins/eddblink_plug.py +387 -251
  30. tradedangerous/plugins/spansh_plug.py +2488 -821
  31. tradedangerous/prices.py +124 -142
  32. tradedangerous/templates/TradeDangerous.sql +6 -6
  33. tradedangerous/tradecalc.py +1227 -1109
  34. tradedangerous/tradedb.py +533 -384
  35. tradedangerous/tradeenv.py +12 -1
  36. tradedangerous/version.py +1 -1
  37. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/METADATA +11 -7
  38. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/RECORD +42 -38
  39. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/WHEEL +1 -1
  40. tradedangerous/commands/update_gui.py +0 -721
  41. tradedangerous/jsonprices.py +0 -254
  42. tradedangerous/plugins/edapi_plug.py +0 -1071
  43. tradedangerous/plugins/journal_plug.py +0 -537
  44. tradedangerous/plugins/netlog_plug.py +0 -316
  45. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/entry_points.txt +0 -0
  46. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info/licenses}/LICENSE +0 -0
  47. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/top_level.txt +0 -0
@@ -1,1071 +0,0 @@
1
- # ----------------------------------------------------------------
2
- # Import plugin that downloads market and ship vendor data from the
3
- # Elite Dangerous mobile API.
4
- # ----------------------------------------------------------------
5
-
6
- import hashlib
7
- import json
8
- import pathlib
9
- import random
10
- import requests
11
- import time
12
- import base64
13
- import webbrowser
14
- import configparser
15
-
16
- from datetime import datetime, timezone
17
- from collections import namedtuple
18
- from http import HTTPStatus
19
- from http.server import HTTPServer, BaseHTTPRequestHandler
20
- from urllib.parse import urlsplit, parse_qs
21
-
22
- from .. import cache, csvexport, plugins, mapping, fs
23
-
24
- import secrets
25
-
26
- __version_info__ = (5, 0, 2)
27
- __version__ = '.'.join(map(str, __version_info__))
28
-
29
- # ----------------------------------------------------------------
30
- # Deal with some differences in names between TD, ED and the API.
31
- # ----------------------------------------------------------------
32
-
33
- bracket_levels = ('?', 'L', 'M', 'H')
34
-
35
- # Categories to ignore. Drones end up here. No idea what they are.
36
- cat_ignore = [
37
- 'NonMarketable',
38
- ]
39
-
40
-
41
- class OAuthCallbackHandler(BaseHTTPRequestHandler):
42
-
43
- def do_GET(self):
44
- split_url = urlsplit(self.path)
45
- if split_url.path == "/callback":
46
- parsed_url = parse_qs(split_url.query)
47
- self.server.callback_code = parsed_url.get("code", [None])[0]
48
- self.server.callback_state = parsed_url.get("state", [None])[0]
49
- self.send_response(HTTPStatus.OK)
50
- body_text = b"<p>You can close me now.</p>"
51
- else:
52
- self.send_response(HTTPStatus.NOT_IMPLEMENTED)
53
- body_text = b"<p>Something went wrong.</p>"
54
- self.end_headers()
55
- self.wfile.write(b"<html><head><title>EDAPI Frontier Login</title></head>")
56
- self.wfile.write(b"<body><h1>AUTHENTICATION</h1>")
57
- self.wfile.write(body_text)
58
- self.wfile.write(b"</body></html>")
59
-
60
- def log_message(self, format, *args):
61
- pass
62
-
63
-
64
- class OAuthCallbackServer:
65
- def __init__(self, hostname, port, handler):
66
- myServer = HTTPServer
67
- myServer.callback_code = None
68
- myServer.callback_state = None
69
- self.httpd = myServer((hostname, port), handler)
70
- self.httpd.handle_request()
71
- self.httpd.server_close()
72
-
73
-
74
- class EDAPI:
75
- '''
76
- A class that handles the Frontier ED API.
77
- '''
78
-
79
- _agent = "EDCD-TradeDangerousPluginEDAPI-%s" % __version__
80
- _basename = 'edapi'
81
- _configfile = _basename + '.config'
82
-
83
- def __init__(
84
- self,
85
- basename = 'edapi',
86
- debug = False,
87
- configfile = None,
88
- json_file = None,
89
- login = False
90
- ):
91
- '''
92
- Initialize
93
- '''
94
-
95
- # Build common file names from basename.
96
- self._basename = basename
97
- if configfile:
98
- self._configfile = configfile
99
-
100
- self.debug = debug
101
- self.login = login
102
-
103
- # If json_file was given, just load that instead.
104
- if json_file:
105
- with open(json_file) as file:
106
- self.profile = json.load(file)
107
- return
108
-
109
- # Setup the session.
110
- self.opener = requests.Session()
111
-
112
- # Setup config
113
- self.config = configparser.ConfigParser()
114
- self.config.read_dict({
115
- "frontier": {
116
- "AUTH_URL": "https://auth.frontierstore.net",
117
- "AUTH_URL_AUTH": "https://auth.frontierstore.net/auth",
118
- "AUTH_URL_TOKEN": "https://auth.frontierstore.net/token",
119
- },
120
- "companion": {
121
- "CAPI_LIVE_URL": "https://companion.orerve.net",
122
- "CAPI_BETA_URL": "https://pts-companion.orerve.net",
123
- "CLIENT_ID": "0d60c9fe-1ae3-4849-91e9-250db5de9d79",
124
- "REDIRECT_URI": "http://127.0.0.1:2989/callback",
125
- },
126
- "authorization": {}
127
- })
128
- self._authorization_set_config({})
129
- self.config.read(self._configfile)
130
-
131
- # If force login, kill the authorization
132
- if self.login:
133
- self._authorization_set_config({})
134
-
135
- # Grab the commander profile
136
- self.text = []
137
- self.profile = self.query_capi("/profile")
138
-
139
- # kfsone: not sure if there was a reason to query these even tho we didn't
140
- # use the resulting data.
141
- # market = self.query_capi("/market")
142
- # shipyard = self.query_capi("/shipyard")
143
-
144
- # Grab the market, outfitting and shipyard data if needed
145
- portServices = self.profile['lastStarport'].get('services')
146
- if self.profile['commander']['docked'] and portServices:
147
- if portServices.get('commodities'):
148
- res = self.query_capi("/market")
149
- if int(res["id"]) == int(self.profile["lastStarport"]["id"]):
150
- self.profile["lastStarport"].update(res)
151
- hasShipyard = portServices.get('shipyard')
152
- if hasShipyard or portServices.get('outfitting'):
153
- # the ships for the shipyard are not always returned the first time
154
- for attempt in range(3):
155
- # try up to 3 times
156
- res = self.query_capi("/shipyard")
157
- if not hasShipyard or res.get('ships'):
158
- break
159
- if self.debug:
160
- print("No shipyard in response, I'll try again in 5s")
161
- time.sleep(5)
162
- if int(res["id"]) == int(self.profile["lastStarport"]["id"]):
163
- self.profile["lastStarport"].update(res)
164
-
165
- def query_capi(self, capi_endpoint):
166
- self._authorization_check()
167
- response = self.opener.get(self.config["companion"]["CAPI_LIVE_URL"] + capi_endpoint)
168
- try:
169
- print(response.text)
170
- data = response.json()
171
- self.text.append(response.text)
172
- except:
173
- if self.debug:
174
- print(' URL:', response.url)
175
- print('status:', response.status_code)
176
- print(' text:', response.text)
177
- txtDebug = ""
178
- else:
179
- txtDebug = "\nTry with --debug and report this."
180
- raise plugins.PluginException(
181
- "Unable to parse JSON response for {}!"
182
- "\nTry to relogin with the 'login' option."
183
- "{}".format(capi_endpoint, txtDebug)
184
- )
185
- return data
186
-
187
- def _authorization_check(self):
188
- status_ok = True
189
- expires_at = self.config.getint("authorization", "expires_at")
190
- if self.debug:
191
- print("auth expires_at", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expires_at)))
192
- if (expires_at - time.time()) < 60:
193
- if self.debug:
194
- print("authorization expired")
195
- status_ok = False
196
- if self.config["authorization"]["refresh_token"]:
197
- status_ok = self._authorization_refresh()
198
- if not status_ok:
199
- status_ok = self._authorization_login()
200
- with open(self._configfile, "w") as c:
201
- self.config.write(c)
202
-
203
- if not status_ok:
204
- # Something terrible happend
205
- raise plugins.PluginException("Couldn't get frontier authorization.")
206
-
207
- # Setup session authorization
208
- self.opener.headers = {
209
- 'User-Agent': self._agent,
210
- 'Authorization': "%s %s" % (
211
- self.config['authorization']['token_type'],
212
- self.config['authorization']['access_token'],
213
- ),
214
- }
215
-
216
- def _authorization_set_config(self, auth_data):
217
- self.config.set("authorization", "access_token", auth_data.get("access_token", ""))
218
- self.config.set("authorization", "token_type", auth_data.get("token_type", ""))
219
- self.config.set("authorization", "expires_at", str(auth_data.get("expires_at", 0)))
220
- self.config.set("authorization", "refresh_token", auth_data.get("refresh_token", ""))
221
-
222
- def _authorization_token(self, data):
223
- expires_at = int(time.time())
224
- res = requests.post(self.config["frontier"]["AUTH_URL_TOKEN"], data = data)
225
- if self.debug:
226
- print(res, res.url)
227
- print(res.text)
228
- if res.status_code == requests.codes.ok: # pylint: disable=no-member
229
- auth_data = res.json()
230
- auth_data['expires_at'] = expires_at + int(auth_data.get('expires_in', 0))
231
- self._authorization_set_config(auth_data)
232
- return True
233
- self._authorization_set_config({})
234
- return False
235
-
236
- def _authorization_refresh(self):
237
- data = {
238
- "grant_type": "refresh_token",
239
- "refresh_token": self.config["authorization"]["refresh_token"],
240
- "client_id": self.config["companion"]["CLIENT_ID"],
241
- }
242
- return self._authorization_token(data)
243
-
244
- def _authorization_login(self):
245
- session_state = secrets.token_urlsafe(36)
246
- code_verifier = secrets.token_urlsafe(36)
247
- code_digest = hashlib.sha256(code_verifier.encode()).digest()
248
- code_challenge = base64.urlsafe_b64encode(code_digest).decode().rstrip("=")
249
- data = {
250
- 'response_type': 'code',
251
- 'redirect_uri': self.config["companion"]["REDIRECT_URI"],
252
- 'client_id': self.config["companion"]["CLIENT_ID"],
253
- 'code_challenge': code_challenge,
254
- 'code_challenge_method': 'S256',
255
- 'state': session_state,
256
- }
257
- req = requests.Request("GET", self.config["frontier"]["AUTH_URL_AUTH"], params = data)
258
- pre = req.prepare()
259
- webbrowser.open_new_tab(pre.url)
260
-
261
- redirect_uri = urlsplit(self.config["companion"]["REDIRECT_URI"])
262
- oauth = OAuthCallbackServer(redirect_uri.hostname, redirect_uri.port, OAuthCallbackHandler)
263
- if oauth.httpd.callback_code and oauth.httpd.callback_state == session_state:
264
- data = {
265
- "grant_type": "authorization_code",
266
- "code": oauth.httpd.callback_code,
267
- "code_verifier": code_verifier,
268
- "client_id": self.config["companion"]["CLIENT_ID"],
269
- "redirect_uri": self.config["companion"]["REDIRECT_URI"],
270
- }
271
- return self._authorization_token(data)
272
- return False
273
-
274
-
275
- class EDDN:
276
- _gateways = (
277
- 'https://eddn.edcd.io:4430/upload/',
278
- # 'http://eddn-gateway.ed-td.space:8080/upload/',
279
- )
280
-
281
- _commodity_schemas = {
282
- 'production': 'https://eddn.edcd.io/schemas/commodity/3',
283
- 'test': 'https://eddn.edcd.io/schemas/commodity/3/test',
284
- }
285
-
286
- _shipyard_schemas = {
287
- 'production': 'https://eddn.edcd.io/schemas/shipyard/2',
288
- 'test': 'https://eddn.edcd.io/schemas/shipyard/2/test',
289
- }
290
-
291
- _outfitting_schemas = {
292
- 'production': 'https://eddn.edcd.io/schemas/outfitting/2',
293
- 'test': 'https://eddn.edcd.io/schemas/outfitting/2/test',
294
- }
295
-
296
- _debug = True
297
-
298
- # As of 1.3, ED reports four levels.
299
- _levels = (
300
- 'Low',
301
- 'Low',
302
- 'Med',
303
- 'High',
304
- )
305
-
306
- def __init__(
307
- self,
308
- uploaderID,
309
- noHash,
310
- softwareName,
311
- softwareVersion
312
- ):
313
- # Obfuscate uploaderID
314
- if noHash:
315
- self.uploaderID = uploaderID
316
- else:
317
- self.uploaderID = hashlib.sha1(uploaderID.encode('utf-8')).hexdigest()
318
- self.softwareName = softwareName
319
- self.softwareVersion = softwareVersion
320
-
321
- def postMessage(
322
- self,
323
- message,
324
- timestamp = 0
325
- ):
326
- if timestamp:
327
- timestamp = datetime.fromtimestamp(timestamp).isoformat()
328
- else:
329
- timestamp = datetime.now(timezone.utc).astimezone().isoformat()
330
-
331
- message['message']['timestamp'] = timestamp
332
-
333
- url = random.choice(self._gateways)
334
-
335
- headers = {
336
- 'content-type': 'application/json; charset=utf8'
337
- }
338
-
339
- if self._debug:
340
- print(
341
- json.dumps(
342
- message,
343
- sort_keys = True,
344
- indent = 4
345
- )
346
- )
347
-
348
- r = requests.post(
349
- url,
350
- headers = headers,
351
- data = json.dumps(
352
- message,
353
- ensure_ascii = False
354
- ).encode('utf8'),
355
- verify = True
356
- )
357
-
358
- r.raise_for_status()
359
-
360
- def publishCommodities(
361
- self,
362
- systemName,
363
- stationName,
364
- marketId,
365
- commodities,
366
- additional = None,
367
- timestamp = 0
368
- ):
369
- message = {}
370
-
371
- message['$schemaRef'] = self._commodity_schemas[('test' if self._debug else 'production')] # NOQA
372
-
373
- message['header'] = {
374
- 'uploaderID': self.uploaderID,
375
- 'softwareName': self.softwareName,
376
- 'softwareVersion': self.softwareVersion
377
- }
378
-
379
- message['message'] = {
380
- 'systemName': systemName,
381
- 'stationName': stationName,
382
- 'marketId': marketId,
383
- 'commodities': commodities,
384
- }
385
- if additional:
386
- message['message'].update(additional)
387
-
388
- self.postMessage(message, timestamp)
389
-
390
- def publishShipyard(
391
- self,
392
- systemName,
393
- stationName,
394
- marketId,
395
- ships,
396
- timestamp = 0
397
- ):
398
- message = {}
399
-
400
- message['$schemaRef'] = self._shipyard_schemas[('test' if self._debug else 'production')] # NOQA
401
-
402
- message['header'] = {
403
- 'uploaderID': self.uploaderID,
404
- 'softwareName': self.softwareName,
405
- 'softwareVersion': self.softwareVersion
406
- }
407
-
408
- message['message'] = {
409
- 'systemName': systemName,
410
- 'stationName': stationName,
411
- 'marketId': marketId,
412
- 'ships': ships,
413
- }
414
-
415
- self.postMessage(message, timestamp)
416
-
417
- def publishOutfitting(
418
- self,
419
- systemName,
420
- stationName,
421
- marketId,
422
- modules,
423
- timestamp = 0
424
- ):
425
- message = {}
426
-
427
- message['$schemaRef'] = self._outfitting_schemas[('test' if self._debug else 'production')] # NOQA
428
-
429
- message['header'] = {
430
- 'uploaderID': self.uploaderID,
431
- 'softwareName': self.softwareName,
432
- 'softwareVersion': self.softwareVersion
433
- }
434
-
435
- message['message'] = {
436
- 'systemName': systemName,
437
- 'stationName': stationName,
438
- 'marketId': marketId,
439
- 'modules': modules,
440
- }
441
-
442
- self.postMessage(message, timestamp)
443
-
444
-
445
- class ImportPlugin(plugins.ImportPluginBase):
446
- """
447
- Plugin that downloads market and ship vendor data from the Elite Dangerous
448
- mobile API.
449
- """
450
-
451
- pluginOptions = {
452
- 'csvs': 'Merge shipyards into ShipVendor.csv.',
453
- 'edcd': 'Call the EDCD plugin first.',
454
- 'eddn': 'Post market, shipyard and outfitting to EDDN.',
455
- 'name': 'Do not obfuscate commander name for EDDN submit.',
456
- 'save': 'Save the API response (tmp/profile.YYYYMMDD_HHMMSS.json).',
457
- 'tdh': 'Save the API response for TDH (tmp/tdh_profile.json).',
458
- 'test': 'Test the plugin with a json file (test=[FILENAME]).',
459
- 'warn': 'Ask for station update if a API<->DB diff is encountered.',
460
- 'login': 'Ask for login credentials.',
461
- }
462
-
463
- configFile = "edapi.config"
464
-
465
- def __init__(self, tdb, tdenv):
466
- super().__init__(tdb, tdenv)
467
-
468
- self.filename = self.defaultImportFile
469
- self.configPath = tdb.dataPath / pathlib.Path(ImportPlugin.configFile)
470
-
471
- def askForStationData(self, system, stnName = None, station = None):
472
- """
473
- Ask for new or updated station data
474
- """
475
- tdb, tdenv = self.tdb, self.tdenv
476
- askForData = False
477
-
478
- stnDefault = namedtuple(
479
- 'stnDefault', [
480
- 'lsFromStar', 'market', 'blackMarket', 'shipyard', 'maxPadSize',
481
- 'outfitting', 'rearm', 'refuel', 'repair', 'planetary',
482
- ]
483
- )
484
-
485
- def tellUserAPIResponse(defName, defValue):
486
- if defValue == "Y":
487
- tdenv.NOTE("{:>12} in API response", defName)
488
- else:
489
- tdenv.NOTE("{:>12} NOT in API response", defName)
490
-
491
- def getYNfromObject(obj, key, val = None):
492
- if val:
493
- return "Y" if obj.get(key) == val else "N"
494
- else:
495
- return "Y" if key in obj else "N"
496
-
497
- # defaults from API response are not reliable!
498
- checkStarport = self.edAPI.profile['lastStarport']
499
- defMarket = getYNfromObject(checkStarport, 'commodities')
500
- defShipyard = getYNfromObject(checkStarport, 'ships')
501
- defOutfitting = getYNfromObject(checkStarport, 'modules')
502
- tellUserAPIResponse("'Outfitting'", defOutfitting)
503
- tellUserAPIResponse("'ShipYard'", defShipyard)
504
- tellUserAPIResponse("'Market'", defMarket)
505
-
506
- def warnAPIResponse(checkName, checkYN):
507
- # no warning if unknown
508
- if checkYN == "?":
509
- return False
510
- warnText = (
511
- "The station should{s} have a {what}, "
512
- "but the API did{d} return one."
513
- )
514
- if checkYN == "Y":
515
- s, d = "", "n't"
516
- else:
517
- s, d = "n't", ""
518
-
519
- tdenv.WARN(warnText, what = checkName, s = s, d = d)
520
- return True if self.getOption('warn') else False
521
-
522
- # station services since ED update 2.4
523
- checkServices = checkStarport.get('services', None)
524
- if checkServices:
525
- if station:
526
- tdenv.NOTE('Station known.')
527
- stnlsFromStar = station.lsFromStar
528
- stnmaxPadSize = station.maxPadSize
529
- stnplanetary = station.planetary
530
- else:
531
- tdenv.NOTE('Station unknown.')
532
- stnlsFromStar = 0
533
- stnmaxPadSize = "?"
534
- stnplanetary = "?"
535
- tdenv.NOTE("Found station services.")
536
- if checkStarport.get('outpostType', None) == 'starport':
537
- # only the big one can be detected
538
- stnmaxPadSize = "L"
539
- stnplanetary = "N"
540
- defStation = stnDefault(
541
- lsFromStar = stnlsFromStar,
542
- market = getYNfromObject(checkServices, 'commodities', val = 'ok'),
543
- blackMarket = getYNfromObject(checkServices, 'blackmarket', val = 'ok'),
544
- shipyard = getYNfromObject(checkServices, 'shipyard', val = 'ok'),
545
- maxPadSize = stnmaxPadSize,
546
- outfitting = getYNfromObject(checkServices, 'outfitting', val = 'ok'),
547
- rearm = getYNfromObject(checkServices, 'rearm', val = 'ok'),
548
- refuel = getYNfromObject(checkServices, 'refuel', val = 'ok'),
549
- repair = getYNfromObject(checkServices, 'repair', val = 'ok'),
550
- planetary = stnplanetary,
551
- )
552
- elif station:
553
- tdenv.NOTE('Station known.')
554
- defStation = stnDefault(
555
- lsFromStar = station.lsFromStar,
556
- market = defMarket if station.market == "?" else station.market,
557
- blackMarket = station.blackMarket,
558
- shipyard = defShipyard if station.shipyard == "?" else station.shipyard,
559
- maxPadSize = station.maxPadSize,
560
- outfitting = defOutfitting if station.outfitting == "?" else station.outfitting,
561
- rearm = station.rearm,
562
- refuel = station.refuel,
563
- repair = station.repair,
564
- planetary = station.planetary,
565
- )
566
- else:
567
- tdenv.NOTE('Station unknown.')
568
- defStation = stnDefault(
569
- lsFromStar = 0, market = defMarket,
570
- blackMarket = "?", shipyard = defShipyard,
571
- maxPadSize = "?", outfitting = defOutfitting,
572
- rearm = "?", refuel = "?",
573
- repair = "?", planetary = "?",
574
- )
575
-
576
- warning = False
577
- if defStation.outfitting != defOutfitting:
578
- warning |= warnAPIResponse('outfitting', defStation.outfitting)
579
- if defStation.shipyard != defShipyard:
580
- warning |= warnAPIResponse('shipyard', defStation.shipyard)
581
- if defStation.market != defMarket:
582
- warning |= warnAPIResponse('market', defStation.market)
583
- if warning:
584
- tdenv.WARN("Please update station data with correct values.")
585
- tdenv.WARN("(Fields will be marked with an leading asterisk '*')")
586
- askForData = True
587
- if ((defStation.lsFromStar == 0) or ("?" in defStation)):
588
- askForData = True
589
-
590
- newStation = {}
591
- for key in defStation._fields:
592
- newStation[key] = getattr(defStation, key)
593
-
594
- if askForData:
595
- tdenv.NOTE("Values in brackets are the default.")
596
- lsFromStar = input(
597
- " Stn/Ls..............[{}]: ".format(defStation.lsFromStar)
598
- ) or defStation.lsFromStar
599
- try:
600
- lsFromStar = int(float(lsFromStar) + 0.5)
601
- except:
602
- print("That doesn't seem to be a number. Defaulting to zero.")
603
- lsFromStar = defStation.lsFromStar
604
- newStation['lsFromStar'] = lsFromStar
605
-
606
- for askText, askField, markValue in [
607
- ('Pad Size....(s,m,l) ', 'maxPadSize', defStation.maxPadSize),
608
- ('Planetary.....(y,n) ', 'planetary', defStation.planetary),
609
- ('B/Market......(y,n) ', 'blackMarket', defStation.blackMarket),
610
- ('Refuel........(y,n) ', 'refuel', defStation.refuel),
611
- ('Repair........(y,n) ', 'repair', defStation.repair),
612
- ('Restock.......(y,n) ', 'rearm', defStation.rearm),
613
- ('Outfitting....(y,n) ', 'outfitting', defOutfitting),
614
- ('Shipyard......(y,n) ', 'shipyard', defShipyard),
615
- ('Market........(y,n) ', 'market', defMarket),
616
- ]:
617
- defValue = getattr(defStation, askField)
618
- if defValue != markValue:
619
- mark = "*"
620
- else:
621
- mark = " "
622
- askValue = input(
623
- "{}{}[{}]: ".format(mark, askText, defValue)
624
- ) or defValue
625
- newStation[askField] = askValue
626
-
627
- else:
628
-
629
- def _detail(value, source):
630
- detail = source[value]
631
- if detail == '?':
632
- detail += ' [unknown]'
633
- return detail
634
-
635
- ls = newStation['lsFromStar']
636
- print(" Stn/Ls....:", ls, '[unknown]' if ls == 0 else '')
637
- print(" Pad Size..:", _detail(newStation['maxPadSize'], tdb.padSizes))
638
- print(" Planetary.:", _detail(newStation['planetary'], tdb.planetStates))
639
- print(" B/Market..:", _detail(newStation['blackMarket'], tdb.marketStates))
640
- print(" Refuel....:", _detail(newStation['refuel'], tdb.marketStates))
641
- print(" Repair....:", _detail(newStation['repair'], tdb.marketStates))
642
- print(" Restock...:", _detail(newStation['rearm'], tdb.marketStates))
643
- print(" Outfitting:", _detail(newStation['outfitting'], tdb.marketStates))
644
- print(" Shipyard..:", _detail(newStation['shipyard'], tdb.marketStates))
645
- print(" Market....:", _detail(newStation['market'], tdb.marketStates))
646
-
647
- exportCSV = False
648
- if not station:
649
- station = tdb.addLocalStation(
650
- system = system,
651
- name = stnName,
652
- lsFromStar = newStation['lsFromStar'],
653
- blackMarket = newStation['blackMarket'],
654
- maxPadSize = newStation['maxPadSize'],
655
- market = newStation['market'],
656
- shipyard = newStation['shipyard'],
657
- outfitting = newStation['outfitting'],
658
- rearm = newStation['rearm'],
659
- refuel = newStation['refuel'],
660
- repair = newStation['repair'],
661
- planetary = newStation['planetary'],
662
- )
663
- exportCSV = True
664
- else:
665
- # let the function check for changes
666
- if tdb.updateLocalStation(
667
- station = station,
668
- lsFromStar = newStation['lsFromStar'],
669
- blackMarket = newStation['blackMarket'],
670
- maxPadSize = newStation['maxPadSize'],
671
- market = newStation['market'],
672
- shipyard = newStation['shipyard'],
673
- outfitting = newStation['outfitting'],
674
- rearm = newStation['rearm'],
675
- refuel = newStation['refuel'],
676
- repair = newStation['repair'],
677
- planetary = newStation['planetary'],
678
- ):
679
- exportCSV = True
680
-
681
- if exportCSV:
682
- lines, csvPath = csvexport.exportTableToFile(
683
- tdb,
684
- tdenv,
685
- "Station",
686
- )
687
- tdenv.DEBUG0("{} updated.", csvPath)
688
- return station
689
-
690
- def run(self):
691
- tdb, tdenv = self.tdb, self.tdenv
692
-
693
- # first check for EDCD
694
- if self.getOption("edcd"):
695
- # Call the EDCD plugin
696
- try:
697
- import plugins.edcd_plug as EDCD # @UnresolvedImport
698
- except:
699
- raise plugins.PluginException("EDCD plugin not found.")
700
- tdenv.NOTE("Calling the EDCD plugin.")
701
- edcdPlugin = EDCD.ImportPlugin(tdb, tdenv)
702
- edcdPlugin.options["csvs"] = True
703
- edcdPlugin.run()
704
- tdenv.NOTE("Going back to EDAPI.\n")
705
-
706
- # now load the mapping tables
707
- itemMap = mapping.FDEVMappingItems(tdb, tdenv)
708
- shipMap = mapping.FDEVMappingShips(tdb, tdenv)
709
-
710
- # Connect to the API, authenticate, and pull down the commander
711
- # /profile.
712
- if self.getOption("test"):
713
- tdenv.WARN("#############################")
714
- tdenv.WARN("### EDAPI in test mode. ###")
715
- tdenv.WARN("#############################")
716
- apiED = namedtuple('EDAPI', ['profile', 'text'])
717
- try:
718
- proPath = pathlib.Path(self.getOption("test"))
719
- except TypeError:
720
- raise plugins.PluginException(
721
- "Option 'test' must be a file name"
722
- )
723
- if proPath.exists():
724
- with proPath.open() as proFile:
725
- proData = json.load(proFile)
726
- if isinstance(proData, list):
727
- # since 4.3.0: list(profile, market, shipyard)
728
- testProfile = proData[0]
729
- for data in proData[1:]:
730
- if int(data["id"]) == int(testProfile["lastStarport"]["id"]):
731
- testProfile["lastStarport"].update(data)
732
- else:
733
- testProfile = proData
734
- api = apiED(
735
- profile = testProfile,
736
- text = '{{"mode":"test","file":"{}"}}'.format(str(proPath))
737
- )
738
- else:
739
- raise plugins.PluginException(
740
- "JSON-file '{}' not found.".format(str(proPath))
741
- )
742
- else:
743
- api = EDAPI(
744
- configfile = str(self.configPath),
745
- login = self.getOption('login'),
746
- debug = tdenv.debug,
747
- )
748
- self.edAPI = api
749
-
750
- fs.ensurefolder(tdenv.tmpDir)
751
-
752
- if self.getOption("tdh"):
753
- self.options["save"] = True
754
-
755
- # save profile if requested
756
- if self.getOption("save"):
757
- saveName = 'tdh_profile.json' if self.getOption("tdh") else \
758
- 'profile.' + time.strftime('%Y%m%d_%H%M%S') + '.json'
759
- savePath = tdenv.tmpDir / pathlib.Path(saveName)
760
- if savePath.exists():
761
- savePath.unlink()
762
- with open(savePath, 'w', encoding = "utf-8") as saveFile:
763
- if isinstance(api.text, list):
764
- # since 4.3.0: list(profile, market, shipyard)
765
- tdenv.DEBUG0("{}", api.text)
766
- saveFile.write('{{"profile":{}}}'.format(api.text[0]))
767
- else:
768
- saveFile.write(api.text)
769
- print('API response saved to: {}'.format(savePath))
770
-
771
- # If TDH is calling the plugin, nothing else needs to be done
772
- # now that the file has been created.
773
- if self.getOption("tdh"):
774
- return False
775
-
776
- # Sanity check that the commander is docked. Otherwise we will get a
777
- # mismatch between the last system and last station.
778
- if not api.profile['commander']['docked']:
779
- print('Commander not docked. Aborting!')
780
- return False
781
-
782
- # Figure out where we are.
783
- sysName = api.profile['lastSystem']['name']
784
- stnName = api.profile['lastStarport']['name']
785
- marketId = int(api.profile['lastStarport']['id'])
786
- print('@{}/{} (ID: {})'.format(sysName.upper(), stnName, marketId))
787
-
788
- # Reload the cache.
789
- tdenv.DEBUG0("Checking the cache")
790
- tdb.close()
791
- tdb.reloadCache()
792
- tdb.load(
793
- maxSystemLinkLy = tdenv.maxSystemLinkLy,
794
- )
795
- tdb.close()
796
-
797
- # Check to see if this system is in the database
798
- try:
799
- system = tdb.lookupSystem(sysName)
800
- except LookupError:
801
- raise plugins.PluginException(
802
- "System '{}' unknown.".format(sysName)
803
- )
804
-
805
- # Check to see if this station is in the database
806
- try:
807
- station = tdb.lookupStation(stnName, system)
808
- except LookupError:
809
- station = None
810
-
811
- # New or update station data
812
- station = self.askForStationData(system, stnName = stnName, station = station)
813
-
814
- # If a shipyard exists, make the ship lists
815
- shipCost = {}
816
- shipList = []
817
- eddn_ships = []
818
- if ((station.shipyard == "Y") and ('ships' in api.profile['lastStarport'])):
819
- if 'shipyard_list' in api.profile['lastStarport']['ships']:
820
- if len(api.profile['lastStarport']['ships']['shipyard_list']):
821
- for ship in api.profile['lastStarport']['ships']['shipyard_list'].values():
822
- shipName = shipMap.mapID(ship['id'], ship['name'])
823
- shipCost[shipName] = ship['basevalue']
824
- shipList.append(shipName)
825
- eddn_ships.append(ship['name'])
826
-
827
- if 'unavailable_list' in api.profile['lastStarport']['ships']:
828
- for ship in api.profile['lastStarport']['ships']['unavailable_list']:
829
- shipName = shipMap.mapID(ship['id'], ship['name'])
830
- shipCost[shipName] = ship['basevalue']
831
- shipList.append(shipName)
832
- eddn_ships.append(ship['name'])
833
-
834
- if self.getOption("csvs"):
835
- addShipList = set()
836
- delShipList = set()
837
- addRows = delRows = 0
838
- db = tdb.getDB()
839
- if station.shipyard == "N":
840
- # delete all ships if there is no shipyard
841
- delRows = db.execute(
842
- """
843
- DELETE FROM ShipVendor
844
- WHERE station_id = ?
845
- """,
846
- [station.ID]
847
- ).rowcount
848
-
849
- if len(shipList):
850
- # and now update the shipyard list
851
- # we go through all ships to decide if a ship needs to be
852
- # added or deleted from the shipyard
853
- for shipID in tdb.shipByID:
854
- shipName = tdb.shipByID[shipID].dbname
855
- if shipName in shipList:
856
- # check for ship discount, costTD = 100%
857
- # python builtin round() uses "Round half to even"
858
- # but we need commercial rounding, so we do it ourself
859
- costTD = tdb.shipByID[shipID].cost
860
- costED = int((shipCost[shipName] + 5) / 10) * 10
861
- if costTD != costED:
862
- prozED = int(shipCost[shipName] * 100 / costTD + 0.5) - 100
863
- tdenv.NOTE(
864
- "CostDiff {}: {} != {} ({}%)",
865
- shipName, costTD, costED, prozED
866
- )
867
- # add the ship to the shipyard
868
- shipSQL = (
869
- "INSERT OR IGNORE"
870
- " INTO ShipVendor(station_id, ship_id)"
871
- " VALUES(?, ?)"
872
- )
873
- tdenv.DEBUG0(shipSQL.replace("?", "{}"), station.ID, shipID)
874
- rc = db.execute(shipSQL, [station.ID, shipID]).rowcount
875
- if rc:
876
- addRows += rc
877
- addShipList.add(shipName)
878
- # remove ship from the list
879
- shipList.remove(shipName)
880
- else:
881
- # delete the ship from the shipyard
882
- shipSQL = (
883
- "DELETE FROM ShipVendor"
884
- " WHERE station_id = ?"
885
- " AND ship_id = ?"
886
- )
887
- tdenv.DEBUG0(shipSQL.replace("?", "{}"), station.ID, shipID)
888
- rc = db.execute(shipSQL, [station.ID, shipID]).rowcount
889
- if rc:
890
- delRows += rc
891
- delShipList.add(shipName)
892
-
893
- if len(shipList):
894
- tdenv.WARN("unknown Ship(s): {}", ",".join(shipList))
895
-
896
- db.commit()
897
- if (addRows + delRows) > 0:
898
- if addRows > 0:
899
- tdenv.NOTE(
900
- "Added {} ({}) ships in '{}' shipyard.",
901
- addRows, ", ".join(sorted(addShipList)), station.name()
902
- )
903
- if delRows > 0:
904
- tdenv.NOTE(
905
- "Deleted {} ({}) ships in '{}' shipyard.",
906
- delRows, ", ".join(sorted(delShipList)), station.name()
907
- )
908
- lines, csvPath = csvexport.exportTableToFile(
909
- tdb,
910
- tdenv,
911
- "ShipVendor",
912
- )
913
- tdenv.DEBUG0("{} updated.", csvPath)
914
-
915
- # If a market exists, make the item lists
916
- itemList = []
917
- eddn_market = []
918
- if ((station.market == "Y") and ('commodities' in api.profile['lastStarport'])):
919
- for commodity in api.profile['lastStarport']['commodities']:
920
- if commodity['categoryname'] in cat_ignore:
921
- continue
922
-
923
- if commodity.get('legality', '') != '':
924
- # ignore if present and not empty
925
- continue
926
-
927
- locName = commodity.get('locName', commodity['name'])
928
- itmName = itemMap.mapID(commodity['id'], locName)
929
-
930
- def commodity_int(key):
931
- try:
932
- ret = int(float(commodity[key]) + 0.5)
933
- except (ValueError, KeyError):
934
- ret = 0
935
- return ret
936
-
937
- itmSupply = commodity_int('stock')
938
- itmDemand = commodity_int('demand')
939
- itmSupplyLevel = commodity_int('stockBracket')
940
- itmDemandLevel = commodity_int('demandBracket')
941
- itmBuyPrice = commodity_int('buyPrice')
942
- itmSellPrice = commodity_int('sellPrice')
943
-
944
- if itmSupplyLevel == 0 or itmBuyPrice == 0:
945
- # If there is not stockBracket or buyPrice, ignore stock
946
- itmBuyPrice = 0
947
- itmSupply = 0
948
- itmSupplyLevel = 0
949
- tdSupply = "-"
950
- tdDemand = "{}{}".format(
951
- itmDemand,
952
- bracket_levels[itmDemandLevel]
953
- )
954
- else:
955
- # otherwise don't care about demand
956
- itmDemand = 0
957
- itmDemandLevel = 0
958
- tdDemand = "?"
959
- tdSupply = "{}{}".format(
960
- itmSupply,
961
- bracket_levels[itmSupplyLevel]
962
- )
963
-
964
- # ignore items without supply or demand bracket (TD only)
965
- if itmSupplyLevel > 0 or itmDemandLevel > 0:
966
- itemTD = (
967
- itmName,
968
- itmSellPrice, itmBuyPrice,
969
- tdDemand, tdSupply,
970
- )
971
- itemList.append(itemTD)
972
-
973
- # Populate EDDN
974
- if self.getOption("eddn"):
975
- itemEDDN = {
976
- "name": commodity['name'],
977
- "meanPrice": commodity_int('meanPrice'),
978
- "buyPrice": commodity_int('buyPrice'),
979
- "stock": commodity_int('stock'),
980
- "stockBracket": commodity['stockBracket'],
981
- "sellPrice": commodity_int('sellPrice'),
982
- "demand": commodity_int('demand'),
983
- "demandBracket": commodity['demandBracket'],
984
- }
985
- if len(commodity['statusFlags']) > 0:
986
- itemEDDN["statusFlags"] = commodity['statusFlags']
987
- eddn_market.append(itemEDDN)
988
-
989
- if itemList:
990
- # Create the import file.
991
- with open(self.filename, 'w', encoding = "utf-8") as f:
992
- # write System/Station line
993
- f.write("@ {}/{}\n".format(sysName, stnName))
994
-
995
- # write Item lines (category lines are not needed)
996
- for itemTD in itemList:
997
- f.write("\t\t%s %s %s %s %s\n" % itemTD)
998
-
999
- tdenv.ignoreUnknown = True
1000
- cache.importDataFromFile(
1001
- tdb,
1002
- tdenv,
1003
- pathlib.Path(self.filename),
1004
- )
1005
-
1006
- # Import EDDN
1007
- if self.getOption("eddn"):
1008
- con = EDDN(
1009
- api.profile['commander']['name'],
1010
- self.getOption("name"),
1011
- 'EDAPI Trade Dangerous Plugin',
1012
- __version__
1013
- )
1014
- if self.getOption("test"):
1015
- con._debug = True
1016
- else:
1017
- con._debug = False
1018
-
1019
- if eddn_market:
1020
- print('Posting commodities to EDDN...')
1021
- eddn_additional = {}
1022
- if 'economies' in api.profile['lastStarport']:
1023
- eddn_additional['economies'] = []
1024
- for economy in api.profile['lastStarport']['economies'].values():
1025
- eddn_additional['economies'].append(economy)
1026
- if 'prohibited' in api.profile['lastStarport']:
1027
- eddn_additional['prohibited'] = []
1028
- for item in api.profile['lastStarport']['prohibited'].values():
1029
- eddn_additional['prohibited'].append(item)
1030
-
1031
- con.publishCommodities(
1032
- sysName,
1033
- stnName,
1034
- marketId,
1035
- eddn_market,
1036
- additional = eddn_additional
1037
- )
1038
-
1039
- if eddn_ships:
1040
- print('Posting shipyard to EDDN...')
1041
- con.publishShipyard(
1042
- sysName,
1043
- stnName,
1044
- marketId,
1045
- eddn_ships
1046
- )
1047
-
1048
- if station.outfitting == "Y" and 'modules' in api.profile['lastStarport'] and len(api.profile['lastStarport']['modules']):
1049
- eddn_modules = []
1050
- for module in api.profile['lastStarport']['modules'].values():
1051
- # see: https://github.com/EDSM-NET/EDDN/wiki
1052
- addModule = False
1053
- if module['name'].startswith(('Hpt_', 'Int_')) or module['name'].find('_Armour_') > 0:
1054
- if module.get('sku', None) in (None, 'ELITE_HORIZONS_V_PLANETARY_LANDINGS'):
1055
- if module['name'] != 'Int_PlanetApproachSuite':
1056
- addModule = True
1057
- if addModule:
1058
- eddn_modules.append(module['name'])
1059
- elif self.getOption("test"):
1060
- tdenv.NOTE("Ignored module ID: {}, name: {}", module['id'], module['name'])
1061
- if eddn_modules:
1062
- print('Posting outfitting to EDDN...')
1063
- con.publishOutfitting(
1064
- sysName,
1065
- stnName,
1066
- marketId,
1067
- sorted(eddn_modules)
1068
- )
1069
-
1070
- # We did all the work
1071
- return False