pythonLogs 6.0.0__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.0
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-Dist: pydantic-settings>=2.12.0
25
- Provides-Extra: test
26
- Requires-Dist: poethepoet>=0.40.0; extra == 'test'
27
- Requires-Dist: psutil>=7.2.1; extra == 'test'
28
- Requires-Dist: pytest-cov>=7.0.0; extra == 'test'
29
- Requires-Dist: pytest>=9.0.2; extra == 'test'
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: pydantic-settings>=2.11.0
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>
@@ -69,9 +65,12 @@ Description-Content-Type: text/markdown
69
65
  - [Context Manager Support](#context-manager-support)
70
66
  - [Using With Multiple Log Levels and Files](#using-with-multiple-log-levels-and-files)
71
67
  - [Environment Variables](#env-variables-optional)
68
+ - [Settings Cache Management](#settings-cache-management)
72
69
  - [Flexible Configuration Options](#flexible-configuration-options)
73
70
  - [Development](#development)
74
- - [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)
75
74
  - [Optionals](#optionals)
76
75
  - [License](#license)
77
76
  - [Support](#support)
@@ -316,6 +315,25 @@ LOG_ROTATE_AT_UTC=True
316
315
  LOG_ROTATE_FILE_SUFIX="%Y%m%d"
317
316
  ```
318
317
 
318
+ ## Settings Cache Management
319
+
320
+ Use `get_log_settings()` to inspect current configuration and `clear_settings_cache()` to reload configuration from environment variables:
321
+
322
+ ```python
323
+ from pythonLogs import get_log_settings, clear_settings_cache
324
+
325
+ # Inspect current settings
326
+ settings = get_log_settings()
327
+ print(settings.level) # Current log level
328
+ print(settings.timezone) # Current timezone
329
+
330
+ # Clear cache and reload .env on next access (default)
331
+ clear_settings_cache()
332
+
333
+ # Clear cache but keep current .env values
334
+ clear_settings_cache(reload_env=False)
335
+ ```
336
+
319
337
 
320
338
 
321
339
 
@@ -360,20 +378,34 @@ RotateWhen.MONDAY # "W0"
360
378
 
361
379
  # Development
362
380
 
363
- Must have [UV](https://uv.run/docs/getting-started/installation),
364
- [Black](https://black.readthedocs.io/en/stable/getting_started.html),
365
- [Ruff](https://docs.astral.sh/ruff/installation/), and
366
- [Poe the Poet](https://poethepoet.naber.dev/installation) installed.
381
+ Must have [UV](https://uv.run/docs/getting-started/installation) installed.
367
382
 
368
- ## 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
369
386
 
370
387
  ```shell
371
- uv sync --all-extras
372
- poe linter
388
+ uv sync --all-extras --all-groups
373
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
374
405
  poe build
375
406
  ```
376
407
 
408
+
377
409
  ## Optionals
378
410
 
379
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>
@@ -38,9 +38,12 @@
38
38
  - [Context Manager Support](#context-manager-support)
39
39
  - [Using With Multiple Log Levels and Files](#using-with-multiple-log-levels-and-files)
40
40
  - [Environment Variables](#env-variables-optional)
41
+ - [Settings Cache Management](#settings-cache-management)
41
42
  - [Flexible Configuration Options](#flexible-configuration-options)
42
43
  - [Development](#development)
43
- - [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)
44
47
  - [Optionals](#optionals)
45
48
  - [License](#license)
46
49
  - [Support](#support)
@@ -285,6 +288,25 @@ LOG_ROTATE_AT_UTC=True
285
288
  LOG_ROTATE_FILE_SUFIX="%Y%m%d"
286
289
  ```
287
290
 
291
+ ## Settings Cache Management
292
+
293
+ Use `get_log_settings()` to inspect current configuration and `clear_settings_cache()` to reload configuration from environment variables:
294
+
295
+ ```python
296
+ from pythonLogs import get_log_settings, clear_settings_cache
297
+
298
+ # Inspect current settings
299
+ settings = get_log_settings()
300
+ print(settings.level) # Current log level
301
+ print(settings.timezone) # Current timezone
302
+
303
+ # Clear cache and reload .env on next access (default)
304
+ clear_settings_cache()
305
+
306
+ # Clear cache but keep current .env values
307
+ clear_settings_cache(reload_env=False)
308
+ ```
309
+
288
310
 
289
311
 
290
312
 
@@ -329,20 +351,34 @@ RotateWhen.MONDAY # "W0"
329
351
 
330
352
  # Development
331
353
 
332
- Must have [UV](https://uv.run/docs/getting-started/installation),
333
- [Black](https://black.readthedocs.io/en/stable/getting_started.html),
334
- [Ruff](https://docs.astral.sh/ruff/installation/), and
335
- [Poe the Poet](https://poethepoet.naber.dev/installation) installed.
354
+ Must have [UV](https://uv.run/docs/getting-started/installation) installed.
355
+
356
+ ## Create DEV Environment and Running Tests
336
357
 
337
- ## Create DEV Environment, Running Tests and Building Wheel
358
+ > **Note:** All poe tasks automatically run ruff linter along with Black formatting
338
359
 
339
360
  ```shell
340
- uv sync --all-extras
341
- poe linter
361
+ uv sync --all-extras --all-groups
342
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
343
378
  poe build
344
379
  ```
345
380
 
381
+
346
382
  ## Optionals
347
383
 
348
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.0"
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
- "pydantic-settings>=2.12.0",
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",
43
- "psutil>=7.2.1",
44
- "pytest>=9.0.2",
52
+ [dependency-groups]
53
+ dev = [
54
+ "psutil>=7.2.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
- linter.shell = "uv run ruff check --fix --select I . && uv run black ."
58
- profile = "uv run python -m cProfile -o cprofile.prof -m pytest"
59
- test = "uv run pytest"
62
+ linter.shell = "uv run ruff check --fix . && uv run black ."
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,11 +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
- select = ["I"]
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"]
101
113
 
102
114
  [tool.ruff.lint.isort]
103
115
  known-first-party = ["pythonLogs"]
104
- force-sort-within-sections = true
116
+ force-sort-within-sections = false
117
+ from-first = false
105
118
  no-sections = true
@@ -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)
@@ -0,0 +1,24 @@
1
+ import logging
2
+ from importlib.metadata import version
3
+ from pythonLogs.core.constants import LogLevel, RotateWhen
4
+ from pythonLogs.core.factory import BasicLog, SizeRotatingLog, TimedRotatingLog
5
+ from pythonLogs.core.settings import clear_settings_cache, get_log_settings
6
+
7
+ __all__ = (
8
+ "BasicLog",
9
+ "SizeRotatingLog",
10
+ "TimedRotatingLog",
11
+ "LogLevel",
12
+ "RotateWhen",
13
+ "clear_settings_cache",
14
+ "get_log_settings",
15
+ )
16
+
17
+ __title__ = "pythonLogs"
18
+ __author__ = "Daniel Costa"
19
+ __email__ = "ddcsoftwares@proton.me"
20
+ __license__ = "MIT"
21
+ __copyright__ = "Copyright 2024-present DDC Softwares"
22
+ __version__ = version(__title__)
23
+
24
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -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
- from enum import Enum
2
1
  import logging
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"
@@ -1,39 +1,39 @@
1
1
  import atexit
2
- from dataclasses import dataclass
3
- from enum import Enum
4
2
  import logging
3
+ import threading
4
+ import time
5
+ from dataclasses import dataclass
6
+ from enum import StrEnum
5
7
  from pythonLogs.basic_log import BasicLog as _BasicLogImpl
6
8
  from pythonLogs.core.constants import LogLevel, RotateWhen
7
9
  from pythonLogs.core.log_utils import cleanup_logger_handlers
8
10
  from pythonLogs.core.settings import get_log_settings
9
11
  from pythonLogs.size_rotating import SizeRotatingLog as _SizeRotatingLogImpl
10
12
  from pythonLogs.timed_rotating import TimedRotatingLog as _TimedRotatingLogImpl
11
- import threading
12
- import time
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,
@@ -1,18 +1,17 @@
1
- from datetime import datetime, timedelta
2
- from datetime import timezone as dttz
3
1
  import errno
4
- from functools import lru_cache
5
2
  import gzip
6
3
  import logging
7
4
  import logging.handlers
8
5
  import os
9
- from pathlib import Path
10
- from pythonLogs.core.constants import DEFAULT_FILE_MODE, LEVEL_MAP
11
6
  import shutil
12
7
  import sys
13
8
  import threading
14
9
  import time
15
- from typing import Callable, Optional, Set
10
+ from collections.abc import Callable
11
+ from datetime import UTC, datetime, timedelta
12
+ from functools import lru_cache
13
+ from pathlib import Path
14
+ from pythonLogs.core.constants import DEFAULT_FILE_MODE, LEVEL_MAP
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
@@ -1,17 +1,18 @@
1
- from . import log_utils
2
- from functools import lru_cache
3
1
  import logging
4
2
  import threading
5
- from typing import Any, Dict, Optional, Set
6
3
  import weakref
4
+ from . import log_utils
5
+ from .settings import get_log_settings
6
+ from functools import lru_cache
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
  }
@@ -16,6 +16,14 @@ from pythonLogs.core.constants import (
16
16
  _dotenv_loaded = False
17
17
 
18
18
 
19
+ def _ensure_dotenv_loaded() -> None:
20
+ """Ensure dotenv is loaded only once."""
21
+ global _dotenv_loaded
22
+ if not _dotenv_loaded:
23
+ load_dotenv()
24
+ _dotenv_loaded = True
25
+
26
+
19
27
  class LogSettings(BaseSettings):
20
28
  """If any ENV variable is omitted, it falls back to default values here"""
21
29
 
@@ -67,6 +75,10 @@ class LogSettings(BaseSettings):
67
75
  default=100,
68
76
  description="Maximum number of loggers to track in memory",
69
77
  )
78
+ max_formatters: int = Field(
79
+ default=50,
80
+ description="Maximum number of formatters to cache in memory",
81
+ )
70
82
  logger_ttl_seconds: int = Field(
71
83
  default=3600,
72
84
  description="Time-to-live in seconds for logger references",
@@ -95,9 +107,18 @@ class LogSettings(BaseSettings):
95
107
 
96
108
  @lru_cache(maxsize=1)
97
109
  def get_log_settings() -> LogSettings:
98
- """Get cached log settings instance to avoid repeated instantiation"""
99
- global _dotenv_loaded
100
- if not _dotenv_loaded:
101
- load_dotenv()
102
- _dotenv_loaded = True
110
+ """Get cached log settings instance to avoid repeated instantiation."""
111
+ _ensure_dotenv_loaded()
103
112
  return LogSettings()
113
+
114
+
115
+ def clear_settings_cache(reload_env: bool = True) -> None:
116
+ """Clear log settings cache. Next call to get_log_settings() will create fresh instance.
117
+
118
+ Args:
119
+ reload_env: If True, also reset dotenv loaded flag to reload .env on next access
120
+ """
121
+ global _dotenv_loaded
122
+ get_log_settings.cache_clear()
123
+ if reload_env:
124
+ _dotenv_loaded = False
@@ -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)
@@ -1,5 +1,6 @@
1
1
  import logging.handlers
2
2
  import os
3
+ import re
3
4
  from pathlib import Path
4
5
  from pythonLogs.core.constants import MB_TO_BYTES
5
6
  from pythonLogs.core.log_utils import (
@@ -17,10 +18,9 @@ from pythonLogs.core.log_utils import (
17
18
  from pythonLogs.core.memory_utils import register_logger_weakref
18
19
  from pythonLogs.core.settings import get_log_settings
19
20
  from pythonLogs.core.thread_safety import auto_thread_safe
20
- import re
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.
@@ -1,63 +0,0 @@
1
- from importlib.metadata import version
2
- import logging
3
- from pythonLogs.core.constants import LogLevel, RotateWhen
4
- from pythonLogs.core.factory import BasicLog, SizeRotatingLog, TimedRotatingLog
5
- from typing import Literal, NamedTuple
6
-
7
- __all__ = (
8
- "BasicLog",
9
- "SizeRotatingLog",
10
- "TimedRotatingLog",
11
- "LogLevel",
12
- "RotateWhen",
13
- )
14
-
15
- __title__ = "pythonLogs"
16
- __author__ = "Daniel Costa"
17
- __email__ = "danieldcsta@gmail.com>"
18
- __license__ = "MIT"
19
- __copyright__ = "Copyright 2024-present DDC Softwares"
20
- _req_python_version = (3, 12, 0)
21
-
22
-
23
- try:
24
- _version = tuple(int(x) for x in version(__title__).split("."))
25
- except ModuleNotFoundError:
26
- _version = (0, 0, 0)
27
-
28
-
29
- class VersionInfo(NamedTuple):
30
- major: int
31
- minor: int
32
- micro: int
33
- releaselevel: Literal["alpha", "beta", "candidate", "final"]
34
- serial: int
35
-
36
-
37
- __version__ = _version
38
- __version_info__: VersionInfo = VersionInfo(
39
- major=__version__[0],
40
- minor=__version__[1],
41
- micro=__version__[2],
42
- releaselevel="final",
43
- serial=0,
44
- )
45
- __req_python_version__: VersionInfo = VersionInfo(
46
- major=_req_python_version[0],
47
- minor=_req_python_version[1],
48
- micro=_req_python_version[2],
49
- releaselevel="final",
50
- serial=0,
51
- )
52
-
53
- logging.getLogger(__name__).addHandler(logging.NullHandler())
54
-
55
- del (
56
- logging,
57
- NamedTuple,
58
- Literal,
59
- VersionInfo,
60
- version,
61
- _version,
62
- _req_python_version,
63
- )
File without changes
File without changes