diff --git a/.gitignore b/.gitignore index 7ba1b39..6a8ae99 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ pyproject.toml .python-version requirements.txt requirement/ignore_requirements.txt -screenshots \ No newline at end of file +screenshots +script/todo/todo_override.json diff --git a/Makefile b/Makefile index 41dbc27..a498c59 100644 --- a/Makefile +++ b/Makefile @@ -725,6 +725,14 @@ open_test_coverage: # TODO load specific test file : ./run.sh -d test_file --log-level=test --test-enable --stop-after-init --test-file ./.venv/test.py +######## +# TODO # +######## + +.PHONY: todo +todo: + ./script/todo/todo.py + ######### # tag # ######### diff --git a/conf/pycharm_default_configuration.csv b/conf/pycharm_default_configuration.csv index 79675da..8e4f437 100644 --- a/conf/pycharm_default_configuration.csv +++ b/conf/pycharm_default_configuration.csv @@ -28,4 +28,5 @@ create demo internal,./script/code_generator/new_project.py,-m demo_internal -d run_robotlibre,./odoo/odoo-bin,--limit-time-real 999999 -c config.conf -d robotlibre,robotlibre, run_robotlibre_update,./odoo/odoo-bin,--limit-time-real 999999 --no-http --stop-after-init --dev cg -d robotlibre -i erplibre_devops -u erplibre_devops,robotlibre, run_robotlibre_generate,./script/code_generator/new_project.py,-f -d ./addons/ERPLibre_erplibre_addons -m erplibre_devops,robotlibre, +run_todo,./script/todo/todo.py,,TODO, diff --git a/script/todo/README.md b/script/todo/README.md new file mode 100644 index 0000000..5fdc582 --- /dev/null +++ b/script/todo/README.md @@ -0,0 +1,2 @@ +TODO is an assistant robot to use ERPLibre +Execute it with `./script/todo/todo.py` or `make todo`. diff --git a/script/todo/logo_ascii.txt b/script/todo/logo_ascii.txt new file mode 100644 index 0000000..63213ca --- /dev/null +++ b/script/todo/logo_ascii.txt @@ -0,0 +1,9 @@ + __ + _(\ |@@| +(__/\__ \--/ __ + \___|----| | __ + \ }{ /\ )_ / _\ + /\__/\ \__O (__ + (--/\--) \__/ + _)( )(_ + `---''---` \ No newline at end of file diff --git a/script/todo/todo.json b/script/todo/todo.json new file mode 100644 index 0000000..eefdefa --- /dev/null +++ b/script/todo/todo.json @@ -0,0 +1,23 @@ +{ + "kdbx": { + "path": "", + "password": "" + }, + "kdbx_config": { + "openai": { + "kdbx_key": "OpenAI api" + } + }, + "instance": [ + { + "prompt_description": "Test - Instance de base minimale", + "makefile_cmd": "db_restore_erplibre_base_db_test", + "database": "test" + } + ], + "function": [ + { + "prompt_description": "Ouvrir ERPLibre avec TODO \uD83E\uDD16" + } + ] +} diff --git a/script/todo/todo.py b/script/todo/todo.py new file mode 100755 index 0000000..f028aa0 --- /dev/null +++ b/script/todo/todo.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python + +import getpass +import json +import logging +import os +import subprocess +import tkinter as tk +from tkinter import filedialog + +import click +import openai +from pykeepass import PyKeePass + +# TODO implement urwid to improve text user interface +# import urwid +_logger = logging.getLogger(__name__) + +CONFIG_FILE = "./script/todo/todo.json" +CONFIG_OVERRIDE_FILE = "./script/todo/todo_override.json" +LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt" + + +class TODO: + def __init__(self): + self.kdbx = None + + def run(self): + with open(LOGO_ASCII_FILE) as my_file: + print(my_file.read()) + print("Ouverture de TODO en cours ...") + print("🤖 => Entre tes directives par son chiffre et fait Entrée!") + help_info = """Commande : +[1] Question +[2] Execute +[3] Fork - Ouvre TODO 🤖 dans une nouvelle tabulation +[0] Quitter +""" + # [3] install + while True: + status = click.prompt(help_info) + print() + if status == "0": + break + elif status == "1": + self.execute_prompt_ia() + elif status == "2": + self.prompt_execute() + elif status == "3": + cmd = f"gnome-terminal --tab -- bash -c 'source ./.venv/bin/activate;make todo'" + self.executer_commande_live(cmd) + # elif status == "3" or status == "install": + # print("install") + else: + print("Commande non trouvée 🤖!") + + print(status) + # manipuler() + + def get_kdbx(self): + if self.kdbx: + return self.kdbx + # Open file + chemin_fichier_kdbx = self.get_config(["kdbx", "path"]) + if not chemin_fichier_kdbx: + root = tk.Tk() + root.withdraw() # Hide the main window + chemin_fichier_kdbx = filedialog.askopenfilename( + title="Select a File", + filetypes=(("KeepassX files", "*.kdbx"),), + ) + if not chemin_fichier_kdbx: + _logger.error(f"KDBX is not configured, please fill {CONFIG_FILE}") + return + + mot_de_passe_kdbx = self.get_config(["kdbx", "password"]) + if not mot_de_passe_kdbx: + mot_de_passe_kdbx = getpass.getpass( + prompt="Entrez votre mot de passe : " + ) + + kp = PyKeePass(chemin_fichier_kdbx, password=mot_de_passe_kdbx) + + if kp: + self.kdbx = kp + return kp + + def get_config(self, lst_params): + # Open file + config_file = CONFIG_FILE + if os.path.exists(CONFIG_OVERRIDE_FILE): + config_file = CONFIG_OVERRIDE_FILE + + with open(config_file) as cfg: + dct_data = json.load(cfg) + for param in lst_params: + try: + dct_data = dct_data[param] + except KeyError: + _logger.error( + f"KeyError on file {config_file} with keys {lst_params}" + ) + return + return dct_data + + def execute_prompt_ia(self): + while True: + help_info = """Commande : +[0] Retour +Écrit moi ta question """ + status = click.prompt(help_info) + print() + if status == "0": + return + kp = self.get_kdbx() + if not kp: + return + nom_configuration = self.get_config( + ["kdbx_config", "openai", "kdbx_key"] + ) + entry = kp.find_entries_by_title(nom_configuration, first=True) + + client = openai.OpenAI(api_key=entry.password) + prompt_update = status + completion = client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": prompt_update}], + ) + + print(completion.choices[0].message.content) + print() + + def prompt_execute(self): + help_info = """Commande : +[1] Gérer une instance +[2] Fonction - Démonstration des fonctions développées +[0] Retour +""" + while True: + status = click.prompt(help_info) + print() + if status == "0": + return + elif status == "1": + status = self.prompt_execute_instance() + if status is not False: + return + elif status == "2": + status = self.prompt_execute_fonction() + if status is not False: + return + else: + print("Commande non trouvée 🤖!") + + def prompt_execute_instance(self): + lst_instance = self.get_config(["instance"]) + help_info = "Commande :\n" + help_end = "[0] Retour\n" + for i, dct_instance in enumerate(lst_instance): + help_info += ( + f"[{i + 1}] " + dct_instance["prompt_description"] + "\n" + ) + help_info += help_end + + while True: + status = click.prompt(help_info) + print() + if status == "0": + return False + else: + cmd_no_found = True + try: + int_cmd = int(status) + if 0 < int_cmd <= len(lst_instance): + cmd_no_found = False + status = click.confirm( + "Voulez-vous une nouvelle instance?" + ) + if status: + dct_instance = lst_instance[int_cmd - 1] + makefile_cmd = dct_instance.get("makefile_cmd") + db_name = dct_instance.get("database") + self.executer_commande_live(f"make {makefile_cmd}") + kdbx_key = dct_instance.get("kdbx_key") + self.prompt_execute_selenium_and_run_db( + db_name, kdbx_key=kdbx_key + ) + except ValueError: + pass + if cmd_no_found: + print("Commande non trouvée 🤖!") + + def prompt_execute_fonction(self): + lst_instance = self.get_config(["function"]) + help_info = "Commande :\n" + help_end = "[0] Retour\n" + for i, dct_instance in enumerate(lst_instance): + help_info += ( + f"[{i + 1}] " + dct_instance["prompt_description"] + "\n" + ) + help_info += help_end + + while True: + status = click.prompt(help_info) + print() + if status == "0": + return False + else: + cmd_no_found = True + try: + int_cmd = int(status) + if 0 < int_cmd <= len(lst_instance): + cmd_no_found = False + dct_instance = lst_instance[int_cmd - 1] + command = dct_instance.get("command") + kdbx_key = dct_instance.get("kdbx_key") + self.prompt_execute_selenium( + command=command, kdbx_key=kdbx_key + ) + except ValueError: + pass + if cmd_no_found: + print("Commande non trouvée 🤖!") + + def kdbx_get_extra_command_user(self, kdbx_key): + lst_value = [] + if kdbx_key: + kp = self.get_kdbx() + if not kp: + return "" + if type(kdbx_key) is not list: + lst_kdbx_key = [kdbx_key] + else: + lst_kdbx_key = kdbx_key + for key in lst_kdbx_key: + entry = kp.find_entries_by_title(key, first=True) + try: + odoo_user = entry.username + except AttributeError: + _logger.error(f"Cannot find username from keys {key}") + try: + odoo_password = entry.password + except AttributeError: + _logger.error(f"Cannot find password from keys {key}") + lst_value.append( + f" --default_email_auth {odoo_user} --default_password_auth '{odoo_password}'" + ) + if len(lst_value) == 0: + return "" + elif len(lst_value) == 1: + return lst_value[0] + return lst_value + + def prompt_execute_selenium_and_run_db(self, bd, kdbx_key=None): + if kdbx_key: + extra_cmd_web_login = self.kdbx_get_extra_command_user(kdbx_key) + else: + extra_cmd_web_login = "" + + cmd = f'parallel ::: "./run.sh -d {bd}" "sleep 3;./script/selenium/web_login.py{extra_cmd_web_login}"' + self.executer_commande_live(cmd) + + def prompt_execute_selenium(self, command=None, kdbx_key=None): + if kdbx_key: + extra_cmd_web_login = self.kdbx_get_extra_command_user(kdbx_key) + else: + extra_cmd_web_login = "" + + lst_cmd = [] + if not command: + cmd = "./script/selenium/web_login.py" + else: + cmd = command + + if type(extra_cmd_web_login) is list: + for item in extra_cmd_web_login: + lst_cmd.append(cmd + item) + else: + lst_cmd.append(cmd + extra_cmd_web_login) + + if len(lst_cmd) == 1: + self.executer_commande_live(lst_cmd[0]) + elif len(lst_cmd) > 1: + new_cmd = "parallel ::: " + for i, cmd in enumerate(lst_cmd): + new_cmd += f' "sleep {1 * i};{cmd}"' + self.executer_commande_live(new_cmd) + + def executer_commande_live(self, commande): + """ + Exécute une commande et affiche la sortie en direct. + + Args: + commande (str): La commande à exécuter (sous forme de chaîne de caractères). + """ + try: + process = subprocess.Popen( + commande, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, # Désactive la mise en tampon pour la sortie en direct + universal_newlines=True, # Pour traiter les sauts de lignes correctement + ) + + while True: + ligne = process.stdout.readline() + if not ligne: + break + print(ligne, end="") + + process.wait() # Attendre la fin du process + + if process.returncode != 0: + print( + f"La commande a retourné un code d'erreur : {process.returncode}" + ) + + except FileNotFoundError: + if "password" in commande: + print( + f"Erreur : La commande '{commande.split(' ')[0]}'[...] n'a pas été trouvée." + ) + else: + print( + f"Erreur : La commande '{commande}' n'a pas été trouvée." + ) + except Exception as e: + print(f"Une erreur s'est produite : {e}") + + +if __name__ == "__main__": + todo = TODO() + todo.run() diff --git a/script/todo/todo_example.json b/script/todo/todo_example.json new file mode 100644 index 0000000..fec4d1f --- /dev/null +++ b/script/todo/todo_example.json @@ -0,0 +1,35 @@ +{ + "kdbx": { + "path": "", + "password": "" + }, + "kdbx_config": { + "openai": { + "kdbx_key": "OpenAI api" + } + }, + "instance": [ + { + "prompt_description": "Test - Instance de base minimale", + "makefile_cmd": "db_restore_erplibre_base_db_test", + "database": "test", + "kdbx_key": "default_user_odoo" + } + ], + "function": [ + { + "prompt_description": "Ouvrir ERPLibre avec TODO \uD83E\uDD16" + }, + { + "prompt_description": "Ouvrir ERPLibre avec tous les utilisateurs", + "kdbx_key": [ + "user 01prod", + "user 02 prod" + ] + }, + { + "prompt_description": "Rapport", + "command": "./script/selenium/selenium_test.py --test --scenario=rapport" + } + ] +}