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,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)
|