atomicshop 2.14.4__py3-none-any.whl → 2.14.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of atomicshop might be problematic. Click here for more details.

@@ -1,10 +1,50 @@
1
1
  import logging
2
2
  from logging.handlers import TimedRotatingFileHandler, QueueListener, QueueHandler
3
+ from logging import FileHandler
4
+ import time
3
5
  import re
4
6
  import os
7
+ from pathlib import Path
8
+ import queue
9
+ from typing import Literal, Union
10
+ import threading
11
+ from datetime import datetime
5
12
 
13
+ from . import loggers, formatters
14
+ from ... import datetimes, filesystem
6
15
 
7
- class TimedRotatingFileHandlerWithHeader(TimedRotatingFileHandler):
16
+
17
+ DEFAULT_DATE_STRING_FORMAT: str = "%Y_%m_%d"
18
+ # Not used, only for the reference:
19
+ # _DEFAULT_DATE_REGEX_PATTERN: str = r"^\d{4}_\d{2}_\d{2}$"
20
+
21
+
22
+ class ForceAtTimeRotationTimedRotatingFileHandler(TimedRotatingFileHandler):
23
+ def __init__(self, *args, **kwargs):
24
+ super().__init__(*args, **kwargs)
25
+ self._last_rotated_date = None
26
+ self._start_rotation_check()
27
+
28
+ def _start_rotation_check(self):
29
+ self._rotation_thread = threading.Thread(target=self._check_for_rotation)
30
+ self._rotation_thread.daemon = True
31
+ self._rotation_thread.start()
32
+
33
+ def _check_for_rotation(self):
34
+ while True:
35
+ now = datetime.now()
36
+ current_date = now.date()
37
+ # Check if it's midnight and the logs haven't been rotated today
38
+ if now.hour == 0 and now.minute == 0 and current_date != self._last_rotated_date:
39
+ self._last_rotated_date = current_date
40
+ self.doRollover()
41
+ time.sleep(0.1)
42
+
43
+ def doRollover(self):
44
+ super().doRollover()
45
+
46
+
47
+ class TimedRotatingFileHandlerWithHeader(ForceAtTimeRotationTimedRotatingFileHandler):
8
48
  """
9
49
  Custom TimedRotatingFileHandler that writes a header to the log file each time there is a file rotation.
10
50
  Useful for writing CSV files.
@@ -31,6 +71,164 @@ class TimedRotatingFileHandlerWithHeader(TimedRotatingFileHandler):
31
71
  super().emit(record)
32
72
 
33
73
 
74
+ def _process_formatter_attribute(
75
+ formatter: Union[
76
+ Literal['DEFAULT', 'MESSAGE'],
77
+ str,
78
+ None],
79
+ file_type: Union[
80
+ Literal['txt', 'csv', 'json'],
81
+ None] = None
82
+ ):
83
+ """
84
+ Function to process the formatter attribute.
85
+ """
86
+
87
+ if formatter == 'DEFAULT' and file_type is None:
88
+ return formatters.DEFAULT_STREAM_FORMATTER
89
+ elif formatter == 'DEFAULT' and file_type == 'txt':
90
+ return formatters.DEFAULT_FORMATTER_TXT_FILE
91
+ elif formatter == 'DEFAULT' and file_type == 'csv':
92
+ return formatters.DEFAULT_FORMATTER_CSV_FILE
93
+ elif formatter == 'DEFAULT' and file_type == 'json':
94
+ return formatters.DEFAULT_MESSAGE_FORMATTER
95
+ elif formatter == 'MESSAGE':
96
+ return formatters.DEFAULT_MESSAGE_FORMATTER
97
+ else:
98
+ return formatter
99
+
100
+
101
+ def add_stream_handler(
102
+ logger: logging.Logger,
103
+ logging_level: str = "DEBUG",
104
+ formatter: Union[
105
+ Literal['DEFAULT', 'MESSAGE'],
106
+ str,
107
+ None] = None,
108
+ formatter_use_nanoseconds: bool = False
109
+ ):
110
+ """
111
+ Function to add StreamHandler to logger.
112
+ Stream formatter will output messages to the console.
113
+ """
114
+
115
+ # Getting the StreamHandler.
116
+ stream_handler = get_stream_handler()
117
+ # Setting log level for the handler, that will use the logger while initiated.
118
+ loggers.set_logging_level(stream_handler, logging_level)
119
+
120
+ # If formatter_message_only is set to True, then formatter will be used only for the 'message' part.
121
+ formatter = _process_formatter_attribute(formatter)
122
+
123
+ # If formatter was provided, then it will be used.
124
+ if formatter:
125
+ logging_formatter = formatters.get_logging_formatter_from_string(
126
+ formatter=formatter, use_nanoseconds=formatter_use_nanoseconds)
127
+ set_formatter(stream_handler, logging_formatter)
128
+
129
+ # Adding the handler to the main logger
130
+ loggers.add_handler(logger, stream_handler)
131
+
132
+ # Disable propagation from the 'root' logger, so we will not see the messages twice.
133
+ loggers.set_propagation(logger)
134
+
135
+
136
+ def add_timedfilehandler_with_queuehandler(
137
+ logger: logging.Logger,
138
+ file_path: str,
139
+ file_type: Literal[
140
+ 'txt',
141
+ 'csv',
142
+ 'json'] = 'txt',
143
+ logging_level="DEBUG",
144
+ formatter: Union[
145
+ Literal['DEFAULT', 'MESSAGE'],
146
+ str,
147
+ None] = None,
148
+ formatter_use_nanoseconds: bool = False,
149
+ when: str = 'midnight',
150
+ interval: int = 1,
151
+ delay: bool = True,
152
+ encoding=None,
153
+ header: str = None
154
+ ):
155
+ """
156
+ Function to add TimedRotatingFileHandler and QueueHandler to logger.
157
+ TimedRotatingFileHandler will output messages to the file through QueueHandler.
158
+ This is needed, since TimedRotatingFileHandler is not thread-safe, though official docs say it is.
159
+ """
160
+
161
+ # If file name wasn't provided we will use the logger name instead.
162
+ # if not file_name_no_extension:
163
+ # file_name_no_extension = logger.name
164
+
165
+ # Setting the TimedRotatingFileHandler, without adding it to the logger.
166
+ # It will be added to the QueueListener, which will use the TimedRotatingFileHandler to write logs.
167
+ # This is needed since there's a bug in TimedRotatingFileHandler, which won't let it be used with
168
+ # threads the same way it would be used for multiprocess.
169
+
170
+ # Creating file handler with log filename. At this stage the log file is created and locked by the handler,
171
+ # Unless we use "delay=True" to tell the class to write the file only if there's something to write.
172
+
173
+ filesystem.create_directory(os.path.dirname(file_path))
174
+
175
+ if file_type == "csv":
176
+ # If file extension is CSV, we'll set the header to the file.
177
+ # This is needed since the CSV file will be rotated, and we'll need to set the header each time.
178
+ # We'll use the custom TimedRotatingFileHandlerWithHeader class.
179
+ file_handler = get_timed_rotating_file_handler_with_header(
180
+ file_path, when=when, interval=interval, delay=delay, encoding=encoding, header=header)
181
+ else:
182
+ file_handler = get_timed_rotating_file_handler(
183
+ file_path, when=when, interval=interval, delay=delay, encoding=encoding)
184
+
185
+ loggers.set_logging_level(file_handler, logging_level)
186
+
187
+ formatter = _process_formatter_attribute(formatter, file_type=file_type)
188
+
189
+ # If formatter was passed to the function we'll add it to handler.
190
+ if formatter:
191
+ # Convert string to Formatter object. Moved to newer styling of python 3: style='{'
192
+ logging_formatter = formatters.get_logging_formatter_from_string(
193
+ formatter=formatter, use_nanoseconds=formatter_use_nanoseconds)
194
+ # Setting the formatter in file handler.
195
+ set_formatter(file_handler, logging_formatter)
196
+
197
+ # This function will change the suffix behavior of the rotated file name.
198
+ change_rotated_filename(file_handler)
199
+
200
+ queue_handler = start_queue_listener_for_file_handler_and_get_queue_handler(file_handler)
201
+ loggers.set_logging_level(queue_handler, logging_level)
202
+
203
+ # Add the QueueHandler to the logger.
204
+ loggers.add_handler(logger, queue_handler)
205
+
206
+ # Disable propagation from the 'root' logger, so we will not see the messages twice.
207
+ loggers.set_propagation(logger)
208
+
209
+
210
+ def start_queue_listener_for_file_handler_and_get_queue_handler(file_handler):
211
+ """
212
+ Function to start QueueListener, which will put the logs from FileHandler to the Queue.
213
+ QueueHandler will get the logs from the Queue and put them to the file that was set in the FileHandler.
214
+
215
+ :param file_handler: FileHandler object.
216
+ :return: QueueHandler object.
217
+ """
218
+
219
+ # Create the Queue between threads. "-1" means that there can infinite number of items that can be
220
+ # put in the Queue. if integer is bigger than 0, it means that this will be the maximum
221
+ # number of items.
222
+ queue_object = queue.Queue(-1)
223
+ # Create QueueListener, which will put the logs from FileHandler to the Queue and put the logs to the queue.
224
+ start_queue_listener_for_file_handler(file_handler, queue_object)
225
+
226
+ return get_queue_handler(queue_object)
227
+
228
+
229
+ # BASE FUNCTIONS =======================================================================================================
230
+
231
+
34
232
  def get_stream_handler() -> logging.StreamHandler:
35
233
  """
36
234
  Function to get a StreamHandler.
@@ -44,7 +242,7 @@ def get_stream_handler() -> logging.StreamHandler:
44
242
 
45
243
  def get_timed_rotating_file_handler(
46
244
  log_file_path: str, when: str = "midnight", interval: int = 1, delay: bool = False, encoding=None
47
- ) -> logging.handlers.TimedRotatingFileHandler:
245
+ ) -> ForceAtTimeRotationTimedRotatingFileHandler:
48
246
  """
49
247
  Function to get a TimedRotatingFileHandler.
50
248
  This handler will output messages to a file, rotating the log file at certain timed intervals.
@@ -62,7 +260,7 @@ def get_timed_rotating_file_handler(
62
260
  :return: TimedRotatingFileHandler.
63
261
  """
64
262
 
65
- return TimedRotatingFileHandler(
263
+ return ForceAtTimeRotationTimedRotatingFileHandler(
66
264
  filename=log_file_path, when=when, interval=interval, delay=delay, encoding=encoding)
67
265
 
68
266
 
@@ -150,7 +348,17 @@ def get_handler_name(handler: logging.Handler) -> str:
150
348
  return handler.get_name()
151
349
 
152
350
 
153
- def change_rotated_filename(file_handler: logging.Handler, file_extension: str):
351
+ def change_rotated_filename(
352
+ file_handler: logging.Handler,
353
+ date_format_string: str = None
354
+ ):
355
+ """
356
+ Function to change the way TimedRotatingFileHandler managing the rotating filename.
357
+
358
+ :param file_handler: FileHandler to change the rotating filename for.
359
+ :param date_format_string: Date format string to use for the rotated log filename.
360
+ If None, the default 'DEFAULT_DATE_STRING_FORMAT' will be used.
361
+ """
154
362
  # Changing the way TimedRotatingFileHandler managing the rotating filename
155
363
  # Default file suffix is only "Year_Month_Day" with addition of the dot (".") character to the
156
364
  # "file name + extension" that you provide it. Example: log file name:
@@ -170,18 +378,43 @@ def change_rotated_filename(file_handler: logging.Handler, file_extension: str):
170
378
  # file_handler.extMatch = re.compile(r"^\d{4}_\d{2}_\d{2}" + re.escape(log_file_extension) + r"$")
171
379
  # file_handler.extMatch = re.compile(r"^\d{4}_\d{2}_\d{2}.txt$")
172
380
 
173
- # Set variables that are responsible for setting TimedRotatingFileHandler filename on rotation.
174
- # Log files time format, need only date
175
- format_date_log_filename: str = "%Y_%m_%d"
176
- # Log file suffix.
177
- logfile_suffix: str = "_" + format_date_log_filename + file_extension
178
- # Regex object to match the TimedRotatingFileHandler file name suffix.
179
- # "re.escape" is used to "escape" strings in regex and use them as is.
180
- logfile_regex_suffix = re.compile(r"^\d{4}_\d{2}_\d{2}" + re.escape(file_extension) + r"$")
181
-
182
- # Changing the setting that we set above
381
+ def callback_namer(name):
382
+ """
383
+ Callback function to change the filename of the rotated log file on file rotation.
384
+ """
385
+ # Currently the 'name' is full file path + '.' + logfile_suffix.
386
+ # Example: 'C:\\path\\to\\file.log._2021_12_24'
387
+ # Get the parent directory of the file: C:\path\to
388
+ parent_dir: str = str(Path(name).parent)
389
+ # Get the base filename without the extension: file.log
390
+ filename: str = Path(name).stem
391
+ # Get the date part of the filename: _2021_12_24
392
+ date_part: str = str(Path(name).suffix).replace(".", "")
393
+ # Get the file extension: log
394
+ file_extension: str = Path(filename).suffix
395
+ # Get the file name without the extension: file
396
+ file_stem: str = Path(filename).stem
397
+
398
+ return f"{parent_dir}{os.sep}{file_stem}{date_part}{file_extension}"
399
+
400
+ if date_format_string is None:
401
+ date_format_string = DEFAULT_DATE_STRING_FORMAT
402
+
403
+ # Construct the new suffix without the file extension
404
+ logfile_suffix = f"_{date_format_string}"
405
+
406
+ # Get regex pattern from string format.
407
+ # Example: '%Y_%m_%d' -> r'\d{4}_\d{2}_\d{2}'
408
+ date_regex_pattern = datetimes.datetime_format_to_regex(date_format_string)
409
+
410
+ # Regex pattern to match the rotated log filenames
411
+ logfile_regex_suffix = re.compile(date_regex_pattern)
412
+
413
+ # Update the handler's suffix to include the date format
183
414
  file_handler.suffix = logfile_suffix
184
- file_handler.namer = lambda name: name.replace(file_extension + ".", "") + file_extension
415
+
416
+ file_handler.namer = callback_namer
417
+ # Update the handler's extMatch regex to match the new filename format
185
418
  file_handler.extMatch = logfile_regex_suffix
186
419
 
187
420
 
@@ -202,3 +435,23 @@ def has_handlers(logger: logging.Logger) -> bool:
202
435
  return False
203
436
  else:
204
437
  return True
438
+
439
+
440
+ def extract_datetime_format_from_file_handler(file_handler: FileHandler) -> Union[str, None]:
441
+ """
442
+ Extract the datetime string formats from all TimedRotatingFileHandlers in the logger.
443
+
444
+ Args:
445
+ - logger: The logger instance.
446
+
447
+ Returns:
448
+ - A list of datetime string formats used by the handlers.
449
+ """
450
+ # Extract the suffix
451
+ suffix = getattr(file_handler, 'suffix', None)
452
+ if suffix:
453
+ datetime_format = datetimes.extract_datetime_format_from_string(suffix)
454
+ if datetime_format:
455
+ return datetime_format
456
+
457
+ return None