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.
- mssql_python/__init__.py +307 -0
- mssql_python/auth.py +222 -0
- mssql_python/connection.py +1493 -0
- mssql_python/connection_string_builder.py +113 -0
- mssql_python/connection_string_parser.py +381 -0
- mssql_python/constants.py +509 -0
- mssql_python/cursor.py +2601 -0
- mssql_python/db_connection.py +53 -0
- mssql_python/ddbc_bindings.cp314-amd64.pyd +0 -0
- mssql_python/ddbc_bindings.py +137 -0
- mssql_python/exceptions.py +574 -0
- mssql_python/helpers.py +357 -0
- mssql_python/libs/LICENSING +19 -0
- mssql_python/libs/linux/alpine/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/alpine/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/alpine/arm64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/alpine/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/alpine/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/alpine/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/alpine/x86_64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/alpine/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/debian_ubuntu/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/debian_ubuntu/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/debian_ubuntu/arm64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/debian_ubuntu/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/debian_ubuntu/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/debian_ubuntu/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/debian_ubuntu/x86_64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/debian_ubuntu/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/rhel/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/rhel/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/rhel/arm64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/rhel/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/rhel/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/rhel/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/rhel/x86_64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/rhel/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/suse/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/suse/x86_64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/suse/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/macos/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/macos/arm64/lib/libltdl.7.dylib +0 -0
- mssql_python/libs/macos/arm64/lib/libmsodbcsql.18.dylib +0 -0
- mssql_python/libs/macos/arm64/lib/libodbc.2.dylib +0 -0
- mssql_python/libs/macos/arm64/lib/libodbcinst.2.dylib +0 -0
- mssql_python/libs/macos/arm64/share/doc/msodbcsql18/LICENSE.txt +101 -0
- mssql_python/libs/macos/arm64/share/doc/msodbcsql18/RELEASE_NOTES +465 -0
- mssql_python/libs/macos/arm64/share/msodbcsql18/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/macos/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/macos/x86_64/lib/libltdl.7.dylib +0 -0
- mssql_python/libs/macos/x86_64/lib/libmsodbcsql.18.dylib +0 -0
- mssql_python/libs/macos/x86_64/lib/libodbc.2.dylib +0 -0
- mssql_python/libs/macos/x86_64/lib/libodbcinst.2.dylib +0 -0
- mssql_python/libs/macos/x86_64/share/doc/msodbcsql18/LICENSE.txt +101 -0
- mssql_python/libs/macos/x86_64/share/doc/msodbcsql18/RELEASE_NOTES +465 -0
- mssql_python/libs/macos/x86_64/share/msodbcsql18/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/windows/x64/1033/msodbcsqlr18.rll +0 -0
- mssql_python/libs/windows/x64/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/windows/x64/MICROSOFT_VISUAL_STUDIO_LICENSE.txt +54 -0
- mssql_python/libs/windows/x64/msodbcdiag18.dll +0 -0
- mssql_python/libs/windows/x64/msodbcsql18.dll +0 -0
- mssql_python/libs/windows/x64/mssql-auth.dll +0 -0
- mssql_python/logging.py +610 -0
- mssql_python/mssql_python.pyi +375 -0
- mssql_python/msvcp140.dll +0 -0
- mssql_python/pooling.py +125 -0
- mssql_python/row.py +194 -0
- mssql_python/type.py +143 -0
- mssql_python-1.0.0.dist-info/METADATA +76 -0
- mssql_python-1.0.0.dist-info/RECORD +73 -0
- mssql_python-1.0.0.dist-info/WHEEL +5 -0
- mssql_python-1.0.0.dist-info/licenses/LICENSE +47 -0
- 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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
mssql_python/logging.py
ADDED
|
@@ -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
|