Skip to content

Commit b178383

Browse files
authored
Merge pull request #65 from UAL-RE/62-feature-add-terminal-coloring-to-console-log-messages
Feat: Add color to messages in the console (Issue #62)
2 parents 6df2e65 + 744892b commit b178383

File tree

4 files changed

+95
-29
lines changed

4 files changed

+95
-29
lines changed

Log.py

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,38 @@
11
from datetime import datetime
22
import logging
3-
from time import asctime
3+
import ctypes
4+
import platform
5+
import sys
6+
import os
47
from Config import Config
58

69

710
# Log class to handle log messages
811
class Log:
912
def __init__(self, config):
1013
self.config = config
14+
file_name = "log-" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".log"
1115

12-
def log_config(self, in_terminal: bool = False):
1316
# Setup log configration
1417
config_obj = Config(self.config)
1518
system_config = config_obj.system_config()
1619
log_location = system_config["logs_location"]
1720

18-
file_name = "log-" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".log"
1921
if (log_location[-1] != "/"):
2022
log_location = log_location + '/'
21-
file_path = log_location + file_name
23+
self.file_path = log_location + file_name
24+
25+
self.ansi_terminal = _check_ansi()
26+
27+
def log_config(self, in_terminal: bool = False):
2228
if (in_terminal):
23-
file_path = ''
24-
logging.basicConfig(filename=file_path,
25-
format="%(asctime)s:%(levelname)s:%(name)s: %(message)s")
29+
f = ''
30+
logging.addLevelName(logging.WARNING, self._format_messagetype_ansi('WARNING'))
31+
logging.addLevelName(logging.ERROR, self._format_messagetype_ansi('ERROR'))
32+
else:
33+
f = self.file_path
34+
logging.basicConfig(filename=f, force=True,
35+
format='%(asctime)s:%(levelname)s: %(message)s')
2636

2737
def show_log_in_terminal(self, type, message, stop_script=False):
2838
# Show log in terminal
@@ -35,7 +45,7 @@ def write_log_in_file(self, type, message, show_in_terminal=False, stop_script=F
3545
# Show log in file
3646
self.log_config(False)
3747
if (show_in_terminal is True):
38-
print(asctime() + ":" + type.upper() + ":Log - " + message)
48+
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":" + self._format_messagetype_ansi(type.upper()) + ": " + message)
3949
self.message(type, message)
4050
if (stop_script is True):
4151
exit()
@@ -61,3 +71,60 @@ def message(self, type, message):
6171
logger.setLevel(logging.ERROR)
6272
logger.error(message)
6373
del logger
74+
75+
def _format_messagetype_ansi(self, type):
76+
'''
77+
Returns a colorized version of the given message type string. If no ANSI support is detected, the same string is returned unchanged.
78+
'''
79+
if not self.ansi_terminal:
80+
return type
81+
if (type.lower() == 'error'):
82+
return '\033[2;30;41m' + type + '\033[0;0m'
83+
elif (type.lower() == 'warning'):
84+
return '\033[2;31;43m' + type + '\033[0;0m'
85+
elif (type.lower() == 'info'):
86+
return type
87+
elif (type.lower() == 'debug'):
88+
return type
89+
else:
90+
return type
91+
92+
93+
def _check_ansi():
94+
'''
95+
Returns True if the terminal the script is being run in supports ANSI escape sequences
96+
Based on: https://gist.github.com/ssbarnea/1316877
97+
'''
98+
for handle in [sys.stdout, sys.stderr]:
99+
if (hasattr(handle, "isatty") and handle.isatty()) or ('TERM' in os.environ and os.environ['TERM'] == 'ANSI'):
100+
if platform.system() == 'Windows' and not ('TERM' in os.environ and os.environ['TERM'] == 'ANSI'):
101+
if _is_wt():
102+
# Windows terminal does support ANSI
103+
return True
104+
else:
105+
# Assume the console does not support ANSI
106+
return False
107+
else:
108+
# Assume ANSI available
109+
return True
110+
else:
111+
# no ANSI available
112+
return False
113+
114+
115+
def _is_wt():
116+
'''
117+
Returns True if the script is run in the Windows Terminal 'wt.exe'
118+
Source: https://github.com/cvzi/AssertWT/blob/3125863ef823d5eaad1bc55155d90d9ca83f4aec/assertwt.py#L74-L88
119+
'''
120+
121+
if platform.system() != 'Windows' or 'idlelib' in sys.modules:
122+
return False
123+
124+
window = ctypes.windll.kernel32.GetConsoleWindow()
125+
if not window:
126+
return False
127+
ctypes.windll.kernel32.CloseHandle(window)
128+
WM_GETICON = 0x7F
129+
ret = ctypes.windll.user32.SendMessageW(window, WM_GETICON, 0, 0)
130+
return not ret

app.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from version import __version__, __commit__
44
from Log import Log
55
from figshare.Article import Article
6-
from time import asctime
6+
from datetime import datetime
77
from Config import Config
88
from figshare.Collection import Collection
99
from pathlib import Path
@@ -41,12 +41,14 @@ def check_logs_path_access(config_file):
4141

4242
logs_access = os.access(log_location, os.W_OK)
4343
if (logs_access is False):
44-
print(asctime() + ":ERROR: Log - " + "The logs location specified in the config file could not be reached or read.")
44+
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":ERROR: "
45+
+ "The logs location specified in the config file could not be reached or read.")
4546
exit()
4647

4748
except OSError as error:
4849
print(error)
49-
print(asctime() + ":ERROR: Log - " + "The logs location specified in the config file could not be reached or read.")
50+
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":ERROR: "
51+
+ "The logs location specified in the config file could not be reached or read.")
5052
exit()
5153

5254

@@ -56,15 +58,15 @@ def main():
5658
Setting up required variables and conditions.
5759
"""
5860
global args
59-
print(asctime() + ":Info: Log - ReBACH script has started.")
61+
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":INFO: ReBACH script has started.")
6062

6163
# Check .env file exists.
6264
if not args.xfg.is_file():
63-
print(asctime() + ":ERROR: Log - " + "Configuration file is missing or cannot be read.")
65+
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":ERROR: " + "Configuration file is missing or cannot be read.")
6466
exit()
6567
env_file = str(args.xfg)
66-
print(asctime() + ":Info: Log - " + "Env file:" + env_file)
67-
print(asctime() + ":Info: Log - " + "Checking configuration file.")
68+
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":INFO: " + "Env file:" + env_file)
69+
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":INFO: " + "Checking configuration file.")
6870
config_obj = Config(env_file)
6971

7072
figshare_config = config_obj.figshare_config()
@@ -80,10 +82,10 @@ def main():
8082

8183
# Check required env variables exist.
8284
if (log_location == ""):
83-
print(asctime() + ":ERROR: Log - " + "Logs file path missing in .env.ini file.")
85+
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] + ":ERROR: " + "Logs file path missing in .env.ini file.")
8486
exit()
8587

86-
log.write_log_in_file('info', "Logs location is accessible. Logging will now start.", True)
88+
log.write_log_in_file('info', "Logs location is accessible. Logging to file will now start.", True)
8789

8890
if (figshare_api_url == "" or figshare_api_token == ""):
8991
log.write_log_in_file('error', "Figshare API URL and Token is required.", True, True)
@@ -129,18 +131,17 @@ def main():
129131
+ " not be reached or read.",
130132
True, False)
131133

132-
return env_file
134+
return env_file, log
133135

134136

135137
if __name__ == "__main__":
136138
get_args()
137-
config_file_path = main()
138-
log = Log(config_file_path)
139+
config_file_path, log = main()
139140

140141
log.write_log_in_file('info',
141142
"Fetching articles...",
142143
True)
143-
article_obj = Article(config_file_path, args.ids)
144+
article_obj = Article(config_file_path, log, args.ids)
144145
article_data = article_obj.get_articles()
145146
log.write_log_in_file('info',
146147
f"Total articles fetched: {len(article_data)}.",
@@ -150,7 +151,7 @@ def main():
150151
log.write_log_in_file('info',
151152
"Fetching collections...",
152153
True)
153-
collection_obj = Collection(config_file_path, args.ids)
154+
collection_obj = Collection(config_file_path, log, args.ids)
154155
collection_data = collection_obj.get_collections()
155156
log.write_log_in_file('info',
156157
f"Total collections fetched: {len(collection_data)}.",

figshare/Article.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import requests
77
import hashlib
88
import re
9-
from Log import Log
109
from Config import Config
1110
from figshare.Integration import Integration
1211
from slugify import slugify
@@ -23,15 +22,15 @@ class Article:
2322
:param config: configuration
2423
:param ids: a list of ids to process. If None or an empty list is passed, all will be processed
2524
"""
26-
def __init__(self, config, ids):
25+
def __init__(self, config, log, ids):
2726
self.config_obj = Config(config)
2827
figshare_config = self.config_obj.figshare_config()
2928
self.system_config = self.config_obj.system_config()
3029
self.api_endpoint = figshare_config["url"]
3130
self.api_token = figshare_config["token"]
3231
self.retries = int(figshare_config["retries"]) if figshare_config["retries"] is not None else 3
3332
self.retry_wait = int(figshare_config["retries_wait"]) if figshare_config["retries_wait"] is not None else 10
34-
self.logs = Log(config)
33+
self.logs = log
3534
self.errors = []
3635
self.exclude_dirs = [".DS_Store"]
3736
self.total_all_articles_file_size = 0

figshare/Collection.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import requests
44
import hashlib
55
import re
6-
from Log import Log
76
from Config import Config
87
from figshare.Article import Article
98
from figshare.Integration import Integration
@@ -18,7 +17,7 @@ class Collection:
1817
:param config: configuration
1918
:param ids: list of ids to process. If None or an empty list is passed, all collections will be processed
2019
"""
21-
def __init__(self, config, ids):
20+
def __init__(self, config, log, ids):
2221
self.config_obj = Config(config)
2322
figshare_config = self.config_obj.figshare_config()
2423
self.system_config = self.config_obj.system_config()
@@ -27,9 +26,9 @@ def __init__(self, config, ids):
2726
self.retries = int(figshare_config["retries"]) if figshare_config["retries"] is not None else 3
2827
self.retry_wait = int(figshare_config["retries_wait"]) if figshare_config["retries_wait"] is not None else 10
2928
self.institution = int(figshare_config["institution"])
30-
self.logs = Log(config)
29+
self.logs = log
3130
self.errors = []
32-
self.article_obj = Article(config, ids)
31+
self.article_obj = Article(config, log, ids)
3332
self.preservation_storage_location = self.system_config["preservation_storage_location"]
3433
if self.preservation_storage_location[-1] != "/":
3534
self.preservation_storage_location = self.preservation_storage_location + "/"

0 commit comments

Comments
 (0)