🚧 More work on managers

This commit is contained in:
GeoffreyCoulaud
2023-05-23 15:26:48 +02:00
parent 9ea2e9652d
commit abe41635fd
4 changed files with 130 additions and 43 deletions

View File

@@ -30,19 +30,20 @@ gi.require_version("Adw", "1")
from gi.repository import Adw, Gio, GLib, Gtk from gi.repository import Adw, Gio, GLib, Gtk
import src.shared as shared import src.shared as shared
from src.store.store import Store
from src.details_window import DetailsWindow from src.details_window import DetailsWindow
from src.importer.importer import Importer from src.importer.importer import Importer
from src.importer.sources.lutris_source import ( from src.importer.sources.lutris_source import LutrisFlatpakSource, LutrisNativeSource
LutrisFlatpakSource,
LutrisNativeSource,
)
from src.importer.sources.steam_source import ( from src.importer.sources.steam_source import (
SteamNativeSource,
SteamFlatpakSource, SteamFlatpakSource,
SteamNativeSource,
SteamWindowsSource, SteamWindowsSource,
) )
from src.preferences import PreferencesWindow from src.preferences import PreferencesWindow
from src.store.display_manager import DisplayManager
from src.store.file_manager import FileManager
from src.store.sgdb_manager import SGDBManager
from src.store.steam_api_manager import SteamAPIManager
from src.store.store import Store
from src.window import CartridgesWindow from src.window import CartridgesWindow
@@ -56,10 +57,13 @@ class CartridgesApplication(Adw.Application):
) )
def do_activate(self): # pylint: disable=arguments-differ def do_activate(self): # pylint: disable=arguments-differ
# Create the games store # Create the games store and its managers
if not self.store: if not self.store:
# TODO add managers to the store
self.store = Store() self.store = Store()
self.store.add_manager(SteamAPIManager())
self.store.add_manager(SGDBManager())
self.store.add_manager(FileManager())
self.store.add_manager(DisplayManager())
# Create the main window # Create the main window
self.win = self.props.active_window # pylint: disable=no-member self.win = self.props.active_window # pylint: disable=no-member

View File

@@ -1,14 +1,46 @@
from abc import abstractmethod from abc import abstractmethod
from gi.repository import Gio
from src.game import Game from src.game import Game
class Manager: class Manager:
"""Class in charge of handling a post creation action for games. """Class in charge of handling a post creation action for games.
May connect to signals on the game to handle them."""
* May connect to signals on the game to handle them.
* May cancel its running tasks on critical error,
in that case a new cancellable must be generated for new tasks to run.
"""
run_after: set[type["Manager"]] run_after: set[type["Manager"]]
cancellable: Gio.Cancellable
errors: list[Exception]
def __init__(self) -> None:
super().__init__()
self.cancellable = Gio.Cancellable()
self.errors = list()
def cancel_tasks(self):
"""Cancel all tasks for this manager"""
self.cancellable.cancel()
def reset_cancellable(self):
"""Reset the cancellable for this manager.
Alreadyn scheduled Tasks will no longer be cancellable."""
self.cancellable = Gio.Cancellable()
def report_error(self, error: Exception):
"""Report an error that happened in of run"""
self.errors.append(error)
def collect_errors(self) -> list[Exception]:
"""Get the errors produced by the manager and remove them from self.errors"""
errors = list(self.errors)
self.errors.clear()
return errors
@abstractmethod @abstractmethod
def run(self, game: Game) -> None: def run(self, game: Game) -> None:
"""Pass the game through the manager. """Pass the game through the manager.

View File

@@ -1,11 +1,22 @@
from src.store.manager import Manager from requests import HTTPError
from src.game import Game from src.game import Game
from src.utils.steamgriddb import SGDBHelper from src.store.manager import Manager
from src.utils.steamgriddb import SGDBAuthError, SGDBError, SGDBHelper
class SGDBManager(Manager): class SGDBManager(Manager):
"""Manager in charge of downloading a game's cover from steamgriddb""" """Manager in charge of downloading a game's cover from steamgriddb"""
def run(self, game: Game) -> None: def run(self, game: Game) -> None:
# TODO try:
sgdb = SGDBHelper()
sgdb.conditionaly_update_cover(game)
except SGDBAuthError as error:
# If invalid auth, cancel all SGDBManager tasks
self.cancellable.cancel()
self.report_error(error)
except (HTTPError, SGDBError) as error:
# On other error, just report it
self.report_error(error)
pass pass

View File

@@ -1,18 +1,39 @@
from typing import Iterable
from gi.repository import GObject
import src.shared as shared import src.shared as shared
from src.game import Game from src.game import Game
from src.store.manager import Manager from src.store.manager import Manager
from src.utils.task import Task from src.utils.task import Task
class Pipeline(set): class Pipeline(GObject.Object):
"""Class representing a set of Managers for a game""" """Class representing a set of managers for a game"""
game: Game
waiting: set[Manager]
running: set[Manager]
done: set[Manager]
def __init__(self, managers: Iterable[Manager]) -> None:
super().__init__()
self.waiting = set(managers)
self.running = set()
self.done = set()
@property @property
def blocked_managers(self) -> set(Manager): def not_done(self) -> set[Manager]:
"""Get the managers that are not done yet"""
return self.waiting + self.running
@property
def blocked(self) -> set[Manager]:
"""Get the managers that cannot run because their dependencies aren't done""" """Get the managers that cannot run because their dependencies aren't done"""
blocked = set() blocked = set()
for manager_a in self: for manager_a in self.waiting:
for manager_b in self: for manager_b in self.not_done:
if manager_a == manager_b: if manager_a == manager_b:
continue continue
if type(manager_b) in manager_a.run_after: if type(manager_b) in manager_a.run_after:
@@ -20,9 +41,43 @@ class Pipeline(set):
return blocked return blocked
@property @property
def startable_managers(self) -> set(Manager): def ready(self) -> set[Manager]:
"""Get the managers that can be run""" """Get the managers that can be run"""
return self - self.blocked_managers return self.waiting - self.blocked
def advance(self):
"""Spawn tasks for managers that are able to run for a game"""
for manager in self.ready:
self.waiting.remove(manager)
self.running.add(manager)
data = (manager,)
task = Task.new(self, manager.cancellable, self.manager_task_callback, data)
task.set_task_data(data)
task.run_in_thread(self.manager_task_thread_func)
@GObject.Signal(name="manager-started")
def manager_started(self, manager: Manager) -> None:
"""Signal emitted when a manager is started"""
pass
def manager_task_thread_func(self, _task, _source_object, data, cancellable):
"""Thread function for manager tasks"""
manager, *_rest = data
self.emit("manager-started", manager)
manager.run(self.game, cancellable)
@GObject.Signal(name="manager-done")
def manager_done(self, manager: Manager) -> None:
"""Signal emitted when a manager is done"""
pass
def manager_task_callback(self, _source_object, _result, data):
"""Callback function for manager tasks"""
manager, *_rest = data
self.running.remove(manager)
self.done.add(manager)
self.emit("manager-done", manager)
self.advance()
class Store: class Store:
@@ -42,35 +97,20 @@ class Store:
"""Add a manager class that will run when games are added""" """Add a manager class that will run when games are added"""
self.managers.add(manager) self.managers.add(manager)
def add_game(self, game: Game, replace=False): def add_game(self, game: Game, replace=False) -> Pipeline:
"""Add a game to the app if not already there """Add a game to the app if not already there
:param replace bool: Replace the game if it already exists""" :param replace bool: Replace the game if it already exists
:return:
"""
if ( if (
game.game_id in self.games game.game_id in self.games
and not self.games[game.game_id].removed and not self.games[game.game_id].removed
and not replace and not replace
): ):
return return
pipeline = Pipeline(self.managers)
self.games[game.game_id] = game self.games[game.game_id] = game
self.pipelines[game.game_id] = Pipeline(self.managers) self.pipelines[game.game_id] = pipeline
self.advance_pipeline(game) pipeline.advance()
return pipeline
def advance_pipeline(self, game: Game):
"""Spawn tasks for managers that are able to run for a game"""
for manager in self.pipelines[game.game_id].startable_managers:
data = (manager, game)
task = Task.new(None, None, self.manager_task_callback, data)
task.set_task_data(data)
task.run_in_thread(self.manager_task_thread_func)
def manager_task_thread_func(self, _task, _source_object, data, _cancellable):
"""Thread function for manager tasks"""
manager, game, *_rest = data
manager.run(game)
def manager_task_callback(self, _source_object, _result, data):
"""Callback function for manager tasks"""
manager, game, *_rest = data
self.pipelines[game.game_id].remove(manager)
self.advance_pipeline(game)