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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
"""
|
|
4
|
+
Lightweight phase-level profiling for the Python layer.
|
|
5
|
+
|
|
6
|
+
Usage in cursor.py:
|
|
7
|
+
from mssql_python.perf_timer import perf_phase
|
|
8
|
+
|
|
9
|
+
with perf_phase("py::execute::param_type_detection"):
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
Control from profiler script:
|
|
13
|
+
from mssql_python.perf_timer import enable, disable, get_stats, reset
|
|
14
|
+
|
|
15
|
+
Stats dict matches the C++ profiling format so both layers can be
|
|
16
|
+
printed with the same reporter. Entries use a "py::" prefix to
|
|
17
|
+
distinguish from C++ timers.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from typing import NamedTuple
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Counter(NamedTuple):
|
|
27
|
+
calls: int
|
|
28
|
+
total_ns: int
|
|
29
|
+
min_ns: int
|
|
30
|
+
max_ns: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Event(NamedTuple):
|
|
34
|
+
name: str
|
|
35
|
+
start_ns: int
|
|
36
|
+
duration_ns: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_enabled = False
|
|
40
|
+
_lock = threading.RLock()
|
|
41
|
+
_local = threading.local()
|
|
42
|
+
_window_start_ns = 0
|
|
43
|
+
_stats: dict[str, _Counter] = {}
|
|
44
|
+
_timeline: list[_Event] = []
|
|
45
|
+
_timeline_enabled = False
|
|
46
|
+
_epoch_ns: int = 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@contextmanager
|
|
50
|
+
def _bookkeeping():
|
|
51
|
+
# GC can run SQL-cleanup finalizers during our own allocations. Suppress only
|
|
52
|
+
# recursive samples on this thread, not the cleanup or ordinary nested phases.
|
|
53
|
+
depth = getattr(_local, "depth", 0)
|
|
54
|
+
_local.depth = depth + 1
|
|
55
|
+
try:
|
|
56
|
+
yield
|
|
57
|
+
finally:
|
|
58
|
+
_local.depth = depth
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def enable():
|
|
62
|
+
global _enabled, _window_start_ns
|
|
63
|
+
with _bookkeeping():
|
|
64
|
+
release = _lock.release
|
|
65
|
+
_lock.acquire()
|
|
66
|
+
try:
|
|
67
|
+
_window_start_ns = time.perf_counter_ns()
|
|
68
|
+
_enabled = True
|
|
69
|
+
finally:
|
|
70
|
+
release()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def disable():
|
|
74
|
+
global _enabled
|
|
75
|
+
with _bookkeeping():
|
|
76
|
+
release = _lock.release
|
|
77
|
+
_lock.acquire()
|
|
78
|
+
try:
|
|
79
|
+
_enabled = False
|
|
80
|
+
finally:
|
|
81
|
+
release()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_enabled() -> bool:
|
|
85
|
+
return _enabled
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def reset():
|
|
89
|
+
global _window_start_ns, _stats, _timeline
|
|
90
|
+
with _bookkeeping():
|
|
91
|
+
stats, timeline = {}, []
|
|
92
|
+
release = _lock.release
|
|
93
|
+
_lock.acquire()
|
|
94
|
+
try:
|
|
95
|
+
_window_start_ns = time.perf_counter_ns()
|
|
96
|
+
_stats = stats
|
|
97
|
+
_timeline = timeline
|
|
98
|
+
finally:
|
|
99
|
+
release()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def reset_stats_only():
|
|
103
|
+
global _window_start_ns, _stats
|
|
104
|
+
with _bookkeeping():
|
|
105
|
+
stats = {}
|
|
106
|
+
release = _lock.release
|
|
107
|
+
_lock.acquire()
|
|
108
|
+
try:
|
|
109
|
+
_window_start_ns = time.perf_counter_ns()
|
|
110
|
+
_stats = stats
|
|
111
|
+
finally:
|
|
112
|
+
release()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def enable_timeline():
|
|
116
|
+
global _timeline_enabled, _epoch_ns, _timeline
|
|
117
|
+
# Clear any previously recorded events when (re)setting the epoch, so every
|
|
118
|
+
# event in _timeline shares the current epoch. Otherwise a second
|
|
119
|
+
# enable_timeline() without an intervening reset() would leave stale events
|
|
120
|
+
# whose offsets were computed from an older epoch, corrupting the sort.
|
|
121
|
+
with _bookkeeping():
|
|
122
|
+
timeline = []
|
|
123
|
+
release = _lock.release
|
|
124
|
+
_lock.acquire()
|
|
125
|
+
try:
|
|
126
|
+
_timeline = timeline
|
|
127
|
+
_epoch_ns = time.perf_counter_ns()
|
|
128
|
+
_timeline_enabled = True
|
|
129
|
+
finally:
|
|
130
|
+
release()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def disable_timeline():
|
|
134
|
+
global _timeline_enabled
|
|
135
|
+
with _bookkeeping():
|
|
136
|
+
release = _lock.release
|
|
137
|
+
_lock.acquire()
|
|
138
|
+
try:
|
|
139
|
+
_timeline_enabled = False
|
|
140
|
+
finally:
|
|
141
|
+
release()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_timeline() -> list[dict]:
|
|
145
|
+
with _bookkeeping():
|
|
146
|
+
# Built-in container copies hold the GIL on supported CPython builds.
|
|
147
|
+
# Immutable entries stay stable; no profiler lock surrounds GC allocations.
|
|
148
|
+
snapshot = _timeline.copy()
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
"name": ev.name,
|
|
152
|
+
"start_us": ev.start_ns // 1000,
|
|
153
|
+
"duration_us": ev.duration_ns // 1000,
|
|
154
|
+
}
|
|
155
|
+
for ev in snapshot
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_stats() -> dict:
|
|
160
|
+
with _bookkeeping():
|
|
161
|
+
snapshot = _stats.copy()
|
|
162
|
+
out = {}
|
|
163
|
+
for name, s in snapshot.items():
|
|
164
|
+
# Keep fractional microseconds when converting accumulated samples.
|
|
165
|
+
out[name] = {
|
|
166
|
+
"calls": s.calls,
|
|
167
|
+
"total_us": s.total_ns / 1000.0,
|
|
168
|
+
"min_us": s.min_ns / 1000.0,
|
|
169
|
+
"max_us": s.max_ns / 1000.0,
|
|
170
|
+
}
|
|
171
|
+
return out
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _NullPhase:
|
|
175
|
+
"""No-op context manager returned by perf_phase when profiling is disabled.
|
|
176
|
+
|
|
177
|
+
A single shared instance is reused so the disabled path costs only a function
|
|
178
|
+
call plus two slot method calls, avoiding the generator + _GeneratorContextManager
|
|
179
|
+
allocation of an @contextmanager that every instrumented call site would
|
|
180
|
+
otherwise pay even when profiling is off.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
__slots__ = ()
|
|
184
|
+
|
|
185
|
+
def __enter__(self):
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
def __exit__(self, *exc):
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class _Phase:
|
|
193
|
+
"""Times one phase and records it on exit (used only when enabled).
|
|
194
|
+
|
|
195
|
+
Recording happens in __exit__, which always runs even if the wrapped block
|
|
196
|
+
raises, so an exception can't silently drop the sample and desync the Python
|
|
197
|
+
call counts from the C++ ones.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
__slots__ = ("_name", "_t0")
|
|
201
|
+
|
|
202
|
+
def __init__(self, name: str):
|
|
203
|
+
self._name = name
|
|
204
|
+
|
|
205
|
+
def __enter__(self):
|
|
206
|
+
self._t0 = perf_start()
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def __exit__(self, *exc):
|
|
210
|
+
perf_stop(self._name, self._t0)
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
_NULL_PHASE = _NullPhase()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def perf_phase(name: str):
|
|
218
|
+
if not _enabled:
|
|
219
|
+
return _NULL_PHASE
|
|
220
|
+
return _Phase(name)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def perf_start() -> int:
|
|
224
|
+
if not _enabled:
|
|
225
|
+
return 0
|
|
226
|
+
return time.perf_counter_ns()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def perf_stop(name: str, t0: int):
|
|
230
|
+
# t0 == 0 means perf_start() ran while disabled (or was never called); a
|
|
231
|
+
# falsy start has no valid interval, so record nothing rather than a bogus
|
|
232
|
+
# "now - 0" duration.
|
|
233
|
+
if not _enabled or not t0:
|
|
234
|
+
return
|
|
235
|
+
_record(name, time.perf_counter_ns() - t0, t0)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _record(name: str, elapsed: int, start_ns: int = 0):
|
|
239
|
+
if getattr(_local, "depth", 0):
|
|
240
|
+
return
|
|
241
|
+
with _bookkeeping():
|
|
242
|
+
# Prebind the no-argument release: RLock.__exit__ and method lookup can
|
|
243
|
+
# allocate before unlocking, which is unsafe around GC finalizers.
|
|
244
|
+
release = _lock.release
|
|
245
|
+
while True:
|
|
246
|
+
_lock.acquire()
|
|
247
|
+
try:
|
|
248
|
+
if not _enabled or (start_ns and start_ns < _window_start_ns):
|
|
249
|
+
return
|
|
250
|
+
stats = _stats
|
|
251
|
+
entry = stats.get(name)
|
|
252
|
+
window = _window_start_ns
|
|
253
|
+
timeline = _timeline
|
|
254
|
+
epoch = _epoch_ns
|
|
255
|
+
record_event = _timeline_enabled and start_ns and start_ns >= epoch
|
|
256
|
+
finally:
|
|
257
|
+
release()
|
|
258
|
+
|
|
259
|
+
# Allocate before locking: GC may run a finalizer that waits for SQL
|
|
260
|
+
# on another thread, which must be able to finish its own recording.
|
|
261
|
+
updated = (
|
|
262
|
+
_Counter(1, elapsed, elapsed, elapsed)
|
|
263
|
+
if entry is None
|
|
264
|
+
else _Counter(
|
|
265
|
+
entry.calls + 1,
|
|
266
|
+
entry.total_ns + elapsed,
|
|
267
|
+
min(entry.min_ns, elapsed),
|
|
268
|
+
max(entry.max_ns, elapsed),
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
event = _Event(name, start_ns - epoch, elapsed) if record_event else None
|
|
272
|
+
|
|
273
|
+
_lock.acquire()
|
|
274
|
+
try:
|
|
275
|
+
# Revalidate after allocations and any callbacks they triggered.
|
|
276
|
+
if not _enabled or window != _window_start_ns or stats is not _stats:
|
|
277
|
+
return
|
|
278
|
+
if stats.get(name) is not entry:
|
|
279
|
+
continue
|
|
280
|
+
stats[name] = updated
|
|
281
|
+
# A timeline-only restart must not discard valid aggregate samples.
|
|
282
|
+
if event is not None and _timeline_enabled and timeline is _timeline:
|
|
283
|
+
timeline.append(event)
|
|
284
|
+
return
|
|
285
|
+
finally:
|
|
286
|
+
release()
|
mssql_python/pooling.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Microsoft Corporation.
|
|
3
|
+
Licensed under the MIT license.
|
|
4
|
+
This module provides connection pooling functionality for the mssql_python package.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import atexit
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Dict
|
|
10
|
+
|
|
11
|
+
from mssql_python import ddbc_bindings
|
|
12
|
+
from mssql_python.logging import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PoolingManager:
|
|
16
|
+
"""
|
|
17
|
+
Manages connection pooling for the mssql_python package.
|
|
18
|
+
|
|
19
|
+
This class provides thread-safe connection pooling functionality using the
|
|
20
|
+
underlying DDBC bindings. It follows a singleton pattern with class-level
|
|
21
|
+
state management.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_enabled: bool = False
|
|
25
|
+
_initialized: bool = False
|
|
26
|
+
_pools_closed: bool = False # Track if pools have been closed
|
|
27
|
+
_lock: threading.Lock = threading.Lock()
|
|
28
|
+
_config: Dict[str, int] = {"max_size": 100, "idle_timeout": 600}
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def enable(cls, max_size: int = 100, idle_timeout: int = 600) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Enable connection pooling with specified parameters.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
max_size: Maximum number of connections in the pool (default: 100)
|
|
37
|
+
idle_timeout: Timeout in seconds for idle connections (default: 600)
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: If parameters are invalid (max_size <= 0 or idle_timeout < 0)
|
|
41
|
+
"""
|
|
42
|
+
logger.debug(
|
|
43
|
+
"PoolingManager.enable: Attempting to enable pooling - max_size=%d, idle_timeout=%d",
|
|
44
|
+
max_size,
|
|
45
|
+
idle_timeout,
|
|
46
|
+
)
|
|
47
|
+
with cls._lock:
|
|
48
|
+
if cls._enabled:
|
|
49
|
+
logger.debug("PoolingManager.enable: Pooling already enabled, skipping")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if max_size <= 0 or idle_timeout < 0:
|
|
53
|
+
logger.error(
|
|
54
|
+
"PoolingManager.enable: Invalid parameters - max_size=%d, idle_timeout=%d",
|
|
55
|
+
max_size,
|
|
56
|
+
idle_timeout,
|
|
57
|
+
)
|
|
58
|
+
raise ValueError("Invalid pooling parameters")
|
|
59
|
+
|
|
60
|
+
logger.info(
|
|
61
|
+
"PoolingManager.enable: Enabling connection pooling - max_size=%d, idle_timeout=%d seconds",
|
|
62
|
+
max_size,
|
|
63
|
+
idle_timeout,
|
|
64
|
+
)
|
|
65
|
+
# enable_pooling() only configures the connection-pool manager; it
|
|
66
|
+
# does not load the native driver, so it must not resolve/freeze
|
|
67
|
+
# the ODBC provider (an explicit pooling() before selecting a
|
|
68
|
+
# provider would otherwise lock in the default prematurely).
|
|
69
|
+
ddbc_bindings.enable_pooling(max_size, idle_timeout)
|
|
70
|
+
cls._config["max_size"] = max_size
|
|
71
|
+
cls._config["idle_timeout"] = idle_timeout
|
|
72
|
+
cls._enabled = True
|
|
73
|
+
# Re-arm the disable guard: enable_pooling() calls setAccepting(true)
|
|
74
|
+
# on the native manager, so the Python state must mirror that a
|
|
75
|
+
# fresh disable() is once again required to disarm it. Without this
|
|
76
|
+
# reset, an enable -> disable -> enable -> disable sequence would
|
|
77
|
+
# leave _pools_closed True after the first disable, causing the
|
|
78
|
+
# second disable to skip disable_pooling() and desync from the
|
|
79
|
+
# native manager (which is still accepting).
|
|
80
|
+
cls._pools_closed = False
|
|
81
|
+
cls._initialized = True
|
|
82
|
+
logger.info("PoolingManager.enable: Connection pooling enabled successfully")
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def disable(cls) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Disable connection pooling and clean up resources.
|
|
88
|
+
|
|
89
|
+
This method safely disables pooling and closes existing connections.
|
|
90
|
+
It can be called multiple times safely.
|
|
91
|
+
"""
|
|
92
|
+
logger.debug("PoolingManager.disable: Attempting to disable pooling")
|
|
93
|
+
with cls._lock:
|
|
94
|
+
if (
|
|
95
|
+
cls._enabled and not cls._pools_closed
|
|
96
|
+
): # Only cleanup if enabled and not already closed
|
|
97
|
+
logger.info("PoolingManager.disable: Closing connection pools")
|
|
98
|
+
ddbc_bindings.disable_pooling()
|
|
99
|
+
logger.info("PoolingManager.disable: Connection pools closed successfully")
|
|
100
|
+
else:
|
|
101
|
+
logger.debug("PoolingManager.disable: Pooling already disabled or closed")
|
|
102
|
+
cls._pools_closed = True
|
|
103
|
+
cls._enabled = False
|
|
104
|
+
cls._initialized = True
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def is_enabled(cls) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Check if connection pooling is currently enabled.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
bool: True if pooling is enabled, False otherwise
|
|
113
|
+
"""
|
|
114
|
+
return cls._enabled
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def is_initialized(cls) -> bool:
|
|
118
|
+
"""
|
|
119
|
+
Check if the pooling manager has been initialized.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
bool: True if initialized (either enabled or disabled), False otherwise
|
|
123
|
+
"""
|
|
124
|
+
return cls._initialized
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def _reset_for_testing(cls) -> None:
|
|
128
|
+
"""Reset pooling state - for testing purposes only"""
|
|
129
|
+
with cls._lock:
|
|
130
|
+
cls._enabled = False
|
|
131
|
+
cls._initialized = False
|
|
132
|
+
cls._pools_closed = False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@atexit.register
|
|
136
|
+
def shutdown_pooling():
|
|
137
|
+
"""
|
|
138
|
+
Shutdown pooling during application exit.
|
|
139
|
+
|
|
140
|
+
This function is registered with atexit to ensure proper cleanup of
|
|
141
|
+
connection pools when the application terminates.
|
|
142
|
+
"""
|
|
143
|
+
logger.debug("shutdown_pooling: atexit cleanup triggered")
|
|
144
|
+
with PoolingManager._lock:
|
|
145
|
+
if PoolingManager._enabled and not PoolingManager._pools_closed:
|
|
146
|
+
logger.info("shutdown_pooling: Closing connection pools during application exit")
|
|
147
|
+
ddbc_bindings.close_pooling()
|
|
148
|
+
PoolingManager._pools_closed = True
|
|
149
|
+
logger.info("shutdown_pooling: Connection pools closed successfully")
|
|
150
|
+
else:
|
|
151
|
+
logger.debug("shutdown_pooling: No active pools to close")
|
mssql_python/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|