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.
- mssql_python/__init__.py +662 -0
- mssql_python/async_query/__init__.py +40 -0
- mssql_python/async_query/_connection_context.py +39 -0
- mssql_python/async_query/_native.py +23 -0
- mssql_python/async_query/async_connection.py +160 -0
- mssql_python/async_query/async_cursor.py +230 -0
- mssql_python/async_query/async_execute.py +115 -0
- mssql_python/async_query/async_fetch.py +134 -0
- mssql_python/async_query/exception_translator.py +114 -0
- mssql_python/auth.py +913 -0
- mssql_python/connection.py +2357 -0
- mssql_python/connection_string_builder.py +114 -0
- mssql_python/connection_string_parser.py +424 -0
- mssql_python/constants.py +701 -0
- mssql_python/cursor.py +4188 -0
- mssql_python/db_connection.py +98 -0
- mssql_python/ddbc_bindings.cp315-arm64.pyd +0 -0
- mssql_python/ddbc_bindings.py +215 -0
- mssql_python/decimal_config.py +114 -0
- mssql_python/exceptions.py +588 -0
- mssql_python/helpers.py +416 -0
- mssql_python/logging.py +714 -0
- mssql_python/mssql_python.pyi +481 -0
- mssql_python/msvcp140.dll +0 -0
- mssql_python/odbc_provider.py +251 -0
- mssql_python/parameter_helper.py +488 -0
- mssql_python/perf_timer.py +286 -0
- mssql_python/pooling.py +151 -0
- mssql_python/py.typed +1 -0
- mssql_python/row.py +398 -0
- mssql_python/type.py +160 -0
- mssql_python-1.16.0.dist-info/METADATA +102 -0
- mssql_python-1.16.0.dist-info/RECORD +36 -0
- mssql_python-1.16.0.dist-info/WHEEL +5 -0
- mssql_python-1.16.0.dist-info/licenses/LICENSE +47 -0
- mssql_python-1.16.0.dist-info/top_level.txt +1 -0
mssql_python/helpers.py
ADDED
|
@@ -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
|