diff --git a/ChangeLog.md b/ChangeLog.md index 240890c3..c73af664 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,76 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are in reverse chronological order (latest first). --- +* We now test against, and package with, Python 3.9.5. + + **As a consequence of this we no longer support Windows 7. + This is due to + [Python 3.9.x itself not supporting Windows 7](https://www.python.org/downloads/windows/). + The application (both EDMarketConnector.exe and EDMC.exe) will crash on + startup due to a missing DLL.** + + This should have no other impact on users or plugin developers, other + than the latter now being free to use features that were introduced since the + Python 3.7 series. + + Developers can check the contents of the `.python-version` file + in the source (it's not distributed with the Windows installer) for the + currently used version in a given branch. + +Release 5.0.2 +=== + +This release is primarily aimed at getting the UI "`Suit: ...`" line working +properly. + +* The "`Suit: ...`" UI line should now function as best it can given the + available data from the game. It should not appear if you have launched + the Horizons version of the game, even if your account has Odyssey + enabled. You might see "``" as the text when this application + does not yet have the required data. + +* Changed the less than obvious "`unable to get endpoint: /profile`" error + message to "`Frontier CAPI query failure: /profile`", and similarly for the + other CAPI endpoints we attempt to access. This new form is potentially + translated, but translators need time to do that. + + In addition the old message "`Received error {r.status_code} from server`" + has been changed to "`Frontier CAPI server error: {r.status_code}`" and is + potentially translated. + +* The filenames used for 'Market data in CSV format file' will now be sane, + and as they were before 5.0.0. + +* Linux: 'Shipyard provider' will no longer default to showing 'False' if + no specific provider has been selected. + +Plugin Developers +--- + +* Extra `Flagse` values added in the live release of Odyssey have been added to + `edmc_data.py`. + +* Odyssey 'BackPack' values should now track better, but might still not be + perfect due to Journal bugs/shortcomings. + +* `state` passed to `journal_entry()` now has a `BackpackJSON` (note the case) + member which is a copy of the data from the `Backpack.json` (yes, that's + currently the correct case) file that is written when there's a `BackPack` + (guess what, yes, that is currently the correct case) event written to + the Journal. + +* `state['Credits']` tracking is almost certainly not perfect. We're + accounting for the credits component of `SuitUpgrade` now, but there + might be other such we've yet accounted for. + +* `state['Suits']` and associated other keys should now be tracking from + Journal events, where possible, as well as CAPI data. + +* There is a section in PLUGINS.md about how to package an extra Python + module with your plugin. Note the new caveat in + [PLUGINS.md:Avoiding-pitfalls](./PLUGINS.md#avoiding-potential-pitfalls) + about the name of your plugin's directory. + Release 5.0.1 === diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 4fad261b..2f94f660 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -620,6 +620,11 @@ class AppWindow(object): def update_suit_text(self) -> None: """Update the suit text for current type and loadout.""" + if not monitor.state['Odyssey']: + # Odyssey not detected, no text should be set so it will hide + self.suit['text'] = '' + return + if (suit := monitor.state.get('SuitCurrent')) is None: self.suit['text'] = f'<{_("Unknown")}>' return @@ -633,6 +638,14 @@ class AppWindow(object): loadout_name = suitloadout['name'] self.suit['text'] = f'{suitname} ({loadout_name})' + def suit_show_if_set(self) -> None: + """Show UI Suit row if we have data, else hide.""" + if self.suit['text'] != '': + self.toggle_suit_row(visible=True) + + else: + self.toggle_suit_row(visible=False) + def toggle_suit_row(self, visible: Optional[bool] = None) -> None: """ Toggle the visibility of the 'Suit' row. @@ -922,10 +935,7 @@ class AppWindow(object): self.suit['text'] = f'{suitname} ({loadout_name})' - self.toggle_suit_row(visible=True) - - else: - self.toggle_suit_row(visible=False) + self.suit_show_if_set() if data['commander'].get('credits') is not None: monitor.state['Credits'] = data['commander']['credits'] @@ -1052,6 +1062,7 @@ class AppWindow(object): self.cmdr['text'] += ' (beta)' self.update_suit_text() + self.suit_show_if_set() self.edit_menu.entryconfigure(0, state=monitor.system and tk.NORMAL or tk.DISABLED) # Copy diff --git a/L10n/cs.strings b/L10n/cs.strings index 1a1fa201..35a85378 100644 --- a/L10n/cs.strings +++ b/L10n/cs.strings @@ -1,6 +1,3 @@ -/* Raised when cannot contact the Companion API server. [companion.py] */ -"Error: Frontier CAPI didn't respond" = "Chyba: Frontier CAPI neodpovídá"; - /* Language name */ "!Language" = "Čeština"; @@ -185,6 +182,9 @@ "Error: EDSM {MSG}" = "Chyba: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "Chyba: Frontier CAPI neodpovídá"; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "Chyba: Server Frontieru nejede"; diff --git a/L10n/de.strings b/L10n/de.strings index e2c15797..cf994243 100644 --- a/L10n/de.strings +++ b/L10n/de.strings @@ -1,6 +1,3 @@ -/* Raised when cannot contact the Companion API server. [companion.py] */ -"Error: Frontier CAPI didn't respond" = "Fehler: Frontier CAPI antwortet nicht"; - /* Language name */ "!Language" = "Deutsch"; @@ -185,6 +182,9 @@ "Error: EDSM {MSG}" = "Fehler: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "Fehler: Frontier CAPI antwortet nicht"; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "Fehler: Frontier-Server ist offline"; diff --git a/L10n/en.template b/L10n/en.template index 11ed447a..0065c1e7 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -230,6 +230,12 @@ /* Section heading in settings. [prefs.py] */ "File location" = "File location"; +/* Failures to access Frontier CAPI endpoints [companion.py] */ +"Frontier CAPI query failure" = "Frontier CAPI query failure"; + +/* Server errors from Frontier CAPI server [companion.py] */ +"Frontier CAPI server error" = "Frontier CAPI server error"; + /* CQC rank. [stats.py] */ "Gladiator" = "Gladiator"; diff --git a/L10n/es.strings b/L10n/es.strings index 9730f672..f0d59d08 100644 --- a/L10n/es.strings +++ b/L10n/es.strings @@ -124,6 +124,15 @@ /* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ "E:D journal file location" = "Localización del archivo de Journal de E:D"; +/* When EDDB functionality has been killswitched. [plugins/eddb.py] */ +"EDDB Journal processing disabled. See Log." = "Procesamiento de Journal para EDDB desactivado. Ver Log."; + +/* When EDDN journal handling has been killswitched [plugins/eddn.py] */ +"EDDN journal handler disabled. See Log." = "Manejo de Journal para EDDN desactivado. Ver Log."; + +/* When EDSM functionality has been killswitched [plugins/edsm.py] */ +"EDSM Handler disabled. See Log." = "Manejo de Journal para EDSM desactivado. Ver Log."; + /* Empire rank. [stats.py] */ "Earl" = "Conde Palatino"; @@ -173,6 +182,9 @@ "Error: EDSM {MSG}" = "Error: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "Error: El CAPI de Frontier no respondió"; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "Error: El servidor de Frontier está caído"; @@ -251,6 +263,9 @@ /* Section heading in settings. [inara.py] */ "Inara credentials" = "Credenciales de Inara"; +/* When Inara support has been killswitched [plugins/inara.py] */ +"Inara disabled. See Log." = "Inara desactivado. Ver Log."; + /* Settings>Plugins>Information on migrating plugins [prefs.py] */ "Information on migrating plugins" = "Información sobre migración de plugins"; @@ -506,6 +521,9 @@ /* Menu item. [EDMarketConnector.py] */ "Status" = "Estado"; +/* 'Suit' label in main UI' [EDMarketConnector.py] */ +"Suit" = "Traje"; + /* Explorer rank. [stats.py] */ "Surveyor" = "Topógrafo"; diff --git a/L10n/ja.strings b/L10n/ja.strings index 88f34fab..4779a177 100644 --- a/L10n/ja.strings +++ b/L10n/ja.strings @@ -124,6 +124,15 @@ /* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ "E:D journal file location" = "E:Dゲームクライアントのジャーナルファイル出力先"; +/* When EDDB functionality has been killswitched. [plugins/eddb.py] */ +"EDDB Journal processing disabled. See Log." = "EDDBジャーナル処理が無効です。ログを確認してください。"; + +/* When EDDN journal handling has been killswitched [plugins/eddn.py] */ +"EDDN journal handler disabled. See Log." = "EDDNジャーナルハンドラーが無効です。ログを確認してください。"; + +/* When EDSM functionality has been killswitched [plugins/edsm.py] */ +"EDSM Handler disabled. See Log." = "EDSMハンドラーが無効です。ログを確認してください。"; + /* Empire rank. [stats.py] */ "Earl" = "Earl"; @@ -173,6 +182,9 @@ "Error: EDSM {MSG}" = "エラー: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "エラー: Frontier CAPIからの応答がありません"; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "エラー: Frontier社のサーバがダウンしています"; @@ -218,6 +230,12 @@ /* Section heading in settings. [prefs.py] */ "File location" = "ファイルの出力先"; +/* Failures to access Frontier CAPI endpoints [companion.py] */ +"Frontier CAPI query failure" = "Frontier CAPIクエリ失敗"; + +/* Server errors from Frontier CAPI server [companion.py] */ +"Frontier CAPI server error" = "Frontier CAPIサーバエラー"; + /* CQC rank. [stats.py] */ "Gladiator" = "Gladiator"; @@ -251,6 +269,9 @@ /* Section heading in settings. [inara.py] */ "Inara credentials" = "Inara認証情報"; +/* When Inara support has been killswitched [plugins/inara.py] */ +"Inara disabled. See Log." = "Inaraが無効です。ログを確認してください。"; + /* Settings>Plugins>Information on migrating plugins [prefs.py] */ "Information on migrating plugins" = "プラグインの移行に関する情報"; diff --git a/L10n/ko.strings b/L10n/ko.strings index a0723345..088231f6 100644 --- a/L10n/ko.strings +++ b/L10n/ko.strings @@ -124,6 +124,15 @@ /* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ "E:D journal file location" = "E:D 저널(journal) 파일 위치"; +/* When EDDB functionality has been killswitched. [plugins/eddb.py] */ +"EDDB Journal processing disabled. See Log." = "EDDB 저널 처리 비활성화됨. 로그 참고 바람."; + +/* When EDDN journal handling has been killswitched [plugins/eddn.py] */ +"EDDN journal handler disabled. See Log." = "EDDN 저널 처리 비활성화됨. 로그 참고 바람."; + +/* When EDSM functionality has been killswitched [plugins/edsm.py] */ +"EDSM Handler disabled. See Log." = "EDSM Handler 비활성화됨. 로그 참고 바람."; + /* Empire rank. [stats.py] */ "Earl" = "Earl"; @@ -173,6 +182,9 @@ "Error: EDSM {MSG}" = "오류: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "오류: Frontier CAPI가 응답이 없음"; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "오류: Frontier 서버 CAPI 연결 불가"; @@ -251,6 +263,9 @@ /* Section heading in settings. [inara.py] */ "Inara credentials" = "Inara 계정 정보"; +/* When Inara support has been killswitched [plugins/inara.py] */ +"Inara disabled. See Log." = "Inara 비활성화됨. 로그 참고 바람."; + /* Settings>Plugins>Information on migrating plugins [prefs.py] */ "Information on migrating plugins" = "플러그인 위치 변경에 대한 정보"; diff --git a/L10n/pl.strings b/L10n/pl.strings index 3958ea31..6e84ce7c 100644 --- a/L10n/pl.strings +++ b/L10n/pl.strings @@ -124,6 +124,15 @@ /* Location of the new Journal file in E:D 2.2. [EDMarketConnector.py] */ "E:D journal file location" = "E:D Lokacja pliku dziennika"; +/* When EDDB functionality has been killswitched. [plugins/eddb.py] */ +"EDDB Journal processing disabled. See Log." = "Wyłączone przetwarzanie dziennika EDDB. Sprawdź logi."; + +/* When EDDN journal handling has been killswitched [plugins/eddn.py] */ +"EDDN journal handler disabled. See Log." = "Wyłączona obsługa dziennika EDDB. Sprawdź logi."; + +/* When EDSM functionality has been killswitched [plugins/edsm.py] */ +"EDSM Handler disabled. See Log." = "Wyłączona obsługa EDDB. Sprawdź logi."; + /* Empire rank. [stats.py] */ "Earl" = "Earl"; @@ -173,6 +182,9 @@ "Error: EDSM {MSG}" = "Błąd: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "Błąd: Brak odpowiedzi od CAPI Frontier"; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "Błąd: Serwer Frontiera nie odpowiada"; @@ -251,6 +263,9 @@ /* Section heading in settings. [inara.py] */ "Inara credentials" = "Inara - uprawnienia"; +/* When Inara support has been killswitched [plugins/inara.py] */ +"Inara disabled. See Log." = "Inara jest wyłączona. Sprawdź logi."; + /* Settings>Plugins>Information on migrating plugins [prefs.py] */ "Information on migrating plugins" = "Informacja o migracji pluginów"; diff --git a/L10n/pt-BR.strings b/L10n/pt-BR.strings index f7df3554..9bc86c83 100644 --- a/L10n/pt-BR.strings +++ b/L10n/pt-BR.strings @@ -1,6 +1,3 @@ -/* Raised when cannot contact the Companion API server. [companion.py] */ -"Error: Frontier CAPI didn't respond" = "Erro: o CAPI da Frontier não respondeu"; - /* Language name */ "!Language" = "Português (Brasil)"; @@ -185,6 +182,9 @@ "Error: EDSM {MSG}" = "Erro: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "Erro: o CAPI da Frontier não respondeu"; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "Erro: O servidor da Frontier está offline"; @@ -230,6 +230,12 @@ /* Section heading in settings. [prefs.py] */ "File location" = "Localização do Arquivo"; +/* Failures to access Frontier CAPI endpoints [companion.py] */ +"Frontier CAPI query failure" = "Falha na requisição ao CAPI da Frontier"; + +/* Server errors from Frontier CAPI server [companion.py] */ +"Frontier CAPI server error" = "Erro de servidor do CAPI da Frontier"; + /* CQC rank. [stats.py] */ "Gladiator" = "Gladiador"; diff --git a/L10n/pt-PT.strings b/L10n/pt-PT.strings index c32eeb84..619eb3bb 100644 --- a/L10n/pt-PT.strings +++ b/L10n/pt-PT.strings @@ -1,6 +1,3 @@ -/* Raised when cannot contact the Companion API server. [companion.py] */ -"Error: Frontier CAPI didn't respond" = "Erro: CAPI da Frontier não respondeu."; - /* Language name */ "!Language" = "Português (Portugal)"; @@ -185,6 +182,9 @@ "Error: EDSM {MSG}" = "Erro: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "Erro: CAPI da Frontier não respondeu."; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "Erro: O servidor da Frontier está offline"; @@ -230,6 +230,12 @@ /* Section heading in settings. [prefs.py] */ "File location" = "Localização do Ficheiro"; +/* Failures to access Frontier CAPI endpoints [companion.py] */ +"Frontier CAPI query failure" = "Falha ao consultar a CAPI da Frontier."; + +/* Server errors from Frontier CAPI server [companion.py] */ +"Frontier CAPI server error" = "Erro do servidor CAPI da Frontier."; + /* CQC rank. [stats.py] */ "Gladiator" = "Gladiador"; diff --git a/L10n/ru.strings b/L10n/ru.strings index 76806d2f..677d518a 100644 --- a/L10n/ru.strings +++ b/L10n/ru.strings @@ -1,6 +1,3 @@ -/* Raised when cannot contact the Companion API server. [companion.py] */ -"Error: Frontier CAPI didn't respond" = "Ошибка: CAPI Frontier не отвечает"; - /* Language name */ "!Language" = "Русский"; @@ -185,6 +182,9 @@ "Error: EDSM {MSG}" = "Ошибка: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "Ошибка: CAPI Frontier не отвечает"; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "Ошибка: сервера Frontier недоступны"; diff --git a/L10n/sr-Latn-BA.strings b/L10n/sr-Latn-BA.strings index c8bb2063..27072cba 100644 --- a/L10n/sr-Latn-BA.strings +++ b/L10n/sr-Latn-BA.strings @@ -1,6 +1,3 @@ -/* Raised when cannot contact the Companion API server. [companion.py] */ -"Error: Frontier CAPI didn't respond" = "Greška: Frontier CAPI nije odgovorio"; - /* Language name */ "!Language" = "Srpski (Latinica, Bosna i Hercegovina)"; @@ -185,6 +182,9 @@ "Error: EDSM {MSG}" = "Greška: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "Greška: Frontier CAPI nije odgovorio"; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "Greška: Frontier server je nedostupan"; diff --git a/L10n/sr-Latn.strings b/L10n/sr-Latn.strings index 702d3dbb..0c51b415 100644 --- a/L10n/sr-Latn.strings +++ b/L10n/sr-Latn.strings @@ -1,6 +1,3 @@ -/* Raised when cannot contact the Companion API server. [companion.py] */ -"Error: Frontier CAPI didn't respond" = "Greška: Frontier CAPI nije odgovorio"; - /* Language name */ "!Language" = "Srpski (Latinica)"; @@ -185,6 +182,9 @@ "Error: EDSM {MSG}" = "Greška: EDSM {MSG}"; +/* Raised when cannot contact the Companion API server. [companion.py] */ +"Error: Frontier CAPI didn't respond" = "Greška: Frontier CAPI nije odgovorio"; + /* OLD: Raised when cannot contact the Companion API server. [companion.py] */ "Error: Frontier server is down" = "Greška: Frontier server je nedostupan"; diff --git a/PLUGINS.md b/PLUGINS.md index fae13d0d..1eb2a4a2 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -270,6 +270,11 @@ There are a number of things that your code should either do or avoiding doing so as to play nicely with the core EDMC code and not risk causing application crashes or hangs. +### Be careful about the name of your plugin directory + +You might want your plugin directory name to be usable in import statements. +See the section on [packaging extra modules](#your-plugin-directory-name-must-be-importable). + ### Use a thread for long-running code By default, your plugin code will be running in the main thread. So, if you @@ -594,6 +599,7 @@ Content of `state` (updated to the current journal entry): | `Consumable` | `dict` | 'Consumable' MicroResources in Odyssey, `int` count each. | | `Data` | `dict` | 'Data' MicroResources in Odyssey, `int` count each. | | `BackPack` | `dict` | `dict` of Odyssey MicroResources in backpack. | +| `BackpackJSON` | `dict` | Content of Backpack.json as of last read. | | `SuitCurrent` | `dict` | CAPI-returned data of currently worn suit. NB: May be `None` if no data. | | `Suits` | `dict`[1] | CAPI-returned data of owned suits. NB: May be `None` if no data. | | `SuitLoadoutCurrent` | `dict` | CAPI-returned data of current Suit Loadout. NB: May be `None` if no data. | @@ -924,12 +930,91 @@ plugin's folder: → Compressed (zipped) folder. - Mac: In Finder right click on your plugin's folder and choose Compress. -If there are any external dependencies then include them in the plugin's -folder. +If there are any external dependencies then +[include them](#packaging-extra-modules) in the plugin's folder. Optionally, for tidiness delete any `.pyc` and `.pyo` files in the archive, as well as the `__pycache__` directory. + +--- + +## Packaging extra modules +EDMarketConnector's Windows installs only package a minimal set of modules. +All of the 'stdlib' of Python is provided, plus any modules the core +application code uses and a small number of additional modules for the +use of plugins. See +[Plugins:Available imports](https://github.com/EDCD/EDMarketConnector/blob/main/PLUGINS.md#available-imports) +for a list. + +As such if your plugin requires additional modules you will need to package +them with your plugin. There is no general Python interpreter in which to +rely on [pip](https://pypi.org/project/pip/) to install them. + +### Environment + +It will be easier of you are using a Python +[virtual environment](https://docs.python.org/3/library/venv.html) for +actually testing the plugin. This is so that you can be sure it is working +*because* you have copied all the correct Python modules inside your +plugin, and not because they are installed within the Python site-packages +in some applicable location (system level or user level). + +So, setup a virtual environment to use when running EDMarketConnector +code to test your plugin, and use the 'system' non-virtual Python to +install modules in order to have somewhere to copy them from. + +NB: If you use PyCharm it's possible to have it do the work of +[creating a virtual environment](https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html) +for your project. + +### Install the modules for the system Python +Technically you could also do this within an additional virtual environment. +If they were in your plugin testing virtual environment then you can't be +sure you have all the necessary files copied into your plugin so it will +work within a vanilla Windows EDMarketConnector install. + +We'll use `xml_dataclasses` for this example. + + pip install xml_dataclasses + +### Copy the module files into your plugin directory + +1. Assuming it's a 'simple' module with no caveats, now we copy: + 1. `pip show xml_dataclasses` - `Location` is where it was installed. + 1. If you have a POSIX-compliant command-line environment: + + cp -pr + + or just use Windows File Explorer, or other GUI means, to copy. + +### Your plugin directory name **must** be importable +You're going to have to refer to your plugin directory in order to import +anything within it. This means it should be compatible with such. + +1. Do **not** use hyphens (`-`) as word separators, or full-stops (`.`). +1. You can use underscore (`_`) as a word separator. + +So: + + - `EDMC-My-Plugin` **BAD**. + - `EDMC.My.Plugin` **BAD**. + - `EDMC_My_Plugin` **GOOD**. + +NB: No, you can't use `from . import xml_dataclasses` because the way +EDMarketConnector:plug.py loads 'found' plugins prevents this from working. + +### Test the module import + +Add an import of this module to your plugin code: + + from EDMC_My_Plugin import xml_dataclasses + +If you're lucky you won't have the "surprise!" of learning your chosen +extra module itself requires other modules. If you are gifted such a surprise +then you will need to repeat the [Copy](#copy-the-module-files-into-your-plugin-directory) +step for the extra module(s) until it works. + --- ## Disable a plugin diff --git a/commodity.py b/commodity.py index a7c39e79..6b167fa1 100644 --- a/commodity.py +++ b/commodity.py @@ -24,9 +24,9 @@ def export(data, kind=COMMODITY_DEFAULT, filename=None) -> None: querytime = config.get_int('querytime', default=int(time.time())) if not filename: - filename_system = data['lastSystem']['name'].strip(), - filename_starport = data['lastStarport']['name'].strip(), - filename_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime)), + filename_system = data['lastSystem']['name'].strip() + filename_starport = data['lastStarport']['name'].strip() + filename_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime)) filename_kind = 'csv' filename = f'{filename_system}.{filename_starport}.{filename_time}.{filename_kind}' filename = join(config.get_str('outdir'), filename) diff --git a/companion.py b/companion.py index ca0a529c..953e5fb4 100644 --- a/companion.py +++ b/companion.py @@ -541,7 +541,7 @@ class Session(object): except Exception as e: logger.debug('Attempting GET', exc_info=e) - raise ServerError(f'unable to get endpoint {endpoint}') from e + raise ServerError(f'{_("Frontier CAPI query failure")}: {endpoint}') from e if r.url.startswith(SERVER_AUTH): logger.info('Redirected back to Auth Server') @@ -556,7 +556,7 @@ class Session(object): # Server error. Typically 500 "Internal Server Error" if server is down logger.debug('500 status back from CAPI') self.dump(r) - raise ServerError(f'Received error {r.status_code} from server') + raise ServerError(f'{_("Frontier CAPI server error")}: {r.status_code}') try: r.raise_for_status() # Typically 403 "Forbidden" on token expiry diff --git a/config.py b/config.py index 3476095d..8c78e64e 100644 --- a/config.py +++ b/config.py @@ -33,7 +33,7 @@ appcmdname = 'EDMC' # # Major.Minor.Patch(-prerelease)(+buildmetadata) # NB: Do *not* import this, use the functions appversion() and appversion_nobuild() -_static_appversion = '5.0.1' +_static_appversion = '5.0.2' copyright = '© 2015-2019 Jonathan Harris, 2020-2021 EDCD' update_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml' @@ -96,9 +96,21 @@ def git_shorthash_from_head() -> str: """ Determine short hash for current git HEAD. + Includes -DIRTY if any changes have been made from HEAD + :return: str - None if we couldn't determine the short hash. """ shorthash: str = None # type: ignore + dirty = False + + with contextlib.suppress(Exception): + result = subprocess.run('git diff --stat HEAD'.split(), capture_output=True) + if len(result.stdout) > 0: + dirty = True + + if len(result.stderr) > 0: + logger.warning(f'Data from git on stderr:\n{str(result.stderr)}') + try: git_cmd = subprocess.Popen('git rev-parse --short HEAD'.split(), stdout=subprocess.PIPE, @@ -115,7 +127,7 @@ def git_shorthash_from_head() -> str: logger.error(f"'{shorthash}' doesn't look like a valid git short hash, forcing to None") shorthash = None # type: ignore - return shorthash + return shorthash + ('-WORKING-DIR-IS-DIRTY' if dirty else '') def appversion() -> semantic_version.Version: diff --git a/edmc_data.py b/edmc_data.py index be1db743..7aea749b 100644 --- a/edmc_data.py +++ b/edmc_data.py @@ -429,6 +429,11 @@ Flags2Cold = 1 << 8 Flags2Hot = 1 << 9 Flags2VeryCold = 1 << 10 Flags2VeryHot = 1 << 11 +Flags2GlideMode = 1 << 12 +Flags2OnFootInHangar = 1 << 13 +Flags2OnFootSocialSpace = 1 << 14 +Flags2OnFootExterior = 1 << 15 +Flags2BreathableAtmosphere = 1 << 16 # Dashboard GuiFocus constants GuiFocusNoFocus = 0 diff --git a/loadout.py b/loadout.py index d3a08743..92c9ff3d 100644 --- a/loadout.py +++ b/loadout.py @@ -32,6 +32,14 @@ def export(data, filename=None): querytime = config.get_int('querytime', default=int(time.time())) # Write + # + # When this is refactored into multi-line CHECK IT WORKS, avoiding the + # brainfart we had with dangling commas in commodity.py:export() !!! + # filename = join(config.get_str('outdir'), '%s.%s.txt' % (ship, time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime)))) + # + # When this is refactored into multi-line CHECK IT WORKS, avoiding the + # brainfart we had with dangling commas in commodity.py:export() !!! + # with open(filename, 'wt') as h: h.write(string) diff --git a/monitor.py b/monitor.py index d496a2ff..eeedfbd6 100644 --- a/monitor.py +++ b/monitor.py @@ -151,6 +151,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below 'Item': defaultdict(int), # BackPack Items 'Data': defaultdict(int), # Backpack Data }, + 'BackpackJSON': None, # Raw JSON from `Backpack.json` file, if available 'SuitCurrent': None, 'Suits': {}, 'SuitLoadoutCurrent': None, @@ -762,20 +763,24 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below self.state[event_type] = payload elif event_type == 'EngineerProgress': - engineers = self.state['Engineers'] - if 'Engineers' in entry: # Startup summary - self.state['Engineers'] = { - e['Engineer']: ((e['Rank'], e.get('RankProgress', 0)) if 'Rank' in e else e['Progress']) - for e in entry['Engineers'] - } + # Sanity check - at least once the 'Engineer' (name) was missing from this in early + # Odyssey 4.0.0.100. Might only have been a server issue causing incomplete data. - else: # Promotion - engineer = entry['Engineer'] - if 'Rank' in entry: - engineers[engineer] = (entry['Rank'], entry.get('RankProgress', 0)) + if self.event_valid_engineerprogress(entry): + engineers = self.state['Engineers'] + if 'Engineers' in entry: # Startup summary + self.state['Engineers'] = { + e['Engineer']: ((e['Rank'], e.get('RankProgress', 0)) if 'Rank' in e else e['Progress']) + for e in entry['Engineers'] + } - else: - engineers[engineer] = entry['Progress'] + else: # Promotion + engineer = entry['Engineer'] + if 'Rank' in entry: + engineers[engineer] = (entry['Rank'], entry.get('RankProgress', 0)) + + else: + engineers[engineer] = entry['Progress'] elif event_type == 'Cargo' and entry.get('Vessel') == 'Ship': self.state['Cargo'] = defaultdict(int) @@ -835,6 +840,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below {self.canonicalise(x['Name']): x['Count'] for x in clean_data} ) + # Journal v31 implies this was removed before Odyssey launch elif event_type == 'BackPackMaterials': # alpha4 - # Lists the contents of the backpack, eg when disembarking from ship @@ -865,6 +871,80 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below {self.canonicalise(x['Name']): x['Count'] for x in clean_data} ) + elif event_type == 'BackPack': + # TODO: v31 doc says this is`backpack.json` ... but Howard Chalkley + # said it's `Backpack.json` + with open(join(self.currentdir, 'Backpack.json'), 'rb') as backpack: # type: ignore + try: + # Preserve property order because why not? + entry = json.load(backpack, object_pairs_hook=OrderedDict) + + except json.JSONDecodeError: + logger.exception('Failed decoding Backpack.json', exc_info=True) + + else: + # Store in monitor.state + self.state['BackpackJSON'] = entry + + # Assume this reflects the current state when written + self.state['BackPack']['Component'] = defaultdict(int) + self.state['BackPack']['Consumable'] = defaultdict(int) + self.state['BackPack']['Item'] = defaultdict(int) + self.state['BackPack']['Data'] = defaultdict(int) + + clean_components = self.coalesce_cargo(entry['Components']) + self.state['BackPack']['Component'].update( + {self.canonicalise(x['Name']): x['Count'] for x in clean_components} + ) + + clean_consumables = self.coalesce_cargo(entry['Consumables']) + self.state['BackPack']['Consumable'].update( + {self.canonicalise(x['Name']): x['Count'] for x in clean_consumables} + ) + + clean_items = self.coalesce_cargo(entry['Items']) + self.state['BackPack']['Item'].update( + {self.canonicalise(x['Name']): x['Count'] for x in clean_items} + ) + + clean_data = self.coalesce_cargo(entry['Data']) + self.state['BackPack']['Data'].update( + {self.canonicalise(x['Name']): x['Count'] for x in clean_data} + ) + + elif event_type == 'BackpackChange': + # Changes to Odyssey Backpack contents *other* than from a Transfer + # See TransferMicroResources event for that. + + if entry.get('Added') is not None: + changes = 'Added' + + elif entry.get('Removed') is not None: + changes = 'Removed' + + else: + logger.warning(f'BackpackChange with neither Added nor Removed: {entry=}') + changes = '' + + if changes != '': + for c in entry[changes]: + category = self.category(c['Type']) + name = self.canonicalise(c['Name']) + + if changes == 'Removed': + self.state['BackPack'][category][name] -= c['Count'] + + elif changes == 'Added': + self.state['BackPack'][category][name] += c['Count'] + + # Paranoia check to see if anything has gone negative. + # As of Odyssey Alpha Phase 1 Hotfix 2 keeping track of BackPack + # materials is impossible when used/picked up anyway. + for c in self.state['BackPack']: + for m in self.state['BackPack'][c]: + if self.state['BackPack'][c][m] < 0: + self.state['BackPack'][c][m] = 0 + elif event_type == 'BuyMicroResources': # Buying from a Pioneer Supplies, goes directly to ShipLocker. # One event per Item, not an array. @@ -948,80 +1028,70 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below self.state['BackPack'][entry['Type']][i] = 0 elif event_type == 'UseConsumable': - # alpha4 - # When using an item from the player’s inventory (backpack) + # TODO: XXX: From v31 doc + # 12.2 BackpackChange + # This is written when there is any change to the contents of the + # suit backpack – note this can be written at the same time as other + # events like UseConsumable + + # In 4.0.0.100 it is observed that: # - # Parameters: - # • Name - # • Type - for c in self.state['BackPack']['Consumable']: - if c == entry['Name']: - self.state['BackPack']['Consumable'][c] -= 1 - # Paranoia in case we lost track - if self.state['BackPack']['Consumable'][c] < 0: - self.state['BackPack']['Consumable'][c] = 0 + # 1. Throw of any grenade type *only* causes a BackpackChange event, no + # accompanying 'UseConsumable'. + # 2. Using an Energy Cell causes both UseConsumable and BackpackChange, + # in that order. + # 3. Medkit acts the same as Energy Cell. + # + # Thus we'll just ignore 'UseConsumable' for now. + # for c in self.state['BackPack']['Consumable']: + # if c == entry['Name']: + # self.state['BackPack']['Consumable'][c] -= 1 + # # Paranoia in case we lost track + # if self.state['BackPack']['Consumable'][c] < 0: + # self.state['BackPack']['Consumable'][c] = 0 + pass + + # TODO: + # + # also there's one additional journal event that was missed out from + # this version of the docs: "SuitLoadout": # when starting on foot, or + # when disembarking from a ship, with the same info as found in "CreateSuitLoadout" + elif event_type == 'SuitLoadout': + suit_slotid, suitloadout_slotid = self.suitloadout_store_from_event(entry) + if not self.suit_and_loadout_setcurrent(suit_slotid, suitloadout_slotid): + logger.error(f"Event was: {entry}") elif event_type == 'SwitchSuitLoadout': - loadoutid = entry['LoadoutID'] - new_slot = self.suit_loadout_id_from_loadoutid(loadoutid) - # If this application is run with the latest Journal showing such an event then we won't - # yet have the CAPI data, so no idea about Suits or Loadouts. - if self.state['Suits'] and self.state['SuitLoadouts']: - try: - self.state['SuitLoadoutCurrent'] = self.state['SuitLoadouts'][f'{new_slot}'] - - except KeyError: - logger.debug(f"KeyError getting suit loadout after switch, bad slot: {new_slot} ({loadoutid})") - self.state['SuitCurrent'] = None - self.state['SuitLoadoutCurrent'] = None - - else: - try: - new_suitid = self.state['SuitLoadoutCurrent']['suit']['suitId'] - - except KeyError: - logger.debug(f"KeyError getting switched-to suit ID from slot {new_slot} ({loadoutid})") - - else: - try: - self.state['SuitCurrent'] = self.state['Suits'][f'{new_suitid}'] - - except KeyError: - logger.debug(f"KeyError getting switched-to suit from slot {new_slot} ({loadoutid}") + # 4.0.0.101 + # + # { "timestamp":"2021-05-21T10:39:43Z", "event":"SwitchSuitLoadout", + # "SuitID":1700217809818876, "SuitName":"utilitysuit_class1", + # "SuitName_Localised":"Maverick Suit", "LoadoutID":4293000002, + # "LoadoutName":"K/P", "Modules":[ { "SlotName":"PrimaryWeapon1", + # "SuitModuleID":1700217863661544, + # "ModuleName":"wpn_m_assaultrifle_kinetic_fauto", + # "ModuleName_Localised":"Karma AR-50" }, + # { "SlotName":"SecondaryWeapon", "SuitModuleID":1700216180036986, + # "ModuleName":"wpn_s_pistol_plasma_charged", + # "ModuleName_Localised":"Manticore Tormentor" } ] } + # + suit_slotid, suitloadout_slotid = self.suitloadout_store_from_event(entry) + if not self.suit_and_loadout_setcurrent(suit_slotid, suitloadout_slotid): + logger.error(f"Event was: {entry}") elif event_type == 'CreateSuitLoadout': - # We know we won't have data for this new one - # Parameters: - # • SuitID - # • SuitName - # • LoadoutID - # • LoadoutName - # alpha4: - # { "timestamp":"2021-04-29T09:37:08Z", "event":"CreateSuitLoadout", "SuitID":1698364940285172, - # "SuitName":"tacticalsuit_class1", "SuitName_Localised":"Dominator Suit", "LoadoutID":4293000001, - # "LoadoutName":"Dom L/K/K", "Modules":[ - # { - # "SlotName":"PrimaryWeapon1", - # "SuitModuleID":1698364962722310, - # "ModuleName":"wpn_m_assaultrifle_laser_fauto", - # "ModuleName_Localised":"TK Aphelion" - # }, - # { "SlotName":"PrimaryWeapon2", - # "SuitModuleID":1698364956302993, "ModuleName":"wpn_m_assaultrifle_kinetic_fauto", - # "ModuleName_Localised":"Karma AR-50" }, { "SlotName":"SecondaryWeapon", - # "SuitModuleID":1698292655291850, "ModuleName":"wpn_s_pistol_kinetic_sauto", - # "ModuleName_Localised":"Karma P-15" } ] } - new_loadout = { - 'loadoutSlotId': self.suit_loadout_id_from_loadoutid(entry['LoadoutID']), - 'suit': { - 'name': entry['SuitName'], - 'locName': entry.get('SuitName_Localised', entry['SuitName']), - 'suitId': entry['SuitID'], - }, - 'name': entry['LoadoutName'], - 'slots': self.suit_loadout_slots_array_to_dict(entry['Modules']), - } - self.state['SuitLoadouts'][new_loadout['loadoutSlotId']] = new_loadout + # 4.0.0.101 + # + # { "timestamp":"2021-05-21T11:13:15Z", "event":"CreateSuitLoadout", "SuitID":1700216165682989, + # "SuitName":"tacticalsuit_class1", "SuitName_Localised":"Dominator Suit", "LoadoutID":4293000004, + # "LoadoutName":"P/P/K", "Modules":[ { "SlotName":"PrimaryWeapon1", "SuitModuleID":1700216182854765, + # "ModuleName":"wpn_m_assaultrifle_plasma_fauto", "ModuleName_Localised":"Manticore Oppressor" }, + # { "SlotName":"PrimaryWeapon2", "SuitModuleID":1700216190363340, + # "ModuleName":"wpn_m_shotgun_plasma_doublebarrel", "ModuleName_Localised":"Manticore Intimidator" }, + # { "SlotName":"SecondaryWeapon", "SuitModuleID":1700217869872834, + # "ModuleName":"wpn_s_pistol_kinetic_sauto", "ModuleName_Localised":"Karma P-15" } ] } + # + _, _ = self.suitloadout_store_from_event(entry) elif event_type == 'DeleteSuitLoadout': # alpha4: @@ -1113,10 +1183,8 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # • SuitID # • Class # • Cost - # Update credits total ? It shouldn't even involve credits! - # Actual alpha4 - need to grind mats - # if self.state['Suits']: - pass + # TODO: Update self.state['Suits'] when we have an example to work from + self.state['Credits'] -= entry.get('Cost', 0) elif event_type == 'LoadoutEquipModule': # alpha4: @@ -1513,6 +1581,120 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below logger.debug(f'Invalid journal entry:\n{line!r}\n', exc_info=ex) return {'event': None} + def suitloadout_store_from_event(self, entry) -> Tuple[int, int]: + """ + Store Suit and SuitLoadout data from a journal event. + + Also use set currently in-use instances of them as being as per this + event. + + :param entry: Journal entry - 'SwitchSuitLoadout' or 'SuitLoadout' + :return Tuple[suit_slotid, suitloadout_slotid]: The IDs we set data for. + """ + suit_slotid = self.suit_loadout_id_from_loadoutid(entry['LoadoutID']) + # Initial suit containing just the data that is then embedded in + # the loadout + new_suit = { + 'name': entry['SuitName'], + 'locName': entry.get('SuitName_Localised', entry['SuitName']), + 'suitId': entry['SuitID'], + } + # Make the new loadout, in the CAPI format + new_loadout = { + 'loadoutSlotId': suit_slotid, + 'suit': new_suit, + 'name': entry['LoadoutName'], + 'slots': self.suit_loadout_slots_array_to_dict( + entry['Modules']), + } + # Assign this loadout into our state + self.state['SuitLoadouts'][f"{new_loadout['loadoutSlotId']}"] = new_loadout + + # Now add in the extra fields for new_suit to be a 'full' Suit structure + new_suit['id'] = None # Not available in 4.0.0.100 journal event + new_suit['slots'] = new_loadout['slots'] # 'slots', not 'Modules', to match CAPI + # Ensure new_suit is in self.state['Suits'] + self.state['Suits'][f"{suit_slotid}"] = new_suit + + return suit_slotid, new_loadout['loadoutSlotId'] + + def suit_and_loadout_setcurrent(self, suit_slotid: int, suitloadout_slotid: int) -> bool: + """ + Set self.state for SuitCurrent and SuitLoadoutCurrent as requested. + + If the specified slots are unknown we abort and return False, else + return True. + + :param suit_slotid: Numeric ID of the slot for the suit. + :param suitloadout_slotid: Numeric ID of the slot for the suit loadout. + :return: True if we could do this, False if not. + """ + str_suitid = f"{suit_slotid}" + str_suitloadoutid = f"{suitloadout_slotid}" + + if (self.state['Suits'].get(str_suitid, False) + and self.state['SuitLoadouts'].get(str_suitloadoutid, False)): + self.state['SuitCurrent'] = self.state['Suits'][str_suitid] + self.state['SuitLoadoutCurrent'] = self.state['SuitLoadouts'][str_suitloadoutid] + return True + + logger.error(f"Tried to set a suit and suitloadout where we didn't know about both: {suit_slotid=}, " + f"{str_suitloadoutid=}") + return False + + # TODO: *This* will need refactoring and a proper validation infrastructure + # designed for this in the future. This is a bandaid for a known issue. + def event_valid_engineerprogress(self, entry) -> bool: # noqa: CCR001 C901 + """ + Check an `EngineerProgress` Journal event for validity. + + :param entry: Journal event dict + :return: True if passes validation, else False. + """ + # The event should have at least one of thes + if 'Engineers' not in entry and 'Progress' not in entry: + logger.warning(f"EngineerProgress has neither 'Engineers' nor 'Progress': {entry=}") + return False + + # But not both of them + if 'Engineers' in entry and 'Progress' in entry: + logger.warning(f"EngineerProgress has BOTH 'Engineers' and 'Progress': {entry=}") + return False + + if 'Engineers' in entry: + # 'Engineers' version should have a list as value + if not isinstance(entry['Engineers'], list): + logger.warning(f"EngineerProgress 'Engineers' is not a list: {entry=}") + return False + + # It should have at least one entry? This might still be valid ? + if len(entry['Engineers']) < 1: + logger.warning(f"EngineerProgress 'Engineers' list is empty ?: {entry=}") + # TODO: As this might be valid, we might want to only log + return False + + # And that list should have all of these keys + for e in entry['Engineers']: + for f in ('Engineer', 'EngineerID', 'Rank', 'Progress', 'RankProgress'): + if f not in e: + # For some Progress there's no Rank/RankProgress yet + if f in ('Rank', 'RankProgress'): + if (progress := e.get('Progress', None)) is not None: + if progress in ('Invited', 'Known'): + continue + + logger.warning(f"Engineer entry without '{f}' key: {e=} in {entry=}") + return False + + if 'Progress' in entry: + for e in entry['Engineers']: + for f in ('Engineer', 'EngineerID', 'Rank', 'Progress', 'RankProgress'): + if f not in e: + logger.warning(f"Engineer entry without '{f}' key: {e=} in {entry=}") + return False + + return True + def suit_loadout_id_from_loadoutid(self, journal_loadoutid: int) -> int: """ Determine the CAPI-oriented numeric slot id for a Suit Loadout. diff --git a/plugins/edsm.py b/plugins/edsm.py index 66b833c6..422d0b56 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -432,6 +432,11 @@ def journal_entry( elif entry['event'] == 'NavBeaconScan': this.navbeaconscan = entry['NumBodies'] + elif entry['event'] == 'BackPack': + # Use the stored file contents, not the empty journal event + if state['BackpackJSON']: + entry = state['BackpackJSON'] + # Send interesting events to EDSM if ( config.get_int('edsm_out') and not is_beta and not this.multicrew and credentials(cmdr) and diff --git a/plugins/inara.py b/plugins/inara.py index e3fe6a4f..5e08cf74 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -449,14 +449,17 @@ def journal_entry( # noqa: C901, CCR001 ) # Update location - new_add_event( - 'setCommanderTravelLocation', - entry['timestamp'], - OrderedDict([ - ('starsystemName', system), - ('stationName', station), # Can be None - ]) - ) + # Might not be available if this event is a 'StartUp' and we're replaying + # a log. + if system: + new_add_event( + 'setCommanderTravelLocation', + entry['timestamp'], + OrderedDict([ + ('starsystemName', system), + ('stationName', station), # Can be None + ]) + ) # Update ship if state['ShipID']: # Unknown if started in Fighter or SRV diff --git a/prefs.py b/prefs.py index 65a018a1..385780d3 100644 --- a/prefs.py +++ b/prefs.py @@ -535,10 +535,9 @@ class PreferencesDialog(tk.Toplevel): ) with row as cur_row: + shipyard_provider = config.get_str('shipyard_provider') self.shipyard_provider = tk.StringVar( - value=str( - config.get_str('shipyard_provider') in plug.provides('shipyard_url') - and config.get_str('shipyard_provider', default='EDSY')) + value=str(shipyard_provider if shipyard_provider in plug.provides('shipyard_url') else 'EDSY') ) # Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis nb.Label(config_frame, text=_('Shipyard')).grid(padx=self.PADX, pady=2*self.PADY, sticky=tk.W, row=cur_row) diff --git a/requirements.txt b/requirements.txt index ee9255db..b72dc3f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ certifi==2020.12.5 requests==2.25.1 -watchdog==2.1.1 +watchdog==2.1.2 # Commented out because this doesn't package well with py2exe # infi.systray==0.1.12; sys_platform == 'win32' # argh==0.26.2 watchdog dep diff --git a/td.py b/td.py index 13c32edd..5f05b732 100644 --- a/td.py +++ b/td.py @@ -25,7 +25,15 @@ def export(data): querytime = config.get_int('querytime', default=int(time.time())) + # + # When this is refactored into multi-line CHECK IT WORKS, avoiding the + # brainfart we had with dangling commas in commodity.py:export() !!! + # filename = join(config.get_str('outdir'), '%s.%s.%s.prices' % (data['lastSystem']['name'].strip(), data['lastStarport']['name'].strip(), time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime)))) + # + # When this is refactored into multi-line CHECK IT WORKS, avoiding the + # brainfart we had with dangling commas in commodity.py:export() !!! + # timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.strptime(data['timestamp'], '%Y-%m-%dT%H:%M:%SZ'))