mssql-python 1.16.0__cp315-cp315-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.
@@ -0,0 +1,416 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation.
3
+ Licensed under the MIT license.
4
+ This module provides helper functions for the mssql_python package.
5
+ """
6
+
7
+ import re
8
+ import threading
9
+ import locale
10
+ from typing import Any, Union, Tuple, Optional
11
+ from mssql_python import ddbc_bindings
12
+ from mssql_python.exceptions import raise_exception
13
+ from mssql_python.logging import logger
14
+ from mssql_python.constants import ConstantsDDBC
15
+
16
+ # normalize_architecture import removed as it's unused
17
+
18
+
19
+ def check_error(handle_type: int, handle: Any, ret: int) -> None:
20
+ """
21
+ Check for errors and raise an exception if an error is found.
22
+
23
+ Args:
24
+ handle_type: The type of the handle (e.g., SQL_HANDLE_ENV, SQL_HANDLE_DBC).
25
+ handle: The SqlHandle object associated with the operation.
26
+ ret: The return code from the DDBC function call.
27
+
28
+ Raises:
29
+ RuntimeError: If an error is found.
30
+ """
31
+ if ret < 0:
32
+ logger.debug(
33
+ "check_error: Error detected - handle_type=%d, return_code=%d", handle_type, ret
34
+ )
35
+ error_info = ddbc_bindings.DDBCSQLCheckError(handle_type, handle, ret)
36
+ logger.error("Error: %s", error_info.ddbcErrorMsg)
37
+ logger.debug("check_error: SQL state=%s", error_info.sqlState)
38
+ raise_exception(error_info.sqlState, error_info.ddbcErrorMsg)
39
+
40
+
41
+ def sanitize_connection_string(conn_str: str) -> str:
42
+ """
43
+ Sanitize the connection string by removing sensitive information.
44
+
45
+ Delegates to the parser-based implementation in connection_string_parser
46
+ which correctly handles ODBC braced values (e.g. PWD={Top;Secret}).
47
+
48
+ Args:
49
+ conn_str (str): The connection string to sanitize.
50
+ Returns:
51
+ str: The sanitized connection string.
52
+ """
53
+ from mssql_python.connection_string_parser import (
54
+ sanitize_connection_string as _sanitize,
55
+ )
56
+
57
+ return _sanitize(conn_str)
58
+
59
+
60
+ def sanitize_user_input(user_input: str, max_length: int = 50) -> str:
61
+ """
62
+ Sanitize user input for safe logging by removing control characters,
63
+ limiting length, and ensuring safe characters only.
64
+
65
+ Args:
66
+ user_input (str): The user input to sanitize.
67
+ max_length (int): Maximum length of the sanitized output.
68
+
69
+ Returns:
70
+ str: The sanitized string safe for logging.
71
+ """
72
+ logger.debug(
73
+ "sanitize_user_input: Sanitizing input (type=%s, length=%d)",
74
+ type(user_input).__name__,
75
+ len(user_input) if isinstance(user_input, str) else 0,
76
+ )
77
+ if not isinstance(user_input, str):
78
+ logger.debug("sanitize_user_input: Non-string input detected")
79
+ return "<non-string>"
80
+
81
+ # Remove control characters and non-printable characters
82
+ # Allow alphanumeric, dash, underscore, and dot (common in encoding names)
83
+ sanitized = re.sub(r"[^\w\-\.]", "", user_input)
84
+
85
+ # Limit length to prevent log flooding
86
+ was_truncated = False
87
+ if len(sanitized) > max_length:
88
+ sanitized = sanitized[:max_length] + "..."
89
+ was_truncated = True
90
+
91
+ # Return placeholder if nothing remains after sanitization
92
+ result = sanitized if sanitized else "<invalid>"
93
+ logger.debug(
94
+ "sanitize_user_input: Result length=%d, truncated=%s", len(result), str(was_truncated)
95
+ )
96
+ return result
97
+
98
+
99
+ def validate_attribute_value(
100
+ attribute: Union[int, str],
101
+ value: Union[int, str, bytes, bytearray],
102
+ is_connected: bool = True,
103
+ sanitize_logs: bool = True,
104
+ max_log_length: int = 50,
105
+ ) -> Tuple[bool, Optional[str], str, str]:
106
+ """
107
+ Validates attribute and value pairs for connection attributes.
108
+
109
+ Performs basic type checking and validation of ODBC connection attributes.
110
+
111
+ Args:
112
+ attribute (int): The connection attribute to validate (SQL_ATTR_*)
113
+ value: The value to set for the attribute (int, str, bytes, or bytearray)
114
+ is_connected (bool): Whether the connection is already established
115
+ sanitize_logs (bool): Whether to include sanitized versions for logging
116
+ max_log_length (int): Maximum length of sanitized output for logging
117
+
118
+ Returns:
119
+ tuple: (is_valid, error_message, sanitized_attribute, sanitized_value)
120
+ """
121
+ logger.debug(
122
+ "validate_attribute_value: Validating attribute=%s, value_type=%s, is_connected=%s",
123
+ str(attribute),
124
+ type(value).__name__,
125
+ str(is_connected),
126
+ )
127
+
128
+ # Sanitize a value for logging
129
+ def _sanitize_for_logging(input_val: Any, max_length: int = max_log_length) -> str:
130
+ if not isinstance(input_val, str):
131
+ try:
132
+ input_val = str(input_val)
133
+ except (TypeError, ValueError):
134
+ return "<non-string>"
135
+
136
+ # Allow alphanumeric, dash, underscore, and dot
137
+ sanitized = re.sub(r"[^\w\-\.]", "", input_val)
138
+
139
+ # Limit length
140
+ if len(sanitized) > max_length:
141
+ sanitized = sanitized[:max_length] + "..."
142
+
143
+ return sanitized if sanitized else "<invalid>"
144
+
145
+ # Create sanitized versions for logging
146
+ sanitized_attr = _sanitize_for_logging(attribute) if sanitize_logs else str(attribute)
147
+ sanitized_val = _sanitize_for_logging(value) if sanitize_logs else str(value)
148
+
149
+ # Basic attribute validation - must be an integer
150
+ if not isinstance(attribute, int):
151
+ logger.debug(
152
+ "validate_attribute_value: Attribute not an integer - type=%s", type(attribute).__name__
153
+ )
154
+ return (
155
+ False,
156
+ f"Attribute must be an integer, got {type(attribute).__name__}",
157
+ sanitized_attr,
158
+ sanitized_val,
159
+ )
160
+
161
+ # Define driver-level attributes that are supported
162
+ supported_attributes = [
163
+ ConstantsDDBC.SQL_ATTR_ACCESS_MODE.value,
164
+ ConstantsDDBC.SQL_ATTR_CONNECTION_TIMEOUT.value,
165
+ ConstantsDDBC.SQL_ATTR_CURRENT_CATALOG.value,
166
+ ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value,
167
+ ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value,
168
+ ConstantsDDBC.SQL_ATTR_TXN_ISOLATION.value,
169
+ ]
170
+
171
+ # Check if attribute is supported
172
+ if attribute not in supported_attributes:
173
+ logger.debug("validate_attribute_value: Unsupported attribute - attr=%d", attribute)
174
+ return (
175
+ False,
176
+ f"Unsupported attribute: {attribute}",
177
+ sanitized_attr,
178
+ sanitized_val,
179
+ )
180
+
181
+ # Check timing constraints for these specific attributes
182
+ before_only_attributes = [
183
+ ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value,
184
+ ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value,
185
+ ]
186
+
187
+ # Check if attribute can be set at the current connection state
188
+ if is_connected and attribute in before_only_attributes:
189
+ logger.debug(
190
+ "validate_attribute_value: Timing violation - attr=%d cannot be set after connection",
191
+ attribute,
192
+ )
193
+ return (
194
+ False,
195
+ (
196
+ f"Attribute {attribute} must be set before connection establishment. "
197
+ "Use the attrs_before parameter when creating the connection."
198
+ ),
199
+ sanitized_attr,
200
+ sanitized_val,
201
+ )
202
+
203
+ # Basic value type validation
204
+ if isinstance(value, int):
205
+ # For integer values, check if negative (login timeout can be -1 for default)
206
+ if value < 0 and attribute != ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value:
207
+ return (
208
+ False,
209
+ f"Integer value cannot be negative: {value}",
210
+ sanitized_attr,
211
+ sanitized_val,
212
+ )
213
+
214
+ elif isinstance(value, str):
215
+ # Basic string length check
216
+ max_string_size = 8192 # 8KB maximum
217
+ if len(value) > max_string_size:
218
+ return (
219
+ False,
220
+ f"String value too large: {len(value)} bytes (max {max_string_size})",
221
+ sanitized_attr,
222
+ sanitized_val,
223
+ )
224
+
225
+ elif isinstance(value, (bytes, bytearray)):
226
+ # Basic binary length check
227
+ max_binary_size = 32768 # 32KB maximum
228
+ if len(value) > max_binary_size:
229
+ return (
230
+ False,
231
+ f"Binary value too large: {len(value)} bytes (max {max_binary_size})",
232
+ sanitized_attr,
233
+ sanitized_val,
234
+ )
235
+
236
+ else:
237
+ # Reject unsupported value types
238
+ return (
239
+ False,
240
+ f"Unsupported attribute value type: {type(value).__name__}",
241
+ sanitized_attr,
242
+ sanitized_val,
243
+ )
244
+
245
+ # All basic validations passed
246
+ logger.debug(
247
+ "validate_attribute_value: Validation passed - attr=%d, value_type=%s",
248
+ attribute,
249
+ type(value).__name__,
250
+ )
251
+ return True, None, sanitized_attr, sanitized_val
252
+
253
+
254
+ _PYCORE_CONNECTION_KEY_MAP = {
255
+ "uid": "user_name",
256
+ "pwd": "password",
257
+ "trusted_connection": "trusted_connection",
258
+ "authentication": "authentication",
259
+ "server": "server",
260
+ "addr": "server",
261
+ "address": "server",
262
+ "database": "database",
263
+ "applicationintent": "application_intent",
264
+ "encrypt": "encryption",
265
+ "trustservercertificate": "trust_server_certificate",
266
+ "trust_server_certificate": "trust_server_certificate",
267
+ "hostnameincertificate": "host_name_in_certificate",
268
+ "servercertificate": "server_certificate",
269
+ "serverspn": "server_spn",
270
+ "multisubnetfailover": "multi_subnet_failover",
271
+ "ipaddresspreference": "ip_address_preference",
272
+ "keepalive": "keep_alive",
273
+ "keepaliveinterval": "keep_alive_interval",
274
+ "packetsize": "packet_size",
275
+ "packet size": "packet_size",
276
+ "connectretrycount": "connect_retry_count",
277
+ "connectretryinterval": "connect_retry_interval",
278
+ }
279
+
280
+ _PYCORE_INTEGER_KEYS = {
281
+ "packet_size",
282
+ "connect_retry_count",
283
+ "connect_retry_interval",
284
+ "keep_alive",
285
+ "keep_alive_interval",
286
+ }
287
+
288
+ _PYCORE_STRICT_INTEGER_RANGES = {
289
+ "connect_retry_count": (0, 255),
290
+ "connect_retry_interval": (1, 60),
291
+ "keep_alive": (0, 2**32 - 1),
292
+ "keep_alive_interval": (0, 2**32 - 1),
293
+ }
294
+
295
+ _PYCORE_SECONDS_TO_MILLISECONDS_KEYS = {
296
+ "keep_alive",
297
+ "keep_alive_interval",
298
+ }
299
+
300
+ _PYCORE_UINT32_MAX = 2**32 - 1
301
+
302
+
303
+ def connstr_to_pycore_params(params: dict, *, strict: bool = False) -> dict:
304
+ """Translate parsed ODBC connection-string parameters for mssql-py-core.
305
+
306
+ Used by async connection setup and by bulk copy when it opens a separate
307
+ mssql-py-core connection. Keys are mapped to py-core's snake_case format,
308
+ numeric strings are converted to integers, and synonym precedence is
309
+ preserved.
310
+
311
+ Unsupported keys and invalid integers are dropped by default. When
312
+ ``strict`` is enabled, they raise ``ValueError`` instead.
313
+ """
314
+ # Only keys listed below are forwarded to py-core.
315
+ # Unknown/reserved keys (app, workstationid, language, connect_timeout,
316
+ # mars_connection) are silently dropped here. In the normal connect()
317
+ # path the parser validates keywords first (validate_keywords=True),
318
+ # but bulkcopy parses with validation off, so this mapping is the
319
+ # authoritative filter in that path.
320
+ pycore_params: dict = {}
321
+ seen_pycore_keys = set()
322
+
323
+ for connstr_key, raw_value in params.items():
324
+ pycore_key = _PYCORE_CONNECTION_KEY_MAP.get(connstr_key)
325
+ if pycore_key is None or raw_value is None:
326
+ continue
327
+
328
+ # First-wins: match ODBC behaviour — first synonym in the
329
+ # connection string takes precedence (e.g. Addr before Server).
330
+ if pycore_key in seen_pycore_keys:
331
+ continue
332
+ seen_pycore_keys.add(pycore_key)
333
+
334
+ # ODBC values are always strings; py-core expects native types for int keys.
335
+ # Boolean params (trust_server_certificate, multi_subnet_failover) are passed
336
+ # as strings — all Yes/No validation is in connection.rs for single-location
337
+ # consistency with Encrypt, ApplicationIntent, IPAddressPreference, etc.
338
+ if pycore_key in _PYCORE_INTEGER_KEYS:
339
+ # Numeric params (timeouts, packet size, etc.) — skip on bad input
340
+ try:
341
+ value = int(raw_value)
342
+ except (ValueError, TypeError):
343
+ if strict:
344
+ raise ValueError(
345
+ f"Connection parameter '{connstr_key}' must be an integer"
346
+ ) from None
347
+ continue
348
+
349
+ if strict and pycore_key in _PYCORE_STRICT_INTEGER_RANGES:
350
+ minimum, maximum = _PYCORE_STRICT_INTEGER_RANGES[pycore_key]
351
+ if not minimum <= value <= maximum:
352
+ raise ValueError(
353
+ f"Connection parameter '{connstr_key}' must be between "
354
+ f"{minimum} and {maximum}"
355
+ )
356
+
357
+ if pycore_key in _PYCORE_SECONDS_TO_MILLISECONDS_KEYS:
358
+ value = min(value * 1000, _PYCORE_UINT32_MAX)
359
+
360
+ pycore_params[pycore_key] = value
361
+ else:
362
+ # String params (server, database, encryption, etc.) — pass through
363
+ pycore_params[pycore_key] = raw_value
364
+
365
+ if strict:
366
+ unsupported = sorted(set(params) - set(_PYCORE_CONNECTION_KEY_MAP))
367
+ if unsupported:
368
+ names = ", ".join(unsupported)
369
+ raise ValueError(
370
+ f"Connection parameters are not supported for mssql-py-core connections: {names}"
371
+ )
372
+
373
+ return pycore_params
374
+
375
+
376
+ # Settings functionality moved here to avoid circular imports
377
+
378
+ # Initialize the locale setting only once at module import time
379
+ # This avoids thread-safety issues with locale
380
+ _default_decimal_separator: str = "."
381
+ try:
382
+ # Get the locale setting once during module initialization
383
+ locale_separator = locale.localeconv()["decimal_point"]
384
+ if locale_separator and len(locale_separator) == 1:
385
+ _default_decimal_separator = locale_separator
386
+ except (AttributeError, KeyError, TypeError, ValueError):
387
+ pass # Keep the default "." if locale access fails
388
+
389
+
390
+ class Settings:
391
+ """
392
+ Settings class for mssql_python package configuration.
393
+
394
+ This class holds global settings that affect the behavior of the package,
395
+ including lowercase column names, decimal separator, and UUID handling.
396
+ """
397
+
398
+ def __init__(self) -> None:
399
+ self.lowercase: bool = False
400
+ # Use the pre-determined separator - no locale access here
401
+ self.decimal_separator: str = _default_decimal_separator
402
+ # Controls whether UNIQUEIDENTIFIER columns return uuid.UUID (True)
403
+ # or str (False). Default True returns native uuid.UUID objects.
404
+ # Set to False to return str for pyodbc-compatible migration.
405
+ self.native_uuid: bool = True
406
+
407
+
408
+ # Global settings instance
409
+ _settings: Settings = Settings()
410
+ _settings_lock: threading.Lock = threading.Lock()
411
+
412
+
413
+ def get_settings() -> Settings:
414
+ """Return the global settings object"""
415
+ with _settings_lock:
416
+ return _settings