pythonLogs 4.0.4__tar.gz → 4.0.6__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.4
3
+ Version: 4.0.6
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
@@ -35,8 +34,10 @@ Description-Content-Type: text/markdown
35
34
  [![PyPi](https://img.shields.io/pypi/v/pythonLogs.svg)](https://pypi.python.org/pypi/pythonLogs)
36
35
  [![PyPI Downloads](https://static.pepy.tech/badge/pythonLogs)](https://pepy.tech/projects/pythonLogs)
37
36
  [![codecov](https://codecov.io/gh/ddc/pythonLogs/graph/badge.svg?token=QsjwsmYzgD)](https://codecov.io/gh/ddc/pythonLogs)
38
- [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
37
+ [![CI/CD Pipeline](https://github.com/ddc/pythonLogs/actions/workflows/workflow.yml/badge.svg)](https://github.com/ddc/pythonLogs/actions/workflows/workflow.yml)
38
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ddc_pythonLogs&metric=alert_status)](https://sonarcloud.io/dashboard?id=ddc_pythonLogs)
39
39
  [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A//actions-badge.atrox.dev/ddc/pythonLogs/badge?ref=main&label=build&logo=none)](https://actions-badge.atrox.dev/ddc/pythonLogs/goto?ref=main)
40
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
40
41
  [![Python](https://img.shields.io/pypi/pyversions/pythonLogs.svg)](https://www.python.org/downloads)
41
42
 
42
43
  [![Support me on GitHub](https://img.shields.io/badge/Support_me_on_GitHub-154c79?style=for-the-badge&logo=github)](https://github.com/sponsors/ddc)
@@ -5,8 +5,10 @@
5
5
  [![PyPi](https://img.shields.io/pypi/v/pythonLogs.svg)](https://pypi.python.org/pypi/pythonLogs)
6
6
  [![PyPI Downloads](https://static.pepy.tech/badge/pythonLogs)](https://pepy.tech/projects/pythonLogs)
7
7
  [![codecov](https://codecov.io/gh/ddc/pythonLogs/graph/badge.svg?token=QsjwsmYzgD)](https://codecov.io/gh/ddc/pythonLogs)
8
- [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
8
+ [![CI/CD Pipeline](https://github.com/ddc/pythonLogs/actions/workflows/workflow.yml/badge.svg)](https://github.com/ddc/pythonLogs/actions/workflows/workflow.yml)
9
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ddc_pythonLogs&metric=alert_status)](https://sonarcloud.io/dashboard?id=ddc_pythonLogs)
9
10
  [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A//actions-badge.atrox.dev/ddc/pythonLogs/badge?ref=main&label=build&logo=none)](https://actions-badge.atrox.dev/ddc/pythonLogs/goto?ref=main)
11
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
10
12
  [![Python](https://img.shields.io/pypi/pyversions/pythonLogs.svg)](https://www.python.org/downloads)
11
13
 
12
14
  [![Support me on GitHub](https://img.shields.io/badge/Support_me_on_GitHub-154c79?style=for-the-badge&logo=github)](https://github.com/sponsors/ddc)
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "pythonLogs"
7
- version = "4.0.4"
7
+ version = "4.0.6"
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
 
@@ -34,7 +34,14 @@ class BasicLog:
34
34
  logger.setLevel(self.level)
35
35
  logging.Formatter.converter = get_timezone_function(self.timezone)
36
36
  _format = get_format(self.showlocation, self.appname, self.timezone)
37
- logging.basicConfig(datefmt=self.datefmt, encoding=self.encoding, format=_format)
37
+
38
+ # Only add handler if logger doesn't have any handlers
39
+ if not logger.handlers:
40
+ handler = logging.StreamHandler()
41
+ formatter = logging.Formatter(_format, datefmt=self.datefmt)
42
+ handler.setFormatter(formatter)
43
+ logger.addHandler(handler)
44
+
38
45
  self.logger = logger
39
46
  # Register weak reference for memory tracking
40
47
  register_logger_weakref(logger)
@@ -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
+ }
@@ -3,6 +3,7 @@ import atexit
3
3
  import logging
4
4
  import threading
5
5
  import time
6
+ from dataclasses import dataclass
6
7
  from enum import Enum
7
8
  from typing import Dict, Optional, Tuple, Union
8
9
  from pythonLogs.basic_log import BasicLog
@@ -12,6 +13,25 @@ from pythonLogs.size_rotating import SizeRotatingLog
12
13
  from pythonLogs.timed_rotating import TimedRotatingLog
13
14
 
14
15
 
16
+ @dataclass
17
+ class LoggerConfig:
18
+ """Configuration class to group logger parameters"""
19
+ level: Optional[Union[LogLevel, str]] = None
20
+ name: Optional[str] = None
21
+ directory: Optional[str] = None
22
+ filenames: Optional[list | tuple] = None
23
+ encoding: Optional[str] = None
24
+ datefmt: Optional[str] = None
25
+ timezone: Optional[str] = None
26
+ streamhandler: Optional[bool] = None
27
+ showlocation: Optional[bool] = None
28
+ maxmbytes: Optional[int] = None
29
+ when: Optional[Union[RotateWhen, str]] = None
30
+ sufix: Optional[str] = None
31
+ rotateatutc: Optional[bool] = None
32
+ daystokeep: Optional[int] = None
33
+
34
+
15
35
  class LoggerType(str, Enum):
16
36
  """Available logger types"""
17
37
  BASIC = "basic"
@@ -80,7 +100,7 @@ class LoggerFactory:
80
100
 
81
101
  # Check if logger already exists in the registry
82
102
  if name in cls._logger_registry:
83
- logger, timestamp = cls._logger_registry[name]
103
+ logger, _ = cls._logger_registry[name]
84
104
  # Update timestamp for LRU tracking
85
105
  cls._logger_registry[name] = (logger, time.time())
86
106
  return logger
@@ -189,21 +209,8 @@ class LoggerFactory:
189
209
  @staticmethod
190
210
  def create_logger(
191
211
  logger_type: Union[LoggerType, str],
192
- level: Optional[Union[LogLevel, str]] = None,
193
- name: Optional[str] = None,
194
- directory: Optional[str] = None,
195
- filenames: Optional[list | tuple] = None,
196
- encoding: Optional[str] = None,
197
- datefmt: Optional[str] = None,
198
- timezone: Optional[str] = None,
199
- streamhandler: Optional[bool] = None,
200
- showlocation: Optional[bool] = None, # Size rotating specific
201
- maxmbytes: Optional[int] = None, # Timed rotating specific
202
- when: Optional[Union[RotateWhen, str]] = None,
203
- sufix: Optional[str] = None,
204
- rotateatutc: Optional[bool] = None,
205
- # Common
206
- daystokeep: Optional[int] = None,
212
+ config: Optional[LoggerConfig] = None,
213
+ **kwargs
207
214
  ) -> logging.Logger:
208
215
 
209
216
  """
@@ -211,20 +218,8 @@ class LoggerFactory:
211
218
 
212
219
  Args:
213
220
  logger_type: Type of logger to create (LoggerType enum or string)
214
- level: Log level (LogLevel enum or string: DEBUG, INFO, WARNING, ERROR, CRITICAL)
215
- name: Logger name
216
- directory: Log directory path
217
- filenames: List/tuple of log filenames
218
- encoding: File encoding
219
- datefmt: Date format string
220
- timezone: Timezone for timestamps
221
- streamhandler: Enable console output
222
- showlocation: Show file location in logs
223
- maxmbytes: Max file size in MB (size rotating only)
224
- when: When to rotate (RotateWhen enum or string: MIDNIGHT, HOURLY, DAILY, etc.)
225
- sufix: Date suffix for rotated files (timed rotating only)
226
- rotateatutc: Rotate at UTC time (timed rotating only)
227
- daystokeep: Days to keep old logs
221
+ config: LoggerConfig object with logger parameters
222
+ **kwargs: Individual logger parameters (for backward compatibility)
228
223
 
229
224
  Returns:
230
225
  Configured logger instance
@@ -239,50 +234,72 @@ class LoggerFactory:
239
234
  except ValueError:
240
235
  raise ValueError(f"Invalid logger type: {logger_type}. Valid types: {[t.value for t in LoggerType]}")
241
236
 
237
+ # Merge config and kwargs (kwargs take precedence for backward compatibility)
238
+ if config is None:
239
+ config = LoggerConfig()
240
+
241
+ # Create a new config with kwargs overriding config values
242
+ final_config = LoggerConfig(
243
+ level=kwargs.get('level', config.level),
244
+ name=kwargs.get('name', config.name),
245
+ directory=kwargs.get('directory', config.directory),
246
+ filenames=kwargs.get('filenames', config.filenames),
247
+ encoding=kwargs.get('encoding', config.encoding),
248
+ datefmt=kwargs.get('datefmt', config.datefmt),
249
+ timezone=kwargs.get('timezone', config.timezone),
250
+ streamhandler=kwargs.get('streamhandler', config.streamhandler),
251
+ showlocation=kwargs.get('showlocation', config.showlocation),
252
+ maxmbytes=kwargs.get('maxmbytes', config.maxmbytes),
253
+ when=kwargs.get('when', config.when),
254
+ sufix=kwargs.get('sufix', config.sufix),
255
+ rotateatutc=kwargs.get('rotateatutc', config.rotateatutc),
256
+ daystokeep=kwargs.get('daystokeep', config.daystokeep)
257
+ )
258
+
242
259
  # Convert enum values to strings for logger classes
243
- level_str = level.value if isinstance(level, LogLevel) else level
244
- when_str = when.value if isinstance(when, RotateWhen) else when
260
+ level_str = final_config.level.value if isinstance(final_config.level, LogLevel) else final_config.level
261
+ when_str = final_config.when.value if isinstance(final_config.when, RotateWhen) else final_config.when
245
262
 
246
263
  # Create logger based on type
247
264
  match logger_type:
248
265
  case LoggerType.BASIC:
249
266
  logger_instance = BasicLog(
250
267
  level=level_str,
251
- name=name,
252
- encoding=encoding,
253
- datefmt=datefmt,
254
- timezone=timezone,
255
- showlocation=showlocation, )
268
+ name=final_config.name,
269
+ encoding=final_config.encoding,
270
+ datefmt=final_config.datefmt,
271
+ timezone=final_config.timezone,
272
+ showlocation=final_config.showlocation, )
256
273
 
257
274
  case LoggerType.SIZE_ROTATING:
258
275
  logger_instance = SizeRotatingLog(
259
276
  level=level_str,
260
- name=name,
261
- directory=directory,
262
- filenames=filenames,
263
- maxmbytes=maxmbytes,
264
- daystokeep=daystokeep,
265
- encoding=encoding,
266
- datefmt=datefmt,
267
- timezone=timezone,
268
- streamhandler=streamhandler,
269
- showlocation=showlocation, )
277
+ name=final_config.name,
278
+ directory=final_config.directory,
279
+ filenames=final_config.filenames,
280
+ maxmbytes=final_config.maxmbytes,
281
+ daystokeep=final_config.daystokeep,
282
+ encoding=final_config.encoding,
283
+ datefmt=final_config.datefmt,
284
+ timezone=final_config.timezone,
285
+ streamhandler=final_config.streamhandler,
286
+ showlocation=final_config.showlocation, )
270
287
 
271
288
  case LoggerType.TIMED_ROTATING:
272
289
  logger_instance = TimedRotatingLog(
273
290
  level=level_str,
274
- name=name,
275
- directory=directory,
276
- filenames=filenames,
291
+ name=final_config.name,
292
+ directory=final_config.directory,
293
+ filenames=final_config.filenames,
277
294
  when=when_str,
278
- sufix=sufix,
279
- daystokeep=daystokeep,
280
- encoding=encoding,
281
- datefmt=datefmt,
282
- timezone=timezone,
283
- streamhandler=streamhandler,
284
- showlocation=showlocation,
285
- rotateatutc=rotateatutc, )
295
+ sufix=final_config.sufix,
296
+ daystokeep=final_config.daystokeep,
297
+ encoding=final_config.encoding,
298
+ datefmt=final_config.datefmt,
299
+ timezone=final_config.timezone,
300
+ streamhandler=final_config.streamhandler,
301
+ showlocation=final_config.showlocation,
302
+ rotateatutc=final_config.rotateatutc, )
286
303
 
287
304
  case _:
288
305
  raise ValueError(f"Unsupported logger type: {logger_type}")
@@ -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:
@@ -11,7 +11,7 @@ from pythonLogs.constants import (
11
11
  DEFAULT_ROTATE_SUFFIX,
12
12
  DEFAULT_TIMEZONE,
13
13
  LogLevel,
14
- RotateWhen
14
+ RotateWhen,
15
15
  )
16
16
 
17
17
 
@@ -1,7 +1,8 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  import functools
3
3
  import threading
4
- from typing import Any, Callable, Dict, TypeVar, Type
4
+ from typing import Any, Callable, Dict, Type, TypeVar
5
+
5
6
 
6
7
  F = TypeVar('F', bound=Callable[..., Any])
7
8
 
@@ -58,34 +59,49 @@ def thread_safe(func: F) -> F:
58
59
  return wrapper
59
60
 
60
61
 
62
+ def _get_wrappable_methods(cls: Type) -> list:
63
+ """Helper function to get methods that should be made thread-safe."""
64
+ return [
65
+ method_name for method_name in dir(cls)
66
+ if (callable(getattr(cls, method_name, None)) and
67
+ not method_name.startswith('_') and
68
+ method_name not in ['__enter__', '__exit__', '__init__'])
69
+ ]
70
+
71
+
72
+ def _ensure_class_has_lock(cls: Type) -> None:
73
+ """Ensure the class has a lock attribute."""
74
+ if not hasattr(cls, '_lock'):
75
+ cls._lock = threading.RLock()
76
+
77
+
78
+ def _should_wrap_method(cls: Type, method_name: str, original_method: Any) -> bool:
79
+ """Check if a method should be wrapped with thread safety."""
80
+ return (hasattr(cls, method_name) and
81
+ callable(original_method) and
82
+ not hasattr(original_method, '_thread_safe_wrapped'))
83
+
84
+
61
85
  def auto_thread_safe(thread_safe_methods: list = None):
62
86
  """Class decorator that adds automatic thread safety to specified methods."""
63
87
 
64
88
  def decorator(cls: Type) -> Type:
65
- # Add lock to class if not present
66
- if not hasattr(cls, '_lock'):
67
- cls._lock = threading.RLock()
89
+ _ensure_class_has_lock(cls)
68
90
 
69
91
  # Store thread-safe methods list
70
92
  if thread_safe_methods:
71
93
  cls._thread_safe_methods = thread_safe_methods
72
94
 
73
95
  # 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
- ]
96
+ methods_to_wrap = thread_safe_methods or _get_wrappable_methods(cls)
80
97
 
81
98
  # Wrap each method
82
99
  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)
100
+ original_method = getattr(cls, method_name, None)
101
+ if _should_wrap_method(cls, method_name, original_method):
102
+ wrapped_method = thread_safe(original_method)
103
+ wrapped_method._thread_safe_wrapped = True
104
+ setattr(cls, method_name, wrapped_method)
89
105
 
90
106
  return cls
91
107
 
@@ -132,4 +148,4 @@ class ThreadSafeContext:
132
148
  return self
133
149
 
134
150
  def __exit__(self, exc_type, exc_val, exc_tb):
135
- self.lock.release()
151
+ self.lock.release()
@@ -10,7 +10,7 @@ from pythonLogs.log_utils import (
10
10
  get_logger_and_formatter,
11
11
  get_stream_handler,
12
12
  gzip_file_with_sufix,
13
- remove_old_logs,
13
+ remove_old_logs,
14
14
  )
15
15
  from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
16
16
  from pythonLogs.settings import get_log_settings
File without changes