bug fix: added plugin handler and improved delegation calls

pull/17/head
Steve Nyemba 2 months ago
parent 8a0f793c01
commit 685dfd3130

@ -21,11 +21,14 @@ import cms
from cms import index
from cms.engine.config.structure import Layout, System
from cms.engine import project, config, themes, plugins
from cms.engine import project, themes
import cms.engine.config
import cms.engine.project
import pandas as pd
import requests
import plugin_ix
from rich.table import Table
from rich import print
start = index.start
__doc__ = f"""
Built and designed by Steve L. Nyemba, steve@the-phi.com
@ -33,8 +36,10 @@ version {meta.__version__}
{meta.__license__}"""
PASSED = ' '.join(['[',colored('\u2713', 'green'),']'])
FAILED= ' '.join(['[',colored('\u2717','red'),']'])
# PASSED = ' '.join(['[',colored('\u2713', 'green'),']'])
# FAILED= ' '.join(['[',colored('\u2717','red'),']'])
FAILED = '[ [red] \u2717 [/red] ]'
PASSED = '[ [green] \u2713 [/green] ]'
INVALID_FOLDER = """
{FAILED} Unable to proceed, could not find project manifest. It should be qcms-manifest.json
@ -43,6 +48,20 @@ INVALID_FOLDER = """
# handling cli interface
cli = typer.Typer()
def to_Table(df: pd.DataFrame):
"""Displays a Pandas DataFrame as a rich table."""
table = Table(show_header=True, header_style="bold magenta")
for col in df.columns:
table.add_column(col)
for _, row in df.iterrows():
table.add_row(*row.astype(str).tolist())
# console.print(table)
return table
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])
@ -63,9 +82,12 @@ def _info():
@cli.command(name='setup')
# def set_app (host:str="0.0.0.0",context:str="",port:int=8084,debug=True):
def set_app (host:Annotated[str,typer.Argument(help="bind host IP address")]="0.0.0.0",
context:Annotated[str,typer.Argument(help="if behind a proxy server (no forward slash needed)")]="",
port:Annotated[int,typer.Argument(help="port on which to run the application")]=8084,
def set_app (
manifest:Annotated[str,typer.Argument(help="path to manifest or manifest folder")],
port:int=typer.Option(default=None,help="port on which to run the application"),
host:str=typer.Option(default='0.0.0.0',help="bind host IP address"),
context:str=typer.Option(default='',help="if behind a proxy server (no forward slash needed)"),
debug:Annotated[bool,typer.Argument(help="set debug mode on|off")]=True):
"""
Setup application access i.e port, debug, and/or context
@ -73,27 +95,30 @@ def set_app (host:Annotated[str,typer.Argument(help="bind host IP address")]="0.
"""
global INVALID_FOLDER
_config = config.get()
path = get_manifest(manifest)
_config = cms.engine.config.get(path)
if _config :
# _system = _config['system']['app']
_app = _config['system']['app']
_app['host'] = host
_app['port'] = port
_app['debug'] = debug
_app['host'] = host if host != _app['host'] else _app['host']
_app['port'] = port if port else _app['port']
_app['debug'] = debug if _app['debug'] != debug else _app['debug']
_config['system']['context'] = context
_config['app'] = _app
config.write(_config)
_msg = f"""{PASSED} Successful update, good job !
cms.engine.config.write(_config,path)
_msg = f"""{PASSED} [bold] {_config['layout']['header']['title']}[/bold]: Successful update, good job !
"""
else:
_msg = INVALID_FOLDER
print (_msg)
@cli.command(name='cloud')
# def set_cloud(path:str): #url:str,uid:str,token:str):
def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for the cloud")]): #url:str,uid:str,token:str):
def set_cloud(manifest:Annotated[str,typer.Argument(help="path of the auth-file for the cloud")]): #url:str,uid:str,token:str):
"""
Setup qcms to generate a site from files on nextcloud
The path must refrence auth-file (data-transport)
"""
path = get_manifest(manifest)
if os.path.exists(path):
f = open (path)
_auth = json.loads(f.read())
@ -101,11 +126,11 @@ def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for
url = _auth['url']
if os.path.exists('qcms-manifest.json') :
_config = config.get()
_config = cms.engine.config.get()
_config['system']['source'] = path #{'id':'cloud','auth':{'url':url,'uid':uid,'token':token}}
config.write(_config)
cms.engine.config.write(_config)
title = _config['layout']['header']['title']
_msg = f"""{PASSED} Successfully update, good job!
_msg = f"""{PASSED} [bold]{_config['system']['layout']['header']['title']}[/bold] : successfully update, good job!
{url}
"""
else:
@ -132,16 +157,16 @@ def secure(
f.write(str(uuid.uuid4()))
f.close()
#
_config = config.get(manifest)
_config = cms.engine.config.get(manifest)
if 'source' not in _config['system']:
_config['system']['source'] = {'id':'disk'}
_config['system']['source']['key'] = keyfile
config.write(_config,manifest)
_msg = f"""{PASSED} A key was generated and written to {keyfile}
cms.engine.config.write(_config,manifest)
_msg = f"""{PASSED} [bold]{_config['system']['layout']['header']['title']}[/bold] : A key was generated and written to {keyfile}
use this key in header to enable reload of the site ...`
"""
else:
_msg = f"""{FAILED} Could NOT generate a key, because it would seem you already have one
_msg = f"""{FAILED} [bold]{_config['system']['layout']['header']['title']}[/bold] : could [bold]NOT[/bold] generate a key, because it would seem you already have one
Please manually delete {keyfile}
@ -173,8 +198,9 @@ def load(**_args):
return getattr(module,_name) if hasattr(module,_name) else None
@cli.command(name='plugins')
def plug_info (manifest:Annotated[str,typer.Argument(help="path to manifest file")],
def plugin_manager (manifest:Annotated[str,typer.Argument(help="path to manifest file")],
show:bool=typer.Option(default=False,help="list plugins loaded"),
add: Annotated[Optional[bool],typer.Option("--register/--unregister",help="add/remove a plugin to manifest use with --pointer option")] = None,
pointer:str=typer.Option(default=None,help="pointer is structured as 'filename.function'")
@ -183,7 +209,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)
_config = cms.engine.config.get(manifest)
if _config :
_root = os.sep.join(manifest.split(os.sep)[:-1] + [_config['layout']['root'],'_plugins'])
else :
@ -193,41 +219,52 @@ def plug_info (manifest:Annotated[str,typer.Argument(help="path to manifest file
_msg = f"""{FAILED} no operation was specified, please use --help option"""
# if 'plugins' in _config :
_plugins = _config['plugins'] if 'plugins' in _config else {}
if show :
if not _plugins :
_msg = f"""{FAILED} no plugins are loaded\n\t{manifest}"""
else:
# _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}"""
_data = []
_plugConf = _config['plugins']
for _name in _plugConf :
_log = {"files":_name,"loaded":len(_plugConf[_name]),"logs":json.dumps(_plugConf[_name])}
_data.append(_log)
_data= pd.DataFrame(_data)
# # # _data = plugins.stats(_plugins)
# # _data = cms.Plugin.stats(_plugins)
print (to_Table(_data))
_msg = f"""{PASSED} [bold]{_config['layout']['header']['title']}[/bold]: 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('.')
file,fnName = pointer.split('.')
# _fnpointer = plugins.load(_root,file+'.py',fnName)
_fnpointer = cms.Plugin.load(_root,file+'.py',fnName)
if _fnpointer and add:
# _fnpointer = cms.Plugin.load(_root,file+'.py',fnName)
_ploader = plugin_ix.Loader(file = os.sep.join([_root,file+'.py']))
if add and _ploader.has(fnName):
if file not in _plugins :
_plugins[file] = []
if fnName not in _plugins[file] :
_plugins[file].append(fnName)
_msg = f"""{PASSED} registered {pointer}, use the --show option to list loaded plugins\n{manifest} """
_msg = f"""{PASSED} [bold]{_config['layout']['header']['title']}[/bold]: registered {pointer}, use the --show option to list loaded plugins"""
else:
_msg = f"""{FAILED} could not register {pointer}, it already exists\n\t{manifest} """
_msg = f"""{FAILED} [bold]{_config['layout']['header']['title']}[/bold]: could not register {pointer}, it already exists"""
elif add is False and file in _plugins:
_plugins[file] = [_name.strip() for _name in _plugins[file] if _name.strip() != fnName.strip() ]
_msg = f"""{PASSED} unregistered {pointer}, use the --show option to list loaded plugins\n{manifest} """
_plugins[file] = [_name.strip() for _name in _plugins[file] if _name.strip() != fnName.strip() ]
_msg = f"""{PASSED} [bold]{_config['layout']['header']['title']}[/bold]: unregistered {pointer}, use the --show option to list loaded plugins """
#
# We need to write this down !!
# #
# # We need to write this down !!
if add in [True,False] :
_config['plugins'] = _plugins
config.write(_config,manifest)
cms.engine.config.write(_config,manifest)
# else:
# _msg = f"""{FAILED} no plugins are loaded\n\t{manifest}"""
# _msg = f"""{FAILED} [bold]{_config['layout']['header']['title']}[/bold]: no plugins are loaded """
print()
print(_msg)
else:
@ -266,7 +303,7 @@ def create(folder:Annotated[str,typer.Argument(help="path of the project folder"
#
# Setup Project on disk
#
project.make(folder=folder,config=_config)
cms.engine.project.make(folder=folder,config=_config)
print (f"""{PASSED} created project at {folder} """)
else:
print ()
@ -284,7 +321,7 @@ def reload (
Reload a site/portal given the manifest ...
"""
path = get_manifest(path)
_config = config.get( path)
_config = cms.engine.config.get( path)
if 'source' in _config['system'] and 'key' in _config['system']['source'] :
_spath = _config['system']['source']['key']
# f = open(_config['system']['source']['key'])
@ -301,7 +338,7 @@ def reload (
url = f"http://localhost:{_port}/reload"
resp = requests.post(url, headers={"key":key})
if resp.status_code == 200 :
_msg = f"""{PASSED} successfully reloaded {url}"""
_msg = f"""{PASSED} [bold]{_config['system']['layout']['header']['title']}[/bold] : successfully reloaded {url}"""
else:
_msg = f"""{FAILED} failed to reload, status code {resp.status_code}\n{url}
"""
@ -332,7 +369,7 @@ def handle_theme (
"""
manifest = get_manifest(manifest)
_config = config.get(manifest)
_config = cms.engine.config.get(manifest)
_root = os.sep.join( manifest.split(os.sep)[:-1]+[_config['layout']['root']])
if show :
@ -356,8 +393,8 @@ def handle_theme (
# values.sort()
# _df['installed'] = values
else:
_df = f"""{FAILED} No themes were found in registry,\ncurl {themes.URL}/api/themes/List (QCMS_HOST_URL should be set)"""
print (_df)
_df = f"""{FAILED} [bold]{_config['system']['layout']['header']['title']}[/bold] : No themes were found in registry,\ncurl {themes.URL}/api/themes/List (QCMS_HOST_URL should be set)"""
print (to_Table(_df))
if name and not show:
# we need to install the theme, i.e download it and update the configuration
#
@ -377,11 +414,11 @@ def handle_theme (
# Let us update the configuration file ...
#
_config['system']['theme'] = name
config.write(_config,manifest)
_msg = f"""{PASSED} successfully downloaded {name} \n{PASSED} updated manifest {manifest}
cms.engine.config.write(_config,manifest)
_msg = f"""{PASSED} [bold]{_config['system']['layout']['header']['title']}[/bold] : successfully downloaded {name} \n{PASSED} updated manifest {manifest}
"""
except Exception as e:
_msg = f"""{FAILED} operation failed "{str(e)}"
_msg = f"""{FAILED} [[bold]{_config['system']['layout']['header']['title']}[/bold] : operation failed "{str(e)}"
"""
pass

@ -29,45 +29,45 @@ class Plugin :
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'])
# @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
# 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
# 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 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):
@ -123,3 +123,56 @@ class Plugin :
return _data,_code,{'Content-Type':_mimeType}
pass
class delegate:
def __call__(self,**_args) :
_handler= _args['handler'] #-- module 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)
if type(_data) == pd.DataFrame :
_data = _data.to_json(orient='records')
elif type(_data) in [dict,list] :
_data = json.dumps(_data)
pass
else:
_code = 404
#
# We should generate a 500 error in this case with a message ...
#
_mimeType = 'plain/html'
_data = f"""
<script>
//
// I should
_msg = '<div class="border-round" style="display:grid; grid-template-columns:100px auto; gap:8px; align-content:center"><div class="large-text">404</div>,div> Resource NOT found</di><div>'
</script>
"""
return _data,_code,{'Content-Type':_mimeType}

@ -8,6 +8,7 @@ import copy
import mistune
from mistune import markdown
import re
import plugin_ix
def folders (_path,_config):
"""
@ -143,29 +144,34 @@ def plugins (**_args):
key = f'{_context}/{key}'
return {key:read}
_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')]
if files:
_uri = [_path,files[0]]
if _context :
_uri = [_context] + _uri
_path = os.sep.join(_uri)
else:
return None
else:
#
# LOG: not a file
return None
#-- We have a file ...
_name = _args['name']
spec = importlib.util.spec_from_file_location(_name, _path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
_path = _args['path'] #os.sep.join([_args['root'],'plugin'])
loader = plugin_ix.Loader(file=_path)
if _args['name'] in loader.names() :
_pointer = loader.get(_args['name'])
return _pointer
return None
# if os.path.isdir(_path):
# files = os.listdir(_path)
# if files :
# files = [name for name in files if name.endswith('.py')]
# if files:
# _uri = [_path,files[0]]
# if _context :
# _uri = [_context] + _uri
# _path = os.sep.join(_uri)
# else:
# return None
# else:
# #
# # LOG: not a file
# return None
# #-- We have a file ...
# _name = _args['name']
# spec = importlib.util.spec_from_file_location(_name, _path)
# module = importlib.util.module_from_spec(spec)
# spec.loader.exec_module(module)
#
# LOG This plugin ....
return getattr(module,_name) if hasattr(module,_name) else None
# #
# # LOG This plugin ....
# return getattr(module,_name) if hasattr(module,_name) else None

@ -137,58 +137,10 @@ def _delegate_call(app,key,module,name):
_handler = _getHandler(app,key)
# print (_handler.config()/)
uri = f'api/{module}/{name}'
return Plugin.call(uri=uri,handler=_handler,request=request)
# return _delegate(_handler,module,name)
# return Plugin.call(uri=uri,handler=_handler,request=request)
_delegate = cms.delegate()
return _delegate(uri=uri,handler=_handler,request=request)
def _delegate(_handler,module,name):
global _route
uri = '/'.join(['api',module,name])
# _args = dict(request.args,**{})
# _args['config'] = _handler.config()
_plugins = _handler.plugins()
_context = _handler.system()['context']
if _context :
uri = f'{_context}/{uri}'
_mimeType = 'application/octet-stream'
if uri not in _plugins :
_data = {}
_code = 404
else:
pointer = _plugins[uri]
# if _args :
# _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())
_mimeType = 'application/octet-stream' if not _mimeType else _mimeType
if type(_data) == pd.DataFrame :
_data = _data.to_dict(orient='records')
if type(_data) == list:
_data = json.dumps(_data)
_code = 200 if _data else 500
return _data,_code,{'Content-Type':_mimeType}
# @_app.route("/api/<module>/<name>" , methods=['POST'],defaults={'app_id':'main','key':None})
# @_app.route('/<app_id>/api/<module>/<name>',methods=['POST'],defaults={'key':None})
# @_app.route('/<app_id>/<key>/api/<module>/<name>',methods=['POST'],defaults={'app_id':'main','key':None})
# @_app.route("/<app_id>/<key>/api/<module>/<name>", methods=['POST'])
# @_app.route("/api/<module>/<name>",defaults={'app_id':'main','key':None},methods=['POST'])
# @_app.route("/<app_id>/api/<module>/<name>",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
# _handler = _getHandler(app_id,key)
# return _delegate(_handler,module,name)
@_app.route('/version')
def _version ():
@ -206,7 +158,7 @@ def _reload(key) :
_systemKey = None
elif 'key' in _system['source'] and _system['source']['key']:
_systemKey = _system['source']['key']
print ([key,_systemKey,_systemKey == key])
# print ([key,_systemKey,_systemKey == key])
if key and _systemKey and _systemKey == key :
_handler.reload()
return "",200
@ -253,18 +205,7 @@ def _POST_CMSPage(app_id,key):
_id = _uri.split('/')[-1].split('.')[0]
else:
_id = request.headers['dom']
# _args = {'layout':_config['layout']}
# if 'plugins' in _config:
# _args['routes'] = _config['plugins']
# _system = _handler.system() #cms.components.get_system(_config)
# # _html = _handler.html(_uri,_id,_args,_system) #cms.components.html(_uri,_id,_args,_system)
# _html = _handler.html(_uri,_id)
# # _system = cms.components.get_system(_config)
# _args['system'] = _handler.system(skip=['source','app'])
# e = Environment(loader=BaseLoader()).from_string(_html)
# _html = e.render(**_args)
if 'read?uri=' in _uri or 'download?doc=' in _uri :
_uri = _uri.split('=')[1]
_args = _route.render(_uri,_id,_getId(app_id,key)) #session.get(app_id,'main'))

Loading…
Cancel
Save