mirror of
https://github.com/yweber/lodel2.git
synced 2025-10-22 00:59:03 +02:00
Merge branch 'newlodel' of git.labocleo.org:lodel2 into newlodel
This commit is contained in:
commit
2a3dd5684a
8 changed files with 389 additions and 40 deletions
|
@ -10,3 +10,6 @@ init_db: dyncode
|
|||
|
||||
list_hooks: dyncode
|
||||
$(python) -c 'import lodel_admin; lodel_admin.list_registered_hooks()'
|
||||
|
||||
discover_plugins:
|
||||
$(python) -c 'import lodel_admin; lodel_admin.update_plugin_discover_cache()'
|
||||
|
|
|
@ -1,30 +1,41 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
##@brief Lodel2 loader script
|
||||
#
|
||||
#@note If you want to avoid settings loading you can set the environment
|
||||
#variable LODEL2_NO_SETTINGS_LOAD (see @ref install.lodel_admin.update_plugin_discover_cache()
|
||||
#
|
||||
|
||||
import sys, os, os.path
|
||||
#
|
||||
# Bootstraping
|
||||
#
|
||||
LODEL2_LIB_ABS_PATH = None
|
||||
if LODEL2_LIB_ABS_PATH is not None:
|
||||
if not os.path.isdir(LODEL2_LIB_ABS_PATH):
|
||||
print("FATAL ERROR : the LODEL2_LIB_ABS_PATH variable in loader.py is \
|
||||
not correct : '%s'" % LODEL2_LIB_ABS_PATH, file=sys.stderr)
|
||||
sys.path.append(os.path.dirname(LODEL2_LIB_ABS_PATH))
|
||||
|
||||
try:
|
||||
import lodel
|
||||
except ImportError:
|
||||
except ImportError as e:
|
||||
print("Unable to load lodel module. exiting...")
|
||||
print(e)
|
||||
exit(1)
|
||||
|
||||
#
|
||||
# Loading settings
|
||||
#
|
||||
from lodel.settings.settings import Settings as settings
|
||||
if not settings.started():
|
||||
settings('conf.d')
|
||||
from lodel.settings import Settings
|
||||
if 'LODEL2_NO_SETTINGS_LOAD' not in os.environ:
|
||||
#
|
||||
# Loading settings
|
||||
#
|
||||
from lodel.settings.settings import Settings as settings
|
||||
if not settings.started():
|
||||
settings('conf.d')
|
||||
from lodel.settings import Settings
|
||||
|
||||
#Starts hooks
|
||||
from lodel.plugin import LodelHook
|
||||
from lodel.plugin import core_hooks
|
||||
#Starts hooks
|
||||
from lodel.plugin import LodelHook
|
||||
from lodel.plugin import core_hooks
|
||||
|
||||
def start():
|
||||
#Load plugins
|
||||
|
|
|
@ -2,10 +2,16 @@
|
|||
|
||||
import sys
|
||||
import os, os.path
|
||||
import loader
|
||||
import argparse
|
||||
|
||||
from lodel.settings import Settings
|
||||
from lodel import logger
|
||||
"""
|
||||
#Dirty hack to solve symlinks problems :
|
||||
# When loader was imported the original one (LODEL_LIBDIR/install/loader)
|
||||
# because lodel_admin.py is a symlink from this folder
|
||||
#Another solution can be delete loader from install folder
|
||||
sys.path[0] = os.getcwd()
|
||||
import loader
|
||||
"""
|
||||
|
||||
## @brief Utility method to generate python code given an emfile and a
|
||||
# translator
|
||||
|
@ -27,6 +33,7 @@ def generate_dyncode(model_file, translator):
|
|||
# @param translator str : a translator name
|
||||
# @param output_filename str : the output file
|
||||
def create_dyncode(model_file, translator, output_filename):
|
||||
from lodel import logger
|
||||
dyncode = generate_dyncode(model_file, translator)
|
||||
with open(output_filename, 'w+') as out_fd:
|
||||
out_fd.write(dyncode)
|
||||
|
@ -36,6 +43,8 @@ def create_dyncode(model_file, translator, output_filename):
|
|||
|
||||
## @brief Refresh dynamic leapi code from settings
|
||||
def refresh_dyncode():
|
||||
import loader
|
||||
from lodel.settings import Settings
|
||||
# EditorialModel update/refresh
|
||||
|
||||
# TODO
|
||||
|
@ -120,3 +129,16 @@ def list_registered_hooks():
|
|||
print(msg)
|
||||
print("\n")
|
||||
|
||||
##@brief update plugin's discover cache
|
||||
#@note impossible to give arguments from a Makefile...
|
||||
#@todo write a __main__ to be able to run ./lodel_admin
|
||||
def update_plugin_discover_cache(path_list = None):
|
||||
os.environ['LODEL2_NO_SETTINGS_LOAD'] = 'True'
|
||||
import loader
|
||||
from lodel.plugin.plugins import Plugin
|
||||
res = Plugin.discover(path_list)
|
||||
print("Plugin discover result in %s :\n" % res['path_list'])
|
||||
for pname, pinfos in res['plugins'].items():
|
||||
print("\t- %s %s -> %s" % (
|
||||
pname, pinfos['version'], pinfos['path']))
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import sys
|
|||
import os.path
|
||||
import importlib
|
||||
import copy
|
||||
import json
|
||||
from importlib.machinery import SourceFileLoader, SourcelessFileLoader
|
||||
|
||||
import plugins
|
||||
|
@ -19,12 +20,131 @@ from .exceptions import *
|
|||
|
||||
##@brief The package in which we will load plugins modules
|
||||
VIRTUAL_PACKAGE_NAME = 'lodel.plugins'
|
||||
##@brief The temporary package to import python sources
|
||||
VIRTUAL_TEMP_PACKAGE_NAME = 'lodel.plugin_tmp'
|
||||
##@brief Plugin init filename
|
||||
INIT_FILENAME = '__init__.py' # Loaded with settings
|
||||
PLUGIN_NAME_VARNAME = '__plugin_name__'
|
||||
PLUGIN_TYPE_VARNAME = '__type__'
|
||||
PLUGIN_VERSION_VARNAME = '__version__'
|
||||
CONFSPEC_FILENAME_VARNAME = '__confspec__'
|
||||
CONFSPEC_VARNAME = 'CONFSPEC'
|
||||
LOADER_FILENAME_VARNAME = '__loader__'
|
||||
PLUGIN_DEPS_VARNAME = '__plugin_deps__'
|
||||
ACTIVATE_METHOD_NAME = '_activate'
|
||||
##@brief Discover stage cache filename
|
||||
DISCOVER_CACHE_FILENAME = '.plugin_discover_cache.json'
|
||||
##@brief Default & failover value for plugins path list
|
||||
DEFAULT_PLUGINS_PATH_LIST = ['./plugins']
|
||||
|
||||
MANDATORY_VARNAMES = [PLUGIN_NAME_VARNAME, LOADER_FILENAME_VARNAME,
|
||||
PLUGIN_VERSION_VARNAME]
|
||||
|
||||
PLUGIN_DEFAULT_TYPE = 'default'
|
||||
PLUGINS_TYPES = [PLUGIN_DEFAULT_TYPE, 'datasource', 'session_handler', 'ui']
|
||||
|
||||
|
||||
##@brief Describe and handle version numbers
|
||||
class PluginVersion(object):
|
||||
|
||||
PROPERTY_LIST = ['major', 'minor', 'revision' ]
|
||||
|
||||
##@brief Version constructor
|
||||
#@param *args : You can either give a str that will be splitted on . or you
|
||||
#can give a iterable containing 3 integer or 3 arguments representing
|
||||
#major, minor and revision version
|
||||
def __init__(self, *args):
|
||||
self.__version = [0 for _ in range(3) ]
|
||||
if len(args) == 1:
|
||||
arg = args[0]
|
||||
if isinstance(arg, str):
|
||||
spl = arg.split('.')
|
||||
invalid = False
|
||||
if len(spl) > 3:
|
||||
raise PluginError("The string '%s' is not a valid plugin \
|
||||
version number" % arg)
|
||||
else:
|
||||
try:
|
||||
if len(arg) >= 1:
|
||||
if len(arg) > 3:
|
||||
raise PluginError("Expected maximum 3 value to \
|
||||
create a plugin version number but found '%s' as argument" % arg)
|
||||
for i, v in enumerate(arg):
|
||||
self.__version[i] = arg[i]
|
||||
except TypeError:
|
||||
raise PluginError("Unable to convert argument into plugin \
|
||||
version number" % arg)
|
||||
elif len(args) > 3:
|
||||
raise PluginError("Expected between 1 and 3 positional arguments \
|
||||
but %d arguments found" % len(args))
|
||||
else:
|
||||
for i,v in enumerate(args):
|
||||
self.__version[i] = v
|
||||
|
||||
@property
|
||||
def major(self):
|
||||
return self.__version[0]
|
||||
|
||||
@property
|
||||
def minor(self):
|
||||
return self.__version[1]
|
||||
|
||||
@property
|
||||
def revision(self):
|
||||
return self.__version[2]
|
||||
|
||||
##@brief Check and prepare comparisoon argument
|
||||
#@return A PluginVersion instance
|
||||
#@throw PluginError if invalid argument provided
|
||||
def __cmp_check(self, other):
|
||||
if not isinstance(other, PluginVersion):
|
||||
try:
|
||||
if len(other) <= 3 and len(other) > 0:
|
||||
return PluginVersion(other)
|
||||
except TypeError:
|
||||
raise PluginError("Cannot compare argument '%s' with \
|
||||
a PluginVerison instance" % other)
|
||||
return other
|
||||
|
||||
##@brief Generic comparison function
|
||||
#@param other PluginVersion or iterable
|
||||
#@param cmp_fun_name function : interger comparison function
|
||||
def __generic_cmp(self, other, cmp_fun_name):
|
||||
other = self.__cmp_check(other)
|
||||
try:
|
||||
cmpfun = getattr(int, cmp_fun_name)
|
||||
except AttributeError:
|
||||
raise LodelFatalError("Invalid comparison callback given \
|
||||
to generic PluginVersion comparison function : '%s'" % cmp_fun_name)
|
||||
for property_name in self.PROPERTY_LIST:
|
||||
if not cmpfun(getattr(self, pname), getattr(other, pname)):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.__generic_cmp(other, '__lt__')
|
||||
|
||||
def __le__(self, other):
|
||||
return self.__generic_cmp(other, '__le__')
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__generic_cmp(other, '__eq__')
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.__generic_cmp(other, '__ne__')
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.__generic_cmp(other, '__gt__')
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.__generic_cmp(other, '__ge__')
|
||||
|
||||
def __str__(self):
|
||||
return '%d.%d.%d' % tuple(self.__version)
|
||||
|
||||
def __repr__(self):
|
||||
return {'major': self.major, 'minor': self.minor,
|
||||
'revision': self.revision}
|
||||
|
||||
|
||||
##@brief Handle plugins
|
||||
|
@ -48,6 +168,9 @@ class Plugin(object):
|
|||
#dependencies
|
||||
_load_called = []
|
||||
|
||||
##@brief Attribute that stores plugins list from discover cache file
|
||||
_plugin_list = None
|
||||
|
||||
##@brief Plugin class constructor
|
||||
#
|
||||
# Called by setting in early stage of lodel2 boot sequence using classmethod
|
||||
|
@ -67,11 +190,11 @@ class Plugin(object):
|
|||
self.__confspecs = dict()
|
||||
self.loaded = False
|
||||
|
||||
# Importing __init__.py
|
||||
# Importing __init__.py infos in it
|
||||
plugin_module = '%s.%s' % (VIRTUAL_PACKAGE_NAME,
|
||||
plugin_name)
|
||||
|
||||
init_source = self.path + INIT_FILENAME
|
||||
init_source = os.path.join(self.path, INIT_FILENAME)
|
||||
try:
|
||||
loader = SourceFileLoader(plugin_module, init_source)
|
||||
self.module = loader.load_module()
|
||||
|
@ -109,23 +232,39 @@ class Plugin(object):
|
|||
varname = CONFSPEC_VARNAME,
|
||||
filename = confspec_filename)
|
||||
raise PluginError(msg)
|
||||
# loading plugin version
|
||||
try:
|
||||
#this try block should be useless. The existance of
|
||||
#PLUGIN_VERSION_VARNAME in init file is mandatory
|
||||
self.__version = getattr(self.module, PLUGIN_VERSION_VARNAME)
|
||||
except AttributeError:
|
||||
msg = "Error that should not append : no %s found in plugin \
|
||||
init file. Malformed plugin"
|
||||
msg %= PLUGIN_VERSION_VARNAME
|
||||
raise LodelFatalError(msg)
|
||||
|
||||
##@brief Browse directory to get plugin
|
||||
#@param plugin_path
|
||||
#@return module existing
|
||||
def _discover_plugin(self, plugin_path):
|
||||
res = os.listdir(plugin_path) is not None
|
||||
if res:
|
||||
dirname = os.path.dirname(plugin_path)
|
||||
for f in os.listdir(plugin_path):
|
||||
file_name = ''.join(dirname, f)
|
||||
if self.is_plugin_dir(file_name):
|
||||
return self.is_plugin_dir(file_name)
|
||||
else:
|
||||
self._discover_plugin(file_name)
|
||||
else:
|
||||
pass
|
||||
|
||||
# Load plugin type
|
||||
try:
|
||||
self.__type = getattr(self.module, PLUGIN_TYPE_VARNAME)
|
||||
except AttributeError:
|
||||
self.__type = PLUGIN_DEFAULT_TYPE
|
||||
self.__type = str(self.__type).lower()
|
||||
if self.__type not in PLUGINS_TYPES:
|
||||
raise PluginError("Unknown plugin type '%s'" % self.__type)
|
||||
# Load plugin name from init file (just for checking)
|
||||
try:
|
||||
#this try block should be useless. The existance of
|
||||
#PLUGIN_NAME_VARNAME in init file is mandatory
|
||||
pname = getattr(self.module, PLUGIN_NAME_VARNAME)
|
||||
except AttributeError:
|
||||
msg = "Error that should not append : no %s found in plugin \
|
||||
init file. Malformed plugin"
|
||||
msg %= PLUGIN_NAME_VARNAME
|
||||
raise LodelFatalError(msg)
|
||||
if pname != plugin_name:
|
||||
msg = "Plugin's discover cache inconsistency detected ! Cached \
|
||||
name differ from the one found in plugin's init file"
|
||||
raise PluginError(msg)
|
||||
|
||||
##@brief Try to import a file from a variable in __init__.py
|
||||
#@param varname str : The variable name
|
||||
|
@ -153,7 +292,7 @@ class Plugin(object):
|
|||
raise PluginError(msg)
|
||||
# importing the file in varname
|
||||
module_name = self.module.__name__+"."+varname
|
||||
filename = self.path + filename
|
||||
filename = os.path.join(self.path, filename)
|
||||
loader = SourceFileLoader(module_name, filename)
|
||||
return loader.load_module()
|
||||
|
||||
|
@ -252,6 +391,9 @@ class Plugin(object):
|
|||
raise RuntimeError("Plugin %s not loaded yet."%self.name)
|
||||
return self.__loader_module
|
||||
|
||||
def __str__(self):
|
||||
return "<LodelPlugin '%s' version %s>" % (self.name, self.__version)
|
||||
|
||||
##@brief Call load method on every pre-loaded plugins
|
||||
#
|
||||
# Called by loader to trigger hooks registration.
|
||||
|
@ -317,18 +459,17 @@ class Plugin(object):
|
|||
@classmethod
|
||||
def plugin_path(cls, plugin_name):
|
||||
cls.started()
|
||||
plist = cls.plugin_list()
|
||||
if plugin_name not in plist:
|
||||
raise PluginError("No plugin named '%s' found" % plugin_name)
|
||||
|
||||
try:
|
||||
return cls.get(plugin_name).path
|
||||
except PluginError:
|
||||
pass
|
||||
|
||||
path = None
|
||||
for cur_path in cls._plugin_directories:
|
||||
plugin_path = os.path.join(cur_path, plugin_name)+'/'
|
||||
if os.path.isdir(plugin_path):
|
||||
return plugin_path
|
||||
raise NameError("No plugin named '%s'" % plugin_name)
|
||||
|
||||
return plist[plugin_name]['path']
|
||||
|
||||
@classmethod
|
||||
def plugin_module_name(cls, plugin_name):
|
||||
return "%s.%s" % (VIRTUAL_PACKAGE_NAME, plugin_name)
|
||||
|
@ -366,6 +507,166 @@ class Plugin(object):
|
|||
cls._plugin_instances = dict()
|
||||
if cls._load_called != []:
|
||||
cls._load_called = []
|
||||
|
||||
@classmethod
|
||||
##@brief Browse directory to get plugin
|
||||
#@param plugin_path
|
||||
#@return module existing
|
||||
def plugin_discover(self, plugin_path):
|
||||
res = os.listdir(plugin_path) is not None
|
||||
if res:
|
||||
dirname = os.path.dirname(plugin_path)
|
||||
for f in os.listdir(plugin_path):
|
||||
file_name = ''.join(dirname, f)
|
||||
if self.is_plugin_dir(file_name):
|
||||
return self.is_plugin_dir(file_name)
|
||||
else:
|
||||
self._discover_plugin(file_name)
|
||||
else:
|
||||
pass
|
||||
|
||||
##@brief Reccursively walk throught paths to find plugin, then stores
|
||||
#found plugin in a file...
|
||||
#@return a dict {'path_list': [...], 'plugins': { see @ref _discover }}
|
||||
@classmethod
|
||||
def discover(cls, paths = None):
|
||||
if paths is None:
|
||||
paths = DEFAULT_PLUGINS_PATH_LIST
|
||||
tmp_res = []
|
||||
for path in paths:
|
||||
tmp_res += cls._discover(path)
|
||||
#Formating and dedoubloning result
|
||||
result = dict()
|
||||
for pinfos in tmp_res:
|
||||
pname = pinfos['name']
|
||||
if (
|
||||
pname in result
|
||||
and pinfos['version'] > result[pname]['version'])\
|
||||
or pname not in result:
|
||||
result[pname] = pinfos
|
||||
else:
|
||||
#dropped
|
||||
pass
|
||||
result = {'path_list': paths, 'plugins': result}
|
||||
#Writing to cache
|
||||
with open(DISCOVER_CACHE_FILENAME, 'w+') as pdcache:
|
||||
pdcache.write(json.dumps(result))
|
||||
return result
|
||||
|
||||
##@brief Return discover result
|
||||
#@param refresh bool : if true invalidate all plugin list cache
|
||||
#@note If discover cache file not found run discover first
|
||||
#@note if refresh is set to True discover MUST have been run at least
|
||||
#one time. In fact refresh action load the list of path to explore
|
||||
#from the plugin's discover cache
|
||||
@classmethod
|
||||
def plugin_list(cls, refresh = False):
|
||||
try:
|
||||
infos = cls._load_discover_cache()
|
||||
path_list = infos['path_list']
|
||||
except PluginError:
|
||||
refresh = True
|
||||
path_list = DEFAULT_PLUGINS_PATH_LIST
|
||||
|
||||
if cls._plugin_list is None or refresh:
|
||||
if not os.path.isfile(DISCOVER_CACHE_FILENAME) or refresh:
|
||||
infos = cls.discover(path_list)
|
||||
cls._plugin_list = infos['plugins']
|
||||
return cls._plugin_list
|
||||
|
||||
##@brief Attempt to open and load plugin discover cache
|
||||
#@return discover cache
|
||||
#@throw PluginError when open or load fails
|
||||
@classmethod
|
||||
def _load_discover_cache(cls):
|
||||
try:
|
||||
pdcache = open(DISCOVER_CACHE_FILENAME, 'r')
|
||||
except Exception as e:
|
||||
msg = "Unable to open discover cache : %s"
|
||||
msg %= e
|
||||
raise PluginError(msg)
|
||||
try:
|
||||
res = json.load(pdcache)
|
||||
except Exception as e:
|
||||
msg = "Unable to load discover cache : %s"
|
||||
msg %= e
|
||||
raise PluginError(msg)
|
||||
pdcache.close()
|
||||
return res
|
||||
|
||||
##@brief Check if a directory is a plugin module
|
||||
#@param path str : path to check
|
||||
#@return a dict with name, version and path if path is a plugin module, else False
|
||||
@classmethod
|
||||
def dir_is_plugin(cls, path):
|
||||
#Checks that path exists
|
||||
if not os.path.isdir(path):
|
||||
raise ValueError(
|
||||
"Expected path to be a directory, but '%s' found" % path)
|
||||
#Checks that path contains plugin's init file
|
||||
initfile = os.path.join(path, INIT_FILENAME)
|
||||
if not os.path.isfile(initfile):
|
||||
return False
|
||||
#Importing plugin's init file to check contained datas
|
||||
try:
|
||||
initmod, modname = cls.import_init(path)
|
||||
except PluginError:
|
||||
return False
|
||||
#Checking mandatory init module variables
|
||||
for attr_name in MANDATORY_VARNAMES:
|
||||
if not hasattr(initmod,attr_name):
|
||||
return False
|
||||
try:
|
||||
pversion = getattr(initmod, PLUGIN_VERSION_VARNAME)
|
||||
except PluginError as e:
|
||||
msg = "Invalid plugin version found in %s : %s"
|
||||
msg %= (path, e)
|
||||
raise PluginError(msg)
|
||||
pname = getattr(initmod, PLUGIN_NAME_VARNAME)
|
||||
return {'name': pname,
|
||||
'version': pversion,
|
||||
'path': path}
|
||||
|
||||
##@brief Import init file from a plugin path
|
||||
#@param path str : Directory path
|
||||
#@return a tuple (init_module, module_name)
|
||||
@classmethod
|
||||
def import_init(self, path):
|
||||
init_source = os.path.join(path, INIT_FILENAME)
|
||||
temp_module = '%s.%s.%s' % (
|
||||
VIRTUAL_TEMP_PACKAGE_NAME, os.path.basename(os.path.dirname(path)),
|
||||
'test_init')
|
||||
try:
|
||||
loader = SourceFileLoader(temp_module, init_source)
|
||||
except (ImportError, FileNotFoundError) as e:
|
||||
raise PluginError("Unable to import init file from '%s' : %s" % (
|
||||
temp_module, e))
|
||||
try:
|
||||
res_module = loader.load_module()
|
||||
except Exception as e:
|
||||
raise PluginError("Unable to import initfile")
|
||||
return (res_module, temp_module)
|
||||
|
||||
##@brief Reccursiv plugin discover given a path
|
||||
#@param path str : the path to walk through
|
||||
#@return A dict with plugin_name as key and {'path':..., 'version':...} as value
|
||||
@classmethod
|
||||
def _discover(cls, path):
|
||||
res = []
|
||||
to_explore = [path]
|
||||
while len(to_explore) > 0:
|
||||
cur_path = to_explore.pop()
|
||||
for f in os.listdir(cur_path):
|
||||
f_path = os.path.join(cur_path, f)
|
||||
if f not in ['.', '..'] and os.path.isdir(f_path):
|
||||
#Check if it is a plugin directory
|
||||
test_result = cls.dir_is_plugin(f_path)
|
||||
if not (test_result is False):
|
||||
res.append(test_result)
|
||||
else:
|
||||
to_explore.append(f_path)
|
||||
return res
|
||||
|
||||
|
||||
##@brief Decorator class designed to allow plugins to add custom methods
|
||||
#to LeObject childs (dyncode objects)
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
from lodel.settings.validator import SettingValidator
|
||||
|
||||
__plugin_name__ = "dummy"
|
||||
__version__ = '0.0.1' #or __version__ = [0,0,1]
|
||||
__loader__ = "main.py"
|
||||
__confspec__ = "confspec.py"
|
||||
__author__ = "Lodel2 dev team"
|
||||
__fullname__ = "Dummy plugin"
|
||||
__name__ = 'yweber.dummy'
|
||||
|
||||
|
||||
##@brief This methods allow plugin writter to write some checks
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from lodel.settings.validator import SettingValidator
|
||||
from .datasource import DummyDatasource as Datasource
|
||||
|
||||
__plugin_name__ = "dummy_datasource"
|
||||
__version__ = '0.0.1'
|
||||
__loader__ = 'main.py'
|
||||
__plugin_deps__ = []
|
||||
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
__plugin_name__ = 'mongodb_datasource'
|
||||
__version__ = '0.0.1'
|
||||
__plugin_type__ = 'datasource'
|
||||
|
||||
__loader__ = "main.py"
|
||||
__confspec__ = "confspec.py"
|
||||
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
__plugin_name__ = 'webui'
|
||||
__version__ = '0.0.1'
|
||||
__type__ = 'ui'
|
||||
__loader__ = 'main.py'
|
||||
__confspec__ = 'confspec.py'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue