mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-22 20:00:29 +03:00
Merge branch 'release-5.6.0' into releases
This commit is contained in:
commit
7ed08396c6
@ -1,6 +0,0 @@
|
||||
[run]
|
||||
omit =
|
||||
# The tests themselves
|
||||
tests/*
|
||||
# Any venv files
|
||||
venv/*
|
@ -21,7 +21,7 @@ repos:
|
||||
# - id: autopep8
|
||||
|
||||
### # flake8 --show-source <file>
|
||||
### - repo: https://gitlab.com/pycqa/flake8
|
||||
### - repo: https://github.com/PyCQA/flake8
|
||||
### rev: ''
|
||||
### hooks:
|
||||
### - id: flake8
|
||||
|
@ -1 +1 @@
|
||||
3.10.7
|
||||
3.10.8
|
||||
|
132
ChangeLog.md
132
ChangeLog.md
@ -9,7 +9,7 @@ produce the Windows executables and installer.
|
||||
|
||||
---
|
||||
|
||||
* We now test against, and package with, Python 3.10.7.
|
||||
* We now test against, and package with, Python 3.10.8.
|
||||
|
||||
**As a consequence of this we no longer support Windows 7.
|
||||
This is due to
|
||||
@ -27,6 +27,136 @@ produce the Windows executables and installer.
|
||||
|
||||
---
|
||||
|
||||
Release 5.6.0
|
||||
===
|
||||
|
||||
Tha major reason for this release is to address the Live versus Legacy galaxy
|
||||
split [coming in Update 14 of the game](https://www.elitedangerous.com/news/elite-dangerous-update-14-and-beyond-live-and-legacy-modes).
|
||||
See the section "Update 14 and the Galaxy Split" below for how this might
|
||||
impact you.
|
||||
|
||||
Changes
|
||||
---
|
||||
|
||||
* We now test against, and package with, Python 3.10.8.
|
||||
* The code for sending data to EDDN has been reworked. This changes the
|
||||
'replay log' from utilising an internal array, backed by a flat file
|
||||
(`replay.jsonl`), to an sqlite3 database.
|
||||
|
||||
As a result:
|
||||
1. Any messages stored in the old `replay.jsonl` are converted at startup,
|
||||
if that file is present, and then the file removed.
|
||||
2. All new messages are stored in this new sqlite3 queue before any attempt
|
||||
is made to send them. An immediate attempt is then made to send any
|
||||
message not affected by "Delay sending until docked".
|
||||
3. Sending of queued messages will be attempted every 5 minutes, unless
|
||||
"Delay sending until docked" is active and the Cmdr is not docked in
|
||||
their own ship. This is in case a message failed to send due to an issue
|
||||
communicating with the EDDN Gateway.
|
||||
4. When you dock in your own ship an immediate attempt to send all queued
|
||||
messages will be initiated.
|
||||
5. When processing queued messages the same 0.4-second inter-message delay
|
||||
as with the old code has been implemented. This serves to not suddenly
|
||||
flood the EDDN Gateway. If any message fails to send for Gateway reasons,
|
||||
i.e. not a bad message, then this processing is abandoned to wait for
|
||||
the next invocation.
|
||||
|
||||
The 5-minute timer in point 3 differs from the old code, where almost any
|
||||
new message sending attempt could initiate processing of the queue. At
|
||||
application startup this delay is only 10 seconds.
|
||||
|
||||
Currently, the feedback of "Sending data to EDDN..." in the UI status line
|
||||
has been removed.
|
||||
|
||||
**If you do not have "Delay sending until docked" active, then the only
|
||||
messages that will be at all delayed will be where there was a communication
|
||||
problem with the EDDN Gateway, or it otherwise indicated a problem other
|
||||
than 'your message is bad'.**
|
||||
* As a result of this EDDN rework this application now sends appropriate
|
||||
`gameversion` and `gamebuild` strings in EDDN message headers.
|
||||
The rework was necessary in order to enable this, in case of any queued
|
||||
or delayed messages which did not contain this information in the legacy
|
||||
`replay.jsonl` format.
|
||||
* For EDSM there is a very unlikely set of circumstances that could, in theory
|
||||
lead to some events not being sent. This is so as to safeguard against
|
||||
sending a batch with a gameversion/build claimed that does not match for
|
||||
*all* of the events in that batch.
|
||||
|
||||
It would take a combination of "communications with EDSM are slow", more
|
||||
events (the ones that would be lost), a game client crash, *and* starting
|
||||
a new game client before the 'more events' are sent.
|
||||
|
||||
Update 14 and the Galaxy Split
|
||||
---
|
||||
Due to the galaxy split [announced by Frontier](https://www.elitedangerous.com/news/elite-dangerous-update-14-and-beyond-live-and-legacy-modes)
|
||||
there are some changes to the major third-party websites and tools.
|
||||
|
||||
* Inara [has chosen](https://inara.cz/elite/board-thread/7049/463292/#463292)
|
||||
to only accept Live galaxy data on its API.
|
||||
|
||||
This application will not even process Journal data for Inara after
|
||||
2022-11-29T09:00:00+00:00 *unless the `gameversion` indicates a Live client*.
|
||||
This explicitly checks that the game's version is semantically equal to or
|
||||
greater than '4.0.0'.
|
||||
|
||||
If a Live client is *not* detected, then there is an INFO level logging
|
||||
message "Inara only accepts Live galaxy data", which is also set as the main
|
||||
UI status line. This message will repeat, at most, every 5 minutes.
|
||||
|
||||
If you continue to play in the Legacy galaxy only then you probably want to
|
||||
just disable the Inara plugin with the checkbox on Settings > Inara.
|
||||
* All batches of events sent to EDSM will be tagged with a `gameversion`, in
|
||||
a similar manner to the EDDN header.
|
||||
|
||||
Ref: [EDSM api-journal-v1](https://www.edsm.net/en/api-journal-v1)
|
||||
* All EDDN messages will now have appropriate `gameversion` and `gamebuild`
|
||||
fields in the `header` as per
|
||||
[EDDN/docs/Developers.md](https://github.com/EDCD/EDDN/blob/live/docs/Developers.md#gameversions-and-gamebuild).
|
||||
|
||||
As a result of this you can expect third-party sites to choose to filter data
|
||||
based on that.
|
||||
|
||||
Look for announcements by individual sites/tools as to what they have chosen
|
||||
to do.
|
||||
|
||||
Known Bugs
|
||||
---
|
||||
In testing if it had been broken at all due to 5.5.0 -> 5.6.0 changes it has
|
||||
come to light that `EDMC.EXE -n`, to send data to EDDN, was already broken in
|
||||
5.5.0.
|
||||
|
||||
In addition, there is now some extra 'INFO' logging output which will be
|
||||
produced by any invocation of `EDMC.EXE`. This might break third-party use of
|
||||
it, e.g. [Trade Computer Extension Mk.II](https://forums.frontier.co.uk/threads/trade-computer-extension-mk-ii.223056/).
|
||||
This will be fixed as soon as the dust settles from Update 14, with emphasis
|
||||
being on ensuring the GUI `EDMarketConnector.exe` functions properly.
|
||||
|
||||
Notes for EDDN Listeners
|
||||
---
|
||||
* Where EDMC sourced data from the Journal files it will set `gameversion`
|
||||
and `gamebuild` as per their values in `Fileheader` or `LoadGame`, whichever
|
||||
was more recent (there are some events that occur between these).
|
||||
* *If any message was already delayed such that it did not
|
||||
have the EDDN header recorded, then the `gameversion` and `gamebuild` will
|
||||
be empty strings*. In order to indicate this the `softwareName` will have
|
||||
` (legacy replay)` appended to it, e.g. `E:D Market Connector Connector
|
||||
[Windows] (legacy replay)`. In general this indicates that the message was
|
||||
queued up using a version of EDMC prior to this one. If you're only
|
||||
interested in Live galaxy data then you might want to ignore such messages.
|
||||
* Where EDMC sourced data from a CAPI endpoint, the resulting EDDN message
|
||||
will have a `gameversion` of `CAPI-<endpoint>` set, e.g. `CAPI-market`.
|
||||
**At this time it is not 100% certain which galaxy this data will be for, so
|
||||
all listeners are advised to ignore/queue such data until this is clarified**.
|
||||
|
||||
`gamebuild` will be an empty string for all CAPI-sourced data.
|
||||
|
||||
Plugin Developers
|
||||
---
|
||||
* There is a new flag in `state` passed to plugins, `IsDocked`. See PLUGINS.md
|
||||
for details.
|
||||
|
||||
---
|
||||
|
||||
Release 5.5.0
|
||||
===
|
||||
|
||||
|
@ -244,6 +244,46 @@ handy if you want to step through the testing code to be sure of anything.
|
||||
|
||||
Otherwise, see the [pytest documentation](https://docs.pytest.org/en/stable/contents.html).
|
||||
|
||||
### Test Coverage
|
||||
As we work towards actually having tests for as much of the code as possible
|
||||
it is useful to monitor the current test coverage.
|
||||
|
||||
Running `pytest` will also produce the overall coverage report, see the
|
||||
configured options in `pyproject.toml`.
|
||||
|
||||
One issue you might run into is where there is code that only runs on one
|
||||
platform. By default `pytest-cov`/`coverage` will count this code as not
|
||||
tested when run on a different platform. We utilise the
|
||||
`coverage-conditional-plugin` module so that `#pragma` comments can be used
|
||||
to give hints to coverage about this.
|
||||
|
||||
The pragmas are defined in the
|
||||
`tool.coverage.coverage_conditional_plugin.rules` section of `pyproject.toml`,
|
||||
e.g.
|
||||
|
||||
```toml
|
||||
[tool.coverage.coverage_conditional_plugin.rules]
|
||||
sys-platform-win32 = "sys_platform != 'win32'"
|
||||
...
|
||||
```
|
||||
And are used as in:
|
||||
```python
|
||||
import sys
|
||||
|
||||
if sys.platform == 'win32': # pragma: sys-platform-win32
|
||||
...
|
||||
else: # pragma: sys-platform-not-win32
|
||||
...
|
||||
```
|
||||
Note the inverted sense of the pragma definitions, as the comments cause
|
||||
`coverage` to *not* consider that code block on this platform.
|
||||
|
||||
As of 2022-10-02 and `coverage-conditional-plugin==0.7.0` there is no way to
|
||||
signal that an entire file should be excluded from coverage reporting on the
|
||||
current platform. See
|
||||
[this GitHub issue comment](https://github.com/wemake-services/coverage-conditional-plugin/issues/2#issuecomment-1263918296)
|
||||
.
|
||||
|
||||
---
|
||||
|
||||
## Imports used only in core plugins
|
||||
|
@ -922,7 +922,7 @@ class AppWindow(object):
|
||||
return False
|
||||
|
||||
# Ignore possibly missing shipyard info
|
||||
elif (config.get_int('output') & config.OUT_MKT_EDDN) \
|
||||
elif (config.get_int('output') & config.OUT_EDDN_SEND_STATION_DATA) \
|
||||
and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')):
|
||||
if not self.status['text']:
|
||||
# LANG: Status - Either no market or no modules data for station from Frontier CAPI
|
||||
|
@ -46,6 +46,9 @@
|
||||
/* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:225; load.py:225; inara.py:234; */
|
||||
"Inara credentials" = "Inara credentials";
|
||||
|
||||
/* inara.py: The Inara API only accepts Live galaxy data, not Legacy galaxy data; In files: inara.py:383; inara.py:386; */
|
||||
"Inara only accepts Live galaxy data" = "Inara only accepts Live galaxy data";
|
||||
|
||||
/* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1316; inara.py:1328; load.py:1319; load.py:1331; inara.py:1587; inara.py:1600; */
|
||||
"Error: Inara {MSG}" = "Error: Inara {MSG}";
|
||||
|
||||
@ -744,4 +747,3 @@
|
||||
|
||||
/* stats.py: Status dialog title; In files: stats.py:422; */
|
||||
"Ships" = "Ships";
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
/* inara.py: The Inara API only accepts Live galaxy data, not Legacy galaxy data; In files: inara.py:383; inara.py:386; */
|
||||
"Inara only accepts Live galaxy data" = "Inaraは現行の銀河データのみ受け付けます";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "日本語";
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
/* inara.py: The Inara API only accepts Live galaxy data, not Legacy galaxy data; In files: inara.py:383; inara.py:386; */
|
||||
"Inara only accepts Live galaxy data" = "Inara apenas aceita dados da versão Live.";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "Português (Brasil)";
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
/* inara.py: The Inara API only accepts Live galaxy data, not Legacy galaxy data; In files: inara.py:383; inara.py:386; */
|
||||
"Inara only accepts Live galaxy data" = "A API Inara só aceita dados da versão Live";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "Português (Portugal)";
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
/* inara.py: The Inara API only accepts Live galaxy data, not Legacy galaxy data; In files: inara.py:383; inara.py:386; */
|
||||
"Inara only accepts Live galaxy data" = "Inara принимает только данные Live-версии";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "Русский";
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
/* inara.py: The Inara API only accepts Live galaxy data, not Legacy galaxy data; In files: inara.py:383; inara.py:386; */
|
||||
"Inara only accepts Live galaxy data" = "Inara prihvata samo Live galaxy podatke";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "Srpski (Latinica, Bosna i Hercegovina)";
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
/* inara.py: The Inara API only accepts Live galaxy data, not Legacy galaxy data; In files: inara.py:383; inara.py:386; */
|
||||
"Inara only accepts Live galaxy data" = "Inara prihvata samo \"žive\" podatke o galaksiji";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "Srpski (Latinica)";
|
||||
|
||||
|
12
PLUGINS.md
12
PLUGINS.md
@ -617,6 +617,7 @@ Content of `state` (updated to the current journal entry):
|
||||
| `Modules` | `dict` | Currently fitted modules |
|
||||
| `NavRoute` | `dict` | Last plotted multi-hop route |
|
||||
| `ModuleInfo` | `dict` | Last loaded ModulesInfo.json data |
|
||||
| `IsDocked` | `bool` | Whether the Cmdr is currently docked *in their own ship*. |
|
||||
| `OnFoot` | `bool` | Whether the Cmdr is on foot |
|
||||
| `Component` | `dict` | 'Component' MicroResources in Odyssey, `int` count each. |
|
||||
| `Item` | `dict` | 'Item' MicroResources in Odyssey, `int` count each. |
|
||||
@ -710,6 +711,17 @@ NB: It *is* possible, if a player is quick enough, to plot and clear a route
|
||||
before we load it, in which case we'd be retaining the *previous* plotted
|
||||
route.
|
||||
|
||||
New in version 5.6.0:
|
||||
|
||||
`IsDocked` boolean added to `state`. This is set True for a `Location` event
|
||||
having `"Docked":true"`, or the `Docked` event. It is set back to False (its
|
||||
default value) for an `Undocked` event. Being on-foot in a station at login
|
||||
time does *not* count as docked for this.
|
||||
|
||||
In general on-foot, including being in a taxi, might not set this 100%
|
||||
correctly. Its main use in core code is to detect being docked so as to send
|
||||
any stored EDDN messages due to "Delay sending until docked" option.
|
||||
|
||||
___
|
||||
|
||||
##### Synthetic Events
|
||||
|
@ -52,7 +52,7 @@ appcmdname = 'EDMC'
|
||||
# <https://semver.org/#semantic-versioning-specification-semver>
|
||||
# Major.Minor.Patch(-prerelease)(+buildmetadata)
|
||||
# NB: Do *not* import this, use the functions appversion() and appversion_nobuild()
|
||||
_static_appversion = '5.5.0'
|
||||
_static_appversion = '5.6.0'
|
||||
_cached_version: Optional[semantic_version.Version] = None
|
||||
copyright = '© 2015-2019 Jonathan Harris, 2020-2022 EDCD'
|
||||
|
||||
@ -162,7 +162,7 @@ def appversion_nobuild() -> semantic_version.Version:
|
||||
class AbstractConfig(abc.ABC):
|
||||
"""Abstract root class of all platform specific Config implementations."""
|
||||
|
||||
OUT_MKT_EDDN = 1
|
||||
OUT_EDDN_SEND_STATION_DATA = 1
|
||||
# OUT_MKT_BPC = 2 # No longer supported
|
||||
OUT_MKT_TD = 4
|
||||
OUT_MKT_CSV = 8
|
||||
@ -171,12 +171,12 @@ class AbstractConfig(abc.ABC):
|
||||
# OUT_SYS_FILE = 32 # No longer supported
|
||||
# OUT_STAT = 64 # No longer available
|
||||
# OUT_SHIP_CORIOLIS = 128 # Replaced by OUT_SHIP
|
||||
OUT_STATION_ANY = OUT_MKT_EDDN | OUT_MKT_TD | OUT_MKT_CSV
|
||||
# OUT_SYS_EDSM = 256 # Now a plugin
|
||||
# OUT_SYS_AUTO = 512 # Now always automatic
|
||||
OUT_MKT_MANUAL = 1024
|
||||
OUT_SYS_EDDN = 2048
|
||||
OUT_SYS_DELAY = 4096
|
||||
OUT_EDDN_SEND_NON_STATION = 2048
|
||||
OUT_EDDN_DELAY = 4096
|
||||
OUT_STATION_ANY = OUT_EDDN_SEND_STATION_DATA | OUT_MKT_TD | OUT_MKT_CSV
|
||||
|
||||
app_dir_path: pathlib.Path
|
||||
plugin_dir_path: pathlib.Path
|
||||
@ -454,19 +454,19 @@ def get_config(*args, **kwargs) -> AbstractConfig:
|
||||
:param kwargs: Args to be passed through to implementation.
|
||||
:return: Instance of the implementation.
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
if sys.platform == "darwin": # pragma: sys-platform-darwin
|
||||
from .darwin import MacConfig
|
||||
return MacConfig(*args, **kwargs)
|
||||
|
||||
elif sys.platform == "win32":
|
||||
elif sys.platform == "win32": # pragma: sys-platform-win32
|
||||
from .windows import WinConfig
|
||||
return WinConfig(*args, **kwargs)
|
||||
|
||||
elif sys.platform == "linux":
|
||||
elif sys.platform == "linux": # pragma: sys-platform-linux
|
||||
from .linux import LinuxConfig
|
||||
return LinuxConfig(*args, **kwargs)
|
||||
|
||||
else:
|
||||
else: # pragma: sys-platform-not-known
|
||||
raise ValueError(f'Unknown platform: {sys.platform=}')
|
||||
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
"""Darwin/macOS implementation of AbstractConfig."""
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import Any, Dict, List, Union
|
||||
|
@ -94,7 +94,7 @@ class JournalLock:
|
||||
|
||||
:return: LockResult - See the class Enum definition
|
||||
"""
|
||||
if sys.platform == 'win32':
|
||||
if sys.platform == 'win32': # pragma: sys-platform-win32
|
||||
logger.trace_if('journal-lock', 'win32, using msvcrt')
|
||||
# win32 doesn't have fcntl, so we have to use msvcrt
|
||||
import msvcrt
|
||||
@ -107,7 +107,7 @@ class JournalLock:
|
||||
f", assuming another process running: {e!r}")
|
||||
return JournalLockResult.ALREADY_LOCKED
|
||||
|
||||
else: # pytest coverage only sees this on !win32
|
||||
else: # pragma: sys-platform-not-win32
|
||||
logger.trace_if('journal-lock', 'NOT win32, using fcntl')
|
||||
try:
|
||||
import fcntl
|
||||
@ -143,7 +143,7 @@ class JournalLock:
|
||||
return True # We weren't locked, and still aren't
|
||||
|
||||
unlocked = False
|
||||
if sys.platform == 'win32':
|
||||
if sys.platform == 'win32': # pragma: sys-platform-win32
|
||||
logger.trace_if('journal-lock', 'win32, using msvcrt')
|
||||
# win32 doesn't have fcntl, so we have to use msvcrt
|
||||
import msvcrt
|
||||
@ -160,7 +160,7 @@ class JournalLock:
|
||||
else:
|
||||
unlocked = True
|
||||
|
||||
else: # pytest coverage only sees this on !win32
|
||||
else: # pragma: sys-platform-not-win32
|
||||
logger.trace_if('journal-lock', 'NOT win32, using fcntl')
|
||||
try:
|
||||
import fcntl
|
||||
|
48
monitor.py
48
monitor.py
@ -21,6 +21,8 @@ from typing import Tuple
|
||||
if TYPE_CHECKING:
|
||||
import tkinter
|
||||
|
||||
import semantic_version
|
||||
|
||||
import util_ships
|
||||
from config import config
|
||||
from edmc_data import edmc_suit_shortnames, edmc_suit_symbol_localised
|
||||
@ -111,6 +113,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
|
||||
# Context for journal handling
|
||||
self.version: Optional[str] = None
|
||||
self.version_semantic: Optional[semantic_version.Version] = None
|
||||
self.is_beta = False
|
||||
self.mode: Optional[str] = None
|
||||
self.group: Optional[str] = None
|
||||
@ -131,6 +134,11 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self._fcmaterials_retries_remaining = 0
|
||||
self._last_fcmaterials_journal_timestamp: Optional[float] = None
|
||||
|
||||
# For determining Live versus Legacy galaxy.
|
||||
# The assumption is gameversion will parse via `coerce()` and always
|
||||
# be >= for Live, and < for Legacy.
|
||||
self.live_galaxy_base_version = semantic_version.Version('4.0.0')
|
||||
|
||||
self.__init_state()
|
||||
|
||||
def __init_state(self) -> None:
|
||||
@ -166,6 +174,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
'Modules': None,
|
||||
'CargoJSON': None, # The raw data from the last time cargo.json was read
|
||||
'Route': None, # Last plotted route from Route.json file
|
||||
'IsDocked': False, # Whether we think cmdr is docked
|
||||
'OnFoot': False, # Whether we think you're on-foot
|
||||
'Component': defaultdict(int), # Odyssey Components in Ship Locker
|
||||
'Item': defaultdict(int), # Odyssey Items in Ship Locker
|
||||
@ -293,6 +302,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
|
||||
self.currentdir = None
|
||||
self.version = None
|
||||
self.version_semantic = None
|
||||
self.mode = None
|
||||
self.group = None
|
||||
self.cmdr = None
|
||||
@ -306,6 +316,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self.systemaddress = None
|
||||
self.is_beta = False
|
||||
self.state['OnFoot'] = False
|
||||
self.state['IsDocked'] = False
|
||||
self.state['Body'] = None
|
||||
self.state['BodyType'] = None
|
||||
|
||||
@ -725,6 +736,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self.station_marketid = None
|
||||
self.stationtype = None
|
||||
self.stationservices = None
|
||||
self.state['IsDocked'] = False
|
||||
|
||||
elif event_type == 'embark':
|
||||
# This event is logged when a player (on foot) gets into a ship or SRV
|
||||
@ -791,6 +803,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self.state['Dropship'] = False
|
||||
|
||||
elif event_type == 'docked':
|
||||
self.state['IsDocked'] = True
|
||||
self.station = entry.get('StationName') # May be None
|
||||
self.station_marketid = entry.get('MarketID') # May be None
|
||||
self.stationtype = entry.get('StationType') # May be None
|
||||
@ -813,6 +826,8 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
|
||||
if event_type == 'location':
|
||||
logger.trace_if('journal.locations', '"Location" event')
|
||||
if entry.get('Docked'):
|
||||
self.state['IsDocked'] = True
|
||||
|
||||
elif event_type == 'fsdjump':
|
||||
self.planet = None
|
||||
@ -1677,6 +1692,20 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self.state['GameVersion'] = entry['gameversion']
|
||||
self.state['GameBuild'] = entry['build']
|
||||
self.version = self.state['GameVersion']
|
||||
|
||||
try:
|
||||
self.version_semantic = semantic_version.Version.coerce(self.state['GameVersion'])
|
||||
|
||||
except Exception:
|
||||
# Catching all Exceptions as this is *one* call, and we won't
|
||||
# get caught out by any semantic_version changes.
|
||||
self.version_semantic = None
|
||||
logger.error(f"Couldn't coerce {self.state['GameVersion']=}")
|
||||
pass
|
||||
|
||||
else:
|
||||
logger.info(f"Parsed {self.state['GameVersion']=} into {self.version_semantic=}")
|
||||
|
||||
self.is_beta = any(v in self.version.lower() for v in ('alpha', 'beta')) # type: ignore
|
||||
except KeyError:
|
||||
if not suppress:
|
||||
@ -2348,6 +2377,25 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self._last_fcmaterials_journal_timestamp = None
|
||||
return file
|
||||
|
||||
def is_live_galaxy(self) -> bool:
|
||||
"""
|
||||
Indicate if current tracking indicates Live galaxy.
|
||||
|
||||
We assume:
|
||||
1) `gameversion` remains something that semantic_verison.Version.coerce() can parse.
|
||||
2) Any Live galaxy client reports a version >= the defined base version.
|
||||
3) Any Legacy client will always report a version < that base version.
|
||||
:return: True for Live, False for Legacy or unknown.
|
||||
"""
|
||||
# If we don't yet know the version we can't tell, so assume the worst
|
||||
if self.version_semantic is None:
|
||||
return False
|
||||
|
||||
if self.version_semantic >= self.live_galaxy_base_version:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# singleton
|
||||
monitor = EDLogs()
|
||||
|
773
plugins/eddn.py
773
plugins/eddn.py
File diff suppressed because it is too large
Load Diff
@ -71,6 +71,9 @@ class This:
|
||||
def __init__(self):
|
||||
self.shutting_down = False # Plugin is shutting down.
|
||||
|
||||
self.game_version = ""
|
||||
self.game_build = ""
|
||||
|
||||
self.session: requests.Session = requests.Session()
|
||||
self.session.headers['User-Agent'] = user_agent
|
||||
self.queue: Queue = Queue() # Items to be sent to EDSM by worker thread
|
||||
@ -432,6 +435,9 @@ def journal_entry( # noqa: C901, CCR001
|
||||
if should_return:
|
||||
return
|
||||
|
||||
this.game_version = state['GameVersion']
|
||||
this.game_build = state['GameBuild']
|
||||
|
||||
entry = new_entry
|
||||
|
||||
this.on_foot = state['OnFoot']
|
||||
@ -546,7 +552,7 @@ entry: {entry!r}'''
|
||||
materials.update(transient)
|
||||
logger.trace_if(CMDR_EVENTS, f'"LoadGame" event, queueing Materials: {cmdr=}')
|
||||
|
||||
this.queue.put((cmdr, materials))
|
||||
this.queue.put((cmdr, this.game_version, this.game_build, materials))
|
||||
|
||||
if entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'):
|
||||
logger.trace_if(
|
||||
@ -555,7 +561,7 @@ Queueing: {entry!r}'''
|
||||
)
|
||||
logger.trace_if(CMDR_EVENTS, f'"{entry["event"]=}" event, queueing: {cmdr=}')
|
||||
|
||||
this.queue.put((cmdr, entry))
|
||||
this.queue.put((cmdr, this.game_version, this.game_build, entry))
|
||||
|
||||
|
||||
# Update system data
|
||||
@ -638,6 +644,8 @@ def worker() -> None: # noqa: CCR001 C901 # Cant be broken up currently
|
||||
pending: List[Mapping[str, Any]] = [] # Unsent events
|
||||
closing = False
|
||||
cmdr: str = ""
|
||||
last_game_version = ""
|
||||
last_game_build = ""
|
||||
entry: Mapping[str, Any] = {}
|
||||
|
||||
while not this.discarded_events:
|
||||
@ -657,10 +665,10 @@ def worker() -> None: # noqa: CCR001 C901 # Cant be broken up currently
|
||||
logger.debug(f'{this.shutting_down=}, so setting closing = True')
|
||||
closing = True
|
||||
|
||||
item: Optional[Tuple[str, Mapping[str, Any]]] = this.queue.get()
|
||||
item: Optional[Tuple[str, str, str, Mapping[str, Any]]] = this.queue.get()
|
||||
if item:
|
||||
(cmdr, entry) = item
|
||||
logger.trace_if(CMDR_EVENTS, f'De-queued ({cmdr=}, {entry["event"]=})')
|
||||
(cmdr, game_version, game_build, entry) = item
|
||||
logger.trace_if(CMDR_EVENTS, f'De-queued ({cmdr=}, {game_version=}, {game_build=}, {entry["event"]=})')
|
||||
|
||||
else:
|
||||
logger.debug('Empty queue message, setting closing = True')
|
||||
@ -686,6 +694,20 @@ def worker() -> None: # noqa: CCR001 C901 # Cant be broken up currently
|
||||
logger.trace_if(
|
||||
CMDR_EVENTS, f'({cmdr=}, {entry["event"]=}): not in discarded_events, appending to pending')
|
||||
|
||||
# Discard the pending list if it's a new Journal file OR
|
||||
# if the gameversion has changed. We claim a single
|
||||
# gameversion for an entire batch of events so can't mix
|
||||
# them.
|
||||
# The specific gameversion check caters for scenarios where
|
||||
# we took some time in the last POST, had new events queued
|
||||
# in the meantime *and* the game client crashed *and* was
|
||||
# changed to a different gameversion.
|
||||
if (
|
||||
entry['event'].lower() == 'fileheader'
|
||||
or last_game_version != game_version or last_game_build != game_build
|
||||
):
|
||||
pending = []
|
||||
|
||||
pending.append(entry)
|
||||
|
||||
# drop events if required by killswitch
|
||||
@ -726,6 +748,8 @@ def worker() -> None: # noqa: CCR001 C901 # Cant be broken up currently
|
||||
'apiKey': apikey,
|
||||
'fromSoftware': applongname,
|
||||
'fromSoftwareVersion': str(appversion()),
|
||||
'fromGameVersion': game_version,
|
||||
'fromGameBuild': game_build,
|
||||
'message': json.dumps(pending, ensure_ascii=False).encode('utf-8'),
|
||||
}
|
||||
|
||||
@ -807,7 +831,7 @@ def worker() -> None: # noqa: CCR001 C901 # Cant be broken up currently
|
||||
plug.show_error(_("Error: Can't connect to EDSM"))
|
||||
|
||||
if entry['event'].lower() in ('shutdown', 'commander', 'fileheader'):
|
||||
# Game shutdown or new login so we MUST not hang on to pending
|
||||
# Game shutdown or new login, so we MUST not hang on to pending
|
||||
pending = []
|
||||
logger.trace_if(CMDR_EVENTS, f'Blanked pending because of event: {entry["event"]}')
|
||||
|
||||
@ -815,6 +839,9 @@ def worker() -> None: # noqa: CCR001 C901 # Cant be broken up currently
|
||||
logger.debug('closing, so returning.')
|
||||
return
|
||||
|
||||
last_game_version = game_version
|
||||
last_game_build = game_build
|
||||
|
||||
logger.debug('Done.')
|
||||
|
||||
|
||||
|
@ -28,6 +28,7 @@ import time
|
||||
import tkinter as tk
|
||||
from collections import OrderedDict, defaultdict, deque
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from operator import itemgetter
|
||||
from threading import Lock, Thread
|
||||
from typing import TYPE_CHECKING, Any, Callable, Deque, Dict, List, Mapping, NamedTuple, Optional
|
||||
@ -44,6 +45,7 @@ import timeout_session
|
||||
from companion import CAPIData
|
||||
from config import applongname, appversion, config, debug_senders
|
||||
from EDMCLogging import get_main_logger
|
||||
from monitor import monitor
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
|
||||
logger = get_main_logger()
|
||||
@ -88,6 +90,11 @@ class This:
|
||||
def __init__(self):
|
||||
self.session = timeout_session.new_session()
|
||||
self.thread: Thread
|
||||
self.parent: tk.Tk
|
||||
|
||||
# Handle only sending Live galaxy data
|
||||
self.legacy_galaxy_last_notified: Optional[datetime] = None
|
||||
|
||||
self.lastlocation = None # eventData from the last Commander's Flight Log event
|
||||
self.lastship = None # eventData from the last addCommanderShip or setCommanderShip event
|
||||
|
||||
@ -210,6 +217,7 @@ def plugin_start3(plugin_dir: str) -> str:
|
||||
|
||||
def plugin_app(parent: tk.Tk) -> None:
|
||||
"""Plugin UI setup Hook."""
|
||||
this.parent = parent
|
||||
this.system_link = parent.children['system'] # system label in main window
|
||||
this.station_link = parent.children['station'] # station label in main window
|
||||
this.system_link.bind_all('<<InaraLocation>>', update_location)
|
||||
@ -361,6 +369,24 @@ def journal_entry( # noqa: C901, CCR001
|
||||
|
||||
:return: str - empty if no error, else error string.
|
||||
"""
|
||||
if not monitor.is_live_galaxy():
|
||||
# This only applies after Update 14, which as of 2022-11-27 is scheduled
|
||||
# for 2022-11-29, with the game servers presumably being down around
|
||||
# 09:00
|
||||
if datetime.now(timezone.utc) >= datetime.fromisoformat("2022-11-27T09:00:00+00:00"):
|
||||
# Update 14 ETA has passed, so perform the check
|
||||
if (
|
||||
this.legacy_galaxy_last_notified is None
|
||||
or (datetime.now(timezone.utc) - this.legacy_galaxy_last_notified) > timedelta(seconds=300)
|
||||
):
|
||||
# LANG: The Inara API only accepts Live galaxy data, not Legacy galaxy data
|
||||
logger.info(_("Inara only accepts Live galaxy data"))
|
||||
# this.parent.children['status']['text'] =
|
||||
this.legacy_galaxy_last_notified = datetime.now(timezone.utc)
|
||||
return _("Inara only accepts Live galaxy data")
|
||||
|
||||
return ''
|
||||
|
||||
should_return, new_entry = killswitch.check_killswitch('plugins.inara.journal', entry, logger)
|
||||
if should_return:
|
||||
plug.show_error(_('Inara disabled. See Log.')) # LANG: INARA support disabled via killswitch
|
||||
|
4
prefs.py
4
prefs.py
@ -1221,7 +1221,9 @@ class PreferencesDialog(tk.Toplevel):
|
||||
(self.out_csv.get() and config.OUT_MKT_CSV) +
|
||||
(config.OUT_MKT_MANUAL if not self.out_auto.get() else 0) +
|
||||
(self.out_ship.get() and config.OUT_SHIP) +
|
||||
(config.get_int('output') & (config.OUT_MKT_EDDN | config.OUT_SYS_EDDN | config.OUT_SYS_DELAY))
|
||||
(config.get_int('output') & (
|
||||
config.OUT_EDDN_SEND_STATION_DATA | config.OUT_EDDN_SEND_NON_STATION | config.OUT_EDDN_DELAY
|
||||
))
|
||||
)
|
||||
|
||||
config.set(
|
||||
|
@ -7,9 +7,25 @@ line_length = 119
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"] # Search for tests in tests/
|
||||
addopts = "--cov . --cov plugins --cov-report=term-missing --no-cov-on-fail"
|
||||
# --cov-fail-under 80"
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = ["venv/*"] # when running pytest --cov, dont report coverage in venv directories
|
||||
omit = [ "tests/*", "venv/*", "dist.win32/*" ]
|
||||
plugins = [ "coverage_conditional_plugin" ]
|
||||
|
||||
[tool.coverage.coverage_conditional_plugin.rules]
|
||||
# NB: The name versus content of all of these are inverted because of the way
|
||||
# they're used. When a pragma cites one it causes that code block to
|
||||
# **NOT** be considered for code coverage.
|
||||
# See Contributing.md#test-coverage for more details.
|
||||
sys-platform-win32 = "sys_platform != 'win32'"
|
||||
sys-platform-not-win32 = "sys_platform == 'win32'"
|
||||
sys-platform-darwin = "sys_platform != 'darwin'"
|
||||
sys-platform-not-darwin = "sys_platform == 'darwin'"
|
||||
sys-platform-linux = "sys_platform != 'linux'"
|
||||
sys-platform-not-linux = "sys_platform == 'linux'"
|
||||
sys-platform-not-known = "sys_platform in ('darwin', 'linux', 'win32')"
|
||||
|
||||
[tool.pyright]
|
||||
# pythonPlatform = 'Darwin'
|
||||
|
@ -5,28 +5,28 @@ wheel
|
||||
# We can't rely on just picking this up from either the base (not venv),
|
||||
# or venv-init-time version. Specify here so that dependabot will prod us
|
||||
# about new versions.
|
||||
setuptools==65.3.0
|
||||
setuptools==65.6.0
|
||||
|
||||
# Static analysis tools
|
||||
flake8==5.0.4
|
||||
flake8-annotations-coverage==0.0.6
|
||||
flake8-cognitive-complexity==0.1.0
|
||||
flake8-comprehensions==3.10.0
|
||||
flake8-comprehensions==3.10.1
|
||||
flake8-docstrings==1.6.0
|
||||
isort==5.10.1
|
||||
flake8-isort==4.2.0
|
||||
flake8-isort==5.0.3
|
||||
flake8-json==21.7.0
|
||||
flake8-noqa==1.2.9
|
||||
flake8-polyfill==1.0.2
|
||||
flake8-use-fstring==1.4
|
||||
|
||||
mypy==0.971
|
||||
mypy==0.991
|
||||
pep8-naming==0.13.2
|
||||
safety==2.2.0
|
||||
types-requests==2.28.11
|
||||
safety==2.3.1
|
||||
types-requests==2.28.11.5
|
||||
|
||||
# Code formatting tools
|
||||
autopep8==1.7.0
|
||||
autopep8==2.0.0
|
||||
|
||||
# HTML changelogs
|
||||
grip==4.6.1
|
||||
@ -37,14 +37,15 @@ lxml==4.9.1
|
||||
# We only need py2exe on windows.
|
||||
# Pre-release version addressing semantic_version 2.9.0+ issues:
|
||||
# <https://github.com/py2exe/py2exe/issues/126>
|
||||
py2exe==0.12.0.1; sys_platform == 'win32'
|
||||
py2exe==0.13.0.0; sys_platform == 'win32'
|
||||
|
||||
# Testing
|
||||
pytest==7.1.3
|
||||
pytest-cov==3.0.0 # Pytest code coverage support
|
||||
coverage[toml]==6.4.4 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
|
||||
pytest==7.2.0
|
||||
pytest-cov==4.0.0 # Pytest code coverage support
|
||||
coverage[toml]==6.5.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
|
||||
coverage-conditional-plugin==0.7.0
|
||||
# For manipulating folder permissions and the like.
|
||||
pywin32==304; sys_platform == 'win32'
|
||||
pywin32==305; sys_platform == 'win32'
|
||||
|
||||
|
||||
# All of the normal requirements
|
||||
|
@ -1,4 +1,4 @@
|
||||
certifi==2022.9.14
|
||||
certifi==2022.9.24
|
||||
requests==2.28.1
|
||||
watchdog==2.1.9
|
||||
# Commented out because this doesn't package well with py2exe
|
||||
|
@ -8,7 +8,6 @@ import pytest
|
||||
# Import as other names else they get picked up when used as fixtures
|
||||
from _pytest import monkeypatch as _pytest_monkeypatch
|
||||
from _pytest import tmpdir as _pytest_tmpdir
|
||||
from py._path.local import LocalPath as py_path_local_LocalPath
|
||||
|
||||
from config import config
|
||||
from journal_lock import JournalLock, JournalLockResult
|
||||
@ -120,7 +119,7 @@ class TestJournalLock:
|
||||
def mock_journaldir(
|
||||
self, monkeypatch: _pytest_monkeypatch,
|
||||
tmp_path_factory: _pytest_tmpdir.TempPathFactory
|
||||
) -> py_path_local_LocalPath:
|
||||
) -> _pytest_tmpdir.TempPathFactory:
|
||||
"""Fixture for mocking config.get_str('journaldir')."""
|
||||
def get_str(key: str, *, default: str = None) -> str:
|
||||
"""Mock config.*Config get_str to provide fake journaldir."""
|
||||
@ -139,7 +138,7 @@ class TestJournalLock:
|
||||
self,
|
||||
monkeypatch: _pytest_monkeypatch,
|
||||
tmp_path_factory: _pytest_tmpdir.TempPathFactory
|
||||
) -> py_path_local_LocalPath:
|
||||
) -> _pytest_tmpdir.TempPathFactory:
|
||||
"""Fixture for mocking config.get_str('journaldir')."""
|
||||
def get_str(key: str, *, default: str = None) -> str:
|
||||
"""Mock config.*Config get_str to provide fake journaldir."""
|
||||
@ -155,7 +154,7 @@ class TestJournalLock:
|
||||
|
||||
###########################################################################
|
||||
# Tests against JournalLock.__init__()
|
||||
def test_journal_lock_init(self, mock_journaldir: py_path_local_LocalPath):
|
||||
def test_journal_lock_init(self, mock_journaldir: _pytest_tmpdir.TempPathFactory):
|
||||
"""Test JournalLock instantiation."""
|
||||
print(f'{type(mock_journaldir)=}')
|
||||
tmpdir = str(mock_journaldir.getbasetemp())
|
||||
@ -177,7 +176,7 @@ class TestJournalLock:
|
||||
jlock.set_path_from_journaldir()
|
||||
assert jlock.journal_dir_path is None
|
||||
|
||||
def test_path_from_journaldir_with_tmpdir(self, mock_journaldir: py_path_local_LocalPath):
|
||||
def test_path_from_journaldir_with_tmpdir(self, mock_journaldir: _pytest_tmpdir.TempPathFactory):
|
||||
"""Test JournalLock.set_path_from_journaldir() with tmpdir."""
|
||||
tmpdir = mock_journaldir
|
||||
|
||||
@ -201,7 +200,7 @@ class TestJournalLock:
|
||||
locked = jlock.obtain_lock()
|
||||
assert locked == JournalLockResult.JOURNALDIR_IS_NONE
|
||||
|
||||
def test_obtain_lock_with_tmpdir(self, mock_journaldir: py_path_local_LocalPath):
|
||||
def test_obtain_lock_with_tmpdir(self, mock_journaldir: _pytest_tmpdir.TempPathFactory):
|
||||
"""Test JournalLock.obtain_lock() with tmpdir."""
|
||||
jlock = JournalLock()
|
||||
|
||||
@ -214,7 +213,7 @@ class TestJournalLock:
|
||||
assert jlock.release_lock()
|
||||
os.unlink(str(jlock.journal_dir_lockfile_name))
|
||||
|
||||
def test_obtain_lock_with_tmpdir_ro(self, mock_journaldir: py_path_local_LocalPath):
|
||||
def test_obtain_lock_with_tmpdir_ro(self, mock_journaldir: _pytest_tmpdir.TempPathFactory):
|
||||
"""Test JournalLock.obtain_lock() with read-only tmpdir."""
|
||||
tmpdir = str(mock_journaldir.getbasetemp())
|
||||
print(f'{tmpdir=}')
|
||||
@ -281,7 +280,7 @@ class TestJournalLock:
|
||||
|
||||
assert locked == JournalLockResult.JOURNALDIR_READONLY
|
||||
|
||||
def test_obtain_lock_already_locked(self, mock_journaldir: py_path_local_LocalPath):
|
||||
def test_obtain_lock_already_locked(self, mock_journaldir: _pytest_tmpdir.TempPathFactory):
|
||||
"""Test JournalLock.obtain_lock() with tmpdir."""
|
||||
continue_q: mp.Queue = mp.Queue()
|
||||
exit_q: mp.Queue = mp.Queue()
|
||||
@ -313,7 +312,7 @@ class TestJournalLock:
|
||||
|
||||
###########################################################################
|
||||
# Tests against JournalLock.release_lock()
|
||||
def test_release_lock(self, mock_journaldir: py_path_local_LocalPath):
|
||||
def test_release_lock(self, mock_journaldir: _pytest_tmpdir.TempPathFactory):
|
||||
"""Test JournalLock.release_lock()."""
|
||||
# First actually obtain the lock, and check it worked
|
||||
jlock = JournalLock()
|
||||
@ -331,12 +330,12 @@ class TestJournalLock:
|
||||
# Cleanup, to avoid side-effect on other tests
|
||||
os.unlink(str(jlock.journal_dir_lockfile_name))
|
||||
|
||||
def test_release_lock_not_locked(self, mock_journaldir: py_path_local_LocalPath):
|
||||
def test_release_lock_not_locked(self, mock_journaldir: _pytest_tmpdir.TempPathFactory):
|
||||
"""Test JournalLock.release_lock() when not locked."""
|
||||
jlock = JournalLock()
|
||||
assert jlock.release_lock()
|
||||
|
||||
def test_release_lock_lie_locked(self, mock_journaldir: py_path_local_LocalPath):
|
||||
def test_release_lock_lie_locked(self, mock_journaldir: _pytest_tmpdir.TempPathFactory):
|
||||
"""Test JournalLock.release_lock() when not locked, but lie we are."""
|
||||
jlock = JournalLock()
|
||||
jlock.locked = True
|
||||
@ -346,7 +345,7 @@ class TestJournalLock:
|
||||
# Tests against JournalLock.update_lock()
|
||||
def test_update_lock(
|
||||
self,
|
||||
mock_journaldir_changing: py_path_local_LocalPath):
|
||||
mock_journaldir_changing: _pytest_tmpdir.TempPathFactory):
|
||||
"""
|
||||
Test JournalLock.update_lock().
|
||||
|
||||
@ -374,7 +373,7 @@ class TestJournalLock:
|
||||
# And the old_journaldir's lockfile too
|
||||
os.unlink(str(old_journaldir_lockfile_name))
|
||||
|
||||
def test_update_lock_same(self, mock_journaldir: py_path_local_LocalPath):
|
||||
def test_update_lock_same(self, mock_journaldir: _pytest_tmpdir.TempPathFactory):
|
||||
"""
|
||||
Test JournalLock.update_lock().
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user