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.
- 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-arm64.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/arm64/1033/msodbcsqlr18.rll +0 -0
- mssql_python/libs/windows/arm64/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/windows/arm64/msodbcdiag18.dll +0 -0
- mssql_python/libs/windows/arm64/msodbcsql18.dll +0 -0
- mssql_python/libs/windows/arm64/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 +72 -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
mssql_python/__init__.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Microsoft Corporation.
|
|
3
|
+
Licensed under the MIT license.
|
|
4
|
+
This module initializes the mssql_python package.
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
import types
|
|
8
|
+
from typing import Dict
|
|
9
|
+
|
|
10
|
+
# Import settings from helpers to avoid circular imports
|
|
11
|
+
from .helpers import Settings, get_settings, _settings, _settings_lock
|
|
12
|
+
|
|
13
|
+
# Driver version
|
|
14
|
+
__version__ = "1.0.0"
|
|
15
|
+
|
|
16
|
+
# Exceptions
|
|
17
|
+
# https://www.python.org/dev/peps/pep-0249/#exceptions
|
|
18
|
+
|
|
19
|
+
# Import necessary modules
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
Warning,
|
|
22
|
+
Error,
|
|
23
|
+
InterfaceError,
|
|
24
|
+
DatabaseError,
|
|
25
|
+
DataError,
|
|
26
|
+
OperationalError,
|
|
27
|
+
IntegrityError,
|
|
28
|
+
InternalError,
|
|
29
|
+
ProgrammingError,
|
|
30
|
+
NotSupportedError,
|
|
31
|
+
ConnectionStringParseError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Type Objects
|
|
35
|
+
from .type import (
|
|
36
|
+
Date,
|
|
37
|
+
Time,
|
|
38
|
+
Timestamp,
|
|
39
|
+
DateFromTicks,
|
|
40
|
+
TimeFromTicks,
|
|
41
|
+
TimestampFromTicks,
|
|
42
|
+
Binary,
|
|
43
|
+
STRING,
|
|
44
|
+
BINARY,
|
|
45
|
+
NUMBER,
|
|
46
|
+
DATETIME,
|
|
47
|
+
ROWID,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Connection Objects
|
|
51
|
+
from .db_connection import connect, Connection
|
|
52
|
+
|
|
53
|
+
# Connection String Handling
|
|
54
|
+
from .connection_string_parser import _ConnectionStringParser
|
|
55
|
+
from .connection_string_builder import _ConnectionStringBuilder
|
|
56
|
+
|
|
57
|
+
# Cursor Objects
|
|
58
|
+
from .cursor import Cursor
|
|
59
|
+
|
|
60
|
+
# Logging Configuration (Simplified single-level DEBUG system)
|
|
61
|
+
from .logging import logger, setup_logging, driver_logger
|
|
62
|
+
|
|
63
|
+
# Constants
|
|
64
|
+
from .constants import ConstantsDDBC, GetInfoConstants
|
|
65
|
+
|
|
66
|
+
# Pooling
|
|
67
|
+
from .pooling import PoolingManager
|
|
68
|
+
|
|
69
|
+
# GLOBALS
|
|
70
|
+
# Read-Only
|
|
71
|
+
apilevel: str = "2.0"
|
|
72
|
+
paramstyle: str = "qmark"
|
|
73
|
+
threadsafety: int = 1
|
|
74
|
+
|
|
75
|
+
# Set the initial decimal separator in C++
|
|
76
|
+
try:
|
|
77
|
+
from .ddbc_bindings import DDBCSetDecimalSeparator
|
|
78
|
+
DDBCSetDecimalSeparator(_settings.decimal_separator)
|
|
79
|
+
except ImportError:
|
|
80
|
+
# Handle case where ddbc_bindings is not available
|
|
81
|
+
DDBCSetDecimalSeparator = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# New functions for decimal separator control
|
|
85
|
+
def setDecimalSeparator(separator: str) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Sets the decimal separator character used when parsing NUMERIC/DECIMAL values
|
|
88
|
+
from the database, e.g. the "." in "1,234.56".
|
|
89
|
+
|
|
90
|
+
The default is to use the current locale's "decimal_point" value when the module
|
|
91
|
+
was first imported, or "." if the locale is not available. This function overrides
|
|
92
|
+
the default.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
separator (str): The character to use as decimal separator
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If the separator is not a single character string
|
|
99
|
+
"""
|
|
100
|
+
# Type validation
|
|
101
|
+
if not isinstance(separator, str):
|
|
102
|
+
raise ValueError("Decimal separator must be a string")
|
|
103
|
+
|
|
104
|
+
# Length validation
|
|
105
|
+
if len(separator) == 0:
|
|
106
|
+
raise ValueError("Decimal separator cannot be empty")
|
|
107
|
+
|
|
108
|
+
if len(separator) > 1:
|
|
109
|
+
raise ValueError("Decimal separator must be a single character")
|
|
110
|
+
|
|
111
|
+
# Character validation
|
|
112
|
+
if separator.isspace():
|
|
113
|
+
raise ValueError("Whitespace characters are not allowed as decimal separators")
|
|
114
|
+
|
|
115
|
+
# Check for specific disallowed characters
|
|
116
|
+
if separator in ["\t", "\n", "\r", "\v", "\f"]:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Control character '{repr(separator)}' is not allowed as a decimal separator"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Set in Python side settings
|
|
122
|
+
_settings.decimal_separator = separator
|
|
123
|
+
|
|
124
|
+
# Update the C++ side
|
|
125
|
+
if DDBCSetDecimalSeparator is not None:
|
|
126
|
+
DDBCSetDecimalSeparator(separator)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def getDecimalSeparator() -> str:
|
|
130
|
+
"""
|
|
131
|
+
Returns the decimal separator character used when parsing NUMERIC/DECIMAL values
|
|
132
|
+
from the database.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
str: The current decimal separator character
|
|
136
|
+
"""
|
|
137
|
+
return _settings.decimal_separator
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Export specific constants for setencoding()
|
|
141
|
+
SQL_CHAR: int = ConstantsDDBC.SQL_CHAR.value
|
|
142
|
+
SQL_WCHAR: int = ConstantsDDBC.SQL_WCHAR.value
|
|
143
|
+
SQL_WMETADATA: int = -99
|
|
144
|
+
|
|
145
|
+
# Export connection attribute constants for set_attr()
|
|
146
|
+
# Only include driver-level attributes that the SQL Server ODBC driver can handle directly
|
|
147
|
+
|
|
148
|
+
# Core driver-level attributes
|
|
149
|
+
SQL_ATTR_ACCESS_MODE: int = ConstantsDDBC.SQL_ATTR_ACCESS_MODE.value
|
|
150
|
+
SQL_ATTR_CONNECTION_TIMEOUT: int = ConstantsDDBC.SQL_ATTR_CONNECTION_TIMEOUT.value
|
|
151
|
+
SQL_ATTR_CURRENT_CATALOG: int = ConstantsDDBC.SQL_ATTR_CURRENT_CATALOG.value
|
|
152
|
+
SQL_ATTR_LOGIN_TIMEOUT: int = ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value
|
|
153
|
+
SQL_ATTR_PACKET_SIZE: int = ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value
|
|
154
|
+
SQL_ATTR_TXN_ISOLATION: int = ConstantsDDBC.SQL_ATTR_TXN_ISOLATION.value
|
|
155
|
+
|
|
156
|
+
# Transaction Isolation Level Constants
|
|
157
|
+
SQL_TXN_READ_UNCOMMITTED: int = ConstantsDDBC.SQL_TXN_READ_UNCOMMITTED.value
|
|
158
|
+
SQL_TXN_READ_COMMITTED: int = ConstantsDDBC.SQL_TXN_READ_COMMITTED.value
|
|
159
|
+
SQL_TXN_REPEATABLE_READ: int = ConstantsDDBC.SQL_TXN_REPEATABLE_READ.value
|
|
160
|
+
SQL_TXN_SERIALIZABLE: int = ConstantsDDBC.SQL_TXN_SERIALIZABLE.value
|
|
161
|
+
|
|
162
|
+
# Access Mode Constants
|
|
163
|
+
SQL_MODE_READ_WRITE: int = ConstantsDDBC.SQL_MODE_READ_WRITE.value
|
|
164
|
+
SQL_MODE_READ_ONLY: int = ConstantsDDBC.SQL_MODE_READ_ONLY.value
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def pooling(max_size: int = 100, idle_timeout: int = 600, enabled: bool = True) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Enable connection pooling with the specified parameters.
|
|
170
|
+
By default:
|
|
171
|
+
- If not explicitly called, pooling will be auto-enabled with default values.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
max_size (int): Maximum number of connections in the pool.
|
|
175
|
+
idle_timeout (int): Time in seconds before idle connections are closed.
|
|
176
|
+
enabled (bool): Whether to enable or disable pooling.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
None
|
|
180
|
+
"""
|
|
181
|
+
if not enabled:
|
|
182
|
+
PoolingManager.disable()
|
|
183
|
+
else:
|
|
184
|
+
PoolingManager.enable(max_size, idle_timeout)
|
|
185
|
+
|
|
186
|
+
_original_module_setattr = sys.modules[__name__].__setattr__
|
|
187
|
+
|
|
188
|
+
def _custom_setattr(name, value):
|
|
189
|
+
if name == 'lowercase':
|
|
190
|
+
with _settings_lock:
|
|
191
|
+
_settings.lowercase = bool(value)
|
|
192
|
+
# Update the module's lowercase variable
|
|
193
|
+
_original_module_setattr(name, _settings.lowercase)
|
|
194
|
+
else:
|
|
195
|
+
_original_module_setattr(name, value)
|
|
196
|
+
|
|
197
|
+
# Replace the module's __setattr__ with our custom version
|
|
198
|
+
sys.modules[__name__].__setattr__ = _custom_setattr
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Export SQL constants at module level
|
|
202
|
+
SQL_VARCHAR: int = ConstantsDDBC.SQL_VARCHAR.value
|
|
203
|
+
SQL_LONGVARCHAR: int = ConstantsDDBC.SQL_LONGVARCHAR.value
|
|
204
|
+
SQL_WVARCHAR: int = ConstantsDDBC.SQL_WVARCHAR.value
|
|
205
|
+
SQL_WLONGVARCHAR: int = ConstantsDDBC.SQL_WLONGVARCHAR.value
|
|
206
|
+
SQL_DECIMAL: int = ConstantsDDBC.SQL_DECIMAL.value
|
|
207
|
+
SQL_NUMERIC: int = ConstantsDDBC.SQL_NUMERIC.value
|
|
208
|
+
SQL_BIT: int = ConstantsDDBC.SQL_BIT.value
|
|
209
|
+
SQL_TINYINT: int = ConstantsDDBC.SQL_TINYINT.value
|
|
210
|
+
SQL_SMALLINT: int = ConstantsDDBC.SQL_SMALLINT.value
|
|
211
|
+
SQL_INTEGER: int = ConstantsDDBC.SQL_INTEGER.value
|
|
212
|
+
SQL_BIGINT: int = ConstantsDDBC.SQL_BIGINT.value
|
|
213
|
+
SQL_REAL: int = ConstantsDDBC.SQL_REAL.value
|
|
214
|
+
SQL_FLOAT: int = ConstantsDDBC.SQL_FLOAT.value
|
|
215
|
+
SQL_DOUBLE: int = ConstantsDDBC.SQL_DOUBLE.value
|
|
216
|
+
SQL_BINARY: int = ConstantsDDBC.SQL_BINARY.value
|
|
217
|
+
SQL_VARBINARY: int = ConstantsDDBC.SQL_VARBINARY.value
|
|
218
|
+
SQL_LONGVARBINARY: int = ConstantsDDBC.SQL_LONGVARBINARY.value
|
|
219
|
+
SQL_DATE: int = ConstantsDDBC.SQL_DATE.value
|
|
220
|
+
SQL_TIME: int = ConstantsDDBC.SQL_TIME.value
|
|
221
|
+
SQL_TIMESTAMP: int = ConstantsDDBC.SQL_TIMESTAMP.value
|
|
222
|
+
|
|
223
|
+
# Export GetInfo constants at module level
|
|
224
|
+
# Driver and database information
|
|
225
|
+
SQL_DRIVER_NAME: int = GetInfoConstants.SQL_DRIVER_NAME.value
|
|
226
|
+
SQL_DRIVER_VER: int = GetInfoConstants.SQL_DRIVER_VER.value
|
|
227
|
+
SQL_DRIVER_ODBC_VER: int = GetInfoConstants.SQL_DRIVER_ODBC_VER.value
|
|
228
|
+
SQL_DATA_SOURCE_NAME: int = GetInfoConstants.SQL_DATA_SOURCE_NAME.value
|
|
229
|
+
SQL_DATABASE_NAME: int = GetInfoConstants.SQL_DATABASE_NAME.value
|
|
230
|
+
SQL_SERVER_NAME: int = GetInfoConstants.SQL_SERVER_NAME.value
|
|
231
|
+
SQL_USER_NAME: int = GetInfoConstants.SQL_USER_NAME.value
|
|
232
|
+
|
|
233
|
+
# SQL conformance and support
|
|
234
|
+
SQL_SQL_CONFORMANCE: int = GetInfoConstants.SQL_SQL_CONFORMANCE.value
|
|
235
|
+
SQL_KEYWORDS: int = GetInfoConstants.SQL_KEYWORDS.value
|
|
236
|
+
SQL_IDENTIFIER_QUOTE_CHAR: int = GetInfoConstants.SQL_IDENTIFIER_QUOTE_CHAR.value
|
|
237
|
+
SQL_SEARCH_PATTERN_ESCAPE: int = GetInfoConstants.SQL_SEARCH_PATTERN_ESCAPE.value
|
|
238
|
+
|
|
239
|
+
# Catalog and schema support
|
|
240
|
+
SQL_CATALOG_TERM: int = GetInfoConstants.SQL_CATALOG_TERM.value
|
|
241
|
+
SQL_SCHEMA_TERM: int = GetInfoConstants.SQL_SCHEMA_TERM.value
|
|
242
|
+
SQL_TABLE_TERM: int = GetInfoConstants.SQL_TABLE_TERM.value
|
|
243
|
+
SQL_PROCEDURE_TERM: int = GetInfoConstants.SQL_PROCEDURE_TERM.value
|
|
244
|
+
|
|
245
|
+
# Transaction support
|
|
246
|
+
SQL_TXN_CAPABLE: int = GetInfoConstants.SQL_TXN_CAPABLE.value
|
|
247
|
+
SQL_DEFAULT_TXN_ISOLATION: int = GetInfoConstants.SQL_DEFAULT_TXN_ISOLATION.value
|
|
248
|
+
|
|
249
|
+
# Data type support
|
|
250
|
+
SQL_NUMERIC_FUNCTIONS: int = GetInfoConstants.SQL_NUMERIC_FUNCTIONS.value
|
|
251
|
+
SQL_STRING_FUNCTIONS: int = GetInfoConstants.SQL_STRING_FUNCTIONS.value
|
|
252
|
+
SQL_DATETIME_FUNCTIONS: int = GetInfoConstants.SQL_DATETIME_FUNCTIONS.value
|
|
253
|
+
|
|
254
|
+
# Limits
|
|
255
|
+
SQL_MAX_COLUMN_NAME_LEN: int = GetInfoConstants.SQL_MAX_COLUMN_NAME_LEN.value
|
|
256
|
+
SQL_MAX_TABLE_NAME_LEN: int = GetInfoConstants.SQL_MAX_TABLE_NAME_LEN.value
|
|
257
|
+
SQL_MAX_SCHEMA_NAME_LEN: int = GetInfoConstants.SQL_MAX_SCHEMA_NAME_LEN.value
|
|
258
|
+
SQL_MAX_CATALOG_NAME_LEN: int = GetInfoConstants.SQL_MAX_CATALOG_NAME_LEN.value
|
|
259
|
+
SQL_MAX_IDENTIFIER_LEN: int = GetInfoConstants.SQL_MAX_IDENTIFIER_LEN.value
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# Also provide a function to get all constants
|
|
263
|
+
def get_info_constants() -> Dict[str, int]:
|
|
264
|
+
"""
|
|
265
|
+
Returns a dictionary of all available GetInfo constants.
|
|
266
|
+
|
|
267
|
+
This provides all SQLGetInfo constants that can be used with the Connection.getinfo() method
|
|
268
|
+
to retrieve metadata about the database server and driver.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
dict: Dictionary mapping constant names to their integer values
|
|
272
|
+
"""
|
|
273
|
+
return {name: member.value for name, member in GetInfoConstants.__members__.items()}
|
|
274
|
+
|
|
275
|
+
# Create a custom module class that uses properties instead of __setattr__
|
|
276
|
+
class _MSSQLModule(types.ModuleType):
|
|
277
|
+
@property
|
|
278
|
+
def lowercase(self) -> bool:
|
|
279
|
+
"""Get the lowercase setting."""
|
|
280
|
+
return _settings.lowercase
|
|
281
|
+
|
|
282
|
+
@lowercase.setter
|
|
283
|
+
def lowercase(self, value: bool) -> None:
|
|
284
|
+
"""Set the lowercase setting."""
|
|
285
|
+
if not isinstance(value, bool):
|
|
286
|
+
raise ValueError("lowercase must be a boolean value")
|
|
287
|
+
with _settings_lock:
|
|
288
|
+
_settings.lowercase = value
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# Replace the current module with our custom module class
|
|
292
|
+
old_module: types.ModuleType = sys.modules[__name__]
|
|
293
|
+
new_module: _MSSQLModule = _MSSQLModule(__name__)
|
|
294
|
+
|
|
295
|
+
# Copy all existing attributes to the new module
|
|
296
|
+
for attr_name in dir(old_module):
|
|
297
|
+
if attr_name != "__class__":
|
|
298
|
+
try:
|
|
299
|
+
setattr(new_module, attr_name, getattr(old_module, attr_name))
|
|
300
|
+
except AttributeError:
|
|
301
|
+
pass
|
|
302
|
+
|
|
303
|
+
# Replace the module in sys.modules
|
|
304
|
+
sys.modules[__name__] = new_module
|
|
305
|
+
|
|
306
|
+
# Initialize property values
|
|
307
|
+
lowercase: bool = _settings.lowercase
|
mssql_python/auth.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Microsoft Corporation.
|
|
3
|
+
Licensed under the MIT license.
|
|
4
|
+
This module handles authentication for the mssql_python package.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import platform
|
|
8
|
+
import struct
|
|
9
|
+
from typing import Tuple, Dict, Optional, List
|
|
10
|
+
|
|
11
|
+
from mssql_python.logging import logger
|
|
12
|
+
from mssql_python.constants import AuthType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AADAuth:
|
|
16
|
+
"""Handles Azure Active Directory authentication"""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def get_token_struct(token: str) -> bytes:
|
|
20
|
+
"""Convert token to SQL Server compatible format"""
|
|
21
|
+
logger.debug('get_token_struct: Converting token to SQL Server format - token_length=%d chars', len(token))
|
|
22
|
+
token_bytes = token.encode("UTF-16-LE")
|
|
23
|
+
logger.debug('get_token_struct: Token encoded to UTF-16-LE - byte_length=%d', len(token_bytes))
|
|
24
|
+
return struct.pack(f"<I{len(token_bytes)}s", len(token_bytes), token_bytes)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def get_token(auth_type: str) -> bytes:
|
|
28
|
+
"""Get token using the specified authentication type"""
|
|
29
|
+
# Import Azure libraries inside method to support test mocking
|
|
30
|
+
# pylint: disable=import-outside-toplevel
|
|
31
|
+
try:
|
|
32
|
+
from azure.identity import (
|
|
33
|
+
DefaultAzureCredential,
|
|
34
|
+
DeviceCodeCredential,
|
|
35
|
+
InteractiveBrowserCredential,
|
|
36
|
+
)
|
|
37
|
+
from azure.core.exceptions import ClientAuthenticationError
|
|
38
|
+
except ImportError as e:
|
|
39
|
+
raise RuntimeError(
|
|
40
|
+
"Azure authentication libraries are not installed. "
|
|
41
|
+
"Please install with: pip install azure-identity azure-core"
|
|
42
|
+
) from e
|
|
43
|
+
|
|
44
|
+
# Mapping of auth types to credential classes
|
|
45
|
+
credential_map = {
|
|
46
|
+
"default": DefaultAzureCredential,
|
|
47
|
+
"devicecode": DeviceCodeCredential,
|
|
48
|
+
"interactive": InteractiveBrowserCredential,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
credential_class = credential_map[auth_type]
|
|
52
|
+
logger.info('get_token: Starting Azure AD authentication - auth_type=%s, credential_class=%s',
|
|
53
|
+
auth_type, credential_class.__name__)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
logger.debug('get_token: Creating credential instance - credential_class=%s', credential_class.__name__)
|
|
57
|
+
credential = credential_class()
|
|
58
|
+
logger.debug('get_token: Requesting token from Azure AD - scope=https://database.windows.net/.default')
|
|
59
|
+
token = credential.get_token("https://database.windows.net/.default").token
|
|
60
|
+
logger.info('get_token: Azure AD token acquired successfully - token_length=%d chars', len(token))
|
|
61
|
+
return AADAuth.get_token_struct(token)
|
|
62
|
+
except ClientAuthenticationError as e:
|
|
63
|
+
# Re-raise with more specific context about Azure AD authentication failure
|
|
64
|
+
logger.error('get_token: Azure AD authentication failed - credential_class=%s, error=%s',
|
|
65
|
+
credential_class.__name__, str(e))
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
f"Azure AD authentication failed for {credential_class.__name__}: {e}. "
|
|
68
|
+
f"This could be due to invalid credentials, missing environment variables, "
|
|
69
|
+
f"user cancellation, network issues, or unsupported configuration."
|
|
70
|
+
) from e
|
|
71
|
+
except Exception as e:
|
|
72
|
+
# Catch any other unexpected exceptions
|
|
73
|
+
logger.error('get_token: Unexpected error during credential creation - credential_class=%s, error=%s',
|
|
74
|
+
credential_class.__name__, str(e))
|
|
75
|
+
raise RuntimeError(
|
|
76
|
+
f"Failed to create {credential_class.__name__}: {e}"
|
|
77
|
+
) from e
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def process_auth_parameters(parameters: List[str]) -> Tuple[List[str], Optional[str]]:
|
|
81
|
+
"""
|
|
82
|
+
Process connection parameters and extract authentication type.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
parameters: List of connection string parameters
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Tuple[list, Optional[str]]: Modified parameters and authentication type
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If an invalid authentication type is provided
|
|
92
|
+
"""
|
|
93
|
+
logger.debug('process_auth_parameters: Processing %d connection parameters', len(parameters))
|
|
94
|
+
modified_parameters = []
|
|
95
|
+
auth_type = None
|
|
96
|
+
|
|
97
|
+
for param in parameters:
|
|
98
|
+
param = param.strip()
|
|
99
|
+
if not param:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
if "=" not in param:
|
|
103
|
+
modified_parameters.append(param)
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
key, value = param.split("=", 1)
|
|
107
|
+
key_lower = key.lower()
|
|
108
|
+
value_lower = value.lower()
|
|
109
|
+
|
|
110
|
+
if key_lower == "authentication":
|
|
111
|
+
# Check for supported authentication types and set auth_type accordingly
|
|
112
|
+
if value_lower == AuthType.INTERACTIVE.value:
|
|
113
|
+
auth_type = "interactive"
|
|
114
|
+
logger.debug('process_auth_parameters: Interactive authentication detected')
|
|
115
|
+
# Interactive authentication (browser-based); only append parameter for non-Windows
|
|
116
|
+
if platform.system().lower() == "windows":
|
|
117
|
+
logger.debug('process_auth_parameters: Windows platform - using native AADInteractive')
|
|
118
|
+
auth_type = None # Let Windows handle AADInteractive natively
|
|
119
|
+
|
|
120
|
+
elif value_lower == AuthType.DEVICE_CODE.value:
|
|
121
|
+
# Device code authentication (for devices without browser)
|
|
122
|
+
logger.debug('process_auth_parameters: Device code authentication detected')
|
|
123
|
+
auth_type = "devicecode"
|
|
124
|
+
elif value_lower == AuthType.DEFAULT.value:
|
|
125
|
+
# Default authentication (uses DefaultAzureCredential)
|
|
126
|
+
logger.debug('process_auth_parameters: Default Azure authentication detected')
|
|
127
|
+
auth_type = "default"
|
|
128
|
+
modified_parameters.append(param)
|
|
129
|
+
|
|
130
|
+
logger.debug('process_auth_parameters: Processing complete - auth_type=%s, param_count=%d',
|
|
131
|
+
auth_type, len(modified_parameters))
|
|
132
|
+
return modified_parameters, auth_type
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def remove_sensitive_params(parameters: List[str]) -> List[str]:
|
|
136
|
+
"""Remove sensitive parameters from connection string"""
|
|
137
|
+
logger.debug('remove_sensitive_params: Removing sensitive parameters - input_count=%d', len(parameters))
|
|
138
|
+
exclude_keys = [
|
|
139
|
+
"uid=",
|
|
140
|
+
"pwd=",
|
|
141
|
+
"encrypt=",
|
|
142
|
+
"trustservercertificate=",
|
|
143
|
+
"authentication=",
|
|
144
|
+
]
|
|
145
|
+
result = [
|
|
146
|
+
param
|
|
147
|
+
for param in parameters
|
|
148
|
+
if not any(param.lower().startswith(exclude) for exclude in exclude_keys)
|
|
149
|
+
]
|
|
150
|
+
logger.debug('remove_sensitive_params: Sensitive parameters removed - output_count=%d', len(result))
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_auth_token(auth_type: str) -> Optional[bytes]:
|
|
155
|
+
"""Get authentication token based on auth type"""
|
|
156
|
+
logger.debug('get_auth_token: Starting - auth_type=%s', auth_type)
|
|
157
|
+
if not auth_type:
|
|
158
|
+
logger.debug('get_auth_token: No auth_type specified, returning None')
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# Handle platform-specific logic for interactive auth
|
|
162
|
+
if auth_type == "interactive" and platform.system().lower() == "windows":
|
|
163
|
+
logger.debug('get_auth_token: Windows interactive auth - delegating to native handler')
|
|
164
|
+
return None # Let Windows handle AADInteractive natively
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
token = AADAuth.get_token(auth_type)
|
|
168
|
+
logger.info('get_auth_token: Token acquired successfully - auth_type=%s', auth_type)
|
|
169
|
+
return token
|
|
170
|
+
except (ValueError, RuntimeError) as e:
|
|
171
|
+
logger.warning('get_auth_token: Token acquisition failed - auth_type=%s, error=%s', auth_type, str(e))
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def process_connection_string(
|
|
176
|
+
connection_string: str,
|
|
177
|
+
) -> Tuple[str, Optional[Dict[int, bytes]]]:
|
|
178
|
+
"""
|
|
179
|
+
Process connection string and handle authentication.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
connection_string: The connection string to process
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Tuple[str, Optional[Dict]]: Processed connection string and attrs_before dict if needed
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
ValueError: If the connection string is invalid or empty
|
|
189
|
+
"""
|
|
190
|
+
logger.debug('process_connection_string: Starting - conn_str_length=%d', len(connection_string) if isinstance(connection_string, str) else 0)
|
|
191
|
+
# Check type first
|
|
192
|
+
if not isinstance(connection_string, str):
|
|
193
|
+
logger.error('process_connection_string: Invalid type - expected str, got %s', type(connection_string).__name__)
|
|
194
|
+
raise ValueError("Connection string must be a string")
|
|
195
|
+
|
|
196
|
+
# Then check if empty
|
|
197
|
+
if not connection_string:
|
|
198
|
+
logger.error('process_connection_string: Connection string is empty')
|
|
199
|
+
raise ValueError("Connection string cannot be empty")
|
|
200
|
+
|
|
201
|
+
parameters = connection_string.split(";")
|
|
202
|
+
logger.debug('process_connection_string: Split connection string - parameter_count=%d', len(parameters))
|
|
203
|
+
|
|
204
|
+
# Validate that there's at least one valid parameter
|
|
205
|
+
if not any("=" in param for param in parameters):
|
|
206
|
+
logger.error('process_connection_string: Invalid connection string format - no key=value pairs found')
|
|
207
|
+
raise ValueError("Invalid connection string format")
|
|
208
|
+
|
|
209
|
+
modified_parameters, auth_type = process_auth_parameters(parameters)
|
|
210
|
+
|
|
211
|
+
if auth_type:
|
|
212
|
+
logger.info('process_connection_string: Authentication type detected - auth_type=%s', auth_type)
|
|
213
|
+
modified_parameters = remove_sensitive_params(modified_parameters)
|
|
214
|
+
token_struct = get_auth_token(auth_type)
|
|
215
|
+
if token_struct:
|
|
216
|
+
logger.info('process_connection_string: Token authentication configured successfully - auth_type=%s', auth_type)
|
|
217
|
+
return ";".join(modified_parameters) + ";", {1256: token_struct}
|
|
218
|
+
else:
|
|
219
|
+
logger.warning('process_connection_string: Token acquisition failed, proceeding without token')
|
|
220
|
+
|
|
221
|
+
logger.debug('process_connection_string: Connection string processing complete - has_auth=%s', bool(auth_type))
|
|
222
|
+
return ";".join(modified_parameters) + ";", None
|