From 76644632958d791a2b5ce9fa0f6133b842cac03c Mon Sep 17 00:00:00 2001 From: noO0oOo0ob <38344038+noO0oOo0ob@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:45:51 +0800 Subject: [PATCH] Feature/supprt settings page (#877) * settings page frontend create * support setting page * version * add restore * add restore * add not display and not inited * delete unused code * update example * add report * fix bug * update example --------- Co-authored-by: noO0ob --- frontend/src/api/settings.js | 29 ++ frontend/src/router.js | 12 + frontend/src/store/settings.js | 59 ++- frontend/src/views/settings/SettingDetail.vue | 377 ++++++++++++++++++ frontend/src/views/settings/SettingList.vue | 165 ++++++++ frontend/src/views/settings/Settings.vue | 42 ++ lyrebird/application.py | 1 + lyrebird/config/__init__.py | 2 +- .../examples/settings/proxy_white_list.py | 195 +++++++++ lyrebird/manager.py | 6 + lyrebird/mock/blueprints/apis/__init__.py | 2 + lyrebird/mock/blueprints/apis/menu.py | 9 + lyrebird/mock/blueprints/apis/settings.py | 73 ++++ lyrebird/settings/__init__.py | 2 + lyrebird/settings/settings_manager.py | 215 ++++++++++ lyrebird/settings/settings_template.py | 58 +++ lyrebird/version.py | 2 +- 17 files changed, 1243 insertions(+), 6 deletions(-) create mode 100644 frontend/src/views/settings/SettingDetail.vue create mode 100644 frontend/src/views/settings/SettingList.vue create mode 100644 frontend/src/views/settings/Settings.vue create mode 100644 lyrebird/examples/settings/proxy_white_list.py create mode 100644 lyrebird/mock/blueprints/apis/settings.py create mode 100644 lyrebird/settings/__init__.py create mode 100644 lyrebird/settings/settings_manager.py create mode 100644 lyrebird/settings/settings_template.py diff --git a/frontend/src/api/settings.js b/frontend/src/api/settings.js index bc06555a1..413871e1e 100644 --- a/frontend/src/api/settings.js +++ b/frontend/src/api/settings.js @@ -13,3 +13,32 @@ export const updateConfigByKey = (data) => { method: 'PATCH' }) } + +export const getSettingModelList = () => { + return axios({ + url: '/api/settings/list', + method: 'GET' + }) +} + +export const getSettingsForm = (name) => { + return axios({ + url: '/api/settings/detail?name='+name, + method: 'GET' + }) +} + +export const setSettingsForm = (name, data) => { + return axios({ + url: '/api/settings?name='+name, + data: { data }, + method: 'POST' + }) +} + +export const restoreSettingsForm = (name) => { + return axios({ + url: '/api/settings?name='+name, + method: 'DELETE' + }) +} diff --git a/frontend/src/router.js b/frontend/src/router.js index 3057ccf31..6f72d8f66 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -6,6 +6,7 @@ import DataManager from './views/datamanager/DataManager.vue' import Checker from './views/checker/Checker.vue' import EventInspector from '@/views/event/EventInspector.vue' import PluginView from './views/PluginView.vue' +import Settings from './views/settings/Settings.vue' //vue router error handler const originalPush = Router.prototype.push @@ -77,6 +78,17 @@ export default new Router({ }, ], }, + { + path: '/settings', + component: Main, + children: [ + { + path: '', + name: 'settings', + component: Settings, + }, + ], + }, { path: '/plugin', name: 'plugin', diff --git a/frontend/src/store/settings.js b/frontend/src/store/settings.js index 4395dcba6..20552bd7a 100644 --- a/frontend/src/store/settings.js +++ b/frontend/src/store/settings.js @@ -28,7 +28,10 @@ export default { state: { config: {}, initialized: false, - preLoadFuncSet: new Set() + preLoadFuncSet: new Set(), + focusSettingPanel: '', + settingsList: [], + settingsCurrentDetail: {}, }, mutations: { setConfig (state, config) { @@ -42,7 +45,16 @@ export default { }, deletePreLoadFuncSet (state, preLoadFunc) { state.preLoadFuncSet.delete(preLoadFunc) - } + }, + setSettingsList(state, data) { + state.settingsList = data + }, + setSettingsCurrentDetail(state, data) { + state.settingsCurrentDetail = data + }, + setFocusSettingPanel(state, data) { + state.focusSettingPanel = data + }, }, actions: { loadConfig({ state, commit, dispatch }) { @@ -98,6 +110,45 @@ export default { .catch(error => { bus.$emit('msg.error', `Update config failed ${error.data.message}`) }) - } - } + }, + loadSettingsList({ state, commit, dispatch }) { + api.getSettingModelList() + .then(response => { + console.log(response.data.data) + commit('setSettingsList', response.data.data) + }) + .catch(error => { + bus.$emit('msg.error', 'Load config failed ' + error.data.message) + }) + }, + loadSettingsForm({ state, commit, dispatch }, payload) { + api.getSettingsForm(payload) + .then(response => { + commit('setSettingsCurrentDetail', response.data.data) + }) + .catch(error => { + bus.$emit('msg.error', 'Load config failed ' + error.data.message) + }) + }, + saveSettingsForm({ state, commit, dispatch }, { formName, formData }) { + api.setSettingsForm(formName, formData) + .then(response => { + dispatch('loadSettingsForm', formName) + bus.$emit('msg.success', 'Data ' + formName + ' update!') + }) + .catch(error => { + bus.$emit('msg.error', 'Data ' + formName + ' update error: ' + error.data.message) + }) + }, + restoreSettingsForm({ state, commit, dispatch }, payload) { + api.restoreSettingsForm(payload) + .then(response => { + dispatch('loadSettingsForm', payload) + bus.$emit('msg.success', 'Data ' + payload + ' update!') + }) + .catch(error => { + bus.$emit('msg.error', 'Data ' + payload + ' update error: ' + error.data.message) + }) + }, + }, } diff --git a/frontend/src/views/settings/SettingDetail.vue b/frontend/src/views/settings/SettingDetail.vue new file mode 100644 index 000000000..9d0e0b7ec --- /dev/null +++ b/frontend/src/views/settings/SettingDetail.vue @@ -0,0 +1,377 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/SettingList.vue b/frontend/src/views/settings/SettingList.vue new file mode 100644 index 000000000..0dd164cfd --- /dev/null +++ b/frontend/src/views/settings/SettingList.vue @@ -0,0 +1,165 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/Settings.vue b/frontend/src/views/settings/Settings.vue new file mode 100644 index 000000000..f14342807 --- /dev/null +++ b/frontend/src/views/settings/Settings.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/lyrebird/application.py b/lyrebird/application.py index d0ed446f2..bb5972252 100644 --- a/lyrebird/application.py +++ b/lyrebird/application.py @@ -40,6 +40,7 @@ def make_fail_response(msg, **kwargs): notice = None checkers = {} +settings = {} on_request = [] on_response = [] diff --git a/lyrebird/config/__init__.py b/lyrebird/config/__init__.py index 278bf573e..7286791c3 100644 --- a/lyrebird/config/__init__.py +++ b/lyrebird/config/__init__.py @@ -251,7 +251,7 @@ def initialize_personal_config(self): self.personal_config = self.read_personal_config() def update_personal_config(self, config_dict: dict): - self.personal_config = config_dict + self.personal_config.update(config_dict) self.write_personal_config() def read_personal_config(self): diff --git a/lyrebird/examples/settings/proxy_white_list.py b/lyrebird/examples/settings/proxy_white_list.py new file mode 100644 index 000000000..e6d1ce9f1 --- /dev/null +++ b/lyrebird/examples/settings/proxy_white_list.py @@ -0,0 +1,195 @@ +import re +import json +from hashlib import md5 +from lyrebird import application, get_logger +from lyrebird.settings import SettingsTemplate + +logger = get_logger() + +DEFAULT_WHITE_LIST = [] +DEFAULT_BLACK_LIST = [] + + +class WhiteListSettings(SettingsTemplate): + + def __init__(self): + super().__init__() + self.display = True + self.name = 'proxy_white_list' + self.title = 'Proxy Whitelist and Blacklist Settings' + self.notice = 'Controls the requests entering Lyrebird proxy logic. Requests filtered out cannot use Mock, Checker, Modifier, etc.' + self.submit_text = 'Submit' + self.language = 'en' + self.category = 'Request Proxy' + self.category_md5 = md5(self.category.encode(encoding='UTF-8')).hexdigest() + self.switch = False + self.configs = {} + self.ori_filters = application.config.get('proxy.filters', []) + self.is_simple_url = re.compile(r'^[a-zA-Z0-9./:_-]+$') + self.is_balck_and_white = re.compile(r'(?=^\(\?=.*\))(?=.*\(\?!.*\)$)') + self.is_balck = re.compile(r'^\(\?=.*\)$') + self.is_white = re.compile(r'^\(\?!.*\)$') + + def getter(self): + white = None + url_black = None + suffix_black = None + regular = None + if self.configs: + white = self.configs.get('white_list', []) + url_black = self.configs.get('black_list', []) + suffix_black = self.configs.get('black_suffix', []) + regular = self.configs.get('regular_list', []) + else: + filters = self.get_config_by_application() + white = filters['white'] + regular = filters['regular'] + url_black, suffix_black = self.get_suffix_black_list(filters['black']) + proxy_white_list_switch = { + 'name': 'proxy_white_list_switch', + 'title': 'Configuration Switch', + 'subtitle': 'Switch for this configuration to take effect. When turned off, it reverts to the default configuration.', + 'category': 'bool', + 'data': self.switch + } + white_list = { + 'name': 'white_list', + 'title': 'Request Whitelist', + 'subtitle': 'Allows requests with specific text in host and path to use Lyrebird proxy', + 'category': 'list', + 'data': white + } + black_list = { + 'name': 'black_list', + 'title': 'Request Blacklist', + 'subtitle': 'Blocks requests with specific text in host and path from using Lyrebird proxy', + 'category': 'list', + 'data': url_black + } + black_suffix = { + 'name': 'black_suffix', + 'title': 'File Type Blacklist', + 'subtitle': 'Globally filters requests of specified resource types, such as png, zip, etc.', + 'category': 'list', + 'data': suffix_black + } + regular_list = { + 'name': 'regular_list', + 'title': 'Additional Regex', + 'subtitle': 'If the above whitelists and blacklists do not meet the needs, you can write your own regular expressions. Note: The logic between regexes is OR, meaning as long as any regex matches, it will hit the proxy.', + 'category': 'list', + 'data': regular + } + return [proxy_white_list_switch, white_list, black_list, black_suffix, regular_list] + + def setter(self, data): + self.switch = bool(data['proxy_white_list_switch']) + self.configs = data + if self.switch: + self.apply_config(data) + self.save(data) + if not self.switch: + application.config['proxy.filters'] = self.ori_filters + logger.warning(f'application.config is updated by proxy_white_list_switch, \nkey: \nproxy.filters \nvalue: \n{json.dumps(self.ori_filters, ensure_ascii=False, indent=4)}') + + def restore(self): + self.save({}) + self.switch = False + self.configs = {} + application.config['proxy.filters'] = self.ori_filters + logger.warning(f'application.config is updated by proxy_white_list_switch, \nkey: \nproxy.filters \nvalue: \n{json.dumps(self.ori_filters, ensure_ascii=False, indent=4)}') + + def load_prepared(self): + self.configs = self.manager.get_config(self).get('data', {}) + self.ori_filters = application.config.get('proxy.filters', []) + have_config = False + for name, item in self.configs.items(): + if name == 'proxy_white_list_switch': + self.switch = bool(item) + else: + have_config |= bool(item) + if have_config and self.switch: + self.apply_config(self.configs) + + def apply_config(self, data): + filter_list = [] + new_reg = '' + white_reg = '' + black_reg = [] + if data['regular_list']: + filter_list.extend(data['regular_list']) + if data['white_list']: + white_reg = '|'.join(data['white_list']) + if data['black_list']: + black_reg.append('|'.join(data['black_list'])) + if data['black_suffix']: + black_reg.append('|'.join(data['black_suffix'])) + + if white_reg: + new_reg = f'(?=.*({white_reg}))' + if black_reg: + new_reg += f'(^(?!.*({"|".join(black_reg)})))' + + if not filter_list and not new_reg: + new_reg = f'(?=.*({"|".join(DEFAULT_WHITE_LIST)}))' + new_reg += f'(^(?!.*({"|".join(DEFAULT_BLACK_LIST)})))' + + if new_reg: + filter_list.append(new_reg) + application.config['proxy.filters'] = filter_list + logger.warning(f'application.config is updated by proxy_white_list_switch, \nkey: \nproxy.filters \nvalue: \n{json.dumps(filter_list, ensure_ascii=False, indent=4)}') + + def save(self, data): + self.manager.write_config(self, {'data': data}) + + def split_regex(self, regex): + white = [] + black = [] + + if '(?=' in regex or '(?!' in regex: + positive_parts = re.findall(r'\(\?=\.\*\(?(.*?)\)?\)', regex) + negative_parts = re.findall(r'\(\?!\.\*\(?(.*?)\)?\)', regex) + + if positive_parts: + for part in positive_parts: + white.extend(part.split('|')) + + if negative_parts: + for part in negative_parts: + black.extend(part.split('|')) + + white = [p for p in white if p.strip()] + black = [n for n in black if n.strip()] + + return white, black + + def get_config_by_application(self): + ori_filters = application.config.get('proxy.filters', []) + white_list = [] + black_list = [] + regular_list = [] + for pattern in ori_filters: + if self.is_simple_url.match(pattern): + white_list.append(pattern) + # Split into three judgments, limit the beginning and end, avoid complex regex misfires. + elif self.is_balck_and_white.match(pattern) or self.is_white.match(pattern) or self.is_balck.match(pattern): + res_white, res_black = self.split_regex(pattern) + white_list.extend(res_white) + black_list.extend(res_black) + else: + regular_list.append(pattern) + + white_list = list(set(white_list)) + black_list = list(set(black_list)) + regular_list = list(set(regular_list)) + + return { + 'white': white_list, + 'black': black_list, + 'regular': regular_list + } + + def get_suffix_black_list(self, black_list): + url_black_list = [item for item in black_list if not (item.startswith('.') and item.count('.') == 1)] + suffix_black_list = [item for item in black_list if item.startswith('.') and item.count('.') == 1] + return url_black_list, suffix_black_list diff --git a/lyrebird/manager.py b/lyrebird/manager.py index 727550b08..d9dc6979b 100644 --- a/lyrebird/manager.py +++ b/lyrebird/manager.py @@ -28,6 +28,7 @@ from lyrebird.log import LogServer from lyrebird.utils import RedisDict, RedisManager from lyrebird.compatibility import compat_redis_check +from lyrebird.settings import SettingsManager from lyrebird import utils logger = log.get_logger() @@ -209,6 +210,9 @@ def run(args: argparse.Namespace): config_str = json.dumps(config_dict, ensure_ascii=False, indent=4) logger.warning(f'Lyrebird start with config:\n{config_str}') + # Settings server + application.server['settings'] = SettingsManager() + application.server['settings'].load_settings() # Main server application.server['event'] = EventServer() # mutilprocess message dispatcher @@ -280,6 +284,8 @@ def run(args: argparse.Namespace): # main process is ready, publish system event application.status_ready() + application.server['settings'].load_finished() + threading.Event().wait() diff --git a/lyrebird/mock/blueprints/apis/__init__.py b/lyrebird/mock/blueprints/apis/__init__.py index 017f752f5..c30e30188 100644 --- a/lyrebird/mock/blueprints/apis/__init__.py +++ b/lyrebird/mock/blueprints/apis/__init__.py @@ -15,6 +15,7 @@ from .bandwidth import Bandwidth, BandwidthTemplates from .status_bar import StatusBar from .snapshot import SnapshotImport, SnapshotExport, Snapshot +from .settings import SettingsApi from lyrebird.log import get_logger from lyrebird import application @@ -92,3 +93,4 @@ def after_request(response): api_source.add_resource(Channel, '/channel', '/channel/') api_source.add_resource(StatusBar, '/statusbar', '/statusbar/') api_source.add_resource(EventFileInfo, '/event/fileinfo') +api_source.add_resource(SettingsApi, '/settings', '/settings/') diff --git a/lyrebird/mock/blueprints/apis/menu.py b/lyrebird/mock/blueprints/apis/menu.py index 02d22ed72..4d7df10f9 100644 --- a/lyrebird/mock/blueprints/apis/menu.py +++ b/lyrebird/mock/blueprints/apis/menu.py @@ -55,6 +55,15 @@ def get(self): 'version': plugin.version } }) + # Load settings page at last one + menu.append({ + 'name': 'settings', + 'title': 'Settings', + 'type': 'router', + 'path': '/settings', + 'icon': 'mdi-cog-outline' + }) + # When there is no actived menu, the first one is displayed by default if not application.active_menu: self.set_active_menu(menu[0]) diff --git a/lyrebird/mock/blueprints/apis/settings.py b/lyrebird/mock/blueprints/apis/settings.py new file mode 100644 index 000000000..f2b468597 --- /dev/null +++ b/lyrebird/mock/blueprints/apis/settings.py @@ -0,0 +1,73 @@ +from flask import request +from lyrebird import application +from flask_restful import Resource + +class SettingsApi(Resource): + + def get(self, action): + if action == 'list': + resp_dict = {} + for script_name, script in application.settings.items(): + if not script.inited or not script.template.display: + continue + if script.category_md5 not in resp_dict: + resp_dict[script.category_md5] = { + 'category_md5': script.category_md5, + 'category': script.category, + 'scripts': [] + } + script_dict = { + 'name': script.name, + 'title': script.title, + 'notice': script.notice, + 'category': script.category + } + resp_dict[script.category_md5]['scripts'].append(script_dict) + return application.make_ok_response(data=list(resp_dict.values())) + elif action == 'detail': + script_name = request.args.get('name') + if not script_name: + return application.make_fail_response('Get setting failed, the query \"name\" not found') + script = application.settings.get(script_name) + if not script: + return application.make_fail_response(f'Get setting failed, {script_name} does not exist') + resp_dict = { + 'name': script.name, + 'title': script.title, + 'notice': script.notice, + 'category': script.category, + 'language': script.language, + 'submitText': script.submit_text, + 'configs': script.getter() + } + return application.make_ok_response(data=resp_dict) + + def post(self): + script_name = request.args.get('name') + if not script_name: + return application.make_fail_response('Get setting failed, the query \"name\" not found') + + script = application.settings.get(script_name) + if not script: + return application.make_fail_response(f'Get setting failed, {script_name} does not exist') + + resp = script.setter(request.json.get('data')) + + if resp: + return application.make_fail_response(str(resp)) + return application.make_ok_response() + + def delete(self): + script_name = request.args.get('name') + if not script_name: + return application.make_fail_response('Get setting failed, the query \"name\" not found') + + script = application.settings.get(script_name) + if not script: + return application.make_fail_response(f'Get setting failed, {script_name} does not exist') + + resp = script.restore() + + if resp: + return application.make_fail_response(str(resp)) + return application.make_ok_response() diff --git a/lyrebird/settings/__init__.py b/lyrebird/settings/__init__.py new file mode 100644 index 000000000..c17929fe5 --- /dev/null +++ b/lyrebird/settings/__init__.py @@ -0,0 +1,2 @@ +from .settings_manager import * +from .settings_template import * diff --git a/lyrebird/settings/settings_manager.py b/lyrebird/settings/settings_manager.py new file mode 100644 index 000000000..2ff4a482a --- /dev/null +++ b/lyrebird/settings/settings_manager.py @@ -0,0 +1,215 @@ +import os +import json +import shutil +import inspect +import lyrebird +import importlib +import traceback + +from lyrebird import application +from lyrebird.log import get_logger +from copy import deepcopy +from types import FunctionType +from pathlib import Path +from lyrebird.base_server import StaticServer +from .settings_template import SettingsTemplate + +logger = get_logger() + +class Settings: + + def __init__(self, script): + self.script = script + self.name = f'{self.script.stem}' + self.inited = False + self.template = None + + def __getattr__(self, name): + if self.template and hasattr(self.template, name): + return getattr(self.template, name) + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def load(self): + try: + module_loader = importlib.util.spec_from_file_location(self.name, self.script) + module = importlib.util.module_from_spec(module_loader) + module_loader.loader.exec_module(module) + except Exception as e: + logger.error(f'Setting item load failed, file path:{str(self.script)}\n {traceback.format_exc()} \n {e}') + + for name, obj in inspect.getmembers(module): + if inspect.isclass(obj) and issubclass(obj, SettingsTemplate) and obj != SettingsTemplate: + try: + template = obj() + self.template = template + break + except Exception as e: + logger.error(f'Setting item init failed, file path:{self.name}\n {traceback.format_exc()} \n {e}') + if self.template: + self.inited = True + + def getter(self): + if not self.inited: + return f'Setting item operate is blocked, because it load failed, item name:{self.name}' + res = {} + try: + res = self.template.getter() + except Exception as e: + logger.error(f'Setting item getter failed, item name:{self.name}\n {traceback.format_exc()} \n {e}') + finally: + return res + + def setter(self, data): + if not self.inited: + return f'Setting item operate is blocked, because it load failed, item name:{self.name}' + res = '' + try: + res = self.template.setter(data) + except Exception as e: + res = f'Setting item setter failed, item name:{self.name}\n {traceback.format_exc()} \n {e}' + logger.error(res) + + application.server['event'].publish('lyrebird_metrics', {'lyrebird_metrics': { + 'sender': 'SettingsManager', + 'action': 'setter', + 'trace_info': json.dumps({'name':self.name, 'param':data, 'res':res}) + }}) + return res + + def restore(self): + if not self.inited: + return f'Setting item operate is blocked, because it load failed, item name:{self.name}' + res = '' + try: + res = self.template.restore() + except Exception as e: + res = f'Setting item restore failed, item name:{self.name}\n {traceback.format_exc()} \n {e}' + logger.error(res) + + application.server['event'].publish('lyrebird_metrics', {'lyrebird_metrics': { + 'sender': 'SettingsManager', + 'action': 'restore', + 'trace_info': json.dumps({'name':self.name, 'res':res}) + }}) + return res + + def load_finished(self): + if not self.inited: + return f'Setting item operate is blocked, because it load failed, item name:{self.name}' + res = '' + try: + res = self.template.load_finished() + except Exception as e: + res = f'Setting item load_finished failed, item name:{self.name}\n {traceback.format_exc()} \n {e}' + logger.error(res) + return res + + def load_prepared(self): + if not self.inited: + return f'Setting item operate is blocked, because it load failed, item name:{self.name}' + res = '' + try: + res = self.template.load_prepared() + except Exception as e: + res = f'Setting item load_prepared failed, item name:{self.name}\n {traceback.format_exc()} \n {e}' + logger.error(res) + return res + + def destory(self): + if not self.inited: + return f'Setting item operate is blocked, because it load failed, item name:{self.name}' + try: + self.template.destory() + except Exception as e: + logger.error(f'Setting item destory failed, item name:{self.name}\n {traceback.format_exc()} \n {e}') + + +class SettingsManager(StaticServer): + + def __init__(self): + self.SCRIPTS_DIR_TEMPLATE = Path(lyrebird.APPLICATION_CONF_DIR)/'settings' + self.PERSONAL_CONFIG_PATH = Path(lyrebird.APPLICATION_CONF_DIR)/'personal_conf.json' + self.EXAMPLE_DIR = Path(__file__).parent.parent/'examples'/'settings' + self.configs = deepcopy(application._cm.personal_config) + self.settings = application.settings + + def stop(self): + for name, setting in self.settings.items(): + setting.destory() + + def load_finished(self): + for name, setting in self.settings.items(): + setting.load_finished() + + def load_prepared(self): + for name, setting in self.settings.items(): + setting.load_prepared() + + def load_settings(self): + scripts_list = self.get_settings_list() + if not scripts_list: + return + + switch_conf = application.config.get('settings.switch', []) + for script in scripts_list: + if script.name not in switch_conf: + continue + if script.inited: + continue + + script.load() + if script.inited: + self.settings[script.name] = script + + self.load_prepared() + + def get_settings_list(self): + workspace_str = application.config.get('settings.workspace') + + if workspace_str: + workspace = Path(workspace_str) + if not workspace.expanduser().exists(): + logger.error(f'Settings scripts dir {workspace_str} not found!') + return + workspace_iterdir = self.get_iterdir_python(workspace) + if not workspace_iterdir: + logger.warning(f'No settings script found in dir {workspace_str}') + return + else: + workspace = Path(self.SCRIPTS_DIR_TEMPLATE) + workspace.mkdir(parents=True, exist_ok=True) + workspace_iterdir = self.get_iterdir_python(workspace) + if not workspace_iterdir: + self.copy_example_scripts() + + return workspace_iterdir + + @staticmethod + def get_iterdir_python(path): + path = Path(path) + end_str = '.py' + scripts_list = [Settings(i) for i in path.iterdir() if i.suffix == end_str] + return scripts_list + + def copy_example_scripts(self): + for example in self.EXAMPLE_DIR.iterdir(): + if not example.name.endswith('.py'): + continue + dst = self.SCRIPTS_DIR_TEMPLATE / example.name + shutil.copy(example, dst) + + def write_config(self, obj:SettingsTemplate, data={}): + if data and not isinstance(data, dict): + return + template_key = f'settings.{obj.name}' + template = { + template_key: self.configs.get(template_key, {}) + } + if data: + template[template_key].update(data) + self.configs.update(template) + application._cm.update_personal_config(template) + + def get_config(self, obj:SettingsTemplate): + template_key = f'settings.{obj.name}' + return self.configs.get(template_key, {}) diff --git a/lyrebird/settings/settings_template.py b/lyrebird/settings/settings_template.py new file mode 100644 index 000000000..68f27b550 --- /dev/null +++ b/lyrebird/settings/settings_template.py @@ -0,0 +1,58 @@ +from hashlib import md5 +from lyrebird import application + +class SettingsTemplate: + + def __init__(self): + self.display = True + self.name = '' + self.setting_items = [] + self.title = '' + self.notice = '' + self.submit_text = '' + self.language = '' + self.category = '' + self.category_md5 = md5(self.category.encode(encoding='UTF-8')).hexdigest() + self.manager = application.server['settings'] + + def getter(self): + pass + + def setter(self, data): + pass + + def restore(self): + pass + + def destory(self): + pass + + def load_finished(self): + pass + + def load_prepared(self): + pass + +class SettingItemTemplate: + def __init__(self): + self.name = '' + self.title = '' + self.subtitle = '' + # enum, value should in {'list', 'dict', 'text', 'selector'} + self.category = '' + # only used in selector + self.options = [] + + def get_data(self): + template_res = { + 'name': self.name, + 'title': self.title, + 'subtitle': self.subtitle, + 'category': self.category, + 'data': '', + 'options': self.options + } + return template_res + + def set_data(self): + return True diff --git a/lyrebird/version.py b/lyrebird/version.py index 37bdc06f2..4b0237019 100644 --- a/lyrebird/version.py +++ b/lyrebird/version.py @@ -1,3 +1,3 @@ -IVERSION = (3, 0, 6) +IVERSION = (3, 1, 0) VERSION = ".".join(str(i) for i in IVERSION) LYREBIRD = "Lyrebird " + VERSION