[UPD] TODO: auto installation

- better script message first execution
- search pycharm-community
- calculate time execution
- integrate crash diagnostic at importation
- show version installed and show installation option into cli
This commit is contained in:
Mathieu Benoit 2025-08-07 06:09:37 -04:00
parent 0ac7728d2f
commit fc73d0b9bd
2 changed files with 366 additions and 83 deletions

View file

@ -1,5 +1,23 @@
#!/bin/bash #!/bin/bash
DIR_VENV_ERPLIBRE_EXIST=1
DIR_VENV_ERPLIBRE=".venv.erplibre"
# If not exist, create it and do installation
# Can be in conflict with ./script/install_locally.sh
source .venv/bin/activate if [[ ! -d "$DIR_VENV_ERPLIBRE" ]]; then
DIR_VENV_ERPLIBRE_EXIST=0
echo "$DIR_VENV_ERPLIBRE does not exist."
if [[ ! -d "~/.pyenv/versions/$(< ./conf/python-erplibre-version)/bin/python" ]]; then
"~/.pyenv/versions/$(< ./conf/python-erplibre-version)/bin/python" -m venv $DIR_VENV_ERPLIBRE
else
python -m venv $DIR_VENV_ERPLIBRE
fi
fi
# If exist, source it and start installation
source ./.venv.erplibre/bin/activate
if [[ $DIR_VENV_ERPLIBRE_EXIST -eq 0 ]]; then
pip install -r requirement/erplibre_require-ments.txt
fi
./script/todo/todo.py ./script/todo/todo.py

View file

@ -1,89 +1,46 @@
#!/usr/bin/env python #!/usr/bin/env python3
import datetime
import getpass import getpass
import json import json
import logging import logging
import os import os
import shutil
import subprocess
import sys import sys
import time import time
import subprocess
file_error_path = ".erplibre.error.txt"
cst_venv_erplibre = ".venv.erplibre"
VERSION_DATA_FILE = os.path.join("conf", "supported_version_erplibre.json")
INSTALLED_ODOO_VERSION_FILE = os.path.join(
".repo", "installed_odoo_version.txt"
)
ODOO_VERSION_FILE = os.path.join(".odoo-version")
ENABLE_CRASH = False
CRASH_E = None
try: try:
import tkinter as tk import tkinter as tk
from tkinter import filedialog from tkinter import filedialog
import click import click
import humanize
import openai import openai
from pykeepass import PyKeePass from pykeepass import PyKeePass
# TODO implement urwid to improve text user interface
# import urwid
# TODO implement rich for beautiful print and table
# import rich
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
# TODO auto-detect gnome-terminal, or choose another. humanize = None
if os.path.exists(".venv"): ENABLE_CRASH = True
print("You forgot to activate source \nsource ./.venv/bin/activate") CRASH_E = e
time.sleep(0.5)
subprocess.run(
"gnome-terminal -- bash -c './script/todo/source_todo.sh'",
shell=True,
executable="/bin/bash",
)
else:
first_installation_input = (
input(
"First installation? This will process system installation"
" before (Y/N): "
)
.strip()
.upper()
)
if first_installation_input == "Y":
subprocess.run(
"gnome-terminal -- bash -c"
" './script/version/update_env_version.py --install;bash'",
shell=True,
executable="/bin/bash",
)
print("Wait after OS installation before continue.")
# First detect pycharm, need to be open before installation and close to increase speed if not ENABLE_CRASH:
result = subprocess.run( print("Importation success!")
["which", "pycharm"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if result.returncode == 0 and not os.path.exists(".idea"):
pycharm_configuration_input = (
input("Open Pycharm? (Y/N): ").strip().upper()
)
if pycharm_configuration_input == "Y":
subprocess.run(
"gnome-terminal -- bash -c 'pycharm .'",
shell=True,
executable="/bin/bash",
)
print(
"Close Pycharm when processing is done before continue"
" this guide."
)
# Propose Odoo installation
# TODO detect last version supported
odoo_installation_input = (
input("Install Odoo 16? (Y/N): ").strip().upper()
)
if odoo_installation_input == "Y":
subprocess.run(
"gnome-terminal -- bash -c 'make install_odoo_16;bash'",
shell=True,
executable="/bin/bash",
)
print("Wait after installation and open projects by terminal.")
print("make open_terminal")
else:
print("Nothing to do, you need a fresh installation to continue.")
sys.exit(0)
sys.exit(1)
# TODO implement urwid to improve text user interface
# import urwid
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
CONFIG_FILE = "./script/todo/todo.json" CONFIG_FILE = "./script/todo/todo.json"
@ -94,6 +51,30 @@ LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt"
class TODO: class TODO:
def __init__(self): def __init__(self):
self.kdbx = None self.kdbx = None
self.init()
def init(self):
# Get command
self.cmd_source_erplibre = ""
exec_path_gnome_terminal = shutil.which("gnome-terminal")
if exec_path_gnome_terminal:
self.cmd_source_erplibre = (
f"gnome-terminal -- bash -c 'source"
f" ./{cst_venv_erplibre}/bin/activate;%s'"
)
else:
exec_path_tell = shutil.which("osascript")
if exec_path_tell:
self.cmd_source_erplibre = (
"osascript -e 'tell application \"Terminal\"'"
)
self.cmd_source_erplibre += " -e 'tell application \"System Events\" to keystroke \"PATH\" using {command down}' -e 'delay 0.1' -e 'do script \""
self.cmd_source_erplibre += f"./{cst_venv_erplibre}/bin/activate; %s\" in front window'"
self.cmd_source_erplibre += " -e 'end tell'"
else:
self.cmd_source_erplibre = (
f"source ./{cst_venv_erplibre}/bin/activate;%s"
)
def run(self): def run(self):
with open(LOGO_ASCII_FILE) as my_file: with open(LOGO_ASCII_FILE) as my_file:
@ -102,26 +83,38 @@ class TODO:
print("🤖 => Entre tes directives par son chiffre et fait Entrée!") print("🤖 => Entre tes directives par son chiffre et fait Entrée!")
help_info = """Commande : help_info = """Commande :
[1] Execute [1] Execute
[2] Question [2] Install
[3] Fork - Ouvre TODO 🤖 dans une nouvelle tabulation [3] Question
[4] Fork - Ouvre TODO 🤖 dans une nouvelle tabulation
[0] Quitter [0] Quitter
""" """
# [3] install
while True: while True:
status = click.prompt(help_info) try:
status = click.prompt(help_info)
except NameError:
print("Do")
print("source .venv.erplibre/bin/activate && make")
sys.exit(1)
except ImportError:
print("Do")
print("source .venv.erplibre/bin/activate && make")
sys.exit(1)
print() print()
if status == "0": if status == "0":
break break
elif status == "1": elif status == "1":
self.prompt_execute() self.prompt_execute()
elif status == "2": elif status == "2":
self.execute_prompt_ia() self.prompt_install()
elif status == "3": elif status == "3":
cmd = ( self.execute_prompt_ia()
f"gnome-terminal --tab -- bash -c 'source" elif status == "4":
f" ./.venv/bin/activate;make todo'" # cmd = (
) # f"gnome-terminal --tab -- bash -c 'source"
self.executer_commande_live(cmd) # f" ./{cst_venv_erplibre}/bin/activate;make todo'"
# )
cmd = "make todo"
self.executer_commande_live(cmd, source_erplibre=True)
# elif status == "3" or status == "install": # elif status == "3" or status == "install":
# print("install") # print("install")
else: else:
@ -236,6 +229,164 @@ class TODO:
else: else:
print("Commande non trouvée 🤖!") print("Commande non trouvée 🤖!")
def prompt_install(self):
print("Detect first installation from code source.")
first_installation_input = (
input(
"First system installation? This will process system installation"
" before (Y/N): "
)
.strip()
.upper()
)
if first_installation_input == "Y":
cmd = "./script/version/update_env_version.py --install"
self.executer_commande_live(cmd, source_erplibre=True)
print("Wait after OS installation before continue.")
# First detect pycharm, need to be open before installation and close to increase speed
has_pycharm = False
has_pycharm_community = False
result = subprocess.run(
["which", "pycharm"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if result.returncode == 0:
has_pycharm = True
else:
result = subprocess.run(
["which", "pycharm-community"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
has_pycharm_community = result.returncode == 0
if (has_pycharm or has_pycharm_community) and not os.path.exists(
".idea"
):
pycharm_configuration_input = (
input("Open Pycharm? (Y/N): ").strip().upper()
)
if pycharm_configuration_input == "Y":
pycharm_bin = "pycharm" if has_pycharm else "pycharm-community"
self.executer_commande_live(pycharm_bin, source_erplibre=True)
print(
"Close Pycharm when processing is done before continue"
" this guide."
)
# Propose Odoo installation
# TODO detect last version supported
odoo_installation_input = (
input("Install virtual environment? (Y/N): ").strip().upper()
)
if odoo_installation_input == "Y":
# cmd_intern = "./script/install/install_erplibre.sh"
# TODO maybe update q to only install erplibre from install_locally
# TODO problem installing with q, the script depend on odoo
key_i = 0
dct_cmd_intern_begin = {
"q": (
"q",
"q: ERPLibre only with system python without Odoo",
"./script/install/install_erplibre.sh",
),
"w": (
"w",
"w: Install all Odoo version with ERPLibre",
"make install_odoo_all_version",
),
}
dct_final_cmd_intern = {}
with open(VERSION_DATA_FILE) as txt:
data_version = json.load(txt)
if not data_version:
raise Exception(
f"Internal error, no Odoo version is supported, please valide file '{VERSION_DATA_FILE}'"
)
lst_version_transform = []
for key, value in data_version.items():
lst_version_transform.append(value)
value["erplibre_version"] = key
lst_version_installed = []
if os.path.exists(INSTALLED_ODOO_VERSION_FILE):
with open(INSTALLED_ODOO_VERSION_FILE) as txt:
lst_version_installed = sorted(txt.read().splitlines())
odoo_installed_version = None
if os.path.exists(ODOO_VERSION_FILE):
with open(ODOO_VERSION_FILE) as txt:
odoo_installed_version = f"odoo{txt.read().strip()}"
# Add odoo version installation on command
lst_version = sorted(
lst_version_transform, key=lambda k: k.get("erplibre_version")
)
for dct_version in lst_version[::-1]:
key_i += 1
key_s = str(key_i)
label = f"{key_s}: Odoo {dct_version.get('odoo_version')}"
odoo_version = f"odoo{dct_version.get('odoo_version')}"
if odoo_version in lst_version_installed:
label += " - Installed"
if odoo_version == odoo_installed_version:
label += " - Actual"
if dct_version.get("default"):
label += " - Default"
if dct_version.get("is_deprecated"):
label += " - Deprecated"
erplibre_version = dct_version.get("erplibre_version")
dct_cmd_intern_begin[key_s] = (
key_s,
label,
f"./script/version/update_env_version.py --erplibre_version {erplibre_version} --install_dev",
)
# Add final command
dct_cmd_intern = {**dct_cmd_intern_begin, **dct_final_cmd_intern}
# Show command
odoo_version_input = ""
while odoo_version_input not in dct_cmd_intern.keys():
if odoo_version_input:
print(
f"Error, cannot understand value '{odoo_version_input}'"
)
str_input_dyn_odoo_version = (
"Choose a version:\n\t"
+ "\n\t".join([a[1] for a in dct_cmd_intern.values()])
+ "\nSelect : "
)
odoo_version_input = (
input(str_input_dyn_odoo_version).strip().lower()
)
cmd_intern = dct_cmd_intern.get(odoo_version_input)[2]
print(f"Will execute :\n{cmd_intern}")
# TODO use external script to detect terminal to use on system
# TODO check script open_terminal_code_generator.sh
# cmd_extern = f"gnome-terminal -- bash -c '{cmd_intern};bash'"
try:
subprocess.run(
cmd_intern, shell=True, executable="/bin/bash", check=True
)
except subprocess.CalledProcessError as e:
print(
f"Le script Bash «{cmd_intern}» a échoué avec le code de retour {e.returncode}."
)
print("Wait after installation and open projects by terminal.")
print("make open_terminal")
self.restart_script(str(e))
else:
print("Nothing to do, you need a fresh installation to continue.")
def execute_from_configuration( def execute_from_configuration(
self, dct_instance, exec_run_db=False, ignore_makefile=False self, dct_instance, exec_run_db=False, ignore_makefile=False
): ):
@ -274,7 +425,6 @@ class TODO:
help_info = "Commande :\n" help_info = "Commande :\n"
help_end = "[0] Retour\n" help_end = "[0] Retour\n"
for i, dct_instance in enumerate(lst_instance): for i, dct_instance in enumerate(lst_instance):
help_info += ( help_info += (
f"[{i + 1}] " + dct_instance["prompt_description"] + "\n" f"[{i + 1}] " + dct_instance["prompt_description"] + "\n"
) )
@ -459,17 +609,28 @@ class TODO:
new_cmd += f' "sleep {1 * i};{cmd}"' new_cmd += f' "sleep {1 * i};{cmd}"'
self.executer_commande_live(new_cmd) self.executer_commande_live(new_cmd)
def executer_commande_live(self, commande): def executer_commande_live(self, commande, source_erplibre=True):
""" """
Exécute une commande et affiche la sortie en direct. Exécute une commande et affiche la sortie en direct.
Args: Args:
commande (str): La commande à exécuter (sous forme de chaîne de caractères). commande (str): La commande à exécuter (sous forme de chaîne de caractères).
""" """
if source_erplibre:
# commande = f"source ./{cst_venv_erplibre}/bin/activate && " + commande
# cmd = (
# f"gnome-terminal --tab -- bash -c 'source"
# f" ./{cst_venv_erplibre}/bin/activate;{commande}'"
# )
commande = self.cmd_source_erplibre % commande
print(f"Execute : {commande}")
# os.system(f"./script/terminal/open_terminal.sh {commande}")
try: try:
process = subprocess.Popen( process = subprocess.Popen(
commande, commande,
shell=True, shell=True,
executable="/bin/bash",
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
text=True, text=True,
@ -504,7 +665,111 @@ class TODO:
except Exception as e: except Exception as e:
print(f"Une erreur s'est produite : {e}") print(f"Une erreur s'est produite : {e}")
def crash_diagnostic(self, e):
# TODO show message at start if os.path.exists(file_error_path)
if os.path.exists(file_error_path) and not os.path.exists(
cst_venv_erplibre
):
print("Got error : ")
print(e)
print("Got error at first execution.", file_error_path)
try:
file = open(file_error_path, "r")
content = file.read()
# TODO si vide, ajouter notre erreur
print(content)
except FileNotFoundError:
print("Error: File not found.")
finally:
if "file" in locals() and file:
file.close()
# Force auto installation
print("Auto installation")
time.sleep(0.5)
cmd = "./script/todo/source_todo.sh"
# self.restart_script(e)
self.executer_commande_live(cmd, source_erplibre=True)
sys.exit(1)
if os.path.exists(cst_venv_erplibre):
print("Import error : ")
print(e)
# TODO auto-detect gnome-terminal, or choose another. Is it done already?
self.restart_script(e)
# self.prompt_install()
# print(
# f"You forgot to activate source \nsource ./{cst_venv_erplibre}/bin/activate"
# )
# time.sleep(0.5)
# cmd = "./script/todo/source_todo.sh"
print("Re-execute TODO 🤖 or execute :")
print()
print(f"source {cst_venv_erplibre}/bin/activate;make")
print()
cmd = "./script/todo/todo.py"
# # self.restart_script(e)
try:
# TODO duplicate
import tkinter as tk
from tkinter import filedialog
import click
import humanize
import openai
from pykeepass import PyKeePass
except ImportError:
print("Rerun and exit")
self.executer_commande_live(cmd, source_erplibre=True)
sys.exit(1)
print("No error")
else:
self.prompt_install()
def restart_script(self, last_error):
print("Reboot TODO 🤖...")
# os.execv(sys.executable, ['python'] + sys.argv)
# TODO mettre check que le répertoire est créé, s'il existe, auto-loop à corriger
if os.path.exists(cst_venv_erplibre) and not os.path.exists(
file_error_path
):
# TODO mettre check import suivant ne vont pas planter
try:
with open(file_error_path, "w") as f_file:
f_file.write(str(last_error))
pass # The file is created and closed here, no content is written
print(
f"Try to reopen process with before :\nsource ./{cst_venv_erplibre}/bin/activate && exec python "
+ " ".join(sys.argv)
)
os.execv(
"/bin/bash",
[
"/bin/bash",
"-c",
f"source ./{cst_venv_erplibre}/bin/activate && exec python "
+ " ".join(sys.argv),
],
)
except Exception as e:
print("Error detect at first execution.")
print(e)
if __name__ == "__main__": if __name__ == "__main__":
todo = TODO() start_time = time.time()
todo.run() try:
todo = TODO()
if ENABLE_CRASH:
todo.crash_diagnostic(CRASH_E)
todo.run()
except KeyboardInterrupt:
print("Keyboard interrupt")
finally:
end_time = time.time()
duration_sec = end_time - start_time
if humanize:
duration_delta = datetime.timedelta(seconds=duration_sec)
humain_time = humanize.precisedelta(duration_delta)
print(f"\nTODO execution time {humain_time}\n")
else:
print(f"\nTODO execution time {duration_sec:.2f} sec.\n")