diff --git a/Log.py b/Log.py index 104e19a..6a63fe6 100644 --- a/Log.py +++ b/Log.py @@ -1,6 +1,9 @@ from datetime import datetime import logging -from time import asctime +import ctypes +import platform +import sys +import os from Config import Config @@ -8,21 +11,28 @@ class Log: def __init__(self, config): self.config = config + file_name = "log-" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".log" - def log_config(self, in_terminal: bool = False): # Setup log configration config_obj = Config(self.config) system_config = config_obj.system_config() log_location = system_config["logs_location"] - file_name = "log-" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".log" if (log_location[-1] != "/"): log_location = log_location + '/' - file_path = log_location + file_name + self.file_path = log_location + file_name + + self.ansi_terminal = _check_ansi() + + def log_config(self, in_terminal: bool = False): if (in_terminal): - file_path = '' - logging.basicConfig(filename=file_path, - format="%(asctime)s:%(levelname)s:%(name)s: %(message)s") + f = '' + logging.addLevelName(logging.WARNING, self._format_messagetype_ansi('WARNING')) + logging.addLevelName(logging.ERROR, self._format_messagetype_ansi('ERROR')) + else: + f = self.file_path + logging.basicConfig(filename=f, force=True, + format='%(asctime)s:%(levelname)s: %(message)s') def show_log_in_terminal(self, type, message, stop_script=False): # Show log in terminal @@ -35,7 +45,7 @@ def write_log_in_file(self, type, message, show_in_terminal=False, stop_script=F # Show log in file self.log_config(False) if (show_in_terminal is True): - print(asctime() + ":" + type.upper() + ":Log - " + message) + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":" + self._format_messagetype_ansi(type.upper()) + ": " + message) self.message(type, message) if (stop_script is True): exit() @@ -61,3 +71,60 @@ def message(self, type, message): logger.setLevel(logging.ERROR) logger.error(message) del logger + + def _format_messagetype_ansi(self, type): + ''' + Returns a colorized version of the given message type string. If no ANSI support is detected, the same string is returned unchanged. + ''' + if not self.ansi_terminal: + return type + if (type.lower() == 'error'): + return '\033[2;30;41m' + type + '\033[0;0m' + elif (type.lower() == 'warning'): + return '\033[2;31;43m' + type + '\033[0;0m' + elif (type.lower() == 'info'): + return type + elif (type.lower() == 'debug'): + return type + else: + return type + + +def _check_ansi(): + ''' + Returns True if the terminal the script is being run in supports ANSI escape sequences + Based on: https://gist.github.com/ssbarnea/1316877 + ''' + for handle in [sys.stdout, sys.stderr]: + if (hasattr(handle, "isatty") and handle.isatty()) or ('TERM' in os.environ and os.environ['TERM'] == 'ANSI'): + if platform.system() == 'Windows' and not ('TERM' in os.environ and os.environ['TERM'] == 'ANSI'): + if _is_wt(): + # Windows terminal does support ANSI + return True + else: + # Assume the console does not support ANSI + return False + else: + # Assume ANSI available + return True + else: + # no ANSI available + return False + + +def _is_wt(): + ''' + Returns True if the script is run in the Windows Terminal 'wt.exe' + Source: https://github.com/cvzi/AssertWT/blob/3125863ef823d5eaad1bc55155d90d9ca83f4aec/assertwt.py#L74-L88 + ''' + + if platform.system() != 'Windows' or 'idlelib' in sys.modules: + return False + + window = ctypes.windll.kernel32.GetConsoleWindow() + if not window: + return False + ctypes.windll.kernel32.CloseHandle(window) + WM_GETICON = 0x7F + ret = ctypes.windll.user32.SendMessageW(window, WM_GETICON, 0, 0) + return not ret diff --git a/app.py b/app.py index dcb06d2..ec3838f 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,7 @@ from version import __version__, __commit__ from Log import Log from figshare.Article import Article -from time import asctime +from datetime import datetime from Config import Config from figshare.Collection import Collection from pathlib import Path @@ -41,12 +41,14 @@ def check_logs_path_access(config_file): logs_access = os.access(log_location, os.W_OK) if (logs_access is False): - print(asctime() + ":ERROR: Log - " + "The logs location specified in the config file could not be reached or read.") + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":ERROR: " + + "The logs location specified in the config file could not be reached or read.") exit() except OSError as error: print(error) - print(asctime() + ":ERROR: Log - " + "The logs location specified in the config file could not be reached or read.") + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":ERROR: " + + "The logs location specified in the config file could not be reached or read.") exit() @@ -56,15 +58,15 @@ def main(): Setting up required variables and conditions. """ global args - print(asctime() + ":Info: Log - ReBACH script has started.") + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":INFO: ReBACH script has started.") # Check .env file exists. if not args.xfg.is_file(): - print(asctime() + ":ERROR: Log - " + "Configuration file is missing or cannot be read.") + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":ERROR: " + "Configuration file is missing or cannot be read.") exit() env_file = str(args.xfg) - print(asctime() + ":Info: Log - " + "Env file:" + env_file) - print(asctime() + ":Info: Log - " + "Checking configuration file.") + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":INFO: " + "Env file:" + env_file) + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":INFO: " + "Checking configuration file.") config_obj = Config(env_file) figshare_config = config_obj.figshare_config() @@ -80,10 +82,10 @@ def main(): # Check required env variables exist. if (log_location == ""): - print(asctime() + ":ERROR: Log - " + "Logs file path missing in .env.ini file.") + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":ERROR: " + "Logs file path missing in .env.ini file.") exit() - log.write_log_in_file('info', "Logs location is accessible. Logging will now start.", True) + log.write_log_in_file('info', "Logs location is accessible. Logging to file will now start.", True) if (figshare_api_url == "" or figshare_api_token == ""): log.write_log_in_file('error', "Figshare API URL and Token is required.", True, True) @@ -129,18 +131,17 @@ def main(): + " not be reached or read.", True, False) - return env_file + return env_file, log if __name__ == "__main__": get_args() - config_file_path = main() - log = Log(config_file_path) + config_file_path, log = main() log.write_log_in_file('info', "Fetching articles...", True) - article_obj = Article(config_file_path, args.ids) + article_obj = Article(config_file_path, log, args.ids) article_data = article_obj.get_articles() log.write_log_in_file('info', f"Total articles fetched: {len(article_data)}.", @@ -150,7 +151,7 @@ def main(): log.write_log_in_file('info', "Fetching collections...", True) - collection_obj = Collection(config_file_path, args.ids) + collection_obj = Collection(config_file_path, log, args.ids) collection_data = collection_obj.get_collections() log.write_log_in_file('info', f"Total collections fetched: {len(collection_data)}.", diff --git a/figshare/Article.py b/figshare/Article.py index 2aa354d..55f6f41 100644 --- a/figshare/Article.py +++ b/figshare/Article.py @@ -6,7 +6,6 @@ import requests import hashlib import re -from Log import Log from Config import Config from figshare.Integration import Integration from slugify import slugify @@ -23,7 +22,7 @@ class Article: :param config: configuration :param ids: a list of ids to process. If None or an empty list is passed, all will be processed """ - def __init__(self, config, ids): + def __init__(self, config, log, ids): self.config_obj = Config(config) figshare_config = self.config_obj.figshare_config() self.system_config = self.config_obj.system_config() @@ -31,7 +30,7 @@ def __init__(self, config, ids): self.api_token = figshare_config["token"] self.retries = int(figshare_config["retries"]) if figshare_config["retries"] is not None else 3 self.retry_wait = int(figshare_config["retries_wait"]) if figshare_config["retries_wait"] is not None else 10 - self.logs = Log(config) + self.logs = log self.errors = [] self.exclude_dirs = [".DS_Store"] self.total_all_articles_file_size = 0 diff --git a/figshare/Collection.py b/figshare/Collection.py index 94e1727..67af7a3 100644 --- a/figshare/Collection.py +++ b/figshare/Collection.py @@ -3,7 +3,6 @@ import requests import hashlib import re -from Log import Log from Config import Config from figshare.Article import Article from figshare.Integration import Integration @@ -18,7 +17,7 @@ class Collection: :param config: configuration :param ids: list of ids to process. If None or an empty list is passed, all collections will be processed """ - def __init__(self, config, ids): + def __init__(self, config, log, ids): self.config_obj = Config(config) figshare_config = self.config_obj.figshare_config() self.system_config = self.config_obj.system_config() @@ -27,9 +26,9 @@ def __init__(self, config, ids): self.retries = int(figshare_config["retries"]) if figshare_config["retries"] is not None else 3 self.retry_wait = int(figshare_config["retries_wait"]) if figshare_config["retries_wait"] is not None else 10 self.institution = int(figshare_config["institution"]) - self.logs = Log(config) + self.logs = log self.errors = [] - self.article_obj = Article(config, ids) + self.article_obj = Article(config, log, ids) self.preservation_storage_location = self.system_config["preservation_storage_location"] if self.preservation_storage_location[-1] != "/": self.preservation_storage_location = self.preservation_storage_location + "/"