update pyCreateTh.py

This commit is contained in:
Alex38Lyon
2025-06-20 00:02:06 +02:00
parent 7fb026c131
commit 19d516997d
14 changed files with 1012 additions and 834 deletions
+285
View File
@@ -0,0 +1,285 @@
"""
#############################################################################################
general_fonctions.py for pyCreateTh.py
#############################################################################################
"""
import os, logging, sys, re, configparser
import Lib.global_data as global_data
import tkinter as tk
from tkinter import filedialog
#################################################################################################
# Couleurs ANSI par niveau de log
#################################################################################################
COLOR_CODES = {
logging.DEBUG: "\033[94m", # Bleu
logging.INFO: "\033[92m", # Vert
logging.WARNING: "\033[95m", # MAGENTA
logging.ERROR: "\033[91m", # Rouge
logging.CRITICAL: "\033[1;91m", # Rouge vif
}
RESET = "\033[0m"
#################################################################################################
# Codes de couleur ANSI
class Colors:
BLACK = '\033[90m'
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
MAGENTA = '\033[95m'
CYAN = '\033[96m'
WHITE = '\033[97m'
ERROR = '\033[91m'
WARNING = '\033[95m'
HEADER = '\033[96m'
DEBUG = '\033[94m' # Bleu
INFO = '\033[92m' # Vert
CRITICAL = '\033[1;91m', # Rouge vif
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
#################################################################################################
def safe_relpath(path):
"""
Renvoie un chemin relatif si possible, sinon un chemin partiel à partir du dossier de référence.
"""
abs_path = os.path.abspath(path)
ref_path = os.path.abspath(os.getcwd())
try:
valeur = "~\\" + os.path.relpath(path, ref_path)
return valeur
except ValueError:
max_depth = 4 # Profondeur maximale pour tronquer le chemin
# Disques différents, afficher le chemin relatif partiel depuis la racine commune
path_parts = abs_path.split(os.sep)
ref_parts = ref_path.split(os.sep)
while path_parts and ref_parts and path_parts[0] == ref_parts[0]:
path_parts.pop(0)
ref_parts.pop(0)
result = os.path.join(*path_parts) if path_parts else os.path.basename(path)
# Si max_depth est défini, tronque le chemin
if max_depth is not None:
parts = result.split(os.sep)
if len(parts) > max_depth:
result = os.path.join("~\\" , *parts[-max_depth:])
return result
#################################################################################################
# Coloration des messages d'aide d'arg #
#################################################################################################
def colored_help(parser):
"""
Affiche l'aide colorée pour les arguments de la ligne de commande.
Args:
parser (argparse.ArgumentParser): Le parseur d'arguments.
Returns:
None
"""
# Captures the help output
help_text = parser.format_help()
# Coloration des différentes parties
colored_help_text = help_text.replace(
'usage:', f'{Colors.ERROR}usage:{Colors.ENDC}'
).replace(
'options:', f'{Colors.GREEN}options:{Colors.ENDC}'
).replace('positional arguments:', f'{Colors.BLUE}positional arguments:{Colors.ENDC}'
).replace(', --help', f'{Colors.BLUE}, --help:{Colors.ENDC}'
).replace('elp:', f'{Colors.BLUE}elp{Colors.ENDC}')
# Surligner les arguments
for action in parser._actions:
if action.option_strings:
# Colorer les options (--xyz)
for opt in action.option_strings:
colored_help_text = colored_help_text.replace(opt, f'{Colors.BLUE}{opt}{Colors.ENDC}').replace('--help', f'{Colors.BLUE}--help:{Colors.ENDC}')
# Imprimer le texte coloré
print(colored_help_text)
sys.exit(1)
#################################################################################################
def select_file_tk_window():
"""
Ouvre une boite de dialogue tkinter pour sélectionner un fichier.
Returns:
str: Le chemin complet du fichier sélectionné.
"""
# Créer une instance de la fenêtre tkinter
root = tk.Tk()
# Cacher la fenêtre principale
root.withdraw()
# Afficher la boite de dialogue de sélection de fichier
file_path = filedialog.askopenfilename(
title="Select your file",
filetypes=[("MAK files", "*.mak"), ("Compatibles files", "*.th *.mak *.dat"), ("TH files", "*.th"), ("DAT files", "*.dat"), ("All files", "*.*")]
)
return file_path # Retourner le chemin complet du fichier sélectionné
#################################################################################################
def read_config(config_file):
"""
Lit le fichier de configuration et initialise les variables globales.
Args:
config_file (str): Le chemin vers le fichier de configuration.
Returns:
None
"""
# Initialize the configparser to read .ini files
config = configparser.ConfigParser()
config.read(config_file, encoding="utf-8")
if 'Survey_Data' in config and 'Author' in config['Survey_Data']:
global_data.Author = config['Survey_Data']['Author']
if 'Survey_Data' in config and 'Copyright1' in config['Survey_Data']:
global_data.Copyright = config['Survey_Data']['Copyright1'] + "\n" + config['Survey_Data']['Copyright2'] + "\n" + config['Survey_Data']['Copyright3'] + "\n"
if 'Survey_Data' in config and 'Copyright_Short' in config['Survey_Data']:
global_data.CopyrightShort = config['Survey_Data']['Copyright_Short']
if 'Survey_Data' in config and 'map_comment' in config['Survey_Data']:
global_data.mapComment = config['Survey_Data']['map_comment']
if 'Survey_Data' in config and 'club' in config['Survey_Data']:
global_data.club = config['Survey_Data']['club']
if 'Survey_Data' in config and 'thanksto' in config['Survey_Data']:
global_data.thanksto = config['Survey_Data']['thanksto']
if 'Survey_Data' in config and 'datat' in config['Survey_Data']:
global_data.datat = config['Survey_Data']['datat']
if 'Survey_Data' in config and 'wpage' in config['Survey_Data']:
global_data.wpage = config['Survey_Data']['wpage']
if 'Survey_Data' in config and 'cs' in config['Survey_Data']:
global_data.cs = config['Survey_Data']['cs']
if 'Application_Data' in config and 'template_path' in config['Application_Data']:
global_data.templatePath = config['Application_Data']['template_path']
if 'Application_Data' in config and 'station_by_scrap' in config['Application_Data']:
global_data.stationByScrap = int(config['Application_Data']['station_by_scrap'])
if 'Application_Data' in config and 'final_therion_exe' in config['Application_Data']:
global_data.finalTherionExe = bool(config['Application_Data']['final_therion_exe'])
if 'Application_Data' in config and 'therion_path' in config['Application_Data']:
global_data.therionPath = config['Application_Data']['therion_path']
if 'Application_Data' in config and 'therion_path' in config['Application_Data']:
global_data.SurveyPrefixName = config['Application_Data']['survey_prefix_name']
if global_data.linesInTh2 == -1 :
if 'Application_Data' in config and 'shot_lines_in_th2_files' in config['Application_Data']:
linesInTh2 = 0 if config['Application_Data']['shot_lines_in_th2_files'] == "False" else 1
if global_data.stationNamesInTh2 == -1 :
if 'Application_Data' in config and 'station_name_in_th2_files' in config['Application_Data']:
global_data.stationNamesInTh2 = 0 if config['Application_Data']['station_name_in_th2_files'] == "False" else 1
#################################################################################################
# Supprime les codes ANSI (pour l'écriture dans les fichiers)
#################################################################################################
def strip_ansi_codes(text):
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
return ansi_escape.sub('', text)
#################################################################################################
# Formatter pour la console avec couleurs
#################################################################################################
class ConsoleFormatter(logging.Formatter):
def format(self, record):
color = COLOR_CODES.get(record.levelno, "")
message = super().format(record)
return f"{color}{message}{RESET}"
#################################################################################################
# Formatter pour le fichier avec "!!!" sur les erreurs
#################################################################################################
class FileFormatter(logging.Formatter):
def format(self, record):
clean_msg = strip_ansi_codes(record.getMessage())
prefix = "!!! " if record.levelno >= logging.ERROR else ""
record_copy = logging.LogRecord(
name=record.name,
level=record.levelno,
pathname=record.pathname,
lineno=record.lineno,
msg=f"{prefix}{clean_msg}",
args=(),
exc_info=record.exc_info,
func=record.funcName,
sinfo=record.stack_info
)
return super().format(record_copy)
#################################################################################################
# Fonction de configuration du logger
#################################################################################################
def setup_logger(logfile="app.log", debug_log=False):
logger = logging.getLogger("Logger")
logger.setLevel(logging.DEBUG)
logger.handlers.clear()
min_level = logging.DEBUG if debug_log else logging.INFO
# Console stderr handler — affichage à l'écran avec couleurs
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setLevel(min_level)
stderr_formatter = ConsoleFormatter("%(levelname)s: %(message)s") # <-- Ta classe personnalisée
stderr_handler.setFormatter(stderr_formatter)
logger.addHandler(stderr_handler)
# File handler — fichier de log
file_handler = logging.FileHandler(logfile, encoding="utf-8")
file_handler.setLevel(min_level)
file_formatter = FileFormatter("%(asctime)s - %(levelname)s - %(message)s") # <-- Ta classe personnalisée
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
return logger
#################################################################################################
def release_log_file(logger):
handlers = logger.handlers[:]
for handler in handlers:
if isinstance(handler, logging.FileHandler):
handler.close()
logger.removeHandler(handler)
+22 -1
View File
@@ -7,9 +7,30 @@ global_data.py for pyCreateTh.py
#################################################################################################
error_count = 0 # Compteur d'erreurs
## [Survey_Data] default values
Author = "Created by pyCreateTh.py"
Copyright = "# global_data.Copyright (C) pyCreateTh.py"
CopyrightShort = "Licence (C) pyCreateTh.py"
mapComment = "Created by pyCreateTh.py"
cs = "UTM30"
club = "Therion"
thanksto = "Therion"
datat = "https://therion.speleo.sk/"
wpage = "https://therion.speleo.sk/"
## [Application_data] default values
templatePath = "./Template"
stationByScrap = 20
finalTherion_exe = True
therionPath = "C:/Therion/therion.exe"
SurveyPrefixName = f"Survey_"
linesInTh2 = -1
stationNamesInTh2 = -1
#################################################################################################
thFileDat = """
encoding utf-8
-83
View File
@@ -1,83 +0,0 @@
"""
#############################################################################################
logger_config.py for pyCreateTh.py
#############################################################################################
"""
import logging
import sys
import re
#################################################################################################
# Couleurs ANSI par niveau de log
#################################################################################################
COLOR_CODES = {
logging.DEBUG: "\033[94m", # Bleu
logging.INFO: "\033[92m", # Vert
logging.WARNING: "\033[95m",
logging.ERROR: "\033[91m", # Rouge
logging.CRITICAL: "\033[1;91m", # Rouge vif
}
RESET = "\033[0m"
#################################################################################################
# Supprime les codes ANSI (pour l'écriture dans les fichiers)
#################################################################################################
def strip_ansi_codes(text):
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
return ansi_escape.sub('', text)
#################################################################################################
# Formatter pour la console avec couleurs
#################################################################################################
class ConsoleFormatter(logging.Formatter):
def format(self, record):
color = COLOR_CODES.get(record.levelno, "")
message = super().format(record)
return f"{color}{message}{RESET}"
#################################################################################################
# Formatter pour le fichier avec "!!!" sur les erreurs
#################################################################################################
class FileFormatter(logging.Formatter):
def format(self, record):
clean_msg = strip_ansi_codes(record.getMessage())
prefix = "!!! " if record.levelno >= logging.ERROR else ""
record_copy = logging.LogRecord(
name=record.name,
level=record.levelno,
pathname=record.pathname,
lineno=record.lineno,
msg=f"{prefix}{clean_msg}",
args=(),
exc_info=record.exc_info,
func=record.funcName,
sinfo=record.stack_info
)
return super().format(record_copy)
#################################################################################################
# Fonction de configuration du logger
#################################################################################################
def setup_logger(logfile="app.log", debug_log=False):
logger = logging.getLogger("Logger")
logger.setLevel(logging.DEBUG)
logger.handlers.clear()
min_level = logging.DEBUG if debug_log else logging.INFO
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(min_level)
console_formatter = ConsoleFormatter("%(levelname)s: %(message)s")
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
# File handler
file_handler = logging.FileHandler(logfile, encoding="utf-8")
file_handler.setLevel(min_level)
file_formatter = FileFormatter("%(asctime)s - %(levelname)s - %(message)s")
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
return logger
+81 -103
View File
@@ -4,111 +4,20 @@ therion.py for pyCreateTh.py
#############################################################################################
"""
import tempfile
import shutil
import os
import tempfile, shutil, os, re, logging, threading, subprocess
from os.path import join
import subprocess
import re
import logging
import threading
import Lib.global_data as global_data
from Lib.general_fonctions import Colors, safe_relpath
log = logging.getLogger("Logger")
#################################################################################################
# Codes de couleur ANSI
class Colors:
BLACK = '\033[90m'
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
MAGENTA = '\033[95m'
CYAN = '\033[96m'
WHITE = '\033[97m'
ERROR = '\033[91m'
WARNING = '\033[95m'
HEADER = '\033[96m'
DEBUG = '\033[94m' # Bleu
INFO = '\033[92m' # Vert
CRITICAL = '\033[1;91m', # Rouge vif
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
#################################################################################################
def safe_relpath(path):
"""
Renvoie un chemin relatif si possible, sinon un chemin partiel à partir du dossier de référence.
"""
abs_path = os.path.abspath(path)
ref_path = os.path.abspath(os.getcwd())
try:
valeur = "~\\" + os.path.relpath(path, ref_path)
return valeur
except ValueError:
max_depth = 4 # Profondeur maximale pour tronquer le chemin
# Disques différents, afficher le chemin relatif partiel depuis la racine commune
path_parts = abs_path.split(os.sep)
ref_parts = ref_path.split(os.sep)
while path_parts and ref_parts and path_parts[0] == ref_parts[0]:
path_parts.pop(0)
ref_parts.pop(0)
result = os.path.join(*path_parts) if path_parts else os.path.basename(path)
# Si max_depth est défini, tronque le chemin
if max_depth is not None:
parts = result.split(os.sep)
if len(parts) > max_depth:
result = os.path.join("~\\" , *parts[-max_depth:])
return result
#################################################################################################
# Compilation Therion 'Template' (version avec blocage) #
#################################################################################################
def compile_templateOld(template, template_args, **kwargs):
try :
logfile = ""
tmpdir = tempfile.mkdtemp()
config = template.format(**template_args, tmpdir=tmpdir.replace("\\", "/"))
log.debug(f"{config}\n")
config_file = join(tmpdir, "config.thconfig")
log_file = join(tmpdir, "log.log")
therion_path = kwargs["therion_path"] if "therion_path" in kwargs else "therion"
with open(config_file, mode="w+", encoding="utf-8") as tmp:
with open(log_file, mode="w+") as tmp2:
tmp.write(config)
tmp.flush()
subprocess.check_output('''"{}" "{}" -l "{}"'''.format(therion_path, config_file, log_file), shell=True, )
tmp2.flush()
logfile = tmp2.read()
if kwargs["cleanup"]:
shutil.rmtree(tmpdir)
log.debug("\n" )
return logfile, tmpdir
except Exception as e:
log.error(f"Therion template compilation error: {Colors.ENDC}{e}")
global_data.error_count += 1
#################################################################################################
# Compilation Therion 'Template' (version sans blocage) #
# Compiler une configuration générée dynamiquement à partir d'un template texte. #
#################################################################################################
def compile_template(template, template_args, **kwargs):
def compile_template(template, template_args, totReadMeError = "", **kwargs ):
logfile = ""
tmpdir = None
try:
@@ -147,23 +56,26 @@ def compile_template(template, template_args, **kwargs):
# Analyse du code retour
if result.returncode != 0 or "press any key" in result.stdout.lower():
log.error(f"Therion compilation failed with return code: {Colors.ENDC}{result.returncode}\n{Colors.WHITE}{result.stdout}")
totReadMeError += f"\tTherion compilation failed with return code: {result.returncode}\n"
global_data.error_count += 1
return "Therion error", tmpdir
return "Therion error", tmpdir, totReadMeError
stat = get_stats_from_log(logfile)
log.info(f"Therion compilation successful, length: {Colors.ENDC}{stat["length"]}m{Colors.INFO}, depth: {Colors.ENDC}{stat["depth"]}m")
return logfile, tmpdir
return logfile, tmpdir, totReadMeError
except subprocess.TimeoutExpired:
log.error(f"Therion process timed out and was terminated : {Colors.ENDC}{logfile}")
log.error(f"Therion process timed out and was terminated: {Colors.ENDC}{logfile}")
totReadMeError += f"\tTherion process timed out and was terminated\n"
global_data.error_count += 1
return "Therion error", tmpdir
return "Therion error", tmpdir, totReadMeError
except Exception as e:
log.error(f"Therion template compilation error: {Colors.ENDC}{e}")
totReadMeError += f"\tTherion template compilation error: {e}\n"
global_data.error_count += 1
return "Therion error", tmpdir
return "Therion error", tmpdir, totReadMeError
finally:
if kwargs.get("cleanup", True) and tmpdir:
@@ -176,8 +88,7 @@ def compile_template(template, template_args, **kwargs):
#################################################################################################
# Compilation Therion (version sans blocage) #
#################################################################################################
def compile_file(filename, **kwargs):
def compile_fileOLd(filename, **kwargs):
tmpdir = os.path.dirname(filename)
log_file = join(tmpdir, "therion.log").replace("\\", "/")
therion_path = kwargs.get("therion_path", "therion")
@@ -240,6 +151,70 @@ def compile_file(filename, **kwargs):
global_data.error_count += 1
def compile_file(filename, **kwargs):
tmpdir = os.path.dirname(filename)
log_file = join(tmpdir, "therion.log").replace("\\", "/")
therion_path = kwargs.get("therion_path", "therion")
timeout = kwargs.get("timeout", 60)
log.info(f"Start therion compilation file: {Colors.ENDC}{safe_relpath(filename)}")
def run():
try:
process = subprocess.Popen(
[therion_path, filename, "-l", log_file],
cwd=tmpdir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1
)
def read_output(proc):
try:
for line in proc.stdout:
line = line.rstrip()
lower_line = line.lower()
if "average loop error" in lower_line:
log.warning(f"[Therion_Compile] {Colors.ENDC}{line}")
elif "error" in lower_line:
log.error(f"[Therion_Compile] {Colors.ENDC}{line}")
elif "warning" in lower_line:
log.warning(f"[Therion_Compile] {Colors.ENDC}{line}")
else:
log.debug(f"[Therion_Compile] {Colors.ENDC}{line}")
except Exception as e:
log.warning(f"Reading Therion output: {Colors.ENDC}{e}")
output_thread = threading.Thread(target=read_output, args=(process,))
output_thread.start()
output_thread.join(timeout)
if output_thread.is_alive():
log.error(f"Therion compilation timed out after {Colors.ENDC}{timeout}{Colors.ERROR} seconds. Killing process...")
global_data.error_count += 1
process.kill()
output_thread.join() # Toujours attendre proprement
process.wait()
if process.returncode != 0:
log.error(f"Therion returned error code {Colors.ENDC}{process.returncode}")
global_data.error_count += 1
else:
log.info(f"Therion file: {Colors.ENDC}{safe_relpath(filename)}{Colors.GREEN} compilation succeeded")
except Exception as e:
log.error(f"Therion file: {Colors.ENDC}{safe_relpath(filename)}{Colors.ERROR} compilation error: {Colors.ENDC}{e}")
global_data.error_count += 1
# Lancer le thread principal pour cette compilation et le retourner
thread = threading.Thread(target=run)
thread.start()
return thread
#################################################################################################
def compile_file_th(filepath, **kwargs):
template = """source {filepath}
@@ -248,15 +223,18 @@ def compile_file_th(filepath, **kwargs):
endlayout
"""
template_args = {"filepath": filepath}
logs, _ = compile_template(template, template_args, cleanup=True, **kwargs)
logs, _, = compile_template(template, template_args, cleanup=True, **kwargs)
return logs
#################################################################################################
# Attention fonctionne pour la version therion en français ! à voir pour les autres langues
lengthre = re.compile(r".*Longueur totale de la topographie = \s*(\S+)m")
depthre = re.compile(r".*Longueur totale verticale =\s*(\S+)m")
def get_stats_from_log(log):
lenmatch = lengthre.findall(log)
depmatch = depthre.findall(log)
if len(lenmatch) == 1 and len(depmatch) == 1: