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