diff --git a/src/main.py b/src/main.py index bb135dc..05f6204 100644 --- a/src/main.py +++ b/src/main.py @@ -241,9 +241,20 @@ class CartridgesApplication(Adw.Application): def main(version): # pylint: disable=unused-argument # Initiate logger - default_log_level = "DEBUG" if shared.PROFILE == "development" else "WARNING" - log_level = os.environ.get("LOGLEVEL", default_log_level).upper() - logging.basicConfig(level=log_level) + # (silence debug info from external libraries) + profile_base_log_level = "DEBUG" if shared.PROFILE == "development" else "WARNING" + profile_lib_log_level = "INFO" if shared.PROFILE == "development" else "WARNING" + base_log_level = os.environ.get("LOGLEVEL", profile_base_log_level).upper() + lib_log_level = os.environ.get("LIBLOGLEVEL", profile_lib_log_level).upper() + log_levels = { + __name__: base_log_level, + "PIL": lib_log_level, + "urllib3": lib_log_level, + } + logging.basicConfig() + for logger, level in log_levels.items(): + logging.getLogger(logger).setLevel(level) + # Start app app = CartridgesApplication() return app.run(sys.argv) diff --git a/src/store/managers/async_manager.py b/src/store/managers/async_manager.py index c3dfb73..8d54681 100644 --- a/src/store/managers/async_manager.py +++ b/src/store/managers/async_manager.py @@ -26,7 +26,8 @@ class AsyncManager(Manager): Already scheduled Tasks will no longer be cancellable.""" self.cancellable = Gio.Cancellable() - def run(self, game: Game, callback: Callable[["Manager"], Any]) -> None: + def process_game(self, game: Game, callback: Callable[["Manager"], Any]) -> None: + """Create a task to process the game in a separate thread""" task = Task.new(None, self.cancellable, self._task_callback, (callback,)) task.set_task_data((game,)) task.run_in_thread(self._task_thread_func) @@ -34,9 +35,9 @@ class AsyncManager(Manager): def _task_thread_func(self, _task, _source_object, data, cancellable): """Task thread entry point""" game, *_rest = data - self.final_run(game) + self.execute_resilient_manager_logic(game) def _task_callback(self, _source_object, _result, data): - """Method run after the async task is done""" + """Method run after the task is done""" callback, *_rest = data callback(self) diff --git a/src/store/managers/display_manager.py b/src/store/managers/display_manager.py index 2e0ef39..90a7bde 100644 --- a/src/store/managers/display_manager.py +++ b/src/store/managers/display_manager.py @@ -10,7 +10,7 @@ class DisplayManager(Manager): run_after = set((SteamAPIManager, SGDBManager)) - def final_run(self, game: Game) -> None: + def manager_logic(self, game: Game) -> None: # TODO decouple a game from its widget shared.win.games[game.game_id] = game game.update() diff --git a/src/store/managers/file_manager.py b/src/store/managers/file_manager.py index 12884ed..07c725f 100644 --- a/src/store/managers/file_manager.py +++ b/src/store/managers/file_manager.py @@ -8,5 +8,5 @@ class FileManager(AsyncManager): run_after = set((SteamAPIManager,)) - def final_run(self, game: Game) -> None: + def manager_logic(self, game: Game) -> None: game.save() diff --git a/src/store/managers/manager.py b/src/store/managers/manager.py index 19b20c2..01535bc 100644 --- a/src/store/managers/manager.py +++ b/src/store/managers/manager.py @@ -40,40 +40,38 @@ class Manager: return errors @abstractmethod - def final_run(self, game: Game) -> None: + def manager_logic(self, game: Game) -> None: """ Manager specific logic triggered by the run method * Implemented by final child classes - * Called by the run method, not used directly * May block its thread - * May raise retryable exceptions that will be be retried if possible + * May raise retryable exceptions that will trigger a retry if possible * May raise other exceptions that will be reported """ - def run(self, game: Game, callback: Callable[["Manager"], Any]) -> None: - """ - Pass the game through the manager - * Public method called by a pipeline - * In charge of calling the final_run method and handling its errors - """ - + def execute_resilient_manager_logic(self, game: Game) -> None: + """Execute the manager logic and handle its errors by reporting them or retrying""" for remaining_tries in range(self.max_tries, -1, -1): try: - self.final_run(game, self.max_tries) + self.manager_logic(game) except Exception as error: + # Handle unretryable errors + log_args = (type(error).__name__, self.name, game.game_id) if type(error) in self.retryable_on: - # Handle unretryable errors - logging.error("Unretryable error in %s", self.name, exc_info=error) + logging.error("Unretryable %s in %s for %s", *log_args) self.report_error(error) break + # Handle being out of retries elif remaining_tries == 0: - # Handle being out of retries - logging.error("Out of retries in %s", self.name, exc_info=error) + logging.error("Too many retries due to %s in %s for %s", *log_args) self.report_error(error) break + # Retry else: - # Retry - logging.debug("Retrying %s (%s)", self.name, type(error).__name__) + logging.debug("Retry caused by %s in %s for %s", *log_args) continue + def process_game(self, game: Game, callback: Callable[["Manager"], Any]) -> None: + """Pass the game through the manager""" + self.execute_resilient_manager_logic(game, tries=0) callback(self) diff --git a/src/store/managers/sgdb_manager.py b/src/store/managers/sgdb_manager.py index d2c1384..27ca8de 100644 --- a/src/store/managers/sgdb_manager.py +++ b/src/store/managers/sgdb_manager.py @@ -12,11 +12,11 @@ class SGDBManager(AsyncManager): run_after = set((SteamAPIManager,)) retryable_on = set((HTTPError,)) - def final_run(self, game: Game) -> None: + def manager_logic(self, game: Game) -> None: try: sgdb = SGDBHelper() sgdb.conditionaly_update_cover(game) - except SGDBAuthError as error: + except SGDBAuthError: # If invalid auth, cancel all SGDBManager tasks self.cancellable.cancel() - self.report_error(error) + raise diff --git a/src/store/managers/steam_api_manager.py b/src/store/managers/steam_api_manager.py index 15ad35b..3b319c0 100644 --- a/src/store/managers/steam_api_manager.py +++ b/src/store/managers/steam_api_manager.py @@ -13,7 +13,7 @@ class SteamAPIManager(AsyncManager): retryable_on = set((HTTPError,)) - def final_run(self, game: Game) -> None: + def manager_logic(self, game: Game) -> None: # Skip non-steam games if not game.source.startswith("steam_"): return diff --git a/src/store/pipeline.py b/src/store/pipeline.py index 3fe4976..4372709 100644 --- a/src/store/pipeline.py +++ b/src/store/pipeline.py @@ -61,7 +61,7 @@ class Pipeline(GObject.Object): for manager in (*parallel, *blocking): self.waiting.remove(manager) self.running.add(manager) - manager.run(self.game, self.manager_callback) + manager.process_game(self.game, self.manager_callback) def manager_callback(self, manager: Manager) -> None: """Method called by a manager when it's done"""