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,39 @@
1
+ """Build the internal py-core context for asynchronous connections."""
2
+
3
+ from typing import Any
4
+
5
+ from ..connection_string_parser import _ConnectionStringParser
6
+ from ..exceptions import InterfaceError, NotSupportedError
7
+ from ..helpers import connstr_to_pycore_params
8
+
9
+
10
+ def build_async_connection_context(connection_str: str, timeout: int) -> dict[str, Any]:
11
+ """Parse and validate a public connection string for py-core."""
12
+ if not isinstance(connection_str, str):
13
+ raise TypeError("connection_str must be a string")
14
+ if "\x00" in connection_str:
15
+ raise InterfaceError(
16
+ driver_error="Connection string must not contain a NUL (\\x00) character.",
17
+ ddbc_error="Embedded NUL in connection string.",
18
+ )
19
+ if isinstance(timeout, bool) or not isinstance(timeout, int):
20
+ raise TypeError("Login timeout must be an integer")
21
+ if timeout < 0:
22
+ raise ValueError("Login timeout cannot be negative")
23
+
24
+ parser = _ConnectionStringParser(validate_keywords=True)
25
+ params = parser._parse(connection_str)
26
+ if not any(params.get(key) for key in ("server", "addr", "address")):
27
+ raise ValueError("SERVER parameter is required in connection string")
28
+
29
+ authentication = params.get("authentication", "").strip()
30
+ if authentication and authentication.lower() != "sqlpassword":
31
+ raise NotSupportedError(
32
+ f"Authentication mode '{authentication}' is not supported for async queries",
33
+ "Use Authentication=SqlPassword or Trusted_Connection for async queries",
34
+ )
35
+
36
+ context = connstr_to_pycore_params(params, strict=True)
37
+ if timeout > 0:
38
+ context["connect_timeout"] = timeout
39
+ return context
@@ -0,0 +1,23 @@
1
+ """Native mssql-py-core dependency boundary for asynchronous queries."""
2
+
3
+ from importlib import import_module
4
+ from types import ModuleType
5
+
6
+
7
+ def load_py_core() -> ModuleType:
8
+ """Load the PyO3 extension that owns asynchronous TDS operations."""
9
+ try:
10
+ py_core = import_module("mssql_py_core")
11
+ except ImportError as exc:
12
+ raise ImportError(
13
+ "Async query support requires the mssql-python-rs dependency "
14
+ "that provides mssql_py_core."
15
+ ) from exc
16
+
17
+ required_types = ("PyAsyncConnection", "PyAsyncCursor")
18
+ missing_types = [name for name in required_types if not hasattr(py_core, name)]
19
+ if missing_types:
20
+ missing = ", ".join(missing_types)
21
+ raise ImportError(f"mssql_py_core does not provide the required async types: {missing}")
22
+
23
+ return py_core
@@ -0,0 +1,160 @@
1
+ """Public asynchronous connection backed directly by mssql-py-core.
2
+
3
+ Warning:
4
+ Async query execution APIs are under active development and are not intended
5
+ for production use. Their signatures, behavior, error handling, and compatibility
6
+ may change without notice.
7
+ """
8
+
9
+ from typing import Any, Optional
10
+
11
+ from ..logging import logger
12
+ from ._native import load_py_core
13
+ from ._connection_context import build_async_connection_context
14
+ from .async_cursor import _AsyncCursor # pyright: ignore[reportPrivateUsage]
15
+ from .exception_translator import (
16
+ DataError,
17
+ DatabaseError,
18
+ Error,
19
+ IntegrityError,
20
+ InterfaceError,
21
+ InternalError,
22
+ NotSupportedError,
23
+ OperationalError,
24
+ ProgrammingError,
25
+ Warning,
26
+ translate_py_core_exceptions,
27
+ )
28
+
29
+
30
+ class _AsyncConnection:
31
+ """Thin Python wrapper over ``mssql_py_core.PyAsyncConnection``.
32
+
33
+ Warning:
34
+ This preview API is under active development and is not intended for production use.
35
+ Its signatures, behavior, error handling, and compatibility may change without notice.
36
+ """
37
+
38
+ Warning = Warning
39
+ Error = Error
40
+ InterfaceError = InterfaceError
41
+ DatabaseError = DatabaseError
42
+ DataError = DataError
43
+ OperationalError = OperationalError
44
+ IntegrityError = IntegrityError
45
+ InternalError = InternalError
46
+ ProgrammingError = ProgrammingError
47
+ NotSupportedError = NotSupportedError
48
+
49
+ def __init__(self, py_core_async_connection: Any) -> None:
50
+ self._py_core_async_connection = py_core_async_connection
51
+
52
+ @classmethod
53
+ async def connect(
54
+ cls,
55
+ connection_str: str = "",
56
+ autocommit: bool = False,
57
+ timeout: int = 0,
58
+ python_logger: Optional[Any] = None,
59
+ ) -> "_AsyncConnection":
60
+ """Establish an asynchronous connection from an ODBC connection string."""
61
+ logger_bridge = python_logger
62
+ if logger_bridge is None and logger.is_debug_enabled:
63
+ logger_bridge = logger
64
+ logger.debug(
65
+ "AsyncConnection.connect: starting; autocommit=%s; logger_source=%s",
66
+ autocommit,
67
+ (
68
+ "custom"
69
+ if python_logger is not None
70
+ else "mssql_python" if logger_bridge is not None else "disabled"
71
+ ),
72
+ )
73
+ with translate_py_core_exceptions():
74
+ client_context_dict = build_async_connection_context(connection_str, timeout)
75
+ py_core = load_py_core()
76
+ py_core_async_connection = await py_core.PyAsyncConnection.connect(
77
+ client_context_dict,
78
+ python_logger=logger_bridge,
79
+ autocommit=autocommit,
80
+ )
81
+ logger.debug("AsyncConnection.connect: connected")
82
+ return cls(py_core_async_connection)
83
+
84
+ def cursor(self) -> _AsyncCursor:
85
+ """Create a public asynchronous cursor sharing this connection."""
86
+ with translate_py_core_exceptions():
87
+ py_core_async_cursor = self._py_core_async_connection.cursor()
88
+ logger.debug("AsyncConnection.cursor: cursor created")
89
+ return _AsyncCursor(py_core_async_cursor, self)
90
+
91
+ async def commit(self) -> None:
92
+ """Commit the active transaction, if any."""
93
+ logger.debug("AsyncConnection.commit: starting")
94
+ with translate_py_core_exceptions():
95
+ await self._py_core_async_connection.commit()
96
+ logger.debug("AsyncConnection.commit: completed")
97
+
98
+ async def rollback(self) -> None:
99
+ """Roll back the active transaction, if any."""
100
+ logger.debug("AsyncConnection.rollback: starting")
101
+ with translate_py_core_exceptions():
102
+ await self._py_core_async_connection.rollback()
103
+ logger.debug("AsyncConnection.rollback: completed")
104
+
105
+ async def close(self) -> None:
106
+ """Close the py-core async connection."""
107
+ logger.debug("AsyncConnection.close: starting")
108
+ with translate_py_core_exceptions():
109
+ await self._py_core_async_connection.close()
110
+ logger.debug("AsyncConnection.close: completed")
111
+
112
+ async def __aenter__(self) -> "_AsyncConnection":
113
+ logger.debug("AsyncConnection.__aenter__: entering context")
114
+ with translate_py_core_exceptions():
115
+ await self._py_core_async_connection.__aenter__()
116
+ logger.debug("AsyncConnection.__aenter__: context entered")
117
+ return self
118
+
119
+ async def __aexit__(self, exc_type, exc_value, traceback) -> Any:
120
+ logger.debug(
121
+ "AsyncConnection.__aexit__: exiting context; block_error=%s",
122
+ exc_type is not None,
123
+ )
124
+ with translate_py_core_exceptions():
125
+ result = await self._py_core_async_connection.__aexit__(exc_type, exc_value, traceback)
126
+ logger.debug("AsyncConnection.__aexit__: context exited")
127
+ return result
128
+
129
+ @property
130
+ def timeout(self) -> int:
131
+ """Default query timeout inherited by subsequently created cursors."""
132
+ with translate_py_core_exceptions():
133
+ return self._py_core_async_connection.timeout
134
+
135
+ @timeout.setter
136
+ def timeout(self, value: int) -> None:
137
+ with translate_py_core_exceptions():
138
+ self._py_core_async_connection.timeout = value
139
+ logger.debug("AsyncConnection.timeout: updated")
140
+
141
+ @property
142
+ def autocommit(self) -> bool:
143
+ """Whether the connection was opened in autocommit mode."""
144
+ with translate_py_core_exceptions():
145
+ return self._py_core_async_connection.autocommit
146
+
147
+ @property
148
+ def closed(self) -> bool:
149
+ """Whether close has been initiated on the py-core async connection."""
150
+ with translate_py_core_exceptions():
151
+ return self._py_core_async_connection.closed
152
+
153
+ def is_connected(self) -> bool:
154
+ """Return whether the py-core async connection remains open."""
155
+ with translate_py_core_exceptions():
156
+ return self._py_core_async_connection.is_connected()
157
+
158
+ def __repr__(self) -> str:
159
+ state = "closed" if self.closed else "connected"
160
+ return f"_AsyncConnection({state})"
@@ -0,0 +1,230 @@
1
+ """Internal asynchronous cursor backed directly by mssql-py-core.
2
+
3
+ Warning:
4
+ Async query execution APIs are under active development and are not intended
5
+ for production use. Their signatures, behavior, error handling, and compatibility
6
+ may change without notice.
7
+ """
8
+
9
+ import asyncio
10
+ from collections.abc import Mapping, Sequence
11
+ from contextlib import asynccontextmanager
12
+ from typing import Any, Optional
13
+ import uuid
14
+
15
+ from ..exceptions import OperationalError
16
+ from ..helpers import get_settings
17
+ from ..logging import logger
18
+ from ..row import Row
19
+ from . import async_execute, async_fetch
20
+ from .exception_translator import translate_py_core_exceptions
21
+
22
+
23
+ class _AsyncCursor:
24
+ """Internal Python wrapper over ``mssql_py_core.PyAsyncCursor``.
25
+
26
+ Warning:
27
+ This preview API is under active development and is not intended for production use.
28
+ Its signatures, behavior, error handling, and compatibility may change without notice.
29
+ """
30
+
31
+ def __init__(self, py_core_async_cursor: Any, connection: Any = None) -> None:
32
+ self._py_core_async_cursor = py_core_async_cursor
33
+ self._connection = connection
34
+ self._closed = False
35
+ self._result_transition_lock = asyncio.Lock()
36
+ self._result_ready = asyncio.Event()
37
+ self._result_ready.set()
38
+ self._result_generation = 0
39
+ self._fetched_row_count = 0
40
+ self._fetch_rowcount: int | None = None
41
+ self._description: list[tuple[Any, ...]] | None = None
42
+ self._column_map: dict[str, int] = {}
43
+ self._column_map_lower: dict[str, int] | None = None
44
+ self._column_names: tuple[str, ...] | None = None
45
+ self._uuid_str_indices: tuple[int, ...] | None = None
46
+
47
+ def _clear_result_metadata(self) -> None:
48
+ self._result_generation += 1
49
+ self._description = None
50
+ self._column_map = {}
51
+ self._column_map_lower = None
52
+ self._column_names = None
53
+ self._uuid_str_indices = None
54
+
55
+ def _initialize_result_metadata(self) -> None:
56
+ with translate_py_core_exceptions():
57
+ description = self._py_core_async_cursor.description
58
+ if description is None:
59
+ self._clear_result_metadata()
60
+ return
61
+
62
+ settings = get_settings()
63
+ self._description = [
64
+ ((column[0].lower() if settings.lowercase else column[0]), *column[1:])
65
+ for column in description
66
+ ]
67
+ self._column_names = tuple(column[0] for column in self._description)
68
+ self._column_map = {column[0]: index for index, column in enumerate(self._description)}
69
+ self._column_map_lower = (
70
+ {name.lower(): index for name, index in self._column_map.items()}
71
+ if settings.lowercase
72
+ else None
73
+ )
74
+ self._uuid_str_indices = (
75
+ tuple(index for index, column in enumerate(self._description) if column[1] is uuid.UUID)
76
+ if not settings.native_uuid
77
+ else None
78
+ )
79
+
80
+ def _reset_fetch_tracking(self) -> None:
81
+ self._fetched_row_count = 0
82
+ self._fetch_rowcount = None
83
+
84
+ @asynccontextmanager
85
+ async def _result_transition(self):
86
+ async with self._result_transition_lock:
87
+ self._result_ready.clear()
88
+ try:
89
+ yield
90
+ finally:
91
+ self._result_ready.set()
92
+
93
+ async def _wait_for_result_publication(self) -> None:
94
+ while True:
95
+ await self._result_ready.wait()
96
+ if self._result_ready.is_set():
97
+ return
98
+
99
+ def _reconcile_failed_result_operation(self, operation: str, error: BaseException) -> None:
100
+ if isinstance(error, OperationalError) and str(error.__cause__).startswith(
101
+ "Connection is busy"
102
+ ):
103
+ return
104
+ self._reset_fetch_tracking()
105
+ self._clear_result_metadata()
106
+ try:
107
+ self._initialize_result_metadata()
108
+ except Exception as error:
109
+ logger.debug("AsyncCursor.%s: metadata recovery failed: %s", operation, error)
110
+
111
+ def _check_closed(self) -> None:
112
+ if self._closed or (self._connection is not None and self._connection.closed):
113
+ message = "Cursor is closed" if self._closed else "Connection is closed"
114
+ with translate_py_core_exceptions():
115
+ raise RuntimeError(message)
116
+
117
+ def _record_fetch(self, generation: int, count: int, exhausted: bool) -> None:
118
+ if generation != self._result_generation:
119
+ return
120
+ if count:
121
+ self._fetched_row_count += count
122
+ self._fetch_rowcount = self._fetched_row_count
123
+ elif exhausted and self._fetched_row_count == 0:
124
+ self._fetch_rowcount = 0
125
+
126
+ async def execute(
127
+ self,
128
+ operation: str,
129
+ *parameters: Any,
130
+ use_prepare: bool = True,
131
+ reset_cursor: bool = True,
132
+ ) -> "_AsyncCursor":
133
+ async with self._result_transition():
134
+ return await async_execute.execute(
135
+ self,
136
+ operation,
137
+ *parameters,
138
+ use_prepare=use_prepare,
139
+ reset_cursor=reset_cursor,
140
+ )
141
+
142
+ async def executemany(
143
+ self,
144
+ operation: str,
145
+ seq_of_parameters: Sequence[Sequence[Any]] | Sequence[Mapping[str, Any]],
146
+ *,
147
+ use_prepare: bool = True,
148
+ ) -> None:
149
+ async with self._result_transition():
150
+ await async_execute.executemany(
151
+ self,
152
+ operation,
153
+ seq_of_parameters,
154
+ use_prepare=use_prepare,
155
+ )
156
+
157
+ async def fetchone(self) -> Row | None:
158
+ return await async_fetch.fetchone(self)
159
+
160
+ async def fetchmany(self, size: Optional[int] = None) -> list[Row]:
161
+ return await async_fetch.fetchmany(self, size)
162
+
163
+ async def fetchall(self) -> list[Row]:
164
+ return await async_fetch.fetchall(self)
165
+
166
+ async def nextset(self) -> bool:
167
+ async with self._result_transition():
168
+ try:
169
+ with translate_py_core_exceptions():
170
+ has_next = await self._py_core_async_cursor.nextset()
171
+ except (Exception, asyncio.CancelledError) as error:
172
+ self._reconcile_failed_result_operation("nextset", error)
173
+ raise
174
+ self._reset_fetch_tracking()
175
+ self._clear_result_metadata()
176
+ if has_next:
177
+ self._initialize_result_metadata()
178
+ return has_next
179
+
180
+ async def close(self) -> None:
181
+ logger.debug("AsyncCursor.close: starting")
182
+ async with self._result_transition():
183
+ with translate_py_core_exceptions():
184
+ close_awaitable = self._py_core_async_cursor.close()
185
+ close_accepted = True
186
+ try:
187
+ with translate_py_core_exceptions():
188
+ await close_awaitable
189
+ except (Exception, asyncio.CancelledError) as error:
190
+ if isinstance(error, OperationalError) and str(error.__cause__).startswith(
191
+ "Connection is busy"
192
+ ):
193
+ close_accepted = False
194
+ raise
195
+ finally:
196
+ if close_accepted:
197
+ self._closed = True
198
+ self._reset_fetch_tracking()
199
+ self._clear_result_metadata()
200
+ logger.debug("AsyncCursor.close: completed")
201
+
202
+ def setinputsizes(self, sizes: Any) -> None:
203
+ with translate_py_core_exceptions():
204
+ self._py_core_async_cursor.setinputsizes(sizes)
205
+
206
+ @property
207
+ def timeout(self) -> int:
208
+ with translate_py_core_exceptions():
209
+ return self._py_core_async_cursor.timeout
210
+
211
+ @property
212
+ def description(self) -> Any:
213
+ return self._description
214
+
215
+ @property
216
+ def rowcount(self) -> int:
217
+ if self._fetch_rowcount is not None:
218
+ return self._fetch_rowcount
219
+ with translate_py_core_exceptions():
220
+ return self._py_core_async_cursor.rowcount
221
+
222
+ @property
223
+ def arraysize(self) -> int:
224
+ with translate_py_core_exceptions():
225
+ return self._py_core_async_cursor.arraysize
226
+
227
+ @arraysize.setter
228
+ def arraysize(self, value: int) -> None:
229
+ with translate_py_core_exceptions():
230
+ self._py_core_async_cursor.arraysize = value
@@ -0,0 +1,115 @@
1
+ """Asynchronous statement execution through mssql-py-core."""
2
+
3
+ import asyncio
4
+ from collections.abc import Mapping, Sequence
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from ..exceptions import OperationalError, ProgrammingError
8
+ from ..logging import logger
9
+ from ..row import Row
10
+ from .exception_translator import translate_py_core_exceptions
11
+
12
+ if TYPE_CHECKING:
13
+ from .async_cursor import _AsyncCursor # pyright: ignore[reportPrivateUsage]
14
+
15
+
16
+ def _get_py_core_async_cursor(cursor: "_AsyncCursor") -> Any:
17
+ return cursor._py_core_async_cursor # pyright: ignore[reportPrivateUsage]
18
+
19
+
20
+ def _is_non_mutating_rejection(error: BaseException) -> bool:
21
+ """Return whether py-core guarantees rejection before result-state mutation."""
22
+ if isinstance(error, OperationalError) and str(error.__cause__).startswith(
23
+ "Connection is busy"
24
+ ):
25
+ return True
26
+ return isinstance(error, (TypeError, KeyError)) or (
27
+ isinstance(error, ProgrammingError) and isinstance(error.__cause__, TypeError)
28
+ )
29
+
30
+
31
+ def _reconcile_failed_execution(cursor: "_AsyncCursor", error: BaseException) -> None:
32
+ if _is_non_mutating_rejection(error):
33
+ return
34
+ cursor._reset_fetch_tracking() # pyright: ignore[reportPrivateUsage]
35
+ cursor._clear_result_metadata() # pyright: ignore[reportPrivateUsage]
36
+ try:
37
+ cursor._initialize_result_metadata() # pyright: ignore[reportPrivateUsage]
38
+ except Exception as error:
39
+ logger.debug("Async execution metadata recovery failed: %s", error)
40
+
41
+
42
+ async def execute(
43
+ cursor: "_AsyncCursor",
44
+ operation: str,
45
+ *parameters: Any,
46
+ use_prepare: bool = True,
47
+ reset_cursor: bool = True,
48
+ ) -> "_AsyncCursor":
49
+ """Execute a statement using the py-core async cursor."""
50
+ if len(parameters) == 1 and isinstance(parameters[0], (tuple, list, Row)):
51
+ parameters = tuple(parameters[0])
52
+
53
+ logger.debug(
54
+ "AsyncCursor.execute: starting; param_count=%d; use_prepare=%s; reset_cursor=%s",
55
+ len(parameters),
56
+ use_prepare,
57
+ reset_cursor,
58
+ )
59
+ with translate_py_core_exceptions():
60
+ execute_awaitable = _get_py_core_async_cursor(cursor).execute(
61
+ operation,
62
+ *parameters,
63
+ use_prepare=use_prepare,
64
+ reset_cursor=reset_cursor,
65
+ )
66
+ try:
67
+ with translate_py_core_exceptions():
68
+ await execute_awaitable
69
+ except (Exception, asyncio.CancelledError) as error:
70
+ _reconcile_failed_execution(cursor, error)
71
+ raise
72
+ cursor._reset_fetch_tracking() # pyright: ignore[reportPrivateUsage]
73
+ cursor._clear_result_metadata() # pyright: ignore[reportPrivateUsage]
74
+ cursor._initialize_result_metadata() # pyright: ignore[reportPrivateUsage]
75
+ description = cursor.description
76
+ logger.debug(
77
+ "AsyncCursor.execute: completed; rowcount=%d; column_count=%d; has_result_set=%s",
78
+ cursor.rowcount,
79
+ len(description) if description is not None else 0,
80
+ description is not None,
81
+ )
82
+ return cursor
83
+
84
+
85
+ async def executemany(
86
+ cursor: "_AsyncCursor",
87
+ operation: str,
88
+ seq_of_parameters: Sequence[Sequence[Any]] | Sequence[Mapping[str, Any]],
89
+ *,
90
+ use_prepare: bool = True,
91
+ ) -> None:
92
+ """Execute a statement for every parameter row using the py-core async cursor."""
93
+ cursor._check_closed() # pyright: ignore[reportPrivateUsage]
94
+ batch_count = len(seq_of_parameters)
95
+ logger.debug(
96
+ "AsyncCursor.executemany: starting; batch_count=%d; use_prepare=%s",
97
+ batch_count,
98
+ use_prepare,
99
+ )
100
+ with translate_py_core_exceptions():
101
+ executemany_awaitable = _get_py_core_async_cursor(cursor).executemany(
102
+ operation,
103
+ seq_of_parameters,
104
+ use_prepare=use_prepare,
105
+ )
106
+ try:
107
+ with translate_py_core_exceptions():
108
+ await executemany_awaitable
109
+ except (Exception, asyncio.CancelledError) as error:
110
+ _reconcile_failed_execution(cursor, error)
111
+ raise
112
+ cursor._reset_fetch_tracking() # pyright: ignore[reportPrivateUsage]
113
+ cursor._clear_result_metadata() # pyright: ignore[reportPrivateUsage]
114
+ cursor._initialize_result_metadata() # pyright: ignore[reportPrivateUsage]
115
+ logger.debug("AsyncCursor.executemany: completed; rowcount=%d", cursor.rowcount)