diff --git a/README.md b/README.md index 207e9ef7e..8aeeba032 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ They are: - [How we Calculate DORA](#-how-we-calculate-dora) - [Roadmap](#%EF%B8%8F-roadmap) - [Contributing guidelines](#%EF%B8%8F-contributing-guidelines) + - [Developer Automations](#-developer-automations) - [Security guidelines](#%EF%B8%8F-security-guidelines) - [License](#license) @@ -371,6 +372,29 @@ To get started contributing to middleware check out our [CONTRIBUTING.md](https: We appreciate your contributions and look forward to working together to make Middleware even better! +## 👨‍💻 Developer Automations + +This sections contains some automation scripts that can generate boilder plate code to extend certain features and ship faster 🚀 + +### 1. Adding New Settings in Backend + +- Context: Adding a new setting required context of the Settings System, changes across some files and making adpater and defaults based on the new setting class structure. +- This can now be done by running the `python make_new_setting.py` scripts in the `./backend/dev_scripts` directory + +If you are in the root directory, you can run: +``` +python ./backend/dev_scripts/make_new_setting.py +``` + +- Enter the Setting name in the consitent format. +- Add the required keys and thier types. Enter `done` once you have added all the fields. +- Update Imports and Linting. +- You are good to go :tada" +- Note: For more non primite types in the setting such as uuid, enums etc, you will have to make changes to the generated adpaters. + + +https://github.com/middlewarehq/middleware/assets/70485812/f0529fa7-a2cb-44b1-ae07-2a7c97f56bef + # ⛓️ Security guidelines To get started contributing to middleware check out our [SECURITY.md](https://github.com/middlewarehq/middleware/blob/main/SECURITY.md). diff --git a/backend/analytics_server/mhq/api/resources/settings_resource.py b/backend/analytics_server/mhq/api/resources/settings_resource.py index 54dbf82f7..750c3e9bb 100644 --- a/backend/analytics_server/mhq/api/resources/settings_resource.py +++ b/backend/analytics_server/mhq/api/resources/settings_resource.py @@ -24,7 +24,6 @@ def _add_entity(config_settings: ConfigurationSettings, response): def _add_setting_data(config_settings: ConfigurationSettings, response): - # Add new if statements to add settings response for new settings if isinstance(config_settings.specific_settings, IncidentSettings): response["setting"] = { "title_includes": config_settings.specific_settings.title_filters @@ -50,6 +49,8 @@ def _add_setting_data(config_settings: ConfigurationSettings, response): ] } + # ADD NEW API ADAPTER HERE + return response response = { diff --git a/backend/analytics_server/mhq/service/settings/configuration_settings.py b/backend/analytics_server/mhq/service/settings/configuration_settings.py index 0fa72e099..fb229386e 100644 --- a/backend/analytics_server/mhq/service/settings/configuration_settings.py +++ b/backend/analytics_server/mhq/service/settings/configuration_settings.py @@ -58,6 +58,8 @@ def _adapt_incident_types_setting_from_setting_data( ] ) + # ADD NEW DICT TO DATACLASS ADAPTERS HERE + def _handle_config_setting_from_db_setting( self, setting_type: SettingType, setting_data ): @@ -75,6 +77,8 @@ def _handle_config_setting_from_db_setting( if setting_type == SettingType.INCIDENT_SOURCES_SETTING: return self._adapt_incident_source_setting_from_setting_data(setting_data) + # ADD NEW HANDLE FROM DB SETTINGS HERE + raise Exception(f"Invalid Setting Type: {setting_type}") def _adapt_config_setting_from_db_setting(self, setting: Settings): @@ -147,6 +151,8 @@ def _adapt_incident_types_setting_from_json( ] ) + # ADD NEW DICT TO API ADAPTERS HERE + def _handle_config_setting_from_json_data( self, setting_type: SettingType, setting_data ): @@ -164,6 +170,8 @@ def _handle_config_setting_from_json_data( if setting_type == SettingType.INCIDENT_TYPES_SETTING: return self._adapt_incident_types_setting_from_json(setting_data) + # ADD NEW HANDLE FROM JSON DATA HERE + raise Exception(f"Invalid Setting Type: {setting_type}") def _adapt_incident_setting_json_data( @@ -195,6 +203,8 @@ def _adapt_incident_types_setting_json_data( ] } + # ADD NEW DATACLASS TO JSON DATA ADAPTERS HERE + def _handle_config_setting_to_db_setting( self, setting_type: SettingType, specific_setting ): @@ -219,6 +229,8 @@ def _handle_config_setting_to_db_setting( ): return self._adapt_incident_source_setting_json_data(specific_setting) + # ADD NEW HANDLE TO DB SETTINGS HERE + raise Exception(f"Invalid Setting Type: {setting_type}") def _adapt_specific_setting_data_from_json( diff --git a/backend/analytics_server/mhq/service/settings/default_settings_data.py b/backend/analytics_server/mhq/service/settings/default_settings_data.py index ae7007889..391b2d878 100644 --- a/backend/analytics_server/mhq/service/settings/default_settings_data.py +++ b/backend/analytics_server/mhq/service/settings/default_settings_data.py @@ -26,4 +26,6 @@ def get_default_setting_data(setting_type: SettingType): "incident_types": [incident_type.value for incident_type in incident_types] } + # ADD NEW DEFAULT SETTING HERE + raise Exception(f"Invalid Setting Type: {setting_type}") diff --git a/backend/analytics_server/mhq/service/settings/models.py b/backend/analytics_server/mhq/service/settings/models.py index a9b8bea9f..24fddbb10 100644 --- a/backend/analytics_server/mhq/service/settings/models.py +++ b/backend/analytics_server/mhq/service/settings/models.py @@ -39,3 +39,12 @@ class IncidentTypesSetting(BaseSetting): @dataclass class IncidentSourcesSetting(BaseSetting): incident_sources: List[IncidentSource] + + +# ADD NEW SETTING CLASS HERE + +# Sample Future Settings +# @dataclass +# class PRSettings(BaseSetting): +# number_filters: List[str] +# merge_time: List[str] diff --git a/backend/analytics_server/mhq/service/settings/setting_type_validator.py b/backend/analytics_server/mhq/service/settings/setting_type_validator.py index c85f0e83c..8ff52d334 100644 --- a/backend/analytics_server/mhq/service/settings/setting_type_validator.py +++ b/backend/analytics_server/mhq/service/settings/setting_type_validator.py @@ -16,4 +16,6 @@ def settings_type_validator(setting_type: str): if setting_type == SettingType.INCIDENT_SOURCES_SETTING.value: return SettingType.INCIDENT_SOURCES_SETTING + # ADD NEW VALIDATOR HERE + raise BadRequest(f"Invalid Setting Type: {setting_type}") diff --git a/backend/analytics_server/mhq/store/models/settings/configuration_settings.py b/backend/analytics_server/mhq/store/models/settings/configuration_settings.py index 7b079961b..3cb0b52b2 100644 --- a/backend/analytics_server/mhq/store/models/settings/configuration_settings.py +++ b/backend/analytics_server/mhq/store/models/settings/configuration_settings.py @@ -16,6 +16,7 @@ class SettingType(Enum): INCIDENT_TYPES_SETTING = "INCIDENT_TYPES_SETTING" INCIDENT_SOURCES_SETTING = "INCIDENT_SOURCES_SETTING" EXCLUDED_PRS_SETTING = "EXCLUDED_PRS_SETTING" + # ADD NEW SETTING TYPE ENUM HERE class Settings(db.Model): diff --git a/backend/dev_scripts/make_new_setting.py b/backend/dev_scripts/make_new_setting.py new file mode 100644 index 000000000..1fd50278f --- /dev/null +++ b/backend/dev_scripts/make_new_setting.py @@ -0,0 +1,271 @@ +""" +This script can be used to generate code for a new setting. +""" +import re +from typing import List, Tuple + +CONFIGURATION_SETTINGS_PATH = ( + "../analytics_server/mhq/store/models/settings/configuration_settings.py" +) +SETTING_UTIL_PATH = "../analytics_server/mhq/service/settings/setting_type_validator.py" +MODELS_PATH = "../analytics_server/mhq/service/settings/models.py" +DEFAULT_SETTINGS_DATA_PATH = ( + "../analytics_server/mhq/service/settings/default_settings_data.py" +) +SETTINGS_SERVICE_PATH = ( + "../analytics_server/mhq/service/settings/configuration_settings.py" +) +SETTINGS_RESOURCE_PATH = "../analytics_server/mhq/api/resources/settings_resource.py" + + +def get_fields() -> List[Tuple[str, str]]: + fields = [] + print("Enter the fields for the new setting type (enter 'done' when finished):") + while True: + field_name = input("Field name: ") + if field_name.lower() == "done": + break + field_type = input("Field type: ") + fields.append((field_name, field_type)) + return fields + + +def generate_default_values(fields: List[Tuple[str, str]]): + defaults = {} + for name, type_ in fields: + if type_ == "list": + defaults[name] = [] + if type_ == "str": + defaults[name] = "" + else: + defaults[name] = None + return defaults + + +def convert_to_pascal_case(snake_str: str) -> str: + components = snake_str.lower().split("_") + return "".join(x.capitalize() for x in components) + + +def append_setting_suffix(setting_type: str) -> str: + return ( + setting_type + if setting_type.split("_")[-1] == "SETTING" + else setting_type + "_SETTING" + ) + + +def get_class_name(setting_type: str) -> str: + return convert_to_pascal_case(setting_type) + + +def add_to_setting_type_enum(setting_type): + with open(CONFIGURATION_SETTINGS_PATH, "r") as file: + content = file.read() + + enum_pattern = r"(?P\s*)# ADD NEW SETTING TYPE ENUM HERE\n" + match = re.search(enum_pattern, content) + if match: + indent = match.group("indent") + new_enum_entry = f'{indent}{setting_type.upper()} = "{setting_type.upper()}"\n' + content = re.sub(enum_pattern, new_enum_entry + match.group(0), content) + + with open(CONFIGURATION_SETTINGS_PATH, "w") as file: + file.write(content) + + +def extend_settings_type_validator(setting_type): + with open(SETTING_UTIL_PATH, "r") as file: + content = file.read() + + validator_pattern = r"(?P\s*)# ADD NEW VALIDATOR HERE\n" + match = re.search(validator_pattern, content) + if match: + indent = match.group("indent") + new_validator_entry = ( + f"{indent}if setting_type == SettingType.{setting_type.upper()}.value:\n" + ) + new_validator_entry += ( + f"{indent} return SettingType.{setting_type.upper()}\n" + ) + content = re.sub( + validator_pattern, new_validator_entry + match.group(0), content + ) + + with open(SETTING_UTIL_PATH, "w") as file: + file.write(content) + + +def add_new_setting_class(setting_type, fields): + class_name = get_class_name(setting_type) + + with open(MODELS_PATH, "r") as file: + content = file.read() + + fields_str = "\n ".join([f"{name}: {type_}" for name, type_ in fields]) + new_class_entry = f""" +@dataclass +class {class_name}(BaseSetting): + {fields_str} +""" + class_pattern = r"(?P\s*)# ADD NEW SETTING CLASS HERE\n" + match = re.search(class_pattern, content) + if match: + indent = match.group("indent") + content = re.sub( + class_pattern, + f"{indent}{new_class_entry.strip()}\n{match.group(0)}", + content, + ) + + with open(MODELS_PATH, "w") as file: + file.write(content) + + +def update_default_setting_data(setting_type, default_values): + with open(DEFAULT_SETTINGS_DATA_PATH, "r") as file: + content = file.read() + + default_pattern = r"(?P\s*)# ADD NEW DEFAULT SETTING HERE\n" + match = re.search(default_pattern, content) + if match: + indent = match.group("indent") + new_default_entry = ( + f"{indent}if setting_type == SettingType.{setting_type.upper()}:\n" + ) + new_default_entry += f"{indent} return {default_values}\n" + content = re.sub(default_pattern, new_default_entry + match.group(0), content) + + with open(DEFAULT_SETTINGS_DATA_PATH, "w") as file: + file.write(content) + + +def update_settings_service(setting_type, fields): + class_name = get_class_name(setting_type) + + with open(SETTINGS_SERVICE_PATH, "r") as file: + content = file.read() + + fields_str = ", ".join([f'{name}=data.get("{name}", None)' for name, _ in fields]) + adapt_data_func = f""" + def _adapt_{setting_type.lower()}_setting_from_setting_data(self, data: Dict[str, any]): + return {class_name}({fields_str}) +""" + adapt_json_func = f""" + def _adapt_{setting_type.lower()}_setting_from_json(self, data: Dict[str, any]): + return {class_name}({fields_str}) +""" + fields_dict_str = ", ".join( + [f'"{name}": specific_setting.{name}' for name, _ in fields] + ) + adapt_json_data_func = f""" + def _adapt_{setting_type.lower()}_setting_json_data(self, specific_setting: {class_name}): + return {{{fields_dict_str}}} +""" + + handle_from_db_func = f""" + if setting_type == SettingType.{setting_type.upper()}: + return self._adapt_{setting_type.lower()}_setting_from_setting_data(setting_data) +""" + handle_from_json_func = f""" + if setting_type == SettingType.{setting_type.upper()}: + return self._adapt_{setting_type.lower()}_setting_from_json(setting_data) +""" + handle_to_db_func = f""" + if setting_type == SettingType.{setting_type.upper()} and isinstance(specific_setting, {class_name}): + return self._adapt_{setting_type.lower()}_setting_json_data(specific_setting) +""" + + content = re.sub( + r"(?P\s*)# ADD NEW DICT TO DATACLASS ADAPTERS HERE\n", + lambda m: f"{m.group('indent')}{adapt_data_func.strip()}\n{m.group(0)}", + content, + ) + content = re.sub( + r"(?P\s*)# ADD NEW DICT TO API ADAPTERS HERE\n", + lambda m: f"{m.group('indent')}{adapt_json_func.strip()}\n{m.group(0)}", + content, + ) + content = re.sub( + r"(?P\s*)# ADD NEW DATACLASS TO JSON DATA ADAPTERS HERE\n", + lambda m: f"{m.group('indent')}{adapt_json_data_func.strip()}\n{m.group(0)}", + content, + ) + content = re.sub( + r"(?P\s*)# ADD NEW HANDLE FROM DB SETTINGS HERE\n", + lambda m: f"{m.group('indent')}{handle_from_db_func.strip()}\n{m.group(0)}", + content, + ) + content = re.sub( + r"(?P\s*)# ADD NEW HANDLE FROM JSON DATA HERE\n", + lambda m: f"{m.group('indent')}{handle_from_json_func.strip()}\n{m.group(0)}", + content, + ) + content = re.sub( + r"(?P\s*)# ADD NEW HANDLE TO DB SETTINGS HERE\n", + lambda m: f"{m.group('indent')}{handle_to_db_func.strip()}\n{m.group(0)}", + content, + ) + + with open(SETTINGS_SERVICE_PATH, "w") as file: + file.write(content) + + +def update_api_adapter(setting_type, fields): + class_name = get_class_name(setting_type) + + with open(SETTINGS_RESOURCE_PATH, "r") as file: + content = file.read() + + fields_str = ", ".join( + [f'"{name}": config_settings.specific_settings.{name}' for name, _ in fields] + ) + new_api_entry = f""" + if isinstance(config_settings.specific_settings, {class_name}): + response["setting"] = {{ + {fields_str} + }} +""" + api_pattern = r"(?P\s*)# ADD NEW API ADAPTER HERE\n" + match = re.search(api_pattern, content) + if match: + indent = match.group("indent") + content = re.sub( + api_pattern, f"{indent}{new_api_entry.strip()}\n{match.group(0)}", content + ) + + with open(SETTINGS_RESOURCE_PATH, "w") as file: + file.write(content) + + +def main(): + setting_type = input( + "Enter the new setting type (e.g., EXCLUDED_TICKET_TYPES_SETTING): " + ) + setting_type = append_setting_suffix(setting_type) + + fields: List[Tuple[str, str]] = get_fields() + default_values = generate_default_values(fields) + add_to_setting_type_enum(setting_type) + extend_settings_type_validator(setting_type) + add_new_setting_class(setting_type, fields) + update_default_setting_data(setting_type, default_values) + update_settings_service(setting_type, fields) + update_api_adapter(setting_type, fields) + + print("-----Files Updated-----\n") + + print(CONFIGURATION_SETTINGS_PATH) + print(SETTING_UTIL_PATH) + print(MODELS_PATH) + print(DEFAULT_SETTINGS_DATA_PATH) + print(SETTINGS_SERVICE_PATH) + print(SETTINGS_RESOURCE_PATH) + + print("-----------------------\n") + + print(f"Setting type {setting_type} added successfully.") + print(f"Please fix imports in the updated files and set default settings.") + + +main()