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