Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions mssql_python/async_query/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Asynchronous query support backed directly by mssql-py-core.

The internal entry points are ``_AsyncConnection`` and ``_AsyncCursor``.
Their methods retain DB-API names, but the classes are not stable public API.

Warning:
Async query execution APIs are under active development and are not intended
for production use. Their signatures, behavior, error handling, and compatibility
may change without notice.
"""

from ._native import load_py_core
from .async_connection import AsyncConnection
from .async_cursor import AsyncCursor
from .async_connection import _AsyncConnection # pyright: ignore[reportPrivateUsage]
from .async_cursor import _AsyncCursor # pyright: ignore[reportPrivateUsage]
from .exception_translator import (
DataError,
DatabaseError,
Expand All @@ -23,8 +26,6 @@
)

__all__ = [
"AsyncConnection",
"AsyncCursor",
"DataError",
"DatabaseError",
"Error",
Expand Down
14 changes: 7 additions & 7 deletions mssql_python/async_query/async_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ..logging import logger
from ._native import load_py_core
from ._connection_context import build_async_connection_context
from .async_cursor import AsyncCursor
from .async_cursor import _AsyncCursor # pyright: ignore[reportPrivateUsage]
from .exception_translator import (
DataError,
DatabaseError,
Expand All @@ -27,7 +27,7 @@
)


class AsyncConnection:
class _AsyncConnection:
Comment thread
subrata-ms marked this conversation as resolved.
"""Thin Python wrapper over ``mssql_py_core.PyAsyncConnection``.

Warning:
Expand Down Expand Up @@ -56,7 +56,7 @@ async def connect(
autocommit: bool = False,
timeout: int = 0,
python_logger: Optional[Any] = None,
) -> "AsyncConnection":
) -> "_AsyncConnection":
"""Establish an asynchronous connection from an ODBC connection string."""
logger_bridge = python_logger
if logger_bridge is None and logger.is_debug_enabled:
Expand All @@ -81,12 +81,12 @@ async def connect(
logger.debug("AsyncConnection.connect: connected")
return cls(py_core_async_connection)

def cursor(self) -> AsyncCursor:
def cursor(self) -> _AsyncCursor:
"""Create a public asynchronous cursor sharing this connection."""
with translate_py_core_exceptions():
py_core_async_cursor = self._py_core_async_connection.cursor()
logger.debug("AsyncConnection.cursor: cursor created")
return AsyncCursor(py_core_async_cursor, self)
return _AsyncCursor(py_core_async_cursor, self)

async def commit(self) -> None:
"""Commit the active transaction, if any."""
Expand All @@ -109,7 +109,7 @@ async def close(self) -> None:
await self._py_core_async_connection.close()
logger.debug("AsyncConnection.close: completed")

async def __aenter__(self) -> "AsyncConnection":
async def __aenter__(self) -> "_AsyncConnection":
logger.debug("AsyncConnection.__aenter__: entering context")
with translate_py_core_exceptions():
await self._py_core_async_connection.__aenter__()
Expand Down Expand Up @@ -157,4 +157,4 @@ def is_connected(self) -> bool:

def __repr__(self) -> str:
state = "closed" if self.closed else "connected"
return f"AsyncConnection({state})"
return f"_AsyncConnection({state})"
8 changes: 4 additions & 4 deletions mssql_python/async_query/async_cursor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Public asynchronous cursor backed directly by mssql-py-core.
"""Internal asynchronous cursor backed directly by mssql-py-core.

Warning:
Async query execution APIs are under active development and are not intended
Expand All @@ -20,8 +20,8 @@
from .exception_translator import translate_py_core_exceptions


class AsyncCursor:
"""Thin Python wrapper over ``mssql_py_core.PyAsyncCursor``.
class _AsyncCursor:
Comment thread
Copilot marked this conversation as resolved.
"""Internal Python wrapper over ``mssql_py_core.PyAsyncCursor``.

Warning:
This preview API is under active development and is not intended for production use.
Expand Down Expand Up @@ -129,7 +129,7 @@ async def execute(
*parameters: Any,
use_prepare: bool = True,
reset_cursor: bool = True,
) -> "AsyncCursor":
) -> "_AsyncCursor":
async with self._result_transition():
return await async_execute.execute(
self,
Expand Down
12 changes: 6 additions & 6 deletions mssql_python/async_query/async_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
from .exception_translator import translate_py_core_exceptions

if TYPE_CHECKING:
from .async_cursor import AsyncCursor
from .async_cursor import _AsyncCursor # pyright: ignore[reportPrivateUsage]


def _get_py_core_async_cursor(cursor: "AsyncCursor") -> Any:
def _get_py_core_async_cursor(cursor: "_AsyncCursor") -> Any:
return cursor._py_core_async_cursor # pyright: ignore[reportPrivateUsage]


Expand All @@ -28,7 +28,7 @@ def _is_non_mutating_rejection(error: BaseException) -> bool:
)


def _reconcile_failed_execution(cursor: "AsyncCursor", error: BaseException) -> None:
def _reconcile_failed_execution(cursor: "_AsyncCursor", error: BaseException) -> None:
if _is_non_mutating_rejection(error):
return
cursor._reset_fetch_tracking() # pyright: ignore[reportPrivateUsage]
Expand All @@ -40,12 +40,12 @@ def _reconcile_failed_execution(cursor: "AsyncCursor", error: BaseException) ->


async def execute(
cursor: "AsyncCursor",
cursor: "_AsyncCursor",
operation: str,
*parameters: Any,
use_prepare: bool = True,
reset_cursor: bool = True,
) -> "AsyncCursor":
) -> "_AsyncCursor":
"""Execute a statement using the py-core async cursor."""
if len(parameters) == 1 and isinstance(parameters[0], (tuple, list, Row)):
parameters = tuple(parameters[0])
Expand Down Expand Up @@ -83,7 +83,7 @@ async def execute(


async def executemany(
cursor: "AsyncCursor",
cursor: "_AsyncCursor",
operation: str,
seq_of_parameters: Sequence[Sequence[Any]] | Sequence[Mapping[str, Any]],
*,
Expand Down
14 changes: 7 additions & 7 deletions mssql_python/async_query/async_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .exception_translator import translate_py_core_exceptions

if TYPE_CHECKING:
from .async_cursor import AsyncCursor
from .async_cursor import _AsyncCursor # pyright: ignore[reportPrivateUsage]

_ResultSnapshot = tuple[
int,
Expand All @@ -21,11 +21,11 @@
]


def _get_py_core_async_cursor(cursor: "AsyncCursor") -> Any:
def _get_py_core_async_cursor(cursor: "_AsyncCursor") -> Any:
return cursor._py_core_async_cursor # pyright: ignore[reportPrivateUsage]


def _snapshot_result(cursor: "AsyncCursor") -> _ResultSnapshot:
def _snapshot_result(cursor: "_AsyncCursor") -> _ResultSnapshot:
return (
cursor._result_generation, # pyright: ignore[reportPrivateUsage]
cursor._column_map, # pyright: ignore[reportPrivateUsage]
Expand All @@ -36,7 +36,7 @@ def _snapshot_result(cursor: "AsyncCursor") -> _ResultSnapshot:


def _reconcile_failed_fetch(
cursor: "AsyncCursor", generation: int, operation: str, error: BaseException
cursor: "_AsyncCursor", generation: int, operation: str, error: BaseException
) -> None:
if generation != cursor._result_generation: # pyright: ignore[reportPrivateUsage]
return
Expand All @@ -60,7 +60,7 @@ def _wrap_row(snapshot: _ResultSnapshot, values: tuple[Any, ...]) -> Row:
)


async def fetchone(cursor: "AsyncCursor") -> Row | None:
async def fetchone(cursor: "_AsyncCursor") -> Row | None:
"""Fetch the next row through the py-core async cursor."""
logger.debug("AsyncCursor.fetchone: starting")
await cursor._wait_for_result_publication() # pyright: ignore[reportPrivateUsage]
Expand All @@ -82,7 +82,7 @@ async def fetchone(cursor: "AsyncCursor") -> Row | None:
return None if row is None else _wrap_row(snapshot, row)


async def fetchmany(cursor: "AsyncCursor", size: int | None = None) -> list[Row]:
async def fetchmany(cursor: "_AsyncCursor", size: int | None = None) -> list[Row]:
"""Fetch up to size rows, using cursor arraysize when size is omitted."""
await cursor._wait_for_result_publication() # pyright: ignore[reportPrivateUsage]
cursor._check_closed() # pyright: ignore[reportPrivateUsage]
Expand Down Expand Up @@ -114,7 +114,7 @@ async def fetchmany(cursor: "AsyncCursor", size: int | None = None) -> list[Row]
return [_wrap_row(snapshot, row) for row in rows]


async def fetchall(cursor: "AsyncCursor") -> list[Row]:
async def fetchall(cursor: "_AsyncCursor") -> list[Row]:
"""Fetch all remaining rows through the py-core async cursor."""
logger.debug("AsyncCursor.fetchall: starting")
await cursor._wait_for_result_publication() # pyright: ignore[reportPrivateUsage]
Expand Down
4 changes: 2 additions & 2 deletions tests/AsyncTest/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
import pytest_asyncio

from mssql_python.async_query import AsyncConnection
from mssql_python.async_query import _AsyncConnection # pyright: ignore[reportPrivateUsage]


@pytest.fixture
Expand All @@ -13,7 +13,7 @@ def async_connection_string(conn_str):

@pytest_asyncio.fixture
async def async_connection(async_connection_string):
connection = await AsyncConnection.connect(async_connection_string)
connection = await _AsyncConnection.connect(async_connection_string)
try:
yield connection
finally:
Expand Down
13 changes: 13 additions & 0 deletions tests/AsyncTest/test_001_async_query_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@

import pytest

import mssql_python.async_query as async_query
import mssql_python.async_query._native as native


def test_async_entry_points_are_internal():
from mssql_python.async_query import _AsyncConnection # pyright: ignore[reportPrivateUsage]
from mssql_python.async_query import _AsyncCursor # pyright: ignore[reportPrivateUsage]

assert _AsyncConnection.__name__ == "_AsyncConnection"
assert _AsyncCursor.__name__ == "_AsyncCursor"
for name in ("AsyncConnection", "AsyncCursor"):
assert not hasattr(async_query, name)
assert name not in async_query.__all__
assert f"_{name}" not in async_query.__all__


def test_load_py_core_uses_direct_native_dependency():
pytest.importorskip("mssql_py_core", exc_type=ImportError)

Expand Down
23 changes: 12 additions & 11 deletions tests/AsyncTest/test_002_async_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
InterfaceError,
NotSupportedError,
)
from mssql_python.async_query import AsyncConnection, AsyncCursor
from mssql_python.async_query import _AsyncConnection # pyright: ignore[reportPrivateUsage]
from mssql_python.async_query import _AsyncCursor # pyright: ignore[reportPrivateUsage]
from mssql_python.async_query._connection_context import build_async_connection_context
from mssql_python.helpers import connstr_to_pycore_params


@pytest.mark.asyncio
async def test_connect_accepts_login_options_with_real_py_core(async_connection_string):
connection = await AsyncConnection.connect(
connection = await _AsyncConnection.connect(
async_connection_string,
autocommit=True,
timeout=12,
Expand All @@ -24,14 +25,14 @@ async def test_connect_accepts_login_options_with_real_py_core(async_connection_
assert connection.autocommit is True
assert connection.closed is False
assert connection.is_connected() is True
assert repr(connection) == "AsyncConnection(connected)"
assert repr(connection) == "_AsyncConnection(connected)"
finally:
await connection.close()


@pytest.mark.asyncio
async def test_connect_defaults_autocommit_to_false(async_connection_string):
connection = await AsyncConnection.connect(async_connection_string)
connection = await _AsyncConnection.connect(async_connection_string)
try:
assert connection.autocommit is False
finally:
Expand Down Expand Up @@ -268,7 +269,7 @@ def test_async_connection_rejects_missing_server():
@pytest.mark.asyncio
async def test_connection_exposes_complete_native_surface(async_connection):
cursor = async_connection.cursor()
assert isinstance(cursor, AsyncCursor)
assert isinstance(cursor, _AsyncCursor)
assert async_connection.timeout == 0
async_connection.timeout = 12
assert async_connection.timeout == 12
Expand All @@ -279,7 +280,7 @@ async def test_connection_exposes_complete_native_surface(async_connection):
with pytest.raises(AttributeError):
setattr(async_connection, "closed", True)
assert async_connection.is_connected() is True
assert repr(async_connection) == "AsyncConnection(connected)"
assert repr(async_connection) == "_AsyncConnection(connected)"

await cursor.close()
assert await async_connection.commit() is None
Expand All @@ -290,7 +291,7 @@ async def test_connection_exposes_complete_native_surface(async_connection):
async def test_async_context_manager_returns_wrapper_and_closes_connection(
async_connection_string,
):
connection = await AsyncConnection.connect(async_connection_string)
connection = await _AsyncConnection.connect(async_connection_string)

async with connection as entered:
assert entered is connection
Expand All @@ -302,19 +303,19 @@ async def test_async_context_manager_returns_wrapper_and_closes_connection(

@pytest.mark.asyncio
async def test_close_can_be_called_repeatedly(async_connection_string):
connection = await AsyncConnection.connect(async_connection_string)
connection = await _AsyncConnection.connect(async_connection_string)

await connection.close()
await connection.close()

assert connection.closed is True
assert connection.is_connected() is False
assert repr(connection) == "AsyncConnection(closed)"
assert repr(connection) == "_AsyncConnection(closed)"


@pytest.mark.asyncio
async def test_operations_after_close_translate_native_errors(async_connection_string):
connection = await AsyncConnection.connect(async_connection_string)
connection = await _AsyncConnection.connect(async_connection_string)
await connection.close()

with pytest.raises(InterfaceError, match="Connection is closed") as cursor_error:
Expand All @@ -338,7 +339,7 @@ async def test_operations_after_close_translate_native_errors(async_connection_s
async def test_context_manager_preserves_block_exception_and_closes_connection(
async_connection_string,
):
connection = await AsyncConnection.connect(async_connection_string)
connection = await _AsyncConnection.connect(async_connection_string)

with pytest.raises(ValueError, match="user_error_42"):
async with connection:
Expand Down
10 changes: 5 additions & 5 deletions tests/AsyncTest/test_003_async_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
mssql_py_core = pytest.importorskip("mssql_py_core", exc_type=ImportError)

from mssql_python import exceptions as public_exceptions
from mssql_python.async_query import AsyncConnection
from mssql_python.async_query import _AsyncConnection # pyright: ignore[reportPrivateUsage]
from mssql_python.async_query.exception_translator import (
translate_py_core_exception,
translate_py_core_exceptions,
Expand Down Expand Up @@ -188,7 +188,7 @@ async def test_closed_cursor_executemany_checks_state_before_parameters(async_co

@pytest.mark.asyncio
async def test_connection_close_invalidates_cursor_fetchmany_fast_path(async_connection_string):
connection = await AsyncConnection.connect(async_connection_string)
connection = await _AsyncConnection.connect(async_connection_string)
cursor = connection.cursor()
await connection.close()

Expand Down Expand Up @@ -280,7 +280,7 @@ async def test_execute_programming_error_preserves_diagnostics_and_cursor_is_reu
async def test_executemany_integrity_error_reports_row_and_preserves_partial_progress(
async_connection_string,
):
connection = await AsyncConnection.connect(async_connection_string, autocommit=True)
connection = await _AsyncConnection.connect(async_connection_string, autocommit=True)
cursor = connection.cursor()
table_name = f"async_exception_{uuid4().hex}"
try:
Expand All @@ -306,7 +306,7 @@ async def test_executemany_integrity_error_reports_row_and_preserves_partial_pro

@pytest.mark.asyncio
async def test_timeout_is_operational_error_and_cursor_is_reusable(async_connection_string):
connection = await AsyncConnection.connect(async_connection_string)
connection = await _AsyncConnection.connect(async_connection_string)
connection.timeout = 1
cursor = connection.cursor()
try:
Expand Down Expand Up @@ -340,4 +340,4 @@ async def test_busy_connection_is_operational_error(async_connection):

def test_async_connection_exposes_public_exception_classes():
for name in EXCEPTION_NAMES:
assert getattr(AsyncConnection, name) is getattr(public_exceptions, name)
assert getattr(_AsyncConnection, name) is getattr(public_exceptions, name)
Loading
Loading