mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-06-08 11:22:10 +03:00
Merge branch 'develop' into main
This commit is contained in:
commit
364aaf2aef
61
.github/workflows/pr-annotate-with-flake8.yml
vendored
61
.github/workflows/pr-annotate-with-flake8.yml
vendored
@ -1,7 +1,9 @@
|
|||||||
# This workflow will:
|
# This workflow will:
|
||||||
#
|
#
|
||||||
# install Python dependencies
|
# 1. Store some github context in env vars.
|
||||||
# Run flake8 to add annotations to the PR
|
# 2. Only checkout if base and head ref owners are the same.
|
||||||
|
# 3. Check if any *.py files are in the diff.
|
||||||
|
# 4. Only then install python and perform the annotation with flake8.
|
||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
name: PR-annotate-flake8
|
name: PR-annotate-flake8
|
||||||
@ -11,19 +13,68 @@ on:
|
|||||||
branches: [ develop ]
|
branches: [ develop ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
flake8_annotate:
|
||||||
|
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
##################################################################
|
||||||
|
# Check if the base and head repos are the same, if not we won't
|
||||||
|
# be able to add annotations.
|
||||||
|
##################################################################
|
||||||
|
- name: Check head and base repo same
|
||||||
|
env:
|
||||||
|
BASE_REPO_OWNER: ${{github.event.pull_request.base.repo.owner.login}}
|
||||||
|
HEAD_REPO_OWNER: ${{github.event.pull_request.head.repo.owner.login}}
|
||||||
|
BASE_REF: ${{github.base_ref}}
|
||||||
|
run: |
|
||||||
|
# Just to be running something
|
||||||
|
env
|
||||||
|
##################################################################
|
||||||
|
|
||||||
|
##################################################################
|
||||||
|
# Perform the checkout
|
||||||
|
##################################################################
|
||||||
|
- name: Checkout
|
||||||
|
if: ${{ github.event.pull_request.base.repo.owner.login == github.event.pull_request.head.repo.owner.login }}
|
||||||
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
##################################################################
|
||||||
|
|
||||||
|
##################################################################
|
||||||
|
# flake8-your-pr 'fails' if no *.py files changed
|
||||||
|
# So we check if any were affected and store that in env
|
||||||
|
##################################################################
|
||||||
|
- name: Check for PY files
|
||||||
|
if: ${{ github.event.pull_request.base.repo.owner.login == github.event.pull_request.head.repo.owner.login }}
|
||||||
|
run: |
|
||||||
|
# Checkout might not have happened.
|
||||||
|
if [ ! -d ".git" ]; then exit 0 ; fi
|
||||||
|
# Get a list of files ending with ".py", stuff in environment.
|
||||||
|
# We don't rely on exit status because Workflows run with
|
||||||
|
# "set -e" so any failure in a pipe is total failure.
|
||||||
|
PYFILES=$(git diff --name-only "refs/remotes/origin/${BASE_REF}" -- | egrep '.py$' || true)
|
||||||
|
# Use magic output to store in env for rest of workflow
|
||||||
|
echo "::set-env name=PYFILES::${PYFILES}"
|
||||||
|
##################################################################
|
||||||
|
|
||||||
|
##################################################################
|
||||||
|
# Get Python set up
|
||||||
|
##################################################################
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.7
|
||||||
|
if: ${{ env.PYFILES != '' }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.7
|
||||||
|
##################################################################
|
||||||
|
|
||||||
|
##################################################################
|
||||||
|
# Perform the annotation
|
||||||
|
##################################################################
|
||||||
- name: Annotate with Flake8
|
- name: Annotate with Flake8
|
||||||
|
# Only if at least one *.py file was affected
|
||||||
|
if: ${{ env.PYFILES != '' }}
|
||||||
uses: "tayfun/flake8-your-pr@master"
|
uses: "tayfun/flake8-your-pr@master"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
##################################################################
|
||||||
|
87
.github/workflows/pr-checks.yml
vendored
Normal file
87
.github/workflows/pr-checks.yml
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# This workflow will:
|
||||||
|
#
|
||||||
|
# * install Python dependencies
|
||||||
|
# * lint with a single version of Python
|
||||||
|
#
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
|
name: PR-Checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
flake8:
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
# Checkout the necessary commits
|
||||||
|
####################################################################
|
||||||
|
# We need the repo from the 'head' of the PR, not what it's
|
||||||
|
# based on.
|
||||||
|
- name: Checkout head commits
|
||||||
|
# https://github.com/actions/checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# But we do need the base references
|
||||||
|
- name: Fetch base commits
|
||||||
|
env:
|
||||||
|
BASE_REPO_URL: ${{github.event.pull_request.base.repo.svn_url}}
|
||||||
|
BASE_REPO_OWNER: ${{github.event.pull_request.base.repo.owner.login}}
|
||||||
|
|
||||||
|
run: |
|
||||||
|
# Add the 'base' repo as a new remote
|
||||||
|
git remote add ${BASE_REPO_OWNER} ${BASE_REPO_URL}
|
||||||
|
# And then fetch its references
|
||||||
|
git fetch ${BASE_REPO_OWNER}
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
# Get Python set up
|
||||||
|
####################################################################
|
||||||
|
- name: Set up Python 3.7
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.7
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install flake8
|
||||||
|
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; elif [ -f requirements.txt ]; then pip install -r requirements.txt ; fi
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
# Show full github context to aid debugging.
|
||||||
|
####################################################################
|
||||||
|
- name: Show github context
|
||||||
|
run: |
|
||||||
|
env
|
||||||
|
cat $GITHUB_EVENT_PATH
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
# Lint with flake8
|
||||||
|
####################################################################
|
||||||
|
- name: Lint with flake8
|
||||||
|
env:
|
||||||
|
BASE_REPO_URL: ${{github.event.pull_request.base.repo.svn_url}}
|
||||||
|
BASE_REPO_OWNER: ${{github.event.pull_request.base.repo.owner.login}}
|
||||||
|
BASE_REF: ${{github.base_ref}}
|
||||||
|
|
||||||
|
run: |
|
||||||
|
# Explicitly check for some errors
|
||||||
|
# E9 - Runtime (syntax and the like)
|
||||||
|
# F63 - 'tests' checking
|
||||||
|
# F7 - syntax errors
|
||||||
|
# F82 - undefined checking
|
||||||
|
git diff "refs/remotes/${BASE_REPO_OWNER}/${BASE_REF}" -- | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --diff
|
||||||
|
# Can optionally add `--exit-zero` to the flake8 arguments so that
|
||||||
|
# this doesn't fail the build.
|
||||||
|
git diff "refs/remotes/${BASE_REPO_OWNER}/${BASE_REF}" -- | flake8 . --count --statistics --diff
|
||||||
|
####################################################################
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -13,3 +13,7 @@ appcast_win_*.xml
|
|||||||
appcast_mac_*.xml
|
appcast_mac_*.xml
|
||||||
EDMarketConnector.VisualElementsManifest.xml
|
EDMarketConnector.VisualElementsManifest.xml
|
||||||
*.zip
|
*.zip
|
||||||
|
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
venv
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
[tool.autopep8]
|
|
||||||
max_line_length = 120
|
|
@ -184,14 +184,14 @@ Coding Conventions
|
|||||||
Yes:
|
Yes:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
if somethingTrue:
|
if something_true:
|
||||||
Things_we_then_do()
|
one_thing_we_do()
|
||||||
```
|
```
|
||||||
|
|
||||||
No:
|
No:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
if somethingTrue: One_thing_we_do()
|
if something_true: one_thing_we_do()
|
||||||
```
|
```
|
||||||
|
|
||||||
Yes, some existing code still flouts this rule.
|
Yes, some existing code still flouts this rule.
|
||||||
@ -224,6 +224,43 @@ No:
|
|||||||
* Going forwards please do place [type hints](https://docs.python.org/3/library/typing.html) on the declarations of your functions, both their arguments and return
|
* Going forwards please do place [type hints](https://docs.python.org/3/library/typing.html) on the declarations of your functions, both their arguments and return
|
||||||
types.
|
types.
|
||||||
|
|
||||||
|
* Use `logging` not `print()`, and definitely not `sys.stdout.write()`!
|
||||||
|
`EDMarketConnector.py` sets up a `logging.Logger` for this under the
|
||||||
|
`appname`, so:
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from config import appname
|
||||||
|
logger = logging.getLogger(appname)
|
||||||
|
|
||||||
|
logger.info(f'Some message with a {variable}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
something
|
||||||
|
except Exception as e: # Try to be more specific
|
||||||
|
logger.error(f'Error in ... with ...', exc_info=e)
|
||||||
|
|
||||||
|
**DO NOT** use the following, as you might cause a circular import:
|
||||||
|
|
||||||
|
from EDMarketConnector import logger
|
||||||
|
|
||||||
|
We have implemented a `logging.Filter` that adds support for the following
|
||||||
|
in `logging.Formatter()` strings:
|
||||||
|
|
||||||
|
1. `%(qualname)s` which gets the full `<module>.ClassA(.ClassB...).func`
|
||||||
|
of the calling function.
|
||||||
|
1. `%(class)s` which gets just the enclosing class name(s) of the calling
|
||||||
|
function.
|
||||||
|
|
||||||
|
If you want to see how we did this, check `EDMCLogging.py`.
|
||||||
|
|
||||||
|
So don't worry about adding anything about the class or function you're
|
||||||
|
logging from, it's taken care of.
|
||||||
|
|
||||||
|
*Do use a pertinent message, even when using `exc_info=...` to log an
|
||||||
|
exception*. e.g. Logging will know you were in your `get_foo()` function
|
||||||
|
but you should still tell it what actually (failed to have) happened
|
||||||
|
in there.
|
||||||
|
|
||||||
* In general, please follow [PEP8](https://www.python.org/dev/peps/pep-0008/).
|
* In general, please follow [PEP8](https://www.python.org/dev/peps/pep-0008/).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
176
EDMC.py
176
EDMC.py
@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import requests
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
# workaround for https://github.com/EDCD/EDMarketConnector/issues/568
|
# workaround for https://github.com/EDCD/EDMarketConnector/issues/568
|
||||||
os.environ["EDMC_NO_UI"] = "1"
|
os.environ["EDMC_NO_UI"] = "1"
|
||||||
@ -39,14 +39,39 @@ import eddn
|
|||||||
SERVER_RETRY = 5 # retry pause for Companion servers [s]
|
SERVER_RETRY = 5 # retry pause for Companion servers [s]
|
||||||
EXIT_SUCCESS, EXIT_SERVER, EXIT_CREDENTIALS, EXIT_VERIFICATION, EXIT_LAGGING, EXIT_SYS_ERR = range(6)
|
EXIT_SUCCESS, EXIT_SERVER, EXIT_CREDENTIALS, EXIT_VERIFICATION, EXIT_LAGGING, EXIT_SYS_ERR = range(6)
|
||||||
|
|
||||||
|
JOURNAL_RE = re.compile(r'^Journal(Beta)?\.[0-9]{12}\.[0-9]{2}\.log$')
|
||||||
|
|
||||||
|
|
||||||
# quick and dirty version comparison assuming "strict" numeric only version numbers
|
# quick and dirty version comparison assuming "strict" numeric only version numbers
|
||||||
def versioncmp(versionstring):
|
def versioncmp(versionstring):
|
||||||
return list(map(int, versionstring.split('.')))
|
return list(map(int, versionstring.split('.')))
|
||||||
|
|
||||||
|
|
||||||
try:
|
def deep_get(target: dict, *args: str, default=None) -> Any:
|
||||||
|
if not hasattr(target, 'get'):
|
||||||
|
raise ValueError(f"Cannot call get on {target} ({type(target)})")
|
||||||
|
|
||||||
|
current = target
|
||||||
|
for arg in args:
|
||||||
|
res = current.get(arg)
|
||||||
|
if res is None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
current = res
|
||||||
|
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
# arg parsing
|
# arg parsing
|
||||||
parser = argparse.ArgumentParser(prog=appcmdname, description='Prints the current system and station (if docked) to stdout and optionally writes player status, ship locations, ship loadout and/or station data to file. Requires prior setup through the accompanying GUI app.')
|
parser = argparse.ArgumentParser(
|
||||||
|
prog=appcmdname,
|
||||||
|
description='Prints the current system and station (if docked) to stdout and optionally writes player '
|
||||||
|
'status, ship locations, ship loadout and/or station data to file. '
|
||||||
|
'Requires prior setup through the accompanying GUI app.'
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument('-v', '--version', help='print program version and exit', action='store_const', const=True)
|
parser.add_argument('-v', '--version', help='print program version and exit', action='store_const', const=True)
|
||||||
parser.add_argument('-a', metavar='FILE', help='write ship loadout to FILE in Companion API json format')
|
parser.add_argument('-a', metavar='FILE', help='write ship loadout to FILE in Companion API json format')
|
||||||
parser.add_argument('-e', metavar='FILE', help='write ship loadout to FILE in E:D Shipyard plain text format')
|
parser.add_argument('-e', metavar='FILE', help='write ship loadout to FILE in E:D Shipyard plain text format')
|
||||||
@ -63,11 +88,9 @@ try:
|
|||||||
|
|
||||||
if args.version:
|
if args.version:
|
||||||
updater = Updater(provider='internal')
|
updater = Updater(provider='internal')
|
||||||
newversion: EDMCVersion = updater.check_appcast()
|
newversion: Optional[EDMCVersion] = updater.check_appcast()
|
||||||
if newversion:
|
if newversion:
|
||||||
print('{CURRENT} ("{UPDATE}" is available)'.format(
|
print(f'{appversion} ({newversion.title!r} is available)')
|
||||||
CURRENT=appversion,
|
|
||||||
UPDATE=newversion.title))
|
|
||||||
else:
|
else:
|
||||||
print(appversion)
|
print(appversion)
|
||||||
sys.exit(EXIT_SUCCESS)
|
sys.exit(EXIT_SUCCESS)
|
||||||
@ -76,26 +99,29 @@ try:
|
|||||||
# Import and collate from JSON dump
|
# Import and collate from JSON dump
|
||||||
data = json.load(open(args.j))
|
data = json.load(open(args.j))
|
||||||
config.set('querytime', int(getmtime(args.j)))
|
config.set('querytime', int(getmtime(args.j)))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Get state from latest Journal file
|
# Get state from latest Journal file
|
||||||
try:
|
try:
|
||||||
logdir = config.get('journaldir') or config.default_journal_dir
|
logdir = config.get('journaldir') or config.default_journal_dir
|
||||||
logfiles = sorted([x for x in os.listdir(logdir) if re.search('^Journal(Beta)?\.[0-9]{12}\.[0-9]{2}\.log$', x)],
|
logfiles = sorted((x for x in os.listdir(logdir) if JOURNAL_RE.search(x)), key=lambda x: x.split('.')[1:])
|
||||||
key=lambda x: x.split('.')[1:])
|
|
||||||
logfile = join(logdir, logfiles[-1])
|
logfile = join(logdir, logfiles[-1])
|
||||||
|
|
||||||
with open(logfile, 'r') as loghandle:
|
with open(logfile, 'r') as loghandle:
|
||||||
for line in loghandle:
|
for line in loghandle:
|
||||||
try:
|
try:
|
||||||
monitor.parse_entry(line)
|
monitor.parse_entry(line)
|
||||||
except:
|
except Exception:
|
||||||
if __debug__:
|
if __debug__:
|
||||||
print('Invalid journal entry "%s"' % repr(line))
|
print(f'Invalid journal entry {line!r}')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Can't read Journal file: {}\n".format(str(e)), file=sys.stderr)
|
print(f"Can't read Journal file: {str(e)}", file=sys.stderr)
|
||||||
sys.exit(EXIT_SYS_ERR)
|
sys.exit(EXIT_SYS_ERR)
|
||||||
|
|
||||||
if not monitor.cmdr:
|
if not monitor.cmdr:
|
||||||
sys.stderr.write('Not available while E:D is at the main menu\n')
|
print('Not available while E:D is at the main menu', file=sys.stderr)
|
||||||
sys.exit(EXIT_SYS_ERR)
|
sys.exit(EXIT_SYS_ERR)
|
||||||
|
|
||||||
# Get data from Companion API
|
# Get data from Companion API
|
||||||
@ -103,73 +129,100 @@ try:
|
|||||||
cmdrs = config.get('cmdrs') or []
|
cmdrs = config.get('cmdrs') or []
|
||||||
if args.p in cmdrs:
|
if args.p in cmdrs:
|
||||||
idx = cmdrs.index(args.p)
|
idx = cmdrs.index(args.p)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for idx, cmdr in enumerate(cmdrs):
|
for idx, cmdr in enumerate(cmdrs):
|
||||||
if cmdr.lower() == args.p.lower():
|
if cmdr.lower() == args.p.lower():
|
||||||
break
|
break
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise companion.CredentialsError()
|
raise companion.CredentialsError()
|
||||||
|
|
||||||
companion.session.login(cmdrs[idx], monitor.is_beta)
|
companion.session.login(cmdrs[idx], monitor.is_beta)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
cmdrs = config.get('cmdrs') or []
|
cmdrs = config.get('cmdrs') or []
|
||||||
if monitor.cmdr not in cmdrs:
|
if monitor.cmdr not in cmdrs:
|
||||||
raise companion.CredentialsError()
|
raise companion.CredentialsError()
|
||||||
|
|
||||||
companion.session.login(monitor.cmdr, monitor.is_beta)
|
companion.session.login(monitor.cmdr, monitor.is_beta)
|
||||||
|
|
||||||
querytime = int(time())
|
querytime = int(time())
|
||||||
data = companion.session.station()
|
data = companion.session.station()
|
||||||
config.set('querytime', querytime)
|
config.set('querytime', querytime)
|
||||||
|
|
||||||
# Validation
|
# Validation
|
||||||
if not data.get('commander') or not data['commander'].get('name','').strip():
|
if not deep_get(data, 'commander', 'name', default='').strip():
|
||||||
sys.stderr.write('Who are you?!\n')
|
print('Who are you?!', file=sys.stderr)
|
||||||
sys.exit(EXIT_SERVER)
|
sys.exit(EXIT_SERVER)
|
||||||
elif (not data.get('lastSystem', {}).get('name') or
|
|
||||||
(data['commander'].get('docked') and not data.get('lastStarport', {}).get('name'))): # Only care if docked
|
elif not deep_get(data, 'lastSystem', 'name') or \
|
||||||
sys.stderr.write('Where are you?!\n') # Shouldn't happen
|
data['commander'].get('docked') and not \
|
||||||
|
deep_get(data, 'lastStarport', 'name'): # Only care if docked
|
||||||
|
|
||||||
|
print('Where are you?!', file=sys.stderr) # Shouldn't happen
|
||||||
sys.exit(EXIT_SERVER)
|
sys.exit(EXIT_SERVER)
|
||||||
elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name','').strip():
|
|
||||||
sys.stderr.write('What are you flying?!\n') # Shouldn't happen
|
elif not deep_get(data, 'ship', 'modules') or not deep_get(data, 'ship', 'name', default=''):
|
||||||
|
print('What are you flying?!', file=sys.stderr) # Shouldn't happen
|
||||||
sys.exit(EXIT_SERVER)
|
sys.exit(EXIT_SERVER)
|
||||||
|
|
||||||
elif args.j:
|
elif args.j:
|
||||||
pass # Skip further validation
|
pass # Skip further validation
|
||||||
|
|
||||||
elif data['commander']['name'] != monitor.cmdr:
|
elif data['commander']['name'] != monitor.cmdr:
|
||||||
sys.stderr.write('Wrong Cmdr\n') # Companion API return doesn't match Journal
|
print('Wrong Cmdr', file=sys.stderr) # Companion API return doesn't match Journal
|
||||||
sys.exit(EXIT_CREDENTIALS)
|
sys.exit(EXIT_CREDENTIALS)
|
||||||
elif ((data['lastSystem']['name'] != monitor.system) or
|
|
||||||
((data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.station) or
|
elif data['lastSystem']['name'] != monitor.system or \
|
||||||
(data['ship']['id'] != monitor.state['ShipID']) or
|
((data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.station) or \
|
||||||
(data['ship']['name'].lower() != monitor.state['ShipType'])):
|
data['ship']['id'] != monitor.state['ShipID'] or \
|
||||||
sys.stderr.write('Frontier server is lagging\n')
|
data['ship']['name'].lower() != monitor.state['ShipType']:
|
||||||
|
|
||||||
|
print('Frontier server is lagging', file=sys.stderr)
|
||||||
sys.exit(EXIT_LAGGING)
|
sys.exit(EXIT_LAGGING)
|
||||||
|
|
||||||
# stuff we can do when not docked
|
# stuff we can do when not docked
|
||||||
if args.d:
|
if args.d:
|
||||||
with open(args.d, 'wb') as h:
|
out = json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': '))
|
||||||
h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8'))
|
with open(args.d, 'wb') as f:
|
||||||
|
f.write(out.encode("utf-8"))
|
||||||
|
|
||||||
if args.a:
|
if args.a:
|
||||||
loadout.export(data, args.a)
|
loadout.export(data, args.a)
|
||||||
|
|
||||||
if args.e:
|
if args.e:
|
||||||
edshipyard.export(data, args.e)
|
edshipyard.export(data, args.e)
|
||||||
|
|
||||||
if args.l:
|
if args.l:
|
||||||
stats.export_ships(data, args.l)
|
stats.export_ships(data, args.l)
|
||||||
|
|
||||||
if args.t:
|
if args.t:
|
||||||
stats.export_status(data, args.t)
|
stats.export_status(data, args.t)
|
||||||
|
|
||||||
if data['commander'].get('docked'):
|
if data['commander'].get('docked'):
|
||||||
print('%s,%s' % (data.get('lastSystem', {}).get('name', 'Unknown'), data.get('lastStarport', {}).get('name', 'Unknown')))
|
print('{},{}'.format(
|
||||||
|
deep_get(data, 'lastSystem', 'name', default='Unknown'),
|
||||||
|
deep_get(data, 'lastStarport', 'name', default='Unknown')
|
||||||
|
))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(data.get('lastSystem', {}).get('name', 'Unknown'))
|
print(deep_get(data, 'lastSystem', 'name', default='Unknown'))
|
||||||
|
|
||||||
if (args.m or args.o or args.s or args.n or args.j):
|
if (args.m or args.o or args.s or args.n or args.j):
|
||||||
if not data['commander'].get('docked'):
|
if not data['commander'].get('docked'):
|
||||||
sys.stderr.write("You're not docked at a station!\n")
|
print("You're not docked at a station!", file=sys.stderr)
|
||||||
sys.exit(EXIT_SUCCESS)
|
sys.exit(EXIT_SUCCESS)
|
||||||
elif not data.get('lastStarport', {}).get('name'):
|
|
||||||
sys.stderr.write("Unknown station!\n")
|
elif not deep_get(data, 'lastStarport', 'name'):
|
||||||
|
print("Unknown station!", file=sys.stderr)
|
||||||
sys.exit(EXIT_LAGGING)
|
sys.exit(EXIT_LAGGING)
|
||||||
elif not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')): # Ignore possibly missing shipyard info
|
|
||||||
sys.stderr.write("Station doesn't have anything!\n")
|
# Ignore possibly missing shipyard info
|
||||||
|
elif not data['lastStarport'].get('commodities') or data['lastStarport'].get('modules'):
|
||||||
|
print("Station doesn't have anything!", file=sys.stderr)
|
||||||
sys.exit(EXIT_SUCCESS)
|
sys.exit(EXIT_SUCCESS)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
sys.exit(EXIT_SUCCESS)
|
sys.exit(EXIT_SUCCESS)
|
||||||
|
|
||||||
@ -186,31 +239,39 @@ try:
|
|||||||
# Fixup anomalies in the commodity data
|
# Fixup anomalies in the commodity data
|
||||||
fixed = companion.fixup(data)
|
fixed = companion.fixup(data)
|
||||||
commodity.export(fixed, COMMODITY_DEFAULT, args.m)
|
commodity.export(fixed, COMMODITY_DEFAULT, args.m)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
sys.stderr.write("Station doesn't have a market\n")
|
print("Station doesn't have a market", file=sys.stderr)
|
||||||
|
|
||||||
if args.o:
|
if args.o:
|
||||||
if data['lastStarport'].get('modules'):
|
if data['lastStarport'].get('modules'):
|
||||||
outfitting.export(data, args.o)
|
outfitting.export(data, args.o)
|
||||||
else:
|
|
||||||
sys.stderr.write("Station doesn't supply outfitting\n")
|
|
||||||
|
|
||||||
if (args.s or args.n) and not args.j and not data['lastStarport'].get('ships') and data['lastStarport']['services'].get('shipyard'):
|
else:
|
||||||
|
print("Station doesn't supply outfitting", file=sys.stderr)
|
||||||
|
|
||||||
|
if (args.s or args.n) and not args.j and not \
|
||||||
|
data['lastStarport'].get('ships') and data['lastStarport']['services'].get('shipyard'):
|
||||||
|
|
||||||
# Retry for shipyard
|
# Retry for shipyard
|
||||||
sleep(SERVER_RETRY)
|
sleep(SERVER_RETRY)
|
||||||
data2 = companion.session.station()
|
new_data = companion.session.station()
|
||||||
if (data2['commander'].get('docked') and # might have undocked while we were waiting for retry in which case station data is unreliable
|
# might have undocked while we were waiting for retry in which case station data is unreliable
|
||||||
data2.get('lastSystem', {}).get('name') == monitor.system and
|
if new_data['commander'].get('docked') and \
|
||||||
data2.get('lastStarport', {}).get('name') == monitor.station):
|
deep_get(new_data, 'lastSystem', 'name') == monitor.system and \
|
||||||
data = data2
|
deep_get(new_data, 'lastStarport', 'name') == monitor.station:
|
||||||
|
|
||||||
|
data = new_data
|
||||||
|
|
||||||
if args.s:
|
if args.s:
|
||||||
if data['lastStarport'].get('ships', {}).get('shipyard_list'):
|
if deep_get(data, 'lastStarport', 'ships', 'shipyard_list'):
|
||||||
shipyard.export(data, args.s)
|
shipyard.export(data, args.s)
|
||||||
|
|
||||||
elif not args.j and monitor.stationservices and 'Shipyard' in monitor.stationservices:
|
elif not args.j and monitor.stationservices and 'Shipyard' in monitor.stationservices:
|
||||||
sys.stderr.write("Failed to get shipyard data\n")
|
print("Failed to get shipyard data", file=sys.stderr)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
sys.stderr.write("Station doesn't have a shipyard\n")
|
print("Station doesn't have a shipyard", file=sys.stderr)
|
||||||
|
|
||||||
if args.n:
|
if args.n:
|
||||||
try:
|
try:
|
||||||
@ -218,17 +279,24 @@ try:
|
|||||||
eddn_sender.export_commodities(data, monitor.is_beta)
|
eddn_sender.export_commodities(data, monitor.is_beta)
|
||||||
eddn_sender.export_outfitting(data, monitor.is_beta)
|
eddn_sender.export_outfitting(data, monitor.is_beta)
|
||||||
eddn_sender.export_shipyard(data, monitor.is_beta)
|
eddn_sender.export_shipyard(data, monitor.is_beta)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write("Failed to send data to EDDN: %s\n" % unicode(e).encode('ascii', 'replace'))
|
print(f"Failed to send data to EDDN: {str(e)}", file=sys.stderr)
|
||||||
|
|
||||||
sys.exit(EXIT_SUCCESS)
|
sys.exit(EXIT_SUCCESS)
|
||||||
|
|
||||||
except companion.ServerError as e:
|
except companion.ServerError:
|
||||||
sys.stderr.write('Server is down\n')
|
print('Server is down', file=sys.stderr)
|
||||||
sys.exit(EXIT_SERVER)
|
sys.exit(EXIT_SERVER)
|
||||||
except companion.SKUError as e:
|
|
||||||
sys.stderr.write('Server SKU problem\n')
|
except companion.SKUError:
|
||||||
|
print('Server SKU problem', file=sys.stderr)
|
||||||
sys.exit(EXIT_SERVER)
|
sys.exit(EXIT_SERVER)
|
||||||
except companion.CredentialsError as e:
|
|
||||||
sys.stderr.write('Invalid Credentials\n')
|
except companion.CredentialsError:
|
||||||
|
print('Invalid Credentials', file=sys.stderr)
|
||||||
sys.exit(EXIT_CREDENTIALS)
|
sys.exit(EXIT_CREDENTIALS)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
293
EDMCLogging.py
Normal file
293
EDMCLogging.py
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
"""
|
||||||
|
Set up required logging for the application.
|
||||||
|
|
||||||
|
This module provides for a common logging-powered log facility.
|
||||||
|
Mostly it implements a logging.Filter() in order to get two extra
|
||||||
|
members on the logging.LogRecord instance for use in logging.Formatter()
|
||||||
|
strings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# So that any warning about accessing a protected member is only in one place.
|
||||||
|
from sys import _getframe as getframe
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
import pathlib
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from config import config
|
||||||
|
|
||||||
|
# TODO: Tests:
|
||||||
|
#
|
||||||
|
# 1. Call from bare function in file.
|
||||||
|
# 2. Call from `if __name__ == "__main__":` section
|
||||||
|
#
|
||||||
|
# 3. Call from 1st level function in 1st level Class in file
|
||||||
|
# 4. Call from 2nd level function in 1st level Class in file
|
||||||
|
# 5. Call from 3rd level function in 1st level Class in file
|
||||||
|
#
|
||||||
|
# 6. Call from 1st level function in 2nd level Class in file
|
||||||
|
# 7. Call from 2nd level function in 2nd level Class in file
|
||||||
|
# 8. Call from 3rd level function in 2nd level Class in file
|
||||||
|
#
|
||||||
|
# 9. Call from 1st level function in 3rd level Class in file
|
||||||
|
# 10. Call from 2nd level function in 3rd level Class in file
|
||||||
|
# 11. Call from 3rd level function in 3rd level Class in file
|
||||||
|
#
|
||||||
|
# 12. Call from 2nd level file, all as above.
|
||||||
|
#
|
||||||
|
# 13. Call from *module*
|
||||||
|
#
|
||||||
|
# 14. Call from *package*
|
||||||
|
|
||||||
|
_default_loglevel = logging.DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
class Logger:
|
||||||
|
"""
|
||||||
|
Wrapper class for all logging configuration and code.
|
||||||
|
|
||||||
|
Class instantiation requires the 'logger name' and optional loglevel.
|
||||||
|
It is intended that this 'logger name' be re-used in all files/modules
|
||||||
|
that need to log.
|
||||||
|
|
||||||
|
Users of this class should then call getLogger() to get the
|
||||||
|
logging.Logger instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, logger_name: str, loglevel: int = _default_loglevel):
|
||||||
|
"""
|
||||||
|
Set up a `logging.Logger` with our preferred configuration.
|
||||||
|
|
||||||
|
This includes using an EDMCContextFilter to add 'class' and 'qualname'
|
||||||
|
expansions for logging.Formatter().
|
||||||
|
"""
|
||||||
|
self.logger = logging.getLogger(logger_name)
|
||||||
|
# Configure the logging.Logger
|
||||||
|
self.logger.setLevel(loglevel)
|
||||||
|
|
||||||
|
# Set up filter for adding class name
|
||||||
|
self.logger_filter = EDMCContextFilter()
|
||||||
|
self.logger.addFilter(self.logger_filter)
|
||||||
|
|
||||||
|
self.logger_channel = logging.StreamHandler()
|
||||||
|
self.logger_channel.setLevel(loglevel)
|
||||||
|
|
||||||
|
self.logger_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(module)s.%(qualname)s:%(lineno)d: %(message)s') # noqa: E501
|
||||||
|
self.logger_formatter.default_time_format = '%Y-%m-%d %H:%M:%S'
|
||||||
|
self.logger_formatter.default_msec_format = '%s.%03d'
|
||||||
|
|
||||||
|
self.logger_channel.setFormatter(self.logger_formatter)
|
||||||
|
self.logger.addHandler(self.logger_channel)
|
||||||
|
|
||||||
|
def get_logger(self) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Obtain the self.logger of the class instance.
|
||||||
|
|
||||||
|
Not to be confused with logging.getLogger().
|
||||||
|
"""
|
||||||
|
return self.logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_logger(name: str, loglevel: int = _default_loglevel) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Return a logger suitable for a plugin.
|
||||||
|
|
||||||
|
'Found' plugins need their own logger to call out where the logging is
|
||||||
|
coming from, but we don't need to set up *everything* for them.
|
||||||
|
|
||||||
|
The name will be '{config.appname}.{plugin.name}', e.g.
|
||||||
|
'EDMarketConnector.miggytest'. This means that any logging sent through
|
||||||
|
there *also* goes to the channels defined in the 'EDMarketConnector'
|
||||||
|
logger, so we can let that take care of the formatting.
|
||||||
|
|
||||||
|
If we add our own channel then the output gets duplicated (assuming same
|
||||||
|
logLevel set).
|
||||||
|
|
||||||
|
However we do need to attach our filter to this still. That's not at
|
||||||
|
the channel level.
|
||||||
|
:param name: Name of this Logger.
|
||||||
|
:param loglevel: Optional logLevel for this Logger.
|
||||||
|
:return: logging.Logger instance, all set up.
|
||||||
|
"""
|
||||||
|
plugin_logger = logging.getLogger(name)
|
||||||
|
plugin_logger.setLevel(loglevel)
|
||||||
|
|
||||||
|
plugin_logger.addFilter(EDMCContextFilter())
|
||||||
|
|
||||||
|
return plugin_logger
|
||||||
|
|
||||||
|
|
||||||
|
class EDMCContextFilter(logging.Filter):
|
||||||
|
"""
|
||||||
|
Implements filtering to add extra format specifiers, and tweak others.
|
||||||
|
|
||||||
|
logging.Filter sub-class to place extra attributes of the calling site
|
||||||
|
into the record.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
|
"""
|
||||||
|
Attempt to set/change fields in the LogRecord.
|
||||||
|
|
||||||
|
1. class = class name(s) of the call site, if applicable
|
||||||
|
2. qualname = __qualname__ of the call site. This simplifies
|
||||||
|
logging.Formatter() as you can use just this no matter if there is
|
||||||
|
a class involved or not, so you get a nice clean:
|
||||||
|
<file/module>.<classA>[.classB....].<function>
|
||||||
|
|
||||||
|
If we fail to be able to properly set either then:
|
||||||
|
|
||||||
|
1. Use print() to alert, to be SURE a message is seen.
|
||||||
|
2. But also return strings noting the error, so there'll be
|
||||||
|
something in the log output if it happens.
|
||||||
|
|
||||||
|
:param record: The LogRecord we're "filtering"
|
||||||
|
:return: bool - Always true in order for this record to be logged.
|
||||||
|
"""
|
||||||
|
(class_name, qualname, module_name) = self.caller_attributes(module_name=getattr(record, 'module'))
|
||||||
|
|
||||||
|
# Only set if we got a useful value
|
||||||
|
if module_name:
|
||||||
|
setattr(record, 'module', module_name)
|
||||||
|
|
||||||
|
# Only set if not already provided by logging itself
|
||||||
|
if getattr(record, 'class', None) is None:
|
||||||
|
setattr(record, 'class', class_name)
|
||||||
|
|
||||||
|
# Only set if not already provided by logging itself
|
||||||
|
if getattr(record, 'qualname', None) is None:
|
||||||
|
setattr(record, 'qualname', qualname)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod # noqa: CCR001 - this is as refactored as is sensible
|
||||||
|
def caller_attributes(cls, module_name: str = '') -> Tuple[str, str, str]:
|
||||||
|
"""
|
||||||
|
Determine extra or changed fields for the caller.
|
||||||
|
|
||||||
|
1. qualname finds the relevant object and its __qualname__
|
||||||
|
2. caller_class_names is just the full class names of the calling
|
||||||
|
class if relevant.
|
||||||
|
3. module is munged if we detect the caller is an EDMC plugin,
|
||||||
|
whether internal or found.
|
||||||
|
"""
|
||||||
|
frame = cls.find_caller_frame()
|
||||||
|
|
||||||
|
caller_qualname = caller_class_names = ''
|
||||||
|
if frame:
|
||||||
|
# <https://stackoverflow.com/questions/2203424/python-how-to-retrieve-class-information-from-a-frame-object#2220759>
|
||||||
|
frame_info = inspect.getframeinfo(frame)
|
||||||
|
args, _, _, value_dict = inspect.getargvalues(frame)
|
||||||
|
if len(args) and args[0] in ('self', 'cls'):
|
||||||
|
frame_class = value_dict[args[0]]
|
||||||
|
|
||||||
|
if frame_class:
|
||||||
|
# Find __qualname__ of the caller
|
||||||
|
fn = getattr(frame_class, frame_info.function)
|
||||||
|
if fn and fn.__qualname__:
|
||||||
|
caller_qualname = fn.__qualname__
|
||||||
|
|
||||||
|
# Find containing class name(s) of caller, if any
|
||||||
|
if frame_class.__class__ and frame_class.__class__.__qualname__:
|
||||||
|
caller_class_names = frame_class.__class__.__qualname__
|
||||||
|
|
||||||
|
# It's a call from the top level module file
|
||||||
|
elif frame_info.function == '<module>':
|
||||||
|
caller_class_names = '<none>'
|
||||||
|
caller_qualname = value_dict['__name__']
|
||||||
|
|
||||||
|
elif frame_info.function != '':
|
||||||
|
caller_class_names = '<none>'
|
||||||
|
caller_qualname = frame_info.function
|
||||||
|
|
||||||
|
module_name = cls.munge_module_name(frame_info, module_name)
|
||||||
|
|
||||||
|
# https://docs.python.org/3.7/library/inspect.html#the-interpreter-stack
|
||||||
|
del frame
|
||||||
|
|
||||||
|
if caller_qualname == '':
|
||||||
|
print('ALERT! Something went wrong with finding caller qualname for logging!')
|
||||||
|
caller_qualname = '<ERROR in EDMCLogging.caller_class_and_qualname() for "qualname">'
|
||||||
|
|
||||||
|
if caller_class_names == '':
|
||||||
|
print('ALERT! Something went wrong with finding caller class name(s) for logging!')
|
||||||
|
caller_class_names = '<ERROR in EDMCLogging.caller_class_and_qualname() for "class">'
|
||||||
|
|
||||||
|
return caller_class_names, caller_qualname, module_name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_caller_frame(cls):
|
||||||
|
"""
|
||||||
|
Find the stack frame of the logging caller.
|
||||||
|
|
||||||
|
:returns: 'frame' object such as from sys._getframe()
|
||||||
|
"""
|
||||||
|
# Go up through stack frames until we find the first with a
|
||||||
|
# type(f_locals.self) of logging.Logger. This should be the start
|
||||||
|
# of the frames internal to logging.
|
||||||
|
frame: 'frame' = getframe(0)
|
||||||
|
while frame:
|
||||||
|
if isinstance(frame.f_locals.get('self'), logging.Logger):
|
||||||
|
frame = frame.f_back # Want to start on the next frame below
|
||||||
|
break
|
||||||
|
frame = frame.f_back
|
||||||
|
# Now continue up through frames until we find the next one where
|
||||||
|
# that is *not* true, as it should be the call site of the logger
|
||||||
|
# call
|
||||||
|
while frame:
|
||||||
|
if not isinstance(frame.f_locals.get('self'), logging.Logger):
|
||||||
|
break # We've found the frame we want
|
||||||
|
frame = frame.f_back
|
||||||
|
return frame
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def munge_module_name(cls, frame_info: inspect.Traceback, module_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Adjust module_name based on the file path for the given frame.
|
||||||
|
|
||||||
|
We want to distinguish between other code and both our internal plugins
|
||||||
|
and the 'found' ones.
|
||||||
|
|
||||||
|
For internal plugins we want "plugins.<filename>".
|
||||||
|
For 'found' plugins we want "<plugins>.<plugin_name>...".
|
||||||
|
|
||||||
|
:param frame_info: The frame_info of the caller.
|
||||||
|
:param module_name: The module_name string to munge.
|
||||||
|
:return: The munged module_name.
|
||||||
|
"""
|
||||||
|
file_name = pathlib.Path(frame_info.filename).expanduser()
|
||||||
|
plugin_dir = pathlib.Path(config.plugin_dir).expanduser()
|
||||||
|
internal_plugin_dir = pathlib.Path(config.internal_plugin_dir).expanduser()
|
||||||
|
# Find the first parent called 'plugins'
|
||||||
|
plugin_top = file_name
|
||||||
|
while plugin_top and plugin_top.name != '':
|
||||||
|
if plugin_top.parent.name == 'plugins':
|
||||||
|
break
|
||||||
|
|
||||||
|
plugin_top = plugin_top.parent
|
||||||
|
|
||||||
|
# Check we didn't walk up to the root/anchor
|
||||||
|
if plugin_top.name != '':
|
||||||
|
# Check we're still inside config.plugin_dir
|
||||||
|
if plugin_top.parent == plugin_dir:
|
||||||
|
# In case of deeper callers we need a range of the file_name
|
||||||
|
pt_len = len(plugin_top.parts)
|
||||||
|
name_path = '.'.join(file_name.parts[(pt_len - 1):-1])
|
||||||
|
module_name = f'<plugins>.{name_path}.{module_name}'
|
||||||
|
|
||||||
|
# Check we're still inside the installation folder.
|
||||||
|
elif file_name.parent == internal_plugin_dir:
|
||||||
|
# Is this a deeper caller ?
|
||||||
|
pt_len = len(plugin_top.parts)
|
||||||
|
name_path = '.'.join(file_name.parts[(pt_len - 1):-1])
|
||||||
|
|
||||||
|
# Pre-pend 'plugins.<plugin folder>.' to module
|
||||||
|
if name_path == '':
|
||||||
|
# No sub-folder involved so module_name is sufficient
|
||||||
|
module_name = f'plugins.{module_name}'
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Sub-folder(s) involved, so include them
|
||||||
|
module_name = f'plugins.{name_path}.{module_name}'
|
||||||
|
|
||||||
|
return module_name
|
@ -5,19 +5,15 @@ from builtins import str
|
|||||||
from builtins import object
|
from builtins import object
|
||||||
import sys
|
import sys
|
||||||
from sys import platform
|
from sys import platform
|
||||||
from collections import OrderedDict
|
|
||||||
from functools import partial
|
|
||||||
import json
|
import json
|
||||||
from os import chdir, environ
|
from os import chdir, environ
|
||||||
from os.path import dirname, expanduser, isdir, join
|
from os.path import dirname, isdir, join
|
||||||
import re
|
import re
|
||||||
import html
|
import html
|
||||||
import requests
|
from time import time, localtime, strftime
|
||||||
from time import gmtime, time, localtime, strftime, strptime
|
|
||||||
import _strptime # Workaround for http://bugs.python.org/issue7980
|
|
||||||
from calendar import timegm
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
|
import EDMCLogging
|
||||||
from config import appname, applongname, appversion, appversion_nobuild, copyright, config
|
from config import appname, applongname, appversion, appversion_nobuild, copyright, config
|
||||||
|
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
@ -36,7 +32,6 @@ import tkinter.messagebox
|
|||||||
from ttkHyperlinkLabel import HyperlinkLabel
|
from ttkHyperlinkLabel import HyperlinkLabel
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
from traceback import print_exc
|
|
||||||
if platform != 'win32':
|
if platform != 'win32':
|
||||||
import pdb
|
import pdb
|
||||||
import signal
|
import signal
|
||||||
@ -99,10 +94,10 @@ class AppWindow(object):
|
|||||||
if platform == 'win32':
|
if platform == 'win32':
|
||||||
self.w.wm_iconbitmap(default='EDMarketConnector.ico')
|
self.w.wm_iconbitmap(default='EDMarketConnector.ico')
|
||||||
else:
|
else:
|
||||||
self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file = join(config.respath, 'EDMarketConnector.png')))
|
self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file=join(config.respath, 'EDMarketConnector.png'))) # noqa: E501
|
||||||
self.theme_icon = tk.PhotoImage(data = 'R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==')
|
self.theme_icon = tk.PhotoImage(data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501
|
||||||
self.theme_minimize = tk.BitmapImage(data = '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n')
|
self.theme_minimize = tk.BitmapImage(data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501
|
||||||
self.theme_close = tk.BitmapImage(data = '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n')
|
self.theme_close = tk.BitmapImage(data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501
|
||||||
|
|
||||||
frame = tk.Frame(self.w, name=appname.lower())
|
frame = tk.Frame(self.w, name=appname.lower())
|
||||||
frame.grid(sticky=tk.NSEW)
|
frame.grid(sticky=tk.NSEW)
|
||||||
@ -118,10 +113,10 @@ class AppWindow(object):
|
|||||||
self.system_label.grid(row=3, column=0, sticky=tk.W)
|
self.system_label.grid(row=3, column=0, sticky=tk.W)
|
||||||
self.station_label.grid(row=4, column=0, sticky=tk.W)
|
self.station_label.grid(row=4, column=0, sticky=tk.W)
|
||||||
|
|
||||||
self.cmdr = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name = 'cmdr')
|
self.cmdr = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='cmdr')
|
||||||
self.ship = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.shipyard_url, name = 'ship')
|
self.ship = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.shipyard_url, name='ship')
|
||||||
self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.system_url, popup_copy = True, name = 'system')
|
self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.system_url, popup_copy=True, name='system')
|
||||||
self.station = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.station_url, name = 'station')
|
self.station = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.station_url, name='station')
|
||||||
|
|
||||||
self.cmdr.grid(row=1, column=1, sticky=tk.EW)
|
self.cmdr.grid(row=1, column=1, sticky=tk.EW)
|
||||||
self.ship.grid(row=2, column=1, sticky=tk.EW)
|
self.ship.grid(row=2, column=1, sticky=tk.EW)
|
||||||
@ -132,38 +127,40 @@ class AppWindow(object):
|
|||||||
appitem = plugin.get_app(frame)
|
appitem = plugin.get_app(frame)
|
||||||
if appitem:
|
if appitem:
|
||||||
tk.Frame(frame, highlightthickness=1).grid(columnspan=2, sticky=tk.EW) # separator
|
tk.Frame(frame, highlightthickness=1).grid(columnspan=2, sticky=tk.EW) # separator
|
||||||
if isinstance(appitem, tuple) and len(appitem)==2:
|
if isinstance(appitem, tuple) and len(appitem) == 2:
|
||||||
row = frame.grid_size()[1]
|
row = frame.grid_size()[1]
|
||||||
appitem[0].grid(row=row, column=0, sticky=tk.W)
|
appitem[0].grid(row=row, column=0, sticky=tk.W)
|
||||||
appitem[1].grid(row=row, column=1, sticky=tk.EW)
|
appitem[1].grid(row=row, column=1, sticky=tk.EW)
|
||||||
else:
|
else:
|
||||||
appitem.grid(columnspan=2, sticky=tk.EW)
|
appitem.grid(columnspan=2, sticky=tk.EW)
|
||||||
|
|
||||||
self.button = ttk.Button(frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window
|
# Update button in main window
|
||||||
self.theme_button = tk.Label(frame, width = platform == 'darwin' and 32 or 28, state=tk.DISABLED)
|
self.button = ttk.Button(frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED)
|
||||||
|
self.theme_button = tk.Label(frame, width=32 if platform == 'darwin' else 28, state=tk.DISABLED)
|
||||||
self.status = tk.Label(frame, name='status', anchor=tk.W)
|
self.status = tk.Label(frame, name='status', anchor=tk.W)
|
||||||
|
|
||||||
row = frame.grid_size()[1]
|
row = frame.grid_size()[1]
|
||||||
self.button.grid(row=row, columnspan=2, sticky=tk.NSEW)
|
self.button.grid(row=row, columnspan=2, sticky=tk.NSEW)
|
||||||
self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW)
|
self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW)
|
||||||
theme.register_alternate((self.button, self.theme_button, self.theme_button), {'row':row, 'columnspan':2, 'sticky':tk.NSEW})
|
theme.register_alternate((self.button, self.theme_button, self.theme_button), {'row': row, 'columnspan': 2, 'sticky': tk.NSEW}) # noqa: E501
|
||||||
self.status.grid(columnspan=2, sticky=tk.EW)
|
self.status.grid(columnspan=2, sticky=tk.EW)
|
||||||
self.button.bind('<Button-1>', self.getandsend)
|
self.button.bind('<Button-1>', self.getandsend)
|
||||||
theme.button_bind(self.theme_button, self.getandsend)
|
theme.button_bind(self.theme_button, self.getandsend)
|
||||||
|
|
||||||
for child in frame.winfo_children():
|
for child in frame.winfo_children():
|
||||||
child.grid_configure(padx=5, pady=(platform!='win32' or isinstance(child, tk.Frame)) and 2 or 0)
|
child.grid_configure(padx=5, pady=(platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0)
|
||||||
|
|
||||||
self.menubar = tk.Menu()
|
self.menubar = tk.Menu()
|
||||||
if platform=='darwin':
|
if platform == 'darwin':
|
||||||
# Can't handle (de)iconify if topmost is set, so suppress iconify button
|
# Can't handle (de)iconify if topmost is set, so suppress iconify button
|
||||||
# http://wiki.tcl.tk/13428 and p15 of https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf
|
# http://wiki.tcl.tk/13428 and p15 of
|
||||||
|
# https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf
|
||||||
root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox resizable')
|
root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox resizable')
|
||||||
|
|
||||||
# https://www.tcl.tk/man/tcl/TkCmd/menu.htm
|
# https://www.tcl.tk/man/tcl/TkCmd/menu.htm
|
||||||
self.system_menu = tk.Menu(self.menubar, name='apple')
|
self.system_menu = tk.Menu(self.menubar, name='apple')
|
||||||
self.system_menu.add_command(command=lambda:self.w.call('tk::mac::standardAboutPanel'))
|
self.system_menu.add_command(command=lambda: self.w.call('tk::mac::standardAboutPanel'))
|
||||||
self.system_menu.add_command(command=lambda:self.updater.checkForUpdates())
|
self.system_menu.add_command(command=lambda: self.updater.checkForUpdates())
|
||||||
self.menubar.add_cascade(menu=self.system_menu)
|
self.menubar.add_cascade(menu=self.system_menu)
|
||||||
self.file_menu = tk.Menu(self.menubar, name='file')
|
self.file_menu = tk.Menu(self.menubar, name='file')
|
||||||
self.file_menu.add_command(command=self.save_raw)
|
self.file_menu.add_command(command=self.save_raw)
|
||||||
@ -173,7 +170,7 @@ class AppWindow(object):
|
|||||||
self.menubar.add_cascade(menu=self.edit_menu)
|
self.menubar.add_cascade(menu=self.edit_menu)
|
||||||
self.w.bind('<Command-c>', self.copy)
|
self.w.bind('<Command-c>', self.copy)
|
||||||
self.view_menu = tk.Menu(self.menubar, name='view')
|
self.view_menu = tk.Menu(self.menubar, name='view')
|
||||||
self.view_menu.add_command(command=lambda:stats.StatsDialog(self))
|
self.view_menu.add_command(command=lambda: stats.StatsDialog(self))
|
||||||
self.menubar.add_cascade(menu=self.view_menu)
|
self.menubar.add_cascade(menu=self.view_menu)
|
||||||
window_menu = tk.Menu(self.menubar, name='window')
|
window_menu = tk.Menu(self.menubar, name='window')
|
||||||
self.menubar.add_cascade(menu=window_menu)
|
self.menubar.add_cascade(menu=window_menu)
|
||||||
@ -185,17 +182,17 @@ class AppWindow(object):
|
|||||||
self.w['menu'] = self.menubar
|
self.w['menu'] = self.menubar
|
||||||
# https://www.tcl.tk/man/tcl/TkCmd/tk_mac.htm
|
# https://www.tcl.tk/man/tcl/TkCmd/tk_mac.htm
|
||||||
self.w.call('set', 'tk::mac::useCompatibilityMetrics', '0')
|
self.w.call('set', 'tk::mac::useCompatibilityMetrics', '0')
|
||||||
self.w.createcommand('tkAboutDialog', lambda:self.w.call('tk::mac::standardAboutPanel'))
|
self.w.createcommand('tkAboutDialog', lambda: self.w.call('tk::mac::standardAboutPanel'))
|
||||||
self.w.createcommand("::tk::mac::Quit", self.onexit)
|
self.w.createcommand("::tk::mac::Quit", self.onexit)
|
||||||
self.w.createcommand("::tk::mac::ShowPreferences", lambda:prefs.PreferencesDialog(self.w, self.postprefs))
|
self.w.createcommand("::tk::mac::ShowPreferences", lambda: prefs.PreferencesDialog(self.w, self.postprefs))
|
||||||
self.w.createcommand("::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore
|
self.w.createcommand("::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore
|
||||||
self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app
|
self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app
|
||||||
self.w.resizable(tk.FALSE, tk.FALSE) # Can't be only resizable on one axis
|
self.w.resizable(tk.FALSE, tk.FALSE) # Can't be only resizable on one axis
|
||||||
else:
|
else:
|
||||||
self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE)
|
self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE)
|
||||||
self.file_menu.add_command(command=lambda:stats.StatsDialog(self))
|
self.file_menu.add_command(command=lambda: stats.StatsDialog(self))
|
||||||
self.file_menu.add_command(command=self.save_raw)
|
self.file_menu.add_command(command=self.save_raw)
|
||||||
self.file_menu.add_command(command=lambda:prefs.PreferencesDialog(self.w, self.postprefs))
|
self.file_menu.add_command(command=lambda: prefs.PreferencesDialog(self.w, self.postprefs))
|
||||||
self.file_menu.add_separator()
|
self.file_menu.add_separator()
|
||||||
self.file_menu.add_command(command=self.onexit)
|
self.file_menu.add_command(command=self.onexit)
|
||||||
self.menubar.add_cascade(menu=self.file_menu)
|
self.menubar.add_cascade(menu=self.file_menu)
|
||||||
@ -206,16 +203,18 @@ class AppWindow(object):
|
|||||||
self.help_menu.add_command(command=self.help_general)
|
self.help_menu.add_command(command=self.help_general)
|
||||||
self.help_menu.add_command(command=self.help_privacy)
|
self.help_menu.add_command(command=self.help_privacy)
|
||||||
self.help_menu.add_command(command=self.help_releases)
|
self.help_menu.add_command(command=self.help_releases)
|
||||||
self.help_menu.add_command(command=lambda:self.updater.checkForUpdates())
|
self.help_menu.add_command(command=lambda: self.updater.checkForUpdates())
|
||||||
self.help_menu.add_command(command=lambda:not self.help_about.showing and self.help_about(self.w))
|
self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w))
|
||||||
|
|
||||||
self.menubar.add_cascade(menu=self.help_menu)
|
self.menubar.add_cascade(menu=self.help_menu)
|
||||||
if platform == 'win32':
|
if platform == 'win32':
|
||||||
# Must be added after at least one "real" menu entry
|
# Must be added after at least one "real" menu entry
|
||||||
self.always_ontop = tk.BooleanVar(value = config.getint('always_ontop'))
|
self.always_ontop = tk.BooleanVar(value=config.getint('always_ontop'))
|
||||||
self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE)
|
self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE)
|
||||||
self.system_menu.add_separator()
|
self.system_menu.add_separator()
|
||||||
self.system_menu.add_checkbutton(label=_('Always on top'), variable = self.always_ontop, command=self.ontop_changed) # Appearance setting
|
self.system_menu.add_checkbutton(label=_('Always on top'),
|
||||||
|
variable=self.always_ontop,
|
||||||
|
command=self.ontop_changed) # Appearance setting
|
||||||
self.menubar.add_cascade(menu=self.system_menu)
|
self.menubar.add_cascade(menu=self.system_menu)
|
||||||
self.w.bind('<Control-c>', self.copy)
|
self.w.bind('<Control-c>', self.copy)
|
||||||
self.w.protocol("WM_DELETE_WINDOW", self.onexit)
|
self.w.protocol("WM_DELETE_WINDOW", self.onexit)
|
||||||
@ -227,7 +226,9 @@ class AppWindow(object):
|
|||||||
# Alternate title bar and menu for dark theme
|
# Alternate title bar and menu for dark theme
|
||||||
self.theme_menubar = tk.Frame(frame)
|
self.theme_menubar = tk.Frame(frame)
|
||||||
self.theme_menubar.columnconfigure(2, weight=1)
|
self.theme_menubar.columnconfigure(2, weight=1)
|
||||||
theme_titlebar = tk.Label(self.theme_menubar, text=applongname, image=self.theme_icon, cursor='fleur', anchor=tk.W, compound=tk.LEFT)
|
theme_titlebar = tk.Label(self.theme_menubar, text=applongname,
|
||||||
|
image=self.theme_icon, cursor='fleur',
|
||||||
|
anchor=tk.W, compound=tk.LEFT)
|
||||||
theme_titlebar.grid(columnspan=3, padx=2, sticky=tk.NSEW)
|
theme_titlebar.grid(columnspan=3, padx=2, sticky=tk.NSEW)
|
||||||
self.drag_offset = None
|
self.drag_offset = None
|
||||||
theme_titlebar.bind('<Button-1>', self.drag_start)
|
theme_titlebar.bind('<Button-1>', self.drag_start)
|
||||||
@ -241,13 +242,22 @@ class AppWindow(object):
|
|||||||
theme.button_bind(theme_close, self.onexit, image=self.theme_close)
|
theme.button_bind(theme_close, self.onexit, image=self.theme_close)
|
||||||
self.theme_file_menu = tk.Label(self.theme_menubar, anchor=tk.W)
|
self.theme_file_menu = tk.Label(self.theme_menubar, anchor=tk.W)
|
||||||
self.theme_file_menu.grid(row=1, column=0, padx=5, sticky=tk.W)
|
self.theme_file_menu.grid(row=1, column=0, padx=5, sticky=tk.W)
|
||||||
theme.button_bind(self.theme_file_menu, lambda e: self.file_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height()))
|
theme.button_bind(self.theme_file_menu,
|
||||||
|
lambda e: self.file_menu.tk_popup(e.widget.winfo_rootx(),
|
||||||
|
e.widget.winfo_rooty()
|
||||||
|
+ e.widget.winfo_height()))
|
||||||
self.theme_edit_menu = tk.Label(self.theme_menubar, anchor=tk.W)
|
self.theme_edit_menu = tk.Label(self.theme_menubar, anchor=tk.W)
|
||||||
self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W)
|
self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W)
|
||||||
theme.button_bind(self.theme_edit_menu, lambda e: self.edit_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height()))
|
theme.button_bind(self.theme_edit_menu,
|
||||||
|
lambda e: self.edit_menu.tk_popup(e.widget.winfo_rootx(),
|
||||||
|
e.widget.winfo_rooty()
|
||||||
|
+ e.widget.winfo_height()))
|
||||||
self.theme_help_menu = tk.Label(self.theme_menubar, anchor=tk.W)
|
self.theme_help_menu = tk.Label(self.theme_menubar, anchor=tk.W)
|
||||||
self.theme_help_menu.grid(row=1, column=2, sticky=tk.W)
|
self.theme_help_menu.grid(row=1, column=2, sticky=tk.W)
|
||||||
theme.button_bind(self.theme_help_menu, lambda e: self.help_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height()))
|
theme.button_bind(self.theme_help_menu,
|
||||||
|
lambda e: self.help_menu.tk_popup(e.widget.winfo_rootx(),
|
||||||
|
e.widget.winfo_rooty()
|
||||||
|
+ e.widget.winfo_height()))
|
||||||
tk.Frame(self.theme_menubar, highlightthickness=1).grid(columnspan=5, padx=5, sticky=tk.EW)
|
tk.Frame(self.theme_menubar, highlightthickness=1).grid(columnspan=5, padx=5, sticky=tk.EW)
|
||||||
theme.register(self.theme_minimize) # images aren't automatically registered
|
theme.register(self.theme_minimize) # images aren't automatically registered
|
||||||
theme.register(self.theme_close)
|
theme.register(self.theme_close)
|
||||||
@ -255,23 +265,26 @@ class AppWindow(object):
|
|||||||
tk.Label(self.blank_menubar).grid()
|
tk.Label(self.blank_menubar).grid()
|
||||||
tk.Label(self.blank_menubar).grid()
|
tk.Label(self.blank_menubar).grid()
|
||||||
tk.Frame(self.blank_menubar, height=2).grid()
|
tk.Frame(self.blank_menubar, height=2).grid()
|
||||||
theme.register_alternate((self.menubar, self.theme_menubar, self.blank_menubar), {'row':0, 'columnspan':2, 'sticky':tk.NSEW})
|
theme.register_alternate((self.menubar, self.theme_menubar, self.blank_menubar),
|
||||||
|
{'row': 0, 'columnspan': 2, 'sticky': tk.NSEW})
|
||||||
self.w.resizable(tk.TRUE, tk.FALSE)
|
self.w.resizable(tk.TRUE, tk.FALSE)
|
||||||
|
|
||||||
# update geometry
|
# update geometry
|
||||||
if config.get('geometry'):
|
if config.get('geometry'):
|
||||||
match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry'))
|
match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) # noqa: W605
|
||||||
if match:
|
if match:
|
||||||
if platform == 'darwin':
|
if platform == 'darwin':
|
||||||
if int(match.group(2)) >= 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||||
|
if int(match.group(2)) >= 0:
|
||||||
self.w.geometry(config.get('geometry'))
|
self.w.geometry(config.get('geometry'))
|
||||||
elif platform == 'win32':
|
elif platform == 'win32':
|
||||||
# Check that the titlebar will be at least partly on screen
|
# Check that the titlebar will be at least partly on screen
|
||||||
import ctypes
|
import ctypes
|
||||||
from ctypes.wintypes import POINT
|
from ctypes.wintypes import POINT
|
||||||
# https://msdn.microsoft.com/en-us/library/dd145064
|
# https://msdn.microsoft.com/en-us/library/dd145064
|
||||||
MONITOR_DEFAULTTONULL = 0
|
MONITOR_DEFAULTTONULL = 0 # noqa: N806
|
||||||
if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16), MONITOR_DEFAULTTONULL):
|
if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16),
|
||||||
|
MONITOR_DEFAULTTONULL):
|
||||||
self.w.geometry(config.get('geometry'))
|
self.w.geometry(config.get('geometry'))
|
||||||
else:
|
else:
|
||||||
self.w.geometry(config.get('geometry'))
|
self.w.geometry(config.get('geometry'))
|
||||||
@ -282,9 +295,9 @@ class AppWindow(object):
|
|||||||
|
|
||||||
self.w.bind('<Map>', self.onmap) # Special handling for overrideredict
|
self.w.bind('<Map>', self.onmap) # Special handling for overrideredict
|
||||||
self.w.bind('<Enter>', self.onenter) # Special handling for transparency
|
self.w.bind('<Enter>', self.onenter) # Special handling for transparency
|
||||||
self.w.bind('<FocusIn>', self.onenter) # "
|
self.w.bind('<FocusIn>', self.onenter) # Special handling for transparency
|
||||||
self.w.bind('<Leave>', self.onleave) # "
|
self.w.bind('<Leave>', self.onleave) # Special handling for transparency
|
||||||
self.w.bind('<FocusOut>', self.onleave) # "
|
self.w.bind('<FocusOut>', self.onleave) # Special handling for transparency
|
||||||
self.w.bind('<Return>', self.getandsend)
|
self.w.bind('<Return>', self.getandsend)
|
||||||
self.w.bind('<KP_Enter>', self.getandsend)
|
self.w.bind('<KP_Enter>', self.getandsend)
|
||||||
self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring
|
self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring
|
||||||
@ -295,7 +308,7 @@ class AppWindow(object):
|
|||||||
self.w.bind_all('<<Quit>>', self.onexit) # Updater
|
self.w.bind_all('<<Quit>>', self.onexit) # Updater
|
||||||
|
|
||||||
# Start a protocol handler to handle cAPI registration. Requires main loop to be running.
|
# Start a protocol handler to handle cAPI registration. Requires main loop to be running.
|
||||||
self.w.after_idle(lambda:protocolhandler.start(self.w))
|
self.w.after_idle(lambda: protocolhandler.start(self.w))
|
||||||
|
|
||||||
# Load updater after UI creation (for WinSparkle)
|
# Load updater after UI creation (for WinSparkle)
|
||||||
import update
|
import update
|
||||||
@ -321,23 +334,22 @@ class AppWindow(object):
|
|||||||
|
|
||||||
self.postprefs(False) # Companion login happens in callback from monitor
|
self.postprefs(False) # Companion login happens in callback from monitor
|
||||||
|
|
||||||
|
|
||||||
# callback after the Preferences dialog is applied
|
# callback after the Preferences dialog is applied
|
||||||
def postprefs(self, dologin=True):
|
def postprefs(self, dologin=True):
|
||||||
self.prefsdialog = None
|
self.prefsdialog = None
|
||||||
self.set_labels() # in case language has changed
|
self.set_labels() # in case language has changed
|
||||||
|
|
||||||
# Reset links in case plugins changed them
|
# Reset links in case plugins changed them
|
||||||
self.ship.configure(url = self.shipyard_url)
|
self.ship.configure(url=self.shipyard_url)
|
||||||
self.system.configure(url = self.system_url)
|
self.system.configure(url=self.system_url)
|
||||||
self.station.configure(url = self.station_url)
|
self.station.configure(url=self.station_url)
|
||||||
|
|
||||||
# (Re-)install hotkey monitoring
|
# (Re-)install hotkey monitoring
|
||||||
hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods'))
|
hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods'))
|
||||||
|
|
||||||
# (Re-)install log monitoring
|
# (Re-)install log monitoring
|
||||||
if not monitor.start(self.w):
|
if not monitor.start(self.w):
|
||||||
self.status['text'] = 'Error: Check %s' % _('E:D journal file location') # Location of the new Journal file in E:D 2.2
|
self.status['text'] = f'Error: Check {_("E:D journal file location")}'
|
||||||
|
|
||||||
if dologin and monitor.cmdr:
|
if dologin and monitor.cmdr:
|
||||||
self.login() # Login if not already logged in with this Cmdr
|
self.login() # Login if not already logged in with this Cmdr
|
||||||
@ -345,8 +357,8 @@ class AppWindow(object):
|
|||||||
# set main window labels, e.g. after language change
|
# set main window labels, e.g. after language change
|
||||||
def set_labels(self):
|
def set_labels(self):
|
||||||
self.cmdr_label['text'] = _('Cmdr') + ':' # Main window
|
self.cmdr_label['text'] = _('Cmdr') + ':' # Main window
|
||||||
self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or # Multicrew role label in main window
|
# Multicrew role label in main window
|
||||||
_('Ship')) + ':' # Main window
|
self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or _('Ship')) + ':' # Main window
|
||||||
self.system_label['text'] = _('System') + ':' # Main window
|
self.system_label['text'] = _('System') + ':' # Main window
|
||||||
self.station_label['text'] = _('Station') + ':' # Main window
|
self.station_label['text'] = _('Station') + ':' # Main window
|
||||||
self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window
|
self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window
|
||||||
@ -370,20 +382,20 @@ class AppWindow(object):
|
|||||||
self.theme_edit_menu['text'] = _('Edit') # Menu title
|
self.theme_edit_menu['text'] = _('Edit') # Menu title
|
||||||
self.theme_help_menu['text'] = _('Help') # Menu title
|
self.theme_help_menu['text'] = _('Help') # Menu title
|
||||||
|
|
||||||
## File menu
|
# File menu
|
||||||
self.file_menu.entryconfigure(0, label=_('Status')) # Menu item
|
self.file_menu.entryconfigure(0, label=_('Status')) # Menu item
|
||||||
self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # Menu item
|
self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # Menu item
|
||||||
self.file_menu.entryconfigure(2, label=_('Settings')) # Item in the File menu on Windows
|
self.file_menu.entryconfigure(2, label=_('Settings')) # Item in the File menu on Windows
|
||||||
self.file_menu.entryconfigure(4, label=_('Exit')) # Item in the File menu on Windows
|
self.file_menu.entryconfigure(4, label=_('Exit')) # Item in the File menu on Windows
|
||||||
|
|
||||||
## Help menu
|
# Help menu
|
||||||
self.help_menu.entryconfigure(0, label=_('Documentation')) # Help menu item
|
self.help_menu.entryconfigure(0, label=_('Documentation')) # Help menu item
|
||||||
self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # Help menu item
|
self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # Help menu item
|
||||||
self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item
|
self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item
|
||||||
self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # Menu item
|
self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # Menu item
|
||||||
self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # App menu entry
|
self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # App menu entry
|
||||||
|
|
||||||
## Edit menu
|
# Edit menu
|
||||||
self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste
|
self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
@ -399,7 +411,8 @@ class AppWindow(object):
|
|||||||
self.w.update_idletasks()
|
self.w.update_idletasks()
|
||||||
try:
|
try:
|
||||||
if companion.session.login(monitor.cmdr, monitor.is_beta):
|
if companion.session.login(monitor.cmdr, monitor.is_beta):
|
||||||
self.status['text'] = _('Authentication successful') # Successfully authenticated with the Frontier website
|
# Successfully authenticated with the Frontier website
|
||||||
|
self.status['text'] = _('Authentication successful')
|
||||||
if platform == 'darwin':
|
if platform == 'darwin':
|
||||||
self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status
|
self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status
|
||||||
self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data
|
self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data
|
||||||
@ -409,7 +422,7 @@ class AppWindow(object):
|
|||||||
except (companion.CredentialsError, companion.ServerError, companion.ServerLagging) as e:
|
except (companion.CredentialsError, companion.ServerError, companion.ServerLagging) as e:
|
||||||
self.status['text'] = str(e)
|
self.status['text'] = str(e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if __debug__: print_exc()
|
logger.debug('Frontier CAPI Auth', exc_info=e)
|
||||||
self.status['text'] = str(e)
|
self.status['text'] = str(e)
|
||||||
self.cooldown()
|
self.cooldown()
|
||||||
|
|
||||||
@ -447,26 +460,36 @@ class AppWindow(object):
|
|||||||
# Validation
|
# Validation
|
||||||
if not data.get('commander', {}).get('name'):
|
if not data.get('commander', {}).get('name'):
|
||||||
self.status['text'] = _("Who are you?!") # Shouldn't happen
|
self.status['text'] = _("Who are you?!") # Shouldn't happen
|
||||||
elif (not data.get('lastSystem', {}).get('name') or
|
elif (not data.get('lastSystem', {}).get('name')
|
||||||
(data['commander'].get('docked') and not data.get('lastStarport', {}).get('name'))): # Only care if docked
|
or (data['commander'].get('docked')
|
||||||
|
and not data.get('lastStarport', {}).get('name'))): # Only care if docked
|
||||||
self.status['text'] = _("Where are you?!") # Shouldn't happen
|
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 data.get('ship', {}).get('name') or not data.get('ship', {}).get('modules'):
|
||||||
self.status['text'] = _("What are you flying?!") # Shouldn't happen
|
self.status['text'] = _("What are you flying?!") # Shouldn't happen
|
||||||
elif monitor.cmdr and data['commander']['name'] != monitor.cmdr:
|
elif monitor.cmdr and data['commander']['name'] != monitor.cmdr:
|
||||||
raise companion.CmdrError() # Companion API return doesn't match Journal
|
# Companion API return doesn't match Journal
|
||||||
elif ((auto_update and not data['commander'].get('docked')) or
|
raise companion.CmdrError()
|
||||||
(data['lastSystem']['name'] != monitor.system) or
|
elif ((auto_update and not data['commander'].get('docked'))
|
||||||
((data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.station) or
|
or (data['lastSystem']['name'] != monitor.system)
|
||||||
(data['ship']['id'] != monitor.state['ShipID']) or
|
or ((data['commander']['docked']
|
||||||
(data['ship']['name'].lower() != monitor.state['ShipType'])):
|
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()
|
raise companion.ServerLagging()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
if __debug__: # Recording
|
if __debug__: # Recording
|
||||||
if isdir('dump'):
|
if isdir('dump'):
|
||||||
with open('dump/%s%s.%s.json' % (data['lastSystem']['name'], data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wb') as h:
|
with open('dump/{system}{station}.{timestamp}.json'.format(
|
||||||
h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8'))
|
system=data['lastSystem']['name'],
|
||||||
|
station=data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '',
|
||||||
|
timestamp=strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wb') as h:
|
||||||
|
h.write(json.dumps(data,
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(',', ': ')).encode('utf-8'))
|
||||||
|
|
||||||
if not monitor.state['ShipType']: # Started game in SRV or fighter
|
if not monitor.state['ShipType']: # Started game in SRV or fighter
|
||||||
self.ship['text'] = companion.ship_map.get(data['ship']['name'].lower(), data['ship']['name'])
|
self.ship['text'] = companion.ship_map.get(data['ship']['name'].lower(), data['ship']['name'])
|
||||||
@ -487,16 +510,19 @@ class AppWindow(object):
|
|||||||
if config.getint('output') & (config.OUT_STATION_ANY):
|
if config.getint('output') & (config.OUT_STATION_ANY):
|
||||||
if not data['commander'].get('docked'):
|
if not data['commander'].get('docked'):
|
||||||
if not self.status['text']:
|
if not self.status['text']:
|
||||||
# Signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up
|
# Signal as error because the user might actually be docked
|
||||||
|
# but the server hosting the Companion API hasn't caught up
|
||||||
self.status['text'] = _("You're not docked at a station!")
|
self.status['text'] = _("You're not docked at a station!")
|
||||||
play_bad = True
|
play_bad = True
|
||||||
elif (config.getint('output') & config.OUT_MKT_EDDN) and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')): # Ignore possibly missing shipyard info
|
# Ignore possibly missing shipyard info
|
||||||
|
elif (config.getint('output') & config.OUT_MKT_EDDN)\
|
||||||
|
and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')):
|
||||||
if not self.status['text']:
|
if not self.status['text']:
|
||||||
self.status['text'] = _("Station doesn't have anything!")
|
self.status['text'] = _("Station doesn't have anything!")
|
||||||
elif not data['lastStarport'].get('commodities'):
|
elif not data['lastStarport'].get('commodities'):
|
||||||
if not self.status['text']:
|
if not self.status['text']:
|
||||||
self.status['text'] = _("Station doesn't have a market!")
|
self.status['text'] = _("Station doesn't have a market!")
|
||||||
elif config.getint('output') & (config.OUT_MKT_CSV|config.OUT_MKT_TD):
|
elif config.getint('output') & (config.OUT_MKT_CSV | config.OUT_MKT_TD):
|
||||||
# Fixup anomalies in the commodity data
|
# Fixup anomalies in the commodity data
|
||||||
fixed = companion.fixup(data)
|
fixed = companion.fixup(data)
|
||||||
if config.getint('output') & config.OUT_MKT_CSV:
|
if config.getint('output') & config.OUT_MKT_CSV:
|
||||||
@ -513,7 +539,7 @@ class AppWindow(object):
|
|||||||
play_bad = True
|
play_bad = True
|
||||||
else:
|
else:
|
||||||
# Retry once if Companion server is unresponsive
|
# Retry once if Companion server is unresponsive
|
||||||
self.w.after(int(SERVER_RETRY * 1000), lambda:self.getandsend(event, True))
|
self.w.after(int(SERVER_RETRY * 1000), lambda: self.getandsend(event, True))
|
||||||
return # early exit to avoid starting cooldown count
|
return # early exit to avoid starting cooldown count
|
||||||
|
|
||||||
except companion.CmdrError as e: # Companion API return doesn't match Journal
|
except companion.CmdrError as e: # Companion API return doesn't match Journal
|
||||||
@ -523,12 +549,12 @@ class AppWindow(object):
|
|||||||
self.login()
|
self.login()
|
||||||
|
|
||||||
except Exception as e: # Including CredentialsError, ServerError
|
except Exception as e: # Including CredentialsError, ServerError
|
||||||
if __debug__: print_exc()
|
logger.debug('"other" exception', exc_info=e)
|
||||||
self.status['text'] = str(e)
|
self.status['text'] = str(e)
|
||||||
play_bad = True
|
play_bad = True
|
||||||
|
|
||||||
if not self.status['text']: # no errors
|
if not self.status['text']: # no errors
|
||||||
self.status['text'] = strftime(_('Last updated at %H:%M:%S').format(HH='%H', MM='%M', SS='%S'), localtime(querytime))
|
self.status['text'] = strftime(_('Last updated at %H:%M:%S'), localtime(querytime))
|
||||||
if play_sound and play_bad:
|
if play_sound and play_bad:
|
||||||
hotkeymgr.play_bad()
|
hotkeymgr.play_bad()
|
||||||
|
|
||||||
@ -538,17 +564,24 @@ class AppWindow(object):
|
|||||||
# Try again to get shipyard data and send to EDDN. Don't report errors if can't get or send the data.
|
# Try again to get shipyard data and send to EDDN. Don't report errors if can't get or send the data.
|
||||||
try:
|
try:
|
||||||
data = companion.session.station()
|
data = companion.session.station()
|
||||||
if __debug__:
|
if data['commander'].get('docked'):
|
||||||
print('Retry for shipyard - ' + (data['commander'].get('docked') and (data.get('lastStarport', {}).get('ships') and 'Success' or 'Failure') or 'Undocked!'))
|
if data.get('lastStarport', {}).get('ships'):
|
||||||
|
report = 'Success'
|
||||||
|
else:
|
||||||
|
report = 'Failure'
|
||||||
|
else:
|
||||||
|
report = 'Undocked!'
|
||||||
|
logger.debug(f'Retry for shipyard - {report}')
|
||||||
if not data['commander'].get('docked'):
|
if not data['commander'].get('docked'):
|
||||||
pass # might have undocked while we were waiting for retry in which case station data is unreliable
|
# might have un-docked while we were waiting for retry in which case station data is unreliable
|
||||||
|
pass
|
||||||
elif (data.get('lastSystem', {}).get('name') == monitor.system and
|
elif (data.get('lastSystem', {}).get('name') == monitor.system and
|
||||||
data.get('lastStarport', {}).get('name') == monitor.station and
|
data.get('lastStarport', {}).get('name') == monitor.station and
|
||||||
data.get('lastStarport', {}).get('ships', {}).get('shipyard_list')):
|
data.get('lastStarport', {}).get('ships', {}).get('shipyard_list')):
|
||||||
self.eddn.export_shipyard(data, monitor.is_beta)
|
self.eddn.export_shipyard(data, monitor.is_beta)
|
||||||
elif tries > 1: # bogus data - retry
|
elif tries > 1: # bogus data - retry
|
||||||
self.w.after(int(SERVER_RETRY * 1000), lambda:self.retry_for_shipyard(tries-1))
|
self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries-1))
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Handle event(s) from the journal
|
# Handle event(s) from the journal
|
||||||
@ -572,17 +605,20 @@ class AppWindow(object):
|
|||||||
# Update main window
|
# Update main window
|
||||||
self.cooldown()
|
self.cooldown()
|
||||||
if monitor.cmdr and monitor.state['Captain']:
|
if monitor.cmdr and monitor.state['Captain']:
|
||||||
self.cmdr['text'] = '%s / %s' % (monitor.cmdr, monitor.state['Captain'])
|
self.cmdr['text'] = f'{monitor.cmdr} / {monitor.state["Captain"]}'
|
||||||
self.ship_label['text'] = _('Role') + ':' # Multicrew role label in main window
|
self.ship_label['text'] = _('Role') + ':' # Multicrew role label in main window
|
||||||
self.ship.configure(state = tk.NORMAL, text = crewroletext(monitor.state['Role']), url = None)
|
self.ship.configure(state=tk.NORMAL, text=crewroletext(monitor.state['Role']), url=None)
|
||||||
elif monitor.cmdr:
|
elif monitor.cmdr:
|
||||||
if monitor.group:
|
if monitor.group:
|
||||||
self.cmdr['text'] = '%s / %s' % (monitor.cmdr, monitor.group)
|
self.cmdr['text'] = f'{monitor.cmdr} / {monitor.group}'
|
||||||
else:
|
else:
|
||||||
self.cmdr['text'] = monitor.cmdr
|
self.cmdr['text'] = monitor.cmdr
|
||||||
self.ship_label['text'] = _('Ship') + ':' # Main window
|
self.ship_label['text'] = _('Ship') + ':' # Main window
|
||||||
self.ship.configure(text = monitor.state['ShipName'] or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType']) or '',
|
self.ship.configure(
|
||||||
url = self.shipyard_url)
|
text=monitor.state['ShipName']
|
||||||
|
or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType'])
|
||||||
|
or '',
|
||||||
|
url=self.shipyard_url)
|
||||||
else:
|
else:
|
||||||
self.cmdr['text'] = ''
|
self.cmdr['text'] = ''
|
||||||
self.ship_label['text'] = _('Ship') + ':' # Main window
|
self.ship_label['text'] = _('Ship') + ':' # Main window
|
||||||
@ -590,7 +626,21 @@ class AppWindow(object):
|
|||||||
|
|
||||||
self.edit_menu.entryconfigure(0, state=monitor.system and tk.NORMAL or tk.DISABLED) # Copy
|
self.edit_menu.entryconfigure(0, state=monitor.system and tk.NORMAL or tk.DISABLED) # Copy
|
||||||
|
|
||||||
if entry['event'] in ['Undocked', 'StartJump', 'SetUserShipName', 'ShipyardBuy', 'ShipyardSell', 'ShipyardSwap', 'ModuleBuy', 'ModuleSell', 'MaterialCollected', 'MaterialDiscarded', 'ScientificResearch', 'EngineerCraft', 'Synthesis', 'JoinACrew']:
|
if entry['event'] in (
|
||||||
|
'Undocked',
|
||||||
|
'StartJump',
|
||||||
|
'SetUserShipName',
|
||||||
|
'ShipyardBuy',
|
||||||
|
'ShipyardSell',
|
||||||
|
'ShipyardSwap',
|
||||||
|
'ModuleBuy',
|
||||||
|
'ModuleSell',
|
||||||
|
'MaterialCollected',
|
||||||
|
'MaterialDiscarded',
|
||||||
|
'ScientificResearch',
|
||||||
|
'EngineerCraft',
|
||||||
|
'Synthesis',
|
||||||
|
'JoinACrew'):
|
||||||
self.status['text'] = '' # Periodically clear any old error
|
self.status['text'] = '' # Periodically clear any old error
|
||||||
self.w.update_idletasks()
|
self.w.update_idletasks()
|
||||||
|
|
||||||
@ -607,37 +657,48 @@ class AppWindow(object):
|
|||||||
# Disable WinSparkle automatic update checks, IFF configured to do so when in-game
|
# Disable WinSparkle automatic update checks, IFF configured to do so when in-game
|
||||||
if config.getint('disable_autoappupdatecheckingame') and 1:
|
if config.getint('disable_autoappupdatecheckingame') and 1:
|
||||||
self.updater.setAutomaticUpdatesCheck(False)
|
self.updater.setAutomaticUpdatesCheck(False)
|
||||||
print('Monitor: Disable WinSparkle automatic update checks')
|
logger.info('Monitor: Disable WinSparkle automatic update checks')
|
||||||
# Can start dashboard monitoring
|
# Can start dashboard monitoring
|
||||||
if not dashboard.start(self.w, monitor.started):
|
if not dashboard.start(self.w, monitor.started):
|
||||||
print("Can't start Status monitoring")
|
logger.info("Can't start Status monitoring")
|
||||||
|
|
||||||
# Export loadout
|
# Export loadout
|
||||||
if entry['event'] == 'Loadout' and not monitor.state['Captain'] and config.getint('output') & config.OUT_SHIP:
|
if entry['event'] == 'Loadout' and not monitor.state['Captain']\
|
||||||
|
and config.getint('output') & config.OUT_SHIP:
|
||||||
monitor.export_ship()
|
monitor.export_ship()
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
err = plug.notify_journal_entry(monitor.cmdr, monitor.is_beta, monitor.system, monitor.station, entry, monitor.state)
|
err = plug.notify_journal_entry(monitor.cmdr,
|
||||||
|
monitor.is_beta,
|
||||||
|
monitor.system,
|
||||||
|
monitor.station,
|
||||||
|
entry,
|
||||||
|
monitor.state)
|
||||||
if err:
|
if err:
|
||||||
self.status['text'] = err
|
self.status['text'] = err
|
||||||
if not config.getint('hotkey_mute'):
|
if not config.getint('hotkey_mute'):
|
||||||
hotkeymgr.play_bad()
|
hotkeymgr.play_bad()
|
||||||
|
|
||||||
# Auto-Update after docking, but not if auth callback is pending
|
# Auto-Update after docking, but not if auth callback is pending
|
||||||
if entry['event'] in ['StartUp', 'Location', 'Docked'] and monitor.station and not config.getint('output') & config.OUT_MKT_MANUAL and config.getint('output') & config.OUT_STATION_ANY and companion.session.state != companion.Session.STATE_AUTH:
|
if entry['event'] in ('StartUp', 'Location', 'Docked')\
|
||||||
|
and monitor.station\
|
||||||
|
and not config.getint('output') & config.OUT_MKT_MANUAL\
|
||||||
|
and config.getint('output') & config.OUT_STATION_ANY\
|
||||||
|
and companion.session.state != companion.Session.STATE_AUTH:
|
||||||
self.w.after(int(SERVER_RETRY * 1000), self.getandsend)
|
self.w.after(int(SERVER_RETRY * 1000), self.getandsend)
|
||||||
|
|
||||||
if entry['event'] == 'ShutDown':
|
if entry['event'] == 'ShutDown':
|
||||||
# Enable WinSparkle automatic update checks
|
# Enable WinSparkle automatic update checks
|
||||||
# NB: Do this blindly, in case option got changed whilst in-game
|
# NB: Do this blindly, in case option got changed whilst in-game
|
||||||
self.updater.setAutomaticUpdatesCheck(True)
|
self.updater.setAutomaticUpdatesCheck(True)
|
||||||
print('Monitor: Enable WinSparkle automatic update checks')
|
logger.info('Monitor: Enable WinSparkle automatic update checks')
|
||||||
|
|
||||||
# cAPI auth
|
# cAPI auth
|
||||||
def auth(self, event=None):
|
def auth(self, event=None):
|
||||||
try:
|
try:
|
||||||
companion.session.auth_callback()
|
companion.session.auth_callback()
|
||||||
self.status['text'] = _('Authentication successful') # Successfully authenticated with the Frontier website
|
# Successfully authenticated with the Frontier website
|
||||||
|
self.status['text'] = _('Authentication successful')
|
||||||
if platform == 'darwin':
|
if platform == 'darwin':
|
||||||
self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status
|
self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status
|
||||||
self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data
|
self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data
|
||||||
@ -647,7 +708,7 @@ class AppWindow(object):
|
|||||||
except companion.ServerError as e:
|
except companion.ServerError as e:
|
||||||
self.status['text'] = str(e)
|
self.status['text'] = str(e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if __debug__: print_exc()
|
logger.debug('Frontier CAPI Auth:', exc_info=e)
|
||||||
self.status['text'] = str(e)
|
self.status['text'] = str(e)
|
||||||
self.cooldown()
|
self.cooldown()
|
||||||
|
|
||||||
@ -696,7 +757,9 @@ class AppWindow(object):
|
|||||||
|
|
||||||
def cooldown(self):
|
def cooldown(self):
|
||||||
if time() < self.holdofftime:
|
if time() < self.holdofftime:
|
||||||
self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window
|
# Update button in main window
|
||||||
|
self.button['text'] = self.theme_button['text'] \
|
||||||
|
= _('cooldown {SS}s').format(SS=int(self.holdofftime - time()))
|
||||||
self.w.after(1000, self.cooldown)
|
self.w.after(1000, self.cooldown)
|
||||||
else:
|
else:
|
||||||
self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window
|
self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window
|
||||||
@ -713,7 +776,7 @@ class AppWindow(object):
|
|||||||
def copy(self, event=None):
|
def copy(self, event=None):
|
||||||
if monitor.system:
|
if monitor.system:
|
||||||
self.w.clipboard_clear()
|
self.w.clipboard_clear()
|
||||||
self.w.clipboard_append(monitor.station and '%s,%s' % (monitor.system, monitor.station) or monitor.system)
|
self.w.clipboard_append(monitor.station and f'{monitor.system},{monitor.station}' or monitor.system)
|
||||||
|
|
||||||
def help_general(self, event=None):
|
def help_general(self, event=None):
|
||||||
webbrowser.open('https://github.com/EDCD/EDMarketConnector/wiki')
|
webbrowser.open('https://github.com/EDCD/EDMarketConnector/wiki')
|
||||||
@ -724,7 +787,7 @@ class AppWindow(object):
|
|||||||
def help_releases(self, event=None):
|
def help_releases(self, event=None):
|
||||||
webbrowser.open('https://github.com/EDCD/EDMarketConnector/releases')
|
webbrowser.open('https://github.com/EDCD/EDMarketConnector/releases')
|
||||||
|
|
||||||
class help_about(tk.Toplevel):
|
class HelpAbout(tk.Toplevel):
|
||||||
showing = False
|
showing = False
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
@ -741,11 +804,12 @@ class AppWindow(object):
|
|||||||
self.transient(parent)
|
self.transient(parent)
|
||||||
|
|
||||||
# position over parent
|
# position over parent
|
||||||
if platform!='darwin' or parent.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||||
self.geometry("+%d+%d" % (parent.winfo_rootx(), parent.winfo_rooty()))
|
if platform != 'darwin' or parent.winfo_rooty() > 0:
|
||||||
|
self.geometry(f'+{parent.winfo_rootx():d}+{parent.winfo_rooty():d}')
|
||||||
|
|
||||||
# remove decoration
|
# remove decoration
|
||||||
if platform=='win32':
|
if platform == 'win32':
|
||||||
self.attributes('-toolwindow', tk.TRUE)
|
self.attributes('-toolwindow', tk.TRUE)
|
||||||
|
|
||||||
self.resizable(tk.FALSE, tk.FALSE)
|
self.resizable(tk.FALSE, tk.FALSE)
|
||||||
@ -753,10 +817,6 @@ class AppWindow(object):
|
|||||||
frame = ttk.Frame(self)
|
frame = ttk.Frame(self)
|
||||||
frame.grid(sticky=tk.NSEW)
|
frame.grid(sticky=tk.NSEW)
|
||||||
|
|
||||||
PADX = 10
|
|
||||||
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
|
||||||
PADY = 2 # close spacing
|
|
||||||
|
|
||||||
row = 1
|
row = 1
|
||||||
############################################################
|
############################################################
|
||||||
# applongname
|
# applongname
|
||||||
@ -772,8 +832,8 @@ class AppWindow(object):
|
|||||||
self.appversion_label = tk.Label(frame, text=appversion)
|
self.appversion_label = tk.Label(frame, text=appversion)
|
||||||
self.appversion_label.grid(row=row, column=0, sticky=tk.E)
|
self.appversion_label.grid(row=row, column=0, sticky=tk.E)
|
||||||
self.appversion = HyperlinkLabel(frame, compound=tk.RIGHT, text=_('Release Notes'),
|
self.appversion = HyperlinkLabel(frame, compound=tk.RIGHT, text=_('Release Notes'),
|
||||||
url='https://github.com/EDCD/EDMarketConnector/releases/tag/Release/{VERSION}'.format(
|
url='https://github.com/EDCD/EDMarketConnector/releases/tag/Release/'
|
||||||
VERSION=appversion_nobuild),
|
f'{appversion_nobuild}',
|
||||||
underline=True)
|
underline=True)
|
||||||
self.appversion.grid(row=row, column=2, sticky=tk.W)
|
self.appversion.grid(row=row, column=2, sticky=tk.W)
|
||||||
row += 1
|
row += 1
|
||||||
@ -798,11 +858,11 @@ class AppWindow(object):
|
|||||||
row += 1
|
row += 1
|
||||||
button = ttk.Button(frame, text=_('OK'), command=self.apply)
|
button = ttk.Button(frame, text=_('OK'), command=self.apply)
|
||||||
button.grid(row=row, column=2, sticky=tk.E)
|
button.grid(row=row, column=2, sticky=tk.E)
|
||||||
button.bind("<Return>", lambda event:self.apply())
|
button.bind("<Return>", lambda event: self.apply())
|
||||||
self.protocol("WM_DELETE_WINDOW", self._destroy)
|
self.protocol("WM_DELETE_WINDOW", self._destroy)
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
print('Current version is {}'.format(appversion))
|
logger.info(f'Current version is {appversion}')
|
||||||
|
|
||||||
def apply(self):
|
def apply(self):
|
||||||
self._destroy()
|
self._destroy()
|
||||||
@ -819,22 +879,35 @@ class AppWindow(object):
|
|||||||
try:
|
try:
|
||||||
data = companion.session.station()
|
data = companion.session.station()
|
||||||
self.status['text'] = ''
|
self.status['text'] = ''
|
||||||
f = tkinter.filedialog.asksaveasfilename(parent = self.w,
|
default_extension: str = ''
|
||||||
defaultextension = platform=='darwin' and '.json' or '',
|
if platform == 'darwin':
|
||||||
filetypes = [('JSON', '.json'), ('All Files', '*')],
|
default_extension = '.json'
|
||||||
initialdir = config.get('outdir'),
|
last_system: str = data.get("lastSystem", {}).get("name", "Unknown")
|
||||||
initialfile = '%s%s.%s.json' % (data.get('lastSystem', {}).get('name', 'Unknown'), data['commander'].get('docked') and '.'+data.get('lastStarport', {}).get('name', 'Unknown') or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())))
|
last_starport: str = ''
|
||||||
|
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('outdir'),
|
||||||
|
initialfile=f'{last_system}{last_starport}.{timestamp}')
|
||||||
if f:
|
if f:
|
||||||
with open(f, 'wb') as h:
|
with open(f, 'wb') as h:
|
||||||
h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8'))
|
h.write(json.dumps(data,
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(',', ': ')).encode('utf-8'))
|
||||||
except companion.ServerError as e:
|
except companion.ServerError as e:
|
||||||
self.status['text'] = str(e)
|
self.status['text'] = str(e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if __debug__: print_exc()
|
logger.debug('"other" exception', exc_info=e)
|
||||||
self.status['text'] = str(e)
|
self.status['text'] = str(e)
|
||||||
|
|
||||||
def onexit(self, event=None):
|
def onexit(self, event=None):
|
||||||
if platform!='darwin' or self.w.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||||
|
if platform != 'darwin' or self.w.winfo_rooty() > 0:
|
||||||
config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+')))
|
config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+')))
|
||||||
self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen
|
self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen
|
||||||
protocolhandler.close()
|
protocolhandler.close()
|
||||||
@ -852,7 +925,9 @@ class AppWindow(object):
|
|||||||
|
|
||||||
def drag_continue(self, event):
|
def drag_continue(self, event):
|
||||||
if self.drag_offset:
|
if self.drag_offset:
|
||||||
self.w.geometry('+%d+%d' % (event.x_root - self.drag_offset[0], event.y_root - self.drag_offset[1]))
|
offset_x = event.x_root - self.drag_offset[0]
|
||||||
|
offset_y = event.y_root - self.drag_offset[1]
|
||||||
|
self.w.geometry(f'+{offset_x:d}+{offset_y:d}')
|
||||||
|
|
||||||
def drag_end(self, event):
|
def drag_end(self, event):
|
||||||
self.drag_offset = None
|
self.drag_offset = None
|
||||||
@ -875,71 +950,100 @@ class AppWindow(object):
|
|||||||
self.theme_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW)
|
self.theme_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW)
|
||||||
|
|
||||||
def onleave(self, event=None):
|
def onleave(self, event=None):
|
||||||
if config.getint('theme') > 1 and event.widget==self.w:
|
if config.getint('theme') > 1 and event.widget == self.w:
|
||||||
self.w.attributes("-transparentcolor", 'grey4')
|
self.w.attributes("-transparentcolor", 'grey4')
|
||||||
self.theme_menubar.grid_remove()
|
self.theme_menubar.grid_remove()
|
||||||
self.blank_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW)
|
self.blank_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW)
|
||||||
|
|
||||||
# Run the app
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
|
def enforce_single_instance() -> None:
|
||||||
# Ensure only one copy of the app is running under this user account. OSX does this automatically. Linux TODO.
|
# Ensure only one copy of the app is running under this user account. OSX does this automatically. Linux TODO.
|
||||||
if platform == 'win32':
|
if platform == 'win32':
|
||||||
import ctypes
|
import ctypes
|
||||||
from ctypes.wintypes import *
|
from ctypes.wintypes import HWND, LPWSTR, LPCWSTR, INT, BOOL, LPARAM
|
||||||
EnumWindows = ctypes.windll.user32.EnumWindows
|
EnumWindows = ctypes.windll.user32.EnumWindows # noqa: N806
|
||||||
GetClassName = ctypes.windll.user32.GetClassNameW
|
GetClassName = ctypes.windll.user32.GetClassNameW # noqa: N806
|
||||||
GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int]
|
GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int] # noqa: N806
|
||||||
GetWindowText = ctypes.windll.user32.GetWindowTextW
|
GetWindowText = ctypes.windll.user32.GetWindowTextW # noqa: N806
|
||||||
GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int]
|
GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] # noqa: N806
|
||||||
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
|
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW # noqa: N806
|
||||||
GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd
|
GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd # noqa: N806
|
||||||
|
|
||||||
SW_RESTORE = 9
|
SW_RESTORE = 9 # noqa: N806
|
||||||
SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow
|
SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow # noqa: N806
|
||||||
ShowWindow = ctypes.windll.user32.ShowWindow
|
ShowWindow = ctypes.windll.user32.ShowWindow # noqa: N806
|
||||||
ShowWindowAsync = ctypes.windll.user32.ShowWindowAsync
|
ShowWindowAsync = ctypes.windll.user32.ShowWindowAsync # noqa: N806
|
||||||
|
|
||||||
COINIT_MULTITHREADED = 0
|
COINIT_MULTITHREADED = 0 # noqa: N806,F841
|
||||||
COINIT_APARTMENTTHREADED = 0x2
|
COINIT_APARTMENTTHREADED = 0x2 # noqa: N806
|
||||||
COINIT_DISABLE_OLE1DDE = 0x4
|
COINIT_DISABLE_OLE1DDE = 0x4 # noqa: N806
|
||||||
CoInitializeEx = ctypes.windll.ole32.CoInitializeEx
|
CoInitializeEx = ctypes.windll.ole32.CoInitializeEx # noqa: N806
|
||||||
|
|
||||||
ShellExecute = ctypes.windll.shell32.ShellExecuteW
|
ShellExecute = ctypes.windll.shell32.ShellExecuteW # noqa: N806
|
||||||
ShellExecute.argtypes = [HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT]
|
ShellExecute.argtypes = [HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT]
|
||||||
|
|
||||||
def WindowTitle(h):
|
def window_title(h):
|
||||||
if h:
|
if h:
|
||||||
l = GetWindowTextLength(h) + 1
|
text_length = GetWindowTextLength(h) + 1
|
||||||
buf = ctypes.create_unicode_buffer(l)
|
buf = ctypes.create_unicode_buffer(text_length)
|
||||||
if GetWindowText(h, buf, l):
|
if GetWindowText(h, buf, text_length):
|
||||||
return buf.value
|
return buf.value
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
|
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
|
||||||
def enumwindowsproc(hWnd, lParam):
|
def enumwindowsproc(window_handle, l_param):
|
||||||
# class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576
|
# class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576
|
||||||
cls = ctypes.create_unicode_buffer(257)
|
cls = ctypes.create_unicode_buffer(257)
|
||||||
if GetClassName(hWnd, cls, 257) and cls.value == 'TkTopLevel' and WindowTitle(hWnd) == applongname and GetProcessHandleFromHwnd(hWnd):
|
if GetClassName(window_handle, cls, 257)\
|
||||||
|
and cls.value == 'TkTopLevel'\
|
||||||
|
and window_title(window_handle) == applongname\
|
||||||
|
and GetProcessHandleFromHwnd(window_handle):
|
||||||
# If GetProcessHandleFromHwnd succeeds then the app is already running as this user
|
# If GetProcessHandleFromHwnd succeeds then the app is already running as this user
|
||||||
if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler.redirect):
|
if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler.redirect):
|
||||||
# Browser invoked us directly with auth response. Forward the response to the other app instance.
|
# Browser invoked us directly with auth response. Forward the response to the other app instance.
|
||||||
CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)
|
CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)
|
||||||
ShowWindow(hWnd, SW_RESTORE) # Wait for it to be responsive to avoid ShellExecute recursing
|
# Wait for it to be responsive to avoid ShellExecute recursing
|
||||||
|
ShowWindow(window_handle, SW_RESTORE)
|
||||||
ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE)
|
ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE)
|
||||||
else:
|
else:
|
||||||
ShowWindowAsync(hWnd, SW_RESTORE)
|
ShowWindowAsync(window_handle, SW_RESTORE)
|
||||||
SetForegroundWindow(hWnd)
|
SetForegroundWindow(window_handle)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
EnumWindows(enumwindowsproc, 0)
|
EnumWindows(enumwindowsproc, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_logging():
|
||||||
|
logger.debug('Test from EDMarketConnector.py top-level test_logging()')
|
||||||
|
|
||||||
|
|
||||||
|
# Run the app
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Keep this as the very first code run to be as sure as possible of no
|
||||||
|
# output until after this redirect is done, if needed.
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
# By default py2exe tries to write log to dirname(sys.executable) which fails when installed
|
# By default py2exe tries to write log to dirname(sys.executable) which fails when installed
|
||||||
import tempfile
|
import tempfile
|
||||||
sys.stdout = sys.stderr = open(join(tempfile.gettempdir(), '%s.log' % appname), 'wt', 1) # unbuffered not allowed for text in python3, so use line buffering
|
# unbuffered not allowed for text in python3, so use `1 for line buffering
|
||||||
print('%s %s %s' % (applongname, appversion, strftime('%Y-%m-%dT%H:%M:%S', localtime())))
|
sys.stdout = sys.stderr = open(join(tempfile.gettempdir(), f'{appname}.log'), mode='wt', buffering=1)
|
||||||
|
|
||||||
|
enforce_single_instance()
|
||||||
|
|
||||||
|
logger = EDMCLogging.Logger(appname).get_logger()
|
||||||
|
|
||||||
|
# TODO: unittests in place of these
|
||||||
|
# logger.debug('Test from __main__')
|
||||||
|
# test_logging()
|
||||||
|
class A(object):
|
||||||
|
class B(object):
|
||||||
|
def __init__(self):
|
||||||
|
logger.debug('A call from A.B.__init__')
|
||||||
|
|
||||||
|
# abinit = A.B()
|
||||||
|
|
||||||
|
# Plain, not via `logger`
|
||||||
|
print(f'{applongname} {appversion}')
|
||||||
|
|
||||||
Translations.install(config.get('language') or None) # Can generate errors so wait til log set up
|
Translations.install(config.get('language') or None) # Can generate errors so wait til log set up
|
||||||
|
|
||||||
|
@ -236,6 +236,7 @@
|
|||||||
"Language" = "Idioma";
|
"Language" = "Idioma";
|
||||||
|
|
||||||
/* [EDMarketConnector.py] - Leave '%H:%M:%S' as-is */
|
/* [EDMarketConnector.py] - Leave '%H:%M:%S' as-is */
|
||||||
|
>>>>>>> develop
|
||||||
"Last updated at %H:%M:%S" = "Última actualización: %H:%M:%S";
|
"Last updated at %H:%M:%S" = "Última actualización: %H:%M:%S";
|
||||||
|
|
||||||
/* Federation rank. [stats.py] */
|
/* Federation rank. [stats.py] */
|
||||||
|
@ -48,6 +48,9 @@ breaking with future code changes.**
|
|||||||
`from monitor import gamerunning` - in case a plugin needs to know if we
|
`from monitor import gamerunning` - in case a plugin needs to know if we
|
||||||
think the game is running.
|
think the game is running.
|
||||||
|
|
||||||
|
`import timeout_session` - provides a method called `new_session` that creates a requests.session with a default timeout
|
||||||
|
on all requests. Recommended to reduce noise in HTTP requests
|
||||||
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from ttkHyperlinkLabel import HyperlinkLabel
|
from ttkHyperlinkLabel import HyperlinkLabel
|
||||||
|
83
companion.py
83
companion.py
@ -4,9 +4,10 @@ from builtins import object
|
|||||||
import base64
|
import base64
|
||||||
import csv
|
import csv
|
||||||
import requests
|
import requests
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
# TODO: see https://github.com/EDCD/EDMarketConnector/issues/569
|
# TODO: see https://github.com/EDCD/EDMarketConnector/issues/569
|
||||||
from http.cookiejar import LWPCookieJar # No longer needed but retained in case plugins use it
|
from http.cookiejar import LWPCookieJar # noqa: F401 - No longer needed but retained in case plugins use it
|
||||||
from email.utils import parsedate
|
from email.utils import parsedate
|
||||||
import hashlib
|
import hashlib
|
||||||
import numbers
|
import numbers
|
||||||
@ -14,14 +15,15 @@ import os
|
|||||||
from os.path import join
|
from os.path import join
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from traceback import print_exc
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
from config import appname, appversion, config
|
from config import appname, appversion, config
|
||||||
from protocol import protocolhandler
|
from protocol import protocolhandler
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(appname)
|
||||||
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
_ = lambda x: x # noqa # to make flake8 stop complaining that the hacked in _ method doesnt exist
|
_ = lambda x: x # noqa # to make flake8 stop complaining that the hacked in _ method doesnt exist
|
||||||
|
|
||||||
@ -196,29 +198,28 @@ class Auth(object):
|
|||||||
return data.get('access_token')
|
return data.get('access_token')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print('Auth\tCan\'t refresh token for {}'.format(self.cmdr))
|
logger.error(f"Frontier CAPI Auth: Can't refresh token for \"{self.cmdr}\"")
|
||||||
self.dump(r)
|
self.dump(r)
|
||||||
|
|
||||||
except Exception:
|
except Exception as e:
|
||||||
print('Auth\tCan\'t refresh token for {}'.format(self.cmdr))
|
logger.exception(f"Frontier CAPI Auth: Can't refresh token for \"{self.cmdr}\"")
|
||||||
print_exc()
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print('Auth\tNo token for {}'.format(self.cmdr))
|
logger.error(f"Frontier CAPI Auth: No token for \"{self.cmdr}\"")
|
||||||
|
|
||||||
# New request
|
# New request
|
||||||
print('Auth\tNew authorization request')
|
logger.info('Frontier CAPI Auth: New authorization request')
|
||||||
v = random.SystemRandom().getrandbits(8 * 32)
|
v = random.SystemRandom().getrandbits(8 * 32)
|
||||||
self.verifier = self.base64URLEncode(v.to_bytes(32, byteorder='big')).encode('utf-8')
|
self.verifier = self.base64_url_encode(v.to_bytes(32, byteorder='big')).encode('utf-8')
|
||||||
s = random.SystemRandom().getrandbits(8 * 32)
|
s = random.SystemRandom().getrandbits(8 * 32)
|
||||||
self.state = self.base64URLEncode(s.to_bytes(32, byteorder='big'))
|
self.state = self.base64_url_encode(s.to_bytes(32, byteorder='big'))
|
||||||
# Won't work under IE: https://blogs.msdn.microsoft.com/ieinternals/2011/07/13/understanding-protocols/
|
# Won't work under IE: https://blogs.msdn.microsoft.com/ieinternals/2011/07/13/understanding-protocols/
|
||||||
webbrowser.open(
|
webbrowser.open(
|
||||||
'{server_auth}{url_auth}?response_type=code&audience=frontier&scope=capi&client_id={client_id}&code_challenge={challenge}&code_challenge_method=S256&state={state}&redirect_uri={redirect}'.format( # noqa: E501 # I cant make this any shorter
|
'{server_auth}{url_auth}?response_type=code&audience=frontier&scope=capi&client_id={client_id}&code_challenge={challenge}&code_challenge_method=S256&state={state}&redirect_uri={redirect}'.format( # noqa: E501 # I cant make this any shorter
|
||||||
server_auth=SERVER_AUTH,
|
server_auth=SERVER_AUTH,
|
||||||
url_auth=URL_AUTH,
|
url_auth=URL_AUTH,
|
||||||
client_id=CLIENT_ID,
|
client_id=CLIENT_ID,
|
||||||
challenge=self.base64URLEncode(hashlib.sha256(self.verifier).digest()),
|
challenge=self.base64_url_encode(hashlib.sha256(self.verifier).digest()),
|
||||||
state=self.state,
|
state=self.state,
|
||||||
redirect=protocolhandler.redirect
|
redirect=protocolhandler.redirect
|
||||||
)
|
)
|
||||||
@ -228,16 +229,16 @@ class Auth(object):
|
|||||||
# Handle OAuth authorization code callback.
|
# Handle OAuth authorization code callback.
|
||||||
# Returns access token if successful, otherwise raises CredentialsError
|
# Returns access token if successful, otherwise raises CredentialsError
|
||||||
if '?' not in payload:
|
if '?' not in payload:
|
||||||
print('Auth\tMalformed response {!r}'.format(payload))
|
logger.error(f'Frontier CAPI Auth: Malformed response\n{payload}\n')
|
||||||
raise CredentialsError('malformed payload') # Not well formed
|
raise CredentialsError('malformed payload') # Not well formed
|
||||||
|
|
||||||
data = urllib.parse.parse_qs(payload[(payload.index('?') + 1):])
|
data = urllib.parse.parse_qs(payload[(payload.index('?') + 1):])
|
||||||
if not self.state or not data.get('state') or data['state'][0] != self.state:
|
if not self.state or not data.get('state') or data['state'][0] != self.state:
|
||||||
print('Auth\tUnexpected response {!r}'.format(payload))
|
logger.error(f'Frontier CAPI Auth: Unexpected response\n{payload}\n')
|
||||||
raise CredentialsError('Unexpected response from authorization {!r}'.format(payload)) # Unexpected reply
|
raise CredentialsError('Unexpected response from authorization {!r}'.format(payload)) # Unexpected reply
|
||||||
|
|
||||||
if not data.get('code'):
|
if not data.get('code'):
|
||||||
print('Auth\tNegative response {!r}'.format(payload))
|
logger.error(f'Frontier CAPI Auth: Negative response\n{payload}\n')
|
||||||
error = next(
|
error = next(
|
||||||
(data[k] for k in ('error_description', 'error', 'message') if k in data), ('<unknown error>',)
|
(data[k] for k in ('error_description', 'error', 'message') if k in data), ('<unknown error>',)
|
||||||
)
|
)
|
||||||
@ -256,7 +257,7 @@ class Auth(object):
|
|||||||
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=data, timeout=auth_timeout)
|
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=data, timeout=auth_timeout)
|
||||||
data = r.json()
|
data = r.json()
|
||||||
if r.status_code == requests.codes.ok:
|
if r.status_code == requests.codes.ok:
|
||||||
print('Auth\tNew token for {}'.format(self.cmdr))
|
logger.info(f'Frontier CAPI Auth: New token for \"{self.cmdr}\"')
|
||||||
cmdrs = config.get('cmdrs')
|
cmdrs = config.get('cmdrs')
|
||||||
idx = cmdrs.index(self.cmdr)
|
idx = cmdrs.index(self.cmdr)
|
||||||
tokens = config.get('fdev_apikeys') or []
|
tokens = config.get('fdev_apikeys') or []
|
||||||
@ -268,21 +269,20 @@ class Auth(object):
|
|||||||
return data.get('access_token')
|
return data.get('access_token')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('Auth\tCan\'t get token for {}'.format(self.cmdr))
|
logger.exception(f"Frontier CAPI Auth: Can't get token for \"{self.cmdr}\"")
|
||||||
print_exc()
|
|
||||||
if r:
|
if r:
|
||||||
self.dump(r)
|
self.dump(r)
|
||||||
|
|
||||||
raise CredentialsError('unable to get token') from e
|
raise CredentialsError('unable to get token') from e
|
||||||
|
|
||||||
print('Auth\tCan\'t get token for {}'.format(self.cmdr))
|
logger.error(f"Frontier CAPI Auth: Can't get token for \"{self.cmdr}\"")
|
||||||
self.dump(r)
|
self.dump(r)
|
||||||
error = next((data[k] for k in ('error_description', 'error', 'message') if k in data), ('<unknown error>',))
|
error = next((data[k] for k in ('error_description', 'error', 'message') if k in data), ('<unknown error>',))
|
||||||
raise CredentialsError('Error: {!r}'.format(error)[0])
|
raise CredentialsError('Error: {!r}'.format(error)[0])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def invalidate(cmdr):
|
def invalidate(cmdr):
|
||||||
print('Auth\tInvalidated token for {}'.format(cmdr))
|
logger.info(f'Frontier CAPI Auth: Invalidated token for "{cmdr}"')
|
||||||
cmdrs = config.get('cmdrs')
|
cmdrs = config.get('cmdrs')
|
||||||
idx = cmdrs.index(cmdr)
|
idx = cmdrs.index(cmdr)
|
||||||
tokens = config.get('fdev_apikeys') or []
|
tokens = config.get('fdev_apikeys') or []
|
||||||
@ -292,9 +292,9 @@ class Auth(object):
|
|||||||
config.save() # Save settings now for use by command-line app
|
config.save() # Save settings now for use by command-line app
|
||||||
|
|
||||||
def dump(self, r):
|
def dump(self, r):
|
||||||
print('Auth\t' + r.url, r.status_code, r.reason if r.reason else 'None', r.text)
|
logger.debug(f'Frontier CAPI Auth: {r.url} {r.status_code} {r.reason if r.reason else "None"} {r.text}')
|
||||||
|
|
||||||
def base64URLEncode(self, text):
|
def base64_url_encode(self, text):
|
||||||
return base64.urlsafe_b64encode(text).decode().replace('=', '')
|
return base64.urlsafe_b64encode(text).decode().replace('=', '')
|
||||||
|
|
||||||
|
|
||||||
@ -379,10 +379,8 @@ class Session(object):
|
|||||||
r = self.session.get(self.server + endpoint, timeout=timeout)
|
r = self.session.get(self.server + endpoint, timeout=timeout)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if __debug__:
|
logger.debug('Attempting GET', exc_info=e)
|
||||||
print_exc()
|
raise ServerError(f'unable to get endpoint {endpoint}') from e
|
||||||
|
|
||||||
raise ServerError('unable to get endpoint {}'.format(endpoint)) from e
|
|
||||||
|
|
||||||
if r.url.startswith(SERVER_AUTH):
|
if r.url.startswith(SERVER_AUTH):
|
||||||
# Redirected back to Auth server - force full re-authentication
|
# Redirected back to Auth server - force full re-authentication
|
||||||
@ -402,7 +400,7 @@ class Session(object):
|
|||||||
data = r.json() # May also fail here if token expired since response is empty
|
data = r.json() # May also fail here if token expired since response is empty
|
||||||
|
|
||||||
except (requests.HTTPError, ValueError) as e:
|
except (requests.HTTPError, ValueError) as e:
|
||||||
print_exc()
|
logger.exception('Frontier CAPI Auth: GET ')
|
||||||
self.dump(r)
|
self.dump(r)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
@ -410,6 +408,7 @@ class Session(object):
|
|||||||
self.invalidate()
|
self.invalidate()
|
||||||
self.retrying = False
|
self.retrying = False
|
||||||
self.login()
|
self.login()
|
||||||
|
logger.error('Frontier CAPI Auth: query failed after refresh')
|
||||||
raise CredentialsError('query failed after refresh') from e
|
raise CredentialsError('query failed after refresh') from e
|
||||||
|
|
||||||
elif self.login(): # Maybe our token expired. Re-authorize in any case
|
elif self.login(): # Maybe our token expired. Re-authorize in any case
|
||||||
@ -418,6 +417,7 @@ class Session(object):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
self.retrying = False
|
self.retrying = False
|
||||||
|
logger.error('Frontier CAPI Auth: HTTP error or invalid JSON')
|
||||||
raise CredentialsError('HTTP error or invalid JSON') from e
|
raise CredentialsError('HTTP error or invalid JSON') from e
|
||||||
|
|
||||||
self.retrying = False
|
self.retrying = False
|
||||||
@ -461,9 +461,8 @@ class Session(object):
|
|||||||
try:
|
try:
|
||||||
self.session.close()
|
self.session.close()
|
||||||
|
|
||||||
except Exception:
|
except Exception as e:
|
||||||
if __debug__:
|
logger.debug('Frontier CAPI Auth: closing', exc_info=e)
|
||||||
print_exc()
|
|
||||||
|
|
||||||
self.session = None
|
self.session = None
|
||||||
|
|
||||||
@ -473,7 +472,7 @@ class Session(object):
|
|||||||
Auth.invalidate(self.credentials['cmdr'])
|
Auth.invalidate(self.credentials['cmdr'])
|
||||||
|
|
||||||
def dump(self, r):
|
def dump(self, r):
|
||||||
print('cAPI\t' + r.url, r.status_code, r.reason and r.reason or 'None', r.text)
|
logger.error(f'Frontier CAPI Auth: {r.url} {r.status_code} {r.reason and r.reason or "None"} {r.text}')
|
||||||
|
|
||||||
# Returns a shallow copy of the received data suitable for export to older tools
|
# Returns a shallow copy of the received data suitable for export to older tools
|
||||||
# English commodity names and anomalies fixed up
|
# English commodity names and anomalies fixed up
|
||||||
@ -497,15 +496,7 @@ def fixup(data):
|
|||||||
# But also see https://github.com/Marginal/EDMarketConnector/issues/32
|
# But also see https://github.com/Marginal/EDMarketConnector/issues/32
|
||||||
for thing in ('buyPrice', 'sellPrice', 'demand', 'demandBracket', 'stock', 'stockBracket'):
|
for thing in ('buyPrice', 'sellPrice', 'demand', 'demandBracket', 'stock', 'stockBracket'):
|
||||||
if not isinstance(commodity.get(thing), numbers.Number):
|
if not isinstance(commodity.get(thing), numbers.Number):
|
||||||
if __debug__:
|
logger.debug(f'Invalid {thing}:{commodity.get(thing)} ({type(commodity.get(thing))}) for {commodity.get("name", "")}') # noqa: E501
|
||||||
print(
|
|
||||||
'Invalid {!r}:{!r} ({}) for {!r}'.format(
|
|
||||||
thing,
|
|
||||||
commodity.get(thing),
|
|
||||||
type(commodity.get(thing)),
|
|
||||||
commodity.get('name', '')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -520,20 +511,16 @@ def fixup(data):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
elif not commodity.get('categoryname'):
|
elif not commodity.get('categoryname'):
|
||||||
if __debug__:
|
logger.debug(f'Missing "categoryname" for {commodity.get("name", "")}')
|
||||||
print('Missing "categoryname" for {!r}'.format(commodity.get('name', '')))
|
|
||||||
|
|
||||||
elif not commodity.get('name'):
|
elif not commodity.get('name'):
|
||||||
if __debug__:
|
logger.debug(f'Missing "name" for a commodity in {commodity.get("categoryname", "")}')
|
||||||
print('Missing "name" for a commodity in {!r}'.format(commodity.get('categoryname', '')))
|
|
||||||
|
|
||||||
elif not commodity['demandBracket'] in range(4):
|
elif not commodity['demandBracket'] in range(4):
|
||||||
if __debug__:
|
logger.debug(f'Invalid "demandBracket":{commodity["demandBracket"]} for {commodity["name"]}')
|
||||||
print('Invalid "demandBracket":{!r} for {!r}'.format(commodity['demandBracket'], commodity['name']))
|
|
||||||
|
|
||||||
elif not commodity['stockBracket'] in range(4):
|
elif not commodity['stockBracket'] in range(4):
|
||||||
if __debug__:
|
logger.debug(f'Invalid "stockBracket":{commodity["stockBracket"]} for {commodity["name"]}')
|
||||||
print('Invalid "stockBracket":{!r} for {!r}'.format(commodity['stockBracket'], commodity['name']))
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Rewrite text fields
|
# Rewrite text fields
|
||||||
|
635
monitor.py
635
monitor.py
File diff suppressed because it is too large
Load Diff
152
plug.py
152
plug.py
@ -7,49 +7,51 @@ import os
|
|||||||
import importlib
|
import importlib
|
||||||
import sys
|
import sys
|
||||||
import operator
|
import operator
|
||||||
import threading # We don't use it, but plugins might
|
import threading # noqa: F401 - We don't use it, but plugins might
|
||||||
from traceback import print_exc
|
from typing import Optional
|
||||||
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import myNotebook as nb
|
|
||||||
|
|
||||||
from config import config
|
import myNotebook as nb # noqa: N813
|
||||||
from time import time as time
|
|
||||||
|
|
||||||
|
from config import config, appname
|
||||||
|
import EDMCLogging
|
||||||
|
|
||||||
|
logger = logging.getLogger(appname)
|
||||||
|
|
||||||
# Dashboard Flags constants
|
# Dashboard Flags constants
|
||||||
FlagsDocked = 1<<0 # on a landing pad
|
FlagsDocked = 1 << 0 # on a landing pad
|
||||||
FlagsLanded = 1<<1 # on planet surface
|
FlagsLanded = 1 << 1 # on planet surface
|
||||||
FlagsLandingGearDown = 1<<2
|
FlagsLandingGearDown = 1 << 2
|
||||||
FlagsShieldsUp = 1<<3
|
FlagsShieldsUp = 1 << 3
|
||||||
FlagsSupercruise = 1<<4
|
FlagsSupercruise = 1 << 4
|
||||||
FlagsFlightAssistOff = 1<<5
|
FlagsFlightAssistOff = 1 << 5
|
||||||
FlagsHardpointsDeployed = 1<<6
|
FlagsHardpointsDeployed = 1 << 6
|
||||||
FlagsInWing = 1<<7
|
FlagsInWing = 1 << 7
|
||||||
FlagsLightsOn = 1<<8
|
FlagsLightsOn = 1 << 8
|
||||||
FlagsCargoScoopDeployed = 1<<9
|
FlagsCargoScoopDeployed = 1 << 9
|
||||||
FlagsSilentRunning = 1<<10
|
FlagsSilentRunning = 1 << 10
|
||||||
FlagsScoopingFuel = 1<<11
|
FlagsScoopingFuel = 1 << 11
|
||||||
FlagsSrvHandbrake = 1<<12
|
FlagsSrvHandbrake = 1 << 12
|
||||||
FlagsSrvTurret = 1<<13 # using turret view
|
FlagsSrvTurret = 1 << 13 # using turret view
|
||||||
FlagsSrvUnderShip = 1<<14 # turret retracted
|
FlagsSrvUnderShip = 1 << 14 # turret retracted
|
||||||
FlagsSrvDriveAssist = 1<<15
|
FlagsSrvDriveAssist = 1 << 15
|
||||||
FlagsFsdMassLocked = 1<<16
|
FlagsFsdMassLocked = 1 << 16
|
||||||
FlagsFsdCharging = 1<<17
|
FlagsFsdCharging = 1 << 17
|
||||||
FlagsFsdCooldown = 1<<18
|
FlagsFsdCooldown = 1 << 18
|
||||||
FlagsLowFuel = 1<<19 # <25%
|
FlagsLowFuel = 1 << 19 # <25%
|
||||||
FlagsOverHeating = 1<<20 # > 100%
|
FlagsOverHeating = 1 << 20 # > 100%
|
||||||
FlagsHasLatLong = 1<<21
|
FlagsHasLatLong = 1 << 21
|
||||||
FlagsIsInDanger = 1<<22
|
FlagsIsInDanger = 1 << 22
|
||||||
FlagsBeingInterdicted = 1<<23
|
FlagsBeingInterdicted = 1 << 23
|
||||||
FlagsInMainShip = 1<<24
|
FlagsInMainShip = 1 << 24
|
||||||
FlagsInFighter = 1<<25
|
FlagsInFighter = 1 << 25
|
||||||
FlagsInSRV = 1<<26
|
FlagsInSRV = 1 << 26
|
||||||
FlagsAnalysisMode = 1<<27 # Hud in Analysis mode
|
FlagsAnalysisMode = 1 << 27 # Hud in Analysis mode
|
||||||
FlagsNightVision = 1<<28
|
FlagsNightVision = 1 << 28
|
||||||
FlagsAverageAltitude = 1<<29 # Altitude from Average radius
|
FlagsAverageAltitude = 1 << 29 # Altitude from Average radius
|
||||||
FlagsFsdJump = 1<<30
|
FlagsFsdJump = 1 << 30
|
||||||
FlagsSrvHighBeam = 1<<31
|
FlagsSrvHighBeam = 1 << 31
|
||||||
|
|
||||||
# Dashboard GuiFocus constants
|
# Dashboard GuiFocus constants
|
||||||
GuiFocusNoFocus = 0
|
GuiFocusNoFocus = 0
|
||||||
@ -79,7 +81,7 @@ last_error = {
|
|||||||
|
|
||||||
class Plugin(object):
|
class Plugin(object):
|
||||||
|
|
||||||
def __init__(self, name, loadfile):
|
def __init__(self, name: str, loadfile: str, plugin_logger: Optional[logging.Logger]):
|
||||||
"""
|
"""
|
||||||
Load a single plugin
|
Load a single plugin
|
||||||
:param name: module name
|
:param name: module name
|
||||||
@ -90,25 +92,28 @@ class Plugin(object):
|
|||||||
self.name = name # Display name.
|
self.name = name # Display name.
|
||||||
self.folder = name # basename of plugin folder. None for internal plugins.
|
self.folder = name # basename of plugin folder. None for internal plugins.
|
||||||
self.module = None # None for disabled plugins.
|
self.module = None # None for disabled plugins.
|
||||||
|
self.logger = plugin_logger
|
||||||
|
|
||||||
if loadfile:
|
if loadfile:
|
||||||
sys.stdout.write('loading plugin {} from "{}"\n'.format(name.replace('.', '_'), loadfile))
|
logger.info(f'loading plugin "{name.replace(".", "_")}" from "{loadfile}"')
|
||||||
try:
|
try:
|
||||||
module = importlib.machinery.SourceFileLoader('plugin_{}'.format(name.encode(encoding='ascii', errors='replace').decode('utf-8').replace('.', '_')), loadfile).load_module()
|
module = importlib.machinery.SourceFileLoader('plugin_{}'.format(
|
||||||
|
name.encode(encoding='ascii', errors='replace').decode('utf-8').replace('.', '_')),
|
||||||
|
loadfile).load_module()
|
||||||
if getattr(module, 'plugin_start3', None):
|
if getattr(module, 'plugin_start3', None):
|
||||||
newname = module.plugin_start3(os.path.dirname(loadfile))
|
newname = module.plugin_start3(os.path.dirname(loadfile))
|
||||||
self.name = newname and str(newname) or name
|
self.name = newname and str(newname) or name
|
||||||
self.module = module
|
self.module = module
|
||||||
elif getattr(module, 'plugin_start', None):
|
elif getattr(module, 'plugin_start', None):
|
||||||
sys.stdout.write('plugin %s needs migrating\n' % name)
|
logger.warning(f'plugin {name} needs migrating\n')
|
||||||
PLUGINS_not_py3.append(self)
|
PLUGINS_not_py3.append(self)
|
||||||
else:
|
else:
|
||||||
sys.stdout.write('plugin %s has no plugin_start3() function\n' % name)
|
logger.error(f'plugin {name} has no plugin_start3() function')
|
||||||
except:
|
except Exception as e:
|
||||||
print_exc()
|
logger.exception(f': Failed for Plugin "{name}"')
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
sys.stdout.write('plugin %s disabled\n' % name)
|
logger.info(f'plugin {name} disabled')
|
||||||
|
|
||||||
def _get_func(self, funcname):
|
def _get_func(self, funcname):
|
||||||
"""
|
"""
|
||||||
@ -136,8 +141,8 @@ class Plugin(object):
|
|||||||
elif not isinstance(appitem, tk.Widget):
|
elif not isinstance(appitem, tk.Widget):
|
||||||
raise AssertionError
|
raise AssertionError
|
||||||
return appitem
|
return appitem
|
||||||
except:
|
except Exception as e:
|
||||||
print_exc()
|
logger.exception(f'Failed for Plugin "{self.name}"')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_prefs(self, parent, cmdr, is_beta):
|
def get_prefs(self, parent, cmdr, is_beta):
|
||||||
@ -156,8 +161,8 @@ class Plugin(object):
|
|||||||
if not isinstance(frame, nb.Frame):
|
if not isinstance(frame, nb.Frame):
|
||||||
raise AssertionError
|
raise AssertionError
|
||||||
return frame
|
return frame
|
||||||
except:
|
except Exception as e:
|
||||||
print_exc()
|
logger.exception(f'Failed for Plugin "{self.name}"')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -171,12 +176,12 @@ def load_plugins(master):
|
|||||||
for name in sorted(os.listdir(config.internal_plugin_dir)):
|
for name in sorted(os.listdir(config.internal_plugin_dir)):
|
||||||
if name.endswith('.py') and not name[0] in ['.', '_']:
|
if name.endswith('.py') and not name[0] in ['.', '_']:
|
||||||
try:
|
try:
|
||||||
plugin = Plugin(name[:-3], os.path.join(config.internal_plugin_dir, name))
|
plugin = Plugin(name[:-3], os.path.join(config.internal_plugin_dir, name), logger)
|
||||||
plugin.folder = None # Suppress listing in Plugins prefs tab
|
plugin.folder = None # Suppress listing in Plugins prefs tab
|
||||||
internal.append(plugin)
|
internal.append(plugin)
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.exception(f'Failure loading internal Plugin "{name}"')
|
||||||
PLUGINS.extend(sorted(internal, key = lambda p: operator.attrgetter('name')(p).lower()))
|
PLUGINS.extend(sorted(internal, key=lambda p: operator.attrgetter('name')(p).lower()))
|
||||||
|
|
||||||
# Add plugin folder to load path so packages can be loaded from plugin folder
|
# Add plugin folder to load path so packages can be loaded from plugin folder
|
||||||
sys.path.append(config.plugin_dir)
|
sys.path.append(config.plugin_dir)
|
||||||
@ -189,15 +194,22 @@ def load_plugins(master):
|
|||||||
pass
|
pass
|
||||||
elif name.endswith('.disabled'):
|
elif name.endswith('.disabled'):
|
||||||
name, discard = name.rsplit('.', 1)
|
name, discard = name.rsplit('.', 1)
|
||||||
found.append(Plugin(name, None))
|
found.append(Plugin(name, None, logger))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
# Add plugin's folder to load path in case plugin has internal package dependencies
|
# Add plugin's folder to load path in case plugin has internal package dependencies
|
||||||
sys.path.append(os.path.join(config.plugin_dir, name))
|
sys.path.append(os.path.join(config.plugin_dir, name))
|
||||||
found.append(Plugin(name, os.path.join(config.plugin_dir, name, 'load.py')))
|
|
||||||
except:
|
# Create a logger for this 'found' plugin. Must be before the
|
||||||
|
# load.py is loaded.
|
||||||
|
plugin_logger = EDMCLogging.get_plugin_logger(f'{appname}.{name}')
|
||||||
|
|
||||||
|
found.append(Plugin(name, os.path.join(config.plugin_dir, name, 'load.py'), plugin_logger))
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f'Failure loading found Plugin "{name}"')
|
||||||
pass
|
pass
|
||||||
PLUGINS.extend(sorted(found, key = lambda p: operator.attrgetter('name')(p).lower()))
|
PLUGINS.extend(sorted(found, key=lambda p: operator.attrgetter('name')(p).lower()))
|
||||||
|
|
||||||
|
|
||||||
def provides(fn_name):
|
def provides(fn_name):
|
||||||
"""
|
"""
|
||||||
@ -240,8 +252,8 @@ def notify_stop():
|
|||||||
try:
|
try:
|
||||||
newerror = plugin_stop()
|
newerror = plugin_stop()
|
||||||
error = error or newerror
|
error = error or newerror
|
||||||
except:
|
except Exception as e:
|
||||||
print_exc()
|
logger.exception(f'Plugin "{plugin.name}" failed')
|
||||||
return error
|
return error
|
||||||
|
|
||||||
|
|
||||||
@ -257,8 +269,8 @@ def notify_prefs_cmdr_changed(cmdr, is_beta):
|
|||||||
if prefs_cmdr_changed:
|
if prefs_cmdr_changed:
|
||||||
try:
|
try:
|
||||||
prefs_cmdr_changed(cmdr, is_beta)
|
prefs_cmdr_changed(cmdr, is_beta)
|
||||||
except:
|
except Exception as e:
|
||||||
print_exc()
|
logger.exception(f'Plugin "{plugin.name}" failed')
|
||||||
|
|
||||||
|
|
||||||
def notify_prefs_changed(cmdr, is_beta):
|
def notify_prefs_changed(cmdr, is_beta):
|
||||||
@ -275,8 +287,8 @@ def notify_prefs_changed(cmdr, is_beta):
|
|||||||
if prefs_changed:
|
if prefs_changed:
|
||||||
try:
|
try:
|
||||||
prefs_changed(cmdr, is_beta)
|
prefs_changed(cmdr, is_beta)
|
||||||
except:
|
except Exception as e:
|
||||||
print_exc()
|
logger.exception(f'Plugin "{plugin.name}" failed')
|
||||||
|
|
||||||
|
|
||||||
def notify_journal_entry(cmdr, is_beta, system, station, entry, state):
|
def notify_journal_entry(cmdr, is_beta, system, station, entry, state):
|
||||||
@ -298,8 +310,8 @@ def notify_journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
# Pass a copy of the journal entry in case the callee modifies it
|
# Pass a copy of the journal entry in case the callee modifies it
|
||||||
newerror = journal_entry(cmdr, is_beta, system, station, dict(entry), dict(state))
|
newerror = journal_entry(cmdr, is_beta, system, station, dict(entry), dict(state))
|
||||||
error = error or newerror
|
error = error or newerror
|
||||||
except:
|
except Exception as e:
|
||||||
print_exc()
|
logger.exception(f'Plugin "{plugin.name}" failed')
|
||||||
return error
|
return error
|
||||||
|
|
||||||
|
|
||||||
@ -319,8 +331,8 @@ def notify_dashboard_entry(cmdr, is_beta, entry):
|
|||||||
# Pass a copy of the status entry in case the callee modifies it
|
# Pass a copy of the status entry in case the callee modifies it
|
||||||
newerror = status(cmdr, is_beta, dict(entry))
|
newerror = status(cmdr, is_beta, dict(entry))
|
||||||
error = error or newerror
|
error = error or newerror
|
||||||
except:
|
except Exception as e:
|
||||||
print_exc()
|
logger.exception(f'Plugin "{plugin.name}" failed')
|
||||||
return error
|
return error
|
||||||
|
|
||||||
|
|
||||||
@ -338,8 +350,8 @@ def notify_newdata(data, is_beta):
|
|||||||
try:
|
try:
|
||||||
newerror = cmdr_data(data, is_beta)
|
newerror = cmdr_data(data, is_beta)
|
||||||
error = error or newerror
|
error = error or newerror
|
||||||
except:
|
except Exception as e:
|
||||||
print_exc()
|
logger.exception(f'Plugin "{plugin.name}" failed')
|
||||||
return error
|
return error
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,17 +7,21 @@ import io
|
|||||||
|
|
||||||
# Migrate settings from <= 3.01
|
# Migrate settings from <= 3.01
|
||||||
from config import config
|
from config import config
|
||||||
|
|
||||||
if not config.get('shipyard_provider') and config.getint('shipyard'):
|
if not config.get('shipyard_provider') and config.getint('shipyard'):
|
||||||
config.set('shipyard_provider', 'Coriolis')
|
config.set('shipyard_provider', 'Coriolis')
|
||||||
|
|
||||||
config.delete('shipyard')
|
config.delete('shipyard')
|
||||||
|
|
||||||
|
|
||||||
def plugin_start3(plugin_dir):
|
def plugin_start3(_):
|
||||||
return 'Coriolis'
|
return 'Coriolis'
|
||||||
|
|
||||||
# Return a URL for the current ship
|
|
||||||
def shipyard_url(loadout, is_beta):
|
def shipyard_url(loadout, is_beta):
|
||||||
string = json.dumps(loadout, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') # most compact representation
|
"""Return a URL for the current ship"""
|
||||||
|
# most compact representation
|
||||||
|
string = json.dumps(loadout, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8')
|
||||||
if not string:
|
if not string:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -25,4 +29,7 @@ def shipyard_url(loadout, is_beta):
|
|||||||
with gzip.GzipFile(fileobj=out, mode='w') as f:
|
with gzip.GzipFile(fileobj=out, mode='w') as f:
|
||||||
f.write(string)
|
f.write(string)
|
||||||
|
|
||||||
return (is_beta and 'https://beta.coriolis.io/import?data=' or 'https://coriolis.io/import?data=') + base64.urlsafe_b64encode(out.getvalue()).decode().replace('=', '%3D')
|
encoded = base64.urlsafe_b64encode(out.getvalue()).decode().replace('=', '%3D')
|
||||||
|
url = 'https://beta.coriolis.io/import?data=' if is_beta else 'https://coriolis.io/import?data='
|
||||||
|
|
||||||
|
return f'{url}{encoded}'
|
||||||
|
@ -24,23 +24,27 @@
|
|||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from config import config
|
from config import config
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from tkinter import Tk
|
||||||
|
|
||||||
STATION_UNDOCKED: str = u'×' # "Station" name to display when not docked = U+00D7
|
|
||||||
|
|
||||||
this = sys.modules[__name__] # For holding module globals
|
STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7
|
||||||
|
|
||||||
|
this: Any = sys.modules[__name__] # For holding module globals
|
||||||
|
|
||||||
# Main window clicks
|
# Main window clicks
|
||||||
this.system_link = None
|
this.system_link: Optional[str] = None
|
||||||
this.system = None
|
this.system: Optional[str] = None
|
||||||
this.system_address = None
|
this.system_address: Optional[str] = None
|
||||||
this.system_population = None
|
this.system_population: Optional[int] = None
|
||||||
this.station_link = None
|
this.station_link: 'Optional[Tk]' = None
|
||||||
this.station = None
|
this.station: Optional[str] = None
|
||||||
this.station_marketid = None
|
this.station_marketid: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
def system_url(system_name: str) -> str:
|
def system_url(system_name: str) -> str:
|
||||||
@ -61,21 +65,21 @@ def station_url(system_name: str, station_name: str) -> str:
|
|||||||
def plugin_start3(plugin_dir):
|
def plugin_start3(plugin_dir):
|
||||||
return 'eddb'
|
return 'eddb'
|
||||||
|
|
||||||
def plugin_app(parent):
|
|
||||||
|
def plugin_app(parent: 'Tk'):
|
||||||
this.system_link = parent.children['system'] # system label in main window
|
this.system_link = parent.children['system'] # system label in main window
|
||||||
this.system = None
|
this.system = None
|
||||||
this.system_address = None
|
this.system_address = None
|
||||||
this.station = None
|
this.station = None
|
||||||
this.station_marketid = None # Frontier MarketID
|
this.station_marketid = None # Frontier MarketID
|
||||||
this.station_link = parent.children['station'] # station label in main window
|
this.station_link = parent.children['station'] # station label in main window
|
||||||
this.station_link.configure(popup_copy = lambda x: x != STATION_UNDOCKED)
|
this.station_link.configure(popup_copy=lambda x: x != STATION_UNDOCKED)
|
||||||
|
|
||||||
def prefs_changed(cmdr, is_beta):
|
def prefs_changed(cmdr, is_beta):
|
||||||
# Do *NOT* set 'url' here, as it's set to a function that will call
|
# Do *NOT* set 'url' here, as it's set to a function that will call
|
||||||
# through correctly. We don't want a static string.
|
# through correctly. We don't want a static string.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def journal_entry(cmdr, is_beta, system, station, entry, state):
|
def journal_entry(cmdr, is_beta, system, station, entry, state):
|
||||||
# Always update our system address even if we're not currently the provider for system or station, but dont update
|
# Always update our system address even if we're not currently the provider for system or station, but dont update
|
||||||
# on events that contain "future" data, such as FSDTarget
|
# on events that contain "future" data, such as FSDTarget
|
||||||
@ -105,7 +109,15 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
|
|
||||||
# But only actually change the URL if we are current station provider.
|
# But only actually change the URL if we are current station provider.
|
||||||
if config.get('station_provider') == 'eddb':
|
if config.get('station_provider') == 'eddb':
|
||||||
this.station_link['text'] = this.station or (this.system_population and this.system_population > 0 and STATION_UNDOCKED or '')
|
text = this.station
|
||||||
|
if not text:
|
||||||
|
if this.system_population is not None and this.system_population > 0:
|
||||||
|
text = STATION_UNDOCKED
|
||||||
|
|
||||||
|
else:
|
||||||
|
text = ''
|
||||||
|
|
||||||
|
this.station_link['text'] = text
|
||||||
# Do *NOT* set 'url' here, as it's set to a function that will call
|
# Do *NOT* set 'url' here, as it's set to a function that will call
|
||||||
# through correctly. We don't want a static string.
|
# through correctly. We don't want a static string.
|
||||||
this.station_link.update_idletasks()
|
this.station_link.update_idletasks()
|
||||||
@ -113,11 +125,15 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
|
|
||||||
def cmdr_data(data, is_beta):
|
def cmdr_data(data, is_beta):
|
||||||
# Always store initially, even if we're not the *current* system provider.
|
# Always store initially, even if we're not the *current* system provider.
|
||||||
if not this.station_marketid:
|
if not this.station_marketid and data['commander']['docked']:
|
||||||
this.station_marketid = data['commander']['docked'] and data['lastStarport']['id']
|
this.station_marketid = data['lastStarport']['id']
|
||||||
|
|
||||||
# Only trust CAPI if these aren't yet set
|
# Only trust CAPI if these aren't yet set
|
||||||
this.system = this.system or data['lastSystem']['name']
|
if not this.system:
|
||||||
this.station = this.station or data['commander']['docked'] and data['lastStarport']['name']
|
this.system = data['lastSystem']['name']
|
||||||
|
|
||||||
|
if not this.station and data['commander']['docked']:
|
||||||
|
this.station = data['lastStarport']['name']
|
||||||
|
|
||||||
# Override standard URL functions
|
# Override standard URL functions
|
||||||
if config.get('system_provider') == 'eddb':
|
if config.get('system_provider') == 'eddb':
|
||||||
@ -125,15 +141,17 @@ def cmdr_data(data, is_beta):
|
|||||||
# Do *NOT* set 'url' here, as it's set to a function that will call
|
# Do *NOT* set 'url' here, as it's set to a function that will call
|
||||||
# through correctly. We don't want a static string.
|
# through correctly. We don't want a static string.
|
||||||
this.system_link.update_idletasks()
|
this.system_link.update_idletasks()
|
||||||
|
|
||||||
if config.get('station_provider') == 'eddb':
|
if config.get('station_provider') == 'eddb':
|
||||||
if data['commander']['docked']:
|
if data['commander']['docked']:
|
||||||
this.station_link['text'] = this.station
|
this.station_link['text'] = this.station
|
||||||
|
|
||||||
elif data['lastStarport']['name'] and data['lastStarport']['name'] != "":
|
elif data['lastStarport']['name'] and data['lastStarport']['name'] != "":
|
||||||
this.station_link['text'] = STATION_UNDOCKED
|
this.station_link['text'] = STATION_UNDOCKED
|
||||||
|
|
||||||
else:
|
else:
|
||||||
this.station_link['text'] = ''
|
this.station_link['text'] = ''
|
||||||
|
|
||||||
# Do *NOT* set 'url' here, as it's set to a function that will call
|
# Do *NOT* set 'url' here, as it's set to a function that will call
|
||||||
# through correctly. We don't want a static string.
|
# through correctly. We don't want a static string.
|
||||||
this.station_link.update_idletasks()
|
this.station_link.update_idletasks()
|
||||||
|
|
||||||
|
576
plugins/eddn.py
576
plugins/eddn.py
@ -1,32 +1,40 @@
|
|||||||
# Export to EDDN
|
# Export to EDDN
|
||||||
|
|
||||||
from collections import OrderedDict
|
import itertools
|
||||||
import json
|
import json
|
||||||
from os import SEEK_SET, SEEK_CUR, SEEK_END
|
import logging
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tkinter as tk
|
||||||
|
from collections import OrderedDict
|
||||||
|
from os import SEEK_SET
|
||||||
from os.path import exists, join
|
from os.path import exists, join
|
||||||
from platform import system
|
from platform import system
|
||||||
import re
|
from typing import TYPE_CHECKING, Any, AnyStr, Dict, Iterator, List, Mapping, MutableMapping, Optional
|
||||||
|
from typing import OrderedDict as OrderedDictT
|
||||||
|
from typing import Sequence, TextIO, Tuple, Union
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from ttkHyperlinkLabel import HyperlinkLabel
|
|
||||||
import myNotebook as nb
|
|
||||||
|
|
||||||
|
import myNotebook as nb # noqa: N813
|
||||||
|
from companion import category_map
|
||||||
|
from config import applongname, appname, appversion, config
|
||||||
|
from myNotebook import Frame
|
||||||
from prefs import prefsVersion
|
from prefs import prefsVersion
|
||||||
|
from ttkHyperlinkLabel import HyperlinkLabel
|
||||||
|
|
||||||
if sys.platform != 'win32':
|
if sys.platform != 'win32':
|
||||||
from fcntl import lockf, LOCK_EX, LOCK_NB
|
from fcntl import lockf, LOCK_EX, LOCK_NB
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
from traceback import print_exc
|
|
||||||
|
|
||||||
from config import applongname, appversion, config
|
if TYPE_CHECKING:
|
||||||
from companion import category_map
|
def _(x: str) -> str:
|
||||||
|
return x
|
||||||
|
|
||||||
|
logger = logging.getLogger(appname)
|
||||||
|
|
||||||
this = sys.modules[__name__] # For holding module globals
|
this: Any = sys.modules[__name__] # For holding module globals
|
||||||
|
|
||||||
# Track location to add to Journal events
|
# Track location to add to Journal events
|
||||||
this.systemaddress = None
|
this.systemaddress = None
|
||||||
@ -35,130 +43,180 @@ this.planet = None
|
|||||||
|
|
||||||
# Avoid duplicates
|
# Avoid duplicates
|
||||||
this.marketId = None
|
this.marketId = None
|
||||||
this.commodities = this.outfitting = this.shipyard = None
|
this.commodities = None
|
||||||
|
this.outfitting: Optional[Tuple[bool, MutableMapping[str, Any]]] = None
|
||||||
|
this.shipyard = None
|
||||||
|
|
||||||
|
HORIZ_SKU = 'ELITE_HORIZONS_V_PLANETARY_LANDINGS'
|
||||||
|
|
||||||
|
|
||||||
class EDDN(object):
|
# TODO: a good few of these methods are static or could be classmethods. they should be created as such.
|
||||||
|
|
||||||
### SERVER = 'http://localhost:8081' # testing
|
class EDDN:
|
||||||
|
# SERVER = 'http://localhost:8081' # testing
|
||||||
SERVER = 'https://eddn.edcd.io:4430'
|
SERVER = 'https://eddn.edcd.io:4430'
|
||||||
UPLOAD = '%s/upload/' % SERVER
|
UPLOAD = f'{SERVER}/upload/'
|
||||||
REPLAYPERIOD = 400 # Roughly two messages per second, accounting for send delays [ms]
|
REPLAYPERIOD = 400 # Roughly two messages per second, accounting for send delays [ms]
|
||||||
REPLAYFLUSH = 20 # Update log on disk roughly every 10 seconds
|
REPLAYFLUSH = 20 # Update log on disk roughly every 10 seconds
|
||||||
TIMEOUT= 10 # requests timeout
|
TIMEOUT = 10 # requests timeout
|
||||||
MODULE_RE = re.compile('^Hpt_|^Int_|Armour_', re.IGNORECASE)
|
MODULE_RE = re.compile(r'^Hpt_|^Int_|Armour_', re.IGNORECASE)
|
||||||
CANONICALISE_RE = re.compile(r'\$(.+)_name;')
|
CANONICALISE_RE = re.compile(r'\$(.+)_name;')
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent: tk.Tk):
|
||||||
self.parent = parent
|
self.parent: tk.Tk = parent
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.replayfile = None # For delayed messages
|
self.replayfile: Optional[TextIO] = None # For delayed messages
|
||||||
self.replaylog = []
|
self.replaylog: List[str] = []
|
||||||
|
|
||||||
def load(self):
|
def load_journal_replay(self) -> bool:
|
||||||
|
"""
|
||||||
|
Load cached journal entries from disk
|
||||||
|
|
||||||
|
:return: a bool indicating success
|
||||||
|
"""
|
||||||
# Try to obtain exclusive access to the journal cache
|
# Try to obtain exclusive access to the journal cache
|
||||||
filename = join(config.app_dir, 'replay.jsonl')
|
filename = join(config.app_dir, 'replay.jsonl')
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
# Try to open existing file
|
# Try to open existing file
|
||||||
self.replayfile = open(filename, 'r+', buffering=1)
|
self.replayfile = open(filename, 'r+', buffering=1)
|
||||||
except:
|
|
||||||
|
except Exception:
|
||||||
if exists(filename):
|
if exists(filename):
|
||||||
raise # Couldn't open existing file
|
raise # Couldn't open existing file
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.replayfile = open(filename, 'w+', buffering=1) # Create file
|
self.replayfile = open(filename, 'w+', buffering=1) # Create file
|
||||||
|
|
||||||
if sys.platform != 'win32': # open for writing is automatically exclusive on Windows
|
if sys.platform != 'win32': # open for writing is automatically exclusive on Windows
|
||||||
lockf(self.replayfile, LOCK_EX|LOCK_NB)
|
lockf(self.replayfile, LOCK_EX | LOCK_NB)
|
||||||
except:
|
|
||||||
if __debug__: print_exc()
|
except Exception as e:
|
||||||
|
logger.debug('Failed opening "replay.jsonl"', exc_info=e)
|
||||||
if self.replayfile:
|
if self.replayfile:
|
||||||
self.replayfile.close()
|
self.replayfile.close()
|
||||||
|
|
||||||
self.replayfile = None
|
self.replayfile = None
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.replaylog = [line.strip() for line in self.replayfile]
|
self.replaylog = [line.strip() for line in self.replayfile]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
|
"""
|
||||||
|
flush flushes the replay file, clearing any data currently there that is not in the replaylog list
|
||||||
|
"""
|
||||||
self.replayfile.seek(0, SEEK_SET)
|
self.replayfile.seek(0, SEEK_SET)
|
||||||
self.replayfile.truncate()
|
self.replayfile.truncate()
|
||||||
for line in self.replaylog:
|
for line in self.replaylog:
|
||||||
self.replayfile.write('%s\n' % line)
|
self.replayfile.write(f'{line}\n')
|
||||||
|
|
||||||
self.replayfile.flush()
|
self.replayfile.flush()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
"""
|
||||||
|
close closes the replay file
|
||||||
|
"""
|
||||||
if self.replayfile:
|
if self.replayfile:
|
||||||
self.replayfile.close()
|
self.replayfile.close()
|
||||||
|
|
||||||
self.replayfile = None
|
self.replayfile = None
|
||||||
|
|
||||||
def send(self, cmdr, msg):
|
def send(self, cmdr: str, msg: Mapping[str, Any]) -> None:
|
||||||
uploaderID = cmdr
|
"""
|
||||||
|
Send sends an update to EDDN
|
||||||
|
|
||||||
msg = OrderedDict([
|
:param cmdr: the CMDR to use as the uploader ID
|
||||||
|
:param msg: the payload to send
|
||||||
|
"""
|
||||||
|
uploader_id = cmdr
|
||||||
|
|
||||||
|
to_send: OrderedDictT[str, str] = OrderedDict([
|
||||||
('$schemaRef', msg['$schemaRef']),
|
('$schemaRef', msg['$schemaRef']),
|
||||||
('header', OrderedDict([
|
('header', OrderedDict([
|
||||||
('softwareName', '%s [%s]' % (applongname, sys.platform=='darwin' and "Mac OS" or system())),
|
('softwareName', f'{applongname} [{system() if sys.platform != "darwin" else "Mac OS"}]'),
|
||||||
('softwareVersion', appversion),
|
('softwareVersion', appversion),
|
||||||
('uploaderID', uploaderID),
|
('uploaderID', uploader_id),
|
||||||
])),
|
])),
|
||||||
('message', msg['message']),
|
('message', msg['message']),
|
||||||
])
|
])
|
||||||
|
|
||||||
r = self.session.post(self.UPLOAD, data=json.dumps(msg), timeout=self.TIMEOUT)
|
r = self.session.post(self.UPLOAD, data=json.dumps(to_send), timeout=self.TIMEOUT)
|
||||||
if __debug__ and r.status_code != requests.codes.ok:
|
if r.status_code != requests.codes.ok:
|
||||||
print('Status\t%s' % r.status_code)
|
logger.debug(f':\nStatus\t{r.status_code}URL\t{r.url}Headers\t{r.headers}Content:\n{r.text}')
|
||||||
print('URL\t%s' % r.url)
|
|
||||||
print('Headers\t%s' % r.headers)
|
|
||||||
print('Content:\n%s' % r.text)
|
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
def sendreplay(self):
|
def sendreplay(self) -> None:
|
||||||
|
"""
|
||||||
|
sendreplay updates EDDN with cached journal lines
|
||||||
|
"""
|
||||||
if not self.replayfile:
|
if not self.replayfile:
|
||||||
return # Probably closing app
|
return # Probably closing app
|
||||||
|
|
||||||
status = self.parent.children['status']
|
status: Dict[str, Any] = self.parent.children['status']
|
||||||
|
|
||||||
if not self.replaylog:
|
if not self.replaylog:
|
||||||
status['text'] = ''
|
status['text'] = ''
|
||||||
return
|
return
|
||||||
|
|
||||||
|
localized: str = _('Sending data to EDDN...')
|
||||||
if len(self.replaylog) == 1:
|
if len(self.replaylog) == 1:
|
||||||
status['text'] = _('Sending data to EDDN...')
|
status['text'] = localized
|
||||||
|
|
||||||
else:
|
else:
|
||||||
status['text'] = '%s [%d]' % (_('Sending data to EDDN...').replace('...',''), len(self.replaylog))
|
status['text'] = f'{localized.replace("...", "")} [{len(self.replaylog)}]'
|
||||||
|
|
||||||
self.parent.update_idletasks()
|
self.parent.update_idletasks()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cmdr, msg = json.loads(self.replaylog[0], object_pairs_hook=OrderedDict)
|
cmdr, msg = json.loads(self.replaylog[0], object_pairs_hook=OrderedDict)
|
||||||
except:
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
# Couldn't decode - shouldn't happen!
|
# Couldn't decode - shouldn't happen!
|
||||||
if __debug__:
|
logger.debug(f'\n{self.replaylog[0]}\n', exc_info=e)
|
||||||
print(self.replaylog[0])
|
# Discard and continue
|
||||||
print_exc()
|
self.replaylog.pop(0)
|
||||||
self.replaylog.pop(0) # Discard and continue
|
|
||||||
else:
|
else:
|
||||||
# Rewrite old schema name
|
# Rewrite old schema name
|
||||||
if msg['$schemaRef'].startswith('http://schemas.elite-markets.net/eddn/'):
|
if msg['$schemaRef'].startswith('http://schemas.elite-markets.net/eddn/'):
|
||||||
msg['$schemaRef'] = 'https://eddn.edcd.io/schemas/' + msg['$schemaRef'][38:]
|
msg['$schemaRef'] = str(msg['$schemaRef']).replace(
|
||||||
|
'http://schemas.elite-markets.net/eddn/',
|
||||||
|
'https://eddn.edcd.io/schemas/'
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.send(cmdr, msg)
|
self.send(cmdr, msg)
|
||||||
self.replaylog.pop(0)
|
self.replaylog.pop(0)
|
||||||
if not len(self.replaylog) % self.REPLAYFLUSH:
|
if not len(self.replaylog) % self.REPLAYFLUSH:
|
||||||
self.flush()
|
self.flush()
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
if __debug__: print_exc()
|
logger.debug('Failed sending', exc_info=e)
|
||||||
status['text'] = _("Error: Can't connect to EDDN")
|
status['text'] = _("Error: Can't connect to EDDN")
|
||||||
return # stop sending
|
return # stop sending
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if __debug__: print_exc()
|
logger.debug('Failed sending', exc_info=e)
|
||||||
status['text'] = str(e)
|
status['text'] = str(e)
|
||||||
return # stop sending
|
return # stop sending
|
||||||
|
|
||||||
self.parent.after(self.REPLAYPERIOD, self.sendreplay)
|
self.parent.after(self.REPLAYPERIOD, self.sendreplay)
|
||||||
|
|
||||||
def export_commodities(self, data, is_beta):
|
def export_commodities(self, data: Mapping[str, Any], is_beta: bool) -> None:
|
||||||
commodities = []
|
"""
|
||||||
|
export_commodities updates EDDN with the commodities on the current (lastStarport) station.
|
||||||
|
Once the send is complete, this.commodities is updated with the new data.
|
||||||
|
|
||||||
|
:param data: a dict containing the starport data
|
||||||
|
:param is_beta: whether or not we're currently in beta mode
|
||||||
|
"""
|
||||||
|
commodities: List[OrderedDictT[str, Any]] = []
|
||||||
for commodity in data['lastStarport'].get('commodities') or []:
|
for commodity in data['lastStarport'].get('commodities') or []:
|
||||||
if (category_map.get(commodity['categoryname'], True) and # Check marketable
|
# Check 'marketable' and 'not prohibited'
|
||||||
not commodity.get('legality')): # check not prohibited
|
if (category_map.get(commodity['categoryname'], True)
|
||||||
|
and not commodity.get('legality')):
|
||||||
commodities.append(OrderedDict([
|
commodities.append(OrderedDict([
|
||||||
('name', commodity['name'].lower()),
|
('name', commodity['name'].lower()),
|
||||||
('meanPrice', int(commodity['meanPrice'])),
|
('meanPrice', int(commodity['meanPrice'])),
|
||||||
@ -169,41 +227,68 @@ class EDDN(object):
|
|||||||
('demand', int(commodity['demand'])),
|
('demand', int(commodity['demand'])),
|
||||||
('demandBracket', commodity['demandBracket']),
|
('demandBracket', commodity['demandBracket']),
|
||||||
]))
|
]))
|
||||||
|
|
||||||
if commodity['statusFlags']:
|
if commodity['statusFlags']:
|
||||||
commodities[-1]['statusFlags'] = commodity['statusFlags']
|
commodities[-1]['statusFlags'] = commodity['statusFlags']
|
||||||
commodities.sort(key = lambda c: c['name'])
|
|
||||||
|
commodities.sort(key=lambda c: c['name'])
|
||||||
|
|
||||||
if commodities and this.commodities != commodities: # Don't send empty commodities list - schema won't allow it
|
if commodities and this.commodities != commodities: # Don't send empty commodities list - schema won't allow it
|
||||||
message = OrderedDict([
|
message: OrderedDictT[str, Any] = OrderedDict([
|
||||||
('timestamp', data['timestamp']),
|
('timestamp', data['timestamp']),
|
||||||
('systemName', data['lastSystem']['name']),
|
('systemName', data['lastSystem']['name']),
|
||||||
('stationName', data['lastStarport']['name']),
|
('stationName', data['lastStarport']['name']),
|
||||||
('marketId', data['lastStarport']['id']),
|
('marketId', data['lastStarport']['id']),
|
||||||
('commodities', commodities),
|
('commodities', commodities),
|
||||||
])
|
])
|
||||||
|
|
||||||
if 'economies' in data['lastStarport']:
|
if 'economies' in data['lastStarport']:
|
||||||
message['economies'] = sorted(list([x for x in (data['lastStarport']['economies'] or {}).values()]), key=lambda x: x['name'])
|
message['economies'] = sorted(
|
||||||
|
(x for x in (data['lastStarport']['economies'] or {}).values()), key=lambda x: x['name']
|
||||||
|
)
|
||||||
|
|
||||||
if 'prohibited' in data['lastStarport']:
|
if 'prohibited' in data['lastStarport']:
|
||||||
message['prohibited'] = sorted(list([x for x in (data['lastStarport']['prohibited'] or {}).values()]))
|
message['prohibited'] = sorted(x for x in (data['lastStarport']['prohibited'] or {}).values())
|
||||||
|
|
||||||
self.send(data['commander']['name'], {
|
self.send(data['commander']['name'], {
|
||||||
'$schemaRef' : 'https://eddn.edcd.io/schemas/commodity/3' + (is_beta and '/test' or ''),
|
'$schemaRef': f'https://eddn.edcd.io/schemas/commodity/3{"/test" if is_beta else ""}',
|
||||||
'message' : message,
|
'message': message,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.commodities = commodities
|
this.commodities = commodities
|
||||||
|
|
||||||
def export_outfitting(self, data, is_beta):
|
def export_outfitting(self, data: Mapping[str, Any], is_beta: bool) -> None:
|
||||||
economies = data['lastStarport'].get('economies') or {}
|
"""
|
||||||
modules = data['lastStarport'].get('modules') or {}
|
export_outfitting updates EDDN with the current (lastStarport) station's outfitting options, if any.
|
||||||
ships = data['lastStarport'].get('ships') or { 'shipyard_list': {}, 'unavailable_list': [] }
|
Once the send is complete, this.outfitting is updated with the given data.
|
||||||
# Horizons flag - will hit at least Int_PlanetApproachSuite other than at engineer bases ("Colony"), prison or rescue Megaships, or under Pirate Attack etc
|
|
||||||
horizons = (any(economy['name'] == 'Colony' for economy in economies.values()) or
|
:param data: dict containing the outfitting data
|
||||||
any(module.get('sku') == 'ELITE_HORIZONS_V_PLANETARY_LANDINGS' for module in modules.values()) or
|
:param is_beta: whether or not we're currently in beta mode
|
||||||
any(ship.get('sku') == 'ELITE_HORIZONS_V_PLANETARY_LANDINGS' for ship in list((ships['shipyard_list'] or {}).values())))
|
"""
|
||||||
outfitting = sorted([self.MODULE_RE.sub(lambda m: m.group(0).capitalize(), module['name'].lower()) for module in modules.values() if self.MODULE_RE.search(module['name']) and module.get('sku') in [None, 'ELITE_HORIZONS_V_PLANETARY_LANDINGS'] and module['name'] != 'Int_PlanetApproachSuite'])
|
modules: Dict[str, Any] = data['lastStarport'].get('modules') or {}
|
||||||
if outfitting and this.outfitting != (horizons, outfitting): # Don't send empty modules list - schema won't allow it
|
|
||||||
|
# Horizons flag - will hit at least Int_PlanetApproachSuite other than at engineer bases ("Colony"),
|
||||||
|
# prison or rescue Megaships, or under Pirate Attack etc
|
||||||
|
horizons: bool = is_horizons(
|
||||||
|
data['lastStarport'].get('economies', {}),
|
||||||
|
modules,
|
||||||
|
data['lastStarport'].get('ships', {'shipyard_list': {}, 'unavailable_list': []})
|
||||||
|
)
|
||||||
|
|
||||||
|
to_search: Iterator[Mapping[str, Any]] = filter(
|
||||||
|
lambda m: self.MODULE_RE.search(m['name']) and m.get('sku') in (None, HORIZ_SKU) and
|
||||||
|
m['name'] != 'Int_PlanetApproachSuite',
|
||||||
|
modules.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
outfitting: List[str] = sorted(
|
||||||
|
self.MODULE_RE.sub(lambda match: match.group(0).capitalize(), mod['name'].lower()) for mod in to_search
|
||||||
|
)
|
||||||
|
# Don't send empty modules list - schema won't allow it
|
||||||
|
if outfitting and this.outfitting != (horizons, outfitting):
|
||||||
self.send(data['commander']['name'], {
|
self.send(data['commander']['name'], {
|
||||||
'$schemaRef' : 'https://eddn.edcd.io/schemas/outfitting/2' + (is_beta and '/test' or ''),
|
'$schemaRef': f'https://eddn.edcd.io/schemas/outfitting/2{"/test" if is_beta else ""}',
|
||||||
'message' : OrderedDict([
|
'message': OrderedDict([
|
||||||
('timestamp', data['timestamp']),
|
('timestamp', data['timestamp']),
|
||||||
('systemName', data['lastSystem']['name']),
|
('systemName', data['lastSystem']['name']),
|
||||||
('stationName', data['lastStarport']['name']),
|
('stationName', data['lastStarport']['name']),
|
||||||
@ -212,20 +297,35 @@ class EDDN(object):
|
|||||||
('modules', outfitting),
|
('modules', outfitting),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.outfitting = (horizons, outfitting)
|
this.outfitting = (horizons, outfitting)
|
||||||
|
|
||||||
def export_shipyard(self, data, is_beta):
|
def export_shipyard(self, data: Dict[str, Any], is_beta: bool) -> None:
|
||||||
economies = data['lastStarport'].get('economies') or {}
|
"""
|
||||||
modules = data['lastStarport'].get('modules') or {}
|
export_shipyard updates EDDN with the current (lastStarport) station's outfitting options, if any.
|
||||||
ships = data['lastStarport'].get('ships') or { 'shipyard_list': {}, 'unavailable_list': [] }
|
once the send is complete, this.shipyard is updated to the new data.
|
||||||
horizons = (any(economy['name'] == 'Colony' for economy in economies.values()) or
|
|
||||||
any(module.get('sku') == 'ELITE_HORIZONS_V_PLANETARY_LANDINGS' for module in modules.values()) or
|
:param data: dict containing the shipyard data
|
||||||
any(ship.get('sku') == 'ELITE_HORIZONS_V_PLANETARY_LANDINGS' for ship in list((ships['shipyard_list'] or {}).values())))
|
:param is_beta: whether or not we are in beta mode
|
||||||
shipyard = sorted([ship['name'].lower() for ship in list((ships['shipyard_list'] or {}).values()) + ships['unavailable_list']])
|
"""
|
||||||
if shipyard and this.shipyard != (horizons, shipyard): # Don't send empty ships list - shipyard data is only guaranteed present if user has visited the shipyard.
|
ships: Dict[str, Any] = data['lastStarport'].get('ships', {'shipyard_list': {}, 'unavailable_list': []})
|
||||||
|
horizons: bool = is_horizons(
|
||||||
|
data['lastStarport'].get('economies', {}),
|
||||||
|
data['lastStarport'].get('modules', {}),
|
||||||
|
ships
|
||||||
|
)
|
||||||
|
|
||||||
|
shipyard: List[Mapping[str, Any]] = sorted(
|
||||||
|
itertools.chain(
|
||||||
|
(ship['name'].lower() for ship in (ships['shipyard_list'] or {}).values()),
|
||||||
|
ships['unavailable_list']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Don't send empty ships list - shipyard data is only guaranteed present if user has visited the shipyard.
|
||||||
|
if shipyard and this.shipyard != (horizons, shipyard):
|
||||||
self.send(data['commander']['name'], {
|
self.send(data['commander']['name'], {
|
||||||
'$schemaRef' : 'https://eddn.edcd.io/schemas/shipyard/2' + (is_beta and '/test' or ''),
|
'$schemaRef': f'https://eddn.edcd.io/schemas/shipyard/2{"/test" if is_beta else ""}',
|
||||||
'message' : OrderedDict([
|
'message': OrderedDict([
|
||||||
('timestamp', data['timestamp']),
|
('timestamp', data['timestamp']),
|
||||||
('systemName', data['lastSystem']['name']),
|
('systemName', data['lastSystem']['name']),
|
||||||
('stationName', data['lastStarport']['name']),
|
('stationName', data['lastStarport']['name']),
|
||||||
@ -234,11 +334,20 @@ class EDDN(object):
|
|||||||
('ships', shipyard),
|
('ships', shipyard),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.shipyard = (horizons, shipyard)
|
this.shipyard = (horizons, shipyard)
|
||||||
|
|
||||||
def export_journal_commodities(self, cmdr, is_beta, entry):
|
def export_journal_commodities(self, cmdr: str, is_beta: bool, entry: Mapping[str, Any]) -> None:
|
||||||
items = entry.get('Items') or []
|
"""
|
||||||
commodities = sorted([OrderedDict([
|
export_journal_commodities updates EDDN with the commodities list on the current station (lastStarport) from
|
||||||
|
data in the journal. As a side effect, it also updates this.commodities with the data
|
||||||
|
|
||||||
|
:param cmdr: The commander to send data under
|
||||||
|
:param is_beta: whether or not we're in beta mode
|
||||||
|
:param entry: the journal entry containing the commodities data
|
||||||
|
"""
|
||||||
|
items: List[Mapping[str, Any]] = entry.get('Items') or []
|
||||||
|
commodities: Sequence[OrderedDictT[AnyStr, Any]] = sorted((OrderedDict([
|
||||||
('name', self.canonicalise(commodity['Name'])),
|
('name', self.canonicalise(commodity['Name'])),
|
||||||
('meanPrice', commodity['MeanPrice']),
|
('meanPrice', commodity['MeanPrice']),
|
||||||
('buyPrice', commodity['BuyPrice']),
|
('buyPrice', commodity['BuyPrice']),
|
||||||
@ -247,12 +356,12 @@ class EDDN(object):
|
|||||||
('sellPrice', commodity['SellPrice']),
|
('sellPrice', commodity['SellPrice']),
|
||||||
('demand', commodity['Demand']),
|
('demand', commodity['Demand']),
|
||||||
('demandBracket', commodity['DemandBracket']),
|
('demandBracket', commodity['DemandBracket']),
|
||||||
]) for commodity in items], key = lambda c: c['name'])
|
]) for commodity in items), key=lambda c: c['name'])
|
||||||
|
|
||||||
if commodities and this.commodities != commodities: # Don't send empty commodities list - schema won't allow it
|
if commodities and this.commodities != commodities: # Don't send empty commodities list - schema won't allow it
|
||||||
self.send(cmdr, {
|
self.send(cmdr, {
|
||||||
'$schemaRef' : 'https://eddn.edcd.io/schemas/commodity/3' + (is_beta and '/test' or ''),
|
'$schemaRef': f'https://eddn.edcd.io/schemas/commodity/3{"/test" if is_beta else ""}',
|
||||||
'message' : OrderedDict([
|
'message': OrderedDict([
|
||||||
('timestamp', entry['timestamp']),
|
('timestamp', entry['timestamp']),
|
||||||
('systemName', entry['StarSystem']),
|
('systemName', entry['StarSystem']),
|
||||||
('stationName', entry['StationName']),
|
('stationName', entry['StationName']),
|
||||||
@ -260,16 +369,31 @@ class EDDN(object):
|
|||||||
('commodities', commodities),
|
('commodities', commodities),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
this.commodities = commodities
|
|
||||||
|
|
||||||
def export_journal_outfitting(self, cmdr, is_beta, entry):
|
this.commodities: OrderedDictT[str, Any] = commodities
|
||||||
modules = entry.get('Items') or []
|
|
||||||
horizons = entry.get('Horizons', False)
|
def export_journal_outfitting(self, cmdr: str, is_beta: bool, entry: Mapping[str, Any]) -> None:
|
||||||
outfitting = sorted([self.MODULE_RE.sub(lambda m: m.group(0).capitalize(), module['Name']) for module in modules if module['Name'] != 'int_planetapproachsuite'])
|
"""
|
||||||
if outfitting and this.outfitting != (horizons, outfitting): # Don't send empty modules list - schema won't allow it
|
export_journal_outfitting updates EDDN with station outfitting based on a journal entry. As a side effect,
|
||||||
|
it also updates this.outfitting with the data
|
||||||
|
|
||||||
|
:param cmdr: The commander to send data under
|
||||||
|
:param is_beta: Whether or not we're in beta mode
|
||||||
|
:param entry: The relevant journal entry
|
||||||
|
"""
|
||||||
|
modules: List[Mapping[str, Any]] = entry.get('Items', [])
|
||||||
|
horizons: bool = entry.get('Horizons', False)
|
||||||
|
# outfitting = sorted([self.MODULE_RE.sub(lambda m: m.group(0).capitalize(), module['Name'])
|
||||||
|
# for module in modules if module['Name'] != 'int_planetapproachsuite'])
|
||||||
|
outfitting: List[str] = sorted(
|
||||||
|
self.MODULE_RE.sub(lambda m: m.group(0).capitalize(), mod['Name']) for mod in
|
||||||
|
filter(lambda m: m['Name'] != 'int_planetapproachsuite', modules)
|
||||||
|
)
|
||||||
|
# Don't send empty modules list - schema won't allow it
|
||||||
|
if outfitting and this.outfitting != (horizons, outfitting):
|
||||||
self.send(cmdr, {
|
self.send(cmdr, {
|
||||||
'$schemaRef' : 'https://eddn.edcd.io/schemas/outfitting/2' + (is_beta and '/test' or ''),
|
'$schemaRef': f'https://eddn.edcd.io/schemas/outfitting/2{"/test" if is_beta else ""}',
|
||||||
'message' : OrderedDict([
|
'message': OrderedDict([
|
||||||
('timestamp', entry['timestamp']),
|
('timestamp', entry['timestamp']),
|
||||||
('systemName', entry['StarSystem']),
|
('systemName', entry['StarSystem']),
|
||||||
('stationName', entry['StationName']),
|
('stationName', entry['StationName']),
|
||||||
@ -278,16 +402,26 @@ class EDDN(object):
|
|||||||
('modules', outfitting),
|
('modules', outfitting),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.outfitting = (horizons, outfitting)
|
this.outfitting = (horizons, outfitting)
|
||||||
|
|
||||||
def export_journal_shipyard(self, cmdr, is_beta, entry):
|
def export_journal_shipyard(self, cmdr: str, is_beta: bool, entry: Mapping[str, Any]) -> None:
|
||||||
ships = entry.get('PriceList') or []
|
"""
|
||||||
horizons = entry.get('Horizons', False)
|
export_journal_shipyard updates EDDN with station shipyard data based on a journal entry. As a side effect,
|
||||||
shipyard = sorted([ship['ShipType'] for ship in ships])
|
this.shipyard is updated with the data.
|
||||||
if shipyard and this.shipyard != (horizons, shipyard): # Don't send empty ships list - shipyard data is only guaranteed present if user has visited the shipyard.
|
|
||||||
|
:param cmdr: the commander to send this update under
|
||||||
|
:param is_beta: Whether or not we're in beta mode
|
||||||
|
:param entry: the relevant journal entry
|
||||||
|
"""
|
||||||
|
ships: List[Mapping[str, Any]] = entry.get('PriceList') or []
|
||||||
|
horizons: bool = entry.get('Horizons', False)
|
||||||
|
shipyard = sorted(ship['ShipType'] for ship in ships)
|
||||||
|
# Don't send empty ships list - shipyard data is only guaranteed present if user has visited the shipyard.
|
||||||
|
if shipyard and this.shipyard != (horizons, shipyard):
|
||||||
self.send(cmdr, {
|
self.send(cmdr, {
|
||||||
'$schemaRef' : 'https://eddn.edcd.io/schemas/shipyard/2' + (is_beta and '/test' or ''),
|
'$schemaRef': f'https://eddn.edcd.io/schemas/shipyard/2{"/test" if is_beta else ""}',
|
||||||
'message' : OrderedDict([
|
'message': OrderedDict([
|
||||||
('timestamp', entry['timestamp']),
|
('timestamp', entry['timestamp']),
|
||||||
('systemName', entry['StarSystem']),
|
('systemName', entry['StarSystem']),
|
||||||
('stationName', entry['StationName']),
|
('stationName', entry['StationName']),
|
||||||
@ -296,130 +430,206 @@ class EDDN(object):
|
|||||||
('ships', shipyard),
|
('ships', shipyard),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.shipyard = (horizons, shipyard)
|
this.shipyard = (horizons, shipyard)
|
||||||
|
|
||||||
def export_journal_entry(self, cmdr, is_beta, entry):
|
def export_journal_entry(self, cmdr: str, is_beta: bool, entry: Mapping[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
export_journal_entry updates EDDN with a line from the journal. Additionally if additional lines are cached,
|
||||||
|
it may send those as well.
|
||||||
|
|
||||||
|
:param cmdr: the commander under which this upload is made
|
||||||
|
:param is_beta: whether or not we are in beta mode
|
||||||
|
:param entry: the journal entry to send
|
||||||
|
"""
|
||||||
msg = {
|
msg = {
|
||||||
'$schemaRef' : 'https://eddn.edcd.io/schemas/journal/1' + (is_beta and '/test' or ''),
|
'$schemaRef': f'https://eddn.edcd.io/schemas/journal/1{"/test" if is_beta else ""}',
|
||||||
'message' : entry
|
'message': entry
|
||||||
}
|
}
|
||||||
if self.replayfile or self.load():
|
|
||||||
|
if self.replayfile or self.load_journal_replay():
|
||||||
# Store the entry
|
# Store the entry
|
||||||
self.replaylog.append(json.dumps([cmdr, msg]))
|
self.replaylog.append(json.dumps([cmdr, msg]))
|
||||||
self.replayfile.write('%s\n' % self.replaylog[-1])
|
self.replayfile.write(f'{self.replaylog[-1]}\n')
|
||||||
|
|
||||||
if (entry['event'] == 'Docked' or
|
if (
|
||||||
(entry['event'] == 'Location' and entry['Docked']) or
|
entry['event'] == 'Docked' or (entry['event'] == 'Location' and entry['Docked']) or not
|
||||||
not (config.getint('output') & config.OUT_SYS_DELAY)):
|
(config.getint('output') & config.OUT_SYS_DELAY)
|
||||||
|
):
|
||||||
self.parent.after(self.REPLAYPERIOD, self.sendreplay) # Try to send this and previous entries
|
self.parent.after(self.REPLAYPERIOD, self.sendreplay) # Try to send this and previous entries
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Can't access replay file! Send immediately.
|
# Can't access replay file! Send immediately.
|
||||||
status = self.parent.children['status']
|
status: MutableMapping[str, str] = self.parent.children['status']
|
||||||
status['text'] = _('Sending data to EDDN...')
|
status['text'] = _('Sending data to EDDN...')
|
||||||
self.parent.update_idletasks()
|
self.parent.update_idletasks()
|
||||||
self.send(cmdr, msg)
|
self.send(cmdr, msg)
|
||||||
status['text'] = ''
|
status['text'] = ''
|
||||||
|
|
||||||
def canonicalise(self, item):
|
def canonicalise(self, item: str) -> str:
|
||||||
match = self.CANONICALISE_RE.match(item)
|
match = self.CANONICALISE_RE.match(item)
|
||||||
return match and match.group(1) or item
|
return match and match.group(1) or item
|
||||||
|
|
||||||
|
|
||||||
# Plugin callbacks
|
# Plugin callbacks
|
||||||
|
|
||||||
def plugin_start3(plugin_dir):
|
def plugin_start3(plugin_dir: str) -> str:
|
||||||
return 'EDDN'
|
return 'EDDN'
|
||||||
|
|
||||||
def plugin_app(parent):
|
|
||||||
|
def plugin_app(parent: tk.Tk) -> None:
|
||||||
this.parent = parent
|
this.parent = parent
|
||||||
this.eddn = EDDN(parent)
|
this.eddn = EDDN(parent)
|
||||||
# Try to obtain exclusive lock on journal cache, even if we don't need it yet
|
# Try to obtain exclusive lock on journal cache, even if we don't need it yet
|
||||||
if not this.eddn.load():
|
if not this.eddn.load_journal_replay():
|
||||||
this.status['text'] = 'Error: Is another copy of this app already running?' # Shouldn't happen - don't bother localizing
|
# Shouldn't happen - don't bother localizing
|
||||||
|
this.status['text'] = 'Error: Is another copy of this app already running?'
|
||||||
|
|
||||||
def plugin_prefs(parent, cmdr, is_beta):
|
|
||||||
|
|
||||||
PADX = 10
|
def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame:
|
||||||
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
PADX = 10 # noqa: N806
|
||||||
PADY = 2 # close spacing
|
BUTTONX = 12 # noqa: N806 # indent Checkbuttons and Radiobuttons
|
||||||
|
|
||||||
if prefsVersion.shouldSetDefaults('0.0.0.0', not bool(config.getint('output'))):
|
if prefsVersion.shouldSetDefaults('0.0.0.0', not bool(config.getint('output'))):
|
||||||
output = (config.OUT_MKT_EDDN | config.OUT_SYS_EDDN) # default settings
|
output: int = (config.OUT_MKT_EDDN | config.OUT_SYS_EDDN) # default settings
|
||||||
|
|
||||||
else:
|
else:
|
||||||
output = config.getint('output')
|
output: int = config.getint('output')
|
||||||
|
|
||||||
eddnframe = nb.Frame(parent)
|
eddnframe = nb.Frame(parent)
|
||||||
|
|
||||||
HyperlinkLabel(eddnframe, text='Elite Dangerous Data Network', background=nb.Label().cget('background'), url='https://github.com/EDSM-NET/EDDN/wiki', underline=True).grid(padx=PADX, sticky=tk.W) # Don't translate
|
HyperlinkLabel(
|
||||||
this.eddn_station= tk.IntVar(value = (output & config.OUT_MKT_EDDN) and 1)
|
eddnframe,
|
||||||
this.eddn_station_button = nb.Checkbutton(eddnframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=this.eddn_station, command=prefsvarchanged) # Output setting
|
text='Elite Dangerous Data Network',
|
||||||
this.eddn_station_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W)
|
background=nb.Label().cget('background'),
|
||||||
this.eddn_system = tk.IntVar(value = (output & config.OUT_SYS_EDDN) and 1)
|
url='https://github.com/EDSM-NET/EDDN/wiki',
|
||||||
this.eddn_system_button = nb.Checkbutton(eddnframe, text=_('Send system and scan data to the Elite Dangerous Data Network'), variable=this.eddn_system, command=prefsvarchanged) # Output setting new in E:D 2.2
|
underline=True
|
||||||
this.eddn_system_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W)
|
).grid(padx=PADX, sticky=tk.W) # Don't translate
|
||||||
this.eddn_delay= tk.IntVar(value = (output & config.OUT_SYS_DELAY) and 1)
|
|
||||||
this.eddn_delay_button = nb.Checkbutton(eddnframe, text=_('Delay sending until docked'), variable=this.eddn_delay) # Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2
|
this.eddn_station = tk.IntVar(value=(output & config.OUT_MKT_EDDN) and 1)
|
||||||
|
this.eddn_station_button = nb.Checkbutton(
|
||||||
|
eddnframe,
|
||||||
|
text=_('Send station data to the Elite Dangerous Data Network'),
|
||||||
|
variable=this.eddn_station,
|
||||||
|
command=prefsvarchanged
|
||||||
|
) # Output setting
|
||||||
|
|
||||||
|
this.eddn_station_button.grid(padx=BUTTONX, pady=(5, 0), sticky=tk.W)
|
||||||
|
this.eddn_system = tk.IntVar(value=(output & config.OUT_SYS_EDDN) and 1)
|
||||||
|
# Output setting new in E:D 2.2
|
||||||
|
this.eddn_system_button = nb.Checkbutton(
|
||||||
|
eddnframe,
|
||||||
|
text=_('Send system and scan data to the Elite Dangerous Data Network'),
|
||||||
|
variable=this.eddn_system,
|
||||||
|
command=prefsvarchanged
|
||||||
|
)
|
||||||
|
|
||||||
|
this.eddn_system_button.grid(padx=BUTTONX, pady=(5, 0), sticky=tk.W)
|
||||||
|
this.eddn_delay = tk.IntVar(value=(output & config.OUT_SYS_DELAY) and 1)
|
||||||
|
# Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2
|
||||||
|
this.eddn_delay_button = nb.Checkbutton(
|
||||||
|
eddnframe,
|
||||||
|
text=_('Delay sending until docked'),
|
||||||
|
variable=this.eddn_delay
|
||||||
|
)
|
||||||
this.eddn_delay_button.grid(padx=BUTTONX, sticky=tk.W)
|
this.eddn_delay_button.grid(padx=BUTTONX, sticky=tk.W)
|
||||||
|
|
||||||
return eddnframe
|
return eddnframe
|
||||||
|
|
||||||
def prefsvarchanged(event=None):
|
|
||||||
|
def prefsvarchanged(event=None) -> None:
|
||||||
this.eddn_station_button['state'] = tk.NORMAL
|
this.eddn_station_button['state'] = tk.NORMAL
|
||||||
this.eddn_system_button['state']= tk.NORMAL
|
this.eddn_system_button['state'] = tk.NORMAL
|
||||||
this.eddn_delay_button['state'] = this.eddn.replayfile and this.eddn_system.get() and tk.NORMAL or tk.DISABLED
|
this.eddn_delay_button['state'] = this.eddn.replayfile and this.eddn_system.get() and tk.NORMAL or tk.DISABLED
|
||||||
|
|
||||||
def prefs_changed(cmdr, is_beta):
|
|
||||||
config.set('output',
|
def prefs_changed(cmdr: str, is_beta: bool) -> None:
|
||||||
(config.getint('output') & (config.OUT_MKT_TD | config.OUT_MKT_CSV | config.OUT_SHIP |config.OUT_MKT_MANUAL)) +
|
config.set(
|
||||||
|
'output',
|
||||||
|
(config.getint('output') & (config.OUT_MKT_TD | config.OUT_MKT_CSV | config.OUT_SHIP | config.OUT_MKT_MANUAL)) +
|
||||||
(this.eddn_station.get() and config.OUT_MKT_EDDN) +
|
(this.eddn_station.get() and config.OUT_MKT_EDDN) +
|
||||||
(this.eddn_system.get() and config.OUT_SYS_EDDN) +
|
(this.eddn_system.get() and config.OUT_SYS_EDDN) +
|
||||||
(this.eddn_delay.get() and config.OUT_SYS_DELAY))
|
(this.eddn_delay.get() and config.OUT_SYS_DELAY)
|
||||||
|
)
|
||||||
|
|
||||||
def plugin_stop():
|
|
||||||
|
def plugin_stop() -> None:
|
||||||
this.eddn.close()
|
this.eddn.close()
|
||||||
|
|
||||||
def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|
||||||
|
|
||||||
|
def journal_entry(
|
||||||
|
cmdr: str, is_beta: bool, system: str, station: str, entry: MutableMapping[str, Any], state: Mapping[str, Any]
|
||||||
|
) -> Optional[str]:
|
||||||
# Recursively filter '*_Localised' keys from dict
|
# Recursively filter '*_Localised' keys from dict
|
||||||
def filter_localised(d):
|
def filter_localised(d: Mapping[str, Any]) -> OrderedDictT[str, Any]:
|
||||||
filtered = OrderedDict()
|
filtered: OrderedDictT[str, Any] = OrderedDict()
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
if k.endswith('_Localised'):
|
if k.endswith('_Localised'):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif hasattr(v, 'items'): # dict -> recurse
|
elif hasattr(v, 'items'): # dict -> recurse
|
||||||
filtered[k] = filter_localised(v)
|
filtered[k] = filter_localised(v)
|
||||||
|
|
||||||
elif isinstance(v, list): # list of dicts -> recurse
|
elif isinstance(v, list): # list of dicts -> recurse
|
||||||
filtered[k] = [filter_localised(x) if hasattr(x, 'items') else x for x in v]
|
filtered[k] = [filter_localised(x) if hasattr(x, 'items') else x for x in v]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
filtered[k] = v
|
filtered[k] = v
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
# Track location
|
# Track location
|
||||||
if entry['event'] in ['Location', 'FSDJump', 'Docked', 'CarrierJump']:
|
if entry['event'] in ('Location', 'FSDJump', 'Docked', 'CarrierJump'):
|
||||||
if entry['event'] in ('Location', 'CarrierJump'):
|
if entry['event'] in ('Location', 'CarrierJump'):
|
||||||
this.planet = entry.get('Body') if entry.get('BodyType') == 'Planet' else None
|
this.planet: Optional[str] = entry.get('Body') if entry.get('BodyType') == 'Planet' else None
|
||||||
|
|
||||||
elif entry['event'] == 'FSDJump':
|
elif entry['event'] == 'FSDJump':
|
||||||
this.planet = None
|
this.planet: Optional[str] = None
|
||||||
|
|
||||||
if 'StarPos' in entry:
|
if 'StarPos' in entry:
|
||||||
this.coordinates = tuple(entry['StarPos'])
|
this.coordinates: Optional[Tuple[int, int, int]] = tuple(entry['StarPos'])
|
||||||
|
|
||||||
elif this.systemaddress != entry.get('SystemAddress'):
|
elif this.systemaddress != entry.get('SystemAddress'):
|
||||||
this.coordinates = None # Docked event doesn't include coordinates
|
this.coordinates: Optional[Tuple[int, int, int]] = None # Docked event doesn't include coordinates
|
||||||
this.systemaddress = entry.get('SystemAddress')
|
|
||||||
|
this.systemaddress: Optional[str] = entry.get('SystemAddress')
|
||||||
|
|
||||||
elif entry['event'] == 'ApproachBody':
|
elif entry['event'] == 'ApproachBody':
|
||||||
this.planet = entry['Body']
|
this.planet = entry['Body']
|
||||||
elif entry['event'] in ['LeaveBody', 'SupercruiseEntry']:
|
|
||||||
|
elif entry['event'] in ('LeaveBody', 'SupercruiseEntry'):
|
||||||
this.planet = None
|
this.planet = None
|
||||||
|
|
||||||
# Send interesting events to EDDN, but not when on a crew
|
# Send interesting events to EDDN, but not when on a crew
|
||||||
if (config.getint('output') & config.OUT_SYS_EDDN and not state['Captain'] and
|
if (config.getint('output') & config.OUT_SYS_EDDN and not state['Captain'] and
|
||||||
(entry['event'] in ('Location', 'FSDJump', 'Docked', 'Scan', 'SAASignalsFound', 'CarrierJump')) and
|
(entry['event'] in ('Location', 'FSDJump', 'Docked', 'Scan', 'SAASignalsFound', 'CarrierJump')) and
|
||||||
('StarPos' in entry or this.coordinates)):
|
('StarPos' in entry or this.coordinates)):
|
||||||
|
|
||||||
# strip out properties disallowed by the schema
|
# strip out properties disallowed by the schema
|
||||||
for thing in ['ActiveFine', 'CockpitBreach', 'BoostUsed', 'FuelLevel', 'FuelUsed', 'JumpDist', 'Latitude', 'Longitude', 'Wanted']:
|
for thing in (
|
||||||
|
'ActiveFine',
|
||||||
|
'CockpitBreach',
|
||||||
|
'BoostUsed',
|
||||||
|
'FuelLevel',
|
||||||
|
'FuelUsed',
|
||||||
|
'JumpDist',
|
||||||
|
'Latitude',
|
||||||
|
'Longitude',
|
||||||
|
'Wanted'
|
||||||
|
):
|
||||||
entry.pop(thing, None)
|
entry.pop(thing, None)
|
||||||
|
|
||||||
if 'Factions' in entry:
|
if 'Factions' in entry:
|
||||||
# Filter faction state. `entry` is a shallow copy so replace 'Factions' value rather than modify in-place.
|
# Filter faction state to comply with schema restrictions regarding personal data. `entry` is a shallow copy
|
||||||
entry['Factions'] = [ {k: v for k, v in f.items() if k not in ['HappiestSystem', 'HomeSystem', 'MyReputation', 'SquadronFaction']} for f in entry['Factions']]
|
# so replace 'Factions' value rather than modify in-place.
|
||||||
|
entry['Factions'] = [
|
||||||
|
{
|
||||||
|
k: v for k, v in f.items() if k not in (
|
||||||
|
'HappiestSystem', 'HomeSystem', 'MyReputation', 'SquadronFaction'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for f in entry['Factions']
|
||||||
|
]
|
||||||
|
|
||||||
# add planet to Docked event for planetary stations if known
|
# add planet to Docked event for planetary stations if known
|
||||||
if entry['event'] == 'Docked' and this.planet:
|
if entry['event'] == 'Docked' and this.planet:
|
||||||
@ -429,50 +639,66 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
|||||||
# add mandatory StarSystem, StarPos and SystemAddress properties to Scan events
|
# add mandatory StarSystem, StarPos and SystemAddress properties to Scan events
|
||||||
if 'StarSystem' not in entry:
|
if 'StarSystem' not in entry:
|
||||||
if not system:
|
if not system:
|
||||||
return("system is None, can't add StarSystem")
|
logger.warn("system is None, can't add StarSystem")
|
||||||
|
return "system is None, can't add StarSystem"
|
||||||
|
|
||||||
entry['StarSystem'] = system
|
entry['StarSystem'] = system
|
||||||
|
|
||||||
if 'StarPos' not in entry:
|
if 'StarPos' not in entry:
|
||||||
if not this.coordinates:
|
if not this.coordinates:
|
||||||
return("this.coordinates is None, can't add StarPos")
|
logger.warn("this.coordinates is None, can't add StarPos")
|
||||||
|
return "this.coordinates is None, can't add StarPos"
|
||||||
|
|
||||||
entry['StarPos'] = list(this.coordinates)
|
entry['StarPos'] = list(this.coordinates)
|
||||||
|
|
||||||
if 'SystemAddress' not in entry:
|
if 'SystemAddress' not in entry:
|
||||||
if not this.systemaddress:
|
if not this.systemaddress:
|
||||||
return("this.systemaddress is None, can't add SystemAddress")
|
logger.warn("this.systemaddress is None, can't add SystemAddress")
|
||||||
|
return "this.systemaddress is None, can't add SystemAddress"
|
||||||
|
|
||||||
entry['SystemAddress'] = this.systemaddress
|
entry['SystemAddress'] = this.systemaddress
|
||||||
|
|
||||||
try:
|
try:
|
||||||
this.eddn.export_journal_entry(cmdr, is_beta, filter_localised(entry))
|
this.eddn.export_journal_entry(cmdr, is_beta, filter_localised(entry))
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
if __debug__: print_exc()
|
logger.debug('Failed in export_journal_entry', exc_info=e)
|
||||||
return _("Error: Can't connect to EDDN")
|
return _("Error: Can't connect to EDDN")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if __debug__: print_exc()
|
logger.debug('Failed in export_journal_entry', exc_info=e)
|
||||||
return str(e)
|
return str(e)
|
||||||
|
|
||||||
elif (config.getint('output') & config.OUT_MKT_EDDN and not state['Captain'] and
|
elif (config.getint('output') & config.OUT_MKT_EDDN and not state['Captain'] and
|
||||||
entry['event'] in ['Market', 'Outfitting', 'Shipyard']):
|
entry['event'] in ('Market', 'Outfitting', 'Shipyard')):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if this.marketId != entry['MarketID']:
|
if this.marketId != entry['MarketID']:
|
||||||
this.commodities = this.outfitting = this.shipyard = None
|
this.commodities = this.outfitting = this.shipyard = None
|
||||||
this.marketId = entry['MarketID']
|
this.marketId = entry['MarketID']
|
||||||
|
|
||||||
with open(join(config.get('journaldir') or config.default_journal_dir, '%s.json' % entry['event']), 'rb') as h:
|
path = pathlib.Path(str(config.get('journaldir') or config.default_journal_dir)) / f'{entry["event"]}.json'
|
||||||
entry = json.load(h)
|
with path.open('rb') as f:
|
||||||
|
entry = json.load(f)
|
||||||
if entry['event'] == 'Market':
|
if entry['event'] == 'Market':
|
||||||
this.eddn.export_journal_commodities(cmdr, is_beta, entry)
|
this.eddn.export_journal_commodities(cmdr, is_beta, entry)
|
||||||
|
|
||||||
elif entry['event'] == 'Outfitting':
|
elif entry['event'] == 'Outfitting':
|
||||||
this.eddn.export_journal_outfitting(cmdr, is_beta, entry)
|
this.eddn.export_journal_outfitting(cmdr, is_beta, entry)
|
||||||
|
|
||||||
elif entry['event'] == 'Shipyard':
|
elif entry['event'] == 'Shipyard':
|
||||||
this.eddn.export_journal_shipyard(cmdr, is_beta, entry)
|
this.eddn.export_journal_shipyard(cmdr, is_beta, entry)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
if __debug__: print_exc()
|
logger.debug(f'Failed exporting {entry["event"]}', exc_info=e)
|
||||||
return _("Error: Can't connect to EDDN")
|
return _("Error: Can't connect to EDDN")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if __debug__: print_exc()
|
logger.debug(f'Failed exporting {entry["event"]}', exc_info=e)
|
||||||
return str(e)
|
return str(e)
|
||||||
|
|
||||||
def cmdr_data(data, is_beta):
|
|
||||||
|
def cmdr_data(data: Mapping[str, Any], is_beta: bool) -> str:
|
||||||
if data['commander'].get('docked') and config.getint('output') & config.OUT_MKT_EDDN:
|
if data['commander'].get('docked') and config.getint('output') & config.OUT_MKT_EDDN:
|
||||||
try:
|
try:
|
||||||
if this.marketId != data['lastStarport']['id']:
|
if this.marketId != data['lastStarport']['id']:
|
||||||
@ -484,6 +710,7 @@ def cmdr_data(data, is_beta):
|
|||||||
if not old_status:
|
if not old_status:
|
||||||
status['text'] = _('Sending data to EDDN...')
|
status['text'] = _('Sending data to EDDN...')
|
||||||
status.update_idletasks()
|
status.update_idletasks()
|
||||||
|
|
||||||
this.eddn.export_commodities(data, is_beta)
|
this.eddn.export_commodities(data, is_beta)
|
||||||
this.eddn.export_outfitting(data, is_beta)
|
this.eddn.export_outfitting(data, is_beta)
|
||||||
this.eddn.export_shipyard(data, is_beta)
|
this.eddn.export_shipyard(data, is_beta)
|
||||||
@ -492,9 +719,20 @@ def cmdr_data(data, is_beta):
|
|||||||
status.update_idletasks()
|
status.update_idletasks()
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
if __debug__: print_exc()
|
logger.debug('Failed exporting data', exc_info=e)
|
||||||
return _("Error: Can't connect to EDDN")
|
return _("Error: Can't connect to EDDN")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if __debug__: print_exc()
|
logger.debug('Failed exporting data', exc_info=e)
|
||||||
return str(e)
|
return str(e)
|
||||||
|
|
||||||
|
|
||||||
|
MAP_STR_ANY = Mapping[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def is_horizons(economies: MAP_STR_ANY, modules: MAP_STR_ANY, ships: MAP_STR_ANY) -> bool:
|
||||||
|
return (
|
||||||
|
any(economy['name'] == 'Colony' for economy in economies.values()) or
|
||||||
|
any(module.get('sku') == HORIZ_SKU for module in modules.values()) or
|
||||||
|
any(ship.get('sku') == HORIZ_SKU for ship in (ships['shipyard_list'] or {}).values())
|
||||||
|
)
|
||||||
|
@ -16,16 +16,16 @@ import requests
|
|||||||
import sys
|
import sys
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
import logging
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from ttkHyperlinkLabel import HyperlinkLabel
|
from ttkHyperlinkLabel import HyperlinkLabel
|
||||||
import myNotebook as nb
|
import myNotebook as nb # noqa: N813
|
||||||
|
|
||||||
from config import appname, applongname, appversion, config
|
from config import appname, applongname, appversion, config
|
||||||
import plug
|
import plug
|
||||||
|
|
||||||
if __debug__:
|
logger = logging.getLogger(appname)
|
||||||
from traceback import print_exc
|
|
||||||
|
|
||||||
EDSM_POLL = 0.1
|
EDSM_POLL = 0.1
|
||||||
_TIMEOUT = 20
|
_TIMEOUT = 20
|
||||||
@ -371,23 +371,28 @@ def worker():
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
reply = r.json()
|
reply = r.json()
|
||||||
(msgnum, msg) = reply['msgnum'], reply['msg']
|
(msgnum, msg) = reply['msgnum'], reply['msg']
|
||||||
# 1xx = OK, 2xx = fatal error, 3&4xx not generated at top-level, 5xx = error but events saved for later processing
|
# 1xx = OK
|
||||||
|
# 2xx = fatal error
|
||||||
|
# 3&4xx not generated at top-level
|
||||||
|
# 5xx = error but events saved for later processing
|
||||||
if msgnum // 100 == 2:
|
if msgnum // 100 == 2:
|
||||||
print('EDSM\t%s %s\t%s' % (msgnum, msg, json.dumps(pending, separators = (',', ': '))))
|
logger.warning(f'EDSM\t{msgnum} {msg}\t{json.dumps(pending, separators = (",", ": "))}')
|
||||||
plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg))
|
plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg))
|
||||||
else:
|
else:
|
||||||
for e, r in zip(pending, reply['events']):
|
for e, r in zip(pending, reply['events']):
|
||||||
if not closing and e['event'] in ['StartUp', 'Location', 'FSDJump', 'CarrierJump']:
|
if not closing and e['event'] in ['StartUp', 'Location', 'FSDJump', 'CarrierJump']:
|
||||||
# Update main window's system status
|
# Update main window's system status
|
||||||
this.lastlookup = r
|
this.lastlookup = r
|
||||||
this.system_link.event_generate('<<EDSMStatus>>', when="tail") # calls update_status in main thread
|
# calls update_status in main thread
|
||||||
|
this.system_link.event_generate('<<EDSMStatus>>', when="tail")
|
||||||
elif r['msgnum'] // 100 != 1:
|
elif r['msgnum'] // 100 != 1:
|
||||||
print('EDSM\t%s %s\t%s' % (r['msgnum'], r['msg'], json.dumps(e, separators = (',', ': '))))
|
logger.warning(f'EDSM\t{r["msgnum"]} {r["msg"]}\t'
|
||||||
|
f'{json.dumps(e, separators = (",", ": "))}')
|
||||||
pending = []
|
pending = []
|
||||||
|
|
||||||
break
|
break
|
||||||
except:
|
except Exception as e:
|
||||||
if __debug__: print_exc()
|
logger.debug('Sending API events', exc_info=e)
|
||||||
retrying += 1
|
retrying += 1
|
||||||
else:
|
else:
|
||||||
plug.show_error(_("Error: Can't connect to EDSM"))
|
plug.show_error(_("Error: Can't connect to EDSM"))
|
||||||
|
1109
plugins/inara.py
1109
plugins/inara.py
File diff suppressed because it is too large
Load Diff
6
pyproject.toml
Normal file
6
pyproject.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[tool.autopep8]
|
||||||
|
max_line_length = 120
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
multi_line_output = 5
|
||||||
|
line_length = 119
|
@ -6,6 +6,7 @@ flake8-comprehensions==3.2.3
|
|||||||
flake8-pep3101==1.3.0
|
flake8-pep3101==1.3.0
|
||||||
flake8-polyfill==1.0.2
|
flake8-polyfill==1.0.2
|
||||||
flake8-json
|
flake8-json
|
||||||
|
flake8-isort==3.0.1
|
||||||
pep8-naming==0.11.1
|
pep8-naming==0.11.1
|
||||||
|
|
||||||
# Code formatting tools
|
# Code formatting tools
|
||||||
|
40
timeout_session.py
Normal file
40
timeout_session.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
|
||||||
|
REQUEST_TIMEOUT = 10 # reasonable timeout that all HTTP requests should use
|
||||||
|
|
||||||
|
|
||||||
|
class TimeoutAdapter(HTTPAdapter):
|
||||||
|
"""
|
||||||
|
TimeoutAdapter is an HTTP Adapter that enforces an overridable default timeout on HTTP requests.
|
||||||
|
"""
|
||||||
|
def __init__(self, timeout, *args, **kwargs):
|
||||||
|
self.default_timeout = timeout
|
||||||
|
if kwargs.get("timeout") is not None:
|
||||||
|
del kwargs["timeout"]
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def send(self, *args, **kwargs):
|
||||||
|
if kwargs["timeout"] is None:
|
||||||
|
kwargs["timeout"] = self.default_timeout
|
||||||
|
|
||||||
|
return super().send(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def new_session(timeout: int = REQUEST_TIMEOUT, session: requests.Session = None) -> requests.Session:
|
||||||
|
"""
|
||||||
|
new_session creates a new requests.Session and overrides the default HTTPAdapter with a TimeoutAdapter.
|
||||||
|
|
||||||
|
:param timeout: the timeout to set the TimeoutAdapter to, defaults to REQUEST_TIMEOUT
|
||||||
|
:param session: the Session object to attach the Adapter to, defaults to a new session
|
||||||
|
:return: The created Session
|
||||||
|
"""
|
||||||
|
if session is None:
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
adapter = TimeoutAdapter(timeout)
|
||||||
|
session.mount("http://", adapter)
|
||||||
|
session.mount("https://", adapter)
|
||||||
|
return session
|
Loading…
x
Reference in New Issue
Block a user