diff --git a/.mypy.ini b/.mypy.ini index f9ec5997..38afbb67 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -2,4 +2,8 @@ follow_imports = skip ignore_missing_imports = True scripts_are_modules = True +; Without this bare `mypy ` may get warnings for e.g. +; ` = ` +; i.e. no typing info. +check-untyped-defs = True ; platform = darwin diff --git a/EDMarketConnector.py b/EDMarketConnector.py index ced2ed33..21531679 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -16,7 +16,7 @@ from builtins import object, str from os import chdir, environ from os.path import dirname, join from time import localtime, strftime, time -from typing import TYPE_CHECKING, Optional, Tuple, Union +from typing import TYPE_CHECKING, Literal, Optional, Tuple, Union # Have this as early as possible for people running EDMarketConnector.exe # from cmd.exe or a bat file or similar. Else they might not be in the correct @@ -1013,11 +1013,6 @@ class AppWindow(object): self.status['text'] = _('CAPI query aborted: GameVersion unknown') return - if not monitor.is_live_galaxy(): - logger.warning("Dropping CAPI request because this is the Legacy galaxy, which is not yet supported") - self.status['text'] = 'CAPI for Legacy not yet supported' - return - if not monitor.system: logger.trace_if('capi.worker', 'Aborting Query: Current star system unknown') # LANG: CAPI queries aborted because current star system name unknown @@ -1210,8 +1205,7 @@ class AppWindow(object): monitor.state['Loan'] = capi_response.capi_data['commander'].get('debt', 0) # stuff we can do when not docked - # TODO: Use plug.notify_capi_legacy if Legacy host - err = plug.notify_newdata(capi_response.capi_data, monitor.is_beta) + err = plug.notify_capidata(capi_response.capi_data, monitor.is_beta) self.status['text'] = err and err or '' if err: play_bad = True @@ -1357,7 +1351,7 @@ class AppWindow(object): # Ensure the ship type/name text is clickable, if it should be. if monitor.state['Modules']: - ship_state = True + ship_state: Literal['normal', 'disabled'] = tk.NORMAL else: ship_state = tk.DISABLED diff --git a/PLUGINS.md b/PLUGINS.md index 97f2275a..ee539032 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -98,6 +98,12 @@ liable to change without notice. `from prefs import prefsVersion` - to allow for versioned preferences. +`from companion import CAPIData, SERVER_LIVE, SERVER_LEGACY, SERVER_BETA` - +`CAPIData` is the actual type of `data` as passed into `cmdr_data()` and +`cmdr_data_legacy()`. +See [Commander Data from Frontier CAPI](#commander-data-from-frontier-capi)) +for further information. + `import edmc_data` (or specific 'from' imports) - This contains various static data that used to be in other files. You should **not** now import anything from the original files unless specified as allowed in this section. @@ -129,11 +135,15 @@ modules for plugin use: - sqlite3 - zipfile -And, of course, anything in the [Python Standard Library](https://docs.python.org/3/library/) -will always be available, dependent on the version of Python we're using to -build Windows installed versions. Check the 'Startup' line in an application -[Debug Log File](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#debug-log-files) -for the version of Python being used. +Unfortunately we cannot promise to include every part of the +[Python Standard Library](https://docs.python.org/3/library/) due to issues +with correctly detecting all the modules, and if they're single file or a +package, and perhaps have sub-modules. For now, if you find something is +missing that you need for your plugin, ask us to add it in, and we'll do so on +a 'best efforts' basis. + +See [#1327 - ModuleNotFound when creating a new plugin.](https://github.com/EDCD/EDMarketConnector/issues/1327) +for some discussion. --- @@ -899,8 +909,14 @@ constants. --- ### Commander Data from Frontier CAPI +If a plugin has a `cmdr_data()` function it gets called when the application +has just fetched fresh Cmdr and station data from Frontier's servers, **but not +for the Legacy galaxy**. See `cmdr_data_legacy()` below for Legacy data +handling. ```python +from companion import CAPIData, SERVER_LIVE, SERVER_LEGACY, SERVER_BETA + def cmdr_data(data, is_beta): """ We have new data on our commander @@ -909,18 +925,58 @@ def cmdr_data(data, is_beta): raise ValueError("this isn't possible") logger.info(data['commander']['name']) + + # Determining source galaxy for the data + if data.source_host == SERVER_LIVE: + ... + + elif data.source_host == SERVER_BETA: + ... + + elif data.source_host == SERVER_LEGACY: + ... +``` + +| Parameter | Type | Description | +| :-------- | :--------------: | :------------------------------------------------------------------------------------------------------- | +| `data` | `CAPIData` | `/profile` API response, with `/market` and `/shipyard` added under the keys `marketdata` and `shipdata` | +| `is_beta` | `bool` | If the game is currently in beta | +`CAPIData` is a class, which you can `from companion import CAPIDATA`, and is +based on `UserDict`. The actual data from CAPI queries is thus accessible +via python's normal `data['key']` syntax. However, being a class, it can also +have extra properties, such as `source_host`, as shown above. Plugin authors +are free to use *that* property, **but MUST NOT rely on any other extra +properties present in `CAPIData`, they are for internal use only.** + + +#### CAPI data for Legacy +When CAPI data has been retrieved from the separate CAPI host for the Legacy +galaxy, because the Journal gameversion indicated the player is playing/last +played in that galaxy, a different function will be called, +`cmdr_data_legacy()`. + +```python +def cmdr_data_legacy(data, is_beta): + """ + We have new data on our commander + """ + if data.get('commander') is None or data['commander'].get('name') is None: + raise ValueError("this isn't possible") + + logger.info(data['commander']['name']) ``` -This gets called when the application has just fetched fresh Cmdr and station -data from Frontier's servers. +**IF AND ONLY IF** your code definitely handles the Live/Legacy split itself +then you *may* simply: -| Parameter | Type | Description | -| :-------- | :--------------: | :------------------------------------------------------------------------------------------------------- | -| `data` | `Dict[str, Any]` | `/profile` API response, with `/market` and `/shipyard` added under the keys `marketdata` and `shipdata` | -| `is_beta` | `bool` | If the game is currently in beta | -NB: Actually `data` is a custom type, based on `UserDict`, called `CAPIData`, -and has some extra properties. However, these are for **internal use only** -at this time, especially as there are some caveats about at least one of them. +```python +def cmdr_data_legacy(data, is_beta): + return cmdr_data(data, is_beta) +``` + +The core 'eddn' plugin might contain some useful hints about how to handle the +split **but do not rely on any extra properties on `data` unless they are +documented in [Available imports](#available-imports) in this document**. --- diff --git a/companion.py b/companion.py index 4aff101a..3aa085ae 100644 --- a/companion.py +++ b/companion.py @@ -44,9 +44,6 @@ else: UserDict = collections.UserDict # type: ignore # Otherwise simply use the actual class -# Define custom type for the dicts that hold CAPI data -# CAPIData = NewType('CAPIData', Dict) - capi_query_cooldown = 60 # Minimum time between (sets of) CAPI queries capi_default_requests_timeout = 10 auth_timeout = 30 # timeout for initial auth @@ -67,8 +64,8 @@ class CAPIData(UserDict): def __init__( self, data: Union[str, Dict[str, Any], 'CAPIData', None] = None, - source_host: str = None, - source_endpoint: str = None + source_host: Optional[str] = None, + source_endpoint: Optional[str] = None ) -> None: if data is None: super().__init__() @@ -652,7 +649,7 @@ class Session(object): self.requests_session.headers['User-Agent'] = user_agent self.state = Session.STATE_OK - def login(self, cmdr: str = None, is_beta: Optional[bool] = None) -> bool: + def login(self, cmdr: Optional[str] = None, is_beta: Optional[bool] = None) -> bool: """ Attempt oAuth2 login. @@ -763,10 +760,6 @@ class Session(object): :return: The resulting CAPI data, of type CAPIData. """ capi_data: CAPIData - if capi_host == SERVER_LEGACY: - logger.warning("Dropping CAPI request because this is the Legacy galaxy") - return capi_data - try: logger.trace_if('capi.worker', 'Sending HTTP request...') if conf_module.capi_pretend_down: @@ -921,7 +914,6 @@ class Session(object): break logger.trace_if('capi.worker', f'Processing query: {query.endpoint}') - capi_data: CAPIData try: if query.endpoint == self._CAPI_PATH_STATION: capi_data = capi_station_queries(query.capi_host) @@ -1080,17 +1072,22 @@ class Session(object): """ if self.credentials is None: # Can't tell if beta or not + logger.warning("Dropping CAPI request because unclear if game beta or not") return '' if self.credentials['beta']: + logger.debug(f"Using {SERVER_BETA} because {self.credentials['beta']=}") return SERVER_BETA if monitor.is_live_galaxy(): + logger.debug(f"Using {SERVER_LIVE} because monitor.is_live_galaxy() was True") return SERVER_LIVE - # return SERVER_LEGACY # Not Yet - logger.warning("Dropping CAPI request because this is the Legacy galaxy, which is not yet supported") - return "" + else: + logger.debug(f"Using {SERVER_LEGACY} because monitor.is_live_galaxy() was False") + return SERVER_LEGACY + + return '' ###################################################################### diff --git a/plug.py b/plug.py index b1c0d3da..deeb6ebe 100644 --- a/plug.py +++ b/plug.py @@ -370,7 +370,7 @@ def notify_dashboard_entry( return error -def notify_newdata( +def notify_capidata( data: companion.CAPIData, is_beta: bool ) -> Optional[str]: @@ -383,13 +383,21 @@ def notify_newdata( """ error = None for plugin in PLUGINS: - cmdr_data = plugin._get_func('cmdr_data') + # TODO: Handle it being Legacy data + if data.source_host == companion.SERVER_LEGACY: + cmdr_data = plugin._get_func('cmdr_data_legacy') + + else: + cmdr_data = plugin._get_func('cmdr_data') + if cmdr_data: try: newerror = cmdr_data(data, is_beta) error = error or newerror + except Exception: logger.exception(f'Plugin "{plugin.name}" failed') + return error diff --git a/plugins/eddn.py b/plugins/eddn.py index 8de9e093..ec13dafe 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -2408,9 +2408,28 @@ def journal_entry( # noqa: C901, CCR001 return None +def cmdr_data_legacy(data: CAPIData, is_beta: bool) -> Optional[str]: + """ + Process new CAPI data for Legacy galaxy. + + Ensuring the correct EDDN `header->gameversion` is achieved by use of + `EDDN.capi_gameversion_from_host_endpoint()` in: + + `EDDN.export_outfitting()` + `EDDN.export_shipyard()` + `EDDN.export_outfitting()` + + Thus we can just call through to the 'not Legacy' version of this function. + :param data: CAPI data to process. + :param is_beta: bool - True if this is a beta version of the Game. + :return: str - Error message, or `None` if no errors. + """ + return cmdr_data(data, is_beta) + + def cmdr_data(data: CAPIData, is_beta: bool) -> Optional[str]: # noqa: CCR001 """ - Process new CAPI data. + Process new CAPI data for not-Legacy galaxy (might be beta). :param data: CAPI data to process. :param is_beta: bool - True if this is a beta version of the Game. diff --git a/td.py b/td.py index cd2c0d3e..00dd1736 100644 --- a/td.py +++ b/td.py @@ -1,6 +1,7 @@ """Export data for Trade Dangerous.""" import pathlib +import sys import time from collections import defaultdict from operator import itemgetter @@ -29,9 +30,9 @@ def export(data: CAPIData) -> None: # codecs can't automatically handle line endings, so encode manually where # required with open(data_path / data_filename, 'wb') as h: - # Format described here: https://bitbucket.org/kfsone/tradedangerous/wiki/Price%20Data + # Format described here: https://github.com/eyeonus/Trade-Dangerous/wiki/Price-Data h.write('#! trade.py import -\n'.encode('utf-8')) - this_platform = 'darwin' and "Mac OS" or system() + this_platform = sys.platform == 'darwin' and "Mac OS" or system() cmdr_name = data['commander']['name'].strip() h.write( f'# Created by {applongname} {appversion()} on {this_platform} for Cmdr {cmdr_name}.\n'.encode('utf-8')