diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE
index 0251d90..e1ae10c 100755
--- a/.github/PULL_REQUEST_TEMPLATE
+++ b/.github/PULL_REQUEST_TEMPLATE
@@ -3,14 +3,14 @@
## Checklist
-- [ ] If code changes were made, then they have been tested.
- - [ ] I have updated the documentation to reflect the changes.
-- [ ] I have thought about how this code may affect other services.
-- [ ] This PR fixes an issue.
-- [ ] This PR add/remove/change unit tests.
-- [ ] This PR adds something new (e.g. new method or parameters).
-- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed)
-- [ ] This PR is **not** a code change (e.g. documentation, README, ...)
+- [ ] If code changes were made, then they have been tested
+- [ ] I have updated the documentation to reflect any changes made
+- [ ] I have thought about how this code may affect other services
+- [ ] This PR fixes an issue
+- [ ] This PR is a breaking change (e.g. method, parameters, env variables)
+- [ ] This PR adds something new (e.g. method, parameters, env variables)
+- [ ] This PR change unit and integration tests
+- [ ] This PR is **NOT** a code change (e.g. documentation, packages)
## Reviewer
-- [ ] I understand that approving this code, I am also responsible for it going into the codebase.
+- [ ] I understand that approving this code, I am also responsible for it going into the codebase
diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
index 80ff883..cf1c821 100755
--- a/.github/workflows/workflow.yml
+++ b/.github/workflows/workflow.yml
@@ -1,23 +1,32 @@
name: CI/CD Pipeline
'on':
+ pull_request:
push:
- branches: ['**']
tags: ['v*']
+env:
+ LATEST_PYTHON_VERSION: '3.14'
jobs:
+ lint:
+ name: Lint (ruff)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - uses: astral-sh/ruff-action@v3
+ with:
+ args: "check"
+
test:
name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
- runs-on: ${{ matrix.runs-on || matrix.os }}
+ runs-on: ${{ matrix.os }}
+ needs: lint
strategy:
fail-fast: false
matrix:
- os: ['ubuntu-latest', 'macos-latest', 'macos-14-arm64', 'windows-latest']
- python-version: ['3.12', '3.13', '3.14']
- include:
- - os: 'macos-14-arm64'
- runs-on: 'macos-14'
+ os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
+ python-version: ['3.11', '3.14']
defaults:
run:
working-directory: ${{ github.workspace }}
@@ -26,12 +35,14 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v7
+ with:
+ enable-cache: true
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
- run: uv sync --all-extras
+ run: uv sync --locked --all-extras --dev
shell: bash
- name: Run tests with coverage
@@ -39,37 +50,44 @@ jobs:
with:
timeout_minutes: 2
max_attempts: 3
- command: uv run pytest
+ command: uv run --no-sync pytest
shell: bash
- name: Upload coverage to Codecov
- if: matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest'
+ if: matrix.python-version == env.LATEST_PYTHON_VERSION && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v5
- name: Upload test results to Codecov
- if: matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest'
+ if: matrix.python-version == env.LATEST_PYTHON_VERSION && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v5
with:
report_type: test_results
-
build:
- name: Build package
+ name: Build Package
runs-on: ubuntu-latest
- needs: [test]
+ needs: test
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
+ with:
+ enable-cache: true
- - name: Set up Python 3.14
- run: uv python install 3.14
+ - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }}
+ run: uv python install ${{ env.LATEST_PYTHON_VERSION }}
- name: Build package
run: uv build
+ - name: Smoke test (wheel)
+ run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
+
+ - name: Smoke test (sdist)
+ run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
+
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
@@ -77,11 +95,10 @@ jobs:
path: dist/
retention-days: 7
-
release:
name: Create Release
runs-on: ubuntu-latest
- needs: [build]
+ needs: build
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..4b84c55
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,5 @@
+repos:
+ - repo: https://github.com/astral-sh/uv-pre-commit
+ rev: 0.10.0
+ hooks:
+ - id: uv-lock
diff --git a/README.md b/README.md
index 24b8692..72d4f8d 100755
--- a/README.md
+++ b/README.md
@@ -8,9 +8,9 @@
+
-
@@ -41,7 +41,9 @@
- [Settings Cache Management](#settings-cache-management)
- [Flexible Configuration Options](#flexible-configuration-options)
- [Development](#development)
- - [Create DEV Environment, Running Tests and Building Wheel](#create-dev-environment-running-tests-and-building-wheel)
+ - [Create DEV Environment and Running Tests](#create-dev-environment-and-running-tests)
+ - [Update DEV Environment Packages](#update-dev-environment-packages)
+ - [Building Wheel](#building-wheel)
- [Optionals](#optionals)
- [License](#license)
- [Support](#support)
@@ -349,20 +351,34 @@ RotateWhen.MONDAY # "W0"
# Development
-Must have [UV](https://uv.run/docs/getting-started/installation),
-[Black](https://black.readthedocs.io/en/stable/getting_started.html),
-[Ruff](https://docs.astral.sh/ruff/installation/), and
-[Poe the Poet](https://poethepoet.naber.dev/installation) installed.
+Must have [UV](https://uv.run/docs/getting-started/installation) installed.
-## Create DEV Environment, Running Tests and Building Wheel
+## Create DEV Environment and Running Tests
+
+> **Note:** All poe tasks automatically run ruff linter along with Black formatting
```shell
-uv sync --all-extras
-poe linter
+uv sync --all-extras --all-groups
poe test
+```
+
+
+## Update DEV Environment Packages
+This will update all packages dependencies
+
+```shell
+poe updatedev
+```
+
+
+## Building Wheel
+This will update all packages, run linter, both unit and integration tests and finally build the wheel
+
+```shell
poe build
```
+
## Optionals
### Create a cprofile.prof file from unit tests
diff --git a/pyproject.toml b/pyproject.toml
index 5a24dca..c1d91a0 100755
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,16 +2,31 @@
requires = ["hatchling"]
build-backend = "hatchling.build"
+[tool.hatch.metadata]
+allow-direct-references = true
+
+[tool.hatch.build]
+include = ["pythonLogs/**/*"]
+
+[tool.hatch.build.targets.wheel]
+packages = ["pythonLogs"]
+
[project]
name = "pythonLogs"
-version = "6.0.1"
+version = "6.0.2"
description = "High-performance Python logging library with file rotation and optimized caching for better performance"
+urls.Repository = "https://github.com/ddc/pythonLogs"
+urls.Homepage = "https://pypi.org/project/pythonLogs"
license = {text = "MIT"}
readme = "README.md"
-authors = [{name = "Daniel Costa", email = "danieldcsta@gmail.com"}]
-maintainers = [{name = "Daniel Costa"}]
+authors = [
+ {name = "Daniel Costa", email = "danieldcsta@gmail.com"},
+]
+maintainers = [
+ {name = "Daniel Costa"},
+]
keywords = [
- "python3", "python-3", "python",
+ "python", "python3", "python-3",
"log", "logging", "logger",
"logutils", "log-utils", "pythonLogs"
]
@@ -20,6 +35,7 @@ classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
@@ -28,52 +44,44 @@ classifiers = [
"Intended Audience :: Developers",
"Natural Language :: English",
]
-requires-python = ">=3.12"
+requires-python = ">=3.11"
dependencies = [
"pydantic-settings>=2.11.0",
]
-[project.urls]
-Homepage = "https://pypi.org/project/pythonLogs"
-Repository = "https://github.com/ddc/pythonLogs"
-
-[project.optional-dependencies]
-test = [
- "poethepoet>=0.40.0",
+[dependency-groups]
+dev = [
"psutil>=7.2.2",
- "pytest>=9.0.2",
"pytest-cov>=7.0.0",
+ "poethepoet>=0.41.0",
+ "ruff>=0.15.0",
+ "black>=26.1.0",
]
-[tool.hatch.build]
-include = ["pythonLogs/**/*"]
-
-[tool.hatch.build.targets.wheel]
-packages = ["pythonLogs"]
-
[tool.poe.tasks]
-build = "uv build --wheel"
-updatedev.shell = "uv lock && uv sync --no-install-project --all-extras"
linter.shell = "uv run ruff check --fix . && uv run black ."
-profile = "uv run python -m cProfile -o cprofile.prof -m pytest"
-test = "uv run pytest"
+profile.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_unit.prof -m pytest --no-cov"}]
+test.sequence = ["linter", {shell = "uv run pytest"}]
+updatedev.sequence = ["linter", {shell = "uv lock --upgrade && uv sync --all-extras --group dev"}]
+build.sequence = ["updatedev", "test", {shell = "uv build --wheel"}]
[tool.pytest.ini_options]
addopts = "-v --cov --cov-report=term --cov-report=xml --junitxml=junit.xml"
junit_family = "legacy"
testpaths = ["tests"]
markers = [
- "slow: marks tests as slow (deselect with '-m \"not slow\"')"
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
]
[tool.coverage.run]
omit = [
"tests/*",
"*/__init__.py",
- "*/_version.py",
]
[tool.coverage.report]
+show_missing = true
+skip_covered = false
exclude_lines = [
"pragma: no cover",
"def __repr__",
@@ -86,8 +94,6 @@ exclude_lines = [
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
-show_missing = true
-skip_covered = false
[tool.black]
line-length = 120
@@ -95,22 +101,18 @@ skip-string-normalization = true
[tool.ruff]
line-length = 120
+target-version = "py311"
[tool.ruff.lint]
-# I - Import sorting and organization
-# F401 - Detect and remove unused imports
-select = ["I", "F401"]
+select = ["E", "W", "F", "I", "B", "C4", "UP"]
+ignore = ["E501", "E402", "UP046", "UP047"]
+
+[tool.ruff.lint.per-file-ignores]
+"__init__.py" = ["F401"]
+"tests/**/*.py" = ["S101", "S105", "S106", "S311", "SLF001", "F841"]
[tool.ruff.lint.isort]
known-first-party = ["pythonLogs"]
force-sort-within-sections = false
from-first = false
no-sections = true
-
-[tool.ruff.lint.per-file-ignores]
-# S101 Use of `assert` detected
-# S105 Possible hardcoded password assigned to variable
-# S106 Possible hardcoded password assigned to argument
-# S311 Standard pseudo-random generators are not suitable for cryptographic purposes
-# SLF001 Private member accessed
-"tests/**/*.py" = ["S101", "S105", "S106", "S311", "SLF001"]
diff --git a/pythonLogs/.env.example b/pythonLogs/.env.example
index 12ba113..42657fc 100644
--- a/pythonLogs/.env.example
+++ b/pythonLogs/.env.example
@@ -15,6 +15,7 @@ LOG_STREAM_HANDLER=True
LOG_SHOW_LOCATION=False
# Memory Management Settings
LOG_MAX_LOGGERS=50
+LOG_MAX_FORMATTERS=50
LOG_LOGGER_TTL_SECONDS=1800
# SizeRotatingLog Settings (only needed when using SizeRotatingLog)
diff --git a/pythonLogs/basic_log.py b/pythonLogs/basic_log.py
index 5ad3231..55e52b4 100644
--- a/pythonLogs/basic_log.py
+++ b/pythonLogs/basic_log.py
@@ -5,7 +5,7 @@
from pythonLogs.core.thread_safety import auto_thread_safe
-@auto_thread_safe(['init'])
+@auto_thread_safe(["init"])
class BasicLog:
"""Basic logger with context manager support for automatic resource cleanup."""
@@ -48,13 +48,13 @@ def init(self):
def __enter__(self):
"""Context manager entry."""
- if not hasattr(self, 'logger') or self.logger is None:
+ if not hasattr(self, "logger") or self.logger is None:
self.init()
return self.logger
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit with automatic cleanup."""
- if hasattr(self, 'logger'):
+ if hasattr(self, "logger"):
cleanup_logger_handlers(self.logger)
@staticmethod
diff --git a/pythonLogs/core/constants.py b/pythonLogs/core/constants.py
index 0edb131..3b5b0b1 100644
--- a/pythonLogs/core/constants.py
+++ b/pythonLogs/core/constants.py
@@ -1,5 +1,5 @@
import logging
-from enum import Enum
+from enum import StrEnum
# File and Directory Constants
MB_TO_BYTES = 1024 * 1024
@@ -17,7 +17,7 @@
DEFAULT_TIMEZONE = "UTC"
-class LogLevel(str, Enum):
+class LogLevel(StrEnum):
"""Log levels"""
CRITICAL = "CRITICAL"
@@ -29,7 +29,7 @@ class LogLevel(str, Enum):
DEBUG = "DEBUG"
-class RotateWhen(str, Enum):
+class RotateWhen(StrEnum):
"""Rotation timing options for TimedRotatingLog"""
MIDNIGHT = "midnight"
diff --git a/pythonLogs/core/factory.py b/pythonLogs/core/factory.py
index 267422c..cf8bc3b 100644
--- a/pythonLogs/core/factory.py
+++ b/pythonLogs/core/factory.py
@@ -3,37 +3,37 @@
import threading
import time
from dataclasses import dataclass
-from enum import Enum
+from enum import StrEnum
from pythonLogs.basic_log import BasicLog as _BasicLogImpl
from pythonLogs.core.constants import LogLevel, RotateWhen
from pythonLogs.core.log_utils import cleanup_logger_handlers
from pythonLogs.core.settings import get_log_settings
from pythonLogs.size_rotating import SizeRotatingLog as _SizeRotatingLogImpl
from pythonLogs.timed_rotating import TimedRotatingLog as _TimedRotatingLogImpl
-from typing import Dict, Optional, Tuple, Union, assert_never
+from typing import assert_never
@dataclass
class LoggerConfig:
"""Configuration class to group logger parameters"""
- level: Optional[Union[LogLevel, str]] = None
- name: Optional[str] = None
- directory: Optional[str] = None
- filenames: Optional[list | tuple] = None
- encoding: Optional[str] = None
- datefmt: Optional[str] = None
- timezone: Optional[str] = None
- streamhandler: Optional[bool] = None
- showlocation: Optional[bool] = None
- maxmbytes: Optional[int] = None
- when: Optional[Union[RotateWhen, str]] = None
- sufix: Optional[str] = None
- rotateatutc: Optional[bool] = None
- daystokeep: Optional[int] = None
-
-
-class LoggerType(str, Enum):
+ level: LogLevel | str | None = None
+ name: str | None = None
+ directory: str | None = None
+ filenames: list | tuple | None = None
+ encoding: str | None = None
+ datefmt: str | None = None
+ timezone: str | None = None
+ streamhandler: bool | None = None
+ showlocation: bool | None = None
+ maxmbytes: int | None = None
+ when: RotateWhen | str | None = None
+ sufix: str | None = None
+ rotateatutc: bool | None = None
+ daystokeep: int | None = None
+
+
+class LoggerType(StrEnum):
"""Available logger types"""
BASIC = "basic"
@@ -45,7 +45,7 @@ class LoggerFactory:
"""Factory for creating different types of loggers with optimized instantiation and memory management"""
# Logger registry for reusing loggers by name with timestamp tracking
- _logger_registry: Dict[str, Tuple[logging.Logger, float]] = {}
+ _logger_registry: dict[str, tuple[logging.Logger, float]] = {}
# Thread lock for registry access
_registry_lock = threading.RLock()
# Memory optimization settings
@@ -71,8 +71,8 @@ def _ensure_initialized(cls) -> None:
@classmethod
def get_or_create_logger(
cls,
- logger_type: Union[LoggerType, str],
- name: Optional[str] = None,
+ logger_type: LoggerType | str,
+ name: str | None = None,
**kwargs,
) -> logging.Logger:
"""
@@ -215,12 +215,10 @@ def get_memory_limits(cls) -> dict[str, int]:
Dictionary with current max_loggers and ttl_seconds settings
"""
with cls._registry_lock:
- return {'max_loggers': cls._max_loggers, 'ttl_seconds': cls._logger_ttl}
+ return {"max_loggers": cls._max_loggers, "ttl_seconds": cls._logger_ttl}
@staticmethod
- def create_logger(
- logger_type: Union[LoggerType, str], config: Optional[LoggerConfig] = None, **kwargs
- ) -> logging.Logger:
+ def create_logger(logger_type: LoggerType | str, config: LoggerConfig | None = None, **kwargs) -> logging.Logger:
"""
Factory method to create loggers based on type.
@@ -239,8 +237,10 @@ def create_logger(
if isinstance(logger_type, str):
try:
logger_type = LoggerType(logger_type.lower())
- except ValueError:
- raise ValueError(f"Invalid logger type: {logger_type}. Valid types: {[t.value for t in LoggerType]}")
+ except ValueError as err:
+ raise ValueError(
+ f"Invalid logger type: {logger_type}. Valid types: {[t.value for t in LoggerType]}"
+ ) from err
# Merge config and kwargs (kwargs take precedence for backward compatibility)
if config is None:
@@ -248,20 +248,20 @@ def create_logger(
# Create a new config with kwargs overriding config values
final_config = LoggerConfig(
- level=kwargs.get('level', config.level),
- name=kwargs.get('name', config.name),
- directory=kwargs.get('directory', config.directory),
- filenames=kwargs.get('filenames', config.filenames),
- encoding=kwargs.get('encoding', config.encoding),
- datefmt=kwargs.get('datefmt', config.datefmt),
- timezone=kwargs.get('timezone', config.timezone),
- streamhandler=kwargs.get('streamhandler', config.streamhandler),
- showlocation=kwargs.get('showlocation', config.showlocation),
- maxmbytes=kwargs.get('maxmbytes', config.maxmbytes),
- when=kwargs.get('when', config.when),
- sufix=kwargs.get('sufix', config.sufix),
- rotateatutc=kwargs.get('rotateatutc', config.rotateatutc),
- daystokeep=kwargs.get('daystokeep', config.daystokeep),
+ level=kwargs.get("level", config.level),
+ name=kwargs.get("name", config.name),
+ directory=kwargs.get("directory", config.directory),
+ filenames=kwargs.get("filenames", config.filenames),
+ encoding=kwargs.get("encoding", config.encoding),
+ datefmt=kwargs.get("datefmt", config.datefmt),
+ timezone=kwargs.get("timezone", config.timezone),
+ streamhandler=kwargs.get("streamhandler", config.streamhandler),
+ showlocation=kwargs.get("showlocation", config.showlocation),
+ maxmbytes=kwargs.get("maxmbytes", config.maxmbytes),
+ when=kwargs.get("when", config.when),
+ sufix=kwargs.get("sufix", config.sufix),
+ rotateatutc=kwargs.get("rotateatutc", config.rotateatutc),
+ daystokeep=kwargs.get("daystokeep", config.daystokeep),
)
# Convert enum values to strings for logger classes
@@ -314,12 +314,12 @@ def create_logger(
@staticmethod
def create_basic_logger(
- level: Optional[Union[LogLevel, str]] = None,
- name: Optional[str] = None,
- encoding: Optional[str] = None,
- datefmt: Optional[str] = None,
- timezone: Optional[str] = None,
- showlocation: Optional[bool] = None,
+ level: LogLevel | str | None = None,
+ name: str | None = None,
+ encoding: str | None = None,
+ datefmt: str | None = None,
+ timezone: str | None = None,
+ showlocation: bool | None = None,
) -> logging.Logger:
"""Convenience method for creating a basic logger"""
return LoggerFactory.create_logger(
@@ -334,17 +334,17 @@ def create_basic_logger(
@staticmethod
def create_size_rotating_logger(
- level: Optional[Union[LogLevel, str]] = None,
- name: Optional[str] = None,
- directory: Optional[str] = None,
- filenames: Optional[list | tuple] = None,
- maxmbytes: Optional[int] = None,
- daystokeep: Optional[int] = None,
- encoding: Optional[str] = None,
- datefmt: Optional[str] = None,
- timezone: Optional[str] = None,
- streamhandler: Optional[bool] = None,
- showlocation: Optional[bool] = None,
+ level: LogLevel | str | None = None,
+ name: str | None = None,
+ directory: str | None = None,
+ filenames: list | tuple | None = None,
+ maxmbytes: int | None = None,
+ daystokeep: int | None = None,
+ encoding: str | None = None,
+ datefmt: str | None = None,
+ timezone: str | None = None,
+ streamhandler: bool | None = None,
+ showlocation: bool | None = None,
) -> logging.Logger:
"""Convenience method for creating a size rotating logger"""
return LoggerFactory.create_logger(
@@ -364,19 +364,19 @@ def create_size_rotating_logger(
@staticmethod
def create_timed_rotating_logger(
- level: Optional[Union[LogLevel, str]] = None,
- name: Optional[str] = None,
- directory: Optional[str] = None,
- filenames: Optional[list | tuple] = None,
- when: Optional[Union[RotateWhen, str]] = None,
- sufix: Optional[str] = None,
- daystokeep: Optional[int] = None,
- encoding: Optional[str] = None,
- datefmt: Optional[str] = None,
- timezone: Optional[str] = None,
- streamhandler: Optional[bool] = None,
- showlocation: Optional[bool] = None,
- rotateatutc: Optional[bool] = None,
+ level: LogLevel | str | None = None,
+ name: str | None = None,
+ directory: str | None = None,
+ filenames: list | tuple | None = None,
+ when: RotateWhen | str | None = None,
+ sufix: str | None = None,
+ daystokeep: int | None = None,
+ encoding: str | None = None,
+ datefmt: str | None = None,
+ timezone: str | None = None,
+ streamhandler: bool | None = None,
+ showlocation: bool | None = None,
+ rotateatutc: bool | None = None,
) -> logging.Logger:
"""Convenience method for creating a timed rotating logger"""
return LoggerFactory.create_logger(
@@ -432,12 +432,12 @@ class BasicLog(_LoggerMixin):
def __init__(
self,
- level: Optional[Union[LogLevel, str]] = None,
- name: Optional[str] = None,
- encoding: Optional[str] = None,
- datefmt: Optional[str] = None,
- timezone: Optional[str] = None,
- showlocation: Optional[bool] = None,
+ level: LogLevel | str | None = None,
+ name: str | None = None,
+ encoding: str | None = None,
+ datefmt: str | None = None,
+ timezone: str | None = None,
+ showlocation: bool | None = None,
):
self._logger = LoggerFactory.create_basic_logger(
level=level,
@@ -465,17 +465,17 @@ class SizeRotatingLog(_LoggerMixin):
def __init__(
self,
- level: Optional[Union[LogLevel, str]] = None,
- name: Optional[str] = None,
- directory: Optional[str] = None,
- filenames: Optional[list | tuple] = None,
- maxmbytes: Optional[int] = None,
- daystokeep: Optional[int] = None,
- encoding: Optional[str] = None,
- datefmt: Optional[str] = None,
- timezone: Optional[str] = None,
- streamhandler: Optional[bool] = None,
- showlocation: Optional[bool] = None,
+ level: LogLevel | str | None = None,
+ name: str | None = None,
+ directory: str | None = None,
+ filenames: list | tuple | None = None,
+ maxmbytes: int | None = None,
+ daystokeep: int | None = None,
+ encoding: str | None = None,
+ datefmt: str | None = None,
+ timezone: str | None = None,
+ streamhandler: bool | None = None,
+ showlocation: bool | None = None,
):
self._logger = LoggerFactory.create_size_rotating_logger(
level=level,
@@ -508,19 +508,19 @@ class TimedRotatingLog(_LoggerMixin):
def __init__(
self,
- level: Optional[Union[LogLevel, str]] = None,
- name: Optional[str] = None,
- directory: Optional[str] = None,
- filenames: Optional[list | tuple] = None,
- when: Optional[Union[RotateWhen, str]] = None,
- sufix: Optional[str] = None,
- daystokeep: Optional[int] = None,
- encoding: Optional[str] = None,
- datefmt: Optional[str] = None,
- timezone: Optional[str] = None,
- streamhandler: Optional[bool] = None,
- showlocation: Optional[bool] = None,
- rotateatutc: Optional[bool] = None,
+ level: LogLevel | str | None = None,
+ name: str | None = None,
+ directory: str | None = None,
+ filenames: list | tuple | None = None,
+ when: RotateWhen | str | None = None,
+ sufix: str | None = None,
+ daystokeep: int | None = None,
+ encoding: str | None = None,
+ datefmt: str | None = None,
+ timezone: str | None = None,
+ streamhandler: bool | None = None,
+ showlocation: bool | None = None,
+ rotateatutc: bool | None = None,
):
self._logger = LoggerFactory.create_timed_rotating_logger(
level=level,
diff --git a/pythonLogs/core/log_utils.py b/pythonLogs/core/log_utils.py
index 8dc1240..af6479b 100644
--- a/pythonLogs/core/log_utils.py
+++ b/pythonLogs/core/log_utils.py
@@ -7,12 +7,11 @@
import sys
import threading
import time
-from datetime import datetime, timedelta
-from datetime import timezone as dttz
+from collections.abc import Callable
+from datetime import UTC, datetime, timedelta
from functools import lru_cache
from pathlib import Path
from pythonLogs.core.constants import DEFAULT_FILE_MODE, LEVEL_MAP
-from typing import Callable, Optional, Set
from zoneinfo import ZoneInfo
@@ -21,15 +20,17 @@ class RotatingLogMixin:
logger: logging.Logger | None
+ def init(self) -> None: ...
+
def __enter__(self):
"""Context manager entry."""
- if not hasattr(self, 'logger') or self.logger is None:
+ if not hasattr(self, "logger") or self.logger is None:
self.init()
return self.logger
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit with automatic cleanup."""
- if hasattr(self, 'logger'):
+ if hasattr(self, "logger"):
cleanup_logger_handlers(self.logger)
@staticmethod
@@ -39,7 +40,7 @@ def cleanup_logger(logger: logging.Logger) -> None:
# Global cache for checked directories with thread safety and size limits
-_checked_directories: Set[str] = set()
+_checked_directories: set[str] = set()
_directory_lock = threading.Lock()
_max_cached_directories = 500 # Limit cache size to prevent unbounded growth
@@ -109,7 +110,7 @@ def check_directory_permissions(directory_path: str) -> None:
except PermissionError as e:
err_msg = f"Unable to create directory | {directory_path}"
write_stderr(f"{err_msg} | {type(e).__name__}: {e}")
- raise PermissionError(err_msg)
+ raise PermissionError(err_msg) from e
# Add to cache with size limit enforcement
if len(_checked_directories) >= _max_cached_directories:
@@ -129,7 +130,7 @@ def remove_old_logs(logs_dir: str, days_to_keep: int) -> None:
try:
if file_path.stat().st_mtime < cutoff_time.timestamp():
file_path.unlink()
- except (OSError, IOError) as e:
+ except OSError as e:
write_stderr(f"Unable to delete old log | {file_path} | {type(e).__name__}: {e}")
except OSError as e:
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:
# Use local timezone
dt = datetime.now()
else:
- dt = datetime.now(dttz.utc).astimezone(tz)
+ dt = datetime.now(UTC).astimezone(tz)
dt_timezone = dt.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
sys.stderr.write(f"[{dt_timezone}]:[ERROR]:{msg}\n")
except (OSError, ValueError, KeyError):
@@ -286,7 +287,7 @@ def gzip_file_with_sufix(file_path: str, sufix: str) -> str | None:
# Final attempt failed or not Windows - treat as regular error
write_stderr(f"Unable to gzip log file | {file_path} | {type(e).__name__}: {e}")
raise e
- except (OSError, IOError) as e:
+ except OSError as e:
write_stderr(f"Unable to gzip log file | {file_path} | {type(e).__name__}: {e}")
raise e
@@ -324,7 +325,7 @@ def get_timezone_function(time_zone: str) -> Callable:
# Shared handler cleanup utility
-def cleanup_logger_handlers(logger: Optional[logging.Logger]) -> None:
+def cleanup_logger_handlers(logger: logging.Logger | None) -> None:
"""Clean up logger resources by closing all handlers.
This is a centralized utility to ensure consistent cleanup behavior
diff --git a/pythonLogs/core/memory_utils.py b/pythonLogs/core/memory_utils.py
index a0f6a1b..2df3a9b 100644
--- a/pythonLogs/core/memory_utils.py
+++ b/pythonLogs/core/memory_utils.py
@@ -2,16 +2,17 @@
import threading
import weakref
from . import log_utils
+from .settings import get_log_settings
from functools import lru_cache
-from typing import Any, Dict, Optional, Set
+from typing import Any
# Formatter cache to reduce memory usage for identical formatters
-_formatter_cache: Dict[str, logging.Formatter] = {}
+_formatter_cache: dict[str, logging.Formatter] = {}
_formatter_cache_lock = threading.Lock()
-_max_formatters = 50 # Limit formatter cache size
+_max_formatters = get_log_settings().max_formatters
-def get_cached_formatter(format_string: str, datefmt: Optional[str] = None) -> logging.Formatter:
+def get_cached_formatter(format_string: str, datefmt: str | None = None) -> logging.Formatter:
"""Get a cached formatter or create and cache a new one.
This reduces memory usage by reusing formatter instances with
@@ -66,7 +67,7 @@ def clear_directory_cache() -> None:
# Weak reference registry for tracking active loggers without preventing GC
-_active_loggers: Set[weakref.ReferenceType] = set()
+_active_loggers: set[weakref.ReferenceType] = set()
_weak_ref_lock = threading.Lock()
@@ -103,7 +104,7 @@ def get_active_logger_count() -> int:
return len(_active_loggers)
-def get_memory_stats() -> Dict[str, Any]:
+def get_memory_stats() -> dict[str, Any]:
"""Get memory usage statistics for the logging system.
Returns:
@@ -125,13 +126,13 @@ def get_memory_stats() -> Dict[str, Any]:
directory_stats = log_utils.get_directory_cache_stats()
return {
- 'registry_size': registry_size,
- 'formatter_cache_size': formatter_cache_size,
- 'directory_cache_size': directory_stats['cached_directories'],
- 'active_logger_count': get_active_logger_count(),
- 'max_registry_size': factory_limits['max_loggers'],
- 'max_formatter_cache': _max_formatters,
- 'max_directory_cache': directory_stats['max_directories'],
+ "registry_size": registry_size,
+ "formatter_cache_size": formatter_cache_size,
+ "directory_cache_size": directory_stats["cached_directories"],
+ "active_logger_count": get_active_logger_count(),
+ "max_registry_size": factory_limits["max_loggers"],
+ "max_formatter_cache": _max_formatters,
+ "max_directory_cache": directory_stats["max_directories"],
}
@@ -153,7 +154,7 @@ def optimize_lru_cache_sizes() -> None:
log_utils.get_stderr_timezone = lru_cache(maxsize=4)(log_utils.get_stderr_timezone.__wrapped__)
-def force_garbage_collection() -> Dict[str, int]:
+def force_garbage_collection() -> dict[str, int]:
"""Force garbage collection and return collection statistics.
This can be useful for testing memory leaks or forcing cleanup
@@ -172,7 +173,7 @@ def force_garbage_collection() -> Dict[str, int]:
collected = gc.collect()
return {
- 'objects_collected': collected,
- 'garbage_count': len(gc.garbage),
- 'reference_cycles': gc.get_count(),
+ "objects_collected": collected,
+ "garbage_count": len(gc.garbage),
+ "reference_cycles": gc.get_count(),
}
diff --git a/pythonLogs/core/settings.py b/pythonLogs/core/settings.py
index 405836e..af9ea0a 100644
--- a/pythonLogs/core/settings.py
+++ b/pythonLogs/core/settings.py
@@ -75,6 +75,10 @@ class LogSettings(BaseSettings):
default=100,
description="Maximum number of loggers to track in memory",
)
+ max_formatters: int = Field(
+ default=50,
+ description="Maximum number of formatters to cache in memory",
+ )
logger_ttl_seconds: int = Field(
default=3600,
description="Time-to-live in seconds for logger references",
diff --git a/pythonLogs/core/thread_safety.py b/pythonLogs/core/thread_safety.py
index 3f98df9..e8f2031 100644
--- a/pythonLogs/core/thread_safety.py
+++ b/pythonLogs/core/thread_safety.py
@@ -1,23 +1,24 @@
import functools
import threading
-from typing import Any, Callable, Dict, Type, TypeVar
+from collections.abc import Callable
+from typing import Any, TypeVar
-F = TypeVar('F', bound=Callable[..., Any])
+F = TypeVar("F", bound=Callable[..., Any])
class ThreadSafeMeta(type):
"""Metaclass that automatically adds thread safety to class methods."""
- def __new__(mcs, name: str, bases: tuple, namespace: Dict[str, Any], **kwargs):
+ def __new__(mcs, name: str, bases: tuple, namespace: dict[str, Any], **kwargs):
# Create the class first
cls = super().__new__(mcs, name, bases, namespace)
# Add a class-level lock if not already present
- if not hasattr(cls, '_lock'):
+ if not hasattr(cls, "_lock"):
cls._lock = threading.RLock()
# Get methods that should be thread-safe (exclude private/dunder methods)
- thread_safe_methods = getattr(cls, '_thread_safe_methods', None)
+ thread_safe_methods = getattr(cls, "_thread_safe_methods", None)
if thread_safe_methods is None:
# Auto-detect public methods that modify state
thread_safe_methods = [
@@ -25,8 +26,8 @@ def __new__(mcs, name: str, bases: tuple, namespace: Dict[str, Any], **kwargs):
for method_name in namespace
if (
callable(getattr(cls, method_name, None))
- and not method_name.startswith('_')
- and method_name not in ['__enter__', '__exit__', '__init__']
+ and not method_name.startswith("_")
+ and method_name not in ["__enter__", "__exit__", "__init__"]
)
]
@@ -47,12 +48,12 @@ def thread_safe(func: F) -> F:
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
# Use instance lock if available, otherwise class lock
- lock = getattr(self, '_lock', None)
+ lock = getattr(self, "_lock", None)
if lock is None:
# Check if class has lock, if not create one
- if not hasattr(self.__class__, '_lock'):
- setattr(self.__class__, '_lock', threading.RLock())
- lock = getattr(self.__class__, '_lock')
+ if not hasattr(self.__class__, "_lock"):
+ self.__class__._lock = threading.RLock()
+ lock = self.__class__._lock
with lock:
return func(self, *args, **kwargs)
@@ -60,36 +61,36 @@ def wrapper(self, *args, **kwargs):
return wrapper
-def _get_wrappable_methods(cls: Type) -> list:
+def _get_wrappable_methods(cls: type) -> list:
"""Helper function to get methods that should be made thread-safe."""
return [
method_name
for method_name in dir(cls)
if (
callable(getattr(cls, method_name, None))
- and not method_name.startswith('_')
- and method_name not in ['__enter__', '__exit__', '__init__']
+ and not method_name.startswith("_")
+ and method_name not in ["__enter__", "__exit__", "__init__"]
)
]
-def _ensure_class_has_lock(cls: Type) -> None:
+def _ensure_class_has_lock(cls: type) -> None:
"""Ensure the class has a lock attribute."""
- if not hasattr(cls, '_lock'):
+ if not hasattr(cls, "_lock"):
cls._lock = threading.RLock()
-def _should_wrap_method(cls: Type, method_name: str, original_method: Any) -> bool:
+def _should_wrap_method(cls: type, method_name: str, original_method: Any) -> bool:
"""Check if a method should be wrapped with thread safety."""
return (
- hasattr(cls, method_name) and callable(original_method) and not hasattr(original_method, '_thread_safe_wrapped')
+ hasattr(cls, method_name) and callable(original_method) and not hasattr(original_method, "_thread_safe_wrapped")
)
def auto_thread_safe(thread_safe_methods: list = None):
"""Class decorator that adds automatic thread safety to specified methods."""
- def decorator(cls: Type) -> Type:
+ def decorator(cls: type) -> type:
_ensure_class_has_lock(cls)
# Store thread-safe methods list
@@ -116,21 +117,21 @@ class AutoThreadSafe:
"""Base class that provides automatic thread safety for all public methods."""
def __init__(self):
- if not hasattr(self, '_lock'):
+ if not hasattr(self, "_lock"):
self._lock = threading.RLock()
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Add class-level lock
- if not hasattr(cls, '_lock'):
+ if not hasattr(cls, "_lock"):
cls._lock = threading.RLock()
# Auto-wrap public methods
for attr_name in dir(cls):
- if not attr_name.startswith('_'):
+ if not attr_name.startswith("_"):
attr = getattr(cls, attr_name)
- if callable(attr) and not hasattr(attr, '_thread_safe_wrapped'):
+ if callable(attr) and not hasattr(attr, "_thread_safe_wrapped"):
wrapped_attr = thread_safe(attr)
wrapped_attr._thread_safe_wrapped = True
setattr(cls, attr_name, wrapped_attr)
diff --git a/pythonLogs/size_rotating.py b/pythonLogs/size_rotating.py
index 6e7e42e..361e3e2 100755
--- a/pythonLogs/size_rotating.py
+++ b/pythonLogs/size_rotating.py
@@ -20,7 +20,7 @@
from pythonLogs.core.thread_safety import auto_thread_safe
-@auto_thread_safe(['init'])
+@auto_thread_safe(["init"])
class SizeRotatingLog(RotatingLogMixin):
"""Size-based rotating logger with context manager support for automatic resource cleanup."""
diff --git a/pythonLogs/timed_rotating.py b/pythonLogs/timed_rotating.py
index 9ae6fb3..ac2298f 100755
--- a/pythonLogs/timed_rotating.py
+++ b/pythonLogs/timed_rotating.py
@@ -16,7 +16,7 @@
from pythonLogs.core.thread_safety import auto_thread_safe
-@auto_thread_safe(['init'])
+@auto_thread_safe(["init"])
class TimedRotatingLog(RotatingLogMixin):
"""
Time-based rotating logger with context manager support for automatic resource cleanup.
diff --git a/tests/README.md b/tests/README.md
deleted file mode 100644
index babede3..0000000
--- a/tests/README.md
+++ /dev/null
@@ -1,180 +0,0 @@
-# Test Suite Documentation
-
-This directory contains comprehensive tests for the pythonLogs library, organized into logical categories for better maintainability and navigation.
-
-## Test Directory Structure
-
-```
-tests/
-├── core/ # Core functionality tests
-├── context_management/ # Context managers & resource management
-├── logger_types/ # Specific logger type tests
-├── factory/ # Factory pattern tests
-├── performance/ # Performance & memory optimization tests
-├── thread_safety/ # Thread safety & concurrency tests
-└── timezone/ # Timezone functionality tests
-```
-
-## Test Files Overview
-
-### Core Functionality Tests (`tests/core/`)
-- **`test_basic_log.py`** - Comprehensive BasicLog functionality testing
- - Tests BasicLog initialization, context managers, thread safety
- - Validates cleanup methods and multiple instance handling
- - **10 test cases** covering all BasicLog features
-
-- **`test_log_utils.py`** - Tests for utility functions
- - Tests helper functions in `log_utils.py`
- - Includes file operations, timezone handling, and validation
- - Multiple test cases for various utilities
-
-### Context Manager & Resource Management Tests (`tests/context_management/`)
-- **`test_context_managers.py`** - Context manager functionality for all logger types
- - Tests automatic resource cleanup for BasicLog, SizeRotatingLog, TimedRotatingLog
- - Validates exception safety and proper handler cleanup
- - Tests nested context managers and multiple file handlers
- - **10 test cases** including the new `shutdown_logger` test
-
-- **`test_resource_management.py`** - Resource lifecycle management
- - Test factory registry cleanup and memory management
- - Validates handler cleanup and resource disposal
- - Tests concurrent access safety and performance
- - **9 test cases** for robust resource management
-
-### Logger Type Tests (`tests/logger_types/`)
-- **`test_size_rotating.py`** - Size-based rotating logger tests
- - Tests file rotation, compression, and cleanup
- - Context manager functionality and resource management
- - Multiple file handling and stream output
- - Comprehensive size rotation scenarios
-
-- **`test_timed_rotating.py`** - Time-based rotating logger tests
- - Tests time-based rotation (hourly, daily, midnight, weekdays)
- - Context manager functionality and resource management
- - Timezone handling and rotation scheduling
- - Comprehensive time rotation scenarios
-
-### Factory Pattern Tests (`tests/factory/`)
-- **`test_factory.py`** - Core factory pattern functionality
- - Tests `LoggerFactory` class and all factory methods
- - Validates logger creation, registry caching, and performance
- - Tests error handling and type validation
- - **Multiple test cases** covering all factory features
-
-- **`test_enums.py`** - Enum usage with factory pattern
- - Tests `LogLevel`, `RotateWhen`, and `LoggerType` enums
- - Validates enum-to-string conversion and type safety
- - Tests backward compatibility with string values
- - **10 test cases** covering all enum scenarios
-
-- **`test_factory_examples.py`** - Integration and practical examples
- - Real-world usage scenarios and production-like setups
- - Multi-logger configurations and file-based logging
- - Registry usage patterns and error scenarios
- - **Multiple test cases** demonstrating practical usage
-
-- **`test_string_levels.py`** - String-based level configuration
- - Tests case-insensitive string level handling
- - Validates string to enum conversion
- - Tests all logger types with string levels
- - Comprehensive string level compatibility
-
-### Performance & Memory Tests (`tests/performance/`)
-- **`test_performance.py`** - Performance and optimization tests
- - Validates caching improvements and performance gains
- - Tests settings caching, registry performance, and memory usage
- - Stress testing and large-scale logger creation
- - Performance benchmarking for optimization features
-
-- **`test_memory_optimization.py`** - Memory management and optimization
- - Test memory usage patterns and cleanup efficiency
- - Validates formatter caching and directory caching
- - Tests garbage collection and memory leak prevention
- - Memory optimization feature validation
-
-- **`test_performance_zoneinfo.py`** - Performance tests for timezone operations
- - Benchmarks timezone function caching and optimization
- - Tests performance under concurrent access and bulk operations
- - Validates memory efficiency of timezone caching
- - Timezone performance optimization validation
-
-### Thread Safety & Concurrency Tests (`tests/thread_safety/`)
-- **`test_thread_safety.py`** - Concurrency and thread safety
- - Tests concurrent logger creation and registry access
- - Validates thread-safe operations across all components
- - Tests concurrent context manager cleanup
- - Stress testing for multithreaded environments
-
-- **`test_automatic_thread_safety.py`** - Automatic thread safety implementation
- - Tests automatic thread-safety decorators applied to logger classes
- - Validates @auto_thread_safe decorator functionality
- - Tests BasicLog, SizeRotatingLog, and TimedRotatingLog with automatic locking
- - **4 test cases** covering automatic thread safety features
-
-- **`test_thread_safety_module.py`** - Comprehensive thread safety module tests
- - Test all thread safety decorators (@thread_safe, @auto_thread_safe)
- - Tests ThreadSafeMeta metaclass and AutoThreadSafe base class
- - Tests ThreadSafeContext context manager and edge cases
- - **19 test cases** covering all thread safety mechanisms
-
-- **`test_thread_safety_patterns.py`** - Advanced thread safety patterns
- - Tests real-world concurrent patterns (producer-consumer, singleton, etc.)
- - Tests resource pool, event bus, and cache patterns with thread safety
- - Tests weak reference cleanup in multithreaded environments
- - **8 test cases** covering complex thread safety scenarios
-
-- **`test_automatic_features.py`** - Integration of all automatic features
- - Test memory optimization, resource cleanup, and thread safety together
- - Validates all three automatic features work seamlessly
- - Tests stress scenarios with multiple logger types concurrently
- - **6 test cases** ensuring all automatic features integrate properly
-
-### Timezone & Migration Tests (`tests/timezone/`)
-- **`test_timezone_migration.py`** - Timezone functionality with zoneinfo
- - Tests migration from pytz to Python's built-in zoneinfo module
- - Validates UTC, localtime, and named timezone support
- - Tests timezone integration with all logger types and factory pattern
- - **Multiple test cases** covering comprehensive timezone scenarios
-
-- **`test_zoneinfo_fallbacks.py`** - Timezone fallback mechanisms and edge cases
- - Tests fallback behavior for systems without complete timezone data
- - Validates error handling and edge cases for timezone operations
- - Tests concurrent access and memory efficiency
- - **Multiple test cases** for robust timezone handling
-
-
-
-## Running Tests
-
-### Run All Tests and Create a Coverage Report
-```bash
-poetry run poe test
-```
-
-### Run Specific Test Categories
-```bash
-# Core functionality tests
-poetry run pytest tests/core/ -v
-
-# Context managers and resource management
-poetry run pytest tests/context_management/ -v
-
-# Logger type tests (size rotating, timed rotating)
-poetry run pytest tests/logger_types/ -v
-
-# Factory pattern tests
-poetry run pytest tests/factory/ -v
-
-# Performance and memory optimization tests
-poetry run pytest tests/performance/ -v
-
-# Thread safety and concurrency tests
-poetry run pytest tests/thread_safety/ -v
-
-# Timezone functionality tests
-poetry run pytest tests/timezone/ -v
-
-# Run specific directories together
-poetry run pytest tests/core/ tests/logger_types/ -v # Core + Logger types
-poetry run pytest tests/performance/ tests/thread_safety/ -v # Performance + Concurrency
-```
diff --git a/tests/context_management/test_context_managers.py b/tests/context_management/test_context_managers.py
index b1be75a..e5f5cc6 100644
--- a/tests/context_management/test_context_managers.py
+++ b/tests/context_management/test_context_managers.py
@@ -71,7 +71,7 @@ def test_size_rotating_context_manager(self):
assert logger.level == logging.DEBUG
# Should have file handlers
- file_handlers = [h for h in logger.handlers if hasattr(h, 'baseFilename')]
+ file_handlers = [h for h in logger.handlers if hasattr(h, "baseFilename")]
assert len(file_handlers) > 0
# Test logging
@@ -98,7 +98,7 @@ def test_timed_rotating_context_manager(self):
assert logger.level == logging.WARNING
# Should have file handlers
- file_handlers = [h for h in logger.handlers if hasattr(h, 'baseFilename')]
+ file_handlers = [h for h in logger.handlers if hasattr(h, "baseFilename")]
assert len(file_handlers) > 0
# Test logging
@@ -134,7 +134,7 @@ def test_context_manager_without_init(self):
with logger_instance as logger:
# Context manager should have called init()
- assert hasattr(logger_instance, 'logger')
+ assert hasattr(logger_instance, "logger")
assert logger_instance.logger is not None
assert isinstance(logger, logging.Logger)
logger.info("Test message")
@@ -148,7 +148,7 @@ def test_context_manager_with_existing_init(self):
# Call init() manually first
manual_logger = logger_instance.init()
- assert hasattr(logger_instance, 'logger')
+ assert hasattr(logger_instance, "logger")
with logger_instance as context_logger:
# Should return the same logger
@@ -167,7 +167,7 @@ def test_multiple_file_handlers_cleanup(self):
name=logger_name, directory=self.temp_dir, filenames=multiple_files, maxmbytes=1
) as logger:
# Should have multiple file handlers
- file_handlers = [h for h in logger.handlers if hasattr(h, 'baseFilename')]
+ file_handlers = [h for h in logger.handlers if hasattr(h, "baseFilename")]
assert len(file_handlers) == len(multiple_files)
logger.info("Test message to multiple files")
@@ -188,7 +188,7 @@ def test_stream_handler_cleanup(self):
) as logger:
# Should have both file and stream handlers
stream_handlers = [h for h in logger.handlers if isinstance(h, logging.StreamHandler)]
- file_handlers = [h for h in logger.handlers if hasattr(h, 'baseFilename')]
+ file_handlers = [h for h in logger.handlers if hasattr(h, "baseFilename")]
assert len(stream_handlers) > 0
assert len(file_handlers) > 0
diff --git a/tests/context_management/test_resource_management.py b/tests/context_management/test_resource_management.py
index fc00bc5..5d94c25 100644
--- a/tests/context_management/test_resource_management.py
+++ b/tests/context_management/test_resource_management.py
@@ -165,7 +165,7 @@ def test_registry_clear_with_file_handlers(self):
logger.info("Test message before cleanup")
# Verify we have multiple handlers
- file_handlers = [h for h in logger.handlers if hasattr(h, 'baseFilename')]
+ file_handlers = [h for h in logger.handlers if hasattr(h, "baseFilename")]
stream_handlers = [h for h in logger.handlers if isinstance(h, logging.StreamHandler)]
assert len(file_handlers) == 2 # Two file handlers
diff --git a/tests/core/test_basic_log.py b/tests/core/test_basic_log.py
index 3f772a7..11b1b1b 100644
--- a/tests/core/test_basic_log.py
+++ b/tests/core/test_basic_log.py
@@ -27,12 +27,12 @@ def teardown_method(self):
def test_basic_log_initialization(self):
"""Test BasicLog initialization with default parameters."""
basic_log = BasicLog()
- assert hasattr(basic_log, 'level')
- assert hasattr(basic_log, 'appname')
- assert hasattr(basic_log, 'encoding')
- assert hasattr(basic_log, 'datefmt')
- assert hasattr(basic_log, 'timezone')
- assert hasattr(basic_log, 'showlocation')
+ assert hasattr(basic_log, "level")
+ assert hasattr(basic_log, "appname")
+ assert hasattr(basic_log, "encoding")
+ assert hasattr(basic_log, "datefmt")
+ assert hasattr(basic_log, "timezone")
+ assert hasattr(basic_log, "showlocation")
assert basic_log.logger is None
def test_basic_log_initialization_with_params(self):
@@ -60,7 +60,7 @@ def test_basic_log_init_method(self):
assert isinstance(logger, logging.Logger)
assert logger.name == "test_init"
assert logger.level == logging.INFO
- assert hasattr(basic_log, 'logger')
+ assert hasattr(basic_log, "logger")
assert basic_log.logger is logger
def test_basic_log_logger_functionality(self):
@@ -178,7 +178,7 @@ def test_basic_log_multiple_instances(self):
assert logger.level == logging.INFO
# Clean up all loggers
- for basic_log, logger in loggers:
+ for _basic_log, logger in loggers:
BasicLog.cleanup_logger(logger)
assert len(logger.handlers) == 0
diff --git a/tests/core/test_log_utils.py b/tests/core/test_log_utils.py
index 4b9479d..877414b 100644
--- a/tests/core/test_log_utils.py
+++ b/tests/core/test_log_utils.py
@@ -80,11 +80,11 @@ def patch_logger_kwargs_with_safe_timezone(kwargs):
"""Patch logger kwargs to use safe timezone if UTC is specified but not available."""
from zoneinfo import ZoneInfo
- if kwargs.get('timezone') == 'UTC':
+ if kwargs.get("timezone") == "UTC":
try:
ZoneInfo("UTC") # Test if UTC timezone data is available
except KeyError:
- kwargs['timezone'] = 'localtime' # Fall back to localtime if UTC data is missing
+ kwargs["timezone"] = "localtime" # Fall back to localtime if UTC data is missing
return kwargs
@@ -333,7 +333,7 @@ def create_windows_safe_temp_file(suffix="", prefix="tmp", dir=None, text=False)
fd, filepath = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir, text=text)
# Convert file descriptor to file handle
- mode = 'w' if text else 'wb'
+ mode = "w" if text else "wb"
file_handle = os.fdopen(fd, mode)
return file_handle, filepath
@@ -385,7 +385,7 @@ def test_check_directory_permissions(self):
try:
os.makedirs(directory, mode=0, exist_ok=True) # No permissions at all
- assert os.path.exists(directory) == True
+ assert os.path.exists(directory)
with pytest.raises(PermissionError) as exec_info:
log_utils.check_directory_permissions(directory)
assert type(exec_info.value) is PermissionError
@@ -419,7 +419,7 @@ def test_check_directory_permissions(self):
def test_remove_old_logs(self):
directory = os.path.join(tempfile.gettempdir(), "test_remove_logs")
os.makedirs(directory, mode=0o755, exist_ok=True)
- assert os.path.exists(directory) == True
+ assert os.path.exists(directory)
# Create a file and manually set its modification time to be old
with tempfile.NamedTemporaryFile(dir=directory, suffix=".gz", delete=False) as tmpfile:
@@ -428,9 +428,9 @@ def test_remove_old_logs(self):
os.utime(file_path, (old_time, old_time))
log_utils.remove_old_logs(directory, 1) # Remove files older than 1 day
- assert os.path.isfile(file_path) == False
+ assert not os.path.isfile(file_path)
log_utils.delete_file(directory)
- assert os.path.exists(directory) == False
+ assert not os.path.exists(directory)
def test_delete_file(self):
"""Test delete_file with standard Unix/Linux file handling."""
@@ -438,9 +438,9 @@ def test_delete_file(self):
file_path = tmp_file.name
tmp_file.write("test content")
- assert os.path.isfile(file_path) == True
+ assert os.path.isfile(file_path)
log_utils.delete_file(file_path)
- assert os.path.isfile(file_path) == False
+ assert not os.path.isfile(file_path)
def test_is_older_than_x_days(self):
"""Test is_older_than_x_days with standard Unix/Linux file handling."""
@@ -449,18 +449,18 @@ def test_is_older_than_x_days(self):
tmp_file.write("test content")
try:
- assert os.path.isfile(file_path) == True
+ assert os.path.isfile(file_path)
# When days=1, it compares against 1 day ago, so newly created file should NOT be older
result = log_utils.is_older_than_x_days(file_path, 1)
- assert result == False
+ assert not result
# When days=5, it compares against 5 days ago, so newly created file should NOT be older
result = log_utils.is_older_than_x_days(file_path, 5)
- assert result == False
+ assert not result
log_utils.delete_file(file_path)
- assert os.path.isfile(file_path) == False
+ assert not os.path.isfile(file_path)
finally:
# Ensure cleanup if the test fails
if os.path.exists(file_path):
@@ -550,12 +550,12 @@ def test_get_format(self):
import re
# The % characters need to be literal in the regex
- offset_pattern = r'\[%\(asctime\)s\.%\(msecs\)03d([+-]\d{4})\]'
+ offset_pattern = r"\[%\(asctime\)s\.%\(msecs\)03d([+-]\d{4})\]"
match = re.search(offset_pattern, result)
assert match is not None, f"No timezone offset found in format: {result}"
# The offset could be +1000 (if timezone is available) or system localtime fallback
offset = match.group(1)
- assert re.match(r'[+-]\d{4}', offset), f"Invalid timezone offset format: {offset}"
+ assert re.match(r"[+-]\d{4}", offset), f"Invalid timezone offset format: {offset}"
def test_gzip_file_with_sufix(self):
"""Test gzip_file_with_sufix with standard Unix/Linux file handling."""
@@ -564,7 +564,7 @@ def test_gzip_file_with_sufix(self):
tmp_file.write("test content for gzip")
try:
- assert os.path.isfile(file_path) == True
+ assert os.path.isfile(file_path)
sufix = "test1"
result = log_utils.gzip_file_with_sufix(file_path, sufix)
file_path_no_suffix = file_path.split(".")[0]
@@ -573,7 +573,7 @@ def test_gzip_file_with_sufix(self):
# Clean up the gzipped file
if os.path.exists(result):
os.unlink(result)
- assert os.path.isfile(result) == False
+ assert not os.path.isfile(result)
finally:
# Ensure cleanup of the original file if it still exists
@@ -747,7 +747,7 @@ def test_is_older_than_x_days_edge_cases(self):
time.sleep(0.001) # 1ms delay to handle Windows timing precision
result = log_utils.is_older_than_x_days(tmp_file.name, 0)
- assert result == True # Should use current time as cutoff
+ assert result # Should use current time as cutoff
# Test with non-existent file
with pytest.raises(FileNotFoundError):
@@ -893,7 +893,7 @@ def mock_glob(self, pattern):
return original_path_glob(self, pattern)
try:
- with unittest.mock.patch.object(Path, 'glob', mock_glob):
+ with unittest.mock.patch.object(Path, "glob", mock_glob):
stderr_capture = io.StringIO()
with contextlib.redirect_stderr(stderr_capture):
log_utils.remove_old_logs(test_dir, 1)
@@ -920,7 +920,7 @@ def test_delete_file_special_file(self):
# delete_file should handle symlinks
result = log_utils.delete_file(link_file)
- assert result == True
+ assert result
assert not os.path.exists(link_file)
@pytest.mark.skipif(sys.platform == "win32", reason="Unix/Linux/macOS-specific chmod test")
@@ -1199,7 +1199,7 @@ def test_cache_eviction_stress_test(self):
finally:
# Cleanup using context managers
- for temp_dir, temp_dir_context in temp_dirs:
+ for _temp_dir, temp_dir_context in temp_dirs:
temp_dir_context.__exit__(None, None, None)
log_utils._max_cached_directories = original_max
@@ -1250,7 +1250,7 @@ def test_timezone_offset_various_timezones(self):
offset = log_utils.get_timezone_offset(tz)
assert isinstance(offset, str)
assert len(offset) == 5 # Format: +/-HHMM
- assert offset[0] in ['+', '-']
+ assert offset[0] in ["+", "-"]
if expected_offset:
assert offset == expected_offset
@@ -1356,7 +1356,7 @@ def test_delete_file_special_file_coverage(self):
# delete_file should handle this special file
result = log_utils.delete_file(fifo_path)
- assert result == True
+ assert result
assert not os.path.exists(fifo_path)
except OSError:
# FIFO creation might not be supported on all systems
@@ -1443,7 +1443,7 @@ def test_timezone_offset_fallback_exception(self):
# Should fall back to localtime (lines 216-219)
assert isinstance(result, str)
assert len(result) == 5 # Format: +/-HHMM
- assert result[0] in ['+', '-']
+ assert result[0] in ["+", "-"]
def test_gzip_file_source_deletion_error_coverage(self):
"""Test gzip_file_with_sufix when source file deletion fails."""
@@ -1467,7 +1467,7 @@ def mock_unlink(self):
try:
stderr_capture = io.StringIO()
with contextlib.redirect_stderr(stderr_capture):
- with unittest.mock.patch.object(Path, 'unlink', mock_unlink):
+ with unittest.mock.patch.object(Path, "unlink", mock_unlink):
with pytest.raises(OSError):
log_utils.gzip_file_with_sufix(test_file, "test")
@@ -1494,7 +1494,7 @@ def mock_zoneinfo(key):
return ZoneInfo(key)
try:
- with unittest.mock.patch('pythonLogs.core.log_utils.ZoneInfo', side_effect=mock_zoneinfo):
+ with unittest.mock.patch("pythonLogs.core.log_utils.ZoneInfo", side_effect=mock_zoneinfo):
result = log_utils.get_timezone_function("UTC")
# Should fall back to localtime (lines 273-275)
@@ -1519,7 +1519,7 @@ def mock_zoneinfo(key):
return ZoneInfo(key)
try:
- with unittest.mock.patch('pythonLogs.core.log_utils.ZoneInfo', side_effect=mock_zoneinfo):
+ with unittest.mock.patch("pythonLogs.core.log_utils.ZoneInfo", side_effect=mock_zoneinfo):
result = log_utils.get_timezone_function("Custom/Timezone")
# Should fall back to localtime (lines 283-285)
@@ -1544,7 +1544,7 @@ def mock_gzip_open(*args, **kwargs):
raise OSError("Mock OSError during gzip compression")
try:
- with unittest.mock.patch('gzip.open', side_effect=mock_gzip_open):
+ with unittest.mock.patch("gzip.open", side_effect=mock_gzip_open):
stderr_capture = io.StringIO()
with contextlib.redirect_stderr(stderr_capture):
with pytest.raises(OSError) as exc_info:
@@ -1577,10 +1577,10 @@ def test_gzip_file_ioerror_handling(self):
def mock_copyfileobj(*args, **kwargs):
# Raise IOError to trigger lines 265-267
- raise IOError("Mock IOError during file copy")
+ raise OSError("Mock IOError during file copy")
try:
- with unittest.mock.patch('shutil.copyfileobj', side_effect=mock_copyfileobj):
+ with unittest.mock.patch("shutil.copyfileobj", side_effect=mock_copyfileobj):
stderr_capture = io.StringIO()
with contextlib.redirect_stderr(stderr_capture):
with pytest.raises(IOError) as exc_info:
diff --git a/tests/core/test_log_utils_windows.py b/tests/core/test_log_utils_windows.py
index 4622776..1f8b90a 100644
--- a/tests/core/test_log_utils_windows.py
+++ b/tests/core/test_log_utils_windows.py
@@ -49,7 +49,7 @@ def test_check_directory_permissions_windows(self):
# Test with a path that contains invalid characters (Windows-specific)
try:
- invalid_chars_path = os.path.join(temp_dir, "invalid<>:|*?\"path")
+ invalid_chars_path = os.path.join(temp_dir, 'invalid<>:|*?"path')
# This might raise different exceptions on different Windows versions
with pytest.raises((OSError, ValueError)) as exec_info:
log_utils.check_directory_permissions(invalid_chars_path)
@@ -92,9 +92,9 @@ def test_delete_file_windows_safe(self):
file_handle.write("test content")
file_handle.close()
- assert os.path.isfile(file_path) == True
+ assert os.path.isfile(file_path)
log_utils.delete_file(file_path)
- assert os.path.isfile(file_path) == False
+ assert not os.path.isfile(file_path)
finally:
# Ensure cleanup if the test fails
if os.path.exists(file_path):
@@ -113,18 +113,18 @@ def test_is_older_than_x_days_windows_safe(self):
file_handle.write("test content")
file_handle.close()
- assert os.path.isfile(file_path) == True
+ assert os.path.isfile(file_path)
# When days=1, it compares against 1 day ago, so newly created file should NOT be older
result = log_utils.is_older_than_x_days(file_path, 1)
- assert result == False
+ assert not result
# When days=5, it compares against 5 days ago, so newly created file should NOT be older
result = log_utils.is_older_than_x_days(file_path, 5)
- assert result == False
+ assert not result
log_utils.delete_file(file_path)
- assert os.path.isfile(file_path) == False
+ assert not os.path.isfile(file_path)
finally:
# Ensure cleanup if the test fails
if os.path.exists(file_path):
@@ -141,7 +141,7 @@ def test_gzip_file_with_sufix_windows_safe(self):
file_handle.write("test content for gzip")
file_handle.close()
- assert os.path.isfile(file_path) == True
+ assert os.path.isfile(file_path)
sufix = "test1"
result = log_utils.gzip_file_with_sufix(file_path, sufix)
file_path_no_suffix = file_path.split(".")[0]
@@ -149,7 +149,7 @@ def test_gzip_file_with_sufix_windows_safe(self):
# Clean up the gzipped file with Windows-safe deletion
safe_close_and_delete_file(None, result)
- assert os.path.isfile(result) == False
+ assert not os.path.isfile(result)
finally:
# Ensure cleanup of the original file if it still exists
@@ -170,7 +170,7 @@ def test_gzip_file_windows_retry_mechanism(self):
file_handle.close()
# Mock time.sleep to verify retry mechanism
- with patch('pythonLogs.core.log_utils.time.sleep') as mock_sleep:
+ with patch("pythonLogs.core.log_utils.time.sleep") as mock_sleep:
# Mock open to raise PermissionError on first call, succeed on second
call_count = 0
original_open = open
@@ -186,8 +186,8 @@ def mock_open_side_effect(*args, real_open=original_open, **kwargs):
return real_open(*args, **kwargs)
# Always mock platform as win32 and open with retry behavior
- with patch('pythonLogs.core.log_utils.sys.platform', 'win32'):
- with patch('pythonLogs.core.log_utils.open', side_effect=mock_open_side_effect):
+ with patch("pythonLogs.core.log_utils.sys.platform", "win32"):
+ with patch("pythonLogs.core.log_utils.open", side_effect=mock_open_side_effect):
result = log_utils.gzip_file_with_sufix(file_path, "retry_test")
# Verify retry was attempted (sleep was called)
@@ -225,12 +225,12 @@ def test_timezone_fallback_windows(self):
import re
# The % characters need to be literal in the regex
- offset_pattern = r'\[%\(asctime\)s\.%\(msecs\)03d([+-]\d{4})\]'
+ offset_pattern = r"\[%\(asctime\)s\.%\(msecs\)03d([+-]\d{4})\]"
match = re.search(offset_pattern, result)
assert match is not None, f"No timezone offset found in format: {result}"
# The offset could be the specific timezone or system localtime fallback
offset = match.group(1)
- assert re.match(r'[+-]\d{4}', offset), f"Invalid timezone offset format: {offset}"
+ assert re.match(r"[+-]\d{4}", offset), f"Invalid timezone offset format: {offset}"
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific tests")
def test_windows_timezone_environment_fallback(self):
@@ -327,7 +327,7 @@ def test_windows_stderr_timezone_with_dst(self):
# Should contain some form of timezone offset
# Windows may fall back to local timezone if specific timezone unavailable
- assert any(char in output for char in ['+', '-']) or 'Z' in output
+ assert any(char in output for char in ["+", "-"]) or "Z" in output
finally:
if original_tz is not None:
@@ -366,10 +366,10 @@ def windows_file_worker(worker_id):
with lock:
results.append(
{
- 'worker_id': worker_id,
- 'file_path': file_path,
- 'is_old': is_old,
- 'gzip_result': gzip_result,
+ "worker_id": worker_id,
+ "file_path": file_path,
+ "is_old": is_old,
+ "gzip_result": gzip_result,
}
)
@@ -399,9 +399,9 @@ def windows_file_worker(worker_id):
# Verify all workers completed successfully
for result in results:
- assert result['is_old'] == False # Files should NOT be considered "old" (created recently)
- assert result['gzip_result'] is not None
- assert f"worker_{result['worker_id']}" in result['gzip_result']
+ assert not result["is_old"] # Files should NOT be considered "old" (created recently)
+ assert result["gzip_result"] is not None
+ assert f"worker_{result['worker_id']}" in result["gzip_result"]
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific tests")
def test_gzip_file_windows_permission_error(self):
@@ -504,9 +504,9 @@ def mock_open_side_effect(*args, **kwargs):
return original_open(*args, **kwargs)
# Mock sys.platform to be Windows and time.sleep to verify retry
- with unittest.mock.patch('pythonLogs.core.log_utils.sys.platform', 'win32'):
- with unittest.mock.patch('pythonLogs.core.log_utils.time.sleep') as mock_sleep:
- with unittest.mock.patch('pythonLogs.core.log_utils.open', side_effect=mock_open_side_effect):
+ with unittest.mock.patch("pythonLogs.core.log_utils.sys.platform", "win32"):
+ with unittest.mock.patch("pythonLogs.core.log_utils.time.sleep") as mock_sleep:
+ with unittest.mock.patch("pythonLogs.core.log_utils.open", side_effect=mock_open_side_effect):
result = log_utils.gzip_file_with_sufix(file_path, "comprehensive_retry")
# Verify retries were attempted (sleep should be called twice)
@@ -556,7 +556,7 @@ def test_windows_delete_file_scenarios(self):
assert os.path.isfile(file_path)
result = log_utils.delete_file(file_path)
- assert result == True
+ assert result
assert not os.path.exists(file_path)
finally:
if os.path.exists(file_path):
@@ -597,7 +597,7 @@ def test_windows_logger_cleanup_integration(self):
for log_file in temp_files:
if os.path.exists(log_file):
result = safe_delete_file(log_file)
- assert result == True
+ assert result
finally:
# Ensure cleanup
cleanup_all_loggers()
@@ -630,7 +630,7 @@ def test_windows_directory_cleanup_stress(self):
# Windows-safe cleanup should handle this
result = safe_delete_directory(nested_dir)
- assert result == True or not os.path.exists(nested_dir)
+ assert result or not os.path.exists(nested_dir)
finally:
# Final cleanup
cleanup_all_loggers()
diff --git a/tests/core/test_settings.py b/tests/core/test_settings.py
index 8f83635..2f830d8 100644
--- a/tests/core/test_settings.py
+++ b/tests/core/test_settings.py
@@ -37,6 +37,7 @@ def test_default_values(self):
assert settings.stream_handler is True
assert settings.show_location is False
assert settings.max_loggers == 100
+ assert settings.max_formatters == 50
assert settings.logger_ttl_seconds == 3600
assert settings.max_file_size_mb == 10
assert settings.rotate_when.value == "midnight"
@@ -67,12 +68,20 @@ def test_custom_values(self):
timezone="UTC",
appname="custom_app",
max_loggers=50,
+ max_formatters=25,
)
assert settings.level == LogLevel.DEBUG
assert settings.timezone == "UTC"
assert settings.appname == "custom_app"
assert settings.max_loggers == 50
+ assert settings.max_formatters == 25
+
+ def test_max_formatters_env_var(self):
+ """Test that LOG_MAX_FORMATTERS env var overrides default."""
+ with patch.dict(os.environ, {"LOG_MAX_FORMATTERS": "75"}):
+ settings = LogSettings()
+ assert settings.max_formatters == 75
class TestGetLogSettings:
diff --git a/tests/factory/test_enums.py b/tests/factory/test_enums.py
index 80185d0..2eae93f 100644
--- a/tests/factory/test_enums.py
+++ b/tests/factory/test_enums.py
@@ -28,7 +28,9 @@ def setup_method(self):
def test_log_level_enum_usage(self):
"""Test LogLevel enum usage."""
logger = LoggerFactory.create_logger(
- LoggerType.BASIC, name="enum_test", level=LogLevel.DEBUG # Using enum instead of string
+ LoggerType.BASIC,
+ name="enum_test",
+ level=LogLevel.DEBUG, # Using enum instead of string
)
assert logger.name == "enum_test"
assert logger.level == 10 # DEBUG level
@@ -96,7 +98,7 @@ def test_all_rotate_when_enum_values(self):
]
# Just verify they're accessible and have expected values
- expected_values = ['midnight', 'H', 'D', 'W0', 'W1', 'W2', 'W3', 'W4', 'W5', 'W6']
+ expected_values = ["midnight", "H", "D", "W0", "W1", "W2", "W3", "W4", "W5", "W6"]
actual_values = [when.value for when in when_options]
assert actual_values == expected_values
diff --git a/tests/factory/test_factory.py b/tests/factory/test_factory.py
index 02d8521..68128c6 100644
--- a/tests/factory/test_factory.py
+++ b/tests/factory/test_factory.py
@@ -95,7 +95,7 @@ def test_performance_improvement_with_caching(self):
# Test with registry (reuses loggers)
clear_logger_registry()
start_time = time.time()
- for i in range(20):
+ for _i in range(20):
LoggerFactory.get_or_create_logger(LoggerType.BASIC, name="cached_perf_test")
cached_time = time.time() - start_time
@@ -318,7 +318,7 @@ def test_factory_ensure_initialized_behavior(self):
@pytest.mark.parametrize("error_type", [OSError, ValueError, RuntimeError])
def test_factory_atexit_cleanup_error_handling(self, error_type):
"""Test atexit cleanup handles expected exceptions gracefully."""
- with patch.object(LoggerFactory, 'clear_registry', side_effect=error_type("Test error")):
+ with patch.object(LoggerFactory, "clear_registry", side_effect=error_type("Test error")):
# Should not raise an exception (silently ignored)
LoggerFactory._atexit_cleanup()
@@ -382,7 +382,7 @@ def test_factory_memory_limits_from_settings(self):
mock_settings.logger_ttl_seconds = 1800
# Patch the import inside the function
- with patch('pythonLogs.core.factory.get_log_settings', return_value=mock_settings):
+ with patch("pythonLogs.core.factory.get_log_settings", return_value=mock_settings):
# Reset initialization flag
LoggerFactory._initialized = False
@@ -456,8 +456,8 @@ def test_factory_get_memory_limits(self):
# Get and verify limits
limits = LoggerFactory.get_memory_limits()
- assert limits['max_loggers'] == 75
- assert limits['ttl_seconds'] == 2400
+ assert limits["max_loggers"] == 75
+ assert limits["ttl_seconds"] == 2400
@pytest.mark.skipif(
sys.platform == "win32",
diff --git a/tests/logger_types/test_size_rotating.py b/tests/logger_types/test_size_rotating.py
index 57b478c..661c2cc 100644
--- a/tests/logger_types/test_size_rotating.py
+++ b/tests/logger_types/test_size_rotating.py
@@ -322,7 +322,7 @@ def test_gzip_rotator_size_call_with_nonexistent_source(self):
gz_files = list(Path(temp_dir).glob("*.gz"))
assert len(gz_files) == 0
- @patch('pythonLogs.size_rotating.remove_old_logs')
+ @patch("pythonLogs.size_rotating.remove_old_logs")
@pytest.mark.skipif(
sys.platform == "win32",
reason="Windows file locking issues with TemporaryDirectory - see equivalent Windows-specific test file",
@@ -365,7 +365,7 @@ def test_gzip_rotator_size_integration(self):
# Force handlers to flush
for handler in logger.handlers:
- if hasattr(handler, 'flush'):
+ if hasattr(handler, "flush"):
handler.flush()
# Verify log file exists
diff --git a/tests/logger_types/test_timed_rotating.py b/tests/logger_types/test_timed_rotating.py
index 30387ea..873437f 100644
--- a/tests/logger_types/test_timed_rotating.py
+++ b/tests/logger_types/test_timed_rotating.py
@@ -323,7 +323,7 @@ def test_gzip_rotator_timed_suffix_extraction(self):
# Reset source file
source_file.write_text("Test content")
- with patch('pythonLogs.timed_rotating.gzip_file_with_sufix') as mock_gzip:
+ with patch("pythonLogs.timed_rotating.gzip_file_with_sufix") as mock_gzip:
rotator(str(source_file), dest_filename)
mock_gzip.assert_called_once_with(str(source_file), expected_suffix)
@@ -343,7 +343,7 @@ def test_gzip_rotator_timed_with_nonexistent_source(self):
gz_files = list(Path(temp_dir).glob("*.gz"))
assert len(gz_files) == 0
- @patch('pythonLogs.timed_rotating.remove_old_logs')
+ @patch("pythonLogs.timed_rotating.remove_old_logs")
@pytest.mark.skipif(
sys.platform == "win32",
reason="Windows file locking issues with TemporaryDirectory - see equivalent Windows-specific test file",
@@ -362,7 +362,7 @@ def test_gzip_rotator_timed_calls_remove_old_logs(self, mock_remove_old_logs):
# Should have called remove_old_logs
mock_remove_old_logs.assert_called_once_with(temp_dir, 7)
- @patch('pythonLogs.timed_rotating.gzip_file_with_sufix')
+ @patch("pythonLogs.timed_rotating.gzip_file_with_sufix")
@pytest.mark.skipif(
sys.platform == "win32",
reason="Windows file locking issues with TemporaryDirectory - see equivalent Windows-specific test file",
@@ -405,7 +405,7 @@ def test_gzip_rotator_timed_integration(self):
# Force handlers to flush
for handler in logger.handlers:
- if hasattr(handler, 'flush'):
+ if hasattr(handler, "flush"):
handler.flush()
# Verify log file exists
@@ -431,7 +431,7 @@ def test_gzip_rotator_timed_suffix_edge_cases(self):
for dest_filename, expected_suffix in edge_cases:
source_file.write_text("Test content") # Reset file
- with patch('pythonLogs.timed_rotating.gzip_file_with_sufix') as mock_gzip:
+ with patch("pythonLogs.timed_rotating.gzip_file_with_sufix") as mock_gzip:
rotator(str(source_file), dest_filename)
if dest_filename: # Only call if dest_filename is not empty
mock_gzip.assert_called_once_with(str(source_file), expected_suffix)
diff --git a/tests/performance/test_memory_optimization.py b/tests/performance/test_memory_optimization.py
index 24b9c5a..3337a76 100644
--- a/tests/performance/test_memory_optimization.py
+++ b/tests/performance/test_memory_optimization.py
@@ -26,7 +26,7 @@
)
-@pytest.mark.skipif(os.getenv('CI') == 'true', reason="Performance tests unstable in CI")
+@pytest.mark.skipif(os.getenv("CI") == "true", reason="Performance tests unstable in CI")
class TestMemoryOptimization:
"""Test cases for memory optimization features."""
@@ -112,7 +112,7 @@ def test_directory_cache_size_limit(self):
finally:
# Cleanup temp directories using context managers
- for temp_dir, temp_dir_context in temp_dirs:
+ for _temp_dir, temp_dir_context in temp_dirs:
temp_dir_context.__exit__(None, None, None)
def test_formatter_cache_efficiency(self):
@@ -151,22 +151,22 @@ def test_memory_stats_reporting(self):
# Verify stats structure
expected_keys = {
- 'registry_size',
- 'formatter_cache_size',
- 'directory_cache_size',
- 'active_logger_count',
- 'max_registry_size',
- 'max_formatter_cache',
- 'max_directory_cache',
+ "registry_size",
+ "formatter_cache_size",
+ "directory_cache_size",
+ "active_logger_count",
+ "max_registry_size",
+ "max_formatter_cache",
+ "max_directory_cache",
}
assert set(stats.keys()) == expected_keys
# Verify some basic constraints
- assert stats['registry_size'] >= 3
- assert stats['max_registry_size'] > 0
- assert stats['max_formatter_cache'] > 0
- assert stats['max_directory_cache'] > 0
- assert isinstance(stats['active_logger_count'], int)
+ assert stats["registry_size"] >= 3
+ assert stats["max_registry_size"] > 0
+ assert stats["max_formatter_cache"] > 0
+ assert stats["max_directory_cache"] > 0
+ assert isinstance(stats["active_logger_count"], int)
def test_weak_reference_tracking(self):
"""Test that weak references track active loggers correctly."""
@@ -228,7 +228,7 @@ def test_force_garbage_collection(self):
gc_stats = force_garbage_collection()
# Verify stats structure
- expected_keys = {'objects_collected', 'garbage_count', 'reference_cycles'}
+ expected_keys = {"objects_collected", "garbage_count", "reference_cycles"}
assert set(gc_stats.keys()) == expected_keys
# Verify all values are integers
@@ -268,10 +268,10 @@ def memory_worker(worker_id):
results.append(
{
- 'worker_id': worker_id,
- 'logger_name': logger.name,
- 'stats': stats,
- 'formatter': formatter is not None,
+ "worker_id": worker_id,
+ "logger_name": logger.name,
+ "stats": stats,
+ "formatter": formatter is not None,
}
)
@@ -291,7 +291,7 @@ def memory_worker(worker_id):
# Verify memory constraints were maintained
final_stats = get_memory_stats()
- assert final_stats['registry_size'] <= 20 # Respect size limit
+ assert final_stats["registry_size"] <= 20 # Respect size limit
def test_memory_leak_prevention(self):
"""Test that the library prevents common memory leaks."""
@@ -316,12 +316,12 @@ def test_memory_leak_prevention(self):
final_stats = get_memory_stats()
# The Registry should not have grown excessively
- registry_growth = final_stats['registry_size'] - initial_stats['registry_size']
+ registry_growth = final_stats["registry_size"] - initial_stats["registry_size"]
assert registry_growth <= 20, f"Registry grew by {registry_growth}, possible memory leak"
# Cache sizes should be reasonable
- assert final_stats['formatter_cache_size'] <= 50
- assert final_stats['directory_cache_size'] <= 500
+ assert final_stats["formatter_cache_size"] <= 50
+ assert final_stats["directory_cache_size"] <= 500
@pytest.mark.skipif(
sys.platform == "win32",
@@ -494,7 +494,7 @@ def test_set_directory_cache_limit_edge_cases(self):
finally:
# Cleanup using context managers
- for temp_dir, temp_dir_context in temp_dirs:
+ for _temp_dir, temp_dir_context in temp_dirs:
temp_dir_context.__exit__(None, None, None)
def test_register_logger_weakref_direct(self):
@@ -639,13 +639,13 @@ def test_memory_stats_comprehensive(self):
# Verify all required fields exist
required_fields = [
- 'registry_size',
- 'formatter_cache_size',
- 'directory_cache_size',
- 'active_logger_count',
- 'max_registry_size',
- 'max_formatter_cache',
- 'max_directory_cache',
+ "registry_size",
+ "formatter_cache_size",
+ "directory_cache_size",
+ "active_logger_count",
+ "max_registry_size",
+ "max_formatter_cache",
+ "max_directory_cache",
]
for field in required_fields:
@@ -654,9 +654,9 @@ def test_memory_stats_comprehensive(self):
assert stats[field] >= 0, f"Field {field} should be non-negative"
# Verify relationships
- assert stats['registry_size'] <= stats['max_registry_size']
- assert stats['formatter_cache_size'] <= stats['max_formatter_cache']
- assert stats['directory_cache_size'] <= stats['max_directory_cache']
+ assert stats["registry_size"] <= stats["max_registry_size"]
+ assert stats["formatter_cache_size"] <= stats["max_formatter_cache"]
+ assert stats["directory_cache_size"] <= stats["max_directory_cache"]
def test_force_garbage_collection_comprehensive(self):
"""Test comprehensive garbage collection functionality."""
@@ -665,13 +665,13 @@ def test_force_garbage_collection_comprehensive(self):
# Create objects that could be garbage collected
test_objects = []
for i in range(100):
- test_objects.append({'data': f"test_data_{i}" * 100, 'nested': {'value': i, 'list': list(range(10))}})
+ test_objects.append({"data": f"test_data_{i}" * 100, "nested": {"value": i, "list": list(range(10))}})
# Create circular references
- obj1 = {'name': 'obj1'}
- obj2 = {'name': 'obj2'}
- obj1['ref'] = obj2
- obj2['ref'] = obj1
+ obj1 = {"name": "obj1"}
+ obj2 = {"name": "obj2"}
+ obj1["ref"] = obj2
+ obj2["ref"] = obj1
test_objects.extend([obj1, obj2])
# Clear references
@@ -681,14 +681,14 @@ def test_force_garbage_collection_comprehensive(self):
gc_stats = force_garbage_collection()
# Verify stats
- assert 'objects_collected' in gc_stats
- assert 'garbage_count' in gc_stats
- assert 'reference_cycles' in gc_stats
+ assert "objects_collected" in gc_stats
+ assert "garbage_count" in gc_stats
+ assert "reference_cycles" in gc_stats
- assert isinstance(gc_stats['objects_collected'], int)
- assert isinstance(gc_stats['garbage_count'], int)
- assert gc_stats['objects_collected'] >= 0
- assert gc_stats['garbage_count'] >= 0
+ assert isinstance(gc_stats["objects_collected"], int)
+ assert isinstance(gc_stats["garbage_count"], int)
+ assert gc_stats["objects_collected"] >= 0
+ assert gc_stats["garbage_count"] >= 0
def test_memory_optimization_integration(self):
"""Test integration of all memory optimization features."""
@@ -723,20 +723,20 @@ def test_memory_optimization_integration(self):
final_stats = get_memory_stats()
# Verify optimization worked
- assert final_stats['formatter_cache_size'] == 0 # Should be cleared
- assert final_stats['directory_cache_size'] == 0 # Should be cleared
- assert gc_result['objects_collected'] >= 0
+ assert final_stats["formatter_cache_size"] == 0 # Should be cleared
+ assert final_stats["directory_cache_size"] == 0 # Should be cleared
+ assert gc_result["objects_collected"] >= 0
def test_memory_utils_module_constants(self):
"""Test module-level constants and their behavior."""
from pythonLogs.core import memory_utils
# Verify module constants exist and have reasonable values
- assert hasattr(memory_utils, '_formatter_cache')
- assert hasattr(memory_utils, '_formatter_cache_lock')
- assert hasattr(memory_utils, '_max_formatters')
- assert hasattr(memory_utils, '_active_loggers')
- assert hasattr(memory_utils, '_weak_ref_lock')
+ assert hasattr(memory_utils, "_formatter_cache")
+ assert hasattr(memory_utils, "_formatter_cache_lock")
+ assert hasattr(memory_utils, "_max_formatters")
+ assert hasattr(memory_utils, "_active_loggers")
+ assert hasattr(memory_utils, "_weak_ref_lock")
# Verify default values
assert memory_utils._max_formatters > 0
diff --git a/tests/performance/test_performance.py b/tests/performance/test_performance.py
index 1a4dc63..a7bb469 100644
--- a/tests/performance/test_performance.py
+++ b/tests/performance/test_performance.py
@@ -26,7 +26,7 @@
from tests.core.test_log_utils import get_safe_timezone
-@pytest.mark.skipif(os.getenv('CI') == 'true', reason="Performance tests unstable in CI")
+@pytest.mark.skipif(os.getenv("CI") == "true", reason="Performance tests unstable in CI")
class TestPerformance:
"""Performance tests for factory pattern and optimizations."""
@@ -65,7 +65,7 @@ def test_registry_caching_performance(self):
# With caching: Reuse same logger
clear_logger_registry()
start_time = time.time()
- for i in range(30):
+ for _i in range(30):
LoggerFactory.get_or_create_logger(LoggerType.BASIC, name="cached_logger")
cache_time = time.time() - start_time
@@ -97,7 +97,8 @@ def test_directory_permission_caching(self):
start_time = time.time()
for i in range(10):
logger = SizeRotatingLog(
- name=f"dir_test_{i+2}", directory=temp_dir # The Same directory should use cache
+ name=f"dir_test_{i + 2}",
+ directory=temp_dir, # The Same directory should use cache
)
subsequent_calls_time = time.time() - start_time
diff --git a/tests/performance/test_performance_windows.py b/tests/performance/test_performance_windows.py
index 6f539db..118f944 100644
--- a/tests/performance/test_performance_windows.py
+++ b/tests/performance/test_performance_windows.py
@@ -24,7 +24,7 @@
from tests.core.test_log_utils import windows_safe_temp_directory
-@pytest.mark.skipif(os.getenv('CI') == 'true', reason="Performance tests unstable in CI")
+@pytest.mark.skipif(os.getenv("CI") == "true", reason="Performance tests unstable in CI")
class TestPerformanceWindows:
"""Windows-specific performance tests for factory pattern and optimizations."""
@@ -45,7 +45,7 @@ def test_directory_permission_caching_windows(self):
start_time = time.perf_counter()
for i in range(10):
logger = SizeRotatingLog(
- name=f"dir_test_{i+2}_win",
+ name=f"dir_test_{i + 2}_win",
directory=temp_dir, # The Same directory should use cache
)
subsequent_calls_time = time.perf_counter() - start_time
diff --git a/tests/performance/test_performance_zoneinfo.py b/tests/performance/test_performance_zoneinfo.py
index 77490ba..2728bfd 100644
--- a/tests/performance/test_performance_zoneinfo.py
+++ b/tests/performance/test_performance_zoneinfo.py
@@ -18,7 +18,7 @@
from pythonLogs.core.factory import clear_logger_registry
-@pytest.mark.skipif(os.getenv('CI') == 'true', reason="Performance tests unstable in CI")
+@pytest.mark.skipif(os.getenv("CI") == "true", reason="Performance tests unstable in CI")
class TestZoneinfoPerformance:
"""Performance tests for zoneinfo timezone operations."""
diff --git a/tests/smoke_test.py b/tests/smoke_test.py
new file mode 100644
index 0000000..01ab688
--- /dev/null
+++ b/tests/smoke_test.py
@@ -0,0 +1,10 @@
+"""Smoke test to verify the built package works correctly."""
+
+from pythonLogs import BasicLog, SizeRotatingLog, TimedRotatingLog, __version__
+
+assert __version__, "Version should not be empty"
+assert BasicLog, "BasicLog should be importable"
+assert TimedRotatingLog, "TimedRotatingLog should be importable"
+assert SizeRotatingLog, "SizeRotatingLog should be importable"
+
+print(f"pythonLogs {__version__} OK")
diff --git a/tests/thread_safety/test_automatic_features.py b/tests/thread_safety/test_automatic_features.py
index 7728a29..cee3eb4 100644
--- a/tests/thread_safety/test_automatic_features.py
+++ b/tests/thread_safety/test_automatic_features.py
@@ -30,7 +30,7 @@ def test_logger_operations():
# Run in multiple threads to test thread safety
threads = []
- for i in range(5):
+ for _i in range(5):
thread = threading.Thread(target=test_logger_operations)
threads.append(thread)
thread.start()
@@ -70,7 +70,7 @@ def test_logger_operations():
# Run in multiple threads to test thread safety
threads = []
- for i in range(3):
+ for _i in range(3):
thread = threading.Thread(target=test_logger_operations)
threads.append(thread)
thread.start()
@@ -106,7 +106,7 @@ def test_logger_operations():
# Run in multiple threads to test thread safety
threads = []
- for i in range(3):
+ for _i in range(3):
thread = threading.Thread(target=test_logger_operations)
threads.append(thread)
thread.start()
@@ -138,13 +138,13 @@ def test_automatic_features_verification(self):
assert logger is not None
# 2. Automatic Resource Cleanup: Context manager support
- assert hasattr(basic_log, '__enter__')
- assert hasattr(basic_log, '__exit__')
- assert hasattr(basic_log, 'cleanup_logger')
+ assert hasattr(basic_log, "__enter__")
+ assert hasattr(basic_log, "__exit__")
+ assert hasattr(basic_log, "cleanup_logger")
# 3. Automatic Thread Safety: Decorator applied
- assert hasattr(basic_log.__class__, '_lock')
- assert hasattr(basic_log.init, '_thread_safe_wrapped')
+ assert hasattr(basic_log.__class__, "_lock")
+ assert hasattr(basic_log.init, "_thread_safe_wrapped")
# cleanup_logger is a static method, so it's not wrapped
BasicLog.cleanup_logger(logger)
diff --git a/tests/thread_safety/test_automatic_thread_safety.py b/tests/thread_safety/test_automatic_thread_safety.py
index 6c60672..1b00455 100644
--- a/tests/thread_safety/test_automatic_thread_safety.py
+++ b/tests/thread_safety/test_automatic_thread_safety.py
@@ -124,8 +124,8 @@ def test_automatic_locking_verification(self):
basic_log = BasicLog(name="test_lock_verification")
# Verify the class has the automatic thread safety decorator applied
- assert hasattr(basic_log.__class__, '_lock'), "Class should have automatic lock"
- assert hasattr(basic_log.init, '_thread_safe_wrapped'), "Method should be wrapped for thread safety"
+ assert hasattr(basic_log.__class__, "_lock"), "Class should have automatic lock"
+ assert hasattr(basic_log.init, "_thread_safe_wrapped"), "Method should be wrapped for thread safety"
# cleanup_logger is a static method, so it's not wrapped with thread safety
# Test that methods can still be called normally
diff --git a/tests/thread_safety/test_thread_safety.py b/tests/thread_safety/test_thread_safety.py
index d8f62cf..5b62466 100644
--- a/tests/thread_safety/test_thread_safety.py
+++ b/tests/thread_safety/test_thread_safety.py
@@ -63,7 +63,7 @@ def create_logger_worker():
def test_concurrent_registry_operations(self):
"""Test concurrent registry operations (create, shutdown, clear)."""
num_threads = 20
- results = {'created': [], 'shutdown': [], 'errors': []}
+ results = {"created": [], "shutdown": [], "errors": []}
def mixed_operations_worker(worker_id):
"""Worker that performs mixed registry operations."""
@@ -72,17 +72,17 @@ def mixed_operations_worker(worker_id):
# Create logger
logger = LoggerFactory.get_or_create_logger(LoggerType.BASIC, name=logger_name, level=LogLevel.DEBUG)
- results['created'].append(logger_name)
+ results["created"].append(logger_name)
# Small delay to increase chance of race conditions
time.sleep(0.01)
# Try to shut down logger
if LoggerFactory.shutdown_logger(logger_name):
- results['shutdown'].append(logger_name)
+ results["shutdown"].append(logger_name)
except Exception as e:
- results['errors'].append(str(e))
+ results["errors"].append(str(e))
# Run concurrent operations
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
@@ -91,10 +91,10 @@ def mixed_operations_worker(worker_id):
future.result()
# No errors should occur
- assert len(results['errors']) == 0, f"Errors occurred: {results['errors']}"
+ assert len(results["errors"]) == 0, f"Errors occurred: {results['errors']}"
# All created loggers should be accounted for
- assert len(results['created']) == num_threads
+ assert len(results["created"]) == num_threads
# Registry should be consistent
registry = LoggerFactory.get_registered_loggers()
@@ -193,7 +193,7 @@ def test_stress_test_factory_pattern(self):
num_threads = 50
operations_per_thread = 10
logger_names = [f"stress_logger_{i}" for i in range(5)] # Shared logger names
- results = {'success': 0, 'errors': []}
+ results = {"success": 0, "errors": []}
results_lock = threading.Lock()
def stress_worker():
@@ -219,11 +219,11 @@ def stress_worker():
time.sleep(0.001)
with results_lock:
- results['success'] += operations_per_thread
+ results["success"] += operations_per_thread
except Exception as e:
with results_lock:
- results['errors'].append(str(e))
+ results["errors"].append(str(e))
# Run stress test
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
@@ -233,8 +233,8 @@ def stress_worker():
# Verify results
expected_operations = num_threads * operations_per_thread
- assert results['success'] == expected_operations, f"Expected {expected_operations}, got {results['success']}"
- assert len(results['errors']) == 0, f"Stress test errors: {results['errors']}"
+ assert results["success"] == expected_operations, f"Expected {expected_operations}, got {results['success']}"
+ assert len(results["errors"]) == 0, f"Stress test errors: {results['errors']}"
# Registry may have evicted some loggers due to memory limits or TTL
# Just verify it has at least some loggers and doesn't exceed the total
@@ -361,7 +361,7 @@ def _create_logger_and_messages(self, worker_id, temp_dir, results_lock, thread_
log_file = os.path.join(temp_dir, f"independent_{worker_id}.log")
assert os.path.exists(log_file), f"Log file missing for thread {worker_id}"
- with open(log_file, 'r') as f:
+ with open(log_file) as f:
_log_content = f.read()
# Verify all messages are in the file
@@ -369,29 +369,29 @@ def _create_logger_and_messages(self, worker_id, temp_dir, results_lock, thread_
assert _message in _log_content
with results_lock:
- thread_results[worker_id] = {'messages': messages, 'log_content': _log_content}
+ thread_results[worker_id] = {"messages": messages, "log_content": _log_content}
def _verify_thread_results(self, thread_results, num_threads):
"""Helper to verify all thread results are successful."""
for worker_id in range(num_threads):
assert worker_id in thread_results
assert (
- 'error' not in thread_results[worker_id]
+ "error" not in thread_results[worker_id]
), f"Thread {worker_id} failed: {thread_results[worker_id].get('error')}"
- assert 'messages' in thread_results[worker_id]
- assert len(thread_results[worker_id]['messages']) == 10
+ assert "messages" in thread_results[worker_id]
+ assert len(thread_results[worker_id]["messages"]) == 10
def _check_worker_log_isolation(self, worker_id, log_content, thread_results, num_threads):
"""Check that a worker's log doesn't contain messages from other workers."""
for other_id in range(num_threads):
if other_id != worker_id:
- for message in thread_results[other_id]['messages']:
+ for message in thread_results[other_id]["messages"]:
assert message not in log_content, f"Thread {worker_id} log contains message from thread {other_id}"
def _verify_no_cross_contamination(self, thread_results, num_threads):
"""Helper to verify no cross-contamination between thread logs."""
for worker_id in range(num_threads):
- log_content = thread_results[worker_id]['log_content']
+ log_content = thread_results[worker_id]["log_content"]
self._check_worker_log_isolation(worker_id, log_content, thread_results, num_threads)
@pytest.mark.skipif(
@@ -412,7 +412,7 @@ def independent_worker(worker_id):
except Exception as e:
with results_lock:
- thread_results[worker_id] = {'error': str(e)}
+ thread_results[worker_id] = {"error": str(e)}
# Run independent workers
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
diff --git a/tests/thread_safety/test_thread_safety_module.py b/tests/thread_safety/test_thread_safety_module.py
index 562c4fc..a429ddd 100644
--- a/tests/thread_safety/test_thread_safety_module.py
+++ b/tests/thread_safety/test_thread_safety_module.py
@@ -67,7 +67,7 @@ def increment(self):
assert obj.counter == 1
# The lock should be accessible via the method's fallback mechanism
- lock = getattr(obj, '_lock', None) or getattr(obj.__class__, '_lock', None)
+ lock = getattr(obj, "_lock", None) or getattr(obj.__class__, "_lock", None)
assert lock is not None
def test_thread_safe_decorator_preserves_metadata(self):
@@ -86,8 +86,8 @@ def test_method(self, arg1, arg2=None):
method = obj.test_method
# Check that wrapper preserves original function name and docstring
- assert method.__name__ == 'test_method'
- assert 'Test method docstring' in method.__doc__
+ assert method.__name__ == "test_method"
+ assert "Test method docstring" in method.__doc__
assert method(1, arg2=2) == "1-2"
@@ -97,7 +97,7 @@ class TestAutoThreadSafeDecorator:
def test_auto_thread_safe_specific_methods(self):
"""Test @auto_thread_safe with specific method list."""
- @auto_thread_safe(['increment'])
+ @auto_thread_safe(["increment"])
class TestClass:
def __init__(self):
self.counter = 0
@@ -113,9 +113,9 @@ def unsafe_increment(self):
obj = TestClass()
# Check that the specified method is wrapped
- assert hasattr(obj.increment, '_thread_safe_wrapped')
+ assert hasattr(obj.increment, "_thread_safe_wrapped")
# Check that non-specified method is not wrapped
- assert not hasattr(obj.unsafe_increment, '_thread_safe_wrapped')
+ assert not hasattr(obj.unsafe_increment, "_thread_safe_wrapped")
# Test thread safety of wrapped method
threads = []
@@ -152,15 +152,15 @@ def _private_method(self):
obj = TestClass()
# Public methods should be wrapped
- assert hasattr(obj.increment, '_thread_safe_wrapped')
- assert hasattr(obj.decrement, '_thread_safe_wrapped')
+ assert hasattr(obj.increment, "_thread_safe_wrapped")
+ assert hasattr(obj.decrement, "_thread_safe_wrapped")
# Private method should not be wrapped
- assert not hasattr(obj._private_method, '_thread_safe_wrapped')
+ assert not hasattr(obj._private_method, "_thread_safe_wrapped")
def test_auto_thread_safe_no_double_wrapping(self):
"""Test that methods are not wrapped multiple times."""
- @auto_thread_safe(['test_method'])
+ @auto_thread_safe(["test_method"])
class TestClass:
def test_method(self):
return "test"
@@ -168,7 +168,7 @@ def test_method(self):
obj = TestClass()
# Apply decorator again (should not double-wrap)
- wrapped_test_class = auto_thread_safe(['test_method'])(TestClass)
+ wrapped_test_class = auto_thread_safe(["test_method"])(TestClass)
obj2 = wrapped_test_class()
# Should still work and not be double-wrapped
@@ -182,7 +182,7 @@ def test_thread_safe_meta_basic(self):
"""Test basic ThreadSafeMeta functionality."""
class TestClass(metaclass=ThreadSafeMeta):
- _thread_safe_methods = ['increment']
+ _thread_safe_methods = ["increment"]
def __init__(self):
self.counter = 0
@@ -198,7 +198,7 @@ def unsafe_method(self):
obj = TestClass()
# Should have class-level lock
- assert hasattr(obj.__class__, '_lock')
+ assert hasattr(obj.__class__, "_lock")
# Test that increment method works
obj.increment()
assert obj.counter == 1
@@ -219,7 +219,7 @@ def _private_method(self):
obj = TestClass()
# Should have class-level lock
- assert hasattr(obj.__class__, '_lock')
+ assert hasattr(obj.__class__, "_lock")
# Test that methods work
assert obj.public_method() == "public"
assert obj._private_method() == "private"
@@ -244,9 +244,9 @@ def increment(self):
obj = TestClass()
# Should have instance lock
- assert hasattr(obj, '_lock')
+ assert hasattr(obj, "_lock")
# Public method should be wrapped
- assert hasattr(obj.increment, '_thread_safe_wrapped')
+ assert hasattr(obj.increment, "_thread_safe_wrapped")
# Test thread safety
threads = []
@@ -282,8 +282,8 @@ def derived_method(self):
obj = DerivedClass()
# Both base and derived methods should be thread-safe
- assert hasattr(obj.base_method, '_thread_safe_wrapped')
- assert hasattr(obj.derived_method, '_thread_safe_wrapped')
+ assert hasattr(obj.base_method, "_thread_safe_wrapped")
+ assert hasattr(obj.derived_method, "_thread_safe_wrapped")
class TestSynchronizedMethodDecorator:
@@ -364,7 +364,7 @@ class TestEdgeCases:
def test_thread_safe_with_static_methods(self):
"""Test thread safety with static methods."""
- @auto_thread_safe(['regular_method'])
+ @auto_thread_safe(["regular_method"])
class TestClass:
counter = 0
@@ -378,14 +378,14 @@ def static_method():
obj = TestClass()
# Regular method should be wrapped
- assert hasattr(obj.regular_method, '_thread_safe_wrapped')
+ assert hasattr(obj.regular_method, "_thread_safe_wrapped")
# Static method should not be affected
assert TestClass.static_method() == "static"
def test_thread_safe_with_class_methods(self):
"""Test thread safety with class methods."""
- @auto_thread_safe(['regular_method'])
+ @auto_thread_safe(["regular_method"])
class TestClass:
counter = 0
@@ -399,14 +399,14 @@ def class_method(cls):
obj = TestClass()
# Regular method should be wrapped
- assert hasattr(obj.regular_method, '_thread_safe_wrapped')
+ assert hasattr(obj.regular_method, "_thread_safe_wrapped")
# Class method should work normally
assert TestClass.class_method() == "class"
def test_thread_safe_with_properties(self):
"""Test thread safety with properties."""
- @auto_thread_safe(['set_value'])
+ @auto_thread_safe(["set_value"])
class TestClass:
def __init__(self):
self._value = 0
@@ -437,8 +437,8 @@ def increment(self):
self.counter += 1
# Apply auto_thread_safe multiple times
- wrapped_test_class = auto_thread_safe(['increment'])(TestClass)
- wrapped_test_class = auto_thread_safe(['increment'])(wrapped_test_class)
+ wrapped_test_class = auto_thread_safe(["increment"])(TestClass)
+ wrapped_test_class = auto_thread_safe(["increment"])(wrapped_test_class)
obj = wrapped_test_class()
obj.increment()
diff --git a/tests/thread_safety/test_thread_safety_patterns.py b/tests/thread_safety/test_thread_safety_patterns.py
index 9e3083e..6d20120 100644
--- a/tests/thread_safety/test_thread_safety_patterns.py
+++ b/tests/thread_safety/test_thread_safety_patterns.py
@@ -97,7 +97,7 @@ def write(self, key, value):
def get_stats(self):
with self._lock:
- return {'reads': self.read_count, 'writes': self.write_count, 'data_size': len(self.data)}
+ return {"reads": self.read_count, "writes": self.write_count, "data_size": len(self.data)}
store = ThreadSafeDataStore()
@@ -132,9 +132,9 @@ def reader(keys):
future.result()
stats = store.get_stats()
- assert stats['writes'] == 50
- assert stats['data_size'] == 50
- assert stats['reads'] > 0 # Some reads should have occurred
+ assert stats["writes"] == 50
+ assert stats["data_size"] == 50
+ assert stats["reads"] > 0 # Some reads should have occurred
def test_singleton_pattern_thread_safety(self):
"""Test thread-safe singleton pattern."""
@@ -217,9 +217,9 @@ def return_resource(self, resource):
def stats(self):
with self._lock:
return {
- 'available': len(self.available),
- 'in_use': len(self.in_use),
- 'total': len(self.available) + len(self.in_use),
+ "available": len(self.available),
+ "in_use": len(self.in_use),
+ "total": len(self.available) + len(self.in_use),
}
# Create a simple resource (just a counter)
@@ -248,9 +248,9 @@ def worker(worker_id):
future.result()
stats = pool.stats()
- assert stats['total'] == 3 # Pool size maintained
- assert stats['available'] == 3 # All resources returned
- assert stats['in_use'] == 0 # No resources stuck
+ assert stats["total"] == 3 # Pool size maintained
+ assert stats["available"] == 3 # All resources returned
+ assert stats["in_use"] == 0 # No resources stuck
assert len(completed_tasks) == 8 # All workers completed
def test_cache_with_expiry_thread_safety(self):
@@ -363,9 +363,9 @@ def publish(self, event_type, data):
def get_stats(self):
with self._lock:
return {
- 'subscriber_count': sum(len(subs) for subs in self.subscribers.values()),
- 'event_types': len(self.subscribers),
- 'events_published': dict(self.event_count),
+ "subscriber_count": sum(len(subs) for subs in self.subscribers.values()),
+ "event_types": len(self.subscribers),
+ "events_published": dict(self.event_count),
}
event_bus = ThreadSafeEventBus()
@@ -418,9 +418,9 @@ def subscriber(event_type):
assert len(type_a_events) > 0 # type_A had 2 subscribers
assert len(type_b_events) > 0 # type_B had 1 subscriber
- assert stats['events_published']['type_A'] == 10
- assert stats['events_published']['type_B'] == 5
- assert stats['events_published']['type_C'] == 3
+ assert stats["events_published"]["type_A"] == 10
+ assert stats["events_published"]["type_B"] == 5
+ assert stats["events_published"]["type_C"] == 3
def test_weak_reference_cleanup_thread_safety(self):
"""Test thread safety with weak references and cleanup."""
diff --git a/tests/timezone/test_timezone_migration.py b/tests/timezone/test_timezone_migration.py
index 9ca8791..529f034 100644
--- a/tests/timezone/test_timezone_migration.py
+++ b/tests/timezone/test_timezone_migration.py
@@ -132,12 +132,12 @@ def test_timezone_offset_calculation(self):
# UTC should return +0000, but may fall back to localtime on Windows
assert isinstance(utc_offset, str)
assert len(utc_offset) == 5
- assert utc_offset[0] in ['+', '-']
+ assert utc_offset[0] in ["+", "-"]
# Test localtime
local_offset = get_timezone_offset("localtime")
assert len(local_offset) == 5 # Format: ±HHMM
- assert local_offset[0] in ['+', '-']
+ assert local_offset[0] in ["+", "-"]
def test_timezone_function_caching(self):
"""Test that timezone functions are properly cached."""
diff --git a/tests/timezone/test_zoneinfo_fallbacks.py b/tests/timezone/test_zoneinfo_fallbacks.py
index 80151b8..b70c945 100644
--- a/tests/timezone/test_zoneinfo_fallbacks.py
+++ b/tests/timezone/test_zoneinfo_fallbacks.py
@@ -33,7 +33,9 @@ def test_timezone_error_handling(self):
# With the new fallback system, invalid timezones should gracefully fall back
# to localtime instead of raising exceptions for better robustness
logger = BasicLog(
- name="error_test", timezone="NonExistent/Timezone", level=LogLevel.INFO # Should fall back to localtime
+ name="error_test",
+ timezone="NonExistent/Timezone",
+ level=LogLevel.INFO, # Should fall back to localtime
)
# Logger should be created successfully with fallback
assert logger.name == "error_test"
@@ -48,13 +50,13 @@ def test_timezone_offset_edge_cases(self):
# UTC should return +0000, but may fall back to localtime on Windows
assert isinstance(utc_offset, str)
assert len(utc_offset) == 5
- assert utc_offset[0] in ['+', '-']
+ assert utc_offset[0] in ["+", "-"]
# Test localtime (should work on any system)
local_offset = get_timezone_offset("localtime")
assert isinstance(local_offset, str)
assert len(local_offset) == 5
- assert local_offset[0] in ['+', '-']
+ assert local_offset[0] in ["+", "-"]
# Test case insensitivity for localtime
local_offset_upper = get_timezone_offset("LOCALTIME")
@@ -67,7 +69,7 @@ def test_stderr_timezone_fallback(self):
from pythonLogs.core.log_utils import write_stderr
# Mock environment variable
- with patch.dict(os.environ, {'LOG_TIMEZONE': 'UTC'}):
+ with patch.dict(os.environ, {"LOG_TIMEZONE": "UTC"}):
stderr_capture = io.StringIO()
with redirect_stderr(stderr_capture):
write_stderr("Test message")
@@ -151,7 +153,7 @@ def test_environment_variable_timezone_handling(self):
"""Test timezone handling through environment variables."""
# Test with environment variable
- with patch.dict(os.environ, {'LOG_TIMEZONE': 'Europe/Paris'}):
+ with patch.dict(os.environ, {"LOG_TIMEZONE": "Europe/Paris"}):
# Environment variable should be used for stderr
from pythonLogs.core.log_utils import get_stderr_timezone
@@ -235,11 +237,11 @@ def test_timezone_validation_edge_cases(self):
# For localtime, just check format
assert isinstance(result, str)
assert len(result) == 5
- assert result[0] in ['+', '-']
+ assert result[0] in ["+", "-"]
# Test that invalid timezone names now fall back gracefully to localtime
result = get_timezone_offset("invalid_timezone")
# Should fall back to localtime format
assert isinstance(result, str)
assert len(result) == 5
- assert result[0] in ['+', '-']
+ assert result[0] in ["+", "-"]
diff --git a/uv.lock b/uv.lock
index 77ef293..8553f02 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,6 +1,6 @@
version = 1
revision = 3
-requires-python = ">=3.12"
+requires-python = ">=3.11"
[[package]]
name = "annotated-types"
@@ -11,6 +11,55 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
+[[package]]
+name = "black"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "mypy-extensions" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "platformdirs" },
+ { name = "pytokens" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" },
+ { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" },
+ { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" },
+ { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" },
+ { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" },
+ { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
[[package]]
name = "colorama"
version = "0.4.6"
@@ -22,76 +71,106 @@ wheels = [
[[package]]
name = "coverage"
-version = "7.13.2"
+version = "7.13.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" },
- { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" },
- { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" },
- { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" },
- { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" },
- { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" },
- { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" },
- { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" },
- { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" },
- { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" },
- { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" },
- { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" },
- { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" },
- { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" },
- { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" },
- { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" },
- { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" },
- { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" },
- { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" },
- { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" },
- { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" },
- { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" },
- { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" },
- { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" },
- { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" },
- { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" },
- { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" },
- { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" },
- { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" },
- { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" },
- { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" },
- { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" },
- { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" },
- { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" },
- { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" },
- { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" },
- { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" },
- { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" },
- { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" },
- { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" },
- { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" },
- { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" },
- { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" },
- { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" },
- { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" },
- { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" },
- { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" },
- { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" },
- { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" },
- { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" },
- { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" },
- { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" },
- { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" },
- { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" },
- { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" },
- { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" },
- { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" },
- { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" },
- { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" },
- { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" },
- { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" },
- { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" },
- { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" },
- { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" },
- { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" },
- { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" },
+ { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" },
+ { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" },
+ { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" },
+ { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" },
+ { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" },
+ { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
+ { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
+ { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
+ { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
+ { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
+ { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
+ { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
+ { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
+ { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
+ { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
+ { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
+ { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
+ { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
+ { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
+ { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
+ { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
+ { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
+ { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
+ { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
+ { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
+ { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
+ { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
+ { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
+ { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
+ { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
+ { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
+ { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
+ { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
+ { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
+ { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
+ { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
+ { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
@@ -103,6 +182,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
[[package]]
name = "packaging"
version = "26.0"
@@ -121,6 +209,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" },
]
+[[package]]
+name = "pathspec"
+version = "1.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
+]
+
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -132,15 +238,15 @@ wheels = [
[[package]]
name = "poethepoet"
-version = "0.40.0"
+version = "0.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pastel" },
{ name = "pyyaml" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/38/9d/054c8435b03324ed9abd5d5ab8c45065b1f42c23952cd23f13a5921d8465/poethepoet-0.40.0.tar.gz", hash = "sha256:91835f00d03d6c4f0e146f80fa510e298ad865e7edd27fe4cb9c94fdc090791b", size = 81114, upload-time = "2026-01-05T19:09:13.116Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a8/b9/fa92286560f70eaa40d473ea48376d20c6c21f63627d33c6bb1c5e385175/poethepoet-0.41.0.tar.gz", hash = "sha256:dcaad621dc061f6a90b17d091bebb9ca043d67bfe9bd6aa4185aea3ebf7ff3e6", size = 87780, upload-time = "2026-02-08T20:45:36.061Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/bc/73327d12b176abea7a3c6c7d760e1a953992f7b59d72c0354e39d7a353b5/poethepoet-0.40.0-py3-none-any.whl", hash = "sha256:afd276ae31d5c53573c0c14898118d4848ccee3709b6b0be6a1c6cbe522bbc8a", size = 106672, upload-time = "2026-01-05T19:09:11.536Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/5e/0b83e0222ce5921b3f9081eeca8c6fb3e1cfd5ca0d06338adf93b28ce061/poethepoet-0.41.0-py3-none-any.whl", hash = "sha256:4bab9fd8271664c5d21407e8f12827daeb6aa484dc6cc7620f0c3b4e62b42ee4", size = 113590, upload-time = "2026-02-08T20:45:34.697Z" },
]
[[package]]
@@ -195,6 +301,20 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
@@ -251,10 +371,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
]
[[package]]
@@ -301,7 +433,7 @@ name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "coverage" },
+ { name = "coverage", extra = ["toml"] },
{ name = "pluggy" },
{ name = "pytest" },
]
@@ -321,29 +453,66 @@ wheels = [
[[package]]
name = "pythonlogs"
-version = "6.0.1"
+version = "6.0.2"
source = { editable = "." }
dependencies = [
{ name = "pydantic-settings" },
]
-[package.optional-dependencies]
-test = [
+[package.dev-dependencies]
+dev = [
+ { name = "black" },
{ name = "poethepoet" },
{ name = "psutil" },
- { name = "pytest" },
{ name = "pytest-cov" },
+ { name = "ruff" },
]
[package.metadata]
-requires-dist = [
- { name = "poethepoet", marker = "extra == 'test'", specifier = ">=0.40.0" },
- { name = "psutil", marker = "extra == 'test'", specifier = ">=7.2.2" },
- { name = "pydantic-settings", specifier = ">=2.11.0" },
- { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2" },
- { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=7.0.0" },
+requires-dist = [{ name = "pydantic-settings", specifier = ">=2.11.0" }]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "black", specifier = ">=26.1.0" },
+ { name = "poethepoet", specifier = ">=0.41.0" },
+ { name = "psutil", specifier = ">=7.2.2" },
+ { name = "pytest-cov", specifier = ">=7.0.0" },
+ { name = "ruff", specifier = ">=0.15.0" },
+]
+
+[[package]]
+name = "pytokens"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" },
+ { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" },
+ { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" },
+ { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" },
+ { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" },
+ { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" },
+ { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" },
+ { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" },
+ { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" },
+ { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" },
+ { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
]
-provides-extras = ["test"]
[[package]]
name = "pyyaml"
@@ -351,6 +520,15 @@ version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
@@ -391,6 +569,85 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
+[[package]]
+name = "ruff"
+version = "0.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" },
+ { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" },
+ { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" },
+ { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" },
+ { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"