1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-21 11:27:38 +03:00

Merge pull request from EDCD/enhancement/945/CAPI-threaded-queries

Change all CAPI queries over to a queue/thread model
This commit is contained in:
Athanasius 2021-08-31 14:27:48 +01:00 committed by GitHub
commit 4482da007f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 856 additions and 481 deletions

@ -247,10 +247,10 @@ from edmc_data import DEBUG_WEBSERVER_HOST, DEBUG_WEBSERVER_PORT
TARGET_URL = 'https://www.edsm.net/api-journal-v1'
if 'edsm' in debug_senders:
TARGET_URL = f'http://{DEBUG_WEBSERVER_HOST}:{DEBUG_WEBSERVER_PORT}/edsm'
TARGET_URL = f'http://{DEBUG_WEBSERVER_HOST}:{DEBUG_WEBSERVER_PORT}/edsm'
...
r = this.session.post(TARGET_URL, data=data, timeout=_TIMEOUT)
r = this.requests_session.post(TARGET_URL, data=data, timeout=_TIMEOUT)
```
Be sure to set a URL path in the `TARGET_URL` that denotes where the data

91
EDMC.py

@ -6,6 +6,7 @@ import argparse
import json
import locale
import os
import queue
import re
import sys
from os.path import getmtime, join
@ -270,8 +271,38 @@ sys.path: {sys.path}'''
companion.session.login(monitor.cmdr, monitor.is_beta)
###################################################################
# Initiate CAPI queries
querytime = int(time())
data = companion.session.station()
companion.session.station(query_time=querytime)
# Wait for the response
_capi_request_timeout = 60
try:
capi_response = companion.session.capi_response_queue.get(
block=True, timeout=_capi_request_timeout
)
except queue.Empty:
logger.error(f'CAPI requests timed out after {_capi_request_timeout} seconds')
sys.exit(EXIT_SERVER)
###################################################################
# noinspection DuplicatedCode
if isinstance(capi_response, companion.EDMCCAPIFailedRequest):
logger.trace_if('capi.worker', f'Failed Request: {capi_response.message}')
if capi_response.exception:
raise capi_response.exception
else:
raise ValueError(capi_response.message)
logger.trace_if('capi.worker', 'Answer is not a Failure')
if not isinstance(capi_response, companion.EDMCCAPIResponse):
raise ValueError(f"Response was neither CAPIFailedRequest nor EDMCAPIResponse: {type(capi_response)}")
data = capi_response.capi_data
config.set('querytime', querytime)
# Validation
@ -279,10 +310,10 @@ sys.path: {sys.path}'''
logger.error("No data['command']['name'] from CAPI")
sys.exit(EXIT_SERVER)
elif not deep_get(data, 'lastSystem', 'name') or \
data['commander'].get('docked') and not \
deep_get(data, 'lastStarport', 'name'): # Only care if docked
elif (
not deep_get(data, 'lastSystem', 'name') or
data['commander'].get('docked') and not deep_get(data, 'lastStarport', 'name')
): # Only care if docked
logger.error("No data['lastSystem']['name'] from CAPI")
sys.exit(EXIT_SERVER)
@ -294,21 +325,20 @@ sys.path: {sys.path}'''
pass # Skip further validation
elif data['commander']['name'] != monitor.cmdr:
logger.error(f'Commander "{data["commander"]["name"]}" from CAPI doesn\'t match "{monitor.cmdr}" from Journal') # noqa: E501
sys.exit(EXIT_CREDENTIALS)
raise companion.CmdrError()
elif data['lastSystem']['name'] != monitor.system or \
((data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.station) or \
data['ship']['id'] != monitor.state['ShipID'] or \
data['ship']['name'].lower() != monitor.state['ShipType']:
logger.error('Mismatch(es) between CAPI and Journal for at least one of: StarSystem, Last Star Port, Ship ID or Ship Name/Type') # noqa: E501
sys.exit(EXIT_LAGGING)
elif (
data['lastSystem']['name'] != monitor.system or
((data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.station) or
data['ship']['id'] != monitor.state['ShipID'] or
data['ship']['name'].lower() != monitor.state['ShipType']
):
raise companion.ServerLagging()
# stuff we can do when not docked
if args.d:
logger.debug(f'Writing raw JSON data to "{args.d}"')
out = json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': '))
out = json.dumps(dict(data), ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': '))
with open(args.d, 'wb') as f:
f.write(out.encode("utf-8"))
@ -408,21 +438,44 @@ sys.path: {sys.path}'''
try:
eddn_sender = eddn.EDDN(None)
logger.debug('Sending Market, Outfitting and Shipyard data to EDDN...')
eddn_sender.export_commodities(data, monitor.is_beta)
eddn_sender.export_outfitting(data, monitor.is_beta)
eddn_sender.export_shipyard(data, monitor.is_beta)
eddn_sender.export_commodities(data, monitor.is_beta, monitor.state['Odyssey'])
eddn_sender.export_outfitting(data, monitor.is_beta, monitor.state['Odyssey'])
eddn_sender.export_shipyard(data, monitor.is_beta, monitor.state['Odyssey'])
except Exception:
logger.exception('Failed to send data to EDDN')
except companion.ServerError:
logger.error('Frontier CAPI Server returned an error')
logger.exception('Frontier CAPI Server returned an error')
sys.exit(EXIT_SERVER)
except companion.ServerConnectionError:
logger.exception('Exception while contacting server')
sys.exit(EXIT_SERVER)
except companion.CredentialsError:
logger.error('Frontier CAPI Server: Invalid Credentials')
sys.exit(EXIT_CREDENTIALS)
# Companion API problem
except companion.ServerLagging:
logger.error(
'Mismatch(es) between CAPI and Journal for at least one of: '
'StarSystem, Last Star Port, Ship ID or Ship Name/Type'
)
sys.exit(EXIT_SERVER)
except companion.CmdrError: # Companion API return doesn't match Journal
logger.error(
f'Commander "{data["commander"]["name"]}" from CAPI doesn\'t match '
f'"{monitor.cmdr}" from Journal'
)
sys.exit(EXIT_SERVER)
except Exception:
logger.exception('"other" exception')
sys.exit(EXIT_SERVER)
if __name__ == '__main__':
main()

@ -4,9 +4,9 @@
import argparse
import html
import json
import locale
import pathlib
import queue
import re
import sys
# import threading
@ -16,7 +16,7 @@ from os import chdir, environ
from os.path import dirname, join
from sys import platform
from time import localtime, strftime, time
from typing import TYPE_CHECKING, Optional, Tuple
from typing import TYPE_CHECKING, Optional, Tuple, Union
# Have this as early as possible for people running EDMarketConnector.exe
# from cmd.exe or a bat file or similar. Else they might not be in the correct
@ -374,6 +374,7 @@ SHIPYARD_HTML_TEMPLATE = """
class AppWindow(object):
"""Define the main application window."""
_CAPI_RESPONSE_TK_EVENT_NAME = '<<CAPIResponse>>'
# Tkinter Event types
EVENT_KEYPRESS = 2
EVENT_BUTTON = 4
@ -383,13 +384,16 @@ class AppWindow(object):
def __init__(self, master: tk.Tk): # noqa: C901, CCR001 # TODO - can possibly factor something out
self.holdofftime = config.get_int('querytime', default=0) + companion.holdoff
self.capi_query_holdoff_time = config.get_int('querytime', default=0) + companion.capi_query_cooldown
self.w = master
self.w.title(applongname)
self.w.rowconfigure(0, weight=1)
self.w.columnconfigure(0, weight=1)
# companion needs to be able to send <<CAPIResponse>> events
companion.session.set_tk_master(self.w)
self.prefsdialog = None
# if platform == 'win32':
@ -485,8 +489,8 @@ class AppWindow(object):
theme.register_alternate((self.button, self.theme_button, self.theme_button),
{'row': ui_row, 'columnspan': 2, 'sticky': tk.NSEW})
self.status.grid(columnspan=2, sticky=tk.EW)
self.button.bind('<Button-1>', self.getandsend)
theme.button_bind(self.theme_button, self.getandsend)
self.button.bind('<Button-1>', self.capi_request_data)
theme.button_bind(self.theme_button, self.capi_request_data)
for child in frame.winfo_children():
child.grid_configure(padx=self.PADX, pady=(platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0)
@ -646,9 +650,10 @@ class AppWindow(object):
self.w.bind('<FocusIn>', self.onenter) # Special handling for transparency
self.w.bind('<Leave>', self.onleave) # Special handling for transparency
self.w.bind('<FocusOut>', self.onleave) # Special handling for transparency
self.w.bind('<Return>', self.getandsend)
self.w.bind('<KP_Enter>', self.getandsend)
self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring
self.w.bind('<Return>', self.capi_request_data)
self.w.bind('<KP_Enter>', self.capi_request_data)
self.w.bind_all('<<Invoke>>', self.capi_request_data) # Ask for CAPI queries to be performed
self.w.bind_all(self._CAPI_RESPONSE_TK_EVENT_NAME, self.capi_handle_response)
self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring
self.w.bind_all('<<DashboardEvent>>', self.dashboard_event) # Dashboard monitoring
self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar
@ -886,32 +891,58 @@ class AppWindow(object):
return True
def getandsend(self, event=None, retrying: bool = False): # noqa: C901, CCR001
def capi_request_data(self, event=None) -> None:
"""
Perform CAPI data retrieval and associated actions.
This can be triggered by hitting the main UI 'Update' button,
automatically on docking, or due to a retry.
:param event: Tk generated event details.
"""
logger.trace_if('capi.worker', 'Begin')
auto_update = not event
play_sound = (auto_update or int(event.type) == self.EVENT_VIRTUAL) and not config.get_int('hotkey_mute')
play_bad = False
err: Optional[str] = None
if (
not monitor.cmdr or not monitor.mode or monitor.state['Captain']
or not monitor.system or monitor.mode == 'CQC'
):
return # In CQC or on crew - do nothing
if not monitor.cmdr:
logger.trace_if('capi.worker', 'Aborting Query: Cmdr unknown')
# LANG: CAPI queries aborted because Cmdr name is unknown
self.status['text'] = _('CAPI query aborted: Cmdr name unknown')
return
if not monitor.mode:
logger.trace_if('capi.worker', 'Aborting Query: Game Mode unknown')
# LANG: CAPI queries aborted because game mode unknown
self.status['text'] = _('CAPI query aborted: Game mode unknown')
return
if not monitor.system:
logger.trace_if('capi.worker', 'Aborting Query: Current star system unknown')
# LANG: CAPI queries aborted because current star system name unknown
self.status['text'] = _('CAPI query aborted: Current system unknown')
return
if monitor.state['Captain']:
logger.trace_if('capi.worker', 'Aborting Query: In multi-crew')
# LANG: CAPI queries aborted because player is in multi-crew on other Cmdr's ship
self.status['text'] = _('CAPI query aborted: In other-ship multi-crew')
return
if monitor.mode == 'CQC':
logger.trace_if('capi.worker', 'Aborting Query: In CQC')
# LANG: CAPI queries aborted because player is in CQC (Arena)
self.status['text'] = _('CAPI query aborted: CQC (Arena) detected')
return
if companion.session.state == companion.Session.STATE_AUTH:
logger.trace_if('capi.worker', 'Auth in progress? Aborting query')
# Attempt another Auth
self.login()
return
if not retrying:
if time() < self.holdofftime: # Was invoked by key while in cooldown
if play_sound and (self.holdofftime - time()) < companion.holdoff * 0.75:
if not companion.session.retrying:
if time() < self.capi_query_holdoff_time: # Was invoked by key while in cooldown
if play_sound and (self.capi_query_holdoff_time - time()) < companion.capi_query_cooldown * 0.75:
self.status['text'] = ''
hotkeymgr.play_bad() # Don't play sound in first few seconds to prevent repeats
@ -925,56 +956,91 @@ class AppWindow(object):
self.button['state'] = self.theme_button['state'] = tk.DISABLED
self.w.update_idletasks()
query_time = int(time())
logger.trace_if('capi.worker', 'Requesting full station data')
config.set('querytime', query_time)
logger.trace_if('capi.worker', 'Calling companion.session.station')
companion.session.station(
query_time=query_time, tk_response_event=self._CAPI_RESPONSE_TK_EVENT_NAME,
play_sound=play_sound
)
def capi_handle_response(self, event=None): # noqa: C901, CCR001
"""Handle the resulting data from a CAPI query."""
logger.trace_if('capi.worker', 'Handling response')
play_bad: bool = False
err: Optional[str] = None
capi_response: Union[companion.EDMCCAPIFailedRequest, companion.EDMCCAPIResponse]
try:
querytime = int(time())
data = companion.session.station()
config.set('querytime', querytime)
logger.trace_if('capi.worker', 'Pulling answer off queue')
capi_response = companion.session.capi_response_queue.get(block=False)
if isinstance(capi_response, companion.EDMCCAPIFailedRequest):
logger.trace_if('capi.worker', f'Failed Request: {capi_response.message}')
if capi_response.exception:
raise capi_response.exception
else:
raise ValueError(capi_response.message)
logger.trace_if('capi.worker', 'Answer is not a Failure')
if not isinstance(capi_response, companion.EDMCCAPIResponse):
msg = f'Response was neither CAPIFailedRequest nor EDMCAPIResponse: {type(capi_response)}'
logger.error(msg)
raise ValueError(msg)
# Validation
if 'commander' not in data:
if 'commander' not in capi_response.capi_data:
# This can happen with EGS Auth if no commander created yet
# LANG: No data was returned for the commander from the Frontier CAPI
err = self.status['text'] = _('CAPI: No commander data returned')
elif not data.get('commander', {}).get('name'):
elif not capi_response.capi_data.get('commander', {}).get('name'):
# LANG: We didn't have the commander name when we should have
err = self.status['text'] = _("Who are you?!") # Shouldn't happen
elif (not data.get('lastSystem', {}).get('name')
or (data['commander'].get('docked')
and not data.get('lastStarport', {}).get('name'))):
elif (not capi_response.capi_data.get('lastSystem', {}).get('name')
or (capi_response.capi_data['commander'].get('docked')
and not capi_response.capi_data.get('lastStarport', {}).get('name'))):
# LANG: We don't know where the commander is, when we should
err = self.status['text'] = _("Where are you?!") # Shouldn't happen
elif not data.get('ship', {}).get('name') or not data.get('ship', {}).get('modules'):
elif (
not capi_response.capi_data.get('ship', {}).get('name')
or not capi_response.capi_data.get('ship', {}).get('modules')
):
# LANG: We don't know what ship the commander is in, when we should
err = self.status['text'] = _("What are you flying?!") # Shouldn't happen
elif monitor.cmdr and data['commander']['name'] != monitor.cmdr:
elif monitor.cmdr and capi_response.capi_data['commander']['name'] != monitor.cmdr:
# Companion API Commander doesn't match Journal
logger.trace_if('capi.worker', 'Raising CmdrError()')
raise companion.CmdrError()
elif auto_update and not monitor.state['OnFoot'] and not data['commander'].get('docked'):
elif (
capi_response.auto_update and not monitor.state['OnFoot']
and not capi_response.capi_data['commander'].get('docked')
):
# auto update is only when just docked
logger.warning(f"{auto_update!r} and not {monitor.state['OnFoot']!r} and "
f"not {data['commander'].get('docked')!r}")
logger.warning(f"{capi_response.auto_update!r} and not {monitor.state['OnFoot']!r} and "
f"not {capi_response.capi_data['commander'].get('docked')!r}")
raise companion.ServerLagging()
elif data['lastSystem']['name'] != monitor.system:
elif capi_response.capi_data['lastSystem']['name'] != monitor.system:
# CAPI system must match last journal one
logger.warning(f"{data['lastSystem']['name']!r} != {monitor.system!r}")
logger.warning(f"{capi_response.capi_data['lastSystem']['name']!r} != {monitor.system!r}")
raise companion.ServerLagging()
elif data['lastStarport']['name'] != monitor.station:
elif capi_response.capi_data['lastStarport']['name'] != monitor.station:
if monitor.state['OnFoot'] and monitor.station:
logger.warning(f"({data['lastStarport']['name']!r} != {monitor.station!r}) AND "
logger.warning(f"({capi_response.capi_data['lastStarport']['name']!r} != {monitor.station!r}) AND "
f"{monitor.state['OnFoot']!r} and {monitor.station!r}")
raise companion.ServerLagging()
else:
last_station = None
if data['commander']['docked']:
last_station = data['lastStarport']['name']
if capi_response.capi_data['commander']['docked']:
last_station = capi_response.capi_data['lastStarport']['name']
if monitor.station is None:
# Likely (re-)Embarked on ship docked at an EDO settlement.
@ -986,32 +1052,40 @@ class AppWindow(object):
if last_station != monitor.station:
# CAPI lastStarport must match
logger.warning(f"({data['lastStarport']['name']!r} != {monitor.station!r}) AND "
f"{last_station!r} != {monitor.station!r}")
logger.warning(f"({capi_response.capi_data['lastStarport']['name']!r} != {monitor.station!r})"
f" AND {last_station!r} != {monitor.station!r}")
raise companion.ServerLagging()
self.holdofftime = querytime + companion.holdoff
self.capi_query_holdoff_time = capi_response.query_time + companion.capi_query_cooldown
elif not monitor.state['OnFoot'] and data['ship']['id'] != monitor.state['ShipID']:
elif not monitor.state['OnFoot'] and capi_response.capi_data['ship']['id'] != monitor.state['ShipID']:
# CAPI ship must match
logger.warning(f"not {monitor.state['OnFoot']!r} and "
f"{data['ship']['id']!r} != {monitor.state['ShipID']!r}")
f"{capi_response.capi_data['ship']['id']!r} != {monitor.state['ShipID']!r}")
raise companion.ServerLagging()
elif not monitor.state['OnFoot'] and data['ship']['name'].lower() != monitor.state['ShipType']:
elif (
not monitor.state['OnFoot']
and capi_response.capi_data['ship']['name'].lower() != monitor.state['ShipType']
):
# CAPI ship type must match
logger.warning(f"not {monitor.state['OnFoot']!r} and "
f"{data['ship']['name'].lower()!r} != {monitor.state['ShipType']!r}")
f"{capi_response.capi_data['ship']['name'].lower()!r} != "
f"{monitor.state['ShipType']!r}")
raise companion.ServerLagging()
else:
# TODO: Change to depend on its own CL arg
if __debug__: # Recording
companion.session.dump_capi_data(data)
companion.session.dump_capi_data(capi_response.capi_data)
if not monitor.state['ShipType']: # Started game in SRV or fighter
self.ship['text'] = ship_name_map.get(data['ship']['name'].lower(), data['ship']['name'])
monitor.state['ShipID'] = data['ship']['id']
monitor.state['ShipType'] = data['ship']['name'].lower()
self.ship['text'] = ship_name_map.get(
capi_response.capi_data['ship']['name'].lower(),
capi_response.capi_data['ship']['name']
)
monitor.state['ShipID'] = capi_response.capi_data['ship']['id']
monitor.state['ShipType'] = capi_response.capi_data['ship']['name'].lower()
if not monitor.state['Modules']:
self.ship.configure(state=tk.DISABLED)
@ -1021,45 +1095,62 @@ class AppWindow(object):
self.ship.configure(state=True)
if monitor.state.get('SuitCurrent') is not None:
if (loadout := data.get('loadout')) is not None:
if (loadout := capi_response.capi_data.get('loadout')) is not None:
if (suit := loadout.get('suit')) is not None:
if (suitname := suit.get('edmcName')) is not None:
# We've been paranoid about loadout->suit->suitname, now just assume loadouts is there
loadout_name = index_possibly_sparse_list(
data['loadouts'], loadout['loadoutSlotId']
capi_response.capi_data['loadouts'], loadout['loadoutSlotId']
)['name']
self.suit['text'] = f'{suitname} ({loadout_name})'
self.suit_show_if_set()
# Update Odyssey Suit data
companion.session.suit_update(capi_response.capi_data)
if data['commander'].get('credits') is not None:
monitor.state['Credits'] = data['commander']['credits']
monitor.state['Loan'] = data['commander'].get('debt', 0)
if capi_response.capi_data['commander'].get('credits') is not None:
monitor.state['Credits'] = capi_response.capi_data['commander']['credits']
monitor.state['Loan'] = capi_response.capi_data['commander'].get('debt', 0)
# stuff we can do when not docked
err = plug.notify_newdata(data, monitor.is_beta)
err = plug.notify_newdata(capi_response.capi_data, monitor.is_beta)
self.status['text'] = err and err or ''
if err:
play_bad = True
# Export market data
if not self.export_market_data(data):
if not self.export_market_data(capi_response.capi_data):
err = 'Error: Exporting Market data'
play_bad = True
self.holdofftime = querytime + companion.holdoff
self.capi_query_holdoff_time = capi_response.query_time + companion.capi_query_cooldown
except queue.Empty:
logger.error('There was no response in the queue!')
# TODO: Set status text
return
except companion.ServerConnectionError:
self.status['text'] = _('Frontier CAPI server error')
except companion.CredentialsError:
companion.session.retrying = False
companion.session.invalidate()
companion.session.login()
return # We need to give Auth time to complete, so can't set a timed retry
# Companion API problem
except companion.ServerLagging as e:
err = str(e)
if retrying:
if companion.session.retrying:
self.status['text'] = err
play_bad = True
else:
# Retry once if Companion server is unresponsive
self.w.after(int(SERVER_RETRY * 1000), lambda: self.getandsend(event, True))
companion.session.retrying = True
self.w.after(int(SERVER_RETRY * 1000), lambda: self.capi_request_data(event))
return # early exit to avoid starting cooldown count
except companion.CmdrError as e: # Companion API return doesn't match Journal
@ -1080,14 +1171,16 @@ class AppWindow(object):
if not err: # not self.status['text']: # no errors
# LANG: Time when we last obtained Frontier CAPI data
self.status['text'] = strftime(_('Last updated at %H:%M:%S'), localtime(querytime))
self.status['text'] = strftime(_('Last updated at %H:%M:%S'), localtime(capi_response.query_time))
if play_sound and play_bad:
if capi_response.play_sound and play_bad:
hotkeymgr.play_bad()
logger.trace_if('capi.worker', 'Updating suit and cooldown...')
self.update_suit_text()
self.suit_show_if_set()
self.cooldown()
logger.trace_if('capi.worker', '...done')
def journal_event(self, event): # noqa: C901, CCR001 # Currently not easily broken up.
"""
@ -1222,7 +1315,7 @@ class AppWindow(object):
logger.info('Monitor: Disable WinSparkle automatic update checks')
# Can't start dashboard monitoring
if not dashboard.start(self.w, monitor.started):
if not dashboard.start_frontier_auth(self.w, monitor.started):
logger.info("Can't start Status monitoring")
# Export loadout
@ -1264,7 +1357,7 @@ class AppWindow(object):
auto_update = True
if auto_update:
self.w.after(int(SERVER_RETRY * 1000), self.getandsend)
self.w.after(int(SERVER_RETRY * 1000), self.capi_request_data)
if entry['event'] == 'ShutDown':
# Enable WinSparkle automatic update checks
@ -1363,10 +1456,12 @@ class AppWindow(object):
def cooldown(self) -> None:
"""Display and update the cooldown timer for 'Update' button."""
if time() < self.holdofftime:
if time() < self.capi_query_holdoff_time:
# Update button in main window
self.button['text'] = self.theme_button['text'] \
= _('cooldown {SS}s').format(SS=int(self.holdofftime - time())) # LANG: Cooldown on 'Update' button
= _('cooldown {SS}s').format( # LANG: Cooldown on 'Update' button
SS=int(self.capi_query_holdoff_time - time())
)
self.w.after(1000, self.cooldown)
else:
@ -1494,47 +1589,32 @@ class AppWindow(object):
self.destroy()
self.__class__.showing = False
def save_raw(self) -> None: # noqa: CCR001 # Not easily broken up.
"""Save newly acquired CAPI data in the configured file."""
# LANG: Status - Attempting to retrieve data from Frontier CAPI to save to file
self.status['text'] = _('Fetching data...')
self.w.update_idletasks()
def save_raw(self) -> None:
"""
Save any CAPI data already acquired to a file.
try:
data: CAPIData = companion.session.station()
self.status['text'] = ''
default_extension: str = ''
This specifically does *not* cause new queries to be performed, as the
purpose is to aid in diagnosing any issues that occurred during 'normal'
queries.
"""
default_extension: str = ''
if platform == 'darwin':
default_extension = '.json'
if platform == 'darwin':
default_extension = '.json'
last_system: str = data.get("lastSystem", {}).get("name", "Unknown")
last_starport: str = ''
timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime())
f = tkinter.filedialog.asksaveasfilename(
parent=self.w,
defaultextension=default_extension,
filetypes=[('JSON', '.json'), ('All Files', '*')],
initialdir=config.get_str('outdir'),
initialfile=f'{monitor.system}{monitor.station}.{timestamp}'
)
if not f:
return
if data['commander'].get('docked'):
last_starport = '.' + data.get('lastStarport', {}).get('name', 'Unknown')
timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime())
f = tkinter.filedialog.asksaveasfilename(
parent=self.w,
defaultextension=default_extension,
filetypes=[('JSON', '.json'), ('All Files', '*')],
initialdir=config.get_str('outdir'),
initialfile=f'{last_system}{last_starport}.{timestamp}'
)
if f:
with open(f, 'wb') as h:
h.write(json.dumps(dict(data),
ensure_ascii=False,
indent=2,
sort_keys=True,
separators=(',', ': ')).encode('utf-8'))
except companion.ServerError as e:
self.status['text'] = str(e)
except Exception as e:
logger.debug('"other" exception', exc_info=e)
self.status['text'] = str(e)
with open(f, 'wb') as h:
h.write(str(companion.session.capi_raw_data).encode(encoding='utf-8'))
# def exit_tray(self, systray: 'SysTrayIcon') -> None:
# """Tray icon is shutting down."""
@ -1577,6 +1657,10 @@ class AppWindow(object):
logger.info('Unregistering hotkey manager...')
hotkeymgr.unregister()
# Now the CAPI query thread
logger.info('Closing CAPI query thread...')
companion.session.capi_query_close_worker()
# Now the main programmatic input methods
logger.info('Closing dashboard...')
dashboard.close()
@ -1617,7 +1701,7 @@ class AppWindow(object):
def oniconify(self, event=None) -> None:
"""Handle minimization of the application."""
self.w.overrideredirect(0) # Can't iconize while overrideredirect
self.w.overrideredirect(False) # Can't iconize while overrideredirect
self.w.iconify()
self.w.update_idletasks() # Size and windows styles get recalculated here
self.w.wait_visibility() # Need main window to be re-created before returning

@ -1,172 +1,175 @@
/* Language name */
"!Language" = "English";
/* companion.py: Frontier CAPI didn't respond; In files: companion.py:171; */
/* companion.py: Frontier CAPI didn't respond; In files: companion.py:212; */
"Error: Frontier CAPI didn't respond" = "Error: Frontier CAPI didn't respond";
/* companion.py: Frontier CAPI data doesn't agree with latest Journal game location; In files: companion.py:190; */
/* companion.py: Frontier CAPI data doesn't agree with latest Journal game location; In files: companion.py:231; */
"Error: Frontier server is lagging" = "Error: Frontier server is lagging";
/* companion.py: Commander is docked at an EDO settlement, got out and back in, we forgot the station; In files: companion.py:205; */
/* companion.py: Commander is docked at an EDO settlement, got out and back in, we forgot the station; In files: companion.py:247; */
"Docked but unknown station: EDO Settlement?" = "Docked but unknown station: EDO Settlement?";
/* companion.py: Generic "something went wrong with Frontier Auth" error; In files: companion.py:214; */
/* companion.py: Generic "something went wrong with Frontier Auth" error; In files: companion.py:257; */
"Error: Invalid Credentials" = "Error: Invalid Credentials";
/* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:230; */
/* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:273; */
"Error: Wrong Cmdr" = "Error: Wrong Cmdr";
/* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:341; companion.py:422; */
/* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:397; companion.py:482; */
"Error" = "Error";
/* companion.py: Frontier auth, no 'usr' section in returned data; companion.py: Frontier auth, no 'customer_id' in 'usr' section in returned data; In files: companion.py:380; companion.py:385; */
/* companion.py: Frontier auth, no 'usr' section in returned data; companion.py: Frontier auth, no 'customer_id' in 'usr' section in returned data; In files: companion.py:440; companion.py:445; */
"Error: Couldn't check token customer_id" = "Error: Couldn't check token customer_id";
/* companion.py: Frontier auth customer_id doesn't match game session FID; In files: companion.py:391; */
/* companion.py: Frontier auth customer_id doesn't match game session FID; In files: companion.py:451; */
"Error: customer_id doesn't match!" = "Error: customer_id doesn't match!";
/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:413; */
/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:473; */
"Error: unable to get token" = "Error: unable to get token";
/* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:575; */
/* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:774; */
"Frontier CAPI query failure" = "Frontier CAPI query failure";
/* companion.py: Frontier CAPI data retrieval failed with 5XX code; In files: companion.py:591; */
/* companion.py: Frontier CAPI data retrieval failed with 5XX code; In files: companion.py:786; */
"Frontier CAPI server error" = "Frontier CAPI server error";
/* EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:424; EDMarketConnector.py:718; EDMarketConnector.py:1302; */
/* EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:485; EDMarketConnector.py:780; EDMarketConnector.py:1440; */
"Update" = "Update";
/* EDMarketConnector.py: Appearance - Label for checkbox to select if application always on top; prefs.py: Appearance - Label for checkbox to select if application always on top; In files: EDMarketConnector.py:507; prefs.py:843; */
/* EDMarketConnector.py: Appearance - Label for checkbox to select if application always on top; prefs.py: Appearance - Label for checkbox to select if application always on top; In files: EDMarketConnector.py:568; prefs.py:866; */
"Always on top" = "Always on top";
/* EDMarketConnector.py: Unknown suit; In files: EDMarketConnector.py:636; */
/* EDMarketConnector.py: Unknown suit; In files: EDMarketConnector.py:698; */
"Unknown" = "Unknown";
/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:705; */
/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:767; */
"Error: Check E:D journal file location" = "Error: Check E:D journal file location";
/* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:712; edsm.py:219; stats.py:50; theme.py:227; */
/* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:774; edsm.py:257; stats.py:50; theme.py:227; */
"Cmdr" = "Cmdr";
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: Multicrew role label in main window; In files: EDMarketConnector.py:714; EDMarketConnector.py:1072; */
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: Multicrew role label in main window; In files: EDMarketConnector.py:776; EDMarketConnector.py:1199; */
"Role" = "Role";
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: 'Ship' label in main UI; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:714; EDMarketConnector.py:1082; EDMarketConnector.py:1105; stats.py:363; */
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: 'Ship' label in main UI; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:776; EDMarketConnector.py:1209; EDMarketConnector.py:1232; stats.py:365; */
"Ship" = "Ship";
/* EDMarketConnector.py: Label for 'Suit' line in main UI; In files: EDMarketConnector.py:715; */
/* EDMarketConnector.py: Label for 'Suit' line in main UI; In files: EDMarketConnector.py:777; */
"Suit" = "Suit";
/* EDMarketConnector.py: Label for 'System' line in main UI; prefs.py: Configuration - Label for selection of 'System' provider website; stats.py: Main window; In files: EDMarketConnector.py:716; prefs.py:605; stats.py:365; */
/* EDMarketConnector.py: Label for 'System' line in main UI; prefs.py: Configuration - Label for selection of 'System' provider website; stats.py: Main window; In files: EDMarketConnector.py:778; prefs.py:606; stats.py:367; */
"System" = "System";
/* EDMarketConnector.py: Label for 'Station' line in main UI; prefs.py: Configuration - Label for selection of 'Station' provider website; prefs.py: Appearance - Example 'Normal' text; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:717; prefs.py:623; prefs.py:738; stats.py:366; */
/* EDMarketConnector.py: Label for 'Station' line in main UI; prefs.py: Configuration - Label for selection of 'Station' provider website; prefs.py: Appearance - Example 'Normal' text; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:779; prefs.py:624; prefs.py:761; stats.py:368; */
"Station" = "Station";
/* EDMarketConnector.py: 'File' menu title on OSX; EDMarketConnector.py: 'File' menu title; EDMarketConnector.py: 'File' menu; In files: EDMarketConnector.py:720; EDMarketConnector.py:735; EDMarketConnector.py:738; EDMarketConnector.py:1791; */
/* EDMarketConnector.py: 'File' menu title on OSX; EDMarketConnector.py: 'File' menu title; EDMarketConnector.py: 'File' menu; In files: EDMarketConnector.py:782; EDMarketConnector.py:797; EDMarketConnector.py:800; EDMarketConnector.py:1917; */
"File" = "File";
/* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:721; EDMarketConnector.py:736; EDMarketConnector.py:739; */
/* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:783; EDMarketConnector.py:798; EDMarketConnector.py:801; */
"Edit" = "Edit";
/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:722; */
/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:784; */
"View" = "View";
/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:723; */
/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:785; */
"Window" = "Window";
/* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:724; EDMarketConnector.py:737; EDMarketConnector.py:740; */
/* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:786; EDMarketConnector.py:799; EDMarketConnector.py:802; */
"Help" = "Help";
/* EDMarketConnector.py: App menu entry on OSX; EDMarketConnector.py: Help > About App; In files: EDMarketConnector.py:727; EDMarketConnector.py:753; EDMarketConnector.py:1347; */
/* EDMarketConnector.py: App menu entry on OSX; EDMarketConnector.py: Help > About App; In files: EDMarketConnector.py:789; EDMarketConnector.py:815; EDMarketConnector.py:1486; */
"About {APP}" = "About {APP}";
/* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:729; EDMarketConnector.py:752; */
/* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:791; EDMarketConnector.py:814; */
"Check for Updates..." = "Check for Updates...";
/* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:730; EDMarketConnector.py:744; */
/* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:792; EDMarketConnector.py:806; */
"Save Raw Data..." = "Save Raw Data...";
/* EDMarketConnector.py: File > Status; stats.py: Status dialog title; In files: EDMarketConnector.py:731; EDMarketConnector.py:743; stats.py:360; */
/* EDMarketConnector.py: File > Status; stats.py: Status dialog title; In files: EDMarketConnector.py:793; EDMarketConnector.py:805; stats.py:362; */
"Status" = "Status";
/* EDMarketConnector.py: Help > Privacy Policy; In files: EDMarketConnector.py:732; EDMarketConnector.py:750; */
/* EDMarketConnector.py: Help > Privacy Policy; In files: EDMarketConnector.py:794; EDMarketConnector.py:812; */
"Privacy Policy" = "Privacy Policy";
/* EDMarketConnector.py: Help > Release Notes; In files: EDMarketConnector.py:733; EDMarketConnector.py:751; EDMarketConnector.py:1381; */
/* EDMarketConnector.py: Help > Release Notes; In files: EDMarketConnector.py:795; EDMarketConnector.py:813; EDMarketConnector.py:1520; */
"Release Notes" = "Release Notes";
/* EDMarketConnector.py: File > Settings; prefs.py: File > Settings (macOS); In files: EDMarketConnector.py:745; EDMarketConnector.py:1792; prefs.py:254; */
/* EDMarketConnector.py: File > Settings; prefs.py: File > Settings (macOS); In files: EDMarketConnector.py:807; EDMarketConnector.py:1918; prefs.py:254; */
"Settings" = "Settings";
/* EDMarketConnector.py: File > Exit; In files: EDMarketConnector.py:746; */
/* EDMarketConnector.py: File > Exit; In files: EDMarketConnector.py:808; */
"Exit" = "Exit";
/* EDMarketConnector.py: Help > Documentation; In files: EDMarketConnector.py:749; */
/* EDMarketConnector.py: Help > Documentation; In files: EDMarketConnector.py:811; */
"Documentation" = "Documentation";
/* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:756; ttkHyperlinkLabel.py:42; */
/* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:818; ttkHyperlinkLabel.py:42; */
"Copy" = "Copy";
/* EDMarketConnector.py: Status - Attempting to get a Frontier Auth Access Token; In files: EDMarketConnector.py:762; */
/* EDMarketConnector.py: Status - Attempting to get a Frontier Auth Access Token; In files: EDMarketConnector.py:824; */
"Logging in..." = "Logging in...";
/* EDMarketConnector.py: Successfully authenticated with the Frontier website; In files: EDMarketConnector.py:778; EDMarketConnector.py:1215; */
/* EDMarketConnector.py: Successfully authenticated with the Frontier website; In files: EDMarketConnector.py:840; EDMarketConnector.py:1351; */
"Authentication successful" = "Authentication successful";
/* EDMarketConnector.py: Player is not docked at a station, when we expect them to be; In files: EDMarketConnector.py:809; */
/* EDMarketConnector.py: Player is not docked at a station, when we expect them to be; In files: EDMarketConnector.py:871; */
"You're not docked at a station!" = "You're not docked at a station!";
/* EDMarketConnector.py: Status - Either no market or no modules data for station from Frontier CAPI; In files: EDMarketConnector.py:817; */
/* EDMarketConnector.py: Status - Either no market or no modules data for station from Frontier CAPI; In files: EDMarketConnector.py:879; */
"Station doesn't have anything!" = "Station doesn't have anything!";
/* EDMarketConnector.py: Status - No station market data from Frontier CAPI; In files: EDMarketConnector.py:822; */
/* EDMarketConnector.py: Status - No station market data from Frontier CAPI; In files: EDMarketConnector.py:884; */
"Station doesn't have a market!" = "Station doesn't have a market!";
/* EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI; EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI to save to file; stats.py: Fetching data from Frontier CAPI in order to display on File > Status; In files: EDMarketConnector.py:867; EDMarketConnector.py:1428; stats.py:279; */
/* EDMarketConnector.py: CAPI queries aborted because player is in CQC (Arena); In files: EDMarketConnector.py:913; */
"CQC detected, aborting CAPI query" = "CQC detected, aborting CAPI query";
/* EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI; stats.py: Fetching data from Frontier CAPI in order to display on File > Status; In files: EDMarketConnector.py:934; stats.py:280; */
"Fetching data..." = "Fetching data...";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:880; */
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:973; */
"CAPI: No commander data returned" = "CAPI: No commander data returned";
/* EDMarketConnector.py: We didn't have the commander name when we should have; stats.py: Unknown commander; In files: EDMarketConnector.py:884; stats.py:297; */
/* EDMarketConnector.py: We didn't have the commander name when we should have; stats.py: Unknown commander; In files: EDMarketConnector.py:977; stats.py:299; */
"Who are you?!" = "Who are you?!";
/* EDMarketConnector.py: We don't know where the commander is, when we should; stats.py: Unknown location; In files: EDMarketConnector.py:890; stats.py:307; */
/* EDMarketConnector.py: We don't know where the commander is, when we should; stats.py: Unknown location; In files: EDMarketConnector.py:983; stats.py:309; */
"Where are you?!" = "Where are you?!";
/* EDMarketConnector.py: We don't know what ship the commander is in, when we should; stats.py: Unknown ship; In files: EDMarketConnector.py:894; stats.py:312; */
/* EDMarketConnector.py: We don't know what ship the commander is in, when we should; stats.py: Unknown ship; In files: EDMarketConnector.py:990; stats.py:314; */
"What are you flying?!" = "What are you flying?!";
/* EDMarketConnector.py: Time when we last obtained Frontier CAPI data; In files: EDMarketConnector.py:1026; */
/* EDMarketConnector.py: Time when we last obtained Frontier CAPI data; In files: EDMarketConnector.py:1145; */
"Last updated at %H:%M:%S" = "Last updated at %H:%M:%S";
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1052; */
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1174; */
"Fighter" = "Fighter";
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1053; */
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1175; */
"Gunner" = "Gunner";
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1054; */
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1176; */
"Helm" = "Helm";
/* EDMarketConnector.py: Cooldown on 'Update' button; In files: EDMarketConnector.py:1298; */
/* EDMarketConnector.py: Cooldown on 'Update' button; In files: EDMarketConnector.py:1434; */
"cooldown {SS}s" = "cooldown {SS}s";
/* EDMarketConnector.py: Generic 'OK' button label; prefs.py: 'OK' button on Settings/Preferences window; In files: EDMarketConnector.py:1407; prefs.py:304; */
/* EDMarketConnector.py: Generic 'OK' button label; prefs.py: 'OK' button on Settings/Preferences window; In files: EDMarketConnector.py:1546; prefs.py:305; */
"OK" = "OK";
/* EDMarketConnector.py: The application is shutting down; In files: EDMarketConnector.py:1489; */
/* EDMarketConnector.py: The application is shutting down; In files: EDMarketConnector.py:1611; */
"Shutting down..." = "Shutting down...";
/* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:1780:1786; */
/* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:1906:1912; */
"One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name.";
/* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:1790; prefs.py:953; */
/* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:1916; prefs.py:976; */
"Plugins" = "Plugins";
/* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:1801; */
/* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:1927; */
"EDMC: Plugins Without Python 3.x Support" = "EDMC: Plugins Without Python 3.x Support";
/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:206; */
@ -181,7 +184,7 @@
/* journal_lock.py: Generic 'Ignore' button label; In files: journal_lock.py:232; */
"Ignore" = "Ignore";
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:194; prefs.py:466; prefs.py:678; prefs.py:711; */
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:194; prefs.py:467; prefs.py:701; prefs.py:734; */
"Default" = "Default";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:52; coriolis.py:55; coriolis.py:101; coriolis.py:117; coriolis.py:123; */
@ -214,193 +217,205 @@
/* eddb.py: Journal Processing disabled due to an active killswitch; In files: eddb.py:100; */
"EDDB Journal processing disabled. See Log." = "EDDB Journal processing disabled. See Log.";
/* eddn.py: Status text shown while attempting to send data; In files: eddn.py:215; eddn.py:619; eddn.py:969; */
/* eddn.py: Status text shown while attempting to send data; In files: eddn.py:225; eddn.py:630; eddn.py:982; */
"Sending data to EDDN..." = "Sending data to EDDN...";
/* eddn.py: Error while trying to send data to EDDN; In files: eddn.py:264; eddn.py:907; eddn.py:942; eddn.py:981; */
/* eddn.py: Error while trying to send data to EDDN; In files: eddn.py:274; eddn.py:920; eddn.py:955; eddn.py:994; */
"Error: Can't connect to EDDN" = "Error: Can't connect to EDDN";
/* eddn.py: EDDN has banned this version of our client; In files: eddn.py:282; */
/* eddn.py: EDDN has banned this version of our client; In files: eddn.py:292; */
"EDDN Error: EDMC is too old for EDDN. Please update." = "EDDN Error: EDMC is too old for EDDN. Please update.";
/* eddn.py: EDDN returned an error that indicates something about what we sent it was wrong; In files: eddn.py:288; */
/* eddn.py: EDDN returned an error that indicates something about what we sent it was wrong; In files: eddn.py:298; */
"EDDN Error: Validation Failed (EDMC Too Old?). See Log" = "EDDN Error: Validation Failed (EDMC Too Old?). See Log";
/* eddn.py: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number; In files: eddn.py:293; */
/* eddn.py: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number; In files: eddn.py:303; */
"EDDN Error: Returned {STATUS} status code" = "EDDN Error: Returned {STATUS} status code";
/* eddn.py: Enable EDDN support for station data checkbox label; In files: eddn.py:699; */
/* eddn.py: Enable EDDN support for station data checkbox label; In files: eddn.py:710; */
"Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network";
/* eddn.py: Enable EDDN support for system and other scan data checkbox label; In files: eddn.py:710; */
/* eddn.py: Enable EDDN support for system and other scan data checkbox label; In files: eddn.py:721; */
"Send system and scan data to the Elite Dangerous Data Network" = "Send system and scan data to the Elite Dangerous Data Network";
/* eddn.py: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this; In files: eddn.py:721; */
/* eddn.py: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this; In files: eddn.py:732; */
"Delay sending until docked" = "Delay sending until docked";
/* eddn.py: Killswitch disabled EDDN; In files: eddn.py:785; */
/* eddn.py: Killswitch disabled EDDN; In files: eddn.py:796; */
"EDDN journal handler disabled. See Log." = "EDDN journal handler disabled. See Log.";
/* edsm.py: Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:198; */
/* edsm.py: Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:236; */
"Send flight log and Cmdr status to EDSM" = "Send flight log and Cmdr status to EDSM";
/* edsm.py: Settings>EDSM - Label on header/URL to EDSM API key page; In files: edsm.py:208; */
/* edsm.py: Settings>EDSM - Label on header/URL to EDSM API key page; In files: edsm.py:246; */
"Elite Dangerous Star Map credentials" = "Elite Dangerous Star Map credentials";
/* edsm.py: EDSM Commander name label in EDSM settings; In files: edsm.py:227; */
/* edsm.py: EDSM Commander name label in EDSM settings; In files: edsm.py:265; */
"Commander Name" = "Commander Name";
/* edsm.py: EDSM API key label; inara.py: Inara API key label; In files: edsm.py:235; inara.py:237; */
/* edsm.py: EDSM API key label; inara.py: Inara API key label; In files: edsm.py:273; inara.py:243; */
"API Key" = "API Key";
/* edsm.py: We have no data on the current commander; prefs.py: No hotkey/shortcut set; stats.py: No rank; In files: edsm.py:262; prefs.py:518; prefs.py:1156; prefs.py:1189; stats.py:117; stats.py:136; stats.py:155; stats.py:172; */
/* edsm.py: We have no data on the current commander; prefs.py: No hotkey/shortcut set; stats.py: No rank; In files: edsm.py:300; prefs.py:519; prefs.py:1173; prefs.py:1206; stats.py:117; stats.py:136; stats.py:155; stats.py:172; */
"None" = "None";
/* edsm.py: EDSM plugin - Journal handling disabled by killswitch; In files: edsm.py:358; */
/* edsm.py: EDSM plugin - Journal handling disabled by killswitch; In files: edsm.py:401; */
"EDSM Handler disabled. See Log." = "EDSM Handler disabled. See Log.";
/* edsm.py: EDSM Plugin - Error message from EDSM API; In files: edsm.py:640; edsm.py:745; */
/* edsm.py: EDSM Plugin - Error message from EDSM API; In files: edsm.py:746; edsm.py:874; */
"Error: EDSM {MSG}" = "Error: EDSM {MSG}";
/* edsm.py: EDSM Plugin - Error connecting to EDSM API; In files: edsm.py:677; edsm.py:740; */
/* edsm.py: EDSM Plugin - Error connecting to EDSM API; In files: edsm.py:783; edsm.py:869; */
"Error: Can't connect to EDSM" = "Error: Can't connect to EDSM";
/* inara.py: Checkbox to enable INARA API Usage; In files: inara.py:216; */
/* inara.py: Checkbox to enable INARA API Usage; In files: inara.py:222; */
"Send flight log and Cmdr status to Inara" = "Send flight log and Cmdr status to Inara";
/* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:228; */
/* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:234; */
"Inara credentials" = "Inara credentials";
/* inara.py: INARA support disabled via killswitch; In files: inara.py:335; */
/* inara.py: INARA support disabled via killswitch; In files: inara.py:341; */
"Inara disabled. See Log." = "Inara disabled. See Log.";
/* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1549; inara.py:1562; */
/* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1580; inara.py:1593; */
"Error: Inara {MSG}" = "Error: Inara {MSG}";
/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:250; */
"Preferences" = "Preferences";
/* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:346; */
/* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:347; */
"Please choose what data to save" = "Please choose what data to save";
/* prefs.py: Settings > Output option; In files: prefs.py:352; */
/* prefs.py: Settings > Output option; In files: prefs.py:353; */
"Market data in CSV format file" = "Market data in CSV format file";
/* prefs.py: Settings > Output option; In files: prefs.py:361; */
/* prefs.py: Settings > Output option; In files: prefs.py:362; */
"Market data in Trade Dangerous format file" = "Market data in Trade Dangerous format file";
/* prefs.py: Settings > Output option; In files: prefs.py:371; */
/* prefs.py: Settings > Output option; In files: prefs.py:372; */
"Ship loadout" = "Ship loadout";
/* prefs.py: Settings > Output option; In files: prefs.py:381; */
/* prefs.py: Settings > Output option; In files: prefs.py:382; */
"Automatically update on docking" = "Automatically update on docking";
/* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:390; prefs.py:409; */
/* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:391; prefs.py:410; */
"File location" = "File location";
/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:398; prefs.py:448; */
/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:399; prefs.py:449; */
"Change..." = "Change...";
/* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:401; prefs.py:451; */
/* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:402; prefs.py:452; */
"Browse..." = "Browse...";
/* prefs.py: Label for 'Output' Settings/Preferences tab; In files: prefs.py:416; */
/* prefs.py: Label for 'Output' Settings/Preferences tab; In files: prefs.py:417; */
"Output" = "Output";
/* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:442; prefs.py:457; */
/* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:443; prefs.py:458; */
"E:D journal file location" = "E:D journal file location";
/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:482; */
/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:483; */
"Keyboard shortcut" = "Keyboard shortcut";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:484; */
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:485; */
"Hotkey" = "Hotkey";
/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:493; */
/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:494; */
"Re-start {APP} to use shortcuts" = "Re-start {APP} to use shortcuts";
/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:502; */
/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:503; */
"{APP} needs permission to use shortcuts" = "{APP} needs permission to use shortcuts";
/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:507; */
/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:508; */
"Open System Preferences" = "Open System Preferences";
/* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:529; */
/* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:530; */
"Only when Elite: Dangerous is the active app" = "Only when Elite: Dangerous is the active app";
/* prefs.py: Configuration - play sound when hotkey used; In files: prefs.py:540; */
/* prefs.py: Configuration - play sound when hotkey used; In files: prefs.py:541; */
"Play sound" = "Play sound";
/* prefs.py: Configuration - disable checks for app updates when in-game; In files: prefs.py:555; */
/* prefs.py: Configuration - disable checks for app updates when in-game; In files: prefs.py:556; */
"Disable Automatic Application Updates Check when in-game" = "Disable Automatic Application Updates Check when in-game";
/* prefs.py: Label for preferred shipyard, system and station 'providers'; In files: prefs.py:568; */
/* prefs.py: Label for preferred shipyard, system and station 'providers'; In files: prefs.py:569; */
"Preferred websites" = "Preferred websites";
/* prefs.py: Label for Shipyard provider selection; In files: prefs.py:579; */
/* prefs.py: Label for Shipyard provider selection; In files: prefs.py:580; */
"Shipyard" = "Shipyard";
/* prefs.py: Label for checkbox to utilise alternative Coriolis URL method; In files: prefs.py:591; */
/* prefs.py: Label for checkbox to utilise alternative Coriolis URL method; In files: prefs.py:592; */
"Use alternate URL method" = "Use alternate URL method";
/* prefs.py: Configuration - Label for selection of Log Level; In files: prefs.py:644; */
/* prefs.py: Configuration - Label for selection of Log Level; In files: prefs.py:645; */
"Log Level" = "Log Level";
/* prefs.py: Label for 'Configuration' tab in Settings; In files: prefs.py:672; */
/* prefs.py: Label for 'Configuration' tab in Settings; In files: prefs.py:673; */
"Configuration" = "Configuration";
/* prefs.py: Label for Settings > Appeareance > selection of 'normal' text colour; In files: prefs.py:685; */
/* prefs.py: UI elements privacy section header in privacy tab of preferences; In files: prefs.py:682; */
"Main UI privacy options" = "Main UI privacy options";
/* prefs.py: Hide private group owner name from UI checkbox; In files: prefs.py:687; */
"Hide private group name in UI" = "Hide private group name in UI";
/* prefs.py: Hide multicrew captain name from main UI checkbox; In files: prefs.py:691; */
"Hide multi-crew captain name" = "Hide multi-crew captain name";
/* prefs.py: Preferences privacy tab title; In files: prefs.py:695; */
"Privacy" = "Privacy";
/* prefs.py: Label for Settings > Appeareance > selection of 'normal' text colour; In files: prefs.py:708; */
"Normal text" = "Normal text";
/* prefs.py: Label for Settings > Appeareance > selection of 'highlightes' text colour; In files: prefs.py:687; */
/* prefs.py: Label for Settings > Appeareance > selection of 'highlightes' text colour; In files: prefs.py:710; */
"Highlighted text" = "Highlighted text";
/* prefs.py: Appearance - Label for selection of application display language; In files: prefs.py:696; */
/* prefs.py: Appearance - Label for selection of application display language; In files: prefs.py:719; */
"Language" = "Language";
/* prefs.py: Label for Settings > Appearance > Theme selection; In files: prefs.py:706; */
/* prefs.py: Label for Settings > Appearance > Theme selection; In files: prefs.py:729; */
"Theme" = "Theme";
/* prefs.py: Label for 'Dark' theme radio button; In files: prefs.py:717; */
/* prefs.py: Label for 'Dark' theme radio button; In files: prefs.py:740; */
"Dark" = "Dark";
/* prefs.py: Label for 'Transparent' theme radio button; In files: prefs.py:724; */
/* prefs.py: Label for 'Transparent' theme radio button; In files: prefs.py:747; */
"Transparent" = "Transparent";
/* prefs.py: Appearance - Label for selection of UI scaling; In files: prefs.py:770; */
/* prefs.py: Appearance - Label for selection of UI scaling; In files: prefs.py:793; */
"UI Scale Percentage" = "UI Scale Percentage";
/* prefs.py: Appearance - Help/hint text for UI scaling selection; In files: prefs.py:791; */
/* prefs.py: Appearance - Help/hint text for UI scaling selection; In files: prefs.py:814; */
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 means Default{CR}Restart Required for{CR}changes to take effect!";
/* prefs.py: Appearance - Label for selection of main window transparency; In files: prefs.py:801; */
/* prefs.py: Appearance - Label for selection of main window transparency; In files: prefs.py:824; */
"Main window transparency" = "Main window transparency";
/* prefs.py: Appearance - Help/hint text for Main window transparency selection; In files: prefs.py:821:824; */
/* prefs.py: Appearance - Help/hint text for Main window transparency selection; In files: prefs.py:844:847; */
"100 means fully opaque.{CR}Window is updated in real time" = "100 means fully opaque.{CR}Window is updated in real time";
/* prefs.py: Label for Settings > Appearance tab; In files: prefs.py:860; */
/* prefs.py: Label for Settings > Appearance tab; In files: prefs.py:883; */
"Appearance" = "Appearance";
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:875; */
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:898; */
"Plugins folder" = "Plugins folder";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:882; */
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:905; */
"Open" = "Open";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:890; */
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:913; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name";
/* prefs.py: Label on list of enabled plugins; In files: prefs.py:901; */
/* prefs.py: Label on list of enabled plugins; In files: prefs.py:924; */
"Enabled Plugins" = "Enabled Plugins";
/* prefs.py: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x; In files: prefs.py:921; */
/* prefs.py: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x; In files: prefs.py:944; */
"Plugins Without Python 3.x Support:" = "Plugins Without Python 3.x Support:";
/* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:929; */
/* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:952; */
"Information on migrating plugins" = "Information on migrating plugins";
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:944; */
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:967; */
"Disabled Plugins" = "Disabled Plugins";
/* stats.py: Cmdr stats; In files: stats.py:51; */
@ -625,20 +640,8 @@
/* stats.py: Power rank; In files: stats.py:160; */
"Rating 5" = "Rating 5";
/* stats.py: Status dialog subtitle - CR value of ship; In files: stats.py:367; */
/* stats.py: Status dialog subtitle - CR value of ship; In files: stats.py:369; */
"Value" = "Value";
/* stats.py: Status dialog title; In files: stats.py:376; */
/* stats.py: Status dialog title; In files: stats.py:378; */
"Ships" = "Ships";
/* prefs.py: UI elements privacy section header in privacy tab of preferences; In files: prefs.py:682; */
"Main UI privacy options" = "Main UI privacy options";
/* prefs.py: Hide private group owner name from UI checkbox; In files: prefs.py:687; */
"Hide private group name in UI" = "Hide private group name in UI";
/* prefs.py: Hide multicrew captain name from main UI checkbox; In files: prefs.py:691; */
"Hide multi-crew captain name" = "Hide multi-crew captain name";
/* prefs.py: Preferences privacy tab title; In files: prefs.py:695; */
"Privacy" = "Privacy";

@ -9,17 +9,21 @@ protocol used for the callback.
import base64
import collections
import csv
import datetime
import hashlib
import json
import numbers
import os
import random
import threading
import time
import tkinter as tk
import urllib.parse
import webbrowser
from builtins import object, range, str
from email.utils import parsedate
from os.path import join
from queue import Queue
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, OrderedDict, TypeVar, Union
import requests
@ -44,26 +48,16 @@ else:
# Define custom type for the dicts that hold CAPI data
# CAPIData = NewType('CAPIData', Dict)
holdoff = 60 # be nice
timeout = 10 # requests timeout
capi_query_cooldown = 60 # Minimum time between (sets of) CAPI queries
capi_default_requests_timeout = 10
auth_timeout = 30 # timeout for initial auth
# Currently the "Elite Dangerous Market Connector (EDCD/Athanasius)" one in
# Athanasius' Frontier account
# Obtain from https://auth.frontierstore.net/client/signup
CLIENT_ID = os.getenv('CLIENT_ID') or 'fb88d428-9110-475f-a3d2-dc151c2b9c7a'
SERVER_AUTH = 'https://auth.frontierstore.net'
URL_AUTH = '/auth'
URL_TOKEN = '/token'
URL_DECODE = '/decode'
# Used by both class Auth and Session
FRONTIER_AUTH_SERVER = 'https://auth.frontierstore.net'
USER_AGENT = f'EDCD-{appname}-{appversion()}'
SERVER_LIVE = 'https://companion.orerve.net'
SERVER_BETA = 'https://pts-companion.orerve.net'
URL_QUERY = '/profile'
URL_MARKET = '/market'
URL_SHIPYARD = '/shipyard'
commodity_map: Dict = {}
@ -71,7 +65,10 @@ commodity_map: Dict = {}
class CAPIData(UserDict):
"""CAPI Response."""
def __init__(self, data: Union[str, Dict[str, Any], 'CAPIData', None] = None, source_endpoint: str = None) -> None:
def __init__(
self,
data: Union[str, Dict[str, Any], 'CAPIData', None] = None, source_endpoint: str = None
) -> None:
if data is None:
super().__init__()
elif isinstance(data, str):
@ -86,7 +83,7 @@ class CAPIData(UserDict):
if source_endpoint is None:
return
if source_endpoint == URL_SHIPYARD and self.data.get('lastStarport'):
if source_endpoint == Session.FRONTIER_CAPI_PATH_SHIPYARD and self.data.get('lastStarport'):
# All the other endpoints may or may not have a lastStarport, but definitely wont have valid data
# for this check, which means it'll just make noise for no reason while we're working on other things
self.check_modules_ships()
@ -128,6 +125,57 @@ class CAPIData(UserDict):
self.data['lastStarport']['ships'] = {'shipyard_list': {}, 'unavailable_list': []}
class CAPIDataEncoder(json.JSONEncoder):
"""Allow for json dumping via specified encoder."""
def default(self, o):
"""Tell JSON encoder that we're actually just a dict."""
return o.__dict__
class CAPIDataRawEndpoint:
"""Last received CAPI response for a specific endpoint."""
def __init__(self, raw_data: str, query_time: datetime.datetime):
self.query_time = query_time
self.raw_data = raw_data
# TODO: Maybe requests.response status ?
class CAPIDataRaw:
"""The last obtained raw CAPI response for each endpoint."""
raw_data: Dict[str, CAPIDataRawEndpoint] = {}
def record_endpoint(
self, endpoint: str,
raw_data: str,
query_time: datetime.datetime
):
"""Record the latest raw data for the given endpoint."""
self.raw_data[endpoint] = CAPIDataRawEndpoint(raw_data, query_time)
def __str__(self):
"""Return a more readable string form of the data."""
capi_data_str = '{'
for k, v in self.raw_data.items():
capi_data_str += f'"{k}":\n{{\n\t"query_time": "{v.query_time}",\n\t' \
f'"raw_data": {v.raw_data}\n}},\n\n'
capi_data_str = capi_data_str.removesuffix(',\n\n')
capi_data_str += '\n\n}'
return capi_data_str
def __iter__(self):
"""Make this iterable on its raw_data dict."""
yield from self.raw_data
def __getitem__(self, item):
"""Make the raw_data dict's items get'able."""
return self.raw_data.__getitem__(item)
def listify(thing: Union[List, Dict]) -> List:
"""
Convert actual JSON array or int-indexed dict into a Python list.
@ -236,17 +284,26 @@ class CmdrError(Exception):
class Auth(object):
"""Handles authentication with the Frontier CAPI service via oAuth2."""
# Currently the "Elite Dangerous Market Connector (EDCD/Athanasius)" one in
# Athanasius' Frontier account
# Obtain from https://auth.frontierstore.net/client/signup
CLIENT_ID = os.getenv('CLIENT_ID') or 'fb88d428-9110-475f-a3d2-dc151c2b9c7a'
FRONTIER_AUTH_PATH_AUTH = '/auth'
FRONTIER_AUTH_PATH_TOKEN = '/token'
FRONTIER_AUTH_PATH_DECODE = '/decode'
def __init__(self, cmdr: str) -> None:
self.cmdr: str = cmdr
self.session = requests.Session()
self.session.headers['User-Agent'] = USER_AGENT
self.requests_session = requests.Session()
self.requests_session.headers['User-Agent'] = USER_AGENT
self.verifier: Union[bytes, None] = None
self.state: Union[str, None] = None
def __del__(self) -> None:
"""Ensure our Session is closed if we're being deleted."""
if self.session:
self.session.close()
if self.requests_session:
self.requests_session.close()
def refresh(self) -> Optional[str]:
"""
@ -271,13 +328,17 @@ class Auth(object):
logger.debug('We have a refresh token for that idx')
data = {
'grant_type': 'refresh_token',
'client_id': CLIENT_ID,
'client_id': self.CLIENT_ID,
'refresh_token': tokens[idx],
}
logger.debug('Attempting refresh with Frontier...')
try:
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=data, timeout=auth_timeout)
r = self.requests_session.post(
FRONTIER_AUTH_SERVER + self.FRONTIER_AUTH_PATH_TOKEN,
data=data,
timeout=auth_timeout
)
if r.status_code == requests.codes.ok:
data = r.json()
tokens[idx] = data.get('refresh_token', '')
@ -307,10 +368,10 @@ class Auth(object):
logger.info(f'Trying auth from scratch for Commander "{self.cmdr}"')
challenge = self.base64_url_encode(hashlib.sha256(self.verifier).digest())
webbrowser.open(
f'{SERVER_AUTH}{URL_AUTH}?response_type=code'
f'{FRONTIER_AUTH_SERVER}{self.FRONTIER_AUTH_PATH_AUTH}?response_type=code'
f'&audience=frontier,steam,epic'
f'&scope=auth capi'
f'&client_id={CLIENT_ID}'
f'&client_id={self.CLIENT_ID}'
f'&code_challenge={challenge}'
f'&code_challenge_method=S256'
f'&state={self.state}'
@ -348,7 +409,7 @@ class Auth(object):
logger.debug('Got code, posting it back...')
request_data = {
'grant_type': 'authorization_code',
'client_id': CLIENT_ID,
'client_id': self.CLIENT_ID,
'code_verifier': self.verifier,
'code': data['code'][0],
'redirect_uri': protocolhandler.redirect,
@ -361,12 +422,16 @@ class Auth(object):
# requests_log.setLevel(logging.DEBUG)
# requests_log.propagate = True
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=request_data, timeout=auth_timeout)
r = self.requests_session.post(
FRONTIER_AUTH_SERVER + self.FRONTIER_AUTH_PATH_TOKEN,
data=request_data,
timeout=auth_timeout
)
data_token = r.json()
if r.status_code == requests.codes.ok:
# Now we need to /decode the token to check the customer_id against FID
r = self.session.get(
SERVER_AUTH + URL_DECODE,
r = self.requests_session.get(
FRONTIER_AUTH_SERVER + self.FRONTIER_AUTH_PATH_DECODE,
headers={
'Authorization': f'Bearer {data_token.get("access_token", "")}',
'Content-Type': 'application/json',
@ -463,30 +528,127 @@ class Auth(object):
return base64.urlsafe_b64encode(text).decode().replace('=', '')
class EDMCCAPIReturn:
"""Base class for Request, Failure or Response."""
def __init__(
self, query_time: int, tk_response_event: Optional[str] = None,
play_sound: bool = False, auto_update: bool = False
):
self.tk_response_event = tk_response_event # Name of tk event to generate when response queued.
self.query_time: int = query_time # When this query is considered to have started (time_t).
self.play_sound: bool = play_sound # Whether to play good/bad sounds for success/failure.
self.auto_update: bool = auto_update # Whether this was automatically triggered.
class EDMCCAPIRequest(EDMCCAPIReturn):
"""Encapsulates a request for CAPI data."""
REQUEST_WORKER_SHUTDOWN = '__EDMC_WORKER_SHUTDOWN'
def __init__(
self, endpoint: str, query_time: int,
tk_response_event: Optional[str] = None,
play_sound: bool = False, auto_update: bool = False
):
super().__init__(
query_time=query_time, tk_response_event=tk_response_event,
play_sound=play_sound, auto_update=auto_update
)
self.endpoint: str = endpoint # The CAPI query to perform.
class EDMCCAPIResponse(EDMCCAPIReturn):
"""Encapsulates a response from CAPI quer(y|ies)."""
def __init__(
self, capi_data: CAPIData,
query_time: int, play_sound: bool = False, auto_update: bool = False
):
super().__init__(query_time=query_time, play_sound=play_sound, auto_update=auto_update)
self.capi_data: CAPIData = capi_data # Frontier CAPI response, possibly augmented (station query)
class EDMCCAPIFailedRequest(EDMCCAPIReturn):
"""CAPI failed query error class."""
def __init__(
self, message: str,
query_time: int, play_sound: bool = False, auto_update: bool = False,
exception=None
):
super().__init__(query_time=query_time, play_sound=play_sound, auto_update=auto_update)
self.message: str = message # User-friendly reason for failure.
self.exception: int = exception # Exception that recipient should raise.
class Session(object):
"""Methods for handling an oAuth2 session."""
"""Methods for handling Frontier Auth and CAPI queries."""
STATE_INIT, STATE_AUTH, STATE_OK = list(range(3))
FRONTIER_CAPI_PATH_PROFILE = '/profile'
FRONTIER_CAPI_PATH_MARKET = '/market'
FRONTIER_CAPI_PATH_SHIPYARD = '/shipyard'
# This is a dummy value, to signal to Session.capi_query_worker that we
# the 'station' triplet of queries.
_CAPI_PATH_STATION = '_edmc_station'
def __init__(self) -> None:
self.state = Session.STATE_INIT
self.server: Optional[str] = None
self.credentials: Optional[Dict[str, Any]] = None
self.session: Optional[requests.Session] = None
self.requests_session: Optional[requests.Session] = None
self.auth: Optional[Auth] = None
self.retrying = False # Avoid infinite loop when successful auth / unsuccessful query
self.tk_master: Optional[tk.Tk] = None
def login(self, cmdr: str = None, is_beta: Union[None, bool] = None) -> bool:
self.capi_raw_data = CAPIDataRaw() # Cache of raw replies from CAPI service
# Queue that holds requests for CAPI queries, the items should always
# be EDMCCAPIRequest objects.
self.capi_request_queue: Queue[EDMCCAPIRequest] = Queue()
# This queue is used to pass the result, possibly a failure, of CAPI
# queries back to the requesting code (technically anything checking
# this queue, but it should be either EDMarketConnector.AppWindow or
# EDMC.py). Items may be EDMCCAPIResponse or EDMCCAPIFailedRequest.
self.capi_response_queue: Queue[Union[EDMCCAPIResponse, EDMCCAPIFailedRequest]] = Queue()
logger.debug('Starting CAPI queries thread...')
self.capi_query_thread = threading.Thread(
target=self.capi_query_worker,
daemon=True,
name='CAPI worker'
)
self.capi_query_thread.start()
logger.debug('Done')
def set_tk_master(self, master: tk.Tk) -> None:
"""Set a reference to main UI Tk root window."""
self.tk_master = master
######################################################################
# Frontier Authorization
######################################################################
def start_frontier_auth(self, access_token: str) -> None:
"""Start an oAuth2 session."""
logger.debug('Starting session')
self.requests_session = requests.Session()
self.requests_session.headers['Authorization'] = f'Bearer {access_token}'
self.requests_session.headers['User-Agent'] = USER_AGENT
self.state = Session.STATE_OK
def login(self, cmdr: str = None, is_beta: Optional[bool] = None) -> bool:
"""
Attempt oAuth2 login.
:return: True if login succeeded, False if re-authorization initiated.
"""
if not CLIENT_ID:
logger.error('CLIENT_ID is None')
if not Auth.CLIENT_ID:
logger.error('self.CLIENT_ID is None')
raise CredentialsError('cannot login without a valid Client ID')
# TODO: WTF is the intent behind this logic ?
# Perhaps to do with not even trying to auth if we're not sure if
# it's beta, but that's moot for *auth* since oAuth2.
if not cmdr or is_beta is None:
# Use existing credentials
if not self.credentials:
@ -516,7 +678,7 @@ class Session(object):
if access_token:
logger.debug('We have an access_token')
self.auth = None
self.start(access_token)
self.start_frontier_auth(access_token)
return True
else:
@ -536,7 +698,7 @@ class Session(object):
try:
logger.debug('Trying authorize with payload from handler')
self.start(self.auth.authorize(protocolhandler.lastpayload)) # type: ignore
self.start_frontier_auth(self.auth.authorize(protocolhandler.lastpayload)) # type: ignore
self.auth = None
except Exception:
@ -545,176 +707,263 @@ class Session(object):
self.auth = None
raise # Bad thing happened
def start(self, access_token: str) -> None:
"""Start an oAuth2 session."""
logger.debug('Starting session')
self.session = requests.Session()
self.session.headers['Authorization'] = f'Bearer {access_token}'
self.session.headers['User-Agent'] = USER_AGENT
self.state = Session.STATE_OK
def close(self) -> None:
"""Close Frontier authorization session."""
self.state = Session.STATE_INIT
if self.requests_session:
try:
self.requests_session.close()
def query(self, endpoint: str) -> CAPIData: # noqa: CCR001
"""Perform a query against the specified CAPI endpoint."""
logger.trace_if('capi.query', f'Performing query for endpoint "{endpoint}"')
if self.state == Session.STATE_INIT:
if self.login():
return self.query(endpoint)
except Exception as e:
logger.debug('Frontier Auth: closing', exc_info=e)
elif self.state == Session.STATE_AUTH:
logger.error('cannot make a query when unauthorized')
raise CredentialsError('cannot make a query when unauthorized')
self.requests_session = None
logger.trace_if('capi.query', 'Trying...')
if conf_module.capi_pretend_down:
raise ServerConnectionError(f'Pretending CAPI down: {endpoint}')
def invalidate(self) -> None:
"""Invalidate Frontier authorization credentials."""
logger.debug('Forcing a full re-authentication')
# Force a full re-authentication
self.close()
Auth.invalidate(self.credentials['cmdr']) # type: ignore
######################################################################
try:
r = self.session.get(self.server + endpoint, timeout=timeout) # type: ignore
######################################################################
# CAPI queries
######################################################################
def capi_query_worker(self): # noqa: C901, CCR001
"""Worker thread that performs actual CAPI queries."""
logger.debug('CAPI worker thread starting')
except requests.ConnectionError as e:
logger.warning(f'Unable to resolve name for CAPI: {e} (for request: {endpoint})')
raise ServerConnectionError(f'Unable to connect to endpoint {endpoint}') from e
def capi_single_query( # noqa: CCR001
capi_endpoint: str, timeout: int = capi_default_requests_timeout
) -> CAPIData:
"""
Perform a *single* CAPI endpoint query within the thread worker.
except Exception as e:
logger.debug('Attempting GET', exc_info=e)
# LANG: Frontier CAPI data retrieval failed
raise ServerError(f'{_("Frontier CAPI query failure")}: {endpoint}') from e
:param capi_endpoint: An actual Frontier CAPI endpoint to query.
:param timeout: requests query timeout to use.
:return: The resulting CAPI data, of type CAPIData.
"""
capi_data: CAPIData
try:
logger.trace_if('capi.worker', 'Sending HTTP request...')
if conf_module.capi_pretend_down:
raise ServerConnectionError(f'Pretending CAPI down: {capi_endpoint}')
if r.url.startswith(SERVER_AUTH):
logger.info('Redirected back to Auth Server')
# Redirected back to Auth server - force full re-authentication
self.dump(r)
self.invalidate()
self.retrying = False
self.login()
raise CredentialsError()
r = self.requests_session.get(self.server + capi_endpoint, timeout=timeout) # type: ignore
logger.trace_if('capi.worker', '... got result...')
r.raise_for_status() # Typically 403 "Forbidden" on token expiry
# May also fail here if token expired since response is empty
# r.status_code = 401
# raise requests.HTTPError
capi_json = r.json()
capi_data = CAPIData(capi_json, capi_endpoint)
self.capi_raw_data.record_endpoint(
capi_endpoint, r.content.decode(encoding='utf-8'),
datetime.datetime.utcnow()
)
elif 500 <= r.status_code < 600:
# Server error. Typically 500 "Internal Server Error" if server is down
logger.debug('500 status back from CAPI')
self.dump(r)
# LANG: Frontier CAPI data retrieval failed with 5XX code
raise ServerError(f'{_("Frontier CAPI server error")}: {r.status_code}')
except requests.ConnectionError as e:
logger.warning(f'Request {capi_endpoint}: {e}')
raise ServerConnectionError(f'Unable to connect to endpoint: {capi_endpoint}') from e
try:
r.raise_for_status() # Typically 403 "Forbidden" on token expiry
data = CAPIData(r.json(), endpoint) # May also fail here if token expired since response is empty
except requests.HTTPError as e: # In response to raise_for_status()
logger.exception(f'Frontier CAPI Auth: GET {capi_endpoint}')
self.dump(r)
except (requests.HTTPError, ValueError) as e:
logger.exception('Frontier CAPI Auth: GET ')
self.dump(r)
self.close()
if r.status_code == 401: # CAPI doesn't think we're Auth'd
# No need for translation, we'll go straight into trying new Auth
# and thus any message would be overwritten.
raise CredentialsError('Frontier CAPI said Auth required') from e
if self.retrying: # Refresh just succeeded but this query failed! Force full re-authentication
logger.error('Frontier CAPI Auth: query failed after refresh')
self.invalidate()
self.retrying = False
self.login()
raise CredentialsError('query failed after refresh') from e
if r.status_code == 418: # "I'm a teapot" - used to signal maintenance
# LANG: Frontier CAPI returned 418, meaning down for maintenance
raise ServerError(_("Frontier CAPI down for maintenance")) from e
elif self.login(): # Maybe our token expired. Re-authorize in any case
logger.debug('Initial query failed, but login() just worked, trying again...')
self.retrying = True
return self.query(endpoint)
logger.exception('Frontier CAPI: Misc. Error')
raise ServerError('Frontier CAPI: Misc. Error') from e
except ValueError as e:
logger.exception(f'decoding CAPI response content:\n{r.content.decode(encoding="utf-8")}\n')
raise ServerError("Frontier CAPI response: couldn't decode JSON") from e
except Exception as e:
logger.debug('Attempting GET', exc_info=e)
# LANG: Frontier CAPI data retrieval failed
raise ServerError(f'{_("Frontier CAPI query failure")}: {capi_endpoint}') from e
if capi_endpoint == self.FRONTIER_CAPI_PATH_PROFILE and 'commander' not in capi_data:
logger.error('No commander in returned data')
if 'timestamp' not in capi_data:
capi_data['timestamp'] = time.strftime(
'%Y-%m-%dT%H:%M:%SZ', parsedate(r.headers['Date']) # type: ignore
)
return capi_data
def capi_station_queries(timeout: int = capi_default_requests_timeout) -> CAPIData: # noqa: CCR001
"""
Perform all 'station' queries for the caller.
A /profile query is performed to check that we are docked (or on foot)
and the station name and marketid match the prior Docked event.
If they do match, and the services list says they're present, also
retrieve CAPI market and/or shipyard/outfitting data and merge into
the /profile data.
:param timeout: requests timeout to use.
:return: CAPIData instance with what we retrieved.
"""
station_data = capi_single_query(self.FRONTIER_CAPI_PATH_PROFILE, timeout=timeout)
if not station_data['commander'].get('docked') and not monitor.state['OnFoot']:
return station_data
# Sanity checks in case data isn't as we expect, and maybe 'docked' flag
# is also lagging.
if (last_starport := station_data.get('lastStarport')) is None:
logger.error("No lastStarport in data!")
return station_data
if (
(last_starport_name := last_starport.get('name')) is None
or last_starport_name == ''
):
# This could well be valid if you've been out exploring for a long
# time.
logger.warning("No lastStarport name!")
return station_data
# WORKAROUND: n/a | 06-08-2021: Issue 1198 and https://issues.frontierstore.net/issue-detail/40706
# -- strip "+" chars off star port names returned by the CAPI
last_starport_name = last_starport["name"] = last_starport_name.rstrip(" +")
services = last_starport.get('services', {})
if not isinstance(services, dict):
# Odyssey Alpha Phase 3 4.0.0.20 has been observed having
# this be an empty list when you've jumped to another system
# and not yet docked. As opposed to no services key at all
# or an empty dict.
logger.error(f'services is "{type(services)}", not dict !')
# TODO: Change this to be dependent on its own CL arg
if __debug__:
self.dump_capi_data(station_data)
# Set an empty dict so as to not have to retest below.
services = {}
last_starport_id = int(last_starport.get('id'))
if services.get('commodities'):
market_data = capi_single_query(self.FRONTIER_CAPI_PATH_MARKET, timeout=timeout)
if last_starport_id != int(market_data['id']):
logger.warning(f"{last_starport_id!r} != {int(market_data['id'])!r}")
raise ServerLagging()
else:
market_data['name'] = last_starport_name
station_data['lastStarport'].update(market_data)
if services.get('outfitting') or services.get('shipyard'):
shipyard_data = capi_single_query(self.FRONTIER_CAPI_PATH_SHIPYARD, timeout=timeout)
if last_starport_id != int(shipyard_data['id']):
logger.warning(f"{last_starport_id!r} != {int(shipyard_data['id'])!r}")
raise ServerLagging()
else:
shipyard_data['name'] = last_starport_name
station_data['lastStarport'].update(shipyard_data)
# WORKAROUND END
return station_data
while True:
query = self.capi_request_queue.get()
logger.trace_if('capi.worker', 'De-queued request')
if not isinstance(query, EDMCCAPIRequest):
logger.error("Item from queue wasn't an EDMCCAPIRequest")
break
if query.endpoint == query.REQUEST_WORKER_SHUTDOWN:
logger.info(f'endpoint {query.REQUEST_WORKER_SHUTDOWN}, exiting...')
break
logger.trace_if('capi.worker', f'Processing query: {query.endpoint}')
capi_data: CAPIData
try:
if query.endpoint == self._CAPI_PATH_STATION:
capi_data = capi_station_queries()
else:
capi_data = capi_single_query(self.FRONTIER_CAPI_PATH_PROFILE)
except Exception as e:
self.capi_response_queue.put(
EDMCCAPIFailedRequest(
message=str(e.args),
exception=e,
query_time=query.query_time,
play_sound=query.play_sound,
auto_update=query.auto_update
)
)
else:
self.retrying = False
logger.error('Frontier CAPI Auth: HTTP error or invalid JSON')
raise CredentialsError('HTTP error or invalid JSON') from e
self.capi_response_queue.put(
EDMCCAPIResponse(
capi_data=capi_data,
query_time=query.query_time,
play_sound=query.play_sound,
auto_update=query.auto_update
)
)
self.retrying = False
if 'timestamp' not in data:
data['timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', parsedate(r.headers['Date'])) # type: ignore
# If the query came from EDMC.(py|exe) there's no tk to send an
# event too, so assume it will be polling there response queue.
if query.tk_response_event is not None:
logger.trace_if('capi.worker', 'Sending <<CAPIResponse>>')
self.tk_master.event_generate('<<CAPIResponse>>')
# Update Odyssey Suit data
if endpoint == URL_QUERY:
self.suit_update(data)
logger.info('CAPI worker thread DONE')
return data
def capi_query_close_worker(self) -> None:
"""Ask the CAPI query thread to finish."""
self.capi_request_queue.put(
EDMCCAPIRequest(
endpoint=EDMCCAPIRequest.REQUEST_WORKER_SHUTDOWN,
query_time=int(time.time())
)
)
def profile(self) -> CAPIData:
"""Perform general CAPI /profile endpoint query."""
data = self.query(URL_QUERY)
if 'commander' not in data:
logger.error('No commander in returned data')
return data
def station(self) -> CAPIData: # noqa: CCR001
def station(
self, query_time: int, tk_response_event: Optional[str] = None,
play_sound: bool = False, auto_update: bool = False
) -> None:
"""
Perform CAPI quer(y|ies) for station data.
A /profile query is performed to check that we are docked (or on foot)
and the station name and marketid match the prior Docked event.
If they do match, and the services list says they're present, also
retrieve CAPI market and/or shipyard/outfitting data and merge into
the /profile data.
:return: Possibly augmented CAPI data.
:param query_time: When this query was initiated.
:param tk_response_event: Name of tk event to generate when response queued.
:param play_sound: Whether the app should play a sound on error.
:param auto_update: Whether this request was triggered automatically.
"""
data = self.query(URL_QUERY)
if 'commander' not in data:
logger.error('No commander in returned data')
return data
if not data['commander'].get('docked') and not monitor.state['OnFoot']:
return data
# Sanity checks in case data isn't as we expect, and maybe 'docked' flag
# is also lagging.
if (last_starport := data.get('lastStarport')) is None:
logger.error("No lastStarport in data!")
return data
if ((last_starport_name := last_starport.get('name')) is None
or last_starport_name == ''):
# This could well be valid if you've been out exploring for a long
# time.
logger.warning("No lastStarport name!")
return data
# WORKAROUND: n/a | 06-08-2021: Issue 1198 and https://issues.frontierstore.net/issue-detail/40706
# -- strip "+" chars off star port names returned by the CAPI
last_starport_name = last_starport["name"] = last_starport_name.rstrip(" +")
services = last_starport.get('services', {})
if not isinstance(services, dict):
# Odyssey Alpha Phase 3 4.0.0.20 has been observed having
# this be an empty list when you've jumped to another system
# and not yet docked. As opposed to no services key at all
# or an empty dict.
logger.error(f'services is "{type(services)}", not dict !')
if __debug__:
self.dump_capi_data(data)
# Set an empty dict so as to not have to retest below.
services = {}
last_starport_id = int(last_starport.get('id'))
if services.get('commodities'):
marketdata = self.query(URL_MARKET)
if last_starport_id != int(marketdata['id']):
logger.warning(f"{last_starport_id!r} != {int(marketdata['id'])!r}")
raise ServerLagging()
else:
marketdata['name'] = last_starport_name
data['lastStarport'].update(marketdata)
if services.get('outfitting') or services.get('shipyard'):
shipdata = self.query(URL_SHIPYARD)
if last_starport_id != int(shipdata['id']):
logger.warning(f"{last_starport_id!r} != {int(shipdata['id'])!r}")
raise ServerLagging()
else:
shipdata['name'] = last_starport_name
data['lastStarport'].update(shipdata)
# WORKAROUND END
return data
# Ask the thread worker to perform all three queries
logger.trace_if('capi.worker', 'Enqueueing request')
self.capi_request_queue.put(
EDMCCAPIRequest(
endpoint=self._CAPI_PATH_STATION,
tk_response_event=tk_response_event,
query_time=query_time,
play_sound=play_sound,
auto_update=auto_update
)
)
######################################################################
######################################################################
# Utility functions
######################################################################
def suit_update(self, data: CAPIData) -> None:
"""
Update monitor.state suit data.
@ -752,25 +1001,6 @@ class Session(object):
else:
monitor.state['SuitLoadouts'] = suit_loadouts
def close(self) -> None:
"""Close CAPI authorization session."""
self.state = Session.STATE_INIT
if self.session:
try:
self.session.close()
except Exception as e:
logger.debug('Frontier CAPI Auth: closing', exc_info=e)
self.session = None
def invalidate(self) -> None:
"""Invalidate oAuth2 credentials."""
logger.debug('Forcing a full re-authentication')
# Force a full re-authentication
self.close()
Auth.invalidate(self.credentials['cmdr']) # type: ignore
# noinspection PyMethodMayBeStatic
def dump(self, r: requests.Response) -> None:
"""Log, as error, status of requests.Response from CAPI request."""
@ -797,13 +1027,17 @@ class Session(object):
timestamp = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime())
with open(f'dump/{system}{station}.{timestamp}.json', 'wb') as h:
h.write(json.dumps(dict(data),
h.write(json.dumps(data, cls=CAPIDataEncoder,
ensure_ascii=False,
indent=2,
sort_keys=True,
separators=(',', ': ')).encode('utf-8'))
######################################################################
######################################################################
# Non-class utility functions
######################################################################
def fixup(data: CAPIData) -> CAPIData: # noqa: C901, CCR001 # Can't be usefully simplified
"""
Fix up commodity names to English & miscellaneous anomalies fixes.
@ -940,6 +1174,7 @@ def index_possibly_sparse_list(data: Union[Mapping[str, V], List[V]], key: int)
else:
raise ValueError(f'Unexpected data type {type(data)}')
######################################################################
# singleton

@ -13,7 +13,10 @@ import util_ships
def export(data, filename=None):
string = json.dumps(companion.ship(data), ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')) # pretty print
string = json.dumps(
companion.ship(data), cls=companion.CAPIDataEncoder,
ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')
) # pretty print
if filename:
with open(filename, 'wt') as h:

@ -310,6 +310,7 @@ Msg:\n{msg}'''
:param data: a dict containing the starport data
:param is_beta: whether or not we're currently in beta mode
:param is_odyssey: whether the account shows as having Odyssey expansion.
"""
commodities: List[OrderedDictT[str, Any]] = []
for commodity in data['lastStarport'].get('commodities') or []:
@ -790,7 +791,6 @@ def journal_entry( # noqa: C901, CCR001
:param state: `dict` - Current `monitor.state` data.
:return: `str` - Error message, or `None` if no errors.
"""
should_return, new_data = killswitch.check_killswitch('plugins.eddn.journal', entry)
if should_return:
plug.show_error(_('EDDN journal handler disabled. See Log.')) # LANG: Killswitch disabled EDDN

@ -1,5 +1,6 @@
"""CMDR Status information."""
import csv
import json
import tkinter
import tkinter as tk
from sys import platform
@ -275,45 +276,41 @@ class StatsDialog():
if not monitor.cmdr:
return
# LANG: Fetching data from Frontier CAPI in order to display on File > Status
self.status['text'] = _('Fetching data...')
self.parent.update_idletasks()
try:
data = companion.session.profile()
except companion.ServerError as e:
self.status['text'] = str(e)
# TODO: This needs to use cached data
if companion.session.FRONTIER_CAPI_PATH_PROFILE not in companion.session.capi_raw_data:
logger.info('No cached data, aborting...')
return
except Exception as e:
logger.exception("error while attempting to show status")
self.status['text'] = str(e)
return
capi_data = json.loads(
companion.session.capi_raw_data[companion.session.FRONTIER_CAPI_PATH_PROFILE].raw_data
)
if not data.get('commander') or not data['commander'].get('name', '').strip():
if not capi_data.get('commander') or not capi_data['commander'].get('name', '').strip():
# Shouldn't happen
# LANG: Unknown commander
self.status['text'] = _("Who are you?!")
elif (
not data.get('lastSystem')
or not data['lastSystem'].get('name', '').strip()
or not data.get('lastStarport')
or not data['lastStarport'].get('name', '').strip()
not capi_data.get('lastSystem')
or not capi_data['lastSystem'].get('name', '').strip()
or not capi_data.get('lastStarport')
or not capi_data['lastStarport'].get('name', '').strip()
):
# Shouldn't happen
# LANG: Unknown location
self.status['text'] = _("Where are you?!")
elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name', '').strip():
elif (
not capi_data.get('ship') or not capi_data['ship'].get('modules')
or not capi_data['ship'].get('name', '').strip()
):
# Shouldn't happen
# LANG: Unknown ship
self.status['text'] = _("What are you flying?!")
else:
self.status['text'] = ''
StatsResults(self.parent, data)
StatsResults(self.parent, capi_data)
class StatsResults(tk.Toplevel):