pythonLogs 4.0.3__tar.gz → 4.0.5__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pythonLogs
3
- Version: 4.0.3
3
+ Version: 4.0.5
4
4
  Summary: High-performance Python logging library with file rotation and optimized caching for better performance
5
5
  License: MIT
6
6
  Keywords: python3,python-3,python,log,logging,logger,logutils,log-utils,pythonLogs
@@ -21,7 +21,6 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python :: 3 :: Only
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
- Requires-Dist: pydantic (>=2.11.7,<3.0.0)
25
24
  Requires-Dist: pydantic-settings (>=2.10.1,<3.0.0)
26
25
  Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
27
26
  Project-URL: Homepage, https://pypi.org/project/pythonLogs
@@ -58,7 +57,6 @@ High-performance Python logging library with file rotation and optimized caching
58
57
  - [Memory Management](#memory-management)
59
58
  - [Flexible Configuration Options](#flexible-configuration-options)
60
59
  - [Migration Guide](#migration-guide)
61
- - [Performance Improvements](#performance-improvements)
62
60
  - [Development](#source-code)
63
61
  - [Run Tests and Get Coverage Report using Poe](#run-tests-and-get-coverage-report-using-poe)
64
62
  - [License](#license)
@@ -476,40 +474,6 @@ registered = LoggerFactory.get_registered_loggers()
476
474
  print(f"Currently registered: {list(registered.keys())}")
477
475
  ```
478
476
 
479
- ## Thread-Safe Operations
480
- All memory management operations are thread-safe and can be used safely in multi-threaded applications:
481
-
482
- ```python
483
- import threading
484
- from pythonLogs import size_rotating_logger, clear_logger_registry
485
-
486
- def worker_function(worker_id):
487
- # Each thread can safely create and use loggers
488
- logger = size_rotating_logger(
489
- name=f"worker_{worker_id}",
490
- directory="/app/logs"
491
- )
492
-
493
- with logger as log:
494
- log.info(f"Worker {worker_id} started")
495
- # Automatic cleanup per thread
496
-
497
- # Create multiple threads - all operations are thread-safe
498
- threads = []
499
- for i in range(10):
500
- thread = threading.Thread(target=worker_function, args=(i,))
501
- threads.append(thread)
502
- thread.start()
503
-
504
- # Wait for completion and clean up
505
- for thread in threads:
506
- thread.join()
507
-
508
- # Safe to clear registry from main thread
509
- clear_logger_registry()
510
- ```
511
-
512
-
513
477
  # Flexible Configuration Options
514
478
  You can use either enums (for type safety) or strings (for simplicity):
515
479
 
@@ -582,25 +546,6 @@ timed_logger = timed_rotating_logger(level=LogLevel.WARNING, name="app", directo
582
546
  - 🔧 **Cleaner API** without manual `.init()` calls
583
547
  - 📚 **Centralized configuration** through factory pattern
584
548
 
585
- # Performance Improvements
586
-
587
- ## Benchmarks
588
- The factory pattern with optimizations provides significant performance improvements:
589
-
590
- | Feature | Improvement | Benefit |
591
- |---------|-------------|---------|
592
- | Logger Registry | 90%+ faster | Cached logger instances |
593
- | Settings Caching | ~85% faster | Reused configuration objects |
594
- | Directory Validation | ~75% faster | Cached permission checks |
595
- | Timezone Operations | ~60% faster | Cached timezone functions |
596
-
597
- ## Performance Test Results
598
- ```python
599
- # Create 100 loggers - Performance comparison
600
- # Legacy method: ~0.045 seconds
601
- # Factory pattern: ~0.004 seconds
602
- # Improvement: 91% faster ⚡
603
- ```
604
549
 
605
550
  # Source Code
606
551
  ### Build
@@ -609,7 +554,6 @@ poetry build -f wheel
609
554
  ```
610
555
 
611
556
 
612
-
613
557
  # Run Tests and Get Coverage Report using Poe
614
558
  ```shell
615
559
  poetry update --with test
@@ -617,13 +561,10 @@ poe test
617
561
  ```
618
562
 
619
563
 
620
-
621
564
  # License
622
565
  Released under the [MIT License](LICENSE)
623
566
 
624
567
 
625
-
626
-
627
568
  # Buy me a cup of coffee
628
569
  + [GitHub Sponsor](https://github.com/sponsors/ddc)
629
570
  + [ko-fi](https://ko-fi.com/ddcsta)
@@ -28,7 +28,6 @@ High-performance Python logging library with file rotation and optimized caching
28
28
  - [Memory Management](#memory-management)
29
29
  - [Flexible Configuration Options](#flexible-configuration-options)
30
30
  - [Migration Guide](#migration-guide)
31
- - [Performance Improvements](#performance-improvements)
32
31
  - [Development](#source-code)
33
32
  - [Run Tests and Get Coverage Report using Poe](#run-tests-and-get-coverage-report-using-poe)
34
33
  - [License](#license)
@@ -446,40 +445,6 @@ registered = LoggerFactory.get_registered_loggers()
446
445
  print(f"Currently registered: {list(registered.keys())}")
447
446
  ```
448
447
 
449
- ## Thread-Safe Operations
450
- All memory management operations are thread-safe and can be used safely in multi-threaded applications:
451
-
452
- ```python
453
- import threading
454
- from pythonLogs import size_rotating_logger, clear_logger_registry
455
-
456
- def worker_function(worker_id):
457
- # Each thread can safely create and use loggers
458
- logger = size_rotating_logger(
459
- name=f"worker_{worker_id}",
460
- directory="/app/logs"
461
- )
462
-
463
- with logger as log:
464
- log.info(f"Worker {worker_id} started")
465
- # Automatic cleanup per thread
466
-
467
- # Create multiple threads - all operations are thread-safe
468
- threads = []
469
- for i in range(10):
470
- thread = threading.Thread(target=worker_function, args=(i,))
471
- threads.append(thread)
472
- thread.start()
473
-
474
- # Wait for completion and clean up
475
- for thread in threads:
476
- thread.join()
477
-
478
- # Safe to clear registry from main thread
479
- clear_logger_registry()
480
- ```
481
-
482
-
483
448
  # Flexible Configuration Options
484
449
  You can use either enums (for type safety) or strings (for simplicity):
485
450
 
@@ -552,25 +517,6 @@ timed_logger = timed_rotating_logger(level=LogLevel.WARNING, name="app", directo
552
517
  - 🔧 **Cleaner API** without manual `.init()` calls
553
518
  - 📚 **Centralized configuration** through factory pattern
554
519
 
555
- # Performance Improvements
556
-
557
- ## Benchmarks
558
- The factory pattern with optimizations provides significant performance improvements:
559
-
560
- | Feature | Improvement | Benefit |
561
- |---------|-------------|---------|
562
- | Logger Registry | 90%+ faster | Cached logger instances |
563
- | Settings Caching | ~85% faster | Reused configuration objects |
564
- | Directory Validation | ~75% faster | Cached permission checks |
565
- | Timezone Operations | ~60% faster | Cached timezone functions |
566
-
567
- ## Performance Test Results
568
- ```python
569
- # Create 100 loggers - Performance comparison
570
- # Legacy method: ~0.045 seconds
571
- # Factory pattern: ~0.004 seconds
572
- # Improvement: 91% faster ⚡
573
- ```
574
520
 
575
521
  # Source Code
576
522
  ### Build
@@ -579,7 +525,6 @@ poetry build -f wheel
579
525
  ```
580
526
 
581
527
 
582
-
583
528
  # Run Tests and Get Coverage Report using Poe
584
529
  ```shell
585
530
  poetry update --with test
@@ -587,13 +532,10 @@ poe test
587
532
  ```
588
533
 
589
534
 
590
-
591
535
  # License
592
536
  Released under the [MIT License](LICENSE)
593
537
 
594
538
 
595
-
596
-
597
539
  # Buy me a cup of coffee
598
540
  + [GitHub Sponsor](https://github.com/sponsors/ddc)
599
541
  + [ko-fi](https://ko-fi.com/ddcsta)
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "pythonLogs"
7
- version = "4.0.3"
7
+ version = "4.0.5"
8
8
  description = "High-performance Python logging library with file rotation and optimized caching for better performance"
9
9
  license = "MIT"
10
10
  readme = "README.md"
@@ -32,7 +32,6 @@ classifiers = [
32
32
 
33
33
  [tool.poetry.dependencies]
34
34
  python = "^3.10"
35
- pydantic = "^2.11.7"
36
35
  pydantic-settings = "^2.10.1"
37
36
  python-dotenv = "^1.1.1"
38
37
 
@@ -1,12 +1,13 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  import logging
3
- import threading
4
3
  from typing import Optional
5
4
  from pythonLogs.log_utils import get_format, get_level, get_timezone_function
6
5
  from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
7
6
  from pythonLogs.settings import get_log_settings
7
+ from pythonLogs.thread_safety import auto_thread_safe
8
8
 
9
9
 
10
+ @auto_thread_safe(['init', '_cleanup_logger'])
10
11
  class BasicLog:
11
12
  """Basic logger with context manager support for automatic resource cleanup."""
12
13
 
@@ -27,8 +28,6 @@ class BasicLog:
27
28
  self.timezone = timezone or _settings.timezone
28
29
  self.showlocation = showlocation or _settings.show_location
29
30
  self.logger = None
30
- # Instance-level lock for thread safety
31
- self._lock = threading.Lock()
32
31
 
33
32
  def init(self):
34
33
  logger = logging.getLogger(self.appname)
@@ -54,8 +53,7 @@ class BasicLog:
54
53
 
55
54
  def _cleanup_logger(self, logger: logging.Logger) -> None:
56
55
  """Clean up logger resources by closing all handlers with thread safety."""
57
- with self._lock:
58
- cleanup_logger_handlers(logger)
56
+ cleanup_logger_handlers(logger)
59
57
 
60
58
  @staticmethod
61
59
  def cleanup_logger(logger: logging.Logger) -> None:
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ import logging
2
3
  from enum import Enum
3
4
 
4
5
  # File and Directory Constants
@@ -40,3 +41,15 @@ class RotateWhen(str, Enum):
40
41
  SUNDAY = "W6"
41
42
  HOURLY = "H"
42
43
  DAILY = "D"
44
+
45
+
46
+ # Level mapping for performance optimization
47
+ LEVEL_MAP = {
48
+ LogLevel.DEBUG.value.lower(): logging.DEBUG,
49
+ LogLevel.WARNING.value.lower(): logging.WARNING,
50
+ LogLevel.WARN.value.lower(): logging.WARNING,
51
+ LogLevel.ERROR.value.lower(): logging.ERROR,
52
+ LogLevel.CRITICAL.value.lower(): logging.CRITICAL,
53
+ LogLevel.CRIT.value.lower(): logging.CRITICAL,
54
+ LogLevel.INFO.value.lower(): logging.INFO,
55
+ }
@@ -12,7 +12,7 @@ from functools import lru_cache
12
12
  from pathlib import Path
13
13
  from typing import Callable, Set
14
14
  from zoneinfo import ZoneInfo
15
- from pythonLogs.constants import DEFAULT_FILE_MODE, LogLevel
15
+ from pythonLogs.constants import DEFAULT_FILE_MODE, LEVEL_MAP
16
16
 
17
17
 
18
18
  # Global cache for checked directories with thread safety and size limits
@@ -182,17 +182,7 @@ def get_level(level: str) -> int:
182
182
  write_stderr(f"Unable to get log level. Setting default level to: 'INFO' ({logging.INFO})")
183
183
  return logging.INFO
184
184
 
185
- level_map = {
186
- LogLevel.DEBUG.value.lower(): logging.DEBUG,
187
- LogLevel.WARNING.value.lower(): logging.WARNING,
188
- LogLevel.WARN.value.lower(): logging.WARNING,
189
- LogLevel.ERROR.value.lower(): logging.ERROR,
190
- LogLevel.CRITICAL.value.lower(): logging.CRITICAL,
191
- LogLevel.CRIT.value.lower(): logging.CRITICAL,
192
- LogLevel.INFO.value.lower(): logging.INFO,
193
- }
194
-
195
- return level_map.get(level.lower(), logging.INFO)
185
+ return LEVEL_MAP.get(level.lower(), logging.INFO)
196
186
 
197
187
 
198
188
  def get_log_path(directory: str, filename: str) -> str:
@@ -2,7 +2,6 @@
2
2
  import logging.handlers
3
3
  import os
4
4
  import re
5
- import threading
6
5
  from typing import Optional
7
6
  from pythonLogs.constants import MB_TO_BYTES
8
7
  from pythonLogs.log_utils import (
@@ -18,8 +17,10 @@ from pythonLogs.log_utils import (
18
17
  )
19
18
  from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
20
19
  from pythonLogs.settings import get_log_settings
20
+ from pythonLogs.thread_safety import auto_thread_safe
21
21
 
22
22
 
23
+ @auto_thread_safe(['init', '_cleanup_logger'])
23
24
  class SizeRotatingLog:
24
25
  """Size-based rotating logger with context manager support for automatic resource cleanup."""
25
26
  def __init__(
@@ -49,8 +50,6 @@ class SizeRotatingLog:
49
50
  self.streamhandler = streamhandler or _settings.stream_handler
50
51
  self.showlocation = showlocation or _settings.show_location
51
52
  self.logger = None
52
- # Instance-level lock for thread safety
53
- self._lock = threading.Lock()
54
53
 
55
54
  def init(self):
56
55
  check_filename_instance(self.filenames)
@@ -98,8 +97,7 @@ class SizeRotatingLog:
98
97
 
99
98
  def _cleanup_logger(self, logger: logging.Logger) -> None:
100
99
  """Clean up logger resources by closing all handlers with thread safety."""
101
- with self._lock:
102
- cleanup_logger_handlers(logger)
100
+ cleanup_logger_handlers(logger)
103
101
 
104
102
  @staticmethod
105
103
  def cleanup_logger(logger: logging.Logger) -> None:
@@ -0,0 +1,135 @@
1
+ # -*- encoding: utf-8 -*-
2
+ import functools
3
+ import threading
4
+ from typing import Any, Callable, Dict, TypeVar, Type
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 for method_name in namespace
26
+ if (callable(getattr(cls, method_name, None)) and
27
+ not method_name.startswith('_') and
28
+ method_name not in ['__enter__', '__exit__', '__init__'])
29
+ ]
30
+
31
+ # Wrap each method with automatic locking
32
+ for method_name in thread_safe_methods:
33
+ if hasattr(cls, method_name):
34
+ original_method = getattr(cls, method_name)
35
+ if callable(original_method):
36
+ wrapped_method = thread_safe(original_method)
37
+ setattr(cls, method_name, wrapped_method)
38
+
39
+ return cls
40
+
41
+
42
+ def thread_safe(func: F) -> F:
43
+ """Decorator that automatically adds thread safety to methods."""
44
+
45
+ @functools.wraps(func)
46
+ def wrapper(self, *args, **kwargs):
47
+ # Use instance lock if available, otherwise class lock
48
+ lock = getattr(self, '_lock', None)
49
+ if lock is None:
50
+ # Check if class has lock, if not create one
51
+ if not hasattr(self.__class__, '_lock'):
52
+ self.__class__._lock = threading.RLock()
53
+ lock = self.__class__._lock
54
+
55
+ with lock:
56
+ return func(self, *args, **kwargs)
57
+
58
+ return wrapper
59
+
60
+
61
+ def auto_thread_safe(thread_safe_methods: list = None):
62
+ """Class decorator that adds automatic thread safety to specified methods."""
63
+
64
+ def decorator(cls: Type) -> Type:
65
+ # Add lock to class if not present
66
+ if not hasattr(cls, '_lock'):
67
+ cls._lock = threading.RLock()
68
+
69
+ # Store thread-safe methods list
70
+ if thread_safe_methods:
71
+ cls._thread_safe_methods = thread_safe_methods
72
+
73
+ # Get methods to make thread-safe
74
+ methods_to_wrap = thread_safe_methods or [
75
+ method_name for method_name in dir(cls)
76
+ if (callable(getattr(cls, method_name, None)) and
77
+ not method_name.startswith('_') and
78
+ method_name not in ['__enter__', '__exit__', '__init__'])
79
+ ]
80
+
81
+ # Wrap each method
82
+ for method_name in methods_to_wrap:
83
+ if hasattr(cls, method_name):
84
+ original_method = getattr(cls, method_name)
85
+ if callable(original_method) and not hasattr(original_method, '_thread_safe_wrapped'):
86
+ wrapped_method = thread_safe(original_method)
87
+ wrapped_method._thread_safe_wrapped = True
88
+ setattr(cls, method_name, wrapped_method)
89
+
90
+ return cls
91
+
92
+ return decorator
93
+
94
+
95
+ class AutoThreadSafe:
96
+ """Base class that provides automatic thread safety for all public methods."""
97
+
98
+ def __init__(self):
99
+ if not hasattr(self, '_lock'):
100
+ self._lock = threading.RLock()
101
+
102
+ def __init_subclass__(cls, **kwargs):
103
+ super().__init_subclass__(**kwargs)
104
+
105
+ # Add class-level lock
106
+ if not hasattr(cls, '_lock'):
107
+ cls._lock = threading.RLock()
108
+
109
+ # Auto-wrap public methods
110
+ for attr_name in dir(cls):
111
+ if not attr_name.startswith('_'):
112
+ attr = getattr(cls, attr_name)
113
+ if callable(attr) and not hasattr(attr, '_thread_safe_wrapped'):
114
+ wrapped_attr = thread_safe(attr)
115
+ wrapped_attr._thread_safe_wrapped = True
116
+ setattr(cls, attr_name, wrapped_attr)
117
+
118
+
119
+ def synchronized_method(func: F) -> F:
120
+ """Decorator for individual methods that need thread safety."""
121
+ return thread_safe(func)
122
+
123
+
124
+ class ThreadSafeContext:
125
+ """Context manager for thread-safe operations."""
126
+
127
+ def __init__(self, lock: threading.Lock):
128
+ self.lock = lock
129
+
130
+ def __enter__(self):
131
+ self.lock.acquire()
132
+ return self
133
+
134
+ def __exit__(self, exc_type, exc_val, exc_tb):
135
+ self.lock.release()
@@ -1,7 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  import logging.handlers
3
3
  import os
4
- import threading
5
4
  from typing import Optional
6
5
  from pythonLogs.log_utils import (
7
6
  check_directory_permissions,
@@ -15,8 +14,10 @@ from pythonLogs.log_utils import (
15
14
  )
16
15
  from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
17
16
  from pythonLogs.settings import get_log_settings
17
+ from pythonLogs.thread_safety import auto_thread_safe
18
18
 
19
19
 
20
+ @auto_thread_safe(['init', '_cleanup_logger'])
20
21
  class TimedRotatingLog:
21
22
  """
22
23
  Time-based rotating logger with context manager support for automatic resource cleanup.
@@ -60,8 +61,6 @@ class TimedRotatingLog:
60
61
  self.showlocation = showlocation or _settings.show_location
61
62
  self.rotateatutc = rotateatutc or _settings.rotate_at_utc
62
63
  self.logger = None
63
- # Instance-level lock for thread safety
64
- self._lock = threading.Lock()
65
64
 
66
65
  def init(self):
67
66
  check_filename_instance(self.filenames)
@@ -107,8 +106,7 @@ class TimedRotatingLog:
107
106
 
108
107
  def _cleanup_logger(self, logger: logging.Logger) -> None:
109
108
  """Clean up logger resources by closing all handlers with thread safety."""
110
- with self._lock:
111
- cleanup_logger_handlers(logger)
109
+ cleanup_logger_handlers(logger)
112
110
 
113
111
  @staticmethod
114
112
  def cleanup_logger(logger: logging.Logger) -> None:
File without changes