Add version system

Remove BaseClass verification
This commit is contained in:
fomys 2019-04-19 11:24:10 +02:00
parent 52bcb60e3a
commit 273d92ec5e
15 changed files with 275 additions and 35 deletions

View File

@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
discord = {extras = ["voice", "doc"], ref = "rewrite", git = "https://github.com/Rapptz/discord.py"}
packaging = "*" packaging = "*"
discord-py = {extras = ["voice"],git = "https://github.com/Rapptz/discord.py",ref = "84c1eac62a775a37b03bd0971b221b0c50724630"}
[dev-packages] [dev-packages]

28
errors.py Normal file
View File

@ -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

166
main.py
View File

@ -6,11 +6,142 @@ import logging
import logging.config import logging.config
import os import os
import traceback import traceback
from typing import Dict
import discord import discord
from packaging.version import Version
from errors import IncompatibleModule
from modules.base import BaseClass 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'): def setup_logging(default_path='config/log_config.json', default_level=logging.INFO, env_key='LBI_LOG_CONFIG'):
"""Setup logging configuration """Setup logging configuration
@ -63,6 +194,11 @@ error = log_LBI.error
critical = log_LBI.critical critical = log_LBI.critical
def load_modules_info():
for mod in os.listdir("modules"):
Module(mod)
class LBI(discord.Client): class LBI(discord.Client):
base_path = "storage" base_path = "storage"
debug = log_LBI.debug debug = log_LBI.debug
@ -113,11 +249,36 @@ class LBI(discord.Client):
@modules_edit @modules_edit
def load_module(self, module): 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: try:
info("Start loading module {module}...".format(module=module)) info("Start loading module {module}...".format(module=module))
imported = importlib.import_module('modules.' + module) imported = importlib.import_module('modules.' + module)
importlib.reload(imported) importlib.reload(imported)
if issubclass(imported.MainClass, BaseClass):
initialized_class = imported.MainClass(self) initialized_class = imported.MainClass(self)
self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}}) self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}})
info("Module {module} successfully imported.".format(module=module)) info("Module {module} successfully imported.".format(module=module))
@ -125,11 +286,10 @@ class LBI(discord.Client):
if module not in self.config["modules"]: if module not in self.config["modules"]:
self.config["modules"].append(module) self.config["modules"].append(module)
self.save_config() self.save_config()
else:
error("Module {module} isn't an instance of BaseClass.".format(module=module))
except AttributeError as e: except AttributeError as e:
error("Module {module} doesn't have MainClass.".format(module=module)) error("Module {module} doesn't have MainClass.".format(module=module))
return e return e
return 0
@modules_edit @modules_edit
def unload_module(self, module): def unload_module(self, module):

View File

@ -4,7 +4,6 @@ import pickle
import zipfile import zipfile
import discord import discord
from packaging import version
class Storage: class Storage:
@ -51,9 +50,6 @@ class BaseClass:
color = 0x000000 color = 0x000000
command_text = None command_text = None
authorized_roles = [] 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): def __init__(self, client):
"""Initialize module class """Initialize module class

10
modules/base/version.json Normal file
View File

@ -0,0 +1,10 @@
{
"version":"0.1.0",
"dependencies": {
},
"bot_version": {
"min": "0.1.0",
"max": "0.1.0"
}
}

View File

@ -1,8 +1,8 @@
import asyncio import asyncio
import collections
import random import random
import traceback import traceback
import collections
import discord import discord
from modules.base import BaseClass from modules.base import BaseClass

View File

@ -0,0 +1,10 @@
{
"version":"0.1.0",
"dependencies": {
"base":"0.1.0"
},
"bot_version": {
"min": "0.1.0",
"max": "0.1.0"
}
}

View File

@ -4,7 +4,7 @@ from modules.base import BaseClass
class MainClass(BaseClass): class MainClass(BaseClass):
name = "Git" name = "git"
super_users = [431043517217898496] super_users = [431043517217898496]

13
modules/git/version.json Normal file
View File

@ -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"
}
}

View File

@ -1,8 +1,5 @@
import discord import discord
from modules.base import BaseClass
import discord
from modules.base import BaseClass from modules.base import BaseClass

10
modules/help/version.json Normal file
View File

@ -0,0 +1,10 @@
{
"version":"0.1.0",
"dependencies": {
"base":"==0.1.0"
},
"bot_version": {
"min": "0.1.0",
"max": "0.1.0"
}
}

View File

@ -1,25 +1,26 @@
import os import os
import discord import discord
from aiohttp import ClientSession
from .base import BaseClass from modules.base import BaseClass
# todo: tout réécrire
class MainClass(BaseClass): class MainClass(BaseClass):
name = "modules" name = "modules"
super_users = [431043517217898496]
command_text = "modules"
color = 0x000000 color = 0x000000
help_active = True
help = { help = {
"description": "Manage bot modules.", "description": "Manage bot modules.",
"commands": { "commands": {
"`{prefix}{command} list`": "List of available modules.", "`{prefix}{command} list`": "List of available modules.",
"`{prefix}{command} enable <module>`": "Enable module `<module>`.", "`{prefix}{command} enable <module>`": "Enable module `<module>`.",
"`{prefix}{command} disable <module>`": "Disable module `<module>`.", "`{prefix}{command} disable <module>`": "Disable module `<module>`.",
"`{prefix}{command} web_list`": "List all available modules from repository",
} }
} }
command_text = "modules"
def __init__(self, client): def __init__(self, client):
super().__init__(client) super().__init__(client)
@ -89,13 +90,5 @@ class MainClass(BaseClass):
) )
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
async def com_download(self, message, args, kwargs): async def com_web_list(self, message, args, kwargs):
if len(args) == 3: pass
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())

View File

@ -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"
}
}

View File

@ -0,0 +1,10 @@
{
"version":"0.1.0",
"dependencies": {
"base":"==0.1.0"
},
"bot_version": {
"min": "0.1.0",
"max": "0.1.0"
}
}