From 439af62ae23e4d5119f3146ace088cdabeca726d Mon Sep 17 00:00:00 2001 From: yoenis hernandez Date: Mon, 26 Oct 2026 07:39:36 +0000 Subject: [PATCH 1/2] Abyss stats: read-only Wallet from EVE, remove Loot (unsold) --- EveProfitManager/core.py | 68 --------------- EveProfitManager/gui.py | 102 ++++++++-------------- EveProfitManager/loot.py | 28 +----- EveProfitManager/test_core.py | 52 ++++++------ EveProfitManager/test_loot.py | 14 +-- EveProfitManager/test_loot_page.py | 76 ----------------- EveProfitManager/test_wallet_page.py | 122 +++++++++++++++++++++++++++ README.md | 10 +-- 8 files changed, 186 insertions(+), 286 deletions(-) delete mode 100644 EveProfitManager/test_loot_page.py create mode 100644 EveProfitManager/test_wallet_page.py diff --git a/EveProfitManager/core.py b/EveProfitManager/core.py index 8b55ed1..bc7f278 100644 --- a/EveProfitManager/core.py +++ b/EveProfitManager/core.py @@ -5,7 +5,6 @@ """ import csv import hashlib -import json import math import os import re @@ -46,14 +45,6 @@ CREATE TABLE IF NOT EXISTS payouts ( journal_id INTEGER PRIMARY KEY ); -CREATE TABLE IF NOT EXISTS loot ( - session_id INTEGER PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE, - character_id INTEGER NOT NULL, - baseline TEXT NOT NULL, - millions REAL, - unpriced INTEGER, - as_of TEXT -); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL @@ -121,21 +112,6 @@ class Run: count: int -@dataclass -class Baseline: - """What a character owned when a session started.""" - character_id: int - quantities: dict # type_id -> quantity - - -@dataclass -class Loot: - """A session's unsold loot: the market value of how its items changed since it started.""" - millions: float - unpriced: int # kinds of item that changed but have no market price - as_of: datetime # when the assets it was valued from were read - - @dataclass class Session: id: int @@ -449,50 +425,6 @@ def current_isk(self, session_id): session = self.session(session_id) return session.starting_isk if session else None - # --- loot ----------------------------------------------------------- - - def set_loot_baseline(self, session_id, character_id, quantities): - """ - Remember what the character owned when the session started, by type_id. - - Only the first baseline counts, so reading the assets again after a - restart doesn't wipe out loot already brought back. - """ - with self.db: - self.db.execute( - "INSERT OR IGNORE INTO loot (session_id, character_id, baseline) VALUES (?, ?, ?)", - (session_id, character_id, json.dumps(quantities)), - ) - - def loot_baseline(self, session_id): - """The session's Baseline, or None if none was taken.""" - row = self.db.execute("SELECT character_id, baseline FROM loot WHERE session_id = ?", (session_id,)).fetchone() - if row is None: - return None - return Baseline(row["character_id"], {int(t): q for t, q in json.loads(row["baseline"]).items()}) - - def set_loot(self, session_id, value, as_of): - """ - Save the session's latest loot valuation. - - Args: - session_id (int): Session with a baseline. - value (loot.LootValue): Worth of the change since the baseline. - as_of (datetime): When the assets it was valued from were read. - """ - with self.db: - self.db.execute( - "UPDATE loot SET millions = ?, unpriced = ?, as_of = ? WHERE session_id = ?", - (value.millions, value.unpriced, as_of.isoformat(timespec="seconds"), session_id), - ) - - def loot(self, session_id): - """The session's latest Loot, or None if it hasn't been valued.""" - row = self.db.execute( - "SELECT millions, unpriced, as_of FROM loot WHERE session_id = ? AND as_of IS NOT NULL", (session_id,) - ).fetchone() - return Loot(row["millions"], row["unpriced"], datetime.fromisoformat(row["as_of"])) if row else None - # --- stats ---------------------------------------------------------- def totals(self, kind=None): diff --git a/EveProfitManager/gui.py b/EveProfitManager/gui.py index 4bef7cb..eb7473f 100644 --- a/EveProfitManager/gui.py +++ b/EveProfitManager/gui.py @@ -31,7 +31,6 @@ from clock import read_clock from journal import get_incursion_payouts from location import AbyssWatcher, get_location -from loot import get_assets, get_prices, value_change from wallet import get_wallet from account import SIGN_IN_ERRORS, Account from theme import NEUTRAL_THEME, asset_path, load_fonts, theme_for, ui_family @@ -864,76 +863,47 @@ def __init__(self, master, app): lambda location: self.see_abyss(location.in_abyss), "Location", self.LOCATION_POLL_MS, self.LOCATION_RETRY_MS, on_start=self.reset_watcher, ).switch - self._valuing = False - self._loot_problem = None # an asset error or mismatch, shown instead of the loot's time - self._loot_waiting = "reading assets..." # shown before the session's first reading - self._loot_poll() - - def _loot_poll(self): - self.value_loot() - self.after(self.LOOT_POLL_MS, self._loot_poll) - - def value_loot(self): - """ - Read the assets off the UI thread: the first reading of a session is its - baseline, and later ones value what changed since. Quiet when signed out. - """ - if self.session_id is None or self._valuing: + self._wallet = None # the latest Wallet read from ESI + self._wallet_note = "reading wallet..." # the stat's sub-line when there is no reading to date + self._wallet_problem = None # a read error, shown instead of the reading's time + self._reading_wallet = False + + def read_wallet(self): + """Read the wallet off the UI thread for the read-only Wallet stat. Quiet when signed out.""" + if self.session_id is None or self._reading_wallet: return - client, _ = self.signed_in_client() + client, why = self.signed_in_client() + self._wallet_problem = None if client is None: - if self._loot_waiting != "sign in to value loot": - self._loot_waiting = "sign in to value loot" - self.refresh() + self._wallet = None + self._wallet_note = "sign in under Settings to see it" + self.refresh() return - self._loot_waiting = "reading assets..." + self._reading_wallet = True session_id = self.session_id - has_baseline = self.tracker.loot_baseline(session_id) is not None - self._valuing = True - - def read(): - return get_assets(client, client.character.id), get_prices(client) if has_baseline else None - def done(result, error): - self._valuing = False + def done(wallet, error): + self._reading_wallet = False if session_id != self.session_id: # the session changed while the read was out - self.value_loot() - elif error is not None: - self.loot_failed(error) + self.read_wallet() + return + if error is not None: + self._wallet_problem = f"Wallet: {error}" else: - self.take_assets(*result) - - self.app.run_async(read, done) - - def take_assets(self, assets, prices): - """Use an asset reading: the session's baseline if it has none, else value the change since.""" - baseline = self.tracker.loot_baseline(self.session_id) - self._loot_problem = None - if baseline is None: - self.tracker.set_loot_baseline(self.session_id, assets.character_id, assets.quantities) - elif baseline.character_id != assets.character_id: - self._loot_problem = "baseline is for another character" - elif prices is not None: - self.tracker.set_loot(self.session_id, value_change(baseline.quantities, assets.quantities, prices), - datetime.fromtimestamp(assets.fetched_at)) - self.refresh() + self._wallet = wallet + self.refresh() - def loot_failed(self, error): - self._loot_problem = f"Assets: {error}" - self.refresh() + self.app.run_async(lambda: get_wallet(client, client.character.id), done) def start_session(self): - self._loot_problem = None + self._wallet = None + self._wallet_note = "reading wallet..." super().start_session() - self.value_loot() - - def end_note(self): - loot = self.tracker.loot(self.session_id) - return f"\n\nUnsold loot: {signed_isk(loot.millions)} ISK at market prices." if loot else "" + self.read_wallet() def resume(self): super().resume() - self.value_loot() + self.read_wallet() def build_start_form(self, frame): self.start_isk = self.field(frame, 0, "Wallet now (million ISK)", 220, "e.g. 1,250 or 1.25b") @@ -1028,7 +998,7 @@ def pocket_fields(self): return self.after_isk, self.time_left def stat_labels(self): - return [("profit", "Session"), ("loot", "Loot (unsold)"), ("rate", "ISK/hr"), ("time", "Time"), + return [("profit", "Session"), ("rate", "ISK/hr"), ("time", "Time"), ("wallet", "Wallet")] def timer_caption(self, session): @@ -1056,7 +1026,7 @@ def log_run(self): def after_log(self): self.after_isk.delete(0, "end") self.time_left.delete(0, "end") - self.value_loot() + self.read_wallet() def edit_fields(self, run): return [("Wallet after run (M)", f"{run.isk_after:g}"), ("Time left (min)", f"{MAXTIME - run.minutes:g}")] @@ -1073,17 +1043,11 @@ def update_stats(self, session, runs): self.stats["rate"].set(format_isk(session.isk_per_hour)) self.stats["time"].set(format_minutes(session.minutes), sub=f"{session.runs} runs" if runs else "") - self.stats["wallet"].set(format_isk(self.tracker.current_isk(session.id) or 0), - sub=f"from {format_isk(session.starting_isk or 0)}") - loot = self.tracker.loot(session.id) - if loot: - unpriced = f" · {loot.unpriced} unpriced" if loot.unpriced else "" - self.stats["loot"].set(signed_isk(loot.millions), profit_color(loot.millions), - self._loot_problem or f"from EVE at {loot.as_of:%H:%M}{unpriced}") - elif self.tracker.loot_baseline(session.id) is not None: - self.stats["loot"].set(signed_isk(0), sub=self._loot_problem or "ESI updates assets hourly") + if self._wallet: + self.stats["wallet"].set(format_isk(self._wallet.millions), sub=self._wallet_problem or + f"from EVE at {datetime.fromtimestamp(self._wallet.fetched_at):%H:%M}") else: - self.stats["loot"].set("—", sub=self._loot_problem or self._loot_waiting) + self.stats["wallet"].set("—", sub=self._wallet_problem or self._wallet_note) class IncursionPage(SessionPage): diff --git a/EveProfitManager/loot.py b/EveProfitManager/loot.py index cdb945b..9d890e8 100644 --- a/EveProfitManager/loot.py +++ b/EveProfitManager/loot.py @@ -1,4 +1,4 @@ -"""Value the items a session brought back, from the character's assets on ESI and market prices.""" +"""Read the character's assets from ESI and the market prices that value them.""" from dataclasses import dataclass @@ -9,12 +9,6 @@ class Assets: fetched_at: float # when ESI last answered; it caches the asset list for an hour -@dataclass(frozen=True) -class LootValue: - millions: float # ISK, at market prices - unpriced: int # kinds of item that changed but have no market price, like abyssal modules - - def get_assets(client, character_id): """ Everything the character owns, counted by type. @@ -55,26 +49,6 @@ def get_prices(client): return prices -def value_change(before, after, prices): - """ - What the change from the `before` quantities to the `after` ones is worth. - - Items gained count for their value and items used up (filaments, ammo, a - lost ship) against it, so buying something only moves ISK out of the wallet - and into the loot. - """ - isk, unpriced = 0.0, 0 - for type_id in before.keys() | after.keys(): - change = after.get(type_id, 0) - before.get(type_id, 0) - if not change: - continue - if type_id in prices: - isk += change * prices[type_id] - else: - unpriced += 1 - return LootValue(millions=isk / 1e6, unpriced=unpriced) - - def _items(body): if not isinstance(body, list) or not all( isinstance(e, dict) and _whole(e.get("type_id")) and _whole(e.get("quantity")) for e in body diff --git a/EveProfitManager/test_core.py b/EveProfitManager/test_core.py index 65628b4..6d543d0 100644 --- a/EveProfitManager/test_core.py +++ b/EveProfitManager/test_core.py @@ -4,9 +4,8 @@ from pathlib import Path from tempfile import TemporaryDirectory -from core import ABYSS, INCURSION, Baseline, Loot, Totals, Tracker, best_weather, isk_per_hour, parse_sessions_txt +from core import ABYSS, INCURSION, Totals, Tracker, best_weather, isk_per_hour, parse_sessions_txt from journal import Payout -from loot import LootValue OLD_FILE = """Abyss Details! @@ -259,29 +258,32 @@ def test_a_site_logged_by_hand_starts_the_next_payouts_time(self): self.assertAlmostEqual(logged[0].minutes, (self.start + timedelta(minutes=30) - manual).total_seconds() / 60) -class LootTest(unittest.TestCase): - def setUp(self): - self.tracker = Tracker(":memory:") - self.sid = self.tracker.start_session(ABYSS, starting_isk=1000) - - def test_the_first_baseline_of_a_session_is_the_one_kept(self): - self.assertIsNone(self.tracker.loot_baseline(self.sid)) - self.tracker.set_loot_baseline(self.sid, 123, {34: 1_000, 47_762: 2}) - self.tracker.set_loot_baseline(self.sid, 456, {34: 5}) # e.g. read again after a restart - self.assertEqual(self.tracker.loot_baseline(self.sid), Baseline(123, {34: 1_000, 47_762: 2})) - - def test_the_latest_valuation_is_kept_with_when_the_assets_were_read(self): - self.assertIsNone(self.tracker.loot(self.sid)) - self.tracker.set_loot_baseline(self.sid, 123, {}) - as_of = datetime(2026, 10, 25, 18, 40) - self.tracker.set_loot(self.sid, LootValue(millions=12.5, unpriced=0), datetime(2026, 10, 25, 18, 0)) - self.tracker.set_loot(self.sid, LootValue(millions=48.25, unpriced=2), as_of) - self.assertEqual(self.tracker.loot(self.sid), Loot(millions=48.25, unpriced=2, as_of=as_of)) - - def test_an_abandoned_session_takes_its_loot_with_it(self): - self.tracker.set_loot_baseline(self.sid, 123, {34: 1_000}) - self.tracker.end_session(self.sid) # no runs, so the session is deleted - self.assertIsNone(self.tracker.loot_baseline(self.sid)) +class LootTableTest(unittest.TestCase): + def tables(self, db): + return {r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type = 'table'")} + + def test_a_new_database_has_no_loot_table(self): + tracker = Tracker(":memory:") + self.assertNotIn("loot", self.tables(tracker.db)) + + def test_an_old_database_with_a_loot_table_still_opens_and_keeps_it(self): + with TemporaryDirectory() as folder: + path = str(Path(folder) / "old.db") + tracker = Tracker(path) + sid = tracker.start_session(ABYSS, starting_isk=1000) + tracker.db.execute( + "CREATE TABLE loot (session_id INTEGER PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE, " + "character_id INTEGER NOT NULL, baseline TEXT NOT NULL, millions REAL, unpriced INTEGER, as_of TEXT)" + ) + tracker.db.execute("INSERT INTO loot (session_id, character_id, baseline) VALUES (?, 1, '{}')", (sid,)) + tracker.db.commit() + tracker.close() + reopened = Tracker(path) + self.addCleanup(reopened.close) + self.assertIn("loot", self.tables(reopened.db)) + self.assertEqual(reopened.session(sid).starting_isk, 1000) + reopened.log_abyss_run(sid, 1000, 1050, 10) + self.assertEqual(len(reopened.runs(sid)), 1) class ImportTest(unittest.TestCase): diff --git a/EveProfitManager/test_loot.py b/EveProfitManager/test_loot.py index 6bbb601..9777fe7 100644 --- a/EveProfitManager/test_loot.py +++ b/EveProfitManager/test_loot.py @@ -1,6 +1,6 @@ import unittest -from loot import Assets, LootValue, get_assets, get_prices, value_change +from loot import Assets, get_assets, get_prices ASSETS = "/characters/123/assets" @@ -59,17 +59,5 @@ def test_rejects_a_response_that_is_not_a_list_of_prices(self): get_prices(FakeClient({"/markets/prices": body})) -class ValueChangeTests(unittest.TestCase): - def test_values_what_was_gained_less_what_was_used_up_in_millions(self): - before = {34: 10_000, 47_762: 1, 30_000: 500} # tritanium, a filament, ammo - after = {34: 10_000, 30_000: 100, 49_000: 2} # filament used, ammo shot, two drops - prices = {34: 4.5, 47_762: 1_200_000.0, 30_000: 500.0, 49_000: 2_100_000.0} - self.assertEqual(value_change(before, after, prices), LootValue(millions=2.8, unpriced=0)) - - def test_counts_the_kinds_of_item_with_no_price(self): - self.assertEqual(value_change({}, {52_227: 1, 52_228: 1, 34: 1_000_000}, {34: 4.5}), - LootValue(millions=4.5, unpriced=2)) - - if __name__ == "__main__": unittest.main() diff --git a/EveProfitManager/test_loot_page.py b/EveProfitManager/test_loot_page.py deleted file mode 100644 index 9410f82..0000000 --- a/EveProfitManager/test_loot_page.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -import tempfile -import tkinter -import unittest -from datetime import datetime -from unittest import mock - -from core import ABYSS, Tracker -from loot import Assets -from theme import load_fonts - -READ_AT = datetime(2026, 10, 25, 18, 40).timestamp() - - -class AbyssLootPageTest(unittest.TestCase): - """Feeds asset readings to the Abyss page and checks the unsold loot it shows.""" - - def setUp(self): - load_fonts() # as the app does, before the first Tk: macOS reads the font list only then - try: - tkinter.Tk().destroy() - except tkinter.TclError as err: # no display, e.g. headless Linux CI - self.skipTest(f"Tk cannot open a window: {err}") - import customtkinter as ctk - import gui - - ctk.set_appearance_mode("dark") - signed_out = mock.patch.object(gui.Account, "client", return_value=None) # keep the real account's ESI out - signed_out.start() - self.addCleanup(signed_out.stop) - folder = tempfile.TemporaryDirectory() - self.addCleanup(folder.cleanup) - self.tracker = Tracker(os.path.join(folder.name, "tracker.db")) - self.addCleanup(self.tracker.close) - self.tracker.start_session(ABYSS, starting_isk=1000, weather="Gamma", tier=2) - self.app = gui.App(self.tracker) - self.addCleanup(self.app.destroy) - self.app.update() - self.page = self.app.pages["abyss"] - self.loot = self.page.stats["loot"] - - def shown(self): - return self.loot.value.cget("text"), self.loot.sub.cget("text") - - def test_shows_the_value_of_what_came_back_beside_the_wallet_profit(self): - self.page.take_assets(Assets(123, {34: 10_000, 47_762: 1}, READ_AT), None) # at the session start - self.page.take_assets(Assets(123, {34: 10_000, 49_000: 2}, READ_AT), {47_762: 1_200_000.0, 49_000: 2_100_000.0}) - value, sub = self.shown() - self.assertEqual(value, "+3.0M") - self.assertIn("18:40", sub) - self.assertEqual(self.page.stats["profit"].value.cget("text"), "0.0M") # the wallet hasn't changed - - def test_says_how_many_kinds_of_item_have_no_price(self): - self.page.take_assets(Assets(123, {}, READ_AT), None) - self.page.take_assets(Assets(123, {52_227: 1, 52_228: 1}, READ_AT), {}) - self.assertIn("2 unpriced", self.shown()[1]) - - def test_waits_for_the_first_reading_before_valuing_anything(self): - self.page.take_assets(Assets(123, {34: 10_000}, READ_AT), None) - self.assertEqual(self.shown()[0], "0.0M") - self.assertIsNone(self.tracker.loot(self.page.session_id)) - - def test_a_reading_for_another_character_is_not_valued_against_the_baseline(self): - self.page.take_assets(Assets(123, {34: 10_000}, READ_AT), None) - self.page.take_assets(Assets(456, {49_000: 50}, READ_AT), {49_000: 2_100_000.0}) # switched character - self.assertIsNone(self.tracker.loot(self.page.session_id)) - self.assertIn("another character", self.shown()[1]) - - def test_an_asset_error_stays_shown_after_a_refresh(self): - self.page.loot_failed("ESI returned 403") - self.app.refresh() - self.assertIn("403", self.shown()[1]) - - -if __name__ == "__main__": - unittest.main() diff --git a/EveProfitManager/test_wallet_page.py b/EveProfitManager/test_wallet_page.py new file mode 100644 index 0000000..42f805d --- /dev/null +++ b/EveProfitManager/test_wallet_page.py @@ -0,0 +1,122 @@ +import os +import tempfile +import tkinter +import unittest +from datetime import datetime +from types import SimpleNamespace +from unittest import mock + +from core import ABYSS, Tracker +from theme import load_fonts +from wallet import Wallet + +READ_AT = datetime(2026, 10, 25, 18, 40).timestamp() + + +class AbyssWalletStatTest(unittest.TestCase): + """Drives the Abyss page with a mocked account and wallet reader and checks the read-only Wallet stat.""" + + def setUp(self): + load_fonts() # as the app does, before the first Tk: macOS reads the font list only then + try: + tkinter.Tk().destroy() + except tkinter.TclError as err: # no display, e.g. headless Linux CI + self.skipTest(f"Tk cannot open a window: {err}") + import customtkinter as ctk + import gui + + self.gui = gui + ctk.set_appearance_mode("dark") + self.client = SimpleNamespace(character=SimpleNamespace(id=123)) + account = mock.patch.object(gui.Account, "client", side_effect=lambda: self.client) + account.start() + self.addCleanup(account.stop) + self.wallet = mock.Mock(return_value=Wallet(millions=1250.0, fetched_at=READ_AT)) + reader = mock.patch.object(gui, "get_wallet", self.wallet) + reader.start() + self.addCleanup(reader.stop) + folder = tempfile.TemporaryDirectory() + self.addCleanup(folder.cleanup) + self.tracker = Tracker(os.path.join(folder.name, "tracker.db")) + self.addCleanup(self.tracker.close) + self.tracker.start_session(ABYSS, starting_isk=1000, weather="Gamma", tier=2) + sync = mock.patch.object(gui.App, "run_async", lambda _app, work, done: self.finish(work, done)) + sync.start() # read on the spot, not on a thread + self.addCleanup(sync.stop) + self.app = gui.App(self.tracker) + self.addCleanup(self.app.destroy) + self.app.update() + self.wallet.reset_mock() # the app read the wallet once as it opened the open session + self.page = self.app.pages["abyss"] + + @staticmethod + def finish(work, done): + try: + result = work() + except Exception as error: # the app's run_async hands the error on the same way + done(None, error) + else: + done(result, None) + + def shown(self): + wallet = self.page.stats["wallet"] + return wallet.value.cget("text"), wallet.sub.cget("text") + + def test_there_is_no_loot_stat_and_the_wallet_stat_is_there(self): + self.assertEqual(list(self.page.stats), ["profit", "rate", "time", "wallet"]) + + def test_shows_the_balance_from_eve_with_when_it_was_read(self): + self.page.resume() + value, sub = self.shown() + self.assertEqual(value, "1.25B") + self.assertEqual(sub, "from EVE at 18:40") + + def test_reads_again_after_each_logged_run(self): + self.page.resume() + self.wallet.return_value = Wallet(millions=1300.0, fetched_at=READ_AT) + self.page.after_isk.insert(0, "1,050") + self.page.time_left.insert(0, "10") + self.page.log() + self.assertEqual(self.wallet.call_count, 2) + self.assertEqual(self.shown()[0], "1.30B") + + def test_reads_when_a_session_starts(self): + self.tracker.end_session(self.page.session_id) + self.page.resume() + self.page.start_isk.insert(0, "1,000") + self.page.start_session() + self.assertEqual(self.wallet.call_count, 1) + self.assertEqual(self.shown()[1], "from EVE at 18:40") + + def test_signed_out_shows_a_dash_with_a_hint(self): + self.client = None + self.page.resume() + value, sub = self.shown() + self.assertEqual(value, "—") + self.assertIn("sign in", sub.lower()) + self.wallet.assert_not_called() + + def test_a_read_error_shows_in_the_sub_line_and_logging_still_works(self): + self.wallet.side_effect = ValueError("ESI returned 403") + self.page.resume() + self.assertIn("403", self.shown()[1]) + self.page.after_isk.insert(0, "1,050") + self.page.time_left.insert(0, "10") + self.page.log() + self.assertEqual(len(self.tracker.runs(self.page.session_id)), 1) + self.assertIn("403", self.shown()[1]) + + def test_no_asset_polling_while_a_session_is_open(self): + with mock.patch.object(self.page, "after") as after: + self.page.resume() + scheduled = [call.args[0] for call in after.call_args_list if call.args] + self.assertNotIn(5 * 60 * 1000, scheduled) + self.assertFalse(hasattr(self.page, "LOOT_POLL_MS")) + self.assertFalse(hasattr(self.page, "value_loot")) + + def test_the_session_end_summary_has_no_loot_line(self): + self.assertNotIn("loot", self.page.end_note().lower()) + + +if __name__ == "__main__": + unittest.main() diff --git a/README.md b/README.md index e3ca07a..ba0787e 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,8 @@ This program is designed to help users track their profits and time spent during timed from the previous site, or from the session start for the first. ESI caches the journal for an hour, so sites can appear up to an hour after you run them; keep the session open until they do. A payout is only logged once, so a site you delete stays deleted. - - **Unsold loot:** when signed in, the Abyss page notes what your character owns when the session starts and - shows **Loot (unsold)** beside the wallet profit: the market value of the items gained since, less what was - used up (filaments, ammo, a lost ship). Prices are CCP's average market prices, so the figure is an estimate, - not a Jita quote. Abyssal modules have no market price and are counted as *unpriced*; blueprint copies are - left out. ESI refreshes assets only hourly, so the loot catches up with your runs up to an hour later; keep the - session open until it does. For the same reason, sign in before starting the session, and give ESI time to - catch up after the last one, or its loot is counted in the new session. Loot listed on the market leaves your - assets, so it drops out of the figure until it sells and shows in the wallet. + - **Wallet stat:** when signed in, the Abyss page shows your wallet balance read from ESI ("from EVE at HH:MM"), + refreshed when a session starts or resumes and after each logged run. It is read-only; signed out it shows "—". - Every run is saved immediately, so closing the app mid-session picks up where you left off. - Amounts are in millions of ISK; you can type `1,250`, `1.25b` or `800k`. Times accept `14.5` or `14:30`. From c10460eb097beb129b04e5fc2b59885deb36b7f1 Mon Sep 17 00:00:00 2001 From: yoenis hernandez Date: Mon, 26 Oct 2026 07:42:58 +0000 Subject: [PATCH 2/2] Remove stale LOOT_POLL_MS; close reopened tracker in loot-table test --- EveProfitManager/gui.py | 1 - EveProfitManager/test_core.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/EveProfitManager/gui.py b/EveProfitManager/gui.py index eb7473f..0d5dfb3 100644 --- a/EveProfitManager/gui.py +++ b/EveProfitManager/gui.py @@ -852,7 +852,6 @@ class AbyssPage(SessionPage): pocket_label = "Wallet after run (M)" LOCATION_POLL_MS = 5000 # ESI caches the location for 5 s; the client never asks sooner LOCATION_RETRY_MS = 30000 # after a failed read - LOOT_POLL_MS = 5 * 60 * 1000 # ESI caches assets for an hour; the client never asks sooner def __init__(self, master, app): super().__init__(master, app) diff --git a/EveProfitManager/test_core.py b/EveProfitManager/test_core.py index 6d543d0..f0c0c0a 100644 --- a/EveProfitManager/test_core.py +++ b/EveProfitManager/test_core.py @@ -279,11 +279,11 @@ def test_an_old_database_with_a_loot_table_still_opens_and_keeps_it(self): tracker.db.commit() tracker.close() reopened = Tracker(path) - self.addCleanup(reopened.close) self.assertIn("loot", self.tables(reopened.db)) self.assertEqual(reopened.session(sid).starting_isk, 1000) reopened.log_abyss_run(sid, 1000, 1050, 10) self.assertEqual(len(reopened.runs(sid)), 1) + reopened.close() class ImportTest(unittest.TestCase):