Spaces:
Running
Running
""" | |
Jaa.py Plugin Framework | |
Author: Janvarev Vladislav | |
Jaa.py - minimalistic one-file plugin framework with no dependencies. | |
Main functions: | |
- run all plugins files from "plugins" folder, base on filename | |
- save each plugin options in "options" folder in JSON text files for further editing | |
- Plugins | |
must located in plugins/ folder | |
must have "start(core)" function, that returns manifest dict | |
manifest must contain keys "name" and "version" | |
can contain "default_options" | |
- if contain - options will be saved in "options" folder and reload instead next time | |
- if contain - "start_with_options(core,manifest)" function will run with manifest with "options" key | |
manifest will be processed in "process_plugin_manifest" function if you override it | |
- Options (for plugins) | |
are saved under "options" folder in JSON format | |
created at first run plugin with "default_options" | |
updated when plugin change "version" | |
- Example usage: | |
class VoiceAssCore(JaaCore): # class must override JaaCore | |
def __init__(self): | |
JaaCore.__init__(self,__file__) | |
... | |
main = VoiceAssCore() | |
main.init_plugins(["core"]) # 1 param - first plugins to be initialized | |
# Good if you need some "core" options/plugin to be loaded before others | |
# not necessary starts with "plugin_" prefix | |
also can be run like | |
main.init_plugins() | |
- Requirements | |
Python 3.5+ (due to dict mix in final_options calc), can be relaxed | |
""" | |
import os | |
import traceback | |
import json | |
# here we trying to use termcolor to highlight plugin info and errors during load | |
try: | |
from termcolor import cprint | |
except Exception as e: | |
# not found? making a stub! | |
def cprint(p,color=None): | |
if color == None: | |
print(p) | |
else: | |
print(str(color).upper(),p) | |
version = "2.2.0" | |
class JaaCore: | |
verbose = False | |
def __init__(self,root_file = __file__): | |
self.jaaPluginPrefix = "plugin_" | |
self.jaaVersion = version | |
self.jaaRootFolder = os.path.dirname(root_file) | |
self.jaaOptionsPath = self.jaaRootFolder+os.path.sep+"plugin_options" | |
self.jaaShowTracebackOnPluginErrors = False | |
if self.verbose: | |
cprint("JAA.PY v{0} class created!".format(version),"blue") | |
# ------------- plugins ----------------- | |
def init_plugins(self, list_first_plugins = []): | |
self.plugin_manifests = {} | |
# 1. run first plugins first! | |
for modname in list_first_plugins: | |
self.init_plugin(modname) | |
# 2. run all plugins from plugins folder | |
from os import listdir | |
from os.path import isfile, join | |
pluginpath = self.jaaRootFolder+"/plugins" | |
files = [f for f in listdir(pluginpath) if isfile(join(pluginpath, f))] | |
for fil in files: | |
# print fil[:-3] | |
if fil.startswith(self.jaaPluginPrefix) and fil.endswith(".py"): | |
modfile = fil[:-3] | |
self.init_plugin(modfile) | |
def init_plugin(self,modname): | |
# import | |
try: | |
mod = self.import_plugin("plugins."+modname) | |
except Exception as e: | |
self.print_error("JAA PLUGIN ERROR: {0} error on load: {1}".format(modname, str(e))) | |
return False | |
# run start function | |
try: | |
res = mod.start(self) | |
except Exception as e: | |
self.print_error("JAA PLUGIN ERROR: {0} error on start: {1}".format(modname, str(e))) | |
return False | |
# if plugin has an options | |
if "default_options" in res: | |
try: | |
# saved options try to read | |
saved_options = {} | |
try: | |
with open(self.jaaOptionsPath+'/'+modname+'.json', 'r', encoding="utf-8") as f: | |
s = f.read() | |
saved_options = json.loads(s) | |
#print("Saved options", saved_options) | |
except Exception as e: | |
pass | |
res["default_options"]["v"] = res["version"] | |
# only string needs Python 3.5 | |
final_options = {**res["default_options"], **saved_options} | |
# if no option found or version is differ from mod version | |
if len(saved_options) == 0 or saved_options["v"] != res["version"]: | |
final_options["v"] = res["version"] | |
self.save_plugin_options(modname,final_options) | |
res["options"] = final_options | |
try: | |
res2 = mod.start_with_options(self,res) | |
if res2 != None: | |
res = res2 | |
except Exception as e: | |
self.print_error("JAA PLUGIN ERROR: {0} error on start_with_options processing: {1}".format(modname, str(e))) | |
return False | |
except Exception as e: | |
self.print_error("JAA PLUGIN ERROR: {0} error on options processing: {1}".format(modname, str(e))) | |
return False | |
# processing plugin manifest | |
try: | |
# set up name and version | |
plugin_name = res["name"] | |
plugin_version = res["version"] | |
self.process_plugin_manifest(modname,res) | |
except Exception as e: | |
print("JAA PLUGIN ERROR: {0} error on process startup options: {1}".format(modname, str(e))) | |
return False | |
self.plugin_manifests[modname] = res | |
self.on_succ_plugin_start(modname,plugin_name,plugin_version) | |
return True | |
def on_succ_plugin_start(self, modname, plugin_name, plugin_version): | |
if self.verbose: | |
cprint("JAA PLUGIN: {1} {2} ({0}) started!".format(modname, plugin_name, plugin_version)) | |
def print_error(self,p): | |
cprint(p,"red") | |
if self.jaaShowTracebackOnPluginErrors: | |
traceback.print_exc() | |
def import_plugin(self, module_name): | |
import sys | |
__import__(module_name) | |
if module_name in sys.modules: | |
return sys.modules[module_name] | |
return None | |
def save_plugin_options(self,modname,options): | |
# check folder exists | |
if not os.path.exists(self.jaaOptionsPath): | |
os.makedirs(self.jaaOptionsPath) | |
str_options = json.dumps(options, sort_keys=True, indent=4, ensure_ascii=False) | |
with open(self.jaaOptionsPath+'/'+modname+'.json', 'w', encoding="utf-8") as f: | |
f.write(str_options) | |
f.close() | |
# process manifest must be overrided in inherit class | |
def process_plugin_manifest(self,modname,manifest): | |
print("JAA PLUGIN: {0} manifest dummy procession (override 'process_plugin_manifest' function)".format(modname)) | |
return | |
def plugin_manifest(self,pluginname): | |
if pluginname in self.plugin_manifests: | |
return self.plugin_manifests[pluginname] | |
return {} | |
def plugin_options(self,pluginname): | |
manifest = self.plugin_manifest(pluginname) | |
if "options" in manifest: | |
return manifest["options"] | |
return None | |
# ------------ gradio stuff -------------- | |
def gradio_save(self,pluginname): | |
print("Saving options for {0}!".format(pluginname)) | |
self.save_plugin_options(pluginname,self.plugin_options(pluginname)) | |
def gradio_upd(self, pluginname, option, val): | |
options = self.plugin_options(pluginname) | |
# special case | |
if isinstance(options[option], (list, dict)) and isinstance(val, str): | |
import json | |
try: | |
options[option] = json.loads(val) | |
except Exception as e: | |
print(e) | |
pass | |
else: | |
options[option] = val | |
print(option,val,options) | |
def gradio_render_settings_interface(self, title:str="Settings manager", required_fields_to_show_plugin:list=["default_options"]): | |
import gradio as gr | |
with gr.Blocks() as gr_interface: | |
gr.Markdown("# {0}".format(title)) | |
for pluginname in self.plugin_manifests: | |
manifest = self.plugin_manifests[pluginname] | |
# calculate if we show plugin | |
is_show_plugin = False | |
if len(required_fields_to_show_plugin) == 0: | |
is_show_plugin = True | |
else: | |
for k in required_fields_to_show_plugin: | |
if manifest.get(k) is not None: | |
is_show_plugin = True | |
if is_show_plugin: | |
with gr.Tab(pluginname): | |
gr.Markdown("## {0} v{1}".format(manifest["name"],manifest["version"])) | |
if manifest.get("description") is not None: | |
gr.Markdown(manifest.get("description")) | |
if manifest.get("url") is not None: | |
gr.Markdown("**URL:** [{0}]({0})".format(manifest.get("url"))) | |
if "options" in manifest: | |
options = manifest["options"] | |
if len(options) > 1: # not only v | |
text_button = gr.Button("Save options".format(pluginname)) | |
#options_int_list = [] | |
for option in options: | |
#gr.Label(label=option) | |
if option != "v": | |
val = options[option] | |
label = option | |
if manifest.get("options_label") is not None: | |
if manifest.get("options_label").get(option) is not None: | |
label = option+": "+manifest.get("options_label").get(option) | |
if isinstance(val, (bool, )): | |
gr_elem = gr.Checkbox(value=val,label=label) | |
elif isinstance(val, (dict,list)): | |
import json | |
gr_elem = gr.Textbox(value=json.dumps(val,ensure_ascii=False), label=label) | |
else: | |
gr_elem = gr.Textbox(value=val, label=label) | |
def handler(x,pluginname=pluginname,option=option): | |
self.gradio_upd(pluginname, option, x) | |
gr_elem.change(handler, gr_elem, None) | |
def handler_save(pluginname=pluginname): | |
self.gradio_save(pluginname) | |
text_button.click(handler_save,inputs=None,outputs=None) | |
else: | |
gr.Markdown("_No options for this plugin_") | |
return gr_interface | |
def load_options(options_file=None,py_file=None,default_options={}): | |
# 1. calculating options filename | |
if options_file == None: | |
if py_file == None: | |
raise Exception('JAA: Options or PY file is not defined, cant calc options filename') | |
else: | |
options_file = py_file[:-3]+'.json' | |
# 2. try to read saved options | |
saved_options = {} | |
try: | |
with open(options_file, 'r', encoding="utf-8") as f: | |
s = f.read() | |
saved_options = json.loads(s) | |
#print("Saved options", saved_options) | |
except Exception as e: | |
pass | |
# 3. calculating final options | |
# only string needs Python 3.5 | |
final_options = {**default_options, **saved_options} | |
# 4. calculating hash from def options to check - is file rewrite needed? | |
import hashlib | |
hash = hashlib.md5((json.dumps(default_options, sort_keys=True)).encode('utf-8')).hexdigest() | |
# 5. if no option file found or hash was from other default options | |
if len(saved_options) == 0 or not ("hash" in saved_options.keys()) or saved_options["hash"] != hash: | |
final_options["hash"] = hash | |
#self.save_plugin_options(modname,final_options) | |
# saving in file | |
str_options = json.dumps(final_options, sort_keys=True, indent=4, ensure_ascii=False) | |
with open(options_file, 'w', encoding="utf-8") as f: | |
f.write(str_options) | |
f.close() | |
return final_options | |
""" | |
The MIT License (MIT) | |
Copyright (c) 2021 Janvarev Vladislav | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the “Software”), to deal | |
in the Software without restriction, including without limitation the rights to use, | |
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, | |
and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all copies or | |
substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | |
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE | |
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
""" |