mssql-python 1.0.0__cp314-cp314-win_arm64.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.
Files changed (72) hide show
  1. mssql_python/__init__.py +307 -0
  2. mssql_python/auth.py +222 -0
  3. mssql_python/connection.py +1493 -0
  4. mssql_python/connection_string_builder.py +113 -0
  5. mssql_python/connection_string_parser.py +381 -0
  6. mssql_python/constants.py +509 -0
  7. mssql_python/cursor.py +2601 -0
  8. mssql_python/db_connection.py +53 -0
  9. mssql_python/ddbc_bindings.cp314-arm64.pyd +0 -0
  10. mssql_python/ddbc_bindings.py +137 -0
  11. mssql_python/exceptions.py +574 -0
  12. mssql_python/helpers.py +357 -0
  13. mssql_python/libs/LICENSING +19 -0
  14. mssql_python/libs/linux/alpine/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  15. mssql_python/libs/linux/alpine/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  16. mssql_python/libs/linux/alpine/arm64/lib/libodbcinst.so.2 +0 -0
  17. mssql_python/libs/linux/alpine/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  18. mssql_python/libs/linux/alpine/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  19. mssql_python/libs/linux/alpine/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  20. mssql_python/libs/linux/alpine/x86_64/lib/libodbcinst.so.2 +0 -0
  21. mssql_python/libs/linux/alpine/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  22. mssql_python/libs/linux/debian_ubuntu/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  23. mssql_python/libs/linux/debian_ubuntu/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  24. mssql_python/libs/linux/debian_ubuntu/arm64/lib/libodbcinst.so.2 +0 -0
  25. mssql_python/libs/linux/debian_ubuntu/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  26. mssql_python/libs/linux/debian_ubuntu/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  27. mssql_python/libs/linux/debian_ubuntu/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  28. mssql_python/libs/linux/debian_ubuntu/x86_64/lib/libodbcinst.so.2 +0 -0
  29. mssql_python/libs/linux/debian_ubuntu/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  30. mssql_python/libs/linux/rhel/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  31. mssql_python/libs/linux/rhel/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  32. mssql_python/libs/linux/rhel/arm64/lib/libodbcinst.so.2 +0 -0
  33. mssql_python/libs/linux/rhel/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  34. mssql_python/libs/linux/rhel/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  35. mssql_python/libs/linux/rhel/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  36. mssql_python/libs/linux/rhel/x86_64/lib/libodbcinst.so.2 +0 -0
  37. mssql_python/libs/linux/rhel/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  38. mssql_python/libs/linux/suse/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
  39. mssql_python/libs/linux/suse/x86_64/lib/libodbcinst.so.2 +0 -0
  40. mssql_python/libs/linux/suse/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
  41. mssql_python/libs/macos/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  42. mssql_python/libs/macos/arm64/lib/libltdl.7.dylib +0 -0
  43. mssql_python/libs/macos/arm64/lib/libmsodbcsql.18.dylib +0 -0
  44. mssql_python/libs/macos/arm64/lib/libodbc.2.dylib +0 -0
  45. mssql_python/libs/macos/arm64/lib/libodbcinst.2.dylib +0 -0
  46. mssql_python/libs/macos/arm64/share/doc/msodbcsql18/LICENSE.txt +101 -0
  47. mssql_python/libs/macos/arm64/share/doc/msodbcsql18/RELEASE_NOTES +465 -0
  48. mssql_python/libs/macos/arm64/share/msodbcsql18/resources/en_US/msodbcsqlr18.rll +0 -0
  49. mssql_python/libs/macos/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  50. mssql_python/libs/macos/x86_64/lib/libltdl.7.dylib +0 -0
  51. mssql_python/libs/macos/x86_64/lib/libmsodbcsql.18.dylib +0 -0
  52. mssql_python/libs/macos/x86_64/lib/libodbc.2.dylib +0 -0
  53. mssql_python/libs/macos/x86_64/lib/libodbcinst.2.dylib +0 -0
  54. mssql_python/libs/macos/x86_64/share/doc/msodbcsql18/LICENSE.txt +101 -0
  55. mssql_python/libs/macos/x86_64/share/doc/msodbcsql18/RELEASE_NOTES +465 -0
  56. mssql_python/libs/macos/x86_64/share/msodbcsql18/resources/en_US/msodbcsqlr18.rll +0 -0
  57. mssql_python/libs/windows/arm64/1033/msodbcsqlr18.rll +0 -0
  58. mssql_python/libs/windows/arm64/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  59. mssql_python/libs/windows/arm64/msodbcdiag18.dll +0 -0
  60. mssql_python/libs/windows/arm64/msodbcsql18.dll +0 -0
  61. mssql_python/libs/windows/arm64/mssql-auth.dll +0 -0
  62. mssql_python/logging.py +610 -0
  63. mssql_python/mssql_python.pyi +375 -0
  64. mssql_python/msvcp140.dll +0 -0
  65. mssql_python/pooling.py +125 -0
  66. mssql_python/row.py +194 -0
  67. mssql_python/type.py +143 -0
  68. mssql_python-1.0.0.dist-info/METADATA +76 -0
  69. mssql_python-1.0.0.dist-info/RECORD +72 -0
  70. mssql_python-1.0.0.dist-info/WHEEL +5 -0
  71. mssql_python-1.0.0.dist-info/licenses/LICENSE +47 -0
  72. mssql_python-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,610 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation.
3
+ Licensed under the MIT license.
4
+
5
+ Enhanced logging module for mssql_python with JDBC-style logging levels.
6
+ This module provides fine-grained logging control with zero overhead when disabled.
7
+ """
8
+
9
+ import logging
10
+ from logging.handlers import RotatingFileHandler
11
+ import os
12
+ import sys
13
+ import threading
14
+ import datetime
15
+ import re
16
+ import platform
17
+ import atexit
18
+ from typing import Optional
19
+
20
+
21
+ # Single DEBUG level - all or nothing philosophy
22
+ # If you need logging, you need to see everything
23
+ DEBUG = logging.DEBUG # 10
24
+
25
+ # Output destination constants
26
+ STDOUT = 'stdout' # Log to stdout only
27
+ FILE = 'file' # Log to file only (default)
28
+ BOTH = 'both' # Log to both file and stdout
29
+
30
+ # Allowed log file extensions
31
+ ALLOWED_LOG_EXTENSIONS = {'.txt', '.log', '.csv'}
32
+
33
+
34
+ class ThreadIDFilter(logging.Filter):
35
+ """Filter that adds thread_id to all log records."""
36
+
37
+ def filter(self, record):
38
+ """Add thread_id (OS native) attribute to log record."""
39
+ # Use OS native thread ID for debugging compatibility
40
+ try:
41
+ thread_id = threading.get_native_id()
42
+ except AttributeError:
43
+ # Fallback for Python < 3.8
44
+ thread_id = threading.current_thread().ident
45
+ record.thread_id = thread_id
46
+ return True
47
+
48
+
49
+
50
+
51
+ class MSSQLLogger:
52
+ """
53
+ Singleton logger for mssql_python with single DEBUG level.
54
+
55
+ Philosophy: All or nothing - if you enable logging, you see EVERYTHING.
56
+ Logging is a troubleshooting tool, not a production feature.
57
+
58
+ Features:
59
+ - Single DEBUG level (no categorization)
60
+ - Automatic file rotation (512MB, 5 backups)
61
+ - Password sanitization
62
+ - Trace ID support with contextvars (automatic propagation)
63
+ - Thread-safe operation
64
+ - Zero overhead when disabled (level check only)
65
+
66
+ ⚠️ Performance Warning: Logging adds ~2-5% overhead. Only enable when troubleshooting.
67
+ """
68
+
69
+ _instance: Optional['MSSQLLogger'] = None
70
+ _lock = threading.Lock()
71
+ _init_lock = threading.Lock() # Separate lock for initialization
72
+
73
+ def __new__(cls) -> 'MSSQLLogger':
74
+ """Ensure singleton pattern"""
75
+ if cls._instance is None:
76
+ with cls._lock:
77
+ if cls._instance is None:
78
+ cls._instance = super(MSSQLLogger, cls).__new__(cls)
79
+ return cls._instance
80
+
81
+ def __init__(self):
82
+ """Initialize the logger (only once) - thread-safe"""
83
+ # Use separate lock for initialization check to prevent race condition
84
+ # This ensures hasattr check and assignment are atomic
85
+ with self._init_lock:
86
+ # Skip if already initialized
87
+ if hasattr(self, '_initialized'):
88
+ return
89
+
90
+ self._initialized = True
91
+
92
+ # Create the underlying Python logger
93
+ self._logger = logging.getLogger('mssql_python')
94
+ self._logger.setLevel(logging.CRITICAL) # Disabled by default
95
+ self._logger.propagate = False # Don't propagate to root logger
96
+
97
+ # Add trace ID filter (injects thread_id into every log record)
98
+ self._logger.addFilter(ThreadIDFilter())
99
+
100
+ # Output mode and handlers
101
+ self._output_mode = FILE # Default to file only
102
+ self._file_handler = None
103
+ self._stdout_handler = None
104
+ self._log_file = None
105
+ self._custom_log_path = None # Custom log file path (if specified)
106
+ self._handlers_initialized = False
107
+ self._handler_lock = threading.RLock() # Reentrant lock for handler operations
108
+ self._cleanup_registered = False # Track if atexit cleanup is registered
109
+
110
+ # Don't setup handlers yet - do it lazily when setLevel is called
111
+ # This prevents creating log files when user changes output mode before enabling logging
112
+
113
+ def _setup_handlers(self):
114
+ """
115
+ Setup handlers based on output mode.
116
+ Creates file handler and/or stdout handler as needed.
117
+ Thread-safe: Protects against concurrent handler removal during logging.
118
+ """
119
+ # Lock prevents race condition where one thread logs while another removes handlers
120
+ with self._handler_lock:
121
+ # Acquire locks on all existing handlers before closing
122
+ # This ensures no thread is mid-write when we close
123
+ old_handlers = self._logger.handlers[:]
124
+ for handler in old_handlers:
125
+ handler.acquire()
126
+
127
+ try:
128
+ # Flush and close each handler while holding its lock
129
+ for handler in old_handlers:
130
+ try:
131
+ handler.flush() # Flush BEFORE close
132
+ except:
133
+ pass # Ignore flush errors
134
+ handler.close()
135
+ self._logger.removeHandler(handler)
136
+ finally:
137
+ # Release locks on old handlers
138
+ for handler in old_handlers:
139
+ try:
140
+ handler.release()
141
+ except:
142
+ pass # Handler might already be closed
143
+
144
+ self._file_handler = None
145
+ self._stdout_handler = None
146
+
147
+ # Create CSV formatter
148
+ # Custom formatter to extract source from message and format as CSV
149
+ class CSVFormatter(logging.Formatter):
150
+ def format(self, record):
151
+ # Extract source from message (e.g., [Python] or [DDBC])
152
+ msg = record.getMessage()
153
+ if msg.startswith('[') and ']' in msg:
154
+ end_bracket = msg.index(']')
155
+ source = msg[1:end_bracket]
156
+ message = msg[end_bracket+2:].strip() # Skip '] '
157
+ else:
158
+ source = 'Unknown'
159
+ message = msg
160
+
161
+ # Format timestamp with milliseconds using period separator
162
+ timestamp = self.formatTime(record, '%Y-%m-%d %H:%M:%S')
163
+ timestamp_with_ms = f"{timestamp}.{int(record.msecs):03d}"
164
+
165
+ # Get thread ID
166
+ thread_id = getattr(record, 'thread_id', 0)
167
+
168
+ # Build CSV row
169
+ location = f"{record.filename}:{record.lineno}"
170
+ csv_row = f"{timestamp_with_ms}, {thread_id}, {record.levelname}, {location}, {source}, {message}"
171
+
172
+ return csv_row
173
+
174
+ formatter = CSVFormatter()
175
+
176
+ # Override format to use milliseconds with period separator
177
+ formatter.default_msec_format = '%s.%03d'
178
+
179
+ # Setup file handler if needed
180
+ if self._output_mode in (FILE, BOTH):
181
+ # Use custom path or auto-generate
182
+ if self._custom_log_path:
183
+ self._log_file = self._custom_log_path
184
+ # Ensure directory exists for custom path
185
+ log_dir = os.path.dirname(self._custom_log_path)
186
+ if log_dir and not os.path.exists(log_dir):
187
+ os.makedirs(log_dir, exist_ok=True)
188
+ else:
189
+ # Create log file in mssql_python_logs folder
190
+ log_dir = os.path.join(os.getcwd(), "mssql_python_logs")
191
+ if not os.path.exists(log_dir):
192
+ os.makedirs(log_dir, exist_ok=True)
193
+
194
+ timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
195
+ pid = os.getpid()
196
+ self._log_file = os.path.join(
197
+ log_dir,
198
+ f"mssql_python_trace_{timestamp}_{pid}.log"
199
+ )
200
+
201
+ # Create rotating file handler (512MB, 5 backups)
202
+ # Use UTF-8 encoding for unicode support on all platforms
203
+ self._file_handler = RotatingFileHandler(
204
+ self._log_file,
205
+ maxBytes=512 * 1024 * 1024, # 512MB
206
+ backupCount=5,
207
+ encoding='utf-8'
208
+ )
209
+ self._file_handler.setFormatter(formatter)
210
+ self._logger.addHandler(self._file_handler)
211
+
212
+ # Write CSV header to new log file
213
+ self._write_log_header()
214
+ else:
215
+ # No file logging - clear the log file path
216
+ self._log_file = None
217
+
218
+ # Setup stdout handler if needed
219
+ if self._output_mode in (STDOUT, BOTH):
220
+ import sys
221
+ self._stdout_handler = logging.StreamHandler(sys.stdout)
222
+ self._stdout_handler.setFormatter(formatter)
223
+ self._logger.addHandler(self._stdout_handler)
224
+
225
+ def _reconfigure_handlers(self):
226
+ """
227
+ Reconfigure handlers when output mode changes.
228
+ Closes existing handlers and creates new ones based on current output mode.
229
+ """
230
+ self._setup_handlers()
231
+
232
+ def _cleanup_handlers(self):
233
+ """
234
+ Cleanup all handlers on process exit.
235
+ Registered with atexit to ensure proper file handle cleanup.
236
+
237
+ Thread-safe: Protects against concurrent logging during cleanup.
238
+
239
+ Note on RotatingFileHandler:
240
+ - File rotation (at 512MB) is already thread-safe
241
+ - doRollover() is called within emit() which holds handler.lock
242
+ - No additional synchronization needed for rotation
243
+ """
244
+ with self._handler_lock:
245
+ handlers = self._logger.handlers[:]
246
+ for handler in handlers:
247
+ handler.acquire()
248
+
249
+ try:
250
+ for handler in handlers:
251
+ try:
252
+ handler.flush()
253
+ handler.close()
254
+ except:
255
+ pass # Ignore errors during cleanup
256
+ self._logger.removeHandler(handler)
257
+ finally:
258
+ for handler in handlers:
259
+ try:
260
+ handler.release()
261
+ except:
262
+ pass
263
+
264
+ def _validate_log_file_extension(self, file_path: str) -> None:
265
+ """
266
+ Validate that the log file has an allowed extension.
267
+
268
+ Args:
269
+ file_path: Path to the log file
270
+
271
+ Raises:
272
+ ValueError: If the file extension is not allowed
273
+ """
274
+ _, ext = os.path.splitext(file_path)
275
+ ext_lower = ext.lower()
276
+
277
+ if ext_lower not in ALLOWED_LOG_EXTENSIONS:
278
+ allowed = ', '.join(sorted(ALLOWED_LOG_EXTENSIONS))
279
+ raise ValueError(
280
+ f"Invalid log file extension '{ext}'. "
281
+ f"Allowed extensions: {allowed}"
282
+ )
283
+
284
+ def _write_log_header(self):
285
+ """
286
+ Write CSV header and metadata to the log file.
287
+ Called once when log file is created.
288
+ """
289
+ if not self._log_file or not self._file_handler:
290
+ return
291
+
292
+ try:
293
+ # Get script name from sys.argv or __main__
294
+ script_name = os.path.basename(sys.argv[0]) if sys.argv else '<interactive>'
295
+
296
+ # Get Python version
297
+ python_version = platform.python_version()
298
+
299
+ # Get driver version (try to import from package)
300
+ try:
301
+ from mssql_python import __version__
302
+ driver_version = __version__
303
+ except:
304
+ driver_version = 'unknown'
305
+
306
+ # Get current time
307
+ start_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
308
+
309
+ # Get PID
310
+ pid = os.getpid()
311
+
312
+ # Get OS info
313
+ os_info = platform.platform()
314
+
315
+ # Build header comment line
316
+ header_line = f"# MSSQL-Python Driver Log | Script: {script_name} | PID: {pid} | Log Level: DEBUG | Python: {python_version} | Driver: {driver_version} | Start: {start_time} | OS: {os_info}\n"
317
+
318
+ # CSV column headers
319
+ csv_header = "Timestamp, ThreadID, Level, Location, Source, Message\n"
320
+
321
+ # Write directly to file (bypass formatter)
322
+ with open(self._log_file, 'a') as f:
323
+ f.write(header_line)
324
+ f.write(csv_header)
325
+
326
+ except Exception as e:
327
+ # Notify on stderr so user knows why header is missing
328
+ try:
329
+ sys.stderr.write(f"[MSSQL-Python] Warning: Failed to write log header to {self._log_file}: {type(e).__name__}\n")
330
+ sys.stderr.flush()
331
+ except:
332
+ pass # Even stderr notification failed
333
+ # Don't crash - logging continues without header
334
+
335
+ def _log(self, level: int, msg: str, add_prefix: bool = True, *args, **kwargs):
336
+ """
337
+ Internal logging method with exception safety.
338
+
339
+ Args:
340
+ level: Log level (DEBUG, INFO, WARNING, ERROR)
341
+ msg: Message format string
342
+ add_prefix: Whether to add [Python] prefix (default True)
343
+ *args: Arguments for message formatting
344
+ **kwargs: Additional keyword arguments
345
+
346
+ Note:
347
+ Callers are responsible for sanitizing sensitive data (passwords,
348
+ tokens, etc.) before logging. Use helpers.sanitize_connection_string()
349
+ for connection strings.
350
+
351
+ Exception Safety:
352
+ NEVER crashes the application. Catches all exceptions:
353
+ - TypeError/ValueError: Bad format string or args
354
+ - IOError/OSError: Disk full, permission denied
355
+ - UnicodeEncodeError: Encoding issues
356
+
357
+ On critical failures (ERROR level), attempts stderr fallback.
358
+ All other failures are silently ignored to prevent app crashes.
359
+ """
360
+ try:
361
+ # Fast level check (zero overhead if disabled)
362
+ if not self._logger.isEnabledFor(level):
363
+ return
364
+
365
+ # Add prefix if requested (only after level check)
366
+ if add_prefix:
367
+ msg = f"[Python] {msg}"
368
+
369
+ # Format message with args if provided
370
+ if args:
371
+ msg = msg % args
372
+
373
+ # Log the message (no args since already formatted)
374
+ self._logger.log(level, msg, **kwargs)
375
+ except Exception:
376
+ # Last resort: Try stderr fallback for any logging failure
377
+ # This helps diagnose critical issues (disk full, permission denied, etc.)
378
+ try:
379
+ import sys
380
+ level_name = logging.getLevelName(level)
381
+ sys.stderr.write(f"[MSSQL-Python Logging Failed - {level_name}] {msg if 'msg' in locals() else 'Unable to format message'}\n")
382
+ sys.stderr.flush()
383
+ except:
384
+ pass # Even stderr failed - give up silently
385
+
386
+ # Convenience methods for logging
387
+
388
+ def debug(self, msg: str, *args, **kwargs):
389
+ """Log at DEBUG level (all diagnostic messages)"""
390
+ self._log(logging.DEBUG, msg, True, *args, **kwargs)
391
+
392
+ def info(self, msg: str, *args, **kwargs):
393
+ """Log at INFO level"""
394
+ self._log(logging.INFO, msg, True, *args, **kwargs)
395
+
396
+ def warning(self, msg: str, *args, **kwargs):
397
+ """Log at WARNING level"""
398
+ self._log(logging.WARNING, msg, True, *args, **kwargs)
399
+
400
+ def error(self, msg: str, *args, **kwargs):
401
+ """Log at ERROR level"""
402
+ self._log(logging.ERROR, msg, True, *args, **kwargs)
403
+
404
+ # Level control
405
+
406
+ def _setLevel(self, level: int, output: Optional[str] = None, log_file_path: Optional[str] = None):
407
+ """
408
+ Internal method to set logging level (use setup_logging() instead).
409
+
410
+ Args:
411
+ level: Logging level (typically DEBUG)
412
+ output: Optional output mode (FILE, STDOUT, BOTH)
413
+ log_file_path: Optional custom path for log file
414
+
415
+ Raises:
416
+ ValueError: If output mode is invalid
417
+ """
418
+ # Validate and set output mode if specified
419
+ if output is not None:
420
+ if output not in (FILE, STDOUT, BOTH):
421
+ raise ValueError(
422
+ f"Invalid output mode: {output}. "
423
+ f"Must be one of: {FILE}, {STDOUT}, {BOTH}"
424
+ )
425
+ self._output_mode = output
426
+
427
+ # Store custom log file path if provided
428
+ if log_file_path is not None:
429
+ self._validate_log_file_extension(log_file_path)
430
+ self._custom_log_path = log_file_path
431
+
432
+ # Setup handlers if not yet initialized or if output mode/path changed
433
+ # Handler setup is protected by _handler_lock inside _setup_handlers()
434
+ if not self._handlers_initialized or output is not None or log_file_path is not None:
435
+ self._setup_handlers()
436
+ self._handlers_initialized = True
437
+
438
+ # Register atexit cleanup on first handler setup
439
+ if not self._cleanup_registered:
440
+ atexit.register(self._cleanup_handlers)
441
+ self._cleanup_registered = True
442
+
443
+ # Set level (atomic operation, no lock needed)
444
+ self._logger.setLevel(level)
445
+
446
+ # Notify C++ bridge of level change
447
+ self._notify_cpp_level_change(level)
448
+
449
+ def getLevel(self) -> int:
450
+ """
451
+ Get the current logging level.
452
+
453
+ Returns:
454
+ int: Current log level
455
+ """
456
+ return self._logger.level
457
+
458
+ def isEnabledFor(self, level: int) -> bool:
459
+ """
460
+ Check if a given log level is enabled.
461
+
462
+ Args:
463
+ level: Log level to check
464
+
465
+ Returns:
466
+ bool: True if the level is enabled
467
+ """
468
+ return self._logger.isEnabledFor(level)
469
+
470
+ # Handler management
471
+
472
+ def addHandler(self, handler: logging.Handler):
473
+ """Add a handler to the logger (thread-safe)"""
474
+ with self._handler_lock:
475
+ self._logger.addHandler(handler)
476
+
477
+ def removeHandler(self, handler: logging.Handler):
478
+ """Remove a handler from the logger (thread-safe)"""
479
+ with self._handler_lock:
480
+ self._logger.removeHandler(handler)
481
+
482
+ @property
483
+ def handlers(self) -> list:
484
+ """Get list of handlers attached to the logger (thread-safe)"""
485
+ with self._handler_lock:
486
+ return self._logger.handlers[:] # Return copy to prevent external modification
487
+
488
+ def reset_handlers(self):
489
+ """
490
+ Reset/recreate handlers.
491
+ Useful when log file has been deleted or needs to be recreated.
492
+ """
493
+ self._setup_handlers()
494
+
495
+ def _notify_cpp_level_change(self, level: int):
496
+ """
497
+ Notify C++ bridge that log level has changed.
498
+ This updates the cached level in C++ for fast checks.
499
+
500
+ Args:
501
+ level: New log level
502
+ """
503
+ try:
504
+ # Import here to avoid circular dependency
505
+ from . import ddbc_bindings
506
+ if hasattr(ddbc_bindings, 'update_log_level'):
507
+ ddbc_bindings.update_log_level(level)
508
+ except (ImportError, AttributeError):
509
+ # C++ bindings not available or not yet initialized
510
+ pass
511
+
512
+ # Properties
513
+
514
+ @property
515
+ def output(self) -> str:
516
+ """Get the current output mode"""
517
+ return self._output_mode
518
+
519
+ @output.setter
520
+ def output(self, mode: str):
521
+ """
522
+ Set the output mode.
523
+
524
+ Args:
525
+ mode: Output mode (FILE, STDOUT, or BOTH)
526
+
527
+ Raises:
528
+ ValueError: If mode is not a valid OutputMode value
529
+ """
530
+ if mode not in (FILE, STDOUT, BOTH):
531
+ raise ValueError(
532
+ f"Invalid output mode: {mode}. "
533
+ f"Must be one of: {FILE}, {STDOUT}, {BOTH}"
534
+ )
535
+ self._output_mode = mode
536
+
537
+ # Only reconfigure if handlers were already initialized
538
+ if self._handlers_initialized:
539
+ self._reconfigure_handlers()
540
+
541
+ @property
542
+ def log_file(self) -> Optional[str]:
543
+ """Get the current log file path (None if file output is disabled)"""
544
+ return self._log_file
545
+
546
+ @property
547
+ def level(self) -> int:
548
+ """Get the current logging level"""
549
+ return self._logger.level
550
+
551
+
552
+ # ============================================================================
553
+ # Module-level exports (Primary API)
554
+ # ============================================================================
555
+
556
+ # Singleton logger instance
557
+ logger = MSSQLLogger()
558
+
559
+ # Expose the underlying Python logger for use in application code
560
+ # This allows applications to access the same logger used by the driver
561
+ # Usage: from mssql_python.logging import driver_logger
562
+ driver_logger = logger._logger
563
+
564
+ # ============================================================================
565
+ # Primary API - setup_logging()
566
+ # ============================================================================
567
+
568
+ def setup_logging(output: str = 'file', log_file_path: Optional[str] = None):
569
+ """
570
+ Enable DEBUG logging for troubleshooting.
571
+
572
+ ⚠️ PERFORMANCE WARNING: Logging adds ~2-5% overhead.
573
+ Only enable when investigating issues. Do NOT enable in production without reason.
574
+
575
+ Philosophy: All or nothing - if you need logging, you need to see EVERYTHING.
576
+ Logging is a troubleshooting tool, not a production monitoring solution.
577
+
578
+ Args:
579
+ output: Where to send logs (default: 'file')
580
+ Options: 'file', 'stdout', 'both'
581
+ log_file_path: Optional custom path for log file
582
+ Must have extension: .txt, .log, or .csv
583
+ If not specified, auto-generates in ./mssql_python_logs/
584
+
585
+ Examples:
586
+ import mssql_python
587
+
588
+ # File only (default, in mssql_python_logs folder)
589
+ mssql_python.setup_logging()
590
+
591
+ # Stdout only (for CI/CD)
592
+ mssql_python.setup_logging(output='stdout')
593
+
594
+ # Both file and stdout (for development)
595
+ mssql_python.setup_logging(output='both')
596
+
597
+ # Custom log file path (must use .txt, .log, or .csv extension)
598
+ mssql_python.setup_logging(log_file_path="/var/log/myapp.log")
599
+ mssql_python.setup_logging(log_file_path="/tmp/debug.txt")
600
+ mssql_python.setup_logging(log_file_path="/tmp/data.csv")
601
+
602
+ # Custom path with both outputs
603
+ mssql_python.setup_logging(output='both', log_file_path="/tmp/debug.log")
604
+
605
+ Future Enhancement:
606
+ For performance analysis, use the universal profiler (coming soon)
607
+ instead of logging. Logging is not designed for performance measurement.
608
+ """
609
+ logger._setLevel(logging.DEBUG, output, log_file_path)
610
+ return logger