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.
@@ -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
+