🎨 SourceIterator can yield addtitional data
SourceIterator-s can yield a game and a tuple of additional data. This data will be passed to the Store, Pipeline and Managers.
This commit is contained in:
@@ -3,9 +3,9 @@ import logging
|
|||||||
from gi.repository import Adw, Gio, Gtk
|
from gi.repository import Adw, Gio, Gtk
|
||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
|
from src.game import Game
|
||||||
from src.utils.task import Task
|
from src.utils.task import Task
|
||||||
from src.store.pipeline import Pipeline
|
from src.store.pipeline import Pipeline
|
||||||
from src.store.managers.manager import Manager
|
|
||||||
from src.importer.sources.source import Source
|
from src.importer.sources.source import Source
|
||||||
|
|
||||||
|
|
||||||
@@ -92,6 +92,7 @@ class Importer:
|
|||||||
def source_task_thread_func(self, _task, _obj, data, _cancellable):
|
def source_task_thread_func(self, _task, _obj, data, _cancellable):
|
||||||
"""Source import task code"""
|
"""Source import task code"""
|
||||||
|
|
||||||
|
source: Source
|
||||||
source, *_rest = data
|
source, *_rest = data
|
||||||
|
|
||||||
# Early exit if not installed
|
# Early exit if not installed
|
||||||
@@ -107,20 +108,35 @@ class Importer:
|
|||||||
while True:
|
while True:
|
||||||
# Handle exceptions raised when iterating
|
# Handle exceptions raised when iterating
|
||||||
try:
|
try:
|
||||||
game = next(iterator)
|
iteration_result = next(iterator)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
break
|
break
|
||||||
except Exception as exception: # pylint: disable=broad-exception-caught
|
except Exception as exception: # pylint: disable=broad-exception-caught
|
||||||
logging.exception(
|
logging.exception(
|
||||||
msg=f"Exception in source {source.id}",
|
"Exception in source %s", source.id, exc_info=exception
|
||||||
exc_info=exception,
|
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if game is None:
|
|
||||||
|
# Handle the result depending on its type
|
||||||
|
if isinstance(iteration_result, Game):
|
||||||
|
game = iteration_result
|
||||||
|
additional_data = tuple()
|
||||||
|
elif isinstance(iteration_result, tuple):
|
||||||
|
game, additional_data = iteration_result
|
||||||
|
elif iteration_result is None:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Warn source implementers that an invalid type was produced
|
||||||
|
# Should not happen on production code
|
||||||
|
logging.warn(
|
||||||
|
"%s produced an invalid iteration return type %s",
|
||||||
|
source.id,
|
||||||
|
type(iteration_result),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Register game
|
# Register game
|
||||||
pipeline: Pipeline = shared.store.add_game(game)
|
pipeline: Pipeline = shared.store.add_game(game, additional_data)
|
||||||
if pipeline is not None:
|
if pipeline is not None:
|
||||||
logging.info("Imported %s (%s)", game.name, game.game_id)
|
logging.info("Imported %s (%s)", game.name, game.game_id)
|
||||||
pipeline.connect("advanced", self.pipeline_advanced_callback)
|
pipeline.connect("advanced", self.pipeline_advanced_callback)
|
||||||
|
|||||||
@@ -48,12 +48,9 @@ class ItchSourceIterator(SourceIterator):
|
|||||||
"game_id": self.source.game_id_format.format(game_id=row[0]),
|
"game_id": self.source.game_id_format.format(game_id=row[0]),
|
||||||
"executable": self.source.executable_format.format(cave_id=row[4]),
|
"executable": self.source.executable_format.format(cave_id=row[4]),
|
||||||
}
|
}
|
||||||
yield Game(values, allow_side_effects=False)
|
additional_data = (row[3], row[2])
|
||||||
|
game = Game(values, allow_side_effects=False)
|
||||||
# TODO pass image URIs to the pipeline somehow
|
yield (game, additional_data)
|
||||||
# - Add a reserved field to the Game object
|
|
||||||
# - Reconstruct those from the pipeline (we already have them)
|
|
||||||
# - Pass game and additional data to the pipeline separately (requires deep changes)
|
|
||||||
|
|
||||||
|
|
||||||
class ItchSource(Source):
|
class ItchSource(Source):
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ from abc import abstractmethod
|
|||||||
from collections.abc import Iterable, Iterator
|
from collections.abc import Iterable, Iterator
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generator, Optional
|
from typing import Generator, Any
|
||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.utils.decorators import replaced_by_path
|
from src.utils.decorators import replaced_by_path
|
||||||
|
|
||||||
|
# Type of the data returned by iterating on a Source
|
||||||
|
SourceIterationResult = None | Game | tuple[Game, tuple[Any]]
|
||||||
|
|
||||||
|
|
||||||
class SourceIterator(Iterator):
|
class SourceIterator(Iterator):
|
||||||
"""Data producer for a source of games"""
|
"""Data producer for a source of games"""
|
||||||
@@ -24,11 +27,11 @@ class SourceIterator(Iterator):
|
|||||||
def __iter__(self) -> "SourceIterator":
|
def __iter__(self) -> "SourceIterator":
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __next__(self) -> Optional[Game]:
|
def __next__(self) -> SourceIterationResult:
|
||||||
return next(self.generator)
|
return next(self.generator)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def generator_builder(self) -> Generator[Optional[Game], None, None]:
|
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
|
||||||
"""
|
"""
|
||||||
Method that returns a generator that produces games
|
Method that returns a generator that produces games
|
||||||
* Should be implemented as a generator method
|
* Should be implemented as a generator method
|
||||||
@@ -50,7 +53,6 @@ class Source(Iterable):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.available_on = set()
|
self.available_on = set()
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self) -> str:
|
def full_name(self) -> str:
|
||||||
"""The source's full name"""
|
"""The source's full name"""
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class CartridgesApplication(Adw.Application):
|
|||||||
for game_file in shared.games_dir.iterdir():
|
for game_file in shared.games_dir.iterdir():
|
||||||
data = json.load(game_file.open())
|
data = json.load(game_file.open())
|
||||||
game = Game(data, allow_side_effects=False)
|
game = Game(data, allow_side_effects=False)
|
||||||
shared.store.add_game(game)
|
shared.store.add_game(game, tuple())
|
||||||
|
|
||||||
def on_about_action(self, *_args):
|
def on_about_action(self, *_args):
|
||||||
about = Adw.AboutWindow(
|
about = Adw.AboutWindow(
|
||||||
|
|||||||
@@ -26,16 +26,18 @@ class AsyncManager(Manager):
|
|||||||
Already scheduled Tasks will no longer be cancellable."""
|
Already scheduled Tasks will no longer be cancellable."""
|
||||||
self.cancellable = Gio.Cancellable()
|
self.cancellable = Gio.Cancellable()
|
||||||
|
|
||||||
def process_game(self, game: Game, callback: Callable[["Manager"], Any]) -> None:
|
def process_game(
|
||||||
|
self, game: Game, additional_data: tuple, callback: Callable[["Manager"], Any]
|
||||||
|
) -> None:
|
||||||
"""Create a task to process the game in a separate thread"""
|
"""Create a task to process the game in a separate thread"""
|
||||||
task = Task.new(None, self.cancellable, self._task_callback, (callback,))
|
task = Task.new(None, self.cancellable, self._task_callback, (callback,))
|
||||||
task.set_task_data((game,))
|
task.set_task_data((game, additional_data))
|
||||||
task.run_in_thread(self._task_thread_func)
|
task.run_in_thread(self._task_thread_func)
|
||||||
|
|
||||||
def _task_thread_func(self, _task, _source_object, data, cancellable):
|
def _task_thread_func(self, _task, _source_object, data, cancellable):
|
||||||
"""Task thread entry point"""
|
"""Task thread entry point"""
|
||||||
game, *_rest = data
|
game, additional_data, *_rest = data
|
||||||
self.execute_resilient_manager_logic(game)
|
self.execute_resilient_manager_logic(game, additional_data)
|
||||||
|
|
||||||
def _task_callback(self, _source_object, _result, data):
|
def _task_callback(self, _source_object, _result, data):
|
||||||
"""Method run after the task is done"""
|
"""Method run after the task is done"""
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class DisplayManager(Manager):
|
|||||||
|
|
||||||
run_after = set((SteamAPIManager, SGDBManager))
|
run_after = set((SteamAPIManager, SGDBManager))
|
||||||
|
|
||||||
def manager_logic(self, game: Game) -> None:
|
def manager_logic(self, game: Game, _additional_data: tuple) -> None:
|
||||||
# TODO decouple a game from its widget
|
# TODO decouple a game from its widget
|
||||||
shared.win.games[game.game_id] = game
|
shared.win.games[game.game_id] = game
|
||||||
game.update()
|
game.update()
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ class FileManager(AsyncManager):
|
|||||||
|
|
||||||
run_after = set((SteamAPIManager,))
|
run_after = set((SteamAPIManager,))
|
||||||
|
|
||||||
def manager_logic(self, game: Game) -> None:
|
def manager_logic(self, game: Game, _additional_data: tuple) -> None:
|
||||||
game.save()
|
game.save()
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class Manager:
|
|||||||
self.errors_lock = Lock()
|
self.errors_lock = Lock()
|
||||||
|
|
||||||
def report_error(self, error: Exception):
|
def report_error(self, error: Exception):
|
||||||
"""Report an error that happened in Manager.run"""
|
"""Report an error that happened in Manager.process_game"""
|
||||||
with self.errors_lock:
|
with self.errors_lock:
|
||||||
self.errors.append(error)
|
self.errors.append(error)
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ class Manager:
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def manager_logic(self, game: Game) -> None:
|
def manager_logic(self, game: Game, additional_data: tuple) -> None:
|
||||||
"""
|
"""
|
||||||
Manager specific logic triggered by the run method
|
Manager specific logic triggered by the run method
|
||||||
* Implemented by final child classes
|
* Implemented by final child classes
|
||||||
@@ -58,10 +58,12 @@ class Manager:
|
|||||||
* May raise other exceptions that will be reported
|
* May raise other exceptions that will be reported
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def execute_resilient_manager_logic(self, game: Game, try_index: int = 0) -> None:
|
def execute_resilient_manager_logic(
|
||||||
|
self, game: Game, additional_data: tuple, try_index: int = 0
|
||||||
|
) -> None:
|
||||||
"""Execute the manager logic and handle its errors by reporting them or retrying"""
|
"""Execute the manager logic and handle its errors by reporting them or retrying"""
|
||||||
try:
|
try:
|
||||||
self.manager_logic(game)
|
self.manager_logic(game, additional_data)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
if error in self.continue_on:
|
if error in self.continue_on:
|
||||||
# Handle skippable errors (skip silently)
|
# Handle skippable errors (skip silently)
|
||||||
@@ -71,7 +73,9 @@ class Manager:
|
|||||||
# Handle retryable errors
|
# Handle retryable errors
|
||||||
logging_format = "Retrying %s in %s for %s"
|
logging_format = "Retrying %s in %s for %s"
|
||||||
sleep(self.retry_delay)
|
sleep(self.retry_delay)
|
||||||
self.execute_resilient_manager_logic(game, try_index + 1)
|
self.execute_resilient_manager_logic(
|
||||||
|
game, additional_data, try_index + 1
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Handle being out of retries
|
# Handle being out of retries
|
||||||
logging_format = "Out of retries dues to %s in %s for %s"
|
logging_format = "Out of retries dues to %s in %s for %s"
|
||||||
@@ -88,7 +92,9 @@ class Manager:
|
|||||||
f"{game.name} ({game.game_id})",
|
f"{game.name} ({game.game_id})",
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_game(self, game: Game, callback: Callable[["Manager"], Any]) -> None:
|
def process_game(
|
||||||
|
self, game: Game, additional_data: tuple, callback: Callable[["Manager"], Any]
|
||||||
|
) -> None:
|
||||||
"""Pass the game through the manager"""
|
"""Pass the game through the manager"""
|
||||||
self.execute_resilient_manager_logic(game)
|
self.execute_resilient_manager_logic(game, additional_data)
|
||||||
callback(self)
|
callback(self)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from urllib3.exceptions import SSLError
|
||||||
|
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.store.managers.async_manager import AsyncManager
|
from src.store.managers.async_manager import AsyncManager
|
||||||
from src.store.managers.steam_api_manager import SteamAPIManager
|
from src.store.managers.steam_api_manager import SteamAPIManager
|
||||||
@@ -8,9 +10,9 @@ class SGDBManager(AsyncManager):
|
|||||||
"""Manager in charge of downloading a game's cover from steamgriddb"""
|
"""Manager in charge of downloading a game's cover from steamgriddb"""
|
||||||
|
|
||||||
run_after = set((SteamAPIManager,))
|
run_after = set((SteamAPIManager,))
|
||||||
retryable_on = set((HTTPError,))
|
retryable_on = set((HTTPError, SSLError))
|
||||||
|
|
||||||
def manager_logic(self, game: Game) -> None:
|
def manager_logic(self, game: Game, _additional_data: tuple) -> None:
|
||||||
try:
|
try:
|
||||||
sgdb = SGDBHelper()
|
sgdb = SGDBHelper()
|
||||||
sgdb.conditionaly_update_cover(game)
|
sgdb.conditionaly_update_cover(game)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class SteamAPIManager(AsyncManager):
|
|||||||
|
|
||||||
retryable_on = set((HTTPError, SSLError))
|
retryable_on = set((HTTPError, SSLError))
|
||||||
|
|
||||||
def manager_logic(self, game: Game) -> None:
|
def manager_logic(self, game: Game, _additional_data: tuple) -> None:
|
||||||
# Skip non-steam games
|
# Skip non-steam games
|
||||||
if not game.source.startswith("steam_"):
|
if not game.source.startswith("steam_"):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -11,14 +11,18 @@ class Pipeline(GObject.Object):
|
|||||||
"""Class representing a set of managers for a game"""
|
"""Class representing a set of managers for a game"""
|
||||||
|
|
||||||
game: Game
|
game: Game
|
||||||
|
additional_data: tuple
|
||||||
|
|
||||||
waiting: set[Manager]
|
waiting: set[Manager]
|
||||||
running: set[Manager]
|
running: set[Manager]
|
||||||
done: set[Manager]
|
done: set[Manager]
|
||||||
|
|
||||||
def __init__(self, game: Game, managers: Iterable[Manager]) -> None:
|
def __init__(
|
||||||
|
self, game: Game, additional_data: tuple, managers: Iterable[Manager]
|
||||||
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.game = game
|
self.game = game
|
||||||
|
self.additional_data = additional_data
|
||||||
self.waiting = set(managers)
|
self.waiting = set(managers)
|
||||||
self.running = set()
|
self.running = set()
|
||||||
self.done = set()
|
self.done = set()
|
||||||
@@ -72,7 +76,7 @@ class Pipeline(GObject.Object):
|
|||||||
for manager in (*parallel, *blocking):
|
for manager in (*parallel, *blocking):
|
||||||
self.waiting.remove(manager)
|
self.waiting.remove(manager)
|
||||||
self.running.add(manager)
|
self.running.add(manager)
|
||||||
manager.process_game(self.game, self.manager_callback)
|
manager.process_game(self.game, self.additional_data, self.manager_callback)
|
||||||
|
|
||||||
def manager_callback(self, manager: Manager) -> None:
|
def manager_callback(self, manager: Manager) -> None:
|
||||||
"""Method called by a manager when it's done"""
|
"""Method called by a manager when it's done"""
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ 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) -> Pipeline | None:
|
def add_game(
|
||||||
|
self, game: Game, additional_data: tuple, replace=False
|
||||||
|
) -> Pipeline | None:
|
||||||
"""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
|
||||||
@@ -49,7 +51,7 @@ class Store:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Run the pipeline for the game
|
# Run the pipeline for the game
|
||||||
pipeline = Pipeline(game, self.managers)
|
pipeline = Pipeline(game, additional_data, self.managers)
|
||||||
self.games[game.game_id] = game
|
self.games[game.game_id] = game
|
||||||
self.pipelines[game.game_id] = pipeline
|
self.pipelines[game.game_id] = pipeline
|
||||||
pipeline.advance()
|
pipeline.advance()
|
||||||
|
|||||||
Reference in New Issue
Block a user