diff --git a/bin/qcms b/bin/qcms index 3ac518e..b7e06f0 100755 --- a/bin/qcms +++ b/bin/qcms @@ -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,17 @@ def secure( f.write(str(uuid.uuid4())) f.close() # - _config = config.get(manifest) + manifest = get_manifest(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['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,63 +199,78 @@ 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, + 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'") ) : """ 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) : + _config = cms.engine.config.get(manifest) + if _config : + _root = os.sep.join(manifest.split(os.sep)[:-1] + [_config['layout']['root'],'_plugins']) + else : + _root = None + if _root and os.path.exists(_root) : # files = os.listdir(_root) _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]),"functions":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: - _msg = f"""{FAILED} no plugin folder found """ - pass + _msg = f"""{FAILED} No plugin folder could be found in {manifest}""" + print (_msg) @cli.command (name='create') @@ -263,7 +304,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 () @@ -281,7 +322,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']) @@ -298,7 +339,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['layout']['header']['title']}[/bold] : successfully reloaded {url}""" else: _msg = f"""{FAILED} failed to reload, status code {resp.status_code}\n{url} """ @@ -329,7 +370,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 : @@ -353,8 +394,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 # @@ -374,11 +415,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 diff --git a/cms/__init__.py b/cms/__init__.py index 42ce092..d621116 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -8,6 +8,8 @@ import importlib import importlib.util import json +from . import apexchart + class Plugin : # # decorator for plugin functions, this is a preliminary to enable future developement @@ -27,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): @@ -122,59 +124,56 @@ class Plugin : return _data,_code,{'Content-Type':_mimeType} pass -# def _get_config (path) : -# if os.path.exists(path) : -# f = open(path) -# _conf = json.loads(f.read()) -# f.close() -# else: -# _conf = {} -# return _conf -# def _isvalid(_allowed,**_args): - -# if not list(set(_allowed) - set(_args.keys())) : -# _pargs = {} -# for key in _allowed : -# _pargs [key] = _args[key] -# return _pargs -# return False - -# def write_config(_config, path): -# f = open(path,'w') -# f.write( json.dumps(_config)) ; -# f.close() - -# def _system(**_args): -# """ -# Both version and context must be provided otherwise they are ignored -# :version -# :context context -# """ -# _info = _isvalid(['version','context'],**_args) -# _info['theme'] = 'default.css' -# _info = _info if _info else {'version':'0.0.0','context':'','theme':'default.css'} -# if _info : -# _info['logo'] = None if 'logo' not in _args else _args['logo'] -# # -# # There is an aggregation entry here in app -# _appInfo = {'debug':True,'port':8084,'threaded':True,'host':'0.0.0.0'} -# if 'app' not in _args: -# _info['app'] = _appInfo -# else: -# _info['app'] = dict(_appInfo,**_args['app']) +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] + _data = {} + if hasattr(_pointer,'mimetype') and hasattr(_pointer,'method'): + # + # we constraint the methods given their execution ... + _mimeType = _pointer.mimetype + if _request.method in _pointer.method : + _data = _pointer(request=_request,config=_config) + elif not hasattr(_pointer,'mimetype'): + + _data,_mimeType = _pointer(request=_request,config=_config) + + else: + _mimeType = 'application/octet-stream' + # _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""" + + """ -# return _info -# def _header(**_args): -# return _isvalid(['logo','title','subtitle'],**_args) -# def _layout(**_args): -# _info = _isvalid(['root','index'],**_args) -# _info['on'] = {"load":{},"error":{}} -# _url = 'qcms.co' -# _overwrite = {"folder1":{"type":"redirect","url":_url},"folder2":{"type":"dialog"},"folder3":{"type":"dialog","url":_url}} -# _info['icons'] = {"comment":"use folder names as keys and fontawesome type as values to add icons to menu"} -# _info["api"] = {"comment":"use keys as uri and function calls as values"} -# _info['map'] = {}, -# _info['order'] = {'menu':[]} -# _info['overwrite'] = _overwrite -# return _info - \ No newline at end of file + return _data,_code,{'Content-Type':_mimeType} diff --git a/cms/apexchart.py b/cms/apexchart.py new file mode 100644 index 0000000..97f93a7 --- /dev/null +++ b/cms/apexchart.py @@ -0,0 +1,185 @@ +import numpy as np +import pandas as pd + +class chart : + @staticmethod + def get(_data,_config) : + # r = {} + # for _key in _config : + # r[_key] = {'about':_config[_key]['about'],'chart':[]} + # _pointers = _config[_key]['apply'] + # _pointers = [_pointers] if type(_pointers) == str else _pointers + # r[_key]['chart'] += [getattr(chart,_name)(_data,_config[_key]) for _name in _pointers if hasattr(chart,_name)] + # return [r] + r = {} + for _key in _config : + _options = _config[_key]['options'] + r[_key] = {'about':_config[_key]['about'],'css':_config[_key]['css'],'charts':[]} + _charts = [] + for _itemOption in _options : + _type = _itemOption['type'] + if hasattr(chart,_type) : + _pointer = getattr(chart,_type) + + _chartOption = _pointer(_data,_itemOption) + _tag = 'options' if _type != 'scalar' else 'html' + if 'title' in _itemOption and _itemOption['type'] != 'scalar' : + _chartOption['title'] = {'text':_itemOption['title'],'align':'center'} + _chartOption['legend'] = {'position':'bottom','itemMargin':{'horizontal':4,'vertical':10}} + + _chartOption['chart']['height'] = 300 + # _chartOption['chart']['height'] = '100%' + # _chartOption['responsive'] = [{'breakpoint':480,'options':{'chart':{'width':300}}}] + _charts.append ({"type":_type,_tag:_chartOption}) + if _charts : + r[_key]['charts'] = _charts + + # for _pointer in _pointers : + # r[_key]['chart'].append(_pointer(_data,_config[_key])) + + # _pointers = [getattr(chart,_name) for _name in _config[_key]['options']['apply'] if hasattr(chart,_name)] + # r[_key] = {'about':_config[_key]['about'],'chart':[]} + # for _pointer in _pointers : + # r[_key]['chart'].append(_pointer(_data,_config[_key])) + + + return r + @staticmethod + def format_digit (value): + if value > 1000000 : + return np.divide(value,1000000).round(2).astype(str) + ' M' + elif value > 1000 : + return np.divide(value,1000).round(2).astype(str)+ ' K' + return value + @staticmethod + def scalar(_data,_config) : + """ + Only focusing on axis.y + """ + _columns = _config['axis']['y'] + if _data.shape[0] > 1 : + _apply = 'sum' if 'apply' not in _config else _config['apply'] + # values = _data[_columns].sum().values.tolist() + values = getattr(_data[_columns],_apply)().values.round(3).tolist() + else: + values = _data[_columns].values.tolist()[0] + + _html = [f'