unctools 0.1.0__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.
- unctools/__init__.py +100 -0
- unctools/converter.py +378 -0
- unctools/detector.py +531 -0
- unctools/operations.py +562 -0
- unctools/utils/__init__.py +40 -0
- unctools/utils/compat.py +331 -0
- unctools/utils/logger.py +228 -0
- unctools/utils/validation.py +321 -0
- unctools/windows/__init__.py +45 -0
- unctools/windows/network.py +490 -0
- unctools/windows/registry.py +410 -0
- unctools/windows/security.py +586 -0
- unctools-0.1.0.dist-info/METADATA +189 -0
- unctools-0.1.0.dist-info/RECORD +17 -0
- unctools-0.1.0.dist-info/WHEEL +5 -0
- unctools-0.1.0.dist-info/licenses/LICENSE +21 -0
- unctools-0.1.0.dist-info/top_level.txt +1 -0
unctools/utils/compat.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-platform compatibility utilities for UNCtools.
|
|
3
|
+
|
|
4
|
+
This module provides functions and utilities for ensuring consistent behavior
|
|
5
|
+
across different operating systems and platforms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import platform
|
|
11
|
+
import logging
|
|
12
|
+
import importlib.util
|
|
13
|
+
from typing import Dict, Tuple, List, Optional, Union, Any
|
|
14
|
+
|
|
15
|
+
# Set up module-level logger
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
def is_windows() -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Check if running on Windows.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if running on Windows, False otherwise.
|
|
24
|
+
"""
|
|
25
|
+
return os.name == 'nt'
|
|
26
|
+
|
|
27
|
+
def is_linux() -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Check if running on Linux.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if running on Linux, False otherwise.
|
|
33
|
+
"""
|
|
34
|
+
return platform.system() == 'Linux'
|
|
35
|
+
|
|
36
|
+
def is_macos() -> bool:
|
|
37
|
+
"""
|
|
38
|
+
Check if running on macOS.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if running on macOS, False otherwise.
|
|
42
|
+
"""
|
|
43
|
+
return platform.system() == 'Darwin'
|
|
44
|
+
|
|
45
|
+
def get_platform_info() -> Dict[str, str]:
|
|
46
|
+
"""
|
|
47
|
+
Get detailed information about the current platform.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
A dictionary with platform information.
|
|
51
|
+
"""
|
|
52
|
+
info = {
|
|
53
|
+
'system': platform.system(),
|
|
54
|
+
'release': platform.release(),
|
|
55
|
+
'version': platform.version(),
|
|
56
|
+
'architecture': platform.machine(),
|
|
57
|
+
'processor': platform.processor(),
|
|
58
|
+
'python_version': platform.python_version(),
|
|
59
|
+
'python_implementation': platform.python_implementation(),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Add Windows-specific information if available
|
|
63
|
+
if is_windows():
|
|
64
|
+
try:
|
|
65
|
+
import winreg
|
|
66
|
+
# Get Windows edition
|
|
67
|
+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion") as key:
|
|
68
|
+
info['windows_edition'] = winreg.QueryValueEx(key, "ProductName")[0]
|
|
69
|
+
info['windows_build'] = winreg.QueryValueEx(key, "CurrentBuildNumber")[0]
|
|
70
|
+
try:
|
|
71
|
+
info['windows_display_version'] = winreg.QueryValueEx(key, "DisplayVersion")[0]
|
|
72
|
+
except:
|
|
73
|
+
pass # Not available on older Windows versions
|
|
74
|
+
except:
|
|
75
|
+
logger.debug("Could not retrieve detailed Windows information")
|
|
76
|
+
|
|
77
|
+
return info
|
|
78
|
+
|
|
79
|
+
def is_module_available(module_name: str) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Check if a module is available without importing it or triggering warnings.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
module_name: Name of the module to check
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if the module is available, False otherwise
|
|
88
|
+
"""
|
|
89
|
+
spec = importlib.util.find_spec(module_name)
|
|
90
|
+
return spec is not None
|
|
91
|
+
|
|
92
|
+
def safe_import(module_name: str, default=None):
|
|
93
|
+
"""
|
|
94
|
+
Safely import a module, returning a default value if the import fails.
|
|
95
|
+
|
|
96
|
+
This is useful for optional dependencies where you want to gracefully
|
|
97
|
+
handle missing modules without warnings or errors.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
module_name: Name of the module to import
|
|
101
|
+
default: Default value to return if the import fails (default: None)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The imported module if successful, or the default value if the import fails
|
|
105
|
+
"""
|
|
106
|
+
if is_module_available(module_name):
|
|
107
|
+
try:
|
|
108
|
+
return importlib.import_module(module_name)
|
|
109
|
+
except ImportError:
|
|
110
|
+
pass
|
|
111
|
+
return default
|
|
112
|
+
|
|
113
|
+
def path_separator() -> str:
|
|
114
|
+
"""
|
|
115
|
+
Get the path separator for the current platform.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
The path separator character ('\\' on Windows, '/' elsewhere).
|
|
119
|
+
"""
|
|
120
|
+
return '\\' if is_windows() else '/'
|
|
121
|
+
|
|
122
|
+
def normalize_path_separators(path: str) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Normalize path separators for the current platform.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
path: The path to normalize.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The path with normalized separators.
|
|
131
|
+
"""
|
|
132
|
+
separator = path_separator()
|
|
133
|
+
if separator == '\\':
|
|
134
|
+
# On Windows, convert forward slashes to backslashes
|
|
135
|
+
return path.replace('/', '\\')
|
|
136
|
+
else:
|
|
137
|
+
# On Unix-like systems, convert backslashes to forward slashes
|
|
138
|
+
return path.replace('\\', '/')
|
|
139
|
+
|
|
140
|
+
def get_home_directory() -> str:
|
|
141
|
+
"""
|
|
142
|
+
Get the user's home directory in a cross-platform way.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
The path to the home directory.
|
|
146
|
+
"""
|
|
147
|
+
return os.path.expanduser("~")
|
|
148
|
+
|
|
149
|
+
def get_temp_directory() -> str:
|
|
150
|
+
"""
|
|
151
|
+
Get the system's temporary directory in a cross-platform way.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
The path to the temporary directory.
|
|
155
|
+
"""
|
|
156
|
+
return os.path.normpath(os.path.abspath(os.environ.get('TEMP') or
|
|
157
|
+
os.environ.get('TMP') or
|
|
158
|
+
os.path.join(os.path.expanduser("~"), '.tmp')))
|
|
159
|
+
|
|
160
|
+
def get_app_data_directory(app_name: str = "unctools") -> str:
|
|
161
|
+
"""
|
|
162
|
+
Get the application data directory in a cross-platform way.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
app_name: The name of the application.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The path to the application data directory.
|
|
169
|
+
"""
|
|
170
|
+
if is_windows():
|
|
171
|
+
# On Windows, use %APPDATA%
|
|
172
|
+
base_dir = os.environ.get('APPDATA', os.path.expanduser("~"))
|
|
173
|
+
elif is_macos():
|
|
174
|
+
# On macOS, use ~/Library/Application Support
|
|
175
|
+
base_dir = os.path.join(os.path.expanduser("~"), 'Library', 'Application Support')
|
|
176
|
+
else:
|
|
177
|
+
# On Linux and other platforms, use ~/.config
|
|
178
|
+
base_dir = os.path.join(os.path.expanduser("~"), '.config')
|
|
179
|
+
|
|
180
|
+
app_dir = os.path.join(base_dir, app_name)
|
|
181
|
+
|
|
182
|
+
# Ensure the directory exists
|
|
183
|
+
os.makedirs(app_dir, exist_ok=True)
|
|
184
|
+
|
|
185
|
+
return app_dir
|
|
186
|
+
|
|
187
|
+
def get_long_path_prefix() -> str:
|
|
188
|
+
"""
|
|
189
|
+
Get the prefix for accessing long paths on Windows.
|
|
190
|
+
|
|
191
|
+
On Windows, paths longer than MAX_PATH (260 characters) require a special prefix.
|
|
192
|
+
This function returns the appropriate prefix for the current platform.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The long path prefix ('\\\\?\\' on Windows, empty string elsewhere).
|
|
196
|
+
"""
|
|
197
|
+
if is_windows():
|
|
198
|
+
return '\\\\?\\'
|
|
199
|
+
return ''
|
|
200
|
+
|
|
201
|
+
def apply_long_path_prefix(path: str) -> str:
|
|
202
|
+
"""
|
|
203
|
+
Apply the long path prefix to a path if necessary.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
path: The path to modify.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
The path with the long path prefix if needed.
|
|
210
|
+
"""
|
|
211
|
+
if is_windows() and not path.startswith('\\\\?\\'):
|
|
212
|
+
# Check if path is already in UNC format
|
|
213
|
+
if path.startswith('\\\\'):
|
|
214
|
+
# For UNC paths, use \\?\UNC\server\share
|
|
215
|
+
return '\\\\?\\UNC\\' + path[2:]
|
|
216
|
+
else:
|
|
217
|
+
# For regular paths, just add \\?\
|
|
218
|
+
return '\\\\?\\' + path
|
|
219
|
+
return path
|
|
220
|
+
|
|
221
|
+
def supports_symlinks() -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Check if the current platform supports symbolic links.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if symbolic links are supported, False otherwise.
|
|
227
|
+
"""
|
|
228
|
+
if is_windows():
|
|
229
|
+
# On Windows, symlinks are available in Vista+ but require extra privileges
|
|
230
|
+
try:
|
|
231
|
+
# Check Windows version
|
|
232
|
+
windows_version = tuple(map(int, platform.version().split('.')))
|
|
233
|
+
if windows_version >= (6, 0): # Vista+
|
|
234
|
+
# Check for administrator privileges
|
|
235
|
+
import ctypes
|
|
236
|
+
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
|
237
|
+
return False
|
|
238
|
+
except:
|
|
239
|
+
return False
|
|
240
|
+
else:
|
|
241
|
+
# On Unix-like systems, symlinks are generally available
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
def has_admin_privileges() -> bool:
|
|
245
|
+
"""
|
|
246
|
+
Check if the current process has administrator/root privileges.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
True if the process has admin privileges, False otherwise.
|
|
250
|
+
"""
|
|
251
|
+
if is_windows():
|
|
252
|
+
try:
|
|
253
|
+
import ctypes
|
|
254
|
+
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
|
255
|
+
except:
|
|
256
|
+
return False
|
|
257
|
+
else:
|
|
258
|
+
# On Unix-like systems, check for root (UID 0)
|
|
259
|
+
return os.geteuid() == 0 if hasattr(os, 'geteuid') else False
|
|
260
|
+
|
|
261
|
+
def path_exists_case_sensitive(path: str) -> bool:
|
|
262
|
+
"""
|
|
263
|
+
Check if a path exists with case sensitivity.
|
|
264
|
+
|
|
265
|
+
On Windows, this function will verify that the case of the path matches
|
|
266
|
+
the case of the actual file system entry, which can be useful when working
|
|
267
|
+
across platforms.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
path: The path to check.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
True if the path exists with the same case, False otherwise.
|
|
274
|
+
"""
|
|
275
|
+
if not os.path.exists(path):
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
if is_windows():
|
|
279
|
+
# Windows is case-insensitive but case-preserving
|
|
280
|
+
# We need to get the actual case from the file system
|
|
281
|
+
try:
|
|
282
|
+
import win32file
|
|
283
|
+
# Get the normalized path with long path prefix
|
|
284
|
+
norm_path = apply_long_path_prefix(os.path.normpath(path))
|
|
285
|
+
# Get the actual case from the file system
|
|
286
|
+
actual_path = win32file.GetLongPathName(win32file.GetShortPathName(norm_path))
|
|
287
|
+
# Remove the long path prefix if it was added
|
|
288
|
+
if actual_path.startswith('\\\\?\\'):
|
|
289
|
+
actual_path = actual_path[4:]
|
|
290
|
+
# Compare the lower-case versions to ignore case differences
|
|
291
|
+
return os.path.normpath(path).lower() == actual_path.lower()
|
|
292
|
+
except:
|
|
293
|
+
# If we can't get the actual case, just check existence
|
|
294
|
+
return True
|
|
295
|
+
else:
|
|
296
|
+
# On Unix-like systems, paths are case-sensitive
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
def get_case_sensitive_path(path: str) -> str:
|
|
300
|
+
"""
|
|
301
|
+
Get the case-sensitive version of a path.
|
|
302
|
+
|
|
303
|
+
On Windows, this function will return the path with the correct case as
|
|
304
|
+
stored in the file system. On other platforms, it returns the path unchanged.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
path: The path to convert.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
The case-sensitive path.
|
|
311
|
+
"""
|
|
312
|
+
if not os.path.exists(path):
|
|
313
|
+
return path
|
|
314
|
+
|
|
315
|
+
if is_windows():
|
|
316
|
+
try:
|
|
317
|
+
import win32file
|
|
318
|
+
# Get the normalized path with long path prefix
|
|
319
|
+
norm_path = apply_long_path_prefix(os.path.normpath(path))
|
|
320
|
+
# Get the actual case from the file system
|
|
321
|
+
actual_path = win32file.GetLongPathName(win32file.GetShortPathName(norm_path))
|
|
322
|
+
# Remove the long path prefix if it was added
|
|
323
|
+
if actual_path.startswith('\\\\?\\'):
|
|
324
|
+
actual_path = actual_path[4:]
|
|
325
|
+
return actual_path
|
|
326
|
+
except:
|
|
327
|
+
# If we can't get the actual case, return the path unchanged
|
|
328
|
+
return path
|
|
329
|
+
else:
|
|
330
|
+
# On Unix-like systems, paths are already case-sensitive
|
|
331
|
+
return path
|
unctools/utils/logger.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging utilities for UNCtools.
|
|
3
|
+
|
|
4
|
+
This module provides functions for configuring and using logging throughout
|
|
5
|
+
the UNCtools library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import logging
|
|
11
|
+
import datetime
|
|
12
|
+
from typing import Optional, Union, Dict, Any, List, TextIO
|
|
13
|
+
|
|
14
|
+
# Default log format
|
|
15
|
+
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
16
|
+
|
|
17
|
+
# Default date format for log timestamps
|
|
18
|
+
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
19
|
+
|
|
20
|
+
# Default log level
|
|
21
|
+
DEFAULT_LOG_LEVEL = logging.INFO
|
|
22
|
+
|
|
23
|
+
# Package logger
|
|
24
|
+
logger = logging.getLogger("unctools")
|
|
25
|
+
|
|
26
|
+
class UNCLogFormatter(logging.Formatter):
|
|
27
|
+
"""
|
|
28
|
+
Custom log formatter for UNCtools.
|
|
29
|
+
|
|
30
|
+
This formatter adds additional context information to log entries
|
|
31
|
+
including thread/process information for concurrent operations.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, fmt: Optional[str] = None, datefmt: Optional[str] = None):
|
|
35
|
+
"""
|
|
36
|
+
Initialize the formatter.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
fmt: Log format string.
|
|
40
|
+
datefmt: Date format string.
|
|
41
|
+
"""
|
|
42
|
+
if fmt is None:
|
|
43
|
+
fmt = DEFAULT_LOG_FORMAT
|
|
44
|
+
if datefmt is None:
|
|
45
|
+
datefmt = DEFAULT_DATE_FORMAT
|
|
46
|
+
super().__init__(fmt=fmt, datefmt=datefmt)
|
|
47
|
+
|
|
48
|
+
def formatException(self, ei) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Format an exception with enhanced details.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
ei: Exception information tuple.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Formatted exception string.
|
|
57
|
+
"""
|
|
58
|
+
# Get the standard exception text
|
|
59
|
+
result = super().formatException(ei)
|
|
60
|
+
# Add custom context information
|
|
61
|
+
return f"{result}\nLogger: {logger.name}"
|
|
62
|
+
|
|
63
|
+
def configure_logging(
|
|
64
|
+
level: int = DEFAULT_LOG_LEVEL,
|
|
65
|
+
format_str: str = DEFAULT_LOG_FORMAT,
|
|
66
|
+
date_format: str = DEFAULT_DATE_FORMAT,
|
|
67
|
+
log_file: Optional[str] = None,
|
|
68
|
+
log_to_console: bool = True,
|
|
69
|
+
log_to_file: bool = False,
|
|
70
|
+
max_bytes: int = 10485760, # 10MB
|
|
71
|
+
backup_count: int = 5,
|
|
72
|
+
propagate: bool = True
|
|
73
|
+
) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Configure logging for UNCtools.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
level: Logging level (default: logging.INFO).
|
|
79
|
+
format_str: Log format string.
|
|
80
|
+
date_format: Date format for log timestamps.
|
|
81
|
+
log_file: Path to log file (if log_to_file is True).
|
|
82
|
+
log_to_console: Whether to log to console.
|
|
83
|
+
log_to_file: Whether to log to a file.
|
|
84
|
+
max_bytes: Maximum log file size before rotation.
|
|
85
|
+
backup_count: Number of backup log files to keep.
|
|
86
|
+
propagate: Whether to propagate logs to the root logger.
|
|
87
|
+
"""
|
|
88
|
+
# Get the package logger
|
|
89
|
+
unc_logger = logging.getLogger("unctools")
|
|
90
|
+
|
|
91
|
+
# Reset handlers
|
|
92
|
+
for handler in unc_logger.handlers[:]:
|
|
93
|
+
unc_logger.removeHandler(handler)
|
|
94
|
+
|
|
95
|
+
# Set log level
|
|
96
|
+
unc_logger.setLevel(level)
|
|
97
|
+
|
|
98
|
+
# Create formatter
|
|
99
|
+
formatter = UNCLogFormatter(format_str, date_format)
|
|
100
|
+
|
|
101
|
+
# Add console handler if requested
|
|
102
|
+
if log_to_console:
|
|
103
|
+
console_handler = logging.StreamHandler()
|
|
104
|
+
console_handler.setFormatter(formatter)
|
|
105
|
+
unc_logger.addHandler(console_handler)
|
|
106
|
+
|
|
107
|
+
# Add file handler if requested
|
|
108
|
+
if log_to_file:
|
|
109
|
+
if log_file is None:
|
|
110
|
+
# Default log file in user's home directory
|
|
111
|
+
log_dir = os.path.join(os.path.expanduser("~"), ".unctools", "logs")
|
|
112
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
113
|
+
|
|
114
|
+
# Use date in filename
|
|
115
|
+
date_str = datetime.datetime.now().strftime("%Y%m%d")
|
|
116
|
+
log_file = os.path.join(log_dir, f"unctools_{date_str}.log")
|
|
117
|
+
|
|
118
|
+
# Create directory if needed
|
|
119
|
+
log_dir = os.path.dirname(log_file)
|
|
120
|
+
if log_dir:
|
|
121
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
# Use rotating file handler for log rotation
|
|
125
|
+
from logging.handlers import RotatingFileHandler
|
|
126
|
+
file_handler = RotatingFileHandler(
|
|
127
|
+
log_file, maxBytes=max_bytes, backupCount=backup_count
|
|
128
|
+
)
|
|
129
|
+
file_handler.setFormatter(formatter)
|
|
130
|
+
unc_logger.addHandler(file_handler)
|
|
131
|
+
|
|
132
|
+
logger.info(f"Logging to file: {log_file}")
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.warning(f"Failed to set up file logging: {e}")
|
|
135
|
+
|
|
136
|
+
# Set propagation
|
|
137
|
+
unc_logger.propagate = propagate
|
|
138
|
+
|
|
139
|
+
logger.debug("Logging configured successfully")
|
|
140
|
+
|
|
141
|
+
def get_logger(name: str) -> logging.Logger:
|
|
142
|
+
"""
|
|
143
|
+
Get a logger for a specific module.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
name: The name of the module.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
A configured logger instance.
|
|
150
|
+
"""
|
|
151
|
+
return logging.getLogger(f"unctools.{name}")
|
|
152
|
+
|
|
153
|
+
def enable_debug_logging() -> None:
|
|
154
|
+
"""
|
|
155
|
+
Enable debug-level logging for UNCtools.
|
|
156
|
+
|
|
157
|
+
This is a convenience function for quickly enabling detailed logging.
|
|
158
|
+
"""
|
|
159
|
+
configure_logging(level=logging.DEBUG)
|
|
160
|
+
logger.debug("Debug logging enabled")
|
|
161
|
+
|
|
162
|
+
def disable_logging() -> None:
|
|
163
|
+
"""
|
|
164
|
+
Disable all logging for UNCtools.
|
|
165
|
+
|
|
166
|
+
This is useful for applications that want to handle all output themselves.
|
|
167
|
+
"""
|
|
168
|
+
unc_logger = logging.getLogger("unctools")
|
|
169
|
+
|
|
170
|
+
# Remove all handlers
|
|
171
|
+
for handler in unc_logger.handlers[:]:
|
|
172
|
+
unc_logger.removeHandler(handler)
|
|
173
|
+
|
|
174
|
+
# Set level to higher than CRITICAL to effectively disable
|
|
175
|
+
unc_logger.setLevel(logging.CRITICAL + 10)
|
|
176
|
+
|
|
177
|
+
def log_exception(exc: Exception, context: Optional[Dict[str, Any]] = None) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Log an exception with additional context information.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
exc: The exception to log.
|
|
183
|
+
context: Additional context information (optional).
|
|
184
|
+
"""
|
|
185
|
+
context_str = ", ".join(f"{k}={v}" for k, v in (context or {}).items())
|
|
186
|
+
|
|
187
|
+
if context_str:
|
|
188
|
+
logger.exception(f"Exception occurred ({context_str}): {exc}")
|
|
189
|
+
else:
|
|
190
|
+
logger.exception(f"Exception occurred: {exc}")
|
|
191
|
+
|
|
192
|
+
class LogContext:
|
|
193
|
+
"""
|
|
194
|
+
Context manager for temporarily changing log levels.
|
|
195
|
+
|
|
196
|
+
This allows for increasing or decreasing verbosity for a specific block of code.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(self, level: int = logging.DEBUG, module: Optional[str] = None):
|
|
200
|
+
"""
|
|
201
|
+
Initialize the context manager.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
level: The logging level to use within the context.
|
|
205
|
+
module: The specific module to change (if None, changes the entire package).
|
|
206
|
+
"""
|
|
207
|
+
self.level = level
|
|
208
|
+
self.module = module
|
|
209
|
+
self.previous_level = None
|
|
210
|
+
self.logger = None
|
|
211
|
+
|
|
212
|
+
def __enter__(self) -> 'LogContext':
|
|
213
|
+
# Get the appropriate logger
|
|
214
|
+
if self.module:
|
|
215
|
+
self.logger = logging.getLogger(f"unctools.{self.module}")
|
|
216
|
+
else:
|
|
217
|
+
self.logger = logging.getLogger("unctools")
|
|
218
|
+
|
|
219
|
+
# Store previous level and set new level
|
|
220
|
+
self.previous_level = self.logger.level
|
|
221
|
+
self.logger.setLevel(self.level)
|
|
222
|
+
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
226
|
+
# Restore previous level
|
|
227
|
+
if self.logger and self.previous_level is not None:
|
|
228
|
+
self.logger.setLevel(self.previous_level)
|