• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ChristianTremblay / BAC0 / 10756798981

08 Sep 2024 03:41AM UTC coverage: 38.92% (-1.3%) from 40.19%
10756798981

push

github

ChristianTremblay
forgot to commit the new lookfordependency

19 of 22 new or added lines in 1 file covered. (86.36%)

1357 existing lines in 25 files now uncovered.

2069 of 5316 relevant lines covered (38.92%)

0.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.48
/BAC0/core/utils/notes.py
1
# -*- coding: utf-8 -*-
2
"""
3
Notes and logger decorator to be used on class
4
This will add a "notes" object to the class and will allow
5
logging feature at the same time.
6
Goal is to be able to access quickly to important informations for
7
the web interface.
8
"""
9
import inspect
1✔
10
import logging
1✔
11
import os
1✔
12
import sys
1✔
13
import typing as t
1✔
14

15
# --- standard Python modules ---
16
from collections import namedtuple
1✔
17
from datetime import datetime
1✔
18
from logging import FileHandler, Logger
1✔
19
from os.path import expanduser, join
1✔
20

21
# --- 3rd party modules ---
22
from ...core.utils.lookfordependency import rich_if_available, pandas_if_available
1✔
23

24
RICH, rich = rich_if_available()
1✔
25
if RICH:
1✔
26
    from rich.logging import RichHandler
×
27

28
_PANDAS, pd, _, _ = pandas_if_available()
1✔
29

30

31
class LogList:
1✔
32
    LOGGERS: t.List[Logger] = []
1✔
33

34

35
def convert_level(level):
1✔
36
    if not level:
1✔
UNCOV
37
        return None
×
38
    _valid_levels = [
1✔
39
        logging.DEBUG,
40
        logging.INFO,
41
        logging.WARNING,
42
        logging.ERROR,
43
        logging.CRITICAL,
44
    ]
45
    if level in _valid_levels:
1✔
UNCOV
46
        return level
×
47
    if level.lower() == "info":
1✔
48
        return logging.INFO
1✔
49
    elif level.lower() == "debug":
1✔
50
        return logging.DEBUG
1✔
51
    elif level.lower() == "warning":
1✔
52
        return logging.WARNING
1✔
UNCOV
53
    elif level.lower() == "error":
×
UNCOV
54
        return logging.ERROR
×
UNCOV
55
    elif level.lower() == "critical":
×
UNCOV
56
        return logging.CRITICAL
×
UNCOV
57
    raise ValueError(f"Wrong log level use one of the following : {_valid_levels}")
×
58

59

60
def update_log_level(
1✔
61
    level=None, *, log_file=None, stderr=None, stdout=None, log_this=True
62
):
63
    """
64
    Typical usage ::
65
        # Silence (use CRITICAL so not much messages will be sent)
66
        BAC0.log_level('silence')
67
        # Verbose
68
        BAC0.log_level('info')
69
        # Default, Info on console....but Warning in file
70
        BAC0.log_level(file='warning', stdout='info', stderr='critical')
71
        # Debug in file and console... this is a bad idea as the console will be filled
72
        BAC0.log_level(file='debug', stdout='debug', stderr='critical')
73

74
        # Preferably, debug in the file, keep console limited to info
75
        BAC0.log_level('debug')
76
        # OR
77
        BAC0.log_level(file='debug', stdout='info', stderr='critical')
78

79

80
    Giving only one parameter will set file and console to the same level.
81
    I tend to keep stderr CRITICAL
82

83
    """
UNCOV
84
    update_log_file_lvl = False
×
UNCOV
85
    update_stderr_lvl = False
×
UNCOV
86
    update_stdout_lvl = False
×
87

UNCOV
88
    if level:
×
89
        logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base").disabled = False
×
90
        if level.lower() == "silence":
×
91
            log_file_lvl = logging.CRITICAL
×
UNCOV
92
            stderr_lvl = logging.CRITICAL
×
93
            stdout_lvl = logging.CRITICAL
×
94
            update_log_file_lvl = True
×
95
            update_stderr_lvl = True
×
96
            update_stdout_lvl = True
×
97
            logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base").disabled = True
×
98
        elif level.lower() == "default":
×
99
            log_file_lvl = logging.WARNING
×
100
            stderr_lvl = logging.CRITICAL
×
101
            stdout_lvl = logging.INFO
×
102
            update_log_file_lvl = True
×
103
            update_stderr_lvl = True
×
104
            update_stdout_lvl = True
×
105
        elif level.lower() == "debug":
×
106
            log_file_lvl = logging.DEBUG
×
107
            stdout_lvl = logging.INFO
×
108
            update_log_file_lvl = True
×
109
            update_stdout_lvl = True
×
110
        else:
111
            level = convert_level(level)
×
112
            log_file_lvl = level
×
113
            stdout_lvl = level
×
114
            update_log_file_lvl = True
×
UNCOV
115
            update_stdout_lvl = True
×
116

117
    else:
118
        if log_file:
×
119
            log_file_lvl = convert_level(log_file)
×
120
            update_log_file_lvl = True
×
UNCOV
121
        if stderr:
×
UNCOV
122
            stderr_lvl = convert_level(stderr)
×
123
            update_stderr_lvl = True
×
124
        if stdout:
×
125
            stdout_lvl = convert_level(stdout)
×
126
            update_stdout_lvl = True
×
127

128
    # Choose Base as logger for this task
129
    if log_this:
×
130
        BAC0_logger = logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base")
×
131

UNCOV
132
    for each in LogList.LOGGERS:
×
UNCOV
133
        for handler in each.handlers:
×
134
            if update_log_file_lvl and handler.get_name() == "file_handler":
×
135
                handler.setLevel(log_file_lvl)
×
UNCOV
136
                if log_this:
×
137
                    BAC0_logger.warning(
×
138
                        "Changed log level of file to {}".format(
139
                            logging.getLevelName(log_file_lvl)
140
                        )
141
                    )
142
            elif update_stdout_lvl and handler.get_name() == "stdout":
×
UNCOV
143
                handler.setLevel(stdout_lvl)
×
UNCOV
144
                if log_this:
×
UNCOV
145
                    BAC0_logger.warning(
×
146
                        "Changed log level of console stdout to {}".format(
147
                            logging.getLevelName(stdout_lvl)
148
                        )
149
                    )
150
            elif update_stderr_lvl and handler.get_name() == "stderr":
×
UNCOV
151
                handler.setLevel(stderr_lvl)
×
UNCOV
152
                if log_this:
×
UNCOV
153
                    BAC0_logger.warning(
×
154
                        "Changed log level of console stderr to {}".format(
155
                            logging.getLevelName(stderr_lvl)
156
                        )
157
                    )
158

159

160
def note_and_log(cls):
1✔
161
    """
162
    This will be used as a decorator on class to activate
163
    logging and store messages in the variable cls._notes
164
    This will allow quick access to events in the web app.
165
    A note can be added to cls._notes without logging if passing
166
    the argument log=false to function note()
167
    Something can be logged without addind a note using function log()
168
    """
169
    if hasattr(cls, "DEBUG_LEVEL"):
1✔
UNCOV
170
        if cls.DEBUG_LEVEL == "debug":
×
UNCOV
171
            file_level = logging.DEBUG
×
UNCOV
172
            console_level = logging.DEBUG
×
UNCOV
173
        elif cls.DEBUG_LEVEL == "info":
×
UNCOV
174
            file_level = logging.INFO
×
175
            console_level = logging.INFO
×
176
    else:
177
        file_level = logging.WARNING
1✔
178
        console_level = logging.INFO
1✔
179
    # Notes object
180
    cls._notes = namedtuple("_notes", ["timestamp", "notes"])
1✔
181
    cls._notes.timestamp = []
1✔
182
    cls._notes.notes = []
1✔
183

184
    # Defining log object
185
    cls.logname = f"{cls.__module__} | {cls.__name__}"
1✔
186
    cls._log = logging.getLogger(f"BAC0_Root.{cls.__module__}.{cls.__name__}")
1✔
187

188
    # Set level to debug so filter is done by handler
189
    cls._log.setLevel(logging.DEBUG)
1✔
190

191
    # Console Handler
192
    if RICH:
1✔
UNCOV
193
        ch = RichHandler()
×
UNCOV
194
        ch2 = RichHandler()
×
195
    else:
196
        ch = logging.StreamHandler(sys.stderr)
1✔
197
        ch2 = logging.StreamHandler(sys.stdout)
1✔
198

199
    ch.set_name("stderr")
1✔
200
    ch.setLevel(logging.CRITICAL)
1✔
201
    ch2.set_name("stdout")
1✔
202
    ch2.setLevel(console_level)
1✔
203

204
    formatter = logging.Formatter("{asctime} - {levelname:<8}| {message}", style="{")
1✔
205

206
    # File Handler
207
    _PERMISSION_TO_WRITE = True
1✔
208
    logUserPath = expanduser("~")
1✔
209
    logSaveFilePath = join(logUserPath, ".BAC0")
1✔
210

211
    logFile = join(logSaveFilePath, "BAC0.log")
1✔
212
    try:
1✔
213
        if not os.path.exists(logSaveFilePath):
1✔
214
            os.makedirs(logSaveFilePath)
1✔
215
        fh = FileHandler(logFile)
1✔
216
        fh.set_name("file_handler")
1✔
217
        fh.setLevel(file_level)
1✔
218
        fh.setFormatter(formatter)
1✔
219

UNCOV
220
    except OSError:
×
UNCOV
221
        _PERMISSION_TO_WRITE = False
×
222

223
    ch.setFormatter(formatter)
1✔
224
    ch2.setFormatter(formatter)
1✔
225
    # Add handlers the first time only...
226
    if not len(cls._log.handlers):
1✔
227
        if _PERMISSION_TO_WRITE:
1✔
228
            cls._log.addHandler(fh)
1✔
229
        cls._log.addHandler(ch)
1✔
230
        cls._log.addHandler(ch2)
1✔
231

232
    LogList.LOGGERS.append(cls._log)
1✔
233

234
    def log_title(self, title, args=None, width=35):
1✔
235
        cls._log.debug("")
1✔
236
        cls._log.debug("#" * width)
1✔
237
        cls._log.debug(f"# {title}")
1✔
238
        cls._log.debug("#" * width)
1✔
239
        if args:
1✔
240
            cls._log.debug(f"{args!r}")
1✔
241
            cls._log.debug("#" * 35)
1✔
242

243
    def log_subtitle(self, subtitle, args=None, width=35):
1✔
UNCOV
244
        cls._log.debug("")
×
UNCOV
245
        cls._log.debug("=" * width)
×
UNCOV
246
        cls._log.debug(f"{subtitle}")
×
UNCOV
247
        cls._log.debug("=" * width)
×
UNCOV
248
        if args:
×
249
            cls._log.debug(f"{args!r}")
×
250
            cls._log.debug("=" * width)
×
251

252
    def log(self, note, *, level: t.Union[str, int] = logging.DEBUG):
1✔
253
        """
254
        Add a log entry...no note
255
        """
256
        if not note:
1✔
UNCOV
257
            raise ValueError("Provide something to log")
×
258
        if isinstance(level, str):
1✔
259
            level = convert_level(level)
1✔
260
        if level == logging.INFO:
1✔
261
            note = f"{note}"
1✔
262
        else:
263
            caller_frame = inspect.stack()[1]
1✔
264
            module = inspect.getmodule(caller_frame[0])
1✔
265
            module_name = module.__name__ if module else "unknown"
1✔
266
            note = f"{cls.logname} | {module_name} | {note}"
1✔
267
        cls._log.log(level, note)
1✔
268

269
    def note(self, note, *, level=logging.INFO, log=True):
1✔
270
        """
271
        Add note to the object. By default, the note will also
272
        be logged
273
        :param note: (str) The note itself
274
        :param level: (logging.level)
275
        :param log: (boolean) Enable or disable logging of note
276
        """
277
        if not note:
1✔
UNCOV
278
            raise ValueError("Provide something to log")
×
279
        note = f"{cls.logname} | {note}"
1✔
280
        cls._notes.timestamp.append(datetime.now().astimezone())
1✔
281
        cls._notes.notes.append(note)
1✔
282
        if log:
1✔
283
            cls.log(level, note)
1✔
284

285
    @property
1✔
286
    def notes(self):
1✔
287
        """
288
        Retrieve notes list as a Pandas Series
289
        """
UNCOV
290
        if not _PANDAS:
×
UNCOV
291
            return dict(zip(self._notes.timestamp, self._notes.notes))
×
UNCOV
292
        return pd.Series(self._notes.notes, index=self._notes.timestamp)
×
293

294
    def clear_notes(self):
1✔
295
        """
296
        Clear notes object
297
        """
UNCOV
298
        cls._notes.timestamp = []
×
UNCOV
299
        cls._notes.notes = []
×
300

301
    # Add the functions to the decorated class
302
    cls.clear_notes = clear_notes
1✔
303
    cls.note = note
1✔
304
    cls.notes = notes
1✔
305
    cls.log = log
1✔
306
    cls.log_title = log_title
1✔
307
    cls.log_subtitle = log_subtitle
1✔
308
    return cls
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc