exonware-xwlazy 0.1.0.10__py3-none-any.whl → 0.1.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exonware/xwlazy/__init__.py +0 -0
- exonware/xwlazy/version.py +2 -2
- exonware_xwlazy-0.1.0.11.dist-info/METADATA +380 -0
- exonware_xwlazy-0.1.0.11.dist-info/RECORD +20 -0
- xwlazy/__init__.py +34 -0
- xwlazy/lazy/__init__.py +301 -0
- xwlazy/lazy/bootstrap.py +106 -0
- xwlazy/lazy/config.py +163 -0
- xwlazy/lazy/host_conf.py +279 -0
- xwlazy/lazy/host_packages.py +122 -0
- xwlazy/lazy/lazy_base.py +465 -0
- xwlazy/lazy/lazy_contracts.py +290 -0
- xwlazy/lazy/lazy_core.py +3727 -0
- xwlazy/lazy/lazy_errors.py +271 -0
- xwlazy/lazy/lazy_state.py +86 -0
- xwlazy/lazy/logging_utils.py +194 -0
- xwlazy/lazy/manifest.py +489 -0
- xwlazy/version.py +77 -0
- exonware_xwlazy-0.1.0.10.dist-info/METADATA +0 -0
- exonware_xwlazy-0.1.0.10.dist-info/RECORD +0 -6
- {exonware_xwlazy-0.1.0.10.dist-info → exonware_xwlazy-0.1.0.11.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.10.dist-info → exonware_xwlazy-0.1.0.11.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
#exonware/xwsystem/src/exonware/xwsystem/utils/lazy_package/lazy_errors.py
|
|
3
|
+
|
|
4
|
+
Company: eXonware.com
|
|
5
|
+
Author: Eng. Muhammad AlShehri
|
|
6
|
+
Email: connect@exonware.com
|
|
7
|
+
Version: 0.1.0.16
|
|
8
|
+
Generation Date: 10-Oct-2025
|
|
9
|
+
|
|
10
|
+
Errors for Lazy Loading System
|
|
11
|
+
|
|
12
|
+
This module defines all exception classes for the lazy loading system
|
|
13
|
+
following DEV_GUIDELINES.md structure.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Optional, Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# =============================================================================
|
|
20
|
+
# BASE EXCEPTION
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
class LazySystemError(Exception):
|
|
24
|
+
"""
|
|
25
|
+
Base exception for all lazy system errors.
|
|
26
|
+
|
|
27
|
+
All lazy system exceptions inherit from this for easy error handling.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, package_name: Optional[str] = None):
|
|
31
|
+
"""
|
|
32
|
+
Initialize lazy system error.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
message: Error message
|
|
36
|
+
package_name: Optional package name for scoped errors
|
|
37
|
+
"""
|
|
38
|
+
self.package_name = package_name
|
|
39
|
+
if package_name:
|
|
40
|
+
message = f"[{package_name}] {message}"
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# SPECIFIC EXCEPTIONS
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
class LazyInstallError(LazySystemError):
|
|
49
|
+
"""
|
|
50
|
+
Raised when package installation fails.
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
- pip install command fails
|
|
54
|
+
- Package not found in PyPI
|
|
55
|
+
- Network error during installation
|
|
56
|
+
"""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LazyDiscoveryError(LazySystemError):
|
|
61
|
+
"""
|
|
62
|
+
Raised when dependency discovery fails.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
- Cannot read pyproject.toml
|
|
66
|
+
- Invalid TOML syntax
|
|
67
|
+
- Missing dependency configuration
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LazyHookError(LazySystemError):
|
|
73
|
+
"""
|
|
74
|
+
Raised when import hook operation fails.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
- Cannot install hook in sys.meta_path
|
|
78
|
+
- Hook is already installed
|
|
79
|
+
- Hook interception fails
|
|
80
|
+
"""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class LazySecurityError(LazySystemError):
|
|
85
|
+
"""
|
|
86
|
+
Raised when security policy is violated.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
- Package not in allow list
|
|
90
|
+
- Package in deny list
|
|
91
|
+
- Untrusted package source
|
|
92
|
+
"""
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ExternallyManagedError(LazyInstallError):
|
|
97
|
+
"""
|
|
98
|
+
Raised when environment is externally managed (PEP 668).
|
|
99
|
+
|
|
100
|
+
This happens when the Python environment has an EXTERNALLY-MANAGED
|
|
101
|
+
marker file, preventing pip installations. Common in system Python
|
|
102
|
+
installations on Linux distributions.
|
|
103
|
+
|
|
104
|
+
Solutions:
|
|
105
|
+
1. Use a virtual environment
|
|
106
|
+
2. Use pipx for isolated installations
|
|
107
|
+
3. Override with --break-system-packages (not recommended)
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, package_name: str):
|
|
111
|
+
"""
|
|
112
|
+
Initialize externally managed error.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
package_name: Package that cannot be installed
|
|
116
|
+
"""
|
|
117
|
+
message = (
|
|
118
|
+
f"Cannot install '{package_name}': Environment is externally managed (PEP 668). "
|
|
119
|
+
f"Please use a virtual environment or pipx."
|
|
120
|
+
)
|
|
121
|
+
super().__init__(message, package_name=None)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class DeferredImportError(Exception):
|
|
125
|
+
"""
|
|
126
|
+
Placeholder for a failed import that will be retried when accessed.
|
|
127
|
+
|
|
128
|
+
This enables two-stage lazy loading:
|
|
129
|
+
- Stage 1: Import fails → Return DeferredImportError placeholder
|
|
130
|
+
- Stage 2: On first use → Install missing package and replace with real module
|
|
131
|
+
|
|
132
|
+
Performance optimized:
|
|
133
|
+
- Zero overhead until user actually accesses the deferred import
|
|
134
|
+
- Only installs dependencies when truly needed
|
|
135
|
+
- Caches successful imports to avoid repeated installs
|
|
136
|
+
|
|
137
|
+
Note: This is both an error class and a proxy object. It stays in
|
|
138
|
+
lazy_errors.py because it represents an error state, but acts as
|
|
139
|
+
a proxy until resolved.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
__slots__ = (
|
|
143
|
+
'_import_name',
|
|
144
|
+
'_original_error',
|
|
145
|
+
'_installer_package',
|
|
146
|
+
'_retry_attempted',
|
|
147
|
+
'_real_module',
|
|
148
|
+
'_async_handle',
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
import_name: str,
|
|
154
|
+
original_error: Exception,
|
|
155
|
+
installer_package: str,
|
|
156
|
+
async_handle: Optional[Any] = None,
|
|
157
|
+
):
|
|
158
|
+
"""
|
|
159
|
+
Initialize deferred import placeholder.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
import_name: Name of the module that failed to import (e.g., 'fastavro')
|
|
163
|
+
original_error: The original ImportError that was caught
|
|
164
|
+
installer_package: Package name to use for lazy installation (e.g., 'xwsystem')
|
|
165
|
+
"""
|
|
166
|
+
self._import_name = import_name
|
|
167
|
+
self._original_error = original_error
|
|
168
|
+
self._installer_package = installer_package
|
|
169
|
+
self._retry_attempted = False
|
|
170
|
+
self._real_module = None
|
|
171
|
+
self._async_handle = async_handle
|
|
172
|
+
super().__init__(f"Deferred import: {import_name}")
|
|
173
|
+
|
|
174
|
+
def _try_install_and_import(self):
|
|
175
|
+
"""
|
|
176
|
+
Attempt to install missing package and import it.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The real module if installation succeeds
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
Original ImportError if installation fails or is disabled
|
|
183
|
+
"""
|
|
184
|
+
# Import here to avoid circular imports
|
|
185
|
+
from .lazy_core import lazy_import_with_install, is_lazy_install_enabled
|
|
186
|
+
from .logging_utils import get_logger
|
|
187
|
+
|
|
188
|
+
logger = get_logger("xwlazy.lazy")
|
|
189
|
+
logger.info(f"[STAGE 2] _try_install_and_import called for '{self._import_name}'")
|
|
190
|
+
|
|
191
|
+
# Return cached module if already installed
|
|
192
|
+
if self._real_module is not None:
|
|
193
|
+
logger.info(f"[STAGE 2] Using cached module for '{self._import_name}'")
|
|
194
|
+
return self._real_module
|
|
195
|
+
|
|
196
|
+
# Only try once to avoid repeated failures
|
|
197
|
+
if self._retry_attempted:
|
|
198
|
+
logger.warning(f"[STAGE 2] Already attempted installation for '{self._import_name}', raising original error")
|
|
199
|
+
raise self._original_error
|
|
200
|
+
|
|
201
|
+
self._retry_attempted = True
|
|
202
|
+
|
|
203
|
+
if self._async_handle is not None:
|
|
204
|
+
logger.info(f"[STAGE 2] Waiting for async install of '{self._import_name}' to finish")
|
|
205
|
+
self._async_handle.wait()
|
|
206
|
+
|
|
207
|
+
if not is_lazy_install_enabled(self._installer_package):
|
|
208
|
+
logger.warning(f"[STAGE 2] Lazy install disabled for {self._installer_package}, cannot load {self._import_name}")
|
|
209
|
+
raise self._original_error
|
|
210
|
+
|
|
211
|
+
logger.info(f"⏳ [STAGE 2] Installing '{self._import_name}' on first use...")
|
|
212
|
+
|
|
213
|
+
# Try to install and import
|
|
214
|
+
module, success = lazy_import_with_install(
|
|
215
|
+
self._import_name,
|
|
216
|
+
installer_package=self._installer_package
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if success and module:
|
|
220
|
+
self._real_module = module
|
|
221
|
+
logger.info(f"✅ [STAGE 2] Successfully installed and loaded '{self._import_name}'")
|
|
222
|
+
return module
|
|
223
|
+
else:
|
|
224
|
+
logger.error(f"❌ [STAGE 2] Failed to install '{self._import_name}'")
|
|
225
|
+
raise self._original_error
|
|
226
|
+
|
|
227
|
+
def __call__(self, *args, **kwargs):
|
|
228
|
+
"""
|
|
229
|
+
When user tries to instantiate, install dependency first.
|
|
230
|
+
|
|
231
|
+
This enables: serializer = AvroSerializer() → installs fastavro → creates instance
|
|
232
|
+
"""
|
|
233
|
+
module = self._try_install_and_import()
|
|
234
|
+
# If module is callable (a class), instantiate it
|
|
235
|
+
if callable(module):
|
|
236
|
+
return module(*args, **kwargs)
|
|
237
|
+
return module
|
|
238
|
+
|
|
239
|
+
def __getattr__(self, name):
|
|
240
|
+
"""
|
|
241
|
+
When user accesses attributes, install dependency first.
|
|
242
|
+
|
|
243
|
+
This enables: from fastavro import reader → installs fastavro → returns reader
|
|
244
|
+
"""
|
|
245
|
+
module = self._try_install_and_import()
|
|
246
|
+
return getattr(module, name)
|
|
247
|
+
|
|
248
|
+
def __repr__(self):
|
|
249
|
+
"""Show helpful message about deferred import."""
|
|
250
|
+
if self._real_module is not None:
|
|
251
|
+
return f"<DeferredImport: {self._import_name} (loaded)>"
|
|
252
|
+
return f"<DeferredImport: {self._import_name} (will install on first use)>"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# =============================================================================
|
|
256
|
+
# EXPORT ALL
|
|
257
|
+
# =============================================================================
|
|
258
|
+
|
|
259
|
+
__all__ = [
|
|
260
|
+
# Base exception
|
|
261
|
+
'LazySystemError',
|
|
262
|
+
# Specific exceptions
|
|
263
|
+
'LazyInstallError',
|
|
264
|
+
'LazyDiscoveryError',
|
|
265
|
+
'LazyHookError',
|
|
266
|
+
'LazySecurityError',
|
|
267
|
+
'ExternallyManagedError',
|
|
268
|
+
# Two-stage loading
|
|
269
|
+
'DeferredImportError',
|
|
270
|
+
]
|
|
271
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_base_config_dir() -> Path:
|
|
10
|
+
"""Determine a cross-platform directory for storing lazy configuration."""
|
|
11
|
+
if os.name == "nt":
|
|
12
|
+
appdata = os.getenv("APPDATA")
|
|
13
|
+
if appdata:
|
|
14
|
+
return Path(appdata) / "exonware" / "lazy"
|
|
15
|
+
return Path.home() / "AppData" / "Roaming" / "exonware" / "lazy"
|
|
16
|
+
|
|
17
|
+
# POSIX-style
|
|
18
|
+
xdg_config = os.getenv("XDG_CONFIG_HOME")
|
|
19
|
+
if xdg_config:
|
|
20
|
+
return Path(xdg_config) / "exonware" / "lazy"
|
|
21
|
+
return Path.home() / ".config" / "exonware" / "lazy"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LazyStateManager:
|
|
25
|
+
"""Persist and retrieve lazy installation state."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, package_name: str) -> None:
|
|
28
|
+
self._package = package_name.lower()
|
|
29
|
+
self._state_path = _get_base_config_dir() / "state.json"
|
|
30
|
+
self._state: Dict[str, Dict[str, bool]] = self._load_state()
|
|
31
|
+
|
|
32
|
+
# --------------------------------------------------------------------- #
|
|
33
|
+
# Persistence helpers
|
|
34
|
+
# --------------------------------------------------------------------- #
|
|
35
|
+
def _load_state(self) -> Dict[str, Dict[str, bool]]:
|
|
36
|
+
if not self._state_path.exists():
|
|
37
|
+
return {}
|
|
38
|
+
try:
|
|
39
|
+
with self._state_path.open("r", encoding="utf-8") as fh:
|
|
40
|
+
data = json.load(fh)
|
|
41
|
+
if isinstance(data, dict):
|
|
42
|
+
return data
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
def _save_state(self) -> None:
|
|
48
|
+
self._state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
with self._state_path.open("w", encoding="utf-8") as fh:
|
|
50
|
+
json.dump(self._state, fh, indent=2, sort_keys=True)
|
|
51
|
+
|
|
52
|
+
def _ensure_entry(self) -> Dict[str, bool]:
|
|
53
|
+
return self._state.setdefault(self._package, {})
|
|
54
|
+
|
|
55
|
+
# --------------------------------------------------------------------- #
|
|
56
|
+
# Manual state management
|
|
57
|
+
# --------------------------------------------------------------------- #
|
|
58
|
+
def get_manual_state(self) -> Optional[bool]:
|
|
59
|
+
entry = self._state.get(self._package, {})
|
|
60
|
+
value = entry.get("manual")
|
|
61
|
+
return bool(value) if isinstance(value, bool) else None
|
|
62
|
+
|
|
63
|
+
def set_manual_state(self, value: Optional[bool]) -> None:
|
|
64
|
+
entry = self._ensure_entry()
|
|
65
|
+
if value is None:
|
|
66
|
+
entry.pop("manual", None)
|
|
67
|
+
else:
|
|
68
|
+
entry["manual"] = bool(value)
|
|
69
|
+
self._save_state()
|
|
70
|
+
|
|
71
|
+
# --------------------------------------------------------------------- #
|
|
72
|
+
# Auto detection cache
|
|
73
|
+
# --------------------------------------------------------------------- #
|
|
74
|
+
def get_cached_auto_state(self) -> Optional[bool]:
|
|
75
|
+
entry = self._state.get(self._package, {})
|
|
76
|
+
value = entry.get("auto")
|
|
77
|
+
return bool(value) if isinstance(value, bool) else None
|
|
78
|
+
|
|
79
|
+
def set_auto_state(self, value: Optional[bool]) -> None:
|
|
80
|
+
entry = self._ensure_entry()
|
|
81
|
+
if value is None:
|
|
82
|
+
entry.pop("auto", None)
|
|
83
|
+
else:
|
|
84
|
+
entry["auto"] = bool(value)
|
|
85
|
+
self._save_state()
|
|
86
|
+
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#exonware/xwlazy/src/exonware/xwlazy/lazy/logging_utils.py
|
|
2
|
+
"""
|
|
3
|
+
Lightweight logging helper for the xwlazy lazy subsystem.
|
|
4
|
+
|
|
5
|
+
Adds category-based filtering so noisy traces (hook/install/audit/etc.) can be
|
|
6
|
+
turned on or off individually via configuration or environment variables.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Dict, Optional
|
|
16
|
+
|
|
17
|
+
_configured = False
|
|
18
|
+
|
|
19
|
+
_CATEGORY_DEFAULTS: Dict[str, bool] = {
|
|
20
|
+
"install": True, # always show installs by default
|
|
21
|
+
"hook": False,
|
|
22
|
+
"enhance": False,
|
|
23
|
+
"audit": False,
|
|
24
|
+
"sbom": False,
|
|
25
|
+
"config": False,
|
|
26
|
+
"discovery": False,
|
|
27
|
+
}
|
|
28
|
+
_category_overrides: Dict[str, bool] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_category(name: str) -> str:
|
|
32
|
+
return name.strip().lower()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load_env_overrides() -> None:
|
|
36
|
+
for category in _CATEGORY_DEFAULTS:
|
|
37
|
+
env_key = f"XWLAZY_LOG_{category.upper()}"
|
|
38
|
+
env_val = os.getenv(env_key)
|
|
39
|
+
if env_val is None:
|
|
40
|
+
continue
|
|
41
|
+
enabled = env_val.strip().lower() not in {"0", "false", "off", "no"}
|
|
42
|
+
_category_overrides[_normalize_category(category)] = enabled
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class XWLazyFormatter(logging.Formatter):
|
|
46
|
+
"""Custom formatter for xwlazy that uses exonware.xwlazy [HH:MM:SS]: [FLAG] format."""
|
|
47
|
+
|
|
48
|
+
# Map logging levels to flags
|
|
49
|
+
LEVEL_FLAGS = {
|
|
50
|
+
logging.DEBUG: "DEBUG",
|
|
51
|
+
logging.INFO: "INFO",
|
|
52
|
+
logging.WARNING: "WARN",
|
|
53
|
+
logging.ERROR: "ERROR",
|
|
54
|
+
logging.CRITICAL: "CRITICAL",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Map flags to emojis
|
|
58
|
+
EMOJI_MAP = {
|
|
59
|
+
"WARN": "⚠️",
|
|
60
|
+
"INFO": "ℹ️",
|
|
61
|
+
"ACTION": "⚙️",
|
|
62
|
+
"SUCCESS": "✅",
|
|
63
|
+
"ERROR": "❌",
|
|
64
|
+
"FAIL": "⛔",
|
|
65
|
+
"DEBUG": "🔍",
|
|
66
|
+
"CRITICAL": "🚨",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
70
|
+
"""Format log record with custom format."""
|
|
71
|
+
# Get flag from level or use INFO as default
|
|
72
|
+
flag = self.LEVEL_FLAGS.get(record.levelno, "INFO")
|
|
73
|
+
|
|
74
|
+
# Get emoji for flag
|
|
75
|
+
emoji = self.EMOJI_MAP.get(flag, "ℹ️")
|
|
76
|
+
|
|
77
|
+
# Format time as HH:MM:SS
|
|
78
|
+
time_str = datetime.now().strftime("%H:%M:%S")
|
|
79
|
+
|
|
80
|
+
# Format message
|
|
81
|
+
message = record.getMessage()
|
|
82
|
+
|
|
83
|
+
# Return formatted: emoji exonware.xwlazy [HH:MM:SS]: [FLAG] message
|
|
84
|
+
return f"{emoji} exonware.xwlazy [{time_str}]: [{flag}] {message}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _ensure_basic_config() -> None:
|
|
88
|
+
global _configured
|
|
89
|
+
if _configured:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Configure root logger with custom formatter
|
|
93
|
+
root_logger = logging.getLogger()
|
|
94
|
+
root_logger.setLevel(logging.INFO)
|
|
95
|
+
|
|
96
|
+
# Remove existing handlers to avoid duplicates
|
|
97
|
+
for handler in root_logger.handlers[:]:
|
|
98
|
+
root_logger.removeHandler(handler)
|
|
99
|
+
|
|
100
|
+
# Create console handler with custom formatter
|
|
101
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
102
|
+
console_handler.setLevel(logging.INFO)
|
|
103
|
+
console_handler.setFormatter(XWLazyFormatter())
|
|
104
|
+
root_logger.addHandler(console_handler)
|
|
105
|
+
|
|
106
|
+
_load_env_overrides()
|
|
107
|
+
_configured = True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
|
111
|
+
"""Return a logger configured for the lazy subsystem."""
|
|
112
|
+
_ensure_basic_config()
|
|
113
|
+
return logging.getLogger(name or "xwlazy.lazy")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def is_log_category_enabled(category: str) -> bool:
|
|
117
|
+
"""Return True if the provided log category is enabled."""
|
|
118
|
+
_ensure_basic_config()
|
|
119
|
+
normalized = _normalize_category(category)
|
|
120
|
+
if normalized in _category_overrides:
|
|
121
|
+
return _category_overrides[normalized]
|
|
122
|
+
return _CATEGORY_DEFAULTS.get(normalized, True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def set_log_category(category: str, enabled: bool) -> None:
|
|
126
|
+
"""Enable/disable an individual log category at runtime."""
|
|
127
|
+
_category_overrides[_normalize_category(category)] = bool(enabled)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def set_log_categories(overrides: Dict[str, bool]) -> None:
|
|
131
|
+
"""Bulk update multiple categories."""
|
|
132
|
+
for category, enabled in overrides.items():
|
|
133
|
+
set_log_category(category, enabled)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_log_categories() -> Dict[str, bool]:
|
|
137
|
+
"""Return the effective state for each built-in log category."""
|
|
138
|
+
_ensure_basic_config()
|
|
139
|
+
result = {}
|
|
140
|
+
for category, default_enabled in _CATEGORY_DEFAULTS.items():
|
|
141
|
+
normalized = _normalize_category(category)
|
|
142
|
+
result[category] = _category_overrides.get(normalized, default_enabled)
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def log_event(category: str, level_fn, msg: str, *args, **kwargs) -> None:
|
|
147
|
+
"""Emit a log for the given category if it is enabled."""
|
|
148
|
+
if is_log_category_enabled(category):
|
|
149
|
+
level_fn(msg, *args, **kwargs)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def format_message(flag: str, message: str) -> str:
|
|
153
|
+
"""
|
|
154
|
+
Format a message with exonware.xwlazy [HH:MM:SS]: [FLAG] format.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
flag: Message flag (WARN, ACTION, SUCCESS, etc.)
|
|
158
|
+
message: Message content
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Formatted message string
|
|
162
|
+
"""
|
|
163
|
+
# Map flags to emojis
|
|
164
|
+
emoji_map = {
|
|
165
|
+
"WARN": "⚠️",
|
|
166
|
+
"INFO": "ℹ️",
|
|
167
|
+
"ACTION": "⚙️",
|
|
168
|
+
"SUCCESS": "✅",
|
|
169
|
+
"ERROR": "❌",
|
|
170
|
+
"FAIL": "⛔",
|
|
171
|
+
"DEBUG": "🔍",
|
|
172
|
+
"CRITICAL": "🚨",
|
|
173
|
+
}
|
|
174
|
+
emoji = emoji_map.get(flag, "ℹ️")
|
|
175
|
+
time_str = datetime.now().strftime("%H:%M:%S")
|
|
176
|
+
return f"{emoji} exonware.xwlazy [{time_str}]: [{flag}] {message}"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def print_formatted(flag: str, message: str, same_line: bool = False) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Print a formatted message with optional same-line support.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
flag: Message flag (WARN, ACTION, SUCCESS, etc.)
|
|
185
|
+
message: Message content
|
|
186
|
+
same_line: If True, use \r to overwrite previous line
|
|
187
|
+
"""
|
|
188
|
+
formatted = format_message(flag, message)
|
|
189
|
+
if same_line:
|
|
190
|
+
sys.stdout.write(f"\r{formatted}")
|
|
191
|
+
sys.stdout.flush()
|
|
192
|
+
else:
|
|
193
|
+
print(formatted)
|
|
194
|
+
|