pythonLogs 5.0.2__cp313-cp313-macosx_15_0_arm64.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.
@@ -0,0 +1,137 @@
1
+ import logging.handlers
2
+ import os
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from pythonLogs.constants import MB_TO_BYTES
7
+ from pythonLogs.log_utils import (
8
+ check_directory_permissions,
9
+ check_filename_instance,
10
+ cleanup_logger_handlers,
11
+ get_level,
12
+ get_log_path,
13
+ get_logger_and_formatter,
14
+ get_stream_handler,
15
+ gzip_file_with_sufix,
16
+ remove_old_logs,
17
+ write_stderr,
18
+ )
19
+ from pythonLogs.memory_utils import register_logger_weakref
20
+ from pythonLogs.settings import get_log_settings
21
+ from pythonLogs.thread_safety import auto_thread_safe
22
+
23
+
24
+ @auto_thread_safe(['init', '_cleanup_logger'])
25
+ class SizeRotatingLog:
26
+ """Size-based rotating logger with context manager support for automatic resource cleanup."""
27
+
28
+ def __init__(
29
+ self,
30
+ level: Optional[str] = None,
31
+ name: Optional[str] = None,
32
+ directory: Optional[str] = None,
33
+ filenames: Optional[list | tuple] = None,
34
+ maxmbytes: Optional[int] = None,
35
+ daystokeep: Optional[int] = None,
36
+ encoding: Optional[str] = None,
37
+ datefmt: Optional[str] = None,
38
+ timezone: Optional[str] = None,
39
+ streamhandler: Optional[bool] = None,
40
+ showlocation: Optional[bool] = None,
41
+ ):
42
+ _settings = get_log_settings()
43
+ self.level = get_level(level or _settings.level)
44
+ self.appname = name or _settings.appname
45
+ self.directory = directory or _settings.directory
46
+ self.filenames = filenames or (_settings.filename,)
47
+ self.maxmbytes = maxmbytes or _settings.max_file_size_mb
48
+ self.daystokeep = daystokeep or _settings.days_to_keep
49
+ self.encoding = encoding or _settings.encoding
50
+ self.datefmt = datefmt or _settings.date_format
51
+ self.timezone = timezone or _settings.timezone
52
+ self.streamhandler = streamhandler or _settings.stream_handler
53
+ self.showlocation = showlocation or _settings.show_location
54
+ self.logger = None
55
+
56
+ def init(self):
57
+ check_filename_instance(self.filenames)
58
+ check_directory_permissions(self.directory)
59
+
60
+ logger, formatter = get_logger_and_formatter(self.appname, self.datefmt, self.showlocation, self.timezone)
61
+ logger.setLevel(self.level)
62
+
63
+ for file in self.filenames:
64
+ log_file_path = get_log_path(self.directory, file)
65
+
66
+ file_handler = logging.handlers.RotatingFileHandler(
67
+ filename=log_file_path,
68
+ mode="a",
69
+ maxBytes=self.maxmbytes * MB_TO_BYTES,
70
+ backupCount=self.daystokeep,
71
+ encoding=self.encoding,
72
+ delay=False,
73
+ errors=None,
74
+ )
75
+ file_handler.rotator = GZipRotatorSize(self.directory, self.daystokeep)
76
+ file_handler.setFormatter(formatter)
77
+ file_handler.setLevel(self.level)
78
+ logger.addHandler(file_handler)
79
+
80
+ if self.streamhandler:
81
+ stream_hdlr = get_stream_handler(self.level, formatter)
82
+ logger.addHandler(stream_hdlr)
83
+
84
+ self.logger = logger
85
+ # Register weak reference for memory tracking
86
+ register_logger_weakref(logger)
87
+ return logger
88
+
89
+ def __enter__(self):
90
+ """Context manager entry."""
91
+ if not hasattr(self, 'logger') or self.logger is None:
92
+ self.init()
93
+ return self.logger
94
+
95
+ def __exit__(self, exc_type, exc_val, exc_tb):
96
+ """Context manager exit with automatic cleanup."""
97
+ if hasattr(self, 'logger'):
98
+ self._cleanup_logger(self.logger)
99
+
100
+ def _cleanup_logger(self, logger: logging.Logger) -> None:
101
+ """Clean up logger resources by closing all handlers with thread safety."""
102
+ cleanup_logger_handlers(logger)
103
+
104
+ @staticmethod
105
+ def cleanup_logger(logger: logging.Logger) -> None:
106
+ """Static method for cleaning up logger resources (backward compatibility)."""
107
+ cleanup_logger_handlers(logger)
108
+
109
+
110
+ class GZipRotatorSize:
111
+ def __init__(self, dir_logs: str, daystokeep: int):
112
+ self.directory = dir_logs
113
+ self.daystokeep = daystokeep
114
+
115
+ def __call__(self, source: str, dest: str) -> None:
116
+ remove_old_logs(self.directory, self.daystokeep)
117
+ if os.path.isfile(source) and os.stat(source).st_size > 0:
118
+ source_filename, _ = os.path.basename(source).split(".")
119
+ new_file_number = self._get_new_file_number(self.directory, source_filename)
120
+ if os.path.isfile(source):
121
+ gzip_file_with_sufix(source, str(new_file_number))
122
+
123
+ @staticmethod
124
+ def _get_new_file_number(directory: str, source_filename: str) -> int:
125
+ pattern = re.compile(rf"{re.escape(source_filename)}_(\d+)\.log\.gz$")
126
+ max_num = 0
127
+ try:
128
+ # Use pathlib for better performance with large directories
129
+ dir_path = Path(directory)
130
+ for file_path in dir_path.iterdir():
131
+ if file_path.is_file():
132
+ match = pattern.match(file_path.name)
133
+ if match:
134
+ max_num = max(max_num, int(match.group(1)))
135
+ except OSError as e:
136
+ write_stderr(f"Unable to get previous gz log file number | {repr(e)}")
137
+ return max_num + 1
@@ -0,0 +1,156 @@
1
+ import functools
2
+ import threading
3
+ from typing import Any, Callable, Dict, Type, TypeVar
4
+
5
+
6
+ F = TypeVar('F', bound=Callable[..., Any])
7
+
8
+
9
+ class ThreadSafeMeta(type):
10
+ """Metaclass that automatically adds thread safety to class methods."""
11
+
12
+ def __new__(mcs, name: str, bases: tuple, namespace: Dict[str, Any], **kwargs):
13
+ # Create the class first
14
+ cls = super().__new__(mcs, name, bases, namespace)
15
+
16
+ # Add a class-level lock if not already present
17
+ if not hasattr(cls, '_lock'):
18
+ cls._lock = threading.RLock()
19
+
20
+ # Get methods that should be thread-safe (exclude private/dunder methods)
21
+ thread_safe_methods = getattr(cls, '_thread_safe_methods', None)
22
+ if thread_safe_methods is None:
23
+ # Auto-detect public methods that modify state
24
+ thread_safe_methods = [
25
+ method_name
26
+ for method_name in namespace
27
+ if (
28
+ callable(getattr(cls, method_name, None))
29
+ and not method_name.startswith('_')
30
+ and method_name not in ['__enter__', '__exit__', '__init__']
31
+ )
32
+ ]
33
+
34
+ # Wrap each method with automatic locking
35
+ for method_name in thread_safe_methods:
36
+ if hasattr(cls, method_name):
37
+ original_method = getattr(cls, method_name)
38
+ if callable(original_method):
39
+ wrapped_method = thread_safe(original_method)
40
+ setattr(cls, method_name, wrapped_method)
41
+
42
+ return cls
43
+
44
+
45
+ def thread_safe(func: F) -> F:
46
+ """Decorator that automatically adds thread safety to methods."""
47
+
48
+ @functools.wraps(func)
49
+ def wrapper(self, *args, **kwargs):
50
+ # Use instance lock if available, otherwise class lock
51
+ lock = getattr(self, '_lock', None)
52
+ if lock is None:
53
+ # Check if class has lock, if not create one
54
+ if not hasattr(self.__class__, '_lock'):
55
+ self.__class__._lock = threading.RLock()
56
+ lock = self.__class__._lock
57
+
58
+ with lock:
59
+ return func(self, *args, **kwargs)
60
+
61
+ return wrapper
62
+
63
+
64
+ def _get_wrappable_methods(cls: Type) -> list:
65
+ """Helper function to get methods that should be made thread-safe."""
66
+ return [
67
+ method_name
68
+ for method_name in dir(cls)
69
+ if (
70
+ callable(getattr(cls, method_name, None))
71
+ and not method_name.startswith('_')
72
+ and method_name not in ['__enter__', '__exit__', '__init__']
73
+ )
74
+ ]
75
+
76
+
77
+ def _ensure_class_has_lock(cls: Type) -> None:
78
+ """Ensure the class has a lock attribute."""
79
+ if not hasattr(cls, '_lock'):
80
+ cls._lock = threading.RLock()
81
+
82
+
83
+ def _should_wrap_method(cls: Type, method_name: str, original_method: Any) -> bool:
84
+ """Check if a method should be wrapped with thread safety."""
85
+ return (
86
+ hasattr(cls, method_name) and callable(original_method) and not hasattr(original_method, '_thread_safe_wrapped')
87
+ )
88
+
89
+
90
+ def auto_thread_safe(thread_safe_methods: list = None):
91
+ """Class decorator that adds automatic thread safety to specified methods."""
92
+
93
+ def decorator(cls: Type) -> Type:
94
+ _ensure_class_has_lock(cls)
95
+
96
+ # Store thread-safe methods list
97
+ if thread_safe_methods:
98
+ cls._thread_safe_methods = thread_safe_methods
99
+
100
+ # Get methods to make thread-safe
101
+ methods_to_wrap = thread_safe_methods or _get_wrappable_methods(cls)
102
+
103
+ # Wrap each method
104
+ for method_name in methods_to_wrap:
105
+ original_method = getattr(cls, method_name, None)
106
+ if _should_wrap_method(cls, method_name, original_method):
107
+ wrapped_method = thread_safe(original_method)
108
+ wrapped_method._thread_safe_wrapped = True
109
+ setattr(cls, method_name, wrapped_method)
110
+
111
+ return cls
112
+
113
+ return decorator
114
+
115
+
116
+ class AutoThreadSafe:
117
+ """Base class that provides automatic thread safety for all public methods."""
118
+
119
+ def __init__(self):
120
+ if not hasattr(self, '_lock'):
121
+ self._lock = threading.RLock()
122
+
123
+ def __init_subclass__(cls, **kwargs):
124
+ super().__init_subclass__(**kwargs)
125
+
126
+ # Add class-level lock
127
+ if not hasattr(cls, '_lock'):
128
+ cls._lock = threading.RLock()
129
+
130
+ # Auto-wrap public methods
131
+ for attr_name in dir(cls):
132
+ if not attr_name.startswith('_'):
133
+ attr = getattr(cls, attr_name)
134
+ if callable(attr) and not hasattr(attr, '_thread_safe_wrapped'):
135
+ wrapped_attr = thread_safe(attr)
136
+ wrapped_attr._thread_safe_wrapped = True
137
+ setattr(cls, attr_name, wrapped_attr)
138
+
139
+
140
+ def synchronized_method(func: F) -> F:
141
+ """Decorator for individual methods that need thread safety."""
142
+ return thread_safe(func)
143
+
144
+
145
+ class ThreadSafeContext:
146
+ """Context manager for thread-safe operations."""
147
+
148
+ def __init__(self, lock: threading.Lock):
149
+ self.lock = lock
150
+
151
+ def __enter__(self):
152
+ self.lock.acquire()
153
+ return self
154
+
155
+ def __exit__(self, exc_type, exc_val, exc_tb):
156
+ self.lock.release()
@@ -0,0 +1,126 @@
1
+ import logging.handlers
2
+ import os
3
+ from typing import Optional
4
+ from pythonLogs.log_utils import (
5
+ check_directory_permissions,
6
+ check_filename_instance,
7
+ cleanup_logger_handlers,
8
+ get_level,
9
+ get_log_path,
10
+ get_logger_and_formatter,
11
+ get_stream_handler,
12
+ gzip_file_with_sufix,
13
+ remove_old_logs,
14
+ )
15
+ from pythonLogs.memory_utils import register_logger_weakref
16
+ from pythonLogs.settings import get_log_settings
17
+ from pythonLogs.thread_safety import auto_thread_safe
18
+
19
+
20
+ @auto_thread_safe(['init', '_cleanup_logger'])
21
+ class TimedRotatingLog:
22
+ """
23
+ Time-based rotating logger with context manager support for automatic resource cleanup.
24
+
25
+ Current 'rotating_when' events supported for TimedRotatingLogs:
26
+ Use RotateWhen enum values:
27
+ RotateWhen.MIDNIGHT - roll over at midnight
28
+ RotateWhen.MONDAY through RotateWhen.SUNDAY - roll over on specific days
29
+ RotateWhen.HOURLY - roll over every hour
30
+ RotateWhen.DAILY - roll over daily
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ level: Optional[str] = None,
36
+ name: Optional[str] = None,
37
+ directory: Optional[str] = None,
38
+ filenames: Optional[list | tuple] = None,
39
+ when: Optional[str] = None,
40
+ sufix: Optional[str] = None,
41
+ daystokeep: Optional[int] = None,
42
+ encoding: Optional[str] = None,
43
+ datefmt: Optional[str] = None,
44
+ timezone: Optional[str] = None,
45
+ streamhandler: Optional[bool] = None,
46
+ showlocation: Optional[bool] = None,
47
+ rotateatutc: Optional[bool] = None,
48
+ ):
49
+ _settings = get_log_settings()
50
+ self.level = get_level(level or _settings.level)
51
+ self.appname = name or _settings.appname
52
+ self.directory = directory or _settings.directory
53
+ self.filenames = filenames or (_settings.filename,)
54
+ self.when = when or _settings.rotate_when
55
+ self.sufix = sufix or _settings.rotate_file_sufix
56
+ self.daystokeep = daystokeep or _settings.days_to_keep
57
+ self.encoding = encoding or _settings.encoding
58
+ self.datefmt = datefmt or _settings.date_format
59
+ self.timezone = timezone or _settings.timezone
60
+ self.streamhandler = streamhandler or _settings.stream_handler
61
+ self.showlocation = showlocation or _settings.show_location
62
+ self.rotateatutc = rotateatutc or _settings.rotate_at_utc
63
+ self.logger = None
64
+
65
+ def init(self):
66
+ check_filename_instance(self.filenames)
67
+ check_directory_permissions(self.directory)
68
+
69
+ logger, formatter = get_logger_and_formatter(self.appname, self.datefmt, self.showlocation, self.timezone)
70
+ logger.setLevel(self.level)
71
+
72
+ for file in self.filenames:
73
+ log_file_path = get_log_path(self.directory, file)
74
+
75
+ file_handler = logging.handlers.TimedRotatingFileHandler(
76
+ filename=log_file_path,
77
+ encoding=self.encoding,
78
+ when=self.when,
79
+ utc=self.rotateatutc,
80
+ backupCount=self.daystokeep,
81
+ )
82
+ file_handler.suffix = self.sufix
83
+ file_handler.rotator = GZipRotatorTimed(self.directory, self.daystokeep)
84
+ file_handler.setFormatter(formatter)
85
+ file_handler.setLevel(self.level)
86
+ logger.addHandler(file_handler)
87
+
88
+ if self.streamhandler:
89
+ stream_hdlr = get_stream_handler(self.level, formatter)
90
+ logger.addHandler(stream_hdlr)
91
+
92
+ self.logger = logger
93
+ # Register weak reference for memory tracking
94
+ register_logger_weakref(logger)
95
+ return logger
96
+
97
+ def __enter__(self):
98
+ """Context manager entry."""
99
+ if not hasattr(self, 'logger') or self.logger is None:
100
+ self.init()
101
+ return self.logger
102
+
103
+ def __exit__(self, exc_type, exc_val, exc_tb):
104
+ """Context manager exit with automatic cleanup."""
105
+ if hasattr(self, 'logger'):
106
+ self._cleanup_logger(self.logger)
107
+
108
+ def _cleanup_logger(self, logger: logging.Logger) -> None:
109
+ """Clean up logger resources by closing all handlers with thread safety."""
110
+ cleanup_logger_handlers(logger)
111
+
112
+ @staticmethod
113
+ def cleanup_logger(logger: logging.Logger) -> None:
114
+ """Static method for cleaning up logger resources (backward compatibility)."""
115
+ cleanup_logger_handlers(logger)
116
+
117
+
118
+ class GZipRotatorTimed:
119
+ def __init__(self, dir_logs: str, days_to_keep: int):
120
+ self.dir = dir_logs
121
+ self.days_to_keep = days_to_keep
122
+
123
+ def __call__(self, source: str, dest: str) -> None:
124
+ remove_old_logs(self.dir, self.days_to_keep)
125
+ sufix = os.path.splitext(dest)[1].replace(".", "")
126
+ gzip_file_with_sufix(source, sufix)