From 273d92ec5e9981aaa58a9701d53dd430b69de72c Mon Sep 17 00:00:00 2001 From: fomys Date: Fri, 19 Apr 2019 11:24:10 +0200 Subject: [PATCH] Add version system Remove BaseClass verification --- Pipfile | 2 +- errors.py | 28 +++ main.py | 180 ++++++++++++++++++-- modules/{base.py => base/__init__.py} | 4 - modules/base/version.json | 10 ++ modules/{errors.py => errors/__init__.py} | 2 +- modules/errors/version.json | 10 ++ modules/{git.py => git/__init__.py} | 2 +- modules/git/version.json | 13 ++ modules/{help.py => help/__init__.py} | 3 - modules/help/version.json | 10 ++ modules/{modules.py => modules/__init__.py} | 23 +-- modules/modules/version.json | 13 ++ modules/{restart.py => restart/__init__.py} | 0 modules/restart/version.json | 10 ++ 15 files changed, 275 insertions(+), 35 deletions(-) create mode 100644 errors.py rename modules/{base.py => base/__init__.py} (99%) create mode 100644 modules/base/version.json rename modules/{errors.py => errors/__init__.py} (100%) create mode 100644 modules/errors/version.json rename modules/{git.py => git/__init__.py} (98%) create mode 100644 modules/git/version.json rename modules/{help.py => help/__init__.py} (96%) create mode 100644 modules/help/version.json rename modules/{modules.py => modules/__init__.py} (83%) create mode 100644 modules/modules/version.json rename modules/{restart.py => restart/__init__.py} (100%) create mode 100644 modules/restart/version.json diff --git a/Pipfile b/Pipfile index 382c847..6749cf4 100644 --- a/Pipfile +++ b/Pipfile @@ -4,8 +4,8 @@ verify_ssl = true name = "pypi" [packages] -discord = {extras = ["voice", "doc"], ref = "rewrite", git = "https://github.com/Rapptz/discord.py"} packaging = "*" +discord-py = {extras = ["voice"],git = "https://github.com/Rapptz/discord.py",ref = "84c1eac62a775a37b03bd0971b221b0c50724630"} [dev-packages] diff --git a/errors.py b/errors.py new file mode 100644 index 0000000..4684fb1 --- /dev/null +++ b/errors.py @@ -0,0 +1,28 @@ +class LBIException(Exception): + """ + Base exception class for LBI + + All other exceptions are subclasses + """ + pass + + +class ModuleException(LBIException): + """ + Base exception class for all module errors + """ + pass + + +class ModuleNotInstalled(ModuleException): + """ + Raised when a module is not found in module directory + """ + pass + + +class IncompatibleModule(ModuleException): + """ + Raised when a module is not compatible with bot version + """ + pass diff --git a/main.py b/main.py index a176e22..08f84b1 100755 --- a/main.py +++ b/main.py @@ -6,11 +6,142 @@ import logging import logging.config import os import traceback +from typing import Dict import discord +from packaging.version import Version +from errors import IncompatibleModule from modules.base import BaseClass +__version__ = "0.1.0" + + +class Module: + name: str + + def __init__(self, name: str): + """ + Init module + + :param name: Module name + :type name: str + """ + self.name = name + MODULES.update({self.name: self}) + + @property + def exists(self) -> bool: + """ + Check if module exists + + :return: True if module is present in modules folders + :rtype: bool + """ + if not os.path.isdir(os.path.join("modules", self.name)): + return False + return True + + @property + def complete(self) -> bool: + """ + Check if module is complete + + :return: True if module is compatible + :rtype: Boolean + """ + # Check if version.json exists + if not os.path.exists(os.path.join("modules", self.name, "version.json")): + return False + with open(os.path.join("modules", self.name, "version.json")) as file: + versions = json.load(file) + if "version" not in versions.keys(): + return False + if "dependencies" not in versions.keys(): + return False + if "bot_version" not in versions.keys(): + return False + return True + + @property + def version(self) -> Version: + """ + Returns module version + + :return: current module version + :rtype: Version + """ + with open(os.path.join("modules", self.name, "version.json")) as file: + versions = json.load(file) + return Version(versions["version"]) + + @property + def bot_version(self) -> dict: + """ + returns the min and max version of the bot that is compatible with the module + + :return: Min and max version for bot + :rtype: dict + :raises IncompatibleModule: If bot_version is not properly formated (there must be min and max keys) + """ + with open(os.path.join("modules", self.name, "version.json")) as file: + versions = json.load(file) + try: + return {"min": Version(versions["bot_version"]["min"]), + "max": Version(versions["bot_version"]["max"])} + except KeyError: + raise IncompatibleModule(f"Module {self.name} is not compatible with bot (version.json does not " + f"contain bot_version.max or bot_version.min item)") + + @property + def dependencies(self) -> dict: + """ + return list of dependencies version + + :return: list of dependencies version + :rtype: dict + :raises IncompatibleModule: If bot_version is not properly formated (there must be min and max keys for each + dependencies) + """ + with open(os.path.join("modules", self.name, "version.json")) as file: + versions = json.load(file) + try: + deps = {} + for name, dep in versions["dependencies"].items(): + dep_ver = {"min": Version(dep["min"]), + "max": Version(dep["max"])} + deps.update({name: dep_ver}) + return deps + except KeyError: + raise IncompatibleModule(f"Module {self.name} is not compatible with bot (version.json does not " + f"contain dependencies.modulename.max or dependencies.modulename.min item)") + + @property + def compatible(self) -> bool: + """ + Check if module is compatible with current installation + + :return: True if all dependencies are okays + :rtype: bool + """ + # Check bot version + bot_ver = Version(__version__) + if bot_ver < self.bot_version["min"]: + return False + if bot_ver > self.bot_version["max"]: + return False + for name, dep in self.dependencies.items(): + if name not in MODULES.keys(): + Module(name) + if MODULES[name].version < dep["min"]: + return False + if MODULES[name].version > dep["max"]: + return False + return True + + +MODULES: Dict[str, Module] = {} + def setup_logging(default_path='config/log_config.json', default_level=logging.INFO, env_key='LBI_LOG_CONFIG'): """Setup logging configuration @@ -63,6 +194,11 @@ error = log_LBI.error critical = log_LBI.critical +def load_modules_info(): + for mod in os.listdir("modules"): + Module(mod) + + class LBI(discord.Client): base_path = "storage" debug = log_LBI.debug @@ -113,23 +249,47 @@ class LBI(discord.Client): @modules_edit def load_module(self, module): + """ + + Status codes: + - 0: Module loaded + - 1: Module not in modules folder + - 2: Module incomplete + - 3: Module incompatible + + :param module: Module name + :return: Status code + """ + + # Check module compatibility + load_modules_info() + if not MODULES.get(module): + return 1 + if not MODULES[module].exists: + return 1 + if not MODULES[module].complete: + return 2 + if not MODULES[module].compatible: + return 3 + deps = MODULES[module].dependencies + for dep in deps.keys(): + if dep not in self.modules.keys(): + self.load_module(dep) try: info("Start loading module {module}...".format(module=module)) imported = importlib.import_module('modules.' + module) importlib.reload(imported) - if issubclass(imported.MainClass, BaseClass): - initialized_class = imported.MainClass(self) - self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}}) - info("Module {module} successfully imported.".format(module=module)) - initialized_class.on_load() - if module not in self.config["modules"]: - self.config["modules"].append(module) - self.save_config() - else: - error("Module {module} isn't an instance of BaseClass.".format(module=module)) + initialized_class = imported.MainClass(self) + self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}}) + info("Module {module} successfully imported.".format(module=module)) + initialized_class.on_load() + if module not in self.config["modules"]: + self.config["modules"].append(module) + self.save_config() except AttributeError as e: error("Module {module} doesn't have MainClass.".format(module=module)) return e + return 0 @modules_edit def unload_module(self, module): diff --git a/modules/base.py b/modules/base/__init__.py similarity index 99% rename from modules/base.py rename to modules/base/__init__.py index abc14eb..a388eab 100644 --- a/modules/base.py +++ b/modules/base/__init__.py @@ -4,7 +4,6 @@ import pickle import zipfile import discord -from packaging import version class Storage: @@ -51,9 +50,6 @@ class BaseClass: color = 0x000000 command_text = None authorized_roles = [] - version = version.parse("0.0.1") - min_base_version = version.parse("0.0.1") - max_base_version = "0.0.1" def __init__(self, client): """Initialize module class diff --git a/modules/base/version.json b/modules/base/version.json new file mode 100644 index 0000000..1d75473 --- /dev/null +++ b/modules/base/version.json @@ -0,0 +1,10 @@ +{ + "version":"0.1.0", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/errors.py b/modules/errors/__init__.py similarity index 100% rename from modules/errors.py rename to modules/errors/__init__.py index a91327b..c6eca44 100644 --- a/modules/errors.py +++ b/modules/errors/__init__.py @@ -1,8 +1,8 @@ import asyncio -import collections import random import traceback +import collections import discord from modules.base import BaseClass diff --git a/modules/errors/version.json b/modules/errors/version.json new file mode 100644 index 0000000..d835f04 --- /dev/null +++ b/modules/errors/version.json @@ -0,0 +1,10 @@ +{ + "version":"0.1.0", + "dependencies": { + "base":"0.1.0" + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/git.py b/modules/git/__init__.py similarity index 98% rename from modules/git.py rename to modules/git/__init__.py index 9c296aa..04c6e04 100644 --- a/modules/git.py +++ b/modules/git/__init__.py @@ -4,7 +4,7 @@ from modules.base import BaseClass class MainClass(BaseClass): - name = "Git" + name = "git" super_users = [431043517217898496] diff --git a/modules/git/version.json b/modules/git/version.json new file mode 100644 index 0000000..4f98c08 --- /dev/null +++ b/modules/git/version.json @@ -0,0 +1,13 @@ +{ + "version": "0.1.0", + "dependencies": { + "base": { + "min": "0.1.0", + "max": "0.1.0" + } + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/help.py b/modules/help/__init__.py similarity index 96% rename from modules/help.py rename to modules/help/__init__.py index 2632819..9f4e427 100644 --- a/modules/help.py +++ b/modules/help/__init__.py @@ -1,8 +1,5 @@ import discord -from modules.base import BaseClass -import discord - from modules.base import BaseClass diff --git a/modules/help/version.json b/modules/help/version.json new file mode 100644 index 0000000..2e85ead --- /dev/null +++ b/modules/help/version.json @@ -0,0 +1,10 @@ +{ + "version":"0.1.0", + "dependencies": { + "base":"==0.1.0" + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/modules.py b/modules/modules/__init__.py similarity index 83% rename from modules/modules.py rename to modules/modules/__init__.py index 9ed5c55..89211c6 100644 --- a/modules/modules.py +++ b/modules/modules/__init__.py @@ -1,25 +1,26 @@ import os import discord -from aiohttp import ClientSession -from .base import BaseClass +from modules.base import BaseClass +# todo: tout réécrire + class MainClass(BaseClass): name = "modules" - super_users = [431043517217898496] + + command_text = "modules" color = 0x000000 - help_active = True help = { "description": "Manage bot modules.", "commands": { "`{prefix}{command} list`": "List of available modules.", "`{prefix}{command} enable `": "Enable module ``.", "`{prefix}{command} disable `": "Disable module ``.", + "`{prefix}{command} web_list`": "List all available modules from repository", } } - command_text = "modules" def __init__(self, client): super().__init__(client) @@ -89,13 +90,5 @@ class MainClass(BaseClass): ) await message.channel.send(embed=embed) - async def com_download(self, message, args, kwargs): - if len(args) == 3: - async with ClientSession() as session: - async with session.get("http://127.0.0.1:5000/api/modules/"+args[1]+"/"+args[2]) as response: - if response.status != 200: - await message.channel.send(f"Erreur lors du téléchargement de {args[1]}") - return - with self.storage.open(os.path.join("modules", args[1]+".zip"), "wb") as file: - file.write(await response.read()) - + async def com_web_list(self, message, args, kwargs): + pass diff --git a/modules/modules/version.json b/modules/modules/version.json new file mode 100644 index 0000000..4f98c08 --- /dev/null +++ b/modules/modules/version.json @@ -0,0 +1,13 @@ +{ + "version": "0.1.0", + "dependencies": { + "base": { + "min": "0.1.0", + "max": "0.1.0" + } + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/restart.py b/modules/restart/__init__.py similarity index 100% rename from modules/restart.py rename to modules/restart/__init__.py diff --git a/modules/restart/version.json b/modules/restart/version.json new file mode 100644 index 0000000..2e85ead --- /dev/null +++ b/modules/restart/version.json @@ -0,0 +1,10 @@ +{ + "version":"0.1.0", + "dependencies": { + "base":"==0.1.0" + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file