From bcc9cee3c128470750d11faf565185a968e38154 Mon Sep 17 00:00:00 2001 From: LeonOstrez Date: Fri, 4 Aug 2023 16:53:40 +0200 Subject: [PATCH] merge, add spinner, add project name and minor bug fixes --- euclid/database/database.py | 30 +++++++++-------- euclid/database/models/app.py | 1 + euclid/helpers/Project.py | 32 +++++++++++-------- euclid/helpers/agents/Architect.py | 9 +++--- euclid/helpers/agents/Developer.py | 3 +- euclid/helpers/agents/ProductOwner.py | 11 +++++-- euclid/helpers/agents/TechLead.py | 1 + euclid/main.py | 1 - .../prompts/architecture/technologies.prompt | 10 +++--- .../development/env_setup/specs.prompt | 2 +- euclid/prompts/development/plan.prompt | 8 ++--- .../prompts/development/task/breakdown.prompt | 8 ++--- .../prompts/high_level_questions/specs.prompt | 4 +-- euclid/prompts/prompts.py | 3 +- euclid/prompts/user_stories/specs.prompt | 4 +-- euclid/utils/files.py | 25 +++++++++++++++ euclid/utils/llm_connection.py | 24 +++++++++----- euclid/utils/spinner.py | 12 +++++++ euclid/utils/utils.py | 11 ++++++- 19 files changed, 134 insertions(+), 65 deletions(-) create mode 100644 euclid/utils/files.py create mode 100644 euclid/utils/spinner.py diff --git a/euclid/database/database.py b/euclid/database/database.py index 9044aa2..92d23fe 100644 --- a/euclid/database/database.py +++ b/euclid/database/database.py @@ -32,7 +32,6 @@ def save_user(user_id, email, password): return existing_user - def get_user(user_id=None, email=None): if not user_id and not email: raise ValueError("Either user_id or email must be provided") @@ -58,7 +57,7 @@ def save_app(args): user = get_user(user_id=args['user_id']) except ValueError: user = save_user(args['user_id'], args['email'], args['password']) - app = App.create(id=args['app_id'], user=user, app_type=args['app_type']) + app = App.create(id=args['app_id'], user=user, app_type=args['app_type'], name=args['name']) return app @@ -145,11 +144,11 @@ def save_development_step(app_id, prompt_path, prompt_data, llm_req_num, message }) try: inserted_id = (DevelopmentSteps - .insert(app=app, hash_id=hash_id, messages=messages, llm_response=response) - .on_conflict(conflict_target=[DevelopmentSteps.app, DevelopmentSteps.hash_id], - preserve=[DevelopmentSteps.messages, DevelopmentSteps.llm_response], - update={}) - .execute()) + .insert(app=app, hash_id=hash_id, messages=messages, llm_response=response) + .on_conflict(conflict_target=[DevelopmentSteps.app, DevelopmentSteps.hash_id], + preserve=[DevelopmentSteps.messages, DevelopmentSteps.llm_response], + update={}) + .execute()) dev_step = DevelopmentSteps.get_by_id(inserted_id) print(colored(f"Saved DEV step => {dev_step.id}", "yellow")) @@ -158,6 +157,7 @@ def save_development_step(app_id, prompt_path, prompt_data, llm_req_num, message return None return dev_step + def get_db_model_from_hash_id(data_to_hash, model, app_id): hash_id = hash_data(data_to_hash) try: @@ -166,6 +166,7 @@ def get_db_model_from_hash_id(data_to_hash, model, app_id): return None return db_row + def hash_and_save_step(Model, app_id, hash_data_args, data_fields, message): app = get_app(app_id) hash_id = hash_data(hash_data_args) @@ -179,11 +180,11 @@ def hash_and_save_step(Model, app_id, hash_data_args, data_fields, message): try: inserted_id = (Model - .insert(**data_to_insert) - .on_conflict(conflict_target=[Model.app, Model.hash_id], - preserve=[field for field in data_fields.keys()], - update={}) - .execute()) + .insert(**data_to_insert) + .on_conflict(conflict_target=[Model.app, Model.hash_id], + preserve=[field for field in data_fields.keys()], + update={}) + .execute()) record = Model.get_by_id(inserted_id) print(colored(f"{message} with id {record.id}", "yellow")) @@ -192,6 +193,7 @@ def hash_and_save_step(Model, app_id, hash_data_args, data_fields, message): return None return record + def save_command_run(project, command, cli_response): hash_data_args = { 'command': command, @@ -203,6 +205,7 @@ def save_command_run(project, command, cli_response): } return hash_and_save_step(CommandRuns, project.args['app_id'], hash_data_args, data_fields, "Saved Command Run") + def get_command_run_from_hash_id(project, command): data_to_hash = { 'command': command, @@ -257,11 +260,10 @@ def drop_tables(): Development, FileSnapshot, CommandRuns - ]: + ]: database.execute_sql(f'DROP TABLE IF EXISTS "{table._meta.table_name}" CASCADE') - if __name__ == "__main__": drop_tables() create_tables() diff --git a/euclid/database/models/app.py b/euclid/database/models/app.py index 4496ec9..ed6ffa0 100644 --- a/euclid/database/models/app.py +++ b/euclid/database/models/app.py @@ -7,4 +7,5 @@ from database.models.user import User class App(BaseModel): user = ForeignKeyField(User, backref='apps') app_type = CharField() + name = CharField() status = CharField(default='started') \ No newline at end of file diff --git a/euclid/helpers/Project.py b/euclid/helpers/Project.py index 4dcf801..4deacb6 100644 --- a/euclid/helpers/Project.py +++ b/euclid/helpers/Project.py @@ -12,31 +12,35 @@ from helpers.agents.ProductOwner import ProductOwner from database.models.development_steps import DevelopmentSteps from database.models.file_snapshot import FileSnapshot +from utils.files import get_parent_folder + class Project: - def __init__(self, args, name=None, description=None, user_stories=None, user_tasks=None, architecture=None, development_plan=None, current_step=None): + def __init__(self, args, name=None, description=None, user_stories=None, user_tasks=None, architecture=None, + development_plan=None, current_step=None): self.args = args self.llm_req_num = 0 self.command_runs_count = 0 self.skip_steps = False if ('skip_until_dev_step' in args and args['skip_until_dev_step'] == '0') else True self.skip_until_dev_step = args['skip_until_dev_step'] if 'skip_until_dev_step' in args else None # TODO make flexible + # self.root_path = get_parent_folder('euclid') self.root_path = '' - self.restore_files({dev_step_id_to_start_from}) + # self.restore_files({dev_step_id_to_start_from}) - if current_step != None: + if current_step is not None: self.current_step = current_step - if name != None: + if name is not None: self.name = name - if description != None: + if description is not None: self.description = description - if user_stories != None: + if user_stories is not None: self.user_stories = user_stories - if user_tasks != None: + if user_tasks is not None: self.user_tasks = user_tasks - if architecture != None: + if architecture is not None: self.architecture = architecture - if development_plan != None: + if development_plan is not None: self.development_plan = development_plan def start(self): @@ -53,7 +57,7 @@ class Project: self.developer = Developer(self) self.developer.set_up_environment(); - + self.developer.start_coding() def get_directory_tree(self): @@ -62,7 +66,7 @@ class Project: def get_test_directory_tree(self): # TODO remove hardcoded path return build_directory_tree(self.root_path + '/tests', ignore=IGNORE_FOLDERS) - + def get_files(self, files): files_with_content = [] for file in files: @@ -71,7 +75,7 @@ class Project: "content": open(self.get_full_file_path(file), 'r').read() }) return files_with_content - + def get_full_file_path(self, file_name): return self.root_path + '/' + file_name @@ -85,7 +89,7 @@ class Project: name=file['name'], defaults={'content': file.get('content', '')} ) - file_snapshot.content = content=file['content'] + file_snapshot.content = content = file['content'] file_snapshot.save() def restore_files(self, development_step_id): @@ -109,4 +113,4 @@ class Project: while answer != 'continue': answer = styled_text( 'Once you are ready, type "continue" to continue.', - ) \ No newline at end of file + ) diff --git a/euclid/helpers/agents/Architect.py b/euclid/helpers/agents/Architect.py index ac2b468..013b75a 100644 --- a/euclid/helpers/agents/Architect.py +++ b/euclid/helpers/agents/Architect.py @@ -31,10 +31,11 @@ class Architect(Agent): logger.info(f"Planning project architecture...") architecture = self.convo_architecture.send_message('architecture/technologies.prompt', - {'prompt': self.project.high_level_summary, - 'user_stories': self.project.user_stories, - 'user_tasks': self.project.user_tasks, - 'app_type': self.project.args['app_type']}, ARCHITECTURE) + {'name': self.project.args['name'], + 'prompt': self.project.high_level_summary, + 'user_stories': self.project.user_stories, + 'user_tasks': self.project.user_tasks, + 'app_type': self.project.args['app_type']}, ARCHITECTURE) if self.project.args.get('advanced', False): architecture = get_additional_info_from_user(architecture, 'architect') diff --git a/euclid/helpers/agents/Developer.py b/euclid/helpers/agents/Developer.py index 7367c91..86e5428 100644 --- a/euclid/helpers/agents/Developer.py +++ b/euclid/helpers/agents/Developer.py @@ -37,6 +37,7 @@ class Developer(Agent): print(colored('-------------------------', 'green')) convo_dev_task = AgentConvo(self) task_steps = convo_dev_task.send_message('development/task/breakdown.prompt', { + "name": self.project.args['name'], "app_summary": self.project.high_level_summary, "clarification": [], "user_stories": self.project.user_stories, @@ -94,7 +95,7 @@ class Developer(Agent): os_info = get_os_info() os_specific_techologies = self.convo_os_specific_tech.send_message('development/env_setup/specs.prompt', - { "os_info": os_info, "technologies": self.project.architecture }, FILTER_OS_TECHNOLOGIES) + { "name": self.project.args['name'], "os_info": os_info, "technologies": self.project.architecture }, FILTER_OS_TECHNOLOGIES) for technology in os_specific_techologies: llm_response = self.convo_os_specific_tech.send_message('development/env_setup/install_next_technology.prompt', diff --git a/euclid/helpers/agents/ProductOwner.py b/euclid/helpers/agents/ProductOwner.py index 09cbad3..2792bb3 100644 --- a/euclid/helpers/agents/ProductOwner.py +++ b/euclid/helpers/agents/ProductOwner.py @@ -4,9 +4,10 @@ from helpers.AgentConvo import AgentConvo from helpers.Agent import Agent from logger.logger import logger from database.database import save_progress, save_app, get_progress_steps -from utils.utils import execute_step, generate_app_data, step_already_finished +from utils.utils import execute_step, generate_app_data, step_already_finished, clean_filename +from utils.files import setup_workspace from prompts.prompts import ask_for_app_type, ask_for_main_app_definition, get_additional_info_from_openai, \ - generate_messages_from_description + generate_messages_from_description, ask_user from const.llm import END_RESPONSE @@ -27,13 +28,16 @@ class ProductOwner(Agent): # PROJECT DESCRIPTION self.project.args['app_type'] = ask_for_app_type() + self.project.args['name'] = clean_filename(ask_user('What is the project name?')) + + setup_workspace(self.project.root_path, self.project.args['name']) save_app(self.project.args) main_prompt = ask_for_main_app_definition() high_level_messages = get_additional_info_from_openai( - generate_messages_from_description(main_prompt, self.project.args['app_type'])) + generate_messages_from_description(main_prompt, self.project.args['app_type'], self.project.args['name'])) high_level_summary = convo_project_description.send_message('utils/summary.prompt', {'conversation': '\n'.join( @@ -68,6 +72,7 @@ class ProductOwner(Agent): logger.info(msg) self.project.user_stories = self.convo_user_stories.continuous_conversation('user_stories/specs.prompt', { + 'name': self.project.args['name'], 'prompt': self.project_description, 'app_type': self.project.args['app_type'], 'END_RESPONSE': END_RESPONSE diff --git a/euclid/helpers/agents/TechLead.py b/euclid/helpers/agents/TechLead.py index e61092d..1ef38f1 100644 --- a/euclid/helpers/agents/TechLead.py +++ b/euclid/helpers/agents/TechLead.py @@ -35,6 +35,7 @@ class TechLead(Agent): # TODO add clarifications self.development_plan = self.convo_development_plan.send_message('development/plan.prompt', { + "name": self.project.args['name'], "app_summary": self.project.high_level_summary, "clarification": [], "user_stories": self.project.user_stories, diff --git a/euclid/main.py b/euclid/main.py index a155c20..5b1f65e 100644 --- a/euclid/main.py +++ b/euclid/main.py @@ -7,7 +7,6 @@ from helpers.Project import Project from utils.utils import get_arguments from logger.logger import logger - def init(): load_dotenv() diff --git a/euclid/prompts/architecture/technologies.prompt b/euclid/prompts/architecture/technologies.prompt index a338524..a2dbe02 100644 --- a/euclid/prompts/architecture/technologies.prompt +++ b/euclid/prompts/architecture/technologies.prompt @@ -1,6 +1,6 @@ -You are working in a software development agency and a project manager approached you telling you that you're assigned to work on a new project. You are working on a {{app_type}} called Euclid and you need to create specifications on what technologies should be used in this project. +You are working in a software development agency and a project manager approached you telling you that you're assigned to work on a new project. You are working on a {{app_type}} called "{{ name }}" and you need to create specifications on what technologies should be used in this project. -Here is a high level description of Euclid: +Here is a high level description of "{{ name }}": ``` {{ prompt }} ``` @@ -13,18 +13,18 @@ A: {{ clarification.answer }} {% endfor %} ``` -Here are user stories that specify how users use Euclid: +Here are user stories that specify how users use "{{ name }}": ``` {% for story in user_stories %} - {{ story }} {% endfor %} ``` -Here are user tasks that specify what users need to do to interact with Euclid: +Here are user tasks that specify what users need to do to interact with "{{ name }}": ``` {% for task in user_tasks %} - {{ task }} {% endfor %} ``` -Now, based on the app's description, user stories and user tasks, think step by step and write up all technologies that will be used by your development team to create the app Euclid. Do not write any explanations behind your choices but only a list of technologies that will be used. \ No newline at end of file +Now, based on the app's description, user stories and user tasks, think step by step and write up all technologies that will be used by your development team to create the app "{{ name }}". Do not write any explanations behind your choices but only a list of technologies that will be used. \ No newline at end of file diff --git a/euclid/prompts/development/env_setup/specs.prompt b/euclid/prompts/development/env_setup/specs.prompt index 8e7b944..1ea1b33 100644 --- a/euclid/prompts/development/env_setup/specs.prompt +++ b/euclid/prompts/development/env_setup/specs.prompt @@ -1,4 +1,4 @@ -You are working in a software development agency and a project manager and software architect approach you telling you that you're assigned to work on a new project. You are working on a web app called Euclid and your first job is to set up the environment on a computer. +You are working in a software development agency and a project manager and software architect approach you telling you that you're assigned to work on a new project. You are working on a web app called "{{ name }}" and your first job is to set up the environment on a computer. Here are the technologies that you need to use for this project: ``` diff --git a/euclid/prompts/development/plan.prompt b/euclid/prompts/development/plan.prompt index 324cf32..40cfbd2 100644 --- a/euclid/prompts/development/plan.prompt +++ b/euclid/prompts/development/plan.prompt @@ -1,6 +1,6 @@ -You are working in a software development agency and a project manager and software architect approach you telling you that you're assigned to work on a new project. You are working on a web app called Euclid and you need to create a detailed development plan so that developers can start developing the app. +You are working in a software development agency and a project manager and software architect approach you telling you that you're assigned to work on a new project. You are working on a web app called "{{ name }}" and you need to create a detailed development plan so that developers can start developing the app. -Here is a high level description of Euclid: +Here is a high level description of "{{ name }}": ``` {{ app_summary }} ``` @@ -13,14 +13,14 @@ A: {{ clarification.answer }} {% endfor %} ``` -Here are user stories that specify how users use Euclid: +Here are user stories that specify how users use "{{ name }}": ``` {% for story in user_stories %} - {{ story }} {% endfor %} ``` -Here are user tasks that specify what users need to do to interact with Euclid: +Here are user tasks that specify what users need to do to interact with "{{ name }}": ``` {% for task in user_tasks %} - {{ task }} diff --git a/euclid/prompts/development/task/breakdown.prompt b/euclid/prompts/development/task/breakdown.prompt index 539a8d4..f681774 100644 --- a/euclid/prompts/development/task/breakdown.prompt +++ b/euclid/prompts/development/task/breakdown.prompt @@ -1,6 +1,6 @@ -You are working on a web app called Euclid and you need to write code for the entire application based on the tasks that the tech lead gives you. So that you understand better what you're working on, you're given other specs for Euclid as well. +You are working on a web app called "{{ name }}" and you need to write code for the entire application based on the tasks that the tech lead gives you. So that you understand better what you're working on, you're given other specs for "{{ name }}" as well. -Here is a high level description of Euclid: +Here is a high level description of "{{ name }}": ``` {{ app_summary }} ``` @@ -13,12 +13,12 @@ A: {{ clarification.answer }} {% endfor %} ``` #} -Here are user stories that specify how users use Euclid: +Here are user stories that specify how users use "{{ name }}": ```{% for story in user_stories %} - {{ story }}{% endfor %} ``` -Here are user tasks that specify what users need to do to interact with Euclid: +Here are user tasks that specify what users need to do to interact with "{{ name }}": ```{% for task in user_tasks %} - {{ task }}{% endfor %} ``` diff --git a/euclid/prompts/high_level_questions/specs.prompt b/euclid/prompts/high_level_questions/specs.prompt index 9e5f535..443f370 100644 --- a/euclid/prompts/high_level_questions/specs.prompt +++ b/euclid/prompts/high_level_questions/specs.prompt @@ -1,4 +1,4 @@ -I want you to create the application (let's call it Euclid) that can be described like this: +I want you to create the application (let's call it "{{ name }}") that can be described like this: ``` {{ prompt }} ``` @@ -19,6 +19,6 @@ Here is an overview of the tasks that you need to do: 3. Break down user tasks. In this task, you will think about the app description, answers from step #1 and the user stories from the step #2 and create a list of user tasks that a user needs to do to interact with the app. In the example description, user tasks could be: - `user runs the CLI command in which they specify the keyword youtube channel needs to contain and the location where the CSV file will be saved to` -Let's start with the task #1 Getting additional answers. Think about the description for the app Euclid and ask questions that you would like to get cleared before going onto breaking down the user stories. +Let's start with the task #1 Getting additional answers. Think about the description for the app "{{ name }}" and ask questions that you would like to get cleared before going onto breaking down the user stories. {{single_question}} diff --git a/euclid/prompts/prompts.py b/euclid/prompts/prompts.py index 40926a2..96bb361 100644 --- a/euclid/prompts/prompts.py +++ b/euclid/prompts/prompts.py @@ -132,8 +132,9 @@ def get_additional_info_from_user(messages, role): return updated_messages -def generate_messages_from_description(description, app_type): +def generate_messages_from_description(description, app_type, name): prompt = get_prompt('high_level_questions/specs.prompt', { + 'name': name, 'prompt': description, 'app_type': app_type, 'MAX_QUESTIONS': MAX_QUESTIONS diff --git a/euclid/prompts/user_stories/specs.prompt b/euclid/prompts/user_stories/specs.prompt index 87c13a6..1630998 100644 --- a/euclid/prompts/user_stories/specs.prompt +++ b/euclid/prompts/user_stories/specs.prompt @@ -1,4 +1,4 @@ -I want you to create {{ app_type }} (let's call it Euclid) that can be described like this: +I want you to create {{ app_type }} (let's call it "{{ name }}") that can be described like this: ``` {{ prompt }} ``` @@ -11,7 +11,7 @@ A: {{ clarification.answer }} {% endfor %} ``` -Think step by step about the description for the app Euclid and the additional questions and answers and break down user stories. You will think about the app description and the answers listed and create a list of all user stories. A user story is a description of how a user can interact with the app. For example, if an app's description is `Create a script that finds Youtube channels with the word "test" inside the channel name`, user stories could be: +Think step by step about the description for the app "{{ name }}" and the additional questions and answers and break down user stories. You will think about the app description and the answers listed and create a list of all user stories. A user story is a description of how a user can interact with the app. For example, if an app's description is `Create a script that finds Youtube channels with the word "test" inside the channel name`, user stories could be: - `user will run the script from the CLI` - `user will get the list of all channels in a CSV file` diff --git a/euclid/utils/files.py b/euclid/utils/files.py new file mode 100644 index 0000000..15fe6ab --- /dev/null +++ b/euclid/utils/files.py @@ -0,0 +1,25 @@ +import os +from pathlib import Path + + +def get_parent_folder(folder_name): + current_path = Path(os.path.abspath(__file__)) # get the path of the current script + + while current_path.name != folder_name: # while the current folder name is not 'folder_name' + current_path = current_path.parent # go up one level + + return current_path.parent + + +def setup_workspace(root, project_name): + create_directory(root, 'workspace') + project_path = create_directory(os.path.join(root, 'workspace'), project_name) + create_directory(project_path, 'tests') + return + + +def create_directory(parent_directory, new_directory): + new_directory_path = os.path.join(parent_directory, new_directory) + os.makedirs(new_directory_path, exist_ok=True) + + return new_directory_path diff --git a/euclid/utils/llm_connection.py b/euclid/utils/llm_connection.py index cfc006e..f4a1b20 100644 --- a/euclid/utils/llm_connection.py +++ b/euclid/utils/llm_connection.py @@ -10,6 +10,7 @@ from const.llm import MIN_TOKENS_FOR_GPT_RESPONSE, MAX_GPT_MODEL_TOKENS, MAX_QUE from logger.logger import logger from termcolor import colored from utils.utils import get_prompt_components, escape_json_special_chars +from utils.spinner import spinner_start, spinner_stop def connect_to_llm(): @@ -44,7 +45,8 @@ def get_tokens_in_messages(messages: List[str]) -> int: return sum(len(tokens) for tokens in tokenized_messages) -def create_gpt_chat_completion(messages: List[dict], req_type, min_tokens=MIN_TOKENS_FOR_GPT_RESPONSE, function_calls=None): +def create_gpt_chat_completion(messages: List[dict], req_type, min_tokens=MIN_TOKENS_FOR_GPT_RESPONSE, + function_calls=None): api_key = os.getenv("OPENAI_API_KEY") # tokens_in_messages = get_tokens_in_messages(messages) tokens_in_messages = 100 @@ -64,7 +66,7 @@ def create_gpt_chat_completion(messages: List[dict], req_type, min_tokens=MIN_TO if len(function_calls['definitions']) > 1: gpt_data['function_call'] = 'auto' else: - gpt_data['function_call'] = { 'name': function_calls['definitions'][0]['name'] } + gpt_data['function_call'] = {'name': function_calls['definitions'][0]['name']} try: response = stream_gpt_completion(gpt_data, req_type) @@ -77,7 +79,12 @@ def create_gpt_chat_completion(messages: List[dict], req_type, min_tokens=MIN_TO def stream_gpt_completion(data, req_type): - print(colored("Waiting for OpenAI API response...", 'yellow')) + def return_result(result_data): + # spinner_stop(spinner) + return result_data + + # spinner = spinner_start(colored("Waiting for OpenAI API response...", 'yellow')) + colored("Waiting for OpenAI API response...", 'yellow') api_key = os.getenv("OPENAI_API_KEY") logger.info(f'Request data: {data}') @@ -95,10 +102,10 @@ def stream_gpt_completion(data, req_type): if response.status_code != 200: print(f'problem with request: {response.text}') logger.debug(f'problem with request: {response.text}') - return {} + return return_result({}) gpt_response = '' - function_calls = { 'name': '', 'arguments': '' } + function_calls = {'name': '', 'arguments': ''} for line in response.iter_lines(): # Ignore keep-alive new lines @@ -116,7 +123,7 @@ def stream_gpt_completion(data, req_type): json_line = json_loads_with_escape(line) if json_line['choices'][0]['finish_reason'] == 'function_call': function_calls['arguments'] = json_loads_with_escape(function_calls['arguments']) - return { 'function_calls': function_calls }; + return return_result({'function_calls': function_calls}); json_line = json_line['choices'][0]['delta'] except json.JSONDecodeError: @@ -140,15 +147,16 @@ def stream_gpt_completion(data, req_type): if function_calls['arguments'] != '': logger.info(f'Response via function call: {function_calls["arguments"]}') function_calls['arguments'] = json_loads_with_escape(function_calls['arguments']) - return { 'function_calls': function_calls }; + return return_result({'function_calls': function_calls}); logger.info(f'Response message: {gpt_response}') new_code = postprocessing(gpt_response, req_type) # TODO add type dynamically - return { 'text': new_code } + return return_result({'text': new_code}) def postprocessing(gpt_response, req_type): return gpt_response + def json_loads_with_escape(str): # return json.loads(escape_json_special_chars(str)) return json.loads(str) diff --git a/euclid/utils/spinner.py b/euclid/utils/spinner.py new file mode 100644 index 0000000..ba9e881 --- /dev/null +++ b/euclid/utils/spinner.py @@ -0,0 +1,12 @@ +from yaspin import yaspin +from yaspin.spinners import Spinners + + +def spinner_start(text="Processing..."): + spinner = yaspin(Spinners.line, text=text) + spinner.start() + return spinner + + +def spinner_stop(spinner): + spinner.stop() diff --git a/euclid/utils/utils.py b/euclid/utils/utils.py index 007f85a..5f4afcb 100644 --- a/euclid/utils/utils.py +++ b/euclid/utils/utils.py @@ -196,4 +196,13 @@ def escape_json_special_chars(s): for char, replacement in replacements.items(): s = s.replace(char, replacement) - return s \ No newline at end of file + return s + +def clean_filename(filename): + # Remove invalid characters + cleaned_filename = re.sub(r'[<>:"/\\|?*]', '', filename) + + # Replace whitespace with underscore + cleaned_filename = re.sub(r'\s', '_', cleaned_filename) + + return cleaned_filename