arrowsql 0.1.0__cp313-cp313-win_amd64.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.
- arrowsql/__init__.py +653 -0
- arrowsql/_rust_auth.py +170 -0
- arrowsql/_server_sqlstate.py +11 -0
- arrowsql/_utf8_strings.py +78 -0
- arrowsql/agent.py +620 -0
- arrowsql/aio.py +533 -0
- arrowsql/arrow_parallel.py +464 -0
- arrowsql/async_query/__init__.py +40 -0
- arrowsql/async_query/_connection_context.py +39 -0
- arrowsql/async_query/_native.py +24 -0
- arrowsql/async_query/async_connection.py +160 -0
- arrowsql/async_query/async_cursor.py +225 -0
- arrowsql/async_query/async_execute.py +113 -0
- arrowsql/async_query/async_fetch.py +153 -0
- arrowsql/async_query/exception_translator.py +132 -0
- arrowsql/auth.py +913 -0
- arrowsql/bulk_parallel.py +385 -0
- arrowsql/connection.py +2377 -0
- arrowsql/connection_string_builder.py +114 -0
- arrowsql/connection_string_parser.py +417 -0
- arrowsql/constants.py +701 -0
- arrowsql/cursor.py +4516 -0
- arrowsql/db_connection.py +98 -0
- arrowsql/ddbc_bindings.cp313-win_amd64.pyd +0 -0
- arrowsql/ddbc_bindings.pyi +283 -0
- arrowsql/decimal_config.py +98 -0
- arrowsql/exceptions.py +378 -0
- arrowsql/helpers.py +436 -0
- arrowsql/logging.py +700 -0
- arrowsql/mssql_python.pyi +579 -0
- arrowsql/odbc_provider.py +269 -0
- arrowsql/parameter_helper.py +442 -0
- arrowsql/perf_timer.py +286 -0
- arrowsql/pooling.py +147 -0
- arrowsql/py.typed +1 -0
- arrowsql/row.py +462 -0
- arrowsql/type.py +160 -0
- arrowsql-0.1.0.dist-info/METADATA +146 -0
- arrowsql-0.1.0.dist-info/RECORD +45 -0
- arrowsql-0.1.0.dist-info/WHEEL +4 -0
- arrowsql-0.1.0.dist-info/entry_points.txt +2 -0
- arrowsql-0.1.0.dist-info/licenses/LICENSE +21 -0
- arrowsql-0.1.0.dist-info/licenses/LICENSE.upstream-mssql-python +47 -0
- arrowsql-0.1.0.dist-info/sboms/ddbc_bindings.cyclonedx.json +5195 -0
- mssql_python/__init__.py +39 -0
arrowsql/_rust_auth.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication= handling for the Rust core (RUST-PORT, new module).
|
|
3
|
+
|
|
4
|
+
The Rust core logs in with exactly what the connection string says; it has no
|
|
5
|
+
msodbcsql underneath that interprets ``Authentication=``. Upstream's auth.py
|
|
6
|
+
returns ``None`` for the modes ODBC "handles natively" (ServicePrincipal,
|
|
7
|
+
Windows Interactive) and does not know ``SqlPassword`` / ``ActiveDirectoryPassword``
|
|
8
|
+
/ ``ActiveDirectoryIntegrated`` at all, so without this module those would fall
|
|
9
|
+
through to a SQL login with the client id or UPN. This module keeps auth.py
|
|
10
|
+
(and the tests that pin its return values) unchanged and makes every
|
|
11
|
+
``Authentication`` value either work or fail with a clear error.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Callable, Dict, Optional
|
|
18
|
+
|
|
19
|
+
from arrowsql.constants import (
|
|
20
|
+
AuthType,
|
|
21
|
+
_KEY_AUTHENTICATION,
|
|
22
|
+
_KEY_PWD,
|
|
23
|
+
_KEY_UID,
|
|
24
|
+
)
|
|
25
|
+
from arrowsql.exceptions import InterfaceError
|
|
26
|
+
|
|
27
|
+
_SQL_PASSWORD = "sqlpassword"
|
|
28
|
+
_AD_PASSWORD = "activedirectorypassword"
|
|
29
|
+
_MANAGED_IDENTITY = "activedirectorymanagedidentity"
|
|
30
|
+
|
|
31
|
+
# Acquired by Connection.__init__ (auth.py) before the native connect.
|
|
32
|
+
_TOKEN_MODES = frozenset(
|
|
33
|
+
{
|
|
34
|
+
AuthType.INTERACTIVE.value,
|
|
35
|
+
AuthType.DEVICE_CODE.value,
|
|
36
|
+
AuthType.DEFAULT.value,
|
|
37
|
+
AuthType.MSI.value,
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
_UNSUPPORTED = {
|
|
41
|
+
"activedirectoryintegrated": (
|
|
42
|
+
"Authentication=ActiveDirectoryIntegrated is not supported by the Rust core. "
|
|
43
|
+
"For Kerberos (GSSAPI) integrated login use Trusted_Connection=yes; for Entra ID "
|
|
44
|
+
"pass a token with token_provider= or Authentication=ActiveDirectoryDefault."
|
|
45
|
+
),
|
|
46
|
+
"activedirectoryworkloadidentity": (
|
|
47
|
+
"Authentication=ActiveDirectoryWorkloadIdentity is not supported by the Rust core. "
|
|
48
|
+
"Pass a token with token_provider= or use Authentication=ActiveDirectoryDefault."
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
_KNOWN = (
|
|
52
|
+
_TOKEN_MODES
|
|
53
|
+
| {AuthType.SERVICE_PRINCIPAL.value, _SQL_PASSWORD, _AD_PASSWORD, _MANAGED_IDENTITY}
|
|
54
|
+
| set(_UNSUPPORTED)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Public client id of the Azure CLI, the one tenant-agnostic first-party client
|
|
58
|
+
# that allows the username/password (ROPC) grant without an app registration.
|
|
59
|
+
_PASSWORD_CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
|
|
60
|
+
|
|
61
|
+
_AZURE_HINT = "Install the optional dependency: pip install arrowsql[azure]"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _require_azure_identity(mode: str) -> None:
|
|
65
|
+
try:
|
|
66
|
+
import azure.identity # noqa: F401 pylint: disable=import-outside-toplevel,unused-import
|
|
67
|
+
except ImportError as e:
|
|
68
|
+
raise InterfaceError(
|
|
69
|
+
driver_error=f"Authentication={mode} needs azure-identity. {_AZURE_HINT}",
|
|
70
|
+
ddbc_error="azure-identity is not installed.",
|
|
71
|
+
) from e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def make_password_token_factory(username: str, password: str) -> Callable[[str, str, str], bytes]:
|
|
75
|
+
"""Token factory for ``ActiveDirectoryPassword``.
|
|
76
|
+
|
|
77
|
+
Same contract as ``ServicePrincipalAuth.make_token_factory``: called by the
|
|
78
|
+
native login with ``(spn, sts_url, auth_method)`` once the server has named
|
|
79
|
+
the tenant, returns the JWT as bare UTF-16LE bytes.
|
|
80
|
+
"""
|
|
81
|
+
if not username or not password:
|
|
82
|
+
raise ValueError("ActiveDirectoryPassword auth requires UID (user principal name) and PWD")
|
|
83
|
+
|
|
84
|
+
def _factory(spn: str, sts_url: str, _auth_method: str) -> bytes:
|
|
85
|
+
# pylint: disable=import-outside-toplevel
|
|
86
|
+
from azure.identity import UsernamePasswordCredential
|
|
87
|
+
from azure.core.exceptions import ClientAuthenticationError
|
|
88
|
+
from arrowsql.auth import _parse_tenant_id
|
|
89
|
+
|
|
90
|
+
if not spn:
|
|
91
|
+
raise RuntimeError("ActiveDirectoryPassword: empty SPN from server")
|
|
92
|
+
tenant_id = _parse_tenant_id(sts_url)
|
|
93
|
+
if not tenant_id:
|
|
94
|
+
raise RuntimeError(f"Could not extract tenant_id from STS URL: {sts_url!r}")
|
|
95
|
+
scope = spn if spn.endswith("/.default") else spn.rstrip("/") + "/.default"
|
|
96
|
+
try:
|
|
97
|
+
credential = UsernamePasswordCredential(
|
|
98
|
+
client_id=_PASSWORD_CLIENT_ID,
|
|
99
|
+
username=username,
|
|
100
|
+
password=password,
|
|
101
|
+
tenant_id=tenant_id,
|
|
102
|
+
)
|
|
103
|
+
token = credential.get_token(scope).token
|
|
104
|
+
except ClientAuthenticationError:
|
|
105
|
+
# The provider text can echo the account; keep it out of the exception chain.
|
|
106
|
+
raise RuntimeError(
|
|
107
|
+
"ActiveDirectoryPassword authentication failed (wrong credentials, MFA "
|
|
108
|
+
"required, or the account is not allowed the password grant)"
|
|
109
|
+
) from None
|
|
110
|
+
return token.encode("utf-16-le")
|
|
111
|
+
|
|
112
|
+
return _factory
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def prepare(parsed_params: Dict[str, str]) -> Optional[Callable[[str, str, str], bytes]]:
|
|
116
|
+
"""Validate ``Authentication`` and return the federated token factory, if any.
|
|
117
|
+
|
|
118
|
+
``ActiveDirectoryManagedIdentity`` is rewritten in *parsed_params* to its
|
|
119
|
+
``ActiveDirectoryMSI`` alias so the token path in ``Connection.__init__``
|
|
120
|
+
picks it up. Raises ``InterfaceError`` for unknown or unsupported values.
|
|
121
|
+
"""
|
|
122
|
+
raw = parsed_params.get(_KEY_AUTHENTICATION, "")
|
|
123
|
+
mode = raw.strip().lower()
|
|
124
|
+
if not mode:
|
|
125
|
+
return None
|
|
126
|
+
if mode not in _KNOWN:
|
|
127
|
+
raise InterfaceError(
|
|
128
|
+
driver_error=f"Invalid value specified for connection string attribute 'Authentication': {raw}",
|
|
129
|
+
ddbc_error=f"Unknown Authentication value: {raw}",
|
|
130
|
+
)
|
|
131
|
+
if mode in _UNSUPPORTED:
|
|
132
|
+
raise InterfaceError(driver_error=_UNSUPPORTED[mode], ddbc_error=f"Unsupported: {raw}")
|
|
133
|
+
if mode == _MANAGED_IDENTITY:
|
|
134
|
+
parsed_params[_KEY_AUTHENTICATION] = "ActiveDirectoryMSI"
|
|
135
|
+
return None
|
|
136
|
+
if mode not in (AuthType.SERVICE_PRINCIPAL.value, _AD_PASSWORD):
|
|
137
|
+
return None
|
|
138
|
+
_require_azure_identity(raw)
|
|
139
|
+
uid = (parsed_params.get(_KEY_UID) or "").strip()
|
|
140
|
+
pwd = parsed_params.get(_KEY_PWD) or ""
|
|
141
|
+
try:
|
|
142
|
+
if mode == _AD_PASSWORD:
|
|
143
|
+
return make_password_token_factory(uid, pwd)
|
|
144
|
+
# pylint: disable=import-outside-toplevel
|
|
145
|
+
from arrowsql.auth import ServicePrincipalAuth
|
|
146
|
+
|
|
147
|
+
return ServicePrincipalAuth.make_token_factory(uid, pwd)
|
|
148
|
+
except ValueError as e:
|
|
149
|
+
raise InterfaceError(
|
|
150
|
+
driver_error=f"Authentication={raw} requires UID and PWD: {e}",
|
|
151
|
+
ddbc_error=str(e),
|
|
152
|
+
) from e
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def reject_unhandled(parsed_params: Dict[str, str]) -> None:
|
|
156
|
+
"""Raise when a token mode produced no token (Windows Interactive).
|
|
157
|
+
|
|
158
|
+
Upstream leaves Windows Interactive to ODBC's native AAD prompt, which the
|
|
159
|
+
Rust core does not have; connecting anyway would be a SQL login.
|
|
160
|
+
"""
|
|
161
|
+
mode = parsed_params.get(_KEY_AUTHENTICATION, "").strip().lower()
|
|
162
|
+
if mode in _TOKEN_MODES:
|
|
163
|
+
raise InterfaceError(
|
|
164
|
+
driver_error=(
|
|
165
|
+
f"Authentication={parsed_params[_KEY_AUTHENTICATION]} is not available on "
|
|
166
|
+
f"{sys.platform}: the Rust core has no native interactive flow. "
|
|
167
|
+
"Use token_provider= or Authentication=ActiveDirectoryDefault."
|
|
168
|
+
),
|
|
169
|
+
ddbc_error="No native interactive Entra ID flow.",
|
|
170
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQL Server engine error number -> ODBC 3.x SQLSTATE.
|
|
3
|
+
|
|
4
|
+
The table lives in the native layer (src/diag.rs); this module re-exports it
|
|
5
|
+
for the async py-core path, so exceptions still carry the SQLSTATE that
|
|
6
|
+
msodbcsql would report (#581).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from arrowsql.ddbc_bindings import sqlstate_for_server_error
|
|
10
|
+
|
|
11
|
+
__all__ = ["sqlstate_for_server_error"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""RUST-PORT: UTF-8 transfer of national character columns for Arrow reads.
|
|
2
|
+
|
|
3
|
+
SQL Server sends nvarchar/nchar/ntext as UTF-16 (two bytes per character,
|
|
4
|
+
even ASCII) and TDS has no compression. ``utf8_query`` rewrites a query so
|
|
5
|
+
those columns travel as ``varchar`` in a UTF-8 collation, about half the
|
|
6
|
+
bytes for mostly-ASCII text; the decoded Arrow type is the same string type.
|
|
7
|
+
The SQL text is built in Rust (``ddbc_bindings.utf8_wrap_query``); this module
|
|
8
|
+
only runs the describe round trip and decides when to skip.
|
|
9
|
+
|
|
10
|
+
Every failure path returns the original query: the rewrite is an
|
|
11
|
+
optimization, never a reason for a read to fail.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import weakref
|
|
16
|
+
from contextlib import closing
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from arrowsql import ddbc_bindings
|
|
20
|
+
from arrowsql.logging import logger
|
|
21
|
+
|
|
22
|
+
# One round trip: server capability plus the described, visible columns.
|
|
23
|
+
_DESCRIBE_SQL = (
|
|
24
|
+
"SELECT CAST(SERVERPROPERTY('ProductMajorVersion') AS int), "
|
|
25
|
+
"CAST(SERVERPROPERTY('EngineEdition') AS int), "
|
|
26
|
+
"d.name, d.system_type_id, d.max_length, d.is_nullable "
|
|
27
|
+
"FROM sys.dm_exec_describe_first_result_set(?, NULL, 0) AS d "
|
|
28
|
+
"WHERE d.is_hidden = 0 ORDER BY d.column_ordinal"
|
|
29
|
+
)
|
|
30
|
+
# Rewrite only when UTF-8 is at most this share of the UTF-16 bytes of the
|
|
31
|
+
# sampled rows. CJK is 3 bytes per character in UTF-8 against 2 in UTF-16.
|
|
32
|
+
_MAX_SIZE_RATIO = 0.9
|
|
33
|
+
# The rewrite puts the query in a derived table, which drops ORDER BY.
|
|
34
|
+
_ORDER_BY = re.compile(r"\bORDER\s+BY\b", re.IGNORECASE)
|
|
35
|
+
|
|
36
|
+
# connection -> server accepts UTF-8 collations (learned from the first describe).
|
|
37
|
+
_server_support: "weakref.WeakKeyDictionary[Any, bool]" = weakref.WeakKeyDictionary()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def utf8_query(conn: Any, query: str) -> str:
|
|
41
|
+
"""``query`` rewritten to transfer nvarchar/nchar/ntext columns as UTF-8.
|
|
42
|
+
|
|
43
|
+
Returns ``query`` itself when the server predates UTF-8 collations
|
|
44
|
+
(SQL Server 2019 / Azure SQL), the query has an ORDER BY, no column would
|
|
45
|
+
change, the column names are empty or duplicated, the describe or the
|
|
46
|
+
sample probe fails, or the first 256 rows are not at least 10% smaller in
|
|
47
|
+
UTF-8 (CJK text grows: 3 bytes per character against 2).
|
|
48
|
+
"""
|
|
49
|
+
if _ORDER_BY.search(query) or _server_support.get(conn) is False:
|
|
50
|
+
return query
|
|
51
|
+
try:
|
|
52
|
+
with closing(conn.cursor()) as cursor:
|
|
53
|
+
cursor.execute(_DESCRIBE_SQL, (query,))
|
|
54
|
+
rows = cursor.fetchall()
|
|
55
|
+
if not rows:
|
|
56
|
+
return query
|
|
57
|
+
supported = ddbc_bindings.utf8_collations_supported(rows[0][0], rows[0][1])
|
|
58
|
+
try:
|
|
59
|
+
_server_support[conn] = supported
|
|
60
|
+
except TypeError:
|
|
61
|
+
pass
|
|
62
|
+
if not supported:
|
|
63
|
+
return query
|
|
64
|
+
plan = ddbc_bindings.utf8_wrap_query(query, [(r[2], r[3], r[4], r[5]) for r in rows])
|
|
65
|
+
if plan is None:
|
|
66
|
+
return query
|
|
67
|
+
wrapped, sample_sql = plan
|
|
68
|
+
# Runs the query's first rows: also finds out that the rewrite is
|
|
69
|
+
# invalid (batches, EXEC, ...) and how big the strings are in UTF-8.
|
|
70
|
+
cursor.execute(sample_sql)
|
|
71
|
+
utf8_bytes, utf16_bytes = cursor.fetchone()
|
|
72
|
+
if not utf16_bytes or utf8_bytes > _MAX_SIZE_RATIO * utf16_bytes:
|
|
73
|
+
logger.debug("utf8_strings: no saving in sample (%s vs %s bytes)", utf8_bytes, utf16_bytes)
|
|
74
|
+
return query
|
|
75
|
+
return wrapped
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
logger.debug("utf8_strings: keeping the original query (%s)", exc)
|
|
78
|
+
return query
|