deev 2.0.2__tar.gz → 2.1.2__tar.gz
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.
- {deev-2.0.2/src/deev.egg-info → deev-2.1.2}/PKG-INFO +3 -1
- {deev-2.0.2 → deev-2.1.2}/pyproject.toml +4 -1
- {deev-2.0.2 → deev-2.1.2}/src/deev/__init__.py +3 -2
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/connection_string.py +1 -0
- deev-2.1.2/src/deev/mssql/__init__.py +31 -0
- deev-2.1.2/src/deev/mssql/async_mssql_proxy_connection.py +54 -0
- deev-2.1.2/src/deev/mssql/async_mssql_proxy_cursor.py +70 -0
- deev-2.1.2/src/deev/mssql/async_mssql_table_adapter.py +146 -0
- deev-2.1.2/src/deev/mssql/async_mssql_transaction_context.py +135 -0
- deev-2.1.2/src/deev/mssql/mssql_ddl_generator.py +91 -0
- deev-2.1.2/src/deev/mssql/mssql_proxy_connection.py +73 -0
- deev-2.1.2/src/deev/mssql/mssql_proxy_cursor.py +119 -0
- deev-2.1.2/src/deev/mssql/mssql_table_adapter.py +407 -0
- deev-2.1.2/src/deev/mssql/mssql_transaction_context.py +399 -0
- deev-2.1.2/src/deev/mssql/mssql_type_mapper.py +75 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/translation/__init__.py +2 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/translation/utils.py +54 -3
- {deev-2.0.2 → deev-2.1.2}/src/deev/utils.py +135 -7
- {deev-2.0.2 → deev-2.1.2/src/deev.egg-info}/PKG-INFO +3 -1
- {deev-2.0.2 → deev-2.1.2}/src/deev.egg-info/SOURCES.txt +11 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev.egg-info/requires.txt +3 -0
- {deev-2.0.2 → deev-2.1.2}/LICENSE +0 -0
- {deev-2.0.2 → deev-2.1.2}/README.md +0 -0
- {deev-2.0.2 → deev-2.1.2}/setup.cfg +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/SKILL.md +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/_immutable_mixin.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/_migration_data.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/__init__.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/async_clickhouse_proxy_connection.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/async_clickhouse_proxy_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/async_clickhouse_table_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/async_clickhouse_transaction_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_ddl_generator.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_proxy_connection.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_proxy_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_table_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_transaction_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_type_mapper.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/utils.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/__init__.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/async_db_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/async_db_connection.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/async_db_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/async_db_table_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/async_db_transaction_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_connection.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_error.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_migrator.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_parameters.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_table_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_transaction_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_type_mapper.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/description_field.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/noop_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/common/tlc_parser.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/db_migrate.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/entities/__init__.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/entities/entity_field_spec.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/entities/entity_spec.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/entities/index_options.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/entities/index_order.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/entities/utils.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/__init__.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/async_mongo_proxy_connection.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/async_mongo_proxy_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/async_mongo_table_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/async_mongo_transaction_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/mongo_proxy_connection.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/mongo_proxy_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/mongo_table_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/mongo_transaction_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/mongo_type_mapper.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/utils.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/__init__.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/async_mysql_proxy_connection.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/async_mysql_proxy_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/async_mysql_table_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/async_mysql_transaction_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_ddl_generator.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_proxy_connection.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_proxy_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_table_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_transaction_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_type_mapper.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/py.typed +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/__init__.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/async_sqlite_proxy_connection.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/async_sqlite_proxy_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/async_sqlite_table_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/async_sqlite_transaction_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_ddl_generator.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_proxy_connection.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_proxy_cursor.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_table_adapter.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_transaction_context.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_type_mapper.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/translation/deev_json_decoder.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/translation/deev_json_encoder.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/validation/__init__.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/validation/utils.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev/validation/validation_error.py +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev.egg-info/dependency_links.txt +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev.egg-info/entry_points.txt +0 -0
- {deev-2.0.2 → deev-2.1.2}/src/deev.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deev
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: ..an entity framework for Python.
|
|
5
5
|
Author-email: Shaun Wilson <mrshaunwilson@msn.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,6 +21,8 @@ Provides-Extra: clickhouse
|
|
|
21
21
|
Requires-Dist: clickhouse-connect[async]>=1.6.0; extra == "clickhouse"
|
|
22
22
|
Provides-Extra: mongodb
|
|
23
23
|
Requires-Dist: pymongo; extra == "mongodb"
|
|
24
|
+
Provides-Extra: mssql
|
|
25
|
+
Requires-Dist: mssql-python>=1.15.0; extra == "mssql"
|
|
24
26
|
Provides-Extra: mysql
|
|
25
27
|
Requires-Dist: mysql-connector-python; extra == "mysql"
|
|
26
28
|
Dynamic: license-file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deev"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.1.2"
|
|
4
4
|
description = "..an entity framework for Python."
|
|
5
5
|
keywords = ["DB", "database", "entity framework", "mapper", "sqlite", "mysql", "mongodb", "clickhouse"]
|
|
6
6
|
authors = [
|
|
@@ -27,6 +27,9 @@ clickhouse = [
|
|
|
27
27
|
mongodb = [
|
|
28
28
|
"pymongo"
|
|
29
29
|
]
|
|
30
|
+
mssql = [
|
|
31
|
+
"mssql-python (>=1.15.0)"
|
|
32
|
+
]
|
|
30
33
|
mysql = [
|
|
31
34
|
"mysql-connector-python"
|
|
32
35
|
]
|
|
@@ -21,8 +21,8 @@ from .translation import hydrate, splat
|
|
|
21
21
|
from .utils import connect
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
__version__ = '2.
|
|
25
|
-
__commit__ = '
|
|
24
|
+
__version__ = '2.1.2'
|
|
25
|
+
__commit__ = 'f71bbe9'
|
|
26
26
|
__all__ = [
|
|
27
27
|
'__version__',
|
|
28
28
|
'__commit__',
|
|
@@ -36,6 +36,7 @@ __all__ = [
|
|
|
36
36
|
'field',
|
|
37
37
|
'hydrate',
|
|
38
38
|
'mongodb',
|
|
39
|
+
'mssql',
|
|
39
40
|
'mysql',
|
|
40
41
|
'splat',
|
|
41
42
|
'translation',
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from .async_mssql_proxy_connection import AsyncMSSQLProxyConnection
|
|
6
|
+
from .async_mssql_proxy_cursor import AsyncMSSQLProxyCursor
|
|
7
|
+
from .async_mssql_table_adapter import AsyncMSSQLTableAdapter
|
|
8
|
+
from .async_mssql_transaction_context import AsyncMSSQLTransactionContext
|
|
9
|
+
from .mssql_ddl_generator import MSSQLDDLGenerator
|
|
10
|
+
from .mssql_proxy_connection import MSSQLProxyConnection
|
|
11
|
+
from .mssql_proxy_cursor import MSSQLProxyCursor
|
|
12
|
+
from .mssql_table_adapter import MSSQLTableAdapter
|
|
13
|
+
from .mssql_transaction_context import MSSQLTransactionContext
|
|
14
|
+
from .mssql_type_mapper import MSSQLTypeMapper
|
|
15
|
+
except Exception:
|
|
16
|
+
# NOTE: if required packages are not enabled we expect this module to import without errors
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
'AsyncMSSQLProxyConnection',
|
|
22
|
+
'AsyncMSSQLProxyCursor',
|
|
23
|
+
'AsyncMSSQLTableAdapter',
|
|
24
|
+
'AsyncMSSQLTransactionContext',
|
|
25
|
+
'MSSQLDDLGenerator',
|
|
26
|
+
'MSSQLProxyConnection',
|
|
27
|
+
'MSSQLProxyCursor',
|
|
28
|
+
'MSSQLTableAdapter',
|
|
29
|
+
'MSSQLTransactionContext',
|
|
30
|
+
'MSSQLTypeMapper'
|
|
31
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from mssql_python import Connection
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Any, Literal, Self
|
|
10
|
+
|
|
11
|
+
from ..common.async_db_connection import AsyncDbConnection
|
|
12
|
+
from ..common.async_db_cursor import AsyncDbCursor
|
|
13
|
+
from .async_mssql_proxy_cursor import AsyncMSSQLProxyCursor
|
|
14
|
+
from .mssql_proxy_connection import MSSQLProxyConnection
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AsyncMSSQLProxyConnection(AsyncDbConnection):
|
|
18
|
+
"""
|
|
19
|
+
Async shim that delegates to ``MSSQLProxyConnection``.
|
|
20
|
+
|
|
21
|
+
Since mssql_python has no native async API, all calls are forwarded to the
|
|
22
|
+
synchronous proxy using ``asyncio.to_thread``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__sync_conn: MSSQLProxyConnection
|
|
26
|
+
|
|
27
|
+
def __init__(self, conn: Connection) -> None:
|
|
28
|
+
self.__sync_conn = MSSQLProxyConnection(conn)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def mssql_connection(self) -> MSSQLProxyConnection:
|
|
32
|
+
return self.__sync_conn
|
|
33
|
+
|
|
34
|
+
async def cursor(self, *args: Any, **kwargs: Any) -> AsyncDbCursor:
|
|
35
|
+
return AsyncMSSQLProxyCursor(self.__sync_conn.cursor(*args, **kwargs)) # type: ignore[arg-type]
|
|
36
|
+
|
|
37
|
+
async def commit(self) -> None:
|
|
38
|
+
await asyncio.to_thread(self.__sync_conn.commit)
|
|
39
|
+
|
|
40
|
+
async def rollback(self) -> None:
|
|
41
|
+
await asyncio.to_thread(self.__sync_conn.rollback)
|
|
42
|
+
|
|
43
|
+
async def close(self) -> None:
|
|
44
|
+
await asyncio.to_thread(self.__sync_conn.close)
|
|
45
|
+
|
|
46
|
+
async def __aenter__(self) -> Self:
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
async def __aexit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /) -> Literal[False]:
|
|
50
|
+
self.__sync_conn.__exit__(exc_type, exc, tb)
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ['AsyncMSSQLProxyConnection']
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from mssql_python import Cursor
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Sequence,
|
|
11
|
+
cast,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from ..common.async_db_cursor import AsyncDbCursor, AsyncDbCursorDescription
|
|
15
|
+
from ..common.db_parameters import DbParameters
|
|
16
|
+
from .mssql_proxy_cursor import MSSQLProxyCursor
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AsyncMSSQLProxyCursor(AsyncDbCursor):
|
|
20
|
+
"""
|
|
21
|
+
Async shim that delegates to ``MSSQLProxyCursor``.
|
|
22
|
+
|
|
23
|
+
Wraps the synchronous deev cursor proxy and exposes an async API
|
|
24
|
+
using ``asyncio.to_thread`` for underlying mssql_python operations.
|
|
25
|
+
"""
|
|
26
|
+
__sync_cursor: MSSQLProxyCursor
|
|
27
|
+
__cursor: Cursor
|
|
28
|
+
__sql_arg_expect: str
|
|
29
|
+
__sql_arg_subst: str
|
|
30
|
+
|
|
31
|
+
def __init__(self, provider_cursor: Cursor) -> None:
|
|
32
|
+
self.__cursor = provider_cursor
|
|
33
|
+
self.__sync_cursor = MSSQLProxyCursor(provider_cursor)
|
|
34
|
+
self.__sql_arg_expect = '%?'
|
|
35
|
+
self.__sql_arg_subst = '?'
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def description(self) -> AsyncDbCursorDescription:
|
|
39
|
+
return cast(AsyncDbCursorDescription, self.__cursor.description)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def rowcount(self) -> int:
|
|
43
|
+
return self.__cursor.rowcount
|
|
44
|
+
|
|
45
|
+
async def execute(self, operation: str, parameters: DbParameters | None = None) -> None:
|
|
46
|
+
if parameters is not None:
|
|
47
|
+
operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
|
|
48
|
+
if parameters is None:
|
|
49
|
+
self.__cursor.execute(operation)
|
|
50
|
+
else:
|
|
51
|
+
self.__cursor.execute(operation, parameters)
|
|
52
|
+
|
|
53
|
+
async def executemany(self, operation: str, seq_of_parameters: Sequence[DbParameters]) -> None:
|
|
54
|
+
operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
|
|
55
|
+
self.__cursor.executemany(operation, [tuple(p) if isinstance(p, (tuple, list)) else dict(p) for p in seq_of_parameters]) # type: ignore[arg-type]
|
|
56
|
+
|
|
57
|
+
async def fetchone(self) -> tuple[Any, ...] | None:
|
|
58
|
+
return cast(tuple[Any, ...] | None, self.__cursor.fetchone())
|
|
59
|
+
|
|
60
|
+
async def fetchmany(self, size: int = 1) -> list[tuple[Any, ...]]:
|
|
61
|
+
return cast(list[tuple[Any, ...]], self.__cursor.fetchmany(size))
|
|
62
|
+
|
|
63
|
+
async def fetchall(self) -> list[tuple[Any, ...]]:
|
|
64
|
+
return cast(list[tuple[Any, ...]], self.__cursor.fetchall())
|
|
65
|
+
|
|
66
|
+
async def close(self) -> None:
|
|
67
|
+
await asyncio.to_thread(self.__cursor.close)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ['AsyncMSSQLProxyCursor']
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, AsyncGenerator, TypeVar, cast, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
from ..common.async_db_table_adapter import AsyncDbTableAdapter
|
|
9
|
+
from ..common.db_context import AsyncDbContext
|
|
10
|
+
from ..common.db_parameters import DbParameters
|
|
11
|
+
from ..entities import get_entity_spec
|
|
12
|
+
from .async_mssql_proxy_connection import AsyncMSSQLProxyConnection
|
|
13
|
+
from .async_mssql_transaction_context import AsyncMSSQLTransactionContext
|
|
14
|
+
from .mssql_proxy_connection import MSSQLProxyConnection
|
|
15
|
+
from .mssql_table_adapter import MSSQLTableAdapter
|
|
16
|
+
from .mssql_type_mapper import MSSQLTypeMapper
|
|
17
|
+
|
|
18
|
+
TEntity = TypeVar('TEntity')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AsyncMSSQLTableAdapter(AsyncDbTableAdapter[TEntity]):
|
|
22
|
+
"""
|
|
23
|
+
Async shim that delegates to ``MSSQLTableAdapter``.
|
|
24
|
+
|
|
25
|
+
Since mssql_python has no native async API, all calls are forwarded to the
|
|
26
|
+
synchronous table adapter using ``asyncio.to_thread``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__sync_adapter: MSSQLTableAdapter[TEntity]
|
|
30
|
+
__context: AsyncDbContext
|
|
31
|
+
__create_table: bool
|
|
32
|
+
__table_name: str | None
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
context: AsyncDbContext,
|
|
37
|
+
*,
|
|
38
|
+
create_table: bool = False,
|
|
39
|
+
table_name: str | None = None
|
|
40
|
+
) -> None:
|
|
41
|
+
self.__context = context if isinstance(context, (AsyncMSSQLProxyConnection, AsyncMSSQLTransactionContext)) else AsyncMSSQLProxyConnection(context) # type: ignore[arg-type]
|
|
42
|
+
self.__create_table = create_table is True
|
|
43
|
+
self.__table_name = table_name
|
|
44
|
+
self.__is_initialized = False
|
|
45
|
+
|
|
46
|
+
async def __deferred_init(self) -> None:
|
|
47
|
+
if not self.__is_initialized:
|
|
48
|
+
self.__sync_deferred_init()
|
|
49
|
+
self.__is_initialized = True
|
|
50
|
+
if self.__create_table is True:
|
|
51
|
+
await self.create_table()
|
|
52
|
+
|
|
53
|
+
def __sync_deferred_init(self) -> None:
|
|
54
|
+
entity_type = self.__get_typearg(self)
|
|
55
|
+
self.__entity_spec = get_entity_spec(entity_type)
|
|
56
|
+
self.__column_names = ', '.join([f'[{k}]' for k in self.__entity_spec.fields.keys()])
|
|
57
|
+
self.__dbtype_mapper = MSSQLTypeMapper(self.__entity_spec)
|
|
58
|
+
|
|
59
|
+
self.__sync_adapter = MSSQLTableAdapter[entity_type]( # type: ignore[valid-type]
|
|
60
|
+
self.mssql_connection,
|
|
61
|
+
create_table=self.__create_table,
|
|
62
|
+
table_name=self.__table_name,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def primary_key(self) -> tuple[str, ...]:
|
|
67
|
+
if not hasattr(self, '_AsyncMSSQLTableAdapter__sync_adapter'):
|
|
68
|
+
self.__sync_deferred_init()
|
|
69
|
+
return self.__sync_adapter.primary_key
|
|
70
|
+
|
|
71
|
+
def __get_typearg(self, obj: object) -> type:
|
|
72
|
+
orig = getattr(obj, '__orig_class__', None)
|
|
73
|
+
if orig is not None:
|
|
74
|
+
args = get_args(orig)
|
|
75
|
+
if args:
|
|
76
|
+
return args[0]
|
|
77
|
+
for base in obj.__class__.__mro__:
|
|
78
|
+
for generic_base in getattr(base, '__orig_bases__', ()):
|
|
79
|
+
if get_origin(generic_base) is AsyncMSSQLTableAdapter:
|
|
80
|
+
args = get_args(generic_base)
|
|
81
|
+
if args is not None and len(args) > 0:
|
|
82
|
+
return args[0]
|
|
83
|
+
raise RuntimeError(
|
|
84
|
+
f'Could not determine the entity type for {obj.__class__.__qualname__}. '
|
|
85
|
+
'Instantiate via the generic alias, e.g. AsyncMSSQLTableAdapter[MyEntity]().'
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def mssql_connection(self) -> MSSQLProxyConnection:
|
|
90
|
+
if isinstance(self.__context, AsyncMSSQLProxyConnection):
|
|
91
|
+
return self.__context.mssql_connection
|
|
92
|
+
elif isinstance(self.__context, AsyncMSSQLTransactionContext):
|
|
93
|
+
return cast(AsyncMSSQLProxyConnection, self.__context.connection).mssql_connection
|
|
94
|
+
raise RuntimeError(f'unsupported context type: {self.__context}')
|
|
95
|
+
|
|
96
|
+
async def create_table(self) -> None:
|
|
97
|
+
await self.__deferred_init()
|
|
98
|
+
self.__sync_adapter.create_table()
|
|
99
|
+
self.__create_table = False
|
|
100
|
+
|
|
101
|
+
async def create(self, entity: TEntity | None = None, **kwargs: Any) -> dict[str, Any]:
|
|
102
|
+
await self.__deferred_init()
|
|
103
|
+
return self.__sync_adapter.create(entity, **kwargs)
|
|
104
|
+
|
|
105
|
+
async def read(self, **kwargs: Any) -> TEntity | None:
|
|
106
|
+
await self.__deferred_init()
|
|
107
|
+
return self.__sync_adapter.read(**kwargs)
|
|
108
|
+
|
|
109
|
+
async def update(self, entity: TEntity) -> None:
|
|
110
|
+
await self.__deferred_init()
|
|
111
|
+
self.__sync_adapter.update(entity)
|
|
112
|
+
|
|
113
|
+
async def delete(self, **kwargs: Any) -> None:
|
|
114
|
+
await self.__deferred_init()
|
|
115
|
+
self.__sync_adapter.delete(**kwargs)
|
|
116
|
+
|
|
117
|
+
async def exists(self, **kwargs: Any) -> bool:
|
|
118
|
+
await self.__deferred_init()
|
|
119
|
+
return self.__sync_adapter.exists(**kwargs)
|
|
120
|
+
|
|
121
|
+
async def upsert(self, entity: TEntity) -> dict[str, Any]:
|
|
122
|
+
await self.__deferred_init()
|
|
123
|
+
return self.__sync_adapter.upsert(entity)
|
|
124
|
+
|
|
125
|
+
async def query( # type: ignore[override]
|
|
126
|
+
self,
|
|
127
|
+
where: str | None = None,
|
|
128
|
+
parameters: DbParameters | None = None,
|
|
129
|
+
orderby: str | None = None,
|
|
130
|
+
limit: int | None = None
|
|
131
|
+
) -> AsyncGenerator[TEntity, None]:
|
|
132
|
+
await self.__deferred_init()
|
|
133
|
+
sync_gen = self.__sync_adapter.query(where, parameters, orderby, limit)
|
|
134
|
+
for e in sync_gen:
|
|
135
|
+
yield e
|
|
136
|
+
|
|
137
|
+
async def commit(self) -> None:
|
|
138
|
+
await self.__deferred_init()
|
|
139
|
+
self.__sync_adapter.commit()
|
|
140
|
+
|
|
141
|
+
async def rollback(self) -> None:
|
|
142
|
+
await self.__deferred_init()
|
|
143
|
+
self.__sync_adapter.rollback()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
__all__ = ['AsyncMSSQLTableAdapter']
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Any, AsyncGenerator, Literal, Self, cast
|
|
9
|
+
|
|
10
|
+
from ..common.async_db_connection import AsyncDbConnection
|
|
11
|
+
from ..common.async_db_cursor import AsyncDbCursor
|
|
12
|
+
from ..common.async_db_transaction_context import AsyncDbTransactionContext
|
|
13
|
+
from ..common.db_context import AsyncDbContext
|
|
14
|
+
from ..common.db_parameters import DbParameters
|
|
15
|
+
from .async_mssql_proxy_connection import AsyncMSSQLProxyConnection
|
|
16
|
+
from .async_mssql_proxy_cursor import AsyncMSSQLProxyCursor
|
|
17
|
+
from .mssql_transaction_context import MSSQLTransactionContext
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncMSSQLTransactionContext(AsyncDbTransactionContext):
|
|
21
|
+
"""
|
|
22
|
+
Async shim that delegates to ``MSSQLTransactionContext``.
|
|
23
|
+
|
|
24
|
+
Wraps the synchronous transaction context and exposes an async API
|
|
25
|
+
using direct method calls — no ``asyncio.to_thread``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__context: AsyncDbContext | None
|
|
29
|
+
__inner_ctx: MSSQLTransactionContext | None
|
|
30
|
+
__owns_context: bool
|
|
31
|
+
|
|
32
|
+
def __init__(self, context: AsyncDbContext, *, owns_context: bool | None = None) -> None:
|
|
33
|
+
self.__owns_context = owns_context is True
|
|
34
|
+
self.__is_deev_context = isinstance(context, (AsyncMSSQLProxyConnection, AsyncMSSQLTransactionContext))
|
|
35
|
+
self.__context = context if self.__is_deev_context else AsyncMSSQLProxyConnection(context) # type: ignore[arg-type]
|
|
36
|
+
self.__inner_ctx = None
|
|
37
|
+
|
|
38
|
+
def __del__(self) -> None:
|
|
39
|
+
try:
|
|
40
|
+
try:
|
|
41
|
+
loop = asyncio.get_running_loop()
|
|
42
|
+
except RuntimeError:
|
|
43
|
+
loop = None
|
|
44
|
+
if loop and loop.is_running():
|
|
45
|
+
loop.create_task(self.close())
|
|
46
|
+
else:
|
|
47
|
+
asyncio.run(self.close())
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def __get_inner_ctx(self) -> MSSQLTransactionContext:
|
|
52
|
+
"""Return the underlying sync transaction context, acquiring it if necessary."""
|
|
53
|
+
if self.__inner_ctx is None:
|
|
54
|
+
self.__inner_ctx = MSSQLTransactionContext(
|
|
55
|
+
context=cast(AsyncMSSQLProxyConnection, self.__context).mssql_connection,
|
|
56
|
+
owns_context=self.__owns_context
|
|
57
|
+
)
|
|
58
|
+
return self.__inner_ctx
|
|
59
|
+
|
|
60
|
+
async def __aenter__(self) -> Self:
|
|
61
|
+
innser_ctx = self.__get_inner_ctx()
|
|
62
|
+
innser_ctx.__enter__()
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
async def __aexit__(
|
|
66
|
+
self,
|
|
67
|
+
exc_type: type[BaseException] | None = None,
|
|
68
|
+
exc_value: BaseException | None = None,
|
|
69
|
+
traceback: TracebackType | None = None
|
|
70
|
+
) -> Literal[False]:
|
|
71
|
+
if self.__inner_ctx is not None:
|
|
72
|
+
self.__inner_ctx.__exit__(exc_type, exc_value, traceback)
|
|
73
|
+
await self.close()
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def connection(self) -> AsyncDbConnection:
|
|
78
|
+
if isinstance(self.__context, AsyncDbTransactionContext):
|
|
79
|
+
return cast(AsyncDbTransactionContext, self.__context).connection
|
|
80
|
+
else:
|
|
81
|
+
return cast(AsyncDbConnection, self.__context)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def transaction_name(self) -> str | None:
|
|
85
|
+
return self.__get_inner_ctx().transaction_name
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def savepoints(self) -> tuple[str, ...]:
|
|
89
|
+
return self.__get_inner_ctx().savepoints
|
|
90
|
+
|
|
91
|
+
async def begin_transaction(self, name: str | None = None) -> Self:
|
|
92
|
+
self.__get_inner_ctx().begin_transaction(name)
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
async def create_savepoint(self, name: str | None = None) -> Self:
|
|
96
|
+
self.__get_inner_ctx().create_savepoint(name)
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
async def rollback_savepoint(self, name: str | None = None) -> None:
|
|
100
|
+
self.__get_inner_ctx().rollback_savepoint(name)
|
|
101
|
+
|
|
102
|
+
async def close(self) -> None:
|
|
103
|
+
if self.__inner_ctx is not None:
|
|
104
|
+
self.__inner_ctx.close()
|
|
105
|
+
|
|
106
|
+
async def commit(self) -> None:
|
|
107
|
+
self.__get_inner_ctx().commit()
|
|
108
|
+
|
|
109
|
+
async def cursor(self) -> Any:
|
|
110
|
+
sync_cursor = self.__get_inner_ctx().cursor()
|
|
111
|
+
raw_cursor = sync_cursor._MSSQLProxyCursor__cursor # type: ignore[attr-defined]
|
|
112
|
+
return AsyncMSSQLProxyCursor(raw_cursor)
|
|
113
|
+
|
|
114
|
+
async def execute(self, sql: str, parameters: DbParameters | None = None, raw: bool | None = False) -> Any:
|
|
115
|
+
return self.__get_inner_ctx().execute(sql, parameters, raw)
|
|
116
|
+
|
|
117
|
+
async def execute_nonquery(self, sql: str, parameters: DbParameters | None = None) -> None:
|
|
118
|
+
self.__get_inner_ctx().execute_nonquery(sql, parameters)
|
|
119
|
+
|
|
120
|
+
async def execute_reader(self, sql: str, parameters: DbParameters | None = None) -> AsyncGenerator[tuple[Any, ...], None]: # type: ignore[override]
|
|
121
|
+
sync_gen = self.__get_inner_ctx().execute_reader(sql, parameters)
|
|
122
|
+
for row in sync_gen:
|
|
123
|
+
yield row
|
|
124
|
+
|
|
125
|
+
async def execute_scalar(self, sql: str, parameters: DbParameters | None = None) -> Any:
|
|
126
|
+
return self.__get_inner_ctx().execute_scalar(sql, parameters)
|
|
127
|
+
|
|
128
|
+
async def execute_script(self, sql: str, raw: bool = False) -> None:
|
|
129
|
+
self.__get_inner_ctx().execute_script(sql, raw)
|
|
130
|
+
|
|
131
|
+
async def rollback(self, name: str | None = None) -> None:
|
|
132
|
+
self.__get_inner_ctx().rollback(name)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__ = ['AsyncMSSQLTransactionContext']
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hanaro
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from ..entities import EntitySpec, IndexOrder
|
|
10
|
+
from .mssql_type_mapper import MSSQLTypeMapper
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MSSQLDDLGenerator:
|
|
14
|
+
"""
|
|
15
|
+
Generates Data Definition Language (DDL) statements for Microsoft SQL Server.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__logger: logging.Logger
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.__logger = hanaro.get_logger()
|
|
24
|
+
|
|
25
|
+
def __generate_table_indexes_ddl(self, entity_spec: EntitySpec, table_name: str) -> list[str]:
|
|
26
|
+
"""
|
|
27
|
+
Build ``CREATE INDEX`` statements for all secondary indexes.
|
|
28
|
+
|
|
29
|
+
:param entity_spec: The entity specification containing index metadata.
|
|
30
|
+
:param table_name: The target table name.
|
|
31
|
+
:returns: A list of ``CREATE INDEX`` SQL statements.
|
|
32
|
+
"""
|
|
33
|
+
ddl = list[str]()
|
|
34
|
+
direction_map = {IndexOrder.ASCENDING: 'ASC', IndexOrder.DESCENDING: 'DESC'}
|
|
35
|
+
groups: dict[str, list[tuple[str, IndexOrder]]] = {}
|
|
36
|
+
for field_name, spec in entity_spec.fields.items():
|
|
37
|
+
if spec.index is None:
|
|
38
|
+
continue
|
|
39
|
+
idx_name = spec.index.name
|
|
40
|
+
direction = spec.index.direction or IndexOrder.ASCENDING
|
|
41
|
+
groups.setdefault(idx_name, []).append((field_name, direction))
|
|
42
|
+
|
|
43
|
+
for idx_name, col_specs in sorted(groups.items()):
|
|
44
|
+
col_specs.sort(key=lambda cs: cs[0]) # type: ignore
|
|
45
|
+
cols = ', '.join(f'[{name}] {direction_map[direction]}' for name, direction in col_specs)
|
|
46
|
+
ddl.append(f'CREATE INDEX [{idx_name}] ON [{table_name}] ({cols})')
|
|
47
|
+
return ddl
|
|
48
|
+
|
|
49
|
+
def generate_table_ddl(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
entity_spec: EntitySpec,
|
|
53
|
+
table_name: str | None = None
|
|
54
|
+
) -> list[str]:
|
|
55
|
+
"""
|
|
56
|
+
Generate DDL statements to create the table and its indexes.
|
|
57
|
+
|
|
58
|
+
:param entity_spec: The entity specification describing the table structure.
|
|
59
|
+
:param table_name: Optional override for the table name.
|
|
60
|
+
:returns: A list of DDL SQL statements.
|
|
61
|
+
"""
|
|
62
|
+
db_type_mapper = MSSQLTypeMapper(entity_spec)
|
|
63
|
+
ddl = list[str]()
|
|
64
|
+
table_name = entity_spec.table_name if table_name is None else table_name
|
|
65
|
+
if len(entity_spec.primary_key) == 1:
|
|
66
|
+
primary_key = entity_spec.primary_key[0]
|
|
67
|
+
id_dbtype = db_type_mapper.get_provider_type(primary_key)
|
|
68
|
+
columns = ', '.join([
|
|
69
|
+
f'[{k}] {db_type_mapper.get_provider_type(k)}'
|
|
70
|
+
for k in entity_spec.attrs.keys()
|
|
71
|
+
if k != primary_key
|
|
72
|
+
])
|
|
73
|
+
auto_increment = f' IDENTITY(1,1)' if id_dbtype in ('INT', 'BIGINT', 'SMALLINT') else ''
|
|
74
|
+
ddl.append(f'CREATE TABLE [{table_name}] ({primary_key} {id_dbtype}{auto_increment}, {columns}, PRIMARY KEY ({primary_key}))')
|
|
75
|
+
else:
|
|
76
|
+
columns = ', '.join([
|
|
77
|
+
f'[{k}] {db_type_mapper.get_provider_type(k)}'
|
|
78
|
+
for k in entity_spec.attrs.keys()
|
|
79
|
+
])
|
|
80
|
+
primary_key = (
|
|
81
|
+
f", PRIMARY KEY ({','.join(entity_spec.primary_key)})"
|
|
82
|
+
if len(entity_spec.primary_key) > 0
|
|
83
|
+
else ''
|
|
84
|
+
)
|
|
85
|
+
ddl.append(f'CREATE TABLE [{table_name}] ({columns}{primary_key})')
|
|
86
|
+
for e in self.__generate_table_indexes_ddl(entity_spec, table_name):
|
|
87
|
+
ddl.append(e)
|
|
88
|
+
return ddl
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = ['MSSQLDDLGenerator']
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from mssql_python import Connection
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Any, Literal, Self
|
|
9
|
+
|
|
10
|
+
from ..common.db_connection import DbConnection
|
|
11
|
+
from ..common.db_cursor import DbCursor
|
|
12
|
+
from .mssql_proxy_cursor import MSSQLProxyCursor
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MSSQLProxyConnection(DbConnection):
|
|
16
|
+
"""
|
|
17
|
+
DB-API 2.0 compliant connection interface for ``mssql_python``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__connection: Connection
|
|
21
|
+
|
|
22
|
+
def __init__(self, provider_connection: Connection) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Initialize a new MSSQLProxyConnection.
|
|
25
|
+
|
|
26
|
+
:param provider_connection: A raw mssql_python Connection instance.
|
|
27
|
+
"""
|
|
28
|
+
self.__connection = provider_connection
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def mssql_connection(self) -> Connection:
|
|
32
|
+
"""
|
|
33
|
+
Access the underlying raw mssql_python connection.
|
|
34
|
+
|
|
35
|
+
:returns: The :class:`mssql_python.Connection` instance.
|
|
36
|
+
"""
|
|
37
|
+
return self.__connection
|
|
38
|
+
|
|
39
|
+
def cursor(self, *args: Any, **kwargs: Any) -> DbCursor:
|
|
40
|
+
"""
|
|
41
|
+
Create a new cursor for executing SQL statements.
|
|
42
|
+
|
|
43
|
+
:returns: A :class:`MSSQLProxyCursor` instance.
|
|
44
|
+
"""
|
|
45
|
+
return MSSQLProxyCursor(self.__connection.cursor(*args, **kwargs))
|
|
46
|
+
|
|
47
|
+
def commit(self) -> None:
|
|
48
|
+
"""Commit the current transaction."""
|
|
49
|
+
self.__connection.commit()
|
|
50
|
+
|
|
51
|
+
def rollback(self) -> None:
|
|
52
|
+
"""Rollback the current transaction."""
|
|
53
|
+
self.__connection.rollback()
|
|
54
|
+
|
|
55
|
+
def close(self) -> None:
|
|
56
|
+
"""Close the database connection."""
|
|
57
|
+
self.__connection.close()
|
|
58
|
+
|
|
59
|
+
def __enter__(self) -> Self:
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def __exit__(
|
|
63
|
+
self,
|
|
64
|
+
exc_type: type[BaseException] | None,
|
|
65
|
+
exc: BaseException | None,
|
|
66
|
+
tb: TracebackType | None,
|
|
67
|
+
/
|
|
68
|
+
) -> Literal[False]:
|
|
69
|
+
self.__connection.__exit__(exc_type, exc, tb) # type: ignore[arg-type]
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = ['MSSQLProxyConnection']
|