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,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()
@@ -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
+