diff --git a/bin/qcms b/bin/qcms index a7a5b56..c7edab8 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,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 diff --git a/cms/__init__.py b/cms/__init__.py index c0f7b95..53c61c0 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -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} diff --git a/cms/disk.py b/cms/disk.py index 65e94d1..1b7efa6 100644 --- a/cms/disk.py +++ b/cms/disk.py @@ -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 \ No newline at end of file diff --git a/cms/index.py b/cms/index.py index 9dce0c1..de5870b 100644 --- a/cms/index.py +++ b/cms/index.py @@ -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'))