mssql-python 1.0.0__cp314-cp314-win_amd64.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 (73) 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-amd64.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/x64/1033/msodbcsqlr18.rll +0 -0
  58. mssql_python/libs/windows/x64/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
  59. mssql_python/libs/windows/x64/MICROSOFT_VISUAL_STUDIO_LICENSE.txt +54 -0
  60. mssql_python/libs/windows/x64/msodbcdiag18.dll +0 -0
  61. mssql_python/libs/windows/x64/msodbcsql18.dll +0 -0
  62. mssql_python/libs/windows/x64/mssql-auth.dll +0 -0
  63. mssql_python/logging.py +610 -0
  64. mssql_python/mssql_python.pyi +375 -0
  65. mssql_python/msvcp140.dll +0 -0
  66. mssql_python/pooling.py +125 -0
  67. mssql_python/row.py +194 -0
  68. mssql_python/type.py +143 -0
  69. mssql_python-1.0.0.dist-info/METADATA +76 -0
  70. mssql_python-1.0.0.dist-info/RECORD +73 -0
  71. mssql_python-1.0.0.dist-info/WHEEL +5 -0
  72. mssql_python-1.0.0.dist-info/licenses/LICENSE +47 -0
  73. mssql_python-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,54 @@
1
+ MICROSOFT SOFTWARE LICENSE TERMS
2
+ MICROSOFT VISUAL STUDIO 2017 TOOLS, ADD-ONs and EXTENSIONS
3
+
4
+ These license terms are an agreement between Microsoft Corporation (or based on where you live, one of its affiliates) and you.
5
+ They apply to the software named above. The terms also apply to any Microsoft services or updates for the
6
+ software, except to the extent those have different terms.
7
+
8
+ IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW.
9
+
10
+ 1. INSTALLATION AND USE RIGHTS.
11
+ You may install and use any number of copies of the software.
12
+
13
+ 2. TERMS FOR SPECIFIC COMPONENTS.
14
+ a. Utilities. The software may contain some items on the Utilities List at https://go.microsoft.com/fwlink/?linkid=823097. You may copy and install these Utilities, if included with the software, onto devices to debug and deploy your applications and databases you developed with the software. Please note that Utilities are designed for temporary use, that Microsoft may not be able to patch or update Utilities separately from the rest of the software, and that some Utilities by their nature may make it possible for others to access devices on which the Utilities are installed. As a result, you should delete all Utilities you have installed after you finish debugging or deploying your applications and databases. Microsoft is not responsible for any third party use or access of Utilities you install on any device.
15
+ b. Build Tools. The software may include build tools which have specific use terms. For build tools, you may copy and install files from the software onto your build devices, including physical devices and virtual machines or containers on those machines, whether on-premises or remote machines that are owned by you, hosted on Azure for you, or dedicated solely to your use (collectively, “Build Devices”). You and others in your organization may use these files on your Build Devices solely to compile, build, and verify applications or run quality or performance tests of those applications as part of the build process. For clarity, “applications” means applications developed by you and others in your organization who are each licensed to use the software.
16
+ c. Microsoft Platforms. The software may include components from Microsoft Windows; Microsoft Windows Server; Microsoft SQL Server; Microsoft Exchange; Microsoft Office; and Microsoft SharePoint. These components are governed by separate agreements and their own product support policies, as described in the Microsoft “Licenses” folder accompanying the software, except that, if license terms for those components are also included in the associated installation directory, those license terms control.
17
+ d. Third Party Components. The software may include third party components with separate legal notices or governed by other agreements, as may be described in the ThirdPartyNotices file(s) accompanying the software.
18
+
19
+ 3. DATA.
20
+ a. Data Collection. The software may collect information about you and your use of the software, and send that to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may opt-out of many of these scenarios, but not all, as described in the product documentation. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications and you should provide a copy of Microsoft’s privacy statement to your users. The Microsoft privacy statement is located here https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.
21
+ b. Processing of Personal Data. To the extent Microsoft is a processor or subprocessor of personal data in connection with the software, Microsoft makes the commitments in the European Union General Data Protection Regulation Terms of the Online Services Terms to all customers effective May 25, 2018, at http://go.microsoft.com/?linkid=9840733.
22
+
23
+ 4. SCOPE OF LICENSE. The software is licensed, not sold. This agreement only gives you some rights to use the software. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the software only as expressly permitted in this agreement. In doing so, you must comply with any technical limitations in the software that only allow you to use it in certain ways. You may not
24
+ · work around any technical limitations in the software;
25
+ · reverse engineer, decompile or disassemble the software, or otherwise attempt to derive the source code for the software except, and only to the extent required by third party licensing terms governing the use of certain open source components that may be included in the software;
26
+ · remove, minimize, block or modify any notices of Microsoft or its suppliers in the software;
27
+ · use the software in any way that is against the law; or
28
+ · share, publish, rent or lease the software, or provide the software as a stand-alone hosted as solution for others to use, or transfer the software or this agreement to any third party.
29
+
30
+ 5. EXPORT RESTRICTIONS. You must comply with all domestic and international export laws and regulations that apply to the software, which include restrictions on destinations, end users, and end use. For further information on export restrictions, visit www.microsoft.com/exporting.
31
+
32
+ 6. SUPPORT SERVICES. Because this software is “as is,” we may not provide support services for it.
33
+
34
+ 7. ENTIRE AGREEMENT. This agreement, and the terms for supplements, updates, Internet-based services and support services that you use, are the entire agreement for the software and support services.
35
+
36
+ 8. APPLICABLE LAW. If you acquired the software in the United States, Washington law applies to interpretation of and claims for breach of this agreement, and the laws of the state where you live apply to all other claims. If you acquired the software in any other country, its laws apply.
37
+
38
+ 9. CONSUMER RIGHTS; REGIONAL VARIATIONS. This agreement describes certain legal rights. You may have other rights, including consumer rights, under the laws of your state or country. Separate and apart from your relationship with Microsoft, you may also have rights with respect to the party from which you acquired the software. This agreement does not change those other rights if the laws of your state or country do not permit it to do so. For example, if you acquired the software in one of the below regions, or mandatory country law applies, then the following provisions apply to you:
39
+ a. Australia. You have statutory guarantees under the Australian Consumer Law and nothing in this agreement is intended to affect those rights.
40
+ b. Canada. If you acquired this software in Canada, you may stop receiving updates by turning off the automatic update feature, disconnecting your device from the Internet (if and when you re-connect to the Internet, however, the software will resume checking for and installing updates), or uninstalling the software. The product documentation, if any, may also specify how to turn off updates for your specific device or software.
41
+ c. Germany and Austria.
42
+ (i) Warranty. The properly licensed software will perform substantially as described in any Microsoft materials that accompany the software. However, Microsoft gives no contractual guarantee in relation to the licensed software.
43
+ (ii) Limitation of Liability. In case of intentional conduct, gross negligence, claims based on the Product Liability Act, as well as, in case of death or personal or physical injury, Microsoft is liable according to the statutory law.
44
+
45
+ Subject to the foregoing clause (ii), Microsoft will only be liable for slight negligence if Microsoft is in breach of such material contractual obligations, the fulfillment of which facilitate the due performance of this agreement, the breach of which would endanger the purpose of this agreement and the compliance with which a party may constantly trust in (so-called "cardinal obligations"). In other cases of slight negligence, Microsoft will not be liable for slight negligence.
46
+
47
+ 10. DISCLAIMER OF WARRANTY. THE SOFTWARE IS LICENSED “AS-IS.” YOU BEAR THE RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES OR CONDITIONS. TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT EXCLUDES THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
48
+
49
+ 11. LIMITATION ON AND EXCLUSION OF DAMAGES. YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES.
50
+ This limitation applies to (a) anything related to the software, services, content (including code) on third party Internet sites, or third party applications; and (b) claims for breach of contract, breach of warranty, guarantee or condition, strict liability, negligence, or other tort to the extent permitted by applicable law.
51
+
52
+ It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your country may not allow the exclusion or limitation of incidental, consequential or other damages.
53
+
54
+ EULA ID: VS 2017_TOOLS_ADDONs_C++_RTW.3_ENU
@@ -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