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.
Files changed (45) hide show
  1. arrowsql/__init__.py +653 -0
  2. arrowsql/_rust_auth.py +170 -0
  3. arrowsql/_server_sqlstate.py +11 -0
  4. arrowsql/_utf8_strings.py +78 -0
  5. arrowsql/agent.py +620 -0
  6. arrowsql/aio.py +533 -0
  7. arrowsql/arrow_parallel.py +464 -0
  8. arrowsql/async_query/__init__.py +40 -0
  9. arrowsql/async_query/_connection_context.py +39 -0
  10. arrowsql/async_query/_native.py +24 -0
  11. arrowsql/async_query/async_connection.py +160 -0
  12. arrowsql/async_query/async_cursor.py +225 -0
  13. arrowsql/async_query/async_execute.py +113 -0
  14. arrowsql/async_query/async_fetch.py +153 -0
  15. arrowsql/async_query/exception_translator.py +132 -0
  16. arrowsql/auth.py +913 -0
  17. arrowsql/bulk_parallel.py +385 -0
  18. arrowsql/connection.py +2377 -0
  19. arrowsql/connection_string_builder.py +114 -0
  20. arrowsql/connection_string_parser.py +417 -0
  21. arrowsql/constants.py +701 -0
  22. arrowsql/cursor.py +4516 -0
  23. arrowsql/db_connection.py +98 -0
  24. arrowsql/ddbc_bindings.cp313-win_amd64.pyd +0 -0
  25. arrowsql/ddbc_bindings.pyi +283 -0
  26. arrowsql/decimal_config.py +98 -0
  27. arrowsql/exceptions.py +378 -0
  28. arrowsql/helpers.py +436 -0
  29. arrowsql/logging.py +700 -0
  30. arrowsql/mssql_python.pyi +579 -0
  31. arrowsql/odbc_provider.py +269 -0
  32. arrowsql/parameter_helper.py +442 -0
  33. arrowsql/perf_timer.py +286 -0
  34. arrowsql/pooling.py +147 -0
  35. arrowsql/py.typed +1 -0
  36. arrowsql/row.py +462 -0
  37. arrowsql/type.py +160 -0
  38. arrowsql-0.1.0.dist-info/METADATA +146 -0
  39. arrowsql-0.1.0.dist-info/RECORD +45 -0
  40. arrowsql-0.1.0.dist-info/WHEEL +4 -0
  41. arrowsql-0.1.0.dist-info/entry_points.txt +2 -0
  42. arrowsql-0.1.0.dist-info/licenses/LICENSE +21 -0
  43. arrowsql-0.1.0.dist-info/licenses/LICENSE.upstream-mssql-python +47 -0
  44. arrowsql-0.1.0.dist-info/sboms/ddbc_bindings.cyclonedx.json +5195 -0
  45. 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