From 763787ea02b43da4d6ef5bfe001fdc0a6ff70c56 Mon Sep 17 00:00:00 2001 From: Steve Nyemba Date: Tue, 1 Oct 2024 11:59:12 -0500 Subject: [PATCH] bug fix: plugin functions, streamline cli runner --- bin/qcms | 24 +++++-- cms/__init__.py | 110 +++++++++++++++++++++++++++++++++ cms/engine/plugins/__init__.py | 8 +++ cms/index.py | 81 +++++++----------------- meta/__init__.py | 2 +- 5 files changed, 161 insertions(+), 64 deletions(-) diff --git a/bin/qcms b/bin/qcms index 9d1da44..f274c5c 100755 --- a/bin/qcms +++ b/bin/qcms @@ -43,6 +43,13 @@ INVALID_FOLDER = """ # handling cli interface cli = typer.Typer() +def get_manifest (manifest): + if not manifest.endswith('json') and os.path.isdir(manifest): + manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1]) + return os.sep.join([manifest,'qcms-manifest.json']) + else: + return manifest + @cli.command(name="info") def _info(): @@ -174,6 +181,7 @@ def plug_info (manifest:Annotated[str,typer.Argument(help="path to manifest file """ Manage plugins list loaded plugins, """ + manifest = get_manifest(manifest) _config = config.get(manifest) _root = os.sep.join(manifest.split(os.sep)[:-1] + [_config['layout']['root'],'_plugins']) if os.path.exists(_root) : @@ -185,13 +193,15 @@ def plug_info (manifest:Annotated[str,typer.Argument(help="path to manifest file if not _plugins : _msg = f"""{FAILED} no plugins are loaded\n\t{manifest}""" else: - _data = plugins.stats(_plugins) + # _data = plugins.stats(_plugins) + _data = cms.Plugin.stats(_plugins) print (_data) _msg = f"""{PASSED} found a total of {_data.loaded.sum()} plugins loaded from {_data.shape[0]} file(s)\n\t{_root}""" if add in [True,False] and pointer : file,fnName = pointer.split('.') - _fnpointer = plugins.load(_root,file+'.py',fnName) + # _fnpointer = plugins.load(_root,file+'.py',fnName) + _fnpointer = cms.Plugin.load(_root,file+'.py',fnName) if _fnpointer and add: if file not in _plugins : _plugins[file] = [] @@ -269,7 +279,7 @@ def reload ( """ Reload a site/portal given the manifest ... """ - _config = config.get(path) + _config = config.get( get_manifest(path)) if 'key' in _config['system']['source'] : f = open(_config['system']['source']['key']) key = f.read() @@ -293,9 +303,10 @@ def bootup ( """ This function will launch a site/project given the location of the manifest file """ - if not manifest.endswith('json') and os.path.isdir(manifest): - manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1]) - manifest = os.sep.join([manifest,'qcms-manifest.json']) + # if not manifest.endswith('json') and os.path.isdir(manifest): + # manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1]) + # manifest = os.sep.join([manifest,'qcms-manifest.json']) + manifest = get_manifest(manifest) index.start(manifest,port) @cli.command(name='theme') def handle_theme ( @@ -307,6 +318,7 @@ def handle_theme ( This function will show the list available themes and can also set a theme in a manifest """ + manifest = get_manifest(manifest) _config = config.get(manifest) _root = os.sep.join( manifest.split(os.sep)[:-1]+[_config['layout']['root']]) diff --git a/cms/__init__.py b/cms/__init__.py index 5fe6631..1f00a43 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -6,8 +6,118 @@ import copy from jinja2 import Environment, BaseLoader, FileSystemLoader import importlib import importlib.util +import json +class Plugin : + # + # decorator for plugin functions, this is a preliminary to enable future developement + # + def __init__(self,**_args): + self._mimetype = _args['mimetype'] + if 'method' in _args : + _method = _args['method'] + if type(_method) != list : + _method = [_method] + else: + _method = ['POST','GET'] + self._method = _method + def __call__(self,_callback): + def wrapper(**_args): + return _callback(**_args) + setattr(wrapper,'method',self._method) + setattr(wrapper,'mimetype',self._mimetype) + return wrapper + @staticmethod + def load(_path,_filename,_name) : + """ + This function will load external module form a given location and return a pointer to a function in a given module + :path absolute path of the file (considered plugin) to be loaded + :name name of the function to be applied + """ + # _path = _args['path'] #os.sep.join([_args['root'],'plugin']) + + if os.path.isdir(_path): + files = os.listdir(_path) + if files : + files = [name for name in files if name.endswith('.py') and name == _filename] + if files: + _path = os.sep.join([_path,files[0]]) + else: + return None + else: + return None + #-- We have a file ... + # _name = _args['name'] + spec = importlib.util.spec_from_file_location(_filename, _path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + # + # we need to make sure we have the plugin decorator here to make sure + + return getattr(module,_name) if hasattr(module,_name) else None + + @staticmethod + def stats (_config) : + """ + Returns the statistics of the plugins + """ + _data = [] + for _name in _config : + _log = {"files":_name,"loaded":len(_config[_name]),"logs":json.dumps(_config[_name])} + _data.append(_log) + return pd.DataFrame(_data) + @staticmethod + def call(**_args): + """ + This function will execute a plugged-in function given + """ + # _uri = _args['uri'] if 'uri' in _args else '/'.join([_args['module'],_args['name']]) + _handler= _args['handler'] #-- handler + _request= _args['request'] + _uri = _args['uri'] #_request.path[1:] + # + # we need to update the _uri (if context/embeded apps) + # + _context = _handler.system()['context'] + if _context : + _uri = f'{_context}/{_uri}' + _plugins = _handler.plugins() + _code = 200 + if _uri in _plugins : + _config = _handler.config() #_args['config'] + + + _pointer = _plugins[_uri] + if hasattr(_pointer,'mimetype') and _request.method in _pointer.method: + # + # we constraint the methods given their execution ... + _mimeType = _pointer.mimetype + _data = _pointer(request=_request,config=_config) + else: + _mimeType = 'application/octet-stream' + try: + _data,_mimeType = _pointer(request=_request,config=_config) + + except Exception as e: + + _data = _pointer(request=_request,config=_config) + + pass + else: + _code = 404 + # + # We should generate a 500 error in this case with a message ... + # + _mimeType = 'plain/html' + _data = f""" + + """ + + return _data,_code,{'Content-Type':_mimeType} + pass # def _get_config (path) : # if os.path.exists(path) : diff --git a/cms/engine/plugins/__init__.py b/cms/engine/plugins/__init__.py index 6ada10b..86858f0 100644 --- a/cms/engine/plugins/__init__.py +++ b/cms/engine/plugins/__init__.py @@ -4,6 +4,11 @@ import importlib import importlib.util import os +# +# Defining the decorator to be used in plugins, this will enable simple loading and assigning mimetype to the output (if any) +# + + def stats (_config) : """ Returns the statistics of the plugins @@ -37,6 +42,9 @@ def load(_path,_filename,_name) : spec = importlib.util.spec_from_file_location(_filename, _path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) + # + # we need to make sure we have the plugin decorator here to make sure + return getattr(module,_name) if hasattr(module,_name) else None diff --git a/cms/index.py b/cms/index.py index dfc1abd..7d4f4cd 100644 --- a/cms/index.py +++ b/cms/index.py @@ -7,6 +7,7 @@ import flask import transport from transport import providers import cms +from cms import Plugin import sys import os import json @@ -144,28 +145,16 @@ def _dialog (app): _args['title'] = _id return render_template('dialog.html',**_args) #title=_id,html=_html) -@_app.route("/api//",defaults={'app':'main','key':None}) -@_app.route("//api//",defaults={'key':None}) -@_app.route("////",defaults={'key':None}) +@_app.route("/api//",defaults={'app':'main','key':None},methods=['GET','POST','DELETE','PUT']) +@_app.route("//api//",defaults={'key':None},methods=['GET','POST','DELETE','PUT']) +@_app.route("////",defaults={'key':None},methods=['GET','POST','DELETE','PUT']) def _delegate_call(app,key,module,name): _handler = _getHandler(app,key) - return _delegate(_handler,module,name) - -# @_app.route('/api//') -@_app.route("///api//", methods=['GET']) -@_app.route("/api//",defaults={'app_id':'main','key':None}) -@_app.route("//api//",defaults={'key':None}) - -def _api(app_id,key,module,name) : - """ - This endpoint will load a module and make a function call - :_module entry specified in plugins of the configuration - :_name name of the function to execute - """ - - _handler = _getHandler( app_id,key) - return _delegate(_handler,module,name) + # print (_handler.config()/) + uri = f'api/{module}/{name}' + return Plugin.call(uri=uri,handler=_handler,request=request) + # return _delegate(_handler,module,name) def _delegate(_handler,module,name): global _route @@ -186,10 +175,13 @@ def _delegate(_handler,module,name): # _data = pointer (**_args) # else: # _data = pointer() + if hasattr(pointer,'mimetype') : + _data = pointer(request=request,config=_handler.config()) + _mimeType = pointer.mimetype + else: + _data,_mimeType = pointer(request=request,config=_handler.config()) - _data,_mimeType = pointer(request=request,config=_handler.config()) - - _mimeType = 'application/octet-stream' if not _mimeType else _mimeType + _mimeType = 'application/octet-stream' if not _mimeType else _mimeType if type(_data) == pd.DataFrame : _data = _data.to_dict(orient='records') if type(_data) == list: @@ -201,18 +193,18 @@ def _delegate(_handler,module,name): # @_app.route('//api//',methods=['POST'],defaults={'key':None}) # @_app.route('///api//',methods=['POST'],defaults={'app_id':'main','key':None}) -@_app.route("///api//", methods=['POST']) -@_app.route("/api//",defaults={'app_id':'main','key':None},methods=['POST']) -@_app.route("//api//",defaults={'key':None},methods=['POST']) +# @_app.route("///api//", methods=['POST']) +# @_app.route("/api//",defaults={'app_id':'main','key':None},methods=['POST']) +# @_app.route("//api//",defaults={'key':None},methods=['POST']) -def _post (app_id,key,module,name): - # global _config - # global _route - # _handler = _route.get() - # app_id = '/'.join([app_id,key]) if key else app_id +# def _post (app_id,key,module,name): +# # global _config +# # global _route +# # _handler = _route.get() +# # app_id = '/'.join([app_id,key]) if key else app_id - _handler = _getHandler(app_id,key) - return _delegate(_handler,module,name) +# _handler = _getHandler(app_id,key) +# return _delegate(_handler,module,name) @_app.route('/version') def _version (): @@ -310,31 +302,6 @@ def _cms_page (app_id,resource): _args = _route.render(_uri,_title,session.get(app_id,'main')) return _args[_title],200 -# @_app.route('/set/') -# def set(id): -# global _route -# _setHandler(id) -# # _route.set(id) -# # _handler = _route.get() -# _handler = _getHandler() -# _context = _handler.system()['context'] -# _uri = f'/{_context}'.replace('//','/') -# return redirect(_uri) -# @_app.route('/') -# def _open(id): -# global _route -# # _handler = _route.get() - -# _handler = _getHandler() -# if id not in _route._apps : - -# _args = {'config':_handler.config(), 'layout':_handler.layout(),'system':_handler.system(skip=['source','app'])} -# return render_template("404.html",**_args) -# else: -# _setHandler(id) -# # _route.set(id) -# return _index() - @cli.command() def start ( diff --git a/meta/__init__.py b/meta/__init__.py index 5a70ba9..c3e7398 100644 --- a/meta/__init__.py +++ b/meta/__init__.py @@ -1,5 +1,5 @@ __author__ = "Steve L. Nyemba" -__version__= "2.1.6" +__version__= "2.2.0" __email__ = "steve@the-phi.com" __license__=""" Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center