pythonLogs 6.0.1__tar.gz → 6.0.2__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,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonLogs
3
- Version: 6.0.1
3
+ Version: 6.0.2
4
4
  Summary: High-performance Python logging library with file rotation and optimized caching for better performance
5
- Project-URL: Homepage, https://pypi.org/project/pythonLogs
6
5
  Project-URL: Repository, https://github.com/ddc/pythonLogs
7
- Author-email: Daniel Costa <danieldcsta@gmail.com>
6
+ Project-URL: Homepage, https://pypi.org/project/pythonLogs
7
+ Author-email: Daniel Costa <ddcsoftwares@proton.me>
8
8
  Maintainer: Daniel Costa
9
9
  License: MIT
10
10
  License-File: LICENSE
@@ -16,17 +16,13 @@ Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Natural Language :: English
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
19
20
  Classifier: Programming Language :: Python :: 3.12
20
21
  Classifier: Programming Language :: Python :: 3.13
21
22
  Classifier: Programming Language :: Python :: 3.14
22
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
- Requires-Python: >=3.12
24
+ Requires-Python: >=3.11
24
25
  Requires-Dist: pydantic-settings>=2.11.0
25
- Provides-Extra: test
26
- Requires-Dist: poethepoet>=0.40.0; extra == 'test'
27
- Requires-Dist: psutil>=7.2.2; extra == 'test'
28
- Requires-Dist: pytest-cov>=7.0.0; extra == 'test'
29
- Requires-Dist: pytest>=9.0.2; extra == 'test'
30
26
  Description-Content-Type: text/markdown
31
27
 
32
28
  <h1 align="center">
@@ -39,9 +35,9 @@ Description-Content-Type: text/markdown
39
35
  <a href="https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ"><img src="https://img.shields.io/badge/Donate-PayPal-brightgreen.svg?style=plastic" alt="Donate"/></a>
40
36
  <a href="https://github.com/sponsors/ddc"><img src="https://img.shields.io/static/v1?style=plastic&label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=ff69b4" alt="Sponsor"/></a>
41
37
  <br>
38
+ <a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic" alt="Code style: black"/></a>
42
39
  <a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json?style=plastic" alt="uv"/></a>
43
40
  <a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json?style=plastic" alt="Ruff"/></a>
44
- <a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic" alt="Code style: black"/></a>
45
41
  <br>
46
42
  <a href="https://www.python.org/downloads"><img src="https://img.shields.io/pypi/pyversions/pythonLogs.svg?style=plastic&logo=python&cacheSeconds=3600" alt="Python"/></a>
47
43
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=plastic" alt="License: MIT"/></a>
@@ -72,7 +68,9 @@ Description-Content-Type: text/markdown
72
68
  - [Settings Cache Management](#settings-cache-management)
73
69
  - [Flexible Configuration Options](#flexible-configuration-options)
74
70
  - [Development](#development)
75
- - [Create DEV Environment, Running Tests and Building Wheel](#create-dev-environment-running-tests-and-building-wheel)
71
+ - [Create DEV Environment and Running Tests](#create-dev-environment-and-running-tests)
72
+ - [Update DEV Environment Packages](#update-dev-environment-packages)
73
+ - [Building Wheel](#building-wheel)
76
74
  - [Optionals](#optionals)
77
75
  - [License](#license)
78
76
  - [Support](#support)
@@ -380,20 +378,34 @@ RotateWhen.MONDAY # "W0"
380
378
 
381
379
  # Development
382
380
 
383
- Must have [UV](https://uv.run/docs/getting-started/installation),
384
- [Black](https://black.readthedocs.io/en/stable/getting_started.html),
385
- [Ruff](https://docs.astral.sh/ruff/installation/), and
386
- [Poe the Poet](https://poethepoet.naber.dev/installation) installed.
381
+ Must have [UV](https://uv.run/docs/getting-started/installation) installed.
387
382
 
388
- ## Create DEV Environment, Running Tests and Building Wheel
383
+ ## Create DEV Environment and Running Tests
384
+
385
+ > **Note:** All poe tasks automatically run ruff linter along with Black formatting
389
386
 
390
387
  ```shell
391
- uv sync --all-extras
392
- poe linter
388
+ uv sync --all-extras --all-groups
393
389
  poe test
390
+ ```
391
+
392
+
393
+ ## Update DEV Environment Packages
394
+ This will update all packages dependencies
395
+
396
+ ```shell
397
+ poe updatedev
398
+ ```
399
+
400
+
401
+ ## Building Wheel
402
+ This will update all packages, run linter, both unit and integration tests and finally build the wheel
403
+
404
+ ```shell
394
405
  poe build
395
406
  ```
396
407
 
408
+
397
409
  ## Optionals
398
410
 
399
411
  ### Create a cprofile.prof file from unit tests
@@ -8,9 +8,9 @@
8
8
  <a href="https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ"><img src="https://img.shields.io/badge/Donate-PayPal-brightgreen.svg?style=plastic" alt="Donate"/></a>
9
9
  <a href="https://github.com/sponsors/ddc"><img src="https://img.shields.io/static/v1?style=plastic&label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=ff69b4" alt="Sponsor"/></a>
10
10
  <br>
11
+ <a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic" alt="Code style: black"/></a>
11
12
  <a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json?style=plastic" alt="uv"/></a>
12
13
  <a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json?style=plastic" alt="Ruff"/></a>
13
- <a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic" alt="Code style: black"/></a>
14
14
  <br>
15
15
  <a href="https://www.python.org/downloads"><img src="https://img.shields.io/pypi/pyversions/pythonLogs.svg?style=plastic&logo=python&cacheSeconds=3600" alt="Python"/></a>
16
16
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=plastic" alt="License: MIT"/></a>
@@ -41,7 +41,9 @@
41
41
  - [Settings Cache Management](#settings-cache-management)
42
42
  - [Flexible Configuration Options](#flexible-configuration-options)
43
43
  - [Development](#development)
44
- - [Create DEV Environment, Running Tests and Building Wheel](#create-dev-environment-running-tests-and-building-wheel)
44
+ - [Create DEV Environment and Running Tests](#create-dev-environment-and-running-tests)
45
+ - [Update DEV Environment Packages](#update-dev-environment-packages)
46
+ - [Building Wheel](#building-wheel)
45
47
  - [Optionals](#optionals)
46
48
  - [License](#license)
47
49
  - [Support](#support)
@@ -349,20 +351,34 @@ RotateWhen.MONDAY # "W0"
349
351
 
350
352
  # Development
351
353
 
352
- Must have [UV](https://uv.run/docs/getting-started/installation),
353
- [Black](https://black.readthedocs.io/en/stable/getting_started.html),
354
- [Ruff](https://docs.astral.sh/ruff/installation/), and
355
- [Poe the Poet](https://poethepoet.naber.dev/installation) installed.
354
+ Must have [UV](https://uv.run/docs/getting-started/installation) installed.
356
355
 
357
- ## Create DEV Environment, Running Tests and Building Wheel
356
+ ## Create DEV Environment and Running Tests
357
+
358
+ > **Note:** All poe tasks automatically run ruff linter along with Black formatting
358
359
 
359
360
  ```shell
360
- uv sync --all-extras
361
- poe linter
361
+ uv sync --all-extras --all-groups
362
362
  poe test
363
+ ```
364
+
365
+
366
+ ## Update DEV Environment Packages
367
+ This will update all packages dependencies
368
+
369
+ ```shell
370
+ poe updatedev
371
+ ```
372
+
373
+
374
+ ## Building Wheel
375
+ This will update all packages, run linter, both unit and integration tests and finally build the wheel
376
+
377
+ ```shell
363
378
  poe build
364
379
  ```
365
380
 
381
+
366
382
  ## Optionals
367
383
 
368
384
  ### Create a cprofile.prof file from unit tests
@@ -2,16 +2,31 @@
2
2
  requires = ["hatchling"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
+ [tool.hatch.metadata]
6
+ allow-direct-references = true
7
+
8
+ [tool.hatch.build]
9
+ include = ["pythonLogs/**/*"]
10
+
11
+ [tool.hatch.build.targets.wheel]
12
+ packages = ["pythonLogs"]
13
+
5
14
  [project]
6
15
  name = "pythonLogs"
7
- version = "6.0.1"
16
+ version = "6.0.2"
8
17
  description = "High-performance Python logging library with file rotation and optimized caching for better performance"
18
+ urls.Repository = "https://github.com/ddc/pythonLogs"
19
+ urls.Homepage = "https://pypi.org/project/pythonLogs"
9
20
  license = {text = "MIT"}
10
21
  readme = "README.md"
11
- authors = [{name = "Daniel Costa", email = "danieldcsta@gmail.com"}]
12
- maintainers = [{name = "Daniel Costa"}]
22
+ authors = [
23
+ {name = "Daniel Costa", email = "ddcsoftwares@proton.me"},
24
+ ]
25
+ maintainers = [
26
+ {name = "Daniel Costa"},
27
+ ]
13
28
  keywords = [
14
- "python3", "python-3", "python",
29
+ "python", "python3", "python-3",
15
30
  "log", "logging", "logger",
16
31
  "logutils", "log-utils", "pythonLogs"
17
32
  ]
@@ -20,6 +35,7 @@ classifiers = [
20
35
  "Development Status :: 5 - Production/Stable",
21
36
  "License :: OSI Approved :: MIT License",
22
37
  "Programming Language :: Python :: 3",
38
+ "Programming Language :: Python :: 3.11",
23
39
  "Programming Language :: Python :: 3.12",
24
40
  "Programming Language :: Python :: 3.13",
25
41
  "Programming Language :: Python :: 3.14",
@@ -28,52 +44,44 @@ classifiers = [
28
44
  "Intended Audience :: Developers",
29
45
  "Natural Language :: English",
30
46
  ]
31
- requires-python = ">=3.12"
47
+ requires-python = ">=3.11"
32
48
  dependencies = [
33
49
  "pydantic-settings>=2.11.0",
34
50
  ]
35
51
 
36
- [project.urls]
37
- Homepage = "https://pypi.org/project/pythonLogs"
38
- Repository = "https://github.com/ddc/pythonLogs"
39
-
40
- [project.optional-dependencies]
41
- test = [
42
- "poethepoet>=0.40.0",
52
+ [dependency-groups]
53
+ dev = [
43
54
  "psutil>=7.2.2",
44
- "pytest>=9.0.2",
45
55
  "pytest-cov>=7.0.0",
56
+ "poethepoet>=0.41.0",
57
+ "ruff>=0.15.0",
58
+ "black>=26.1.0",
46
59
  ]
47
60
 
48
- [tool.hatch.build]
49
- include = ["pythonLogs/**/*"]
50
-
51
- [tool.hatch.build.targets.wheel]
52
- packages = ["pythonLogs"]
53
-
54
61
  [tool.poe.tasks]
55
- build = "uv build --wheel"
56
- updatedev.shell = "uv lock && uv sync --no-install-project --all-extras"
57
62
  linter.shell = "uv run ruff check --fix . && uv run black ."
58
- profile = "uv run python -m cProfile -o cprofile.prof -m pytest"
59
- test = "uv run pytest"
63
+ profile.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_unit.prof -m pytest --no-cov"}]
64
+ test.sequence = ["linter", {shell = "uv run pytest"}]
65
+ updatedev.sequence = ["linter", {shell = "uv lock --upgrade && uv sync --all-extras --group dev"}]
66
+ build.sequence = ["updatedev", "test", {shell = "uv build --wheel"}]
60
67
 
61
68
  [tool.pytest.ini_options]
62
69
  addopts = "-v --cov --cov-report=term --cov-report=xml --junitxml=junit.xml"
63
70
  junit_family = "legacy"
64
71
  testpaths = ["tests"]
65
72
  markers = [
66
- "slow: marks tests as slow (deselect with '-m \"not slow\"')"
73
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
67
74
  ]
68
75
 
69
76
  [tool.coverage.run]
70
77
  omit = [
71
78
  "tests/*",
72
79
  "*/__init__.py",
73
- "*/_version.py",
74
80
  ]
75
81
 
76
82
  [tool.coverage.report]
83
+ show_missing = true
84
+ skip_covered = false
77
85
  exclude_lines = [
78
86
  "pragma: no cover",
79
87
  "def __repr__",
@@ -86,8 +94,6 @@ exclude_lines = [
86
94
  "class .*\\bProtocol\\):",
87
95
  "@(abc\\.)?abstractmethod",
88
96
  ]
89
- show_missing = true
90
- skip_covered = false
91
97
 
92
98
  [tool.black]
93
99
  line-length = 120
@@ -95,22 +101,18 @@ skip-string-normalization = true
95
101
 
96
102
  [tool.ruff]
97
103
  line-length = 120
104
+ target-version = "py311"
98
105
 
99
106
  [tool.ruff.lint]
100
- # I - Import sorting and organization
101
- # F401 - Detect and remove unused imports
102
- select = ["I", "F401"]
107
+ select = ["E", "W", "F", "I", "B", "C4", "UP"]
108
+ ignore = ["E501", "E402", "UP046", "UP047"]
109
+
110
+ [tool.ruff.lint.per-file-ignores]
111
+ "__init__.py" = ["F401"]
112
+ "tests/**/*.py" = ["S101", "S105", "S106", "S311", "SLF001", "F841"]
103
113
 
104
114
  [tool.ruff.lint.isort]
105
115
  known-first-party = ["pythonLogs"]
106
116
  force-sort-within-sections = false
107
117
  from-first = false
108
118
  no-sections = true
109
-
110
- [tool.ruff.lint.per-file-ignores]
111
- # S101 Use of `assert` detected
112
- # S105 Possible hardcoded password assigned to variable
113
- # S106 Possible hardcoded password assigned to argument
114
- # S311 Standard pseudo-random generators are not suitable for cryptographic purposes
115
- # SLF001 Private member accessed
116
- "tests/**/*.py" = ["S101", "S105", "S106", "S311", "SLF001"]
@@ -15,6 +15,7 @@ LOG_STREAM_HANDLER=True
15
15
  LOG_SHOW_LOCATION=False
16
16
  # Memory Management Settings
17
17
  LOG_MAX_LOGGERS=50
18
+ LOG_MAX_FORMATTERS=50
18
19
  LOG_LOGGER_TTL_SECONDS=1800
19
20
 
20
21
  # SizeRotatingLog Settings (only needed when using SizeRotatingLog)
@@ -16,7 +16,7 @@ __all__ = (
16
16
 
17
17
  __title__ = "pythonLogs"
18
18
  __author__ = "Daniel Costa"
19
- __email__ = "danieldcsta@gmail.com"
19
+ __email__ = "ddcsoftwares@proton.me"
20
20
  __license__ = "MIT"
21
21
  __copyright__ = "Copyright 2024-present DDC Softwares"
22
22
  __version__ = version(__title__)
@@ -5,7 +5,7 @@ from pythonLogs.core.settings import get_log_settings
5
5
  from pythonLogs.core.thread_safety import auto_thread_safe
6
6
 
7
7
 
8
- @auto_thread_safe(['init'])
8
+ @auto_thread_safe(["init"])
9
9
  class BasicLog:
10
10
  """Basic logger with context manager support for automatic resource cleanup."""
11
11
 
@@ -48,13 +48,13 @@ class BasicLog:
48
48
 
49
49
  def __enter__(self):
50
50
  """Context manager entry."""
51
- if not hasattr(self, 'logger') or self.logger is None:
51
+ if not hasattr(self, "logger") or self.logger is None:
52
52
  self.init()
53
53
  return self.logger
54
54
 
55
55
  def __exit__(self, exc_type, exc_val, exc_tb):
56
56
  """Context manager exit with automatic cleanup."""
57
- if hasattr(self, 'logger'):
57
+ if hasattr(self, "logger"):
58
58
  cleanup_logger_handlers(self.logger)
59
59
 
60
60
  @staticmethod
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from enum import Enum
2
+ from enum import StrEnum
3
3
 
4
4
  # File and Directory Constants
5
5
  MB_TO_BYTES = 1024 * 1024
@@ -17,7 +17,7 @@ DEFAULT_ENCODING = "UTF-8"
17
17
  DEFAULT_TIMEZONE = "UTC"
18
18
 
19
19
 
20
- class LogLevel(str, Enum):
20
+ class LogLevel(StrEnum):
21
21
  """Log levels"""
22
22
 
23
23
  CRITICAL = "CRITICAL"
@@ -29,7 +29,7 @@ class LogLevel(str, Enum):
29
29
  DEBUG = "DEBUG"
30
30
 
31
31
 
32
- class RotateWhen(str, Enum):
32
+ class RotateWhen(StrEnum):
33
33
  """Rotation timing options for TimedRotatingLog"""
34
34
 
35
35
  MIDNIGHT = "midnight"
@@ -3,37 +3,37 @@ import logging
3
3
  import threading
4
4
  import time
5
5
  from dataclasses import dataclass
6
- from enum import Enum
6
+ from enum import StrEnum
7
7
  from pythonLogs.basic_log import BasicLog as _BasicLogImpl
8
8
  from pythonLogs.core.constants import LogLevel, RotateWhen
9
9
  from pythonLogs.core.log_utils import cleanup_logger_handlers
10
10
  from pythonLogs.core.settings import get_log_settings
11
11
  from pythonLogs.size_rotating import SizeRotatingLog as _SizeRotatingLogImpl
12
12
  from pythonLogs.timed_rotating import TimedRotatingLog as _TimedRotatingLogImpl
13
- from typing import Dict, Optional, Tuple, Union, assert_never
13
+ from typing import assert_never
14
14
 
15
15
 
16
16
  @dataclass
17
17
  class LoggerConfig:
18
18
  """Configuration class to group logger parameters"""
19
19
 
20
- level: Optional[Union[LogLevel, str]] = None
21
- name: Optional[str] = None
22
- directory: Optional[str] = None
23
- filenames: Optional[list | tuple] = None
24
- encoding: Optional[str] = None
25
- datefmt: Optional[str] = None
26
- timezone: Optional[str] = None
27
- streamhandler: Optional[bool] = None
28
- showlocation: Optional[bool] = None
29
- maxmbytes: Optional[int] = None
30
- when: Optional[Union[RotateWhen, str]] = None
31
- sufix: Optional[str] = None
32
- rotateatutc: Optional[bool] = None
33
- daystokeep: Optional[int] = None
34
-
35
-
36
- class LoggerType(str, Enum):
20
+ level: LogLevel | str | None = None
21
+ name: str | None = None
22
+ directory: str | None = None
23
+ filenames: list | tuple | None = None
24
+ encoding: str | None = None
25
+ datefmt: str | None = None
26
+ timezone: str | None = None
27
+ streamhandler: bool | None = None
28
+ showlocation: bool | None = None
29
+ maxmbytes: int | None = None
30
+ when: RotateWhen | str | None = None
31
+ sufix: str | None = None
32
+ rotateatutc: bool | None = None
33
+ daystokeep: int | None = None
34
+
35
+
36
+ class LoggerType(StrEnum):
37
37
  """Available logger types"""
38
38
 
39
39
  BASIC = "basic"
@@ -45,7 +45,7 @@ class LoggerFactory:
45
45
  """Factory for creating different types of loggers with optimized instantiation and memory management"""
46
46
 
47
47
  # Logger registry for reusing loggers by name with timestamp tracking
48
- _logger_registry: Dict[str, Tuple[logging.Logger, float]] = {}
48
+ _logger_registry: dict[str, tuple[logging.Logger, float]] = {}
49
49
  # Thread lock for registry access
50
50
  _registry_lock = threading.RLock()
51
51
  # Memory optimization settings
@@ -71,8 +71,8 @@ class LoggerFactory:
71
71
  @classmethod
72
72
  def get_or_create_logger(
73
73
  cls,
74
- logger_type: Union[LoggerType, str],
75
- name: Optional[str] = None,
74
+ logger_type: LoggerType | str,
75
+ name: str | None = None,
76
76
  **kwargs,
77
77
  ) -> logging.Logger:
78
78
  """
@@ -215,12 +215,10 @@ class LoggerFactory:
215
215
  Dictionary with current max_loggers and ttl_seconds settings
216
216
  """
217
217
  with cls._registry_lock:
218
- return {'max_loggers': cls._max_loggers, 'ttl_seconds': cls._logger_ttl}
218
+ return {"max_loggers": cls._max_loggers, "ttl_seconds": cls._logger_ttl}
219
219
 
220
220
  @staticmethod
221
- def create_logger(
222
- logger_type: Union[LoggerType, str], config: Optional[LoggerConfig] = None, **kwargs
223
- ) -> logging.Logger:
221
+ def create_logger(logger_type: LoggerType | str, config: LoggerConfig | None = None, **kwargs) -> logging.Logger:
224
222
  """
225
223
  Factory method to create loggers based on type.
226
224
 
@@ -239,8 +237,10 @@ class LoggerFactory:
239
237
  if isinstance(logger_type, str):
240
238
  try:
241
239
  logger_type = LoggerType(logger_type.lower())
242
- except ValueError:
243
- raise ValueError(f"Invalid logger type: {logger_type}. Valid types: {[t.value for t in LoggerType]}")
240
+ except ValueError as err:
241
+ raise ValueError(
242
+ f"Invalid logger type: {logger_type}. Valid types: {[t.value for t in LoggerType]}"
243
+ ) from err
244
244
 
245
245
  # Merge config and kwargs (kwargs take precedence for backward compatibility)
246
246
  if config is None:
@@ -248,20 +248,20 @@ class LoggerFactory:
248
248
 
249
249
  # Create a new config with kwargs overriding config values
250
250
  final_config = LoggerConfig(
251
- level=kwargs.get('level', config.level),
252
- name=kwargs.get('name', config.name),
253
- directory=kwargs.get('directory', config.directory),
254
- filenames=kwargs.get('filenames', config.filenames),
255
- encoding=kwargs.get('encoding', config.encoding),
256
- datefmt=kwargs.get('datefmt', config.datefmt),
257
- timezone=kwargs.get('timezone', config.timezone),
258
- streamhandler=kwargs.get('streamhandler', config.streamhandler),
259
- showlocation=kwargs.get('showlocation', config.showlocation),
260
- maxmbytes=kwargs.get('maxmbytes', config.maxmbytes),
261
- when=kwargs.get('when', config.when),
262
- sufix=kwargs.get('sufix', config.sufix),
263
- rotateatutc=kwargs.get('rotateatutc', config.rotateatutc),
264
- daystokeep=kwargs.get('daystokeep', config.daystokeep),
251
+ level=kwargs.get("level", config.level),
252
+ name=kwargs.get("name", config.name),
253
+ directory=kwargs.get("directory", config.directory),
254
+ filenames=kwargs.get("filenames", config.filenames),
255
+ encoding=kwargs.get("encoding", config.encoding),
256
+ datefmt=kwargs.get("datefmt", config.datefmt),
257
+ timezone=kwargs.get("timezone", config.timezone),
258
+ streamhandler=kwargs.get("streamhandler", config.streamhandler),
259
+ showlocation=kwargs.get("showlocation", config.showlocation),
260
+ maxmbytes=kwargs.get("maxmbytes", config.maxmbytes),
261
+ when=kwargs.get("when", config.when),
262
+ sufix=kwargs.get("sufix", config.sufix),
263
+ rotateatutc=kwargs.get("rotateatutc", config.rotateatutc),
264
+ daystokeep=kwargs.get("daystokeep", config.daystokeep),
265
265
  )
266
266
 
267
267
  # Convert enum values to strings for logger classes
@@ -314,12 +314,12 @@ class LoggerFactory:
314
314
 
315
315
  @staticmethod
316
316
  def create_basic_logger(
317
- level: Optional[Union[LogLevel, str]] = None,
318
- name: Optional[str] = None,
319
- encoding: Optional[str] = None,
320
- datefmt: Optional[str] = None,
321
- timezone: Optional[str] = None,
322
- showlocation: Optional[bool] = None,
317
+ level: LogLevel | str | None = None,
318
+ name: str | None = None,
319
+ encoding: str | None = None,
320
+ datefmt: str | None = None,
321
+ timezone: str | None = None,
322
+ showlocation: bool | None = None,
323
323
  ) -> logging.Logger:
324
324
  """Convenience method for creating a basic logger"""
325
325
  return LoggerFactory.create_logger(
@@ -334,17 +334,17 @@ class LoggerFactory:
334
334
 
335
335
  @staticmethod
336
336
  def create_size_rotating_logger(
337
- level: Optional[Union[LogLevel, str]] = None,
338
- name: Optional[str] = None,
339
- directory: Optional[str] = None,
340
- filenames: Optional[list | tuple] = None,
341
- maxmbytes: Optional[int] = None,
342
- daystokeep: Optional[int] = None,
343
- encoding: Optional[str] = None,
344
- datefmt: Optional[str] = None,
345
- timezone: Optional[str] = None,
346
- streamhandler: Optional[bool] = None,
347
- showlocation: Optional[bool] = None,
337
+ level: LogLevel | str | None = None,
338
+ name: str | None = None,
339
+ directory: str | None = None,
340
+ filenames: list | tuple | None = None,
341
+ maxmbytes: int | None = None,
342
+ daystokeep: int | None = None,
343
+ encoding: str | None = None,
344
+ datefmt: str | None = None,
345
+ timezone: str | None = None,
346
+ streamhandler: bool | None = None,
347
+ showlocation: bool | None = None,
348
348
  ) -> logging.Logger:
349
349
  """Convenience method for creating a size rotating logger"""
350
350
  return LoggerFactory.create_logger(
@@ -364,19 +364,19 @@ class LoggerFactory:
364
364
 
365
365
  @staticmethod
366
366
  def create_timed_rotating_logger(
367
- level: Optional[Union[LogLevel, str]] = None,
368
- name: Optional[str] = None,
369
- directory: Optional[str] = None,
370
- filenames: Optional[list | tuple] = None,
371
- when: Optional[Union[RotateWhen, str]] = None,
372
- sufix: Optional[str] = None,
373
- daystokeep: Optional[int] = None,
374
- encoding: Optional[str] = None,
375
- datefmt: Optional[str] = None,
376
- timezone: Optional[str] = None,
377
- streamhandler: Optional[bool] = None,
378
- showlocation: Optional[bool] = None,
379
- rotateatutc: Optional[bool] = None,
367
+ level: LogLevel | str | None = None,
368
+ name: str | None = None,
369
+ directory: str | None = None,
370
+ filenames: list | tuple | None = None,
371
+ when: RotateWhen | str | None = None,
372
+ sufix: str | None = None,
373
+ daystokeep: int | None = None,
374
+ encoding: str | None = None,
375
+ datefmt: str | None = None,
376
+ timezone: str | None = None,
377
+ streamhandler: bool | None = None,
378
+ showlocation: bool | None = None,
379
+ rotateatutc: bool | None = None,
380
380
  ) -> logging.Logger:
381
381
  """Convenience method for creating a timed rotating logger"""
382
382
  return LoggerFactory.create_logger(
@@ -432,12 +432,12 @@ class BasicLog(_LoggerMixin):
432
432
 
433
433
  def __init__(
434
434
  self,
435
- level: Optional[Union[LogLevel, str]] = None,
436
- name: Optional[str] = None,
437
- encoding: Optional[str] = None,
438
- datefmt: Optional[str] = None,
439
- timezone: Optional[str] = None,
440
- showlocation: Optional[bool] = None,
435
+ level: LogLevel | str | None = None,
436
+ name: str | None = None,
437
+ encoding: str | None = None,
438
+ datefmt: str | None = None,
439
+ timezone: str | None = None,
440
+ showlocation: bool | None = None,
441
441
  ):
442
442
  self._logger = LoggerFactory.create_basic_logger(
443
443
  level=level,
@@ -465,17 +465,17 @@ class SizeRotatingLog(_LoggerMixin):
465
465
 
466
466
  def __init__(
467
467
  self,
468
- level: Optional[Union[LogLevel, str]] = None,
469
- name: Optional[str] = None,
470
- directory: Optional[str] = None,
471
- filenames: Optional[list | tuple] = None,
472
- maxmbytes: Optional[int] = None,
473
- daystokeep: Optional[int] = None,
474
- encoding: Optional[str] = None,
475
- datefmt: Optional[str] = None,
476
- timezone: Optional[str] = None,
477
- streamhandler: Optional[bool] = None,
478
- showlocation: Optional[bool] = None,
468
+ level: LogLevel | str | None = None,
469
+ name: str | None = None,
470
+ directory: str | None = None,
471
+ filenames: list | tuple | None = None,
472
+ maxmbytes: int | None = None,
473
+ daystokeep: int | None = None,
474
+ encoding: str | None = None,
475
+ datefmt: str | None = None,
476
+ timezone: str | None = None,
477
+ streamhandler: bool | None = None,
478
+ showlocation: bool | None = None,
479
479
  ):
480
480
  self._logger = LoggerFactory.create_size_rotating_logger(
481
481
  level=level,
@@ -508,19 +508,19 @@ class TimedRotatingLog(_LoggerMixin):
508
508
 
509
509
  def __init__(
510
510
  self,
511
- level: Optional[Union[LogLevel, str]] = None,
512
- name: Optional[str] = None,
513
- directory: Optional[str] = None,
514
- filenames: Optional[list | tuple] = None,
515
- when: Optional[Union[RotateWhen, str]] = None,
516
- sufix: Optional[str] = None,
517
- daystokeep: Optional[int] = None,
518
- encoding: Optional[str] = None,
519
- datefmt: Optional[str] = None,
520
- timezone: Optional[str] = None,
521
- streamhandler: Optional[bool] = None,
522
- showlocation: Optional[bool] = None,
523
- rotateatutc: Optional[bool] = None,
511
+ level: LogLevel | str | None = None,
512
+ name: str | None = None,
513
+ directory: str | None = None,
514
+ filenames: list | tuple | None = None,
515
+ when: RotateWhen | str | None = None,
516
+ sufix: str | None = None,
517
+ daystokeep: int | None = None,
518
+ encoding: str | None = None,
519
+ datefmt: str | None = None,
520
+ timezone: str | None = None,
521
+ streamhandler: bool | None = None,
522
+ showlocation: bool | None = None,
523
+ rotateatutc: bool | None = None,
524
524
  ):
525
525
  self._logger = LoggerFactory.create_timed_rotating_logger(
526
526
  level=level,
@@ -7,12 +7,11 @@ import shutil
7
7
  import sys
8
8
  import threading
9
9
  import time
10
- from datetime import datetime, timedelta
11
- from datetime import timezone as dttz
10
+ from collections.abc import Callable
11
+ from datetime import UTC, datetime, timedelta
12
12
  from functools import lru_cache
13
13
  from pathlib import Path
14
14
  from pythonLogs.core.constants import DEFAULT_FILE_MODE, LEVEL_MAP
15
- from typing import Callable, Optional, Set
16
15
  from zoneinfo import ZoneInfo
17
16
 
18
17
 
@@ -21,15 +20,17 @@ class RotatingLogMixin:
21
20
 
22
21
  logger: logging.Logger | None
23
22
 
23
+ def init(self) -> None: ...
24
+
24
25
  def __enter__(self):
25
26
  """Context manager entry."""
26
- if not hasattr(self, 'logger') or self.logger is None:
27
+ if not hasattr(self, "logger") or self.logger is None:
27
28
  self.init()
28
29
  return self.logger
29
30
 
30
31
  def __exit__(self, exc_type, exc_val, exc_tb):
31
32
  """Context manager exit with automatic cleanup."""
32
- if hasattr(self, 'logger'):
33
+ if hasattr(self, "logger"):
33
34
  cleanup_logger_handlers(self.logger)
34
35
 
35
36
  @staticmethod
@@ -39,7 +40,7 @@ class RotatingLogMixin:
39
40
 
40
41
 
41
42
  # Global cache for checked directories with thread safety and size limits
42
- _checked_directories: Set[str] = set()
43
+ _checked_directories: set[str] = set()
43
44
  _directory_lock = threading.Lock()
44
45
  _max_cached_directories = 500 # Limit cache size to prevent unbounded growth
45
46
 
@@ -109,7 +110,7 @@ def check_directory_permissions(directory_path: str) -> None:
109
110
  except PermissionError as e:
110
111
  err_msg = f"Unable to create directory | {directory_path}"
111
112
  write_stderr(f"{err_msg} | {type(e).__name__}: {e}")
112
- raise PermissionError(err_msg)
113
+ raise PermissionError(err_msg) from e
113
114
 
114
115
  # Add to cache with size limit enforcement
115
116
  if len(_checked_directories) >= _max_cached_directories:
@@ -129,7 +130,7 @@ def remove_old_logs(logs_dir: str, days_to_keep: int) -> None:
129
130
  try:
130
131
  if file_path.stat().st_mtime < cutoff_time.timestamp():
131
132
  file_path.unlink()
132
- except (OSError, IOError) as e:
133
+ except OSError as e:
133
134
  write_stderr(f"Unable to delete old log | {file_path} | {type(e).__name__}: {e}")
134
135
  except OSError as e:
135
136
  write_stderr(f"Unable to scan directory for old logs | {logs_dir} | {type(e).__name__}: {e}")
@@ -196,7 +197,7 @@ def write_stderr(msg: str) -> None:
196
197
  # Use local timezone
197
198
  dt = datetime.now()
198
199
  else:
199
- dt = datetime.now(dttz.utc).astimezone(tz)
200
+ dt = datetime.now(UTC).astimezone(tz)
200
201
  dt_timezone = dt.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
201
202
  sys.stderr.write(f"[{dt_timezone}]:[ERROR]:{msg}\n")
202
203
  except (OSError, ValueError, KeyError):
@@ -286,7 +287,7 @@ def gzip_file_with_sufix(file_path: str, sufix: str) -> str | None:
286
287
  # Final attempt failed or not Windows - treat as regular error
287
288
  write_stderr(f"Unable to gzip log file | {file_path} | {type(e).__name__}: {e}")
288
289
  raise e
289
- except (OSError, IOError) as e:
290
+ except OSError as e:
290
291
  write_stderr(f"Unable to gzip log file | {file_path} | {type(e).__name__}: {e}")
291
292
  raise e
292
293
 
@@ -324,7 +325,7 @@ def get_timezone_function(time_zone: str) -> Callable:
324
325
 
325
326
 
326
327
  # Shared handler cleanup utility
327
- def cleanup_logger_handlers(logger: Optional[logging.Logger]) -> None:
328
+ def cleanup_logger_handlers(logger: logging.Logger | None) -> None:
328
329
  """Clean up logger resources by closing all handlers.
329
330
 
330
331
  This is a centralized utility to ensure consistent cleanup behavior
@@ -2,16 +2,17 @@ import logging
2
2
  import threading
3
3
  import weakref
4
4
  from . import log_utils
5
+ from .settings import get_log_settings
5
6
  from functools import lru_cache
6
- from typing import Any, Dict, Optional, Set
7
+ from typing import Any
7
8
 
8
9
  # Formatter cache to reduce memory usage for identical formatters
9
- _formatter_cache: Dict[str, logging.Formatter] = {}
10
+ _formatter_cache: dict[str, logging.Formatter] = {}
10
11
  _formatter_cache_lock = threading.Lock()
11
- _max_formatters = 50 # Limit formatter cache size
12
+ _max_formatters = get_log_settings().max_formatters
12
13
 
13
14
 
14
- def get_cached_formatter(format_string: str, datefmt: Optional[str] = None) -> logging.Formatter:
15
+ def get_cached_formatter(format_string: str, datefmt: str | None = None) -> logging.Formatter:
15
16
  """Get a cached formatter or create and cache a new one.
16
17
 
17
18
  This reduces memory usage by reusing formatter instances with
@@ -66,7 +67,7 @@ def clear_directory_cache() -> None:
66
67
 
67
68
 
68
69
  # Weak reference registry for tracking active loggers without preventing GC
69
- _active_loggers: Set[weakref.ReferenceType] = set()
70
+ _active_loggers: set[weakref.ReferenceType] = set()
70
71
  _weak_ref_lock = threading.Lock()
71
72
 
72
73
 
@@ -103,7 +104,7 @@ def get_active_logger_count() -> int:
103
104
  return len(_active_loggers)
104
105
 
105
106
 
106
- def get_memory_stats() -> Dict[str, Any]:
107
+ def get_memory_stats() -> dict[str, Any]:
107
108
  """Get memory usage statistics for the logging system.
108
109
 
109
110
  Returns:
@@ -125,13 +126,13 @@ def get_memory_stats() -> Dict[str, Any]:
125
126
  directory_stats = log_utils.get_directory_cache_stats()
126
127
 
127
128
  return {
128
- 'registry_size': registry_size,
129
- 'formatter_cache_size': formatter_cache_size,
130
- 'directory_cache_size': directory_stats['cached_directories'],
131
- 'active_logger_count': get_active_logger_count(),
132
- 'max_registry_size': factory_limits['max_loggers'],
133
- 'max_formatter_cache': _max_formatters,
134
- 'max_directory_cache': directory_stats['max_directories'],
129
+ "registry_size": registry_size,
130
+ "formatter_cache_size": formatter_cache_size,
131
+ "directory_cache_size": directory_stats["cached_directories"],
132
+ "active_logger_count": get_active_logger_count(),
133
+ "max_registry_size": factory_limits["max_loggers"],
134
+ "max_formatter_cache": _max_formatters,
135
+ "max_directory_cache": directory_stats["max_directories"],
135
136
  }
136
137
 
137
138
 
@@ -153,7 +154,7 @@ def optimize_lru_cache_sizes() -> None:
153
154
  log_utils.get_stderr_timezone = lru_cache(maxsize=4)(log_utils.get_stderr_timezone.__wrapped__)
154
155
 
155
156
 
156
- def force_garbage_collection() -> Dict[str, int]:
157
+ def force_garbage_collection() -> dict[str, int]:
157
158
  """Force garbage collection and return collection statistics.
158
159
 
159
160
  This can be useful for testing memory leaks or forcing cleanup
@@ -172,7 +173,7 @@ def force_garbage_collection() -> Dict[str, int]:
172
173
  collected = gc.collect()
173
174
 
174
175
  return {
175
- 'objects_collected': collected,
176
- 'garbage_count': len(gc.garbage),
177
- 'reference_cycles': gc.get_count(),
176
+ "objects_collected": collected,
177
+ "garbage_count": len(gc.garbage),
178
+ "reference_cycles": gc.get_count(),
178
179
  }
@@ -75,6 +75,10 @@ class LogSettings(BaseSettings):
75
75
  default=100,
76
76
  description="Maximum number of loggers to track in memory",
77
77
  )
78
+ max_formatters: int = Field(
79
+ default=50,
80
+ description="Maximum number of formatters to cache in memory",
81
+ )
78
82
  logger_ttl_seconds: int = Field(
79
83
  default=3600,
80
84
  description="Time-to-live in seconds for logger references",
@@ -1,23 +1,24 @@
1
1
  import functools
2
2
  import threading
3
- from typing import Any, Callable, Dict, Type, TypeVar
3
+ from collections.abc import Callable
4
+ from typing import Any, TypeVar
4
5
 
5
- F = TypeVar('F', bound=Callable[..., Any])
6
+ F = TypeVar("F", bound=Callable[..., Any])
6
7
 
7
8
 
8
9
  class ThreadSafeMeta(type):
9
10
  """Metaclass that automatically adds thread safety to class methods."""
10
11
 
11
- def __new__(mcs, name: str, bases: tuple, namespace: Dict[str, Any], **kwargs):
12
+ def __new__(mcs, name: str, bases: tuple, namespace: dict[str, Any], **kwargs):
12
13
  # Create the class first
13
14
  cls = super().__new__(mcs, name, bases, namespace)
14
15
 
15
16
  # Add a class-level lock if not already present
16
- if not hasattr(cls, '_lock'):
17
+ if not hasattr(cls, "_lock"):
17
18
  cls._lock = threading.RLock()
18
19
 
19
20
  # Get methods that should be thread-safe (exclude private/dunder methods)
20
- thread_safe_methods = getattr(cls, '_thread_safe_methods', None)
21
+ thread_safe_methods = getattr(cls, "_thread_safe_methods", None)
21
22
  if thread_safe_methods is None:
22
23
  # Auto-detect public methods that modify state
23
24
  thread_safe_methods = [
@@ -25,8 +26,8 @@ class ThreadSafeMeta(type):
25
26
  for method_name in namespace
26
27
  if (
27
28
  callable(getattr(cls, method_name, None))
28
- and not method_name.startswith('_')
29
- and method_name not in ['__enter__', '__exit__', '__init__']
29
+ and not method_name.startswith("_")
30
+ and method_name not in ["__enter__", "__exit__", "__init__"]
30
31
  )
31
32
  ]
32
33
 
@@ -47,12 +48,12 @@ def thread_safe(func: F) -> F:
47
48
  @functools.wraps(func)
48
49
  def wrapper(self, *args, **kwargs):
49
50
  # Use instance lock if available, otherwise class lock
50
- lock = getattr(self, '_lock', None)
51
+ lock = getattr(self, "_lock", None)
51
52
  if lock is None:
52
53
  # Check if class has lock, if not create one
53
- if not hasattr(self.__class__, '_lock'):
54
- setattr(self.__class__, '_lock', threading.RLock())
55
- lock = getattr(self.__class__, '_lock')
54
+ if not hasattr(self.__class__, "_lock"):
55
+ self.__class__._lock = threading.RLock()
56
+ lock = self.__class__._lock
56
57
 
57
58
  with lock:
58
59
  return func(self, *args, **kwargs)
@@ -60,36 +61,36 @@ def thread_safe(func: F) -> F:
60
61
  return wrapper
61
62
 
62
63
 
63
- def _get_wrappable_methods(cls: Type) -> list:
64
+ def _get_wrappable_methods(cls: type) -> list:
64
65
  """Helper function to get methods that should be made thread-safe."""
65
66
  return [
66
67
  method_name
67
68
  for method_name in dir(cls)
68
69
  if (
69
70
  callable(getattr(cls, method_name, None))
70
- and not method_name.startswith('_')
71
- and method_name not in ['__enter__', '__exit__', '__init__']
71
+ and not method_name.startswith("_")
72
+ and method_name not in ["__enter__", "__exit__", "__init__"]
72
73
  )
73
74
  ]
74
75
 
75
76
 
76
- def _ensure_class_has_lock(cls: Type) -> None:
77
+ def _ensure_class_has_lock(cls: type) -> None:
77
78
  """Ensure the class has a lock attribute."""
78
- if not hasattr(cls, '_lock'):
79
+ if not hasattr(cls, "_lock"):
79
80
  cls._lock = threading.RLock()
80
81
 
81
82
 
82
- def _should_wrap_method(cls: Type, method_name: str, original_method: Any) -> bool:
83
+ def _should_wrap_method(cls: type, method_name: str, original_method: Any) -> bool:
83
84
  """Check if a method should be wrapped with thread safety."""
84
85
  return (
85
- hasattr(cls, method_name) and callable(original_method) and not hasattr(original_method, '_thread_safe_wrapped')
86
+ hasattr(cls, method_name) and callable(original_method) and not hasattr(original_method, "_thread_safe_wrapped")
86
87
  )
87
88
 
88
89
 
89
90
  def auto_thread_safe(thread_safe_methods: list = None):
90
91
  """Class decorator that adds automatic thread safety to specified methods."""
91
92
 
92
- def decorator(cls: Type) -> Type:
93
+ def decorator(cls: type) -> type:
93
94
  _ensure_class_has_lock(cls)
94
95
 
95
96
  # Store thread-safe methods list
@@ -116,21 +117,21 @@ class AutoThreadSafe:
116
117
  """Base class that provides automatic thread safety for all public methods."""
117
118
 
118
119
  def __init__(self):
119
- if not hasattr(self, '_lock'):
120
+ if not hasattr(self, "_lock"):
120
121
  self._lock = threading.RLock()
121
122
 
122
123
  def __init_subclass__(cls, **kwargs):
123
124
  super().__init_subclass__(**kwargs)
124
125
 
125
126
  # Add class-level lock
126
- if not hasattr(cls, '_lock'):
127
+ if not hasattr(cls, "_lock"):
127
128
  cls._lock = threading.RLock()
128
129
 
129
130
  # Auto-wrap public methods
130
131
  for attr_name in dir(cls):
131
- if not attr_name.startswith('_'):
132
+ if not attr_name.startswith("_"):
132
133
  attr = getattr(cls, attr_name)
133
- if callable(attr) and not hasattr(attr, '_thread_safe_wrapped'):
134
+ if callable(attr) and not hasattr(attr, "_thread_safe_wrapped"):
134
135
  wrapped_attr = thread_safe(attr)
135
136
  wrapped_attr._thread_safe_wrapped = True
136
137
  setattr(cls, attr_name, wrapped_attr)
@@ -20,7 +20,7 @@ from pythonLogs.core.settings import get_log_settings
20
20
  from pythonLogs.core.thread_safety import auto_thread_safe
21
21
 
22
22
 
23
- @auto_thread_safe(['init'])
23
+ @auto_thread_safe(["init"])
24
24
  class SizeRotatingLog(RotatingLogMixin):
25
25
  """Size-based rotating logger with context manager support for automatic resource cleanup."""
26
26
 
@@ -16,7 +16,7 @@ from pythonLogs.core.settings import get_log_settings
16
16
  from pythonLogs.core.thread_safety import auto_thread_safe
17
17
 
18
18
 
19
- @auto_thread_safe(['init'])
19
+ @auto_thread_safe(["init"])
20
20
  class TimedRotatingLog(RotatingLogMixin):
21
21
  """
22
22
  Time-based rotating logger with context manager support for automatic resource cleanup.
File without changes
File without changes