Repository: Senth/minecraft-mod-manager
Pull request #28 (2021-04-21 09:16:18)

Various release fixes
### What changed?
- Now only downloads from the first site it founds the mod on
- Improved logging when during install/update

### Testing
- [X] Added unit tests




Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c2f205..daf9c50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [1.0.2] - 2021-04-21
+
+### Added
+
+- Improved logging for when installing and updating. Had been removed in application restructure.
+
+### Changed
+
+- Now only downloads from the first site it finds, will make it download updates from all sites later down the line.
+
+### Fixed
+
+- Missing .com in Modrinth url
+- Mod didn't do anything when running
+- Crash when running
+- Fixed crash when CurseForge supplied a date with missing milliseconds
+
 ## [1.0.1] - 2021-04-20
 
 ### Fixed
diff --git a/fixtures/curse_api/files_carpet.json b/fixtures/curse_api/files_carpet.json
index baf2632..302db52 100644
--- a/fixtures/curse_api/files_carpet.json
+++ b/fixtures/curse_api/files_carpet.json
@@ -160,7 +160,7 @@
     "id": 3276130,
     "displayName": "Carpet Mod v1.4.32 for 1.17",
     "fileName": "fabric-carpet-21w15a-1.4.32+v210414.jar",
-    "fileDate": "2021-04-14T18:34:39.417Z",
+    "fileDate": "2021-04-14T18:34:39Z",
     "fileLength": 1263266,
     "releaseType": 1,
     "fileStatus": 4,
diff --git a/minecraft_mod_manager/__main__.py b/minecraft_mod_manager/__main__.py
index bb15e1a..630f3af 100644
--- a/minecraft_mod_manager/__main__.py
+++ b/minecraft_mod_manager/__main__.py
@@ -4,6 +4,7 @@ from .app.install.install import Install
 from .app.show.show import Show
 from .app.update.update import Update
 from .config import config
+from .core.entities.actions import Actions
 from .gateways.arg_parser import parse_args
 from .gateways.downloader import Downloader
 from .gateways.jar_parser import JarParser
@@ -19,19 +20,19 @@ def main():
     downloader = Downloader()
     repo = RepoImpl(jar_parser, sqlite, downloader)
     try:
-        if config.action == "update":
+        if config.action == Actions.update:
             update = Update(repo)
             update.execute(config.arg_mods)
 
-        elif config.action == "install":
+        elif config.action == Actions.install:
             install = Install(repo)
             install.execute(config.arg_mods)
 
-        elif config.action == "configure":
+        elif config.action == Actions.configure:
             configure = Configure(repo)
             configure.execute(config.arg_mods)
 
-        elif config.action == "list":
+        elif config.action == Actions.list:
             show = Show(repo)
             show.execute()
     finally:
diff --git a/minecraft_mod_manager/adapters/repo_impl.py b/minecraft_mod_manager/adapters/repo_impl.py
index b5566b5..a850bb6 100644
--- a/minecraft_mod_manager/adapters/repo_impl.py
+++ b/minecraft_mod_manager/adapters/repo_impl.py
@@ -15,6 +15,7 @@ from ..gateways.api.modrinth_api import ModrinthApi
 from ..gateways.downloader import Downloader
 from ..gateways.jar_parser import JarParser
 from ..gateways.sqlite import Sqlite
+from ..utils.logger import LogColors, Logger
 
 
 class RepoImpl(ConfigureRepo, UpdateRepo, InstallRepo, ShowRepo):
@@ -43,42 +44,41 @@ class RepoImpl(ConfigureRepo, UpdateRepo, InstallRepo, ShowRepo):
     def get_all_mods(self) -> Sequence[Mod]:
         return self.mods
 
-    def get_latest_version(self, mod: Mod) -> VersionInfo:
+    def get_latest_version(self, mod: Mod) -> Union[VersionInfo, None]:
         versions: List[VersionInfo] = []
 
-        find_count = 0
-        site_last = Sites.unknown
-
         # Modrinth
         if mod.site == Sites.modrinth or mod.site == Sites.unknown:
             try:
+                Logger.verbose("🔍 Searching on Modrinth...", indent=1)
                 versions.extend(self.modrinth_api.get_all_versions(mod))
-                site_last = Sites.modrinth
-                find_count += 1
+                mod.site = Sites.modrinth
+                RepoImpl._print_found()
             except ModNotFoundException:
-                pass
+                RepoImpl._print_not_found()
 
         # Curse
         if mod.site == Sites.curse or mod.site == Sites.unknown:
             try:
+                Logger.verbose("🔍 Searching on CurseForge...", indent=1)
                 versions.extend(self.curse_api.get_all_versions(mod))
-                site_last = Sites.curse
-                find_count += 1
+                mod.site = Sites.curse
+                RepoImpl._print_found()
             except ModNotFoundException:
-                pass
-
-        # Only set site if found in one place
-        if find_count == 1:
-            mod.site = site_last
-
-        version_info: Union[VersionInfo, None] = None
-        if len(versions) > 0:
-            version_info = LatestVersionFinder.find_latest_version(mod, versions)
+                RepoImpl._print_not_found()
 
-        if version_info:
-            return version_info
-        else:
+        if len(versions) == 0:
             raise ModNotFoundException(mod)
 
+        return LatestVersionFinder.find_latest_version(mod, versions)
+
     def download(self, url: str, filename: str = "") -> Path:
         return Path(self.downloader.download(url, filename))
+
+    @staticmethod
+    def _print_found():
+        Logger.verbose("Found", LogColors.green, indent=2)
+
+    @staticmethod
+    def _print_not_found():
+        Logger.verbose("Not found", LogColors.yellow, indent=2)
diff --git a/minecraft_mod_manager/app/download/download.py b/minecraft_mod_manager/app/download/download.py
index b6f404a..fe36106 100644
--- a/minecraft_mod_manager/app/download/download.py
+++ b/minecraft_mod_manager/app/download/download.py
@@ -26,24 +26,30 @@ class Download:
     def __init__(self, repo: DownloadRepo, success_prefix: str):
         self._repo = repo
         self._success_prefix = success_prefix
-        self._name_width_max = 0
 
     def find_download_and_install(self, mods: Sequence[Mod]) -> None:
-        mods_to_install = self._find_latest_versions(mods)
-        self._download_and_install(mods_to_install)
-
-    def _find_latest_versions(self, mods: Sequence[Mod]) -> Sequence[DownloadInfo]:
-        mods_to_install: List[DownloadInfo] = []
         mods_not_found: List[ModNotFoundException] = []
 
         # Find latest version of mod
         for mod in mods:
             try:
+                Logger.info(mod.id, LogColors.bold)
                 latest_version = self._repo.get_latest_version(mod)
-                download_info = DownloadInfo(mod, latest_version)
-                mods_to_install.append(download_info)
+
+                if latest_version:
+                    download_info = DownloadInfo(mod, latest_version)
+                    Logger.verbose("⬇ Downloading...", indent=1)
+                    self._download_and_install(download_info)
+                    Logger.info(
+                        f"🟢 {self._success_prefix} -> {download_info.version_info.filename}",
+                        LogColors.green,
+                        indent=1,
+                    )
+                else:
+                    Logger.verbose("🟨 No new version found", LogColors.skip, indent=1)
 
             except ModNotFoundException as exception:
+                Logger.error("🔺 Mod not found on any site...", indent=1)
                 mods_not_found.append(exception)
 
         # Print errors
@@ -51,29 +57,11 @@ class Download:
             errorMessage = ""
             for error in mods_not_found:
                 errorMessage += str(error) + "\n\n"
-            Logger.error(errorMessage, exit=True)
-
-        return mods_to_install
-
-    def _download_and_install(self, downloads: Sequence[DownloadInfo]):
-        # Check max name width
-        for download_info in downloads:
-            name_width = len(download_info.name)
-            if name_width > self._name_width_max:
-                self._name_width_max = name_width
-
-        # Download
-        for download_info in downloads:
-            downloaded_mod = self._download(download_info.mod, download_info.version_info)
-            self._repo.update_mod(downloaded_mod)
-            self._print_installed(download_info)
-
-    def _print_installed(self, download_info: DownloadInfo) -> None:
-        name = download_info.name.ljust(self._name_width_max)
-        Logger.info(
-            f"{self._success_prefix} {name} -> {download_info.version_info.name}",
-            LogColors.green,
-        )
+            Logger.error(errorMessage)
+
+    def _download_and_install(self, download_info: DownloadInfo) -> None:
+        downloaded_mod = self._download(download_info.mod, download_info.version_info)
+        self._repo.update_mod(downloaded_mod)
 
     def _download(self, mod: ModArg, latest_version: VersionInfo) -> Mod:
         downloaded_file = self._repo.download(latest_version.download_url, latest_version.filename)
diff --git a/minecraft_mod_manager/config.py b/minecraft_mod_manager/config.py
index 1af7d9d..ae60e51 100644
--- a/minecraft_mod_manager/config.py
+++ b/minecraft_mod_manager/config.py
@@ -1,9 +1,8 @@
 from pathlib import Path
 from typing import Any, List, Literal, Union
 
-from minecraft_mod_manager.core.entities.mod_loaders import ModLoaders
-
 from .core.entities.mod import ModArg
+from .core.entities.mod_loaders import ModLoaders
 from .core.entities.version_info import Stabilities
 
 _app_name = __package__.replace("_", "-")
@@ -36,7 +35,7 @@ class Config:
             self.verbose = True
 
         if args.dir:
-            self.dir = args.dir
+            self.dir = Path(args.dir)
 
         if args.minecraft_version:
             self.filter.version = args.minecraft_version
diff --git a/minecraft_mod_manager/core/entities/mod.py b/minecraft_mod_manager/core/entities/mod.py
index 7e24e7a..304334f 100644
--- a/minecraft_mod_manager/core/entities/mod.py
+++ b/minecraft_mod_manager/core/entities/mod.py
@@ -68,7 +68,7 @@ class Mod(ModArg):
         return Mod(mod_arg.id, mod_arg.id, site=mod_arg.site, site_slug=mod_arg.site_slug)
 
     def __str__(self) -> str:
-        return f"{self.id}-{self.version} ({self.name}) [{self.mod_loader}]"
+        return f"{self.id}-{self.version} ({self.name}) [{self.mod_loader.value}]"
 
     def __repr__(self) -> str:
         return str(self.__members())
diff --git a/minecraft_mod_manager/gateways/api/api.py b/minecraft_mod_manager/gateways/api/api.py
index 650cd82..32d9990 100644
--- a/minecraft_mod_manager/gateways/api/api.py
+++ b/minecraft_mod_manager/gateways/api/api.py
@@ -32,7 +32,16 @@ class Api:
 
     @staticmethod
     def _to_epoch_time(date_string: str) -> int:
-        date = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S.%f%z")
+        # Has milliseconds
+        date = 0
+        try:
+            date = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S.%f%z")
+        except ValueError:
+            pass
+
+        if date == 0:
+            date = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S%z")
+
         return round(date.timestamp())
 
     @staticmethod
diff --git a/minecraft_mod_manager/gateways/api/modrinth_api.py b/minecraft_mod_manager/gateways/api/modrinth_api.py
index aae4ee2..fdf5a27 100644
--- a/minecraft_mod_manager/gateways/api/modrinth_api.py
+++ b/minecraft_mod_manager/gateways/api/modrinth_api.py
@@ -8,7 +8,7 @@ from ...core.entities.version_info import Stabilities, VersionInfo
 from ...gateways.downloader import Downloader
 from .api import Api
 
-_base_url = "https://api.modrinth/api/v1"
+_base_url = "https://api.modrinth.com/api/v1"
 
 
 class ModrinthApi(Api):
diff --git a/minecraft_mod_manager/gateways/arg_parser.py b/minecraft_mod_manager/gateways/arg_parser.py
index ef50331..be4ae2b 100644
--- a/minecraft_mod_manager/gateways/arg_parser.py
+++ b/minecraft_mod_manager/gateways/arg_parser.py
@@ -72,7 +72,7 @@ def parse_args():
 
     args = parser.parse_args()
     args.action = Actions.from_name(args.action)
-    args.mods = _parse_mods(args)
+    args.mods = _parse_mods(args.mods)
     return args
 
 
diff --git a/minecraft_mod_manager/gateways/downloader.py b/minecraft_mod_manager/gateways/downloader.py
index 107c923..09f2f6b 100644
--- a/minecraft_mod_manager/gateways/downloader.py
+++ b/minecraft_mod_manager/gateways/downloader.py
@@ -7,7 +7,6 @@ from requests.models import Response
 
 # from .. import web_driver
 from ..config import config
-from ..utils.logger import Logger
 
 _user_agent = (
     "user-agent=Mozilla/5.0 (Windows NT 6.1; Win64; x64) "
@@ -26,7 +25,6 @@ class Downloader:
         Returns:
             Filename of the downloaded and saved file
         """
-        Logger.verbose("Downloading...")
 
         if config.pretend:
             return filename
@@ -45,8 +43,6 @@ class Downloader:
             with open(filename, "wb") as file:
                 file.write(response.content)
 
-            Logger.verbose("Download finished")
-
         return filename
 
     @staticmethod
diff --git a/minecraft_mod_manager/gateways/jar_parser.py b/minecraft_mod_manager/gateways/jar_parser.py
index fd36b4d..5da3572 100644
--- a/minecraft_mod_manager/gateways/jar_parser.py
+++ b/minecraft_mod_manager/gateways/jar_parser.py
@@ -87,4 +87,4 @@ class JarParser:
 
     @staticmethod
     def _log_found_mod(mod: Mod) -> None:
-        Logger.verbose(f"Found {mod.mod_loader} mod: {mod}", LogColors.found)
+        Logger.verbose(f"Found {mod.mod_loader.value} mod: {mod}", LogColors.found)
diff --git a/minecraft_mod_manager/gateways/sqlite.py b/minecraft_mod_manager/gateways/sqlite.py
index 90914e6..7305020 100644
--- a/minecraft_mod_manager/gateways/sqlite.py
+++ b/minecraft_mod_manager/gateways/sqlite.py
@@ -2,7 +2,7 @@ import sqlite3
 from os import path
 from typing import Dict, List
 
-from minecraft_mod_manager.gateways.sqlite_upgrader import SqliteUpgrader
+from ..gateways.sqlite_upgrader import SqliteUpgrader
 
 from ..config import config
 from ..core.entities.mod import Mod
diff --git a/minecraft_mod_manager/utils/logger.py b/minecraft_mod_manager/utils/logger.py
index a4a6cda..9b1226b 100644
--- a/minecraft_mod_manager/utils/logger.py
+++ b/minecraft_mod_manager/utils/logger.py
@@ -1,6 +1,6 @@
 import sys
 
-import minecraft_mod_manager.config
+from ..config import config
 
 
 class LogColors:
@@ -23,59 +23,59 @@ class LogColors:
 
 class Logger:
     @staticmethod
-    def error(message: str, exit: bool = False):
+    def error(message: str, indent: int = 0, exit: bool = False):
         """Logs a message and prints is at red
 
         Args:
             message (str): The message to log
             exit (bool): If the program should exit after printing the error
         """
-        print(f"{LogColors.red}{message}{LogColors.no_color}")
+        Logger._print(message, LogColors.red, indent)
         if exit:
             sys.exit(1)
 
     @staticmethod
-    def warning(message: str):
+    def warning(message: str, indent: int = 0):
         """Logs a message and prints it as orange"""
-        print(f"{LogColors.orange}{message}{LogColors.no_color}")
+        Logger._print(message, LogColors.orange, indent)
 
     @staticmethod
-    def info(message: str, color: str = LogColors.no_color):
+    def info(message: str, color: str = LogColors.no_color, indent: int = 0):
         """Print an information message that always is shown
 
         Args:
             message (str): The message to log
             color (LogColors): Optional color of the message
         """
-        if color == LogColors.no_color:
-            print(message)
-        else:
-            print(f"{color}{message}{LogColors.no_color}")
+        Logger._print(message, color, indent)
 
     @staticmethod
-    def verbose(message: str, color: str = LogColors.no_color):
+    def verbose(message: str, color: str = LogColors.no_color, indent: int = 0):
         """Log message if verbose has been set to true
 
         Args:
             message (str): The message to log
             color (LogColors): Optional color of the message
         """
-        if minecraft_mod_manager.config.config.verbose:
-            if color == LogColors.no_color:
-                print(message)
-            else:
-                print(f"{color}{message}{LogColors.no_color}")
+        if config.verbose:
+            Logger._print(message, color, indent)
 
     @staticmethod
-    def debug(message: str, color: str = LogColors.no_color):
+    def debug(message: str, color: str = LogColors.no_color, indent: int = 0):
         """A debug message if --debug has been set to true
 
         Args:
             message (str): The message to log
             color (LogColors): Optional color of the message
         """
-        if minecraft_mod_manager.config.config.debug:
-            if color == LogColors.no_color:
-                print(message)
-            else:
-                print(f"{color}{message}{LogColors.no_color}")
+        if config.debug:
+            Logger._print(message, color, indent)
+
+    @staticmethod
+    def _print(message: str, color: str, indent: int):
+        if indent > 0:
+            message = "".ljust(indent * 4) + message
+        if color == LogColors.no_color:
+            print(message)
+        else:
+            print(f"{color}{message}{LogColors.no_color}")


Test patch:
diff --git a/minecraft_mod_manager/adapters/repo_impl_test.py b/minecraft_mod_manager/adapters/repo_impl_test.py
index e7fe9b8..747e6e4 100644
--- a/minecraft_mod_manager/adapters/repo_impl_test.py
+++ b/minecraft_mod_manager/adapters/repo_impl_test.py
@@ -45,7 +45,7 @@ class TestGetLatestVersion:
         self,
         name: str,
         mod: Mod,
-        expected_result: Union[VersionInfo, type],
+        expected_result: Union[VersionInfo, type, None],
         expected_mod: Mod,
         curse_api_returns: Union[List[VersionInfo], ModNotFoundException, None] = None,
         modrinth_api_returns: Union[List[VersionInfo], ModNotFoundException, None] = None,
@@ -83,7 +83,6 @@ def version(site: Sites = Sites.unknown) -> VersionInfo:
                 name="Get mod from Modrinth API when site type is unspecified",
                 mod=mod(),
                 modrinth_api_returns=[version(site=Sites.modrinth)],
-                curse_api_returns=ModNotFoundException(mod()),
                 version_finder_returns=version(site=Sites.modrinth),
                 expected_result=version(site=Sites.modrinth),
                 expected_mod=mod(site=Sites.modrinth),
@@ -120,17 +119,18 @@ def version(site: Sites = Sites.unknown) -> VersionInfo:
                 expected_mod=mod(site=Sites.curse),
             )
         ),
-        (
-            TestGetLatestVersion(
-                name="Don't specify mod site when returning mods from all sites",
-                mod=mod(),
-                modrinth_api_returns=[version(site=Sites.modrinth)],
-                curse_api_returns=[version(site=Sites.curse)],
-                version_finder_returns=version(site=Sites.curse),
-                expected_result=version(site=Sites.curse),
-                expected_mod=mod(),
-            )
-        ),
+        # FIXME Ability to download from both sites
+        # (
+        #     TestGetLatestVersion(
+        #         name="Don't specify mod site when returning mods from all sites",
+        #         mod=mod(),
+        #         modrinth_api_returns=[version(site=Sites.modrinth)],
+        #         curse_api_returns=[version(site=Sites.curse)],
+        #         version_finder_returns=version(site=Sites.curse),
+        #         expected_result=version(site=Sites.curse),
+        #         expected_mod=mod(),
+        #     )
+        # ),
         (
             TestGetLatestVersion(
                 name="Mod not found in any API",
@@ -148,7 +148,7 @@ def version(site: Sites = Sites.unknown) -> VersionInfo:
                 modrinth_api_returns=ModNotFoundException(mod()),
                 curse_api_returns=[version(site=Sites.curse)],
                 version_finder_returns="None",
-                expected_result=ModNotFoundException,
+                expected_result=None,
                 expected_mod=mod(site=Sites.curse),
             )
         ),
diff --git a/minecraft_mod_manager/app/download/download_test.py b/minecraft_mod_manager/app/download/download_test.py
index e382f83..e291c71 100644
--- a/minecraft_mod_manager/app/download/download_test.py
+++ b/minecraft_mod_manager/app/download/download_test.py
@@ -7,7 +7,6 @@ from ...core.entities.mod import Mod
 from ...core.entities.mod_loaders import ModLoaders
 from ...core.entities.sites import Sites
 from ...core.entities.version_info import Stabilities, VersionInfo
-from ...core.errors.mod_not_found_exception import ModNotFoundException
 from .download import Download
 from .download_repo import DownloadRepo
 
@@ -17,49 +16,11 @@ def mock_repo():
     return mock(DownloadRepo)
 
 
-def test_exit_when_mod_not_found(mock_repo):
-    input = [Mod("", "")]
-    when(mock_repo).get_latest_version(...).thenRaise(ModNotFoundException(input[0]))
-
-    download = Download(mock_repo, "")
-    with pytest.raises(SystemExit) as e:
-        download.find_download_and_install(input)
-
-    verifyStubbedInvocationsAreUsed()
-    unstub()
-    assert e.type == SystemExit
-
-
-def test_exit_when_later_mod_not_found(mock_repo):
-    input = [
-        Mod("found", ""),
-        Mod("not-found", ""),
-    ]
-    version_info = VersionInfo(
-        stability=Stabilities.beta,
-        mod_loaders=ModLoaders.fabric,
-        site=Sites.curse,
-        upload_time=0,
-        minecraft_versions=[],
-        download_url="",
-    )
-    when(mock_repo).get_latest_version(input[0]).thenReturn(version_info)
-    when(mock_repo).get_latest_version(input[1]).thenRaise(ModNotFoundException(input[0]))
-
-    download = Download(mock_repo, "")
-    with pytest.raises(SystemExit) as e:
-        download.find_download_and_install(input)
-
-    verifyStubbedInvocationsAreUsed()
-    unstub()
-    assert e.type == SystemExit
-
-
 def test_download_and_install_when_found(mock_repo):
     input = [Mod("found", "")]
     version_info = VersionInfo(
         stability=Stabilities.beta,
-        mod_loaders=ModLoaders.fabric,
+        mod_loaders=set([ModLoaders.fabric]),
         site=Sites.curse,
         upload_time=0,
         minecraft_versions=[],
diff --git a/minecraft_mod_manager/app/install/install_test.py b/minecraft_mod_manager/app/install/install_test.py
index 2efdb80..d0464fc 100644
--- a/minecraft_mod_manager/app/install/install_test.py
+++ b/minecraft_mod_manager/app/install/install_test.py
@@ -1,5 +1,5 @@
 import pytest
-from minecraft_mod_manager.core.entities.mod_loaders import ModLoaders
+from ...core.entities.mod_loaders import ModLoaders
 from mockito import mock, unstub, verifyStubbedInvocationsAreUsed, when
 
 from ...core.entities.mod import Mod, ModArg
diff --git a/minecraft_mod_manager/gateways/api/api_test.py b/minecraft_mod_manager/gateways/api/api_test.py
index 1949893..afe03f5 100644
--- a/minecraft_mod_manager/gateways/api/api_test.py
+++ b/minecraft_mod_manager/gateways/api/api_test.py
@@ -1,11 +1,9 @@
 import pytest
-from minecraft_mod_manager.core.errors.mod_not_found_exception import (
-    ModNotFoundException,
-)
-from minecraft_mod_manager.gateways.downloader import Downloader
 from mockito import mock, unstub, verifyStubbedInvocationsAreUsed, when
 
 from ...core.entities.mod import Mod
+from ...core.errors.mod_not_found_exception import ModNotFoundException
+from ...gateways.downloader import Downloader
 from .api import Api
 
 
diff --git a/minecraft_mod_manager/gateways/sqlite_test.py b/minecraft_mod_manager/gateways/sqlite_test.py
index 569693c..f8f3325 100644
--- a/minecraft_mod_manager/gateways/sqlite_test.py
+++ b/minecraft_mod_manager/gateways/sqlite_test.py
@@ -3,8 +3,8 @@ import sqlite3
 from typing import Any, List, Tuple, Union
 
 import pytest
-from minecraft_mod_manager.core.entities.sites import Sites
-from minecraft_mod_manager.core.errors.mod_already_exists import ModAlreadyExists
+from ..core.entities.sites import Sites
+from ..core.errors.mod_already_exists import ModAlreadyExists
 
 from ..config import config
 from ..core.entities.mod import Mod