spanner-adbc 0.8.0__py3-none-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.
@@ -0,0 +1,80 @@
1
+ """ADBC driver for Google Cloud Spanner.
2
+
3
+ This package bundles the prebuilt Spanner ADBC driver shared library and exposes
4
+ a thin Python wrapper around it. The heavy lifting lives in the Rust cdylib; this
5
+ module just locates the bundled library and hands it to ``adbc_driver_manager``,
6
+ which loads it over the ADBC C ABI.
7
+
8
+ For a DBAPI 2.0 (PEP 249) connection with pandas/polars/Arrow helpers, use
9
+ :func:`spanner_adbc.dbapi.connect` instead of the low-level
10
+ :func:`connect` here.
11
+
12
+ To reach the driver through the *generic* ``adbc_driver_manager`` instead — as
13
+ ``driver="spanner"``, or by URI scheme alone from ``uri="spanner:///..."`` —
14
+ install an ADBC driver manifest once with ``python -m
15
+ spanner_adbc.manifest install`` (see :mod:`spanner_adbc.manifest`).
16
+ """
17
+
18
+ import functools
19
+ import pathlib
20
+ import typing
21
+
22
+ import adbc_driver_manager
23
+
24
+ from ._options import ConnectionOptions, DatabaseOptions, StatementOptions
25
+ from ._version import __version__
26
+
27
+ __all__ = [
28
+ "connect",
29
+ "ENTRYPOINT",
30
+ "DatabaseOptions",
31
+ "ConnectionOptions",
32
+ "StatementOptions",
33
+ "__version__",
34
+ ]
35
+
36
+ #: C entrypoint exported by the shared library (see src/ffi.rs).
37
+ ENTRYPOINT = "AdbcSpannerInit"
38
+
39
+
40
+ def connect(
41
+ db_kwargs: typing.Optional[typing.Mapping[str, str]] = None,
42
+ ) -> adbc_driver_manager.AdbcDatabase:
43
+ """Create a low-level ADBC database handle for Spanner.
44
+
45
+ Parameters
46
+ ----------
47
+ db_kwargs:
48
+ Database-level driver options, keyed by their raw option strings — best
49
+ written with the :class:`DatabaseOptions` constants, e.g.
50
+ ``{DatabaseOptions.URI.value: "spanner:///projects/<p>/instances/<i>/databases/<d>"}``
51
+ (see src/lib.rs / docs/options.md). The ``uri`` option requires the
52
+ ``spanner://`` scheme; a bare database path is rejected. Credentials, the
53
+ emulator, endpoint overrides, and every other setting are all passed here
54
+ as their raw keys — for example
55
+ ``{DatabaseOptions.URI.value: "...", DatabaseOptions.KEYFILE.value: "/path/key.json"}``
56
+ or ``{DatabaseOptions.URI.value: "...", DatabaseOptions.EMULATOR.value: "true"}``.
57
+
58
+ For a DBAPI 2.0 connection, prefer :func:`spanner_adbc.dbapi.connect`.
59
+ """
60
+ # ** unpacking accepts the dotted, non-identifier option keys; they land in
61
+ # AdbcDatabase's **kwargs and are forwarded as ADBC options.
62
+ options = dict(db_kwargs) if db_kwargs else {}
63
+ return adbc_driver_manager.AdbcDatabase(
64
+ driver=_driver_path(), entrypoint=ENTRYPOINT, **options
65
+ )
66
+
67
+
68
+ @functools.cache
69
+ def _driver_path() -> str:
70
+ """Absolute path to the shared library bundled in this wheel."""
71
+ here = pathlib.Path(__file__).resolve().parent
72
+ for name in ("libspanner_adbc.so", "libspanner_adbc.dylib", "spanner_adbc.dll"):
73
+ candidate = here / name
74
+ if candidate.is_file():
75
+ return str(candidate)
76
+ raise RuntimeError(
77
+ "spanner_adbc: no bundled Spanner driver library found next to "
78
+ f"{here}. This usually means a source/sdist install without a matching "
79
+ "platform wheel; install a prebuilt wheel for your platform instead."
80
+ )
@@ -0,0 +1,176 @@
1
+ """Typed option-key constants for the Spanner ADBC driver.
2
+
3
+ These enums mirror the style of the BigQuery ADBC driver's ``DatabaseOptions`` /
4
+ ``StatementOptions``: each member's ``.value`` is the raw option-key string the
5
+ driver understands. Every driver setting is passed as one of these keys — through
6
+ ``db_kwargs=`` / ``conn_kwargs=`` on :func:`spanner_adbc.dbapi.connect`, or
7
+ ``adbc_stmt_kwargs=`` on ``conn.cursor(...)`` — and these enums make that
8
+ discoverable and typo-safe.
9
+
10
+ Example::
11
+
12
+ import spanner_adbc.dbapi as spanner
13
+ from spanner_adbc import ConnectionOptions, DatabaseOptions, StatementOptions
14
+
15
+ with spanner.connect(
16
+ db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/p/instances/i/databases/d"},
17
+ conn_kwargs={ConnectionOptions.READ_STALENESS.value: "max:10s"},
18
+ ) as conn:
19
+ cur = conn.cursor(
20
+ adbc_stmt_kwargs={StatementOptions.ROWS_PER_BATCH.value: "1024"}
21
+ )
22
+
23
+ Standard ADBC option keys (``adbc.*``) live in ``adbc_driver_manager``; a few that
24
+ the driver honours are included here for convenience. The authoritative reference
25
+ for every option, its type, default and behaviour is ``docs/options.md``.
26
+ """
27
+
28
+ import enum
29
+
30
+ __all__ = ["DatabaseOptions", "ConnectionOptions", "StatementOptions"]
31
+
32
+
33
+ class DatabaseOptions(enum.Enum):
34
+ """Database-level options (set before connecting, via ``db_kwargs``)."""
35
+
36
+ #: **Standard ADBC.** A ``spanner://`` connection URI whose path is the fully-qualified
37
+ #: database path, ``spanner:///projects/<p>/instances/<i>/databases/<d>``. The scheme is
38
+ #: required; a bare path is rejected. Also settable as the ``uri`` kwarg on ``connect``.
39
+ URI = "uri"
40
+ #: Override the Spanner gRPC endpoint (e.g. an emulator ``host:port``).
41
+ ENDPOINT = "spanner.endpoint"
42
+ #: ``"true"`` to connect with anonymous credentials (emulator mode).
43
+ EMULATOR = "spanner.emulator"
44
+ #: Path to a service-account / credential JSON file.
45
+ KEYFILE = "spanner.auth.keyfile"
46
+ #: The same credential JSON passed inline as a string. Write-only: never
47
+ #: readable back via ``get_option``, and not accepted as a ``URI`` query
48
+ #: parameter (URIs get logged) — pass it here instead.
49
+ KEYFILE_JSON = "spanner.auth.keyfile_json"
50
+ #: A caller-supplied OAuth 2.0 bearer token, sent verbatim (no refresh).
51
+ #: Write-only: never readable back via ``get_option``, and not accepted as a
52
+ #: ``URI`` query parameter (URIs get logged) — pass it here instead.
53
+ ACCESS_TOKEN = "spanner.auth.access_token"
54
+ #: Service account to impersonate; setting it enables impersonation.
55
+ IMPERSONATE_TARGET_PRINCIPAL = "spanner.auth.impersonate.target_principal"
56
+ #: Optional impersonation delegation chain (comma-separated emails).
57
+ IMPERSONATE_DELEGATES = "spanner.auth.impersonate.delegates"
58
+ #: Optional OAuth scopes for the impersonated token (comma-separated).
59
+ IMPERSONATE_SCOPES = "spanner.auth.impersonate.scopes"
60
+ #: Optional impersonated-token lifetime, in seconds (default ``3600``).
61
+ IMPERSONATE_LIFETIME = "spanner.auth.impersonate.lifetime"
62
+ #: The GCP project charged for API quota (the ``x-goog-user-project`` header),
63
+ #: decoupled from the project owning the data. ``""`` unsets.
64
+ QUOTA_PROJECT = "spanner.auth.quota_project"
65
+
66
+
67
+ class ConnectionOptions(enum.Enum):
68
+ """Connection-level options (set via ``conn_kwargs``)."""
69
+
70
+ #: Standard ADBC. ``"false"`` enters buffer-and-commit manual-transaction mode.
71
+ AUTOCOMMIT = "adbc.connection.autocommit"
72
+ #: Standard ADBC. ``"true"`` rejects all writes on the connection.
73
+ READONLY = "adbc.connection.readonly"
74
+ #: Standard ADBC. Isolation level for read/write transactions.
75
+ ISOLATION_LEVEL = "adbc.connection.transaction.isolation_level"
76
+ #: Read bound: one of ``exact:<duration>`` / ``max:<duration>`` / ``read:<rfc3339>`` /
77
+ #: ``min:<rfc3339>``.
78
+ READ_STALENESS = "spanner.read.staleness"
79
+ #: How ``TIMESTAMP`` maps to Arrow (``nanoseconds_error_on_overflow`` / ``microseconds``).
80
+ MAX_TIMESTAMP_PRECISION = "spanner.max_timestamp_precision"
81
+ #: Request priority (``low`` / ``medium`` / ``high``).
82
+ REQUEST_PRIORITY = "spanner.request.priority"
83
+ #: Request tag attached to every query/DML request.
84
+ REQUEST_TAG = "spanner.request.tag"
85
+ #: Transaction tag attached to every read/write transaction (connection-only).
86
+ TRANSACTION_TAG = "spanner.transaction.tag"
87
+ #: Directed-read replica selection for read-only queries.
88
+ DIRECTED_READ = "spanner.directed_read"
89
+ #: Query optimizer version.
90
+ QUERY_OPTIMIZER_VERSION = "spanner.query.optimizer_version"
91
+ #: Query optimizer statistics package.
92
+ QUERY_OPTIMIZER_STATISTICS_PACKAGE = "spanner.query.optimizer_statistics_package"
93
+ #: Maximum commit delay (``0..=500ms``) Spanner may add to batch commits.
94
+ MAX_COMMIT_DELAY = "spanner.commit.max_delay"
95
+ #: ``"true"`` requests commit statistics on read/write commits.
96
+ COMMIT_STATS = "spanner.commit_stats"
97
+ #: ``"true"`` excludes the transaction's writes from change-stream capture.
98
+ EXCLUDE_TXN_FROM_CHANGE_STREAMS = "spanner.transaction.exclude_from_change_streams"
99
+ #: Overall deadline (seconds) on a query's initial execution; ``0`` disables.
100
+ RPC_TIMEOUT_QUERY = "spanner.rpc.timeout_seconds.query"
101
+ #: Overall deadline (seconds) on each write operation; ``0`` disables.
102
+ RPC_TIMEOUT_UPDATE = "spanner.rpc.timeout_seconds.update"
103
+ #: Overall deadline (seconds) on each subsequent chunk fetch; ``0`` disables.
104
+ RPC_TIMEOUT_FETCH = "spanner.rpc.timeout_seconds.fetch"
105
+ #: Cap on retry attempts (first try + retries) for a retryable RPC.
106
+ RETRY_MAX_ATTEMPTS = "spanner.retry.max_attempts"
107
+ #: Cap (seconds) on total wall-clock time spent retrying a retryable RPC.
108
+ RETRY_MAX_ELAPSED_SECONDS = "spanner.retry.max_elapsed_seconds"
109
+ #: Initial delay (seconds) of the exponential backoff between retry attempts.
110
+ RETRY_BACKOFF_INITIAL_SECONDS = "spanner.retry.backoff.initial_seconds"
111
+ #: Ceiling (seconds) the growing retry backoff delay is truncated at.
112
+ RETRY_BACKOFF_MAX_SECONDS = "spanner.retry.backoff.max_seconds"
113
+ #: Per-attempt growth factor for the retry backoff delay.
114
+ RETRY_BACKOFF_MULTIPLIER = "spanner.retry.backoff.multiplier"
115
+
116
+
117
+ class StatementOptions(enum.Enum):
118
+ """Statement-level options (set via ``conn.cursor(adbc_stmt_kwargs=...)``).
119
+
120
+ The options shared with :class:`ConnectionOptions` are per-statement overrides
121
+ of the value inherited from the connection.
122
+ """
123
+
124
+ #: Rows converted into each streamed Arrow ``RecordBatch`` (default ``8192``).
125
+ ROWS_PER_BATCH = "spanner.rows_per_batch"
126
+ #: ``"true"`` runs ``execute_partitions`` partitions on Data Boost.
127
+ DATA_BOOST = "spanner.data_boost"
128
+ #: Per-statement override of :attr:`ConnectionOptions.READ_STALENESS`.
129
+ READ_STALENESS = "spanner.read.staleness"
130
+ #: Per-statement override of :attr:`ConnectionOptions.MAX_TIMESTAMP_PRECISION`.
131
+ MAX_TIMESTAMP_PRECISION = "spanner.max_timestamp_precision"
132
+ #: Per-statement override of :attr:`ConnectionOptions.REQUEST_PRIORITY`.
133
+ REQUEST_PRIORITY = "spanner.request.priority"
134
+ #: Per-statement override of :attr:`ConnectionOptions.REQUEST_TAG`.
135
+ REQUEST_TAG = "spanner.request.tag"
136
+ #: Per-statement override of :attr:`ConnectionOptions.DIRECTED_READ`.
137
+ DIRECTED_READ = "spanner.directed_read"
138
+ #: Per-statement override of :attr:`ConnectionOptions.MAX_COMMIT_DELAY`.
139
+ MAX_COMMIT_DELAY = "spanner.commit.max_delay"
140
+ #: Per-statement override of :attr:`ConnectionOptions.COMMIT_STATS`.
141
+ COMMIT_STATS = "spanner.commit_stats"
142
+ #: Per-statement override of
143
+ #: :attr:`ConnectionOptions.EXCLUDE_TXN_FROM_CHANGE_STREAMS`.
144
+ EXCLUDE_TXN_FROM_CHANGE_STREAMS = "spanner.transaction.exclude_from_change_streams"
145
+ #: Per-statement override of :attr:`ConnectionOptions.QUERY_OPTIMIZER_VERSION`.
146
+ QUERY_OPTIMIZER_VERSION = "spanner.query.optimizer_version"
147
+ #: Per-statement override of :attr:`ConnectionOptions.QUERY_OPTIMIZER_STATISTICS_PACKAGE`.
148
+ QUERY_OPTIMIZER_STATISTICS_PACKAGE = "spanner.query.optimizer_statistics_package"
149
+ #: Per-statement override of :attr:`ConnectionOptions.RPC_TIMEOUT_QUERY`.
150
+ RPC_TIMEOUT_QUERY = "spanner.rpc.timeout_seconds.query"
151
+ #: Per-statement override of :attr:`ConnectionOptions.RPC_TIMEOUT_UPDATE`.
152
+ RPC_TIMEOUT_UPDATE = "spanner.rpc.timeout_seconds.update"
153
+ #: Per-statement override of :attr:`ConnectionOptions.RPC_TIMEOUT_FETCH`.
154
+ RPC_TIMEOUT_FETCH = "spanner.rpc.timeout_seconds.fetch"
155
+ #: Per-statement override of :attr:`ConnectionOptions.RETRY_MAX_ATTEMPTS`.
156
+ RETRY_MAX_ATTEMPTS = "spanner.retry.max_attempts"
157
+ #: Per-statement override of :attr:`ConnectionOptions.RETRY_MAX_ELAPSED_SECONDS`.
158
+ RETRY_MAX_ELAPSED_SECONDS = "spanner.retry.max_elapsed_seconds"
159
+ #: Per-statement override of :attr:`ConnectionOptions.RETRY_BACKOFF_INITIAL_SECONDS`.
160
+ RETRY_BACKOFF_INITIAL_SECONDS = "spanner.retry.backoff.initial_seconds"
161
+ #: Per-statement override of :attr:`ConnectionOptions.RETRY_BACKOFF_MAX_SECONDS`.
162
+ RETRY_BACKOFF_MAX_SECONDS = "spanner.retry.backoff.max_seconds"
163
+ #: Per-statement override of :attr:`ConnectionOptions.RETRY_BACKOFF_MULTIPLIER`.
164
+ RETRY_BACKOFF_MULTIPLIER = "spanner.retry.backoff.multiplier"
165
+ #: How bound Arrow columns pair with ``@name`` parameters (``"true"`` = by name).
166
+ BIND_BY_NAME = "adbc.statement.bind_by_name"
167
+ #: ``"true"`` routes an autocommit bulk ingest's chunks through Spanner's
168
+ #: non-atomic **BatchWrite** RPC instead of a write-only transaction.
169
+ #: Ignored in manual-transaction mode.
170
+ INGEST_BATCH_WRITE = "spanner.ingest.batch_write"
171
+ #: ``"true"`` runs the statement's DML as Partitioned DML: Spanner splits it
172
+ #: across partitions and applies each independently, so a large ``UPDATE`` or
173
+ #: ``DELETE`` never hits the per-commit mutation limit. Not atomic — a partition
174
+ #: may be applied more than once, so the statement must be idempotent — and
175
+ #: ``rowcount`` is a lower bound, not an exact count.
176
+ DML_PARTITIONED = "spanner.dml.partitioned"
@@ -0,0 +1 @@
1
+ __version__ = "0.8.0"
spanner_adbc/dbapi.py ADDED
@@ -0,0 +1,69 @@
1
+ """DBAPI 2.0 (PEP 249) interface for the Spanner ADBC driver.
2
+
3
+ This is the layer most users want: it returns a standard DBAPI connection with
4
+ cursors, plus the ADBC Arrow extensions (``fetch_arrow_table``, ``fetch_df``,
5
+ ``adbc_ingest``) that pandas / polars / DuckDB consume directly.
6
+
7
+ import spanner_adbc.dbapi as spanner
8
+ with spanner.connect(uri="spanner:///projects/p/instances/i/databases/d") as conn:
9
+ df = conn.cursor().execute("SELECT * FROM Singers").fetch_df()
10
+
11
+ Note: DBAPI is autocommit-off by default, which puts this driver into its
12
+ buffer-and-commit manual-transaction mode — call ``conn.commit()`` to apply DML.
13
+ Pass ``autocommit=True`` to keep the driver's default single-statement mode.
14
+ """
15
+
16
+ import typing
17
+
18
+ import adbc_driver_manager.dbapi
19
+
20
+ from . import (
21
+ ENTRYPOINT,
22
+ ConnectionOptions,
23
+ DatabaseOptions,
24
+ StatementOptions,
25
+ _driver_path,
26
+ )
27
+
28
+ __all__ = [
29
+ "connect",
30
+ "DatabaseOptions",
31
+ "ConnectionOptions",
32
+ "StatementOptions",
33
+ ]
34
+
35
+
36
+ def connect(
37
+ db_kwargs: typing.Optional[typing.Mapping[str, str]] = None,
38
+ conn_kwargs: typing.Optional[typing.Mapping[str, str]] = None,
39
+ autocommit: bool = False,
40
+ ) -> adbc_driver_manager.dbapi.Connection:
41
+ """Open a DBAPI 2.0 connection to a Spanner database.
42
+
43
+ Parameters
44
+ ----------
45
+ db_kwargs:
46
+ Database-level driver options, keyed by their raw option strings — best
47
+ written with the :class:`DatabaseOptions` constants, e.g.
48
+ ``{DatabaseOptions.URI.value: "spanner:///projects/<p>/instances/<i>/databases/<d>"}``
49
+ (see src/lib.rs / docs/options.md). The ``uri`` option requires the
50
+ ``spanner://`` scheme; a bare database path is rejected. Credentials, the
51
+ emulator, endpoint overrides, and every other setting are passed here as
52
+ their raw keys — and the secret-holding ``KEYFILE_JSON`` / ``ACCESS_TOKEN``
53
+ options must be, since a ``uri`` may not carry them as query parameters.
54
+ conn_kwargs:
55
+ Connection-level options, keyed with the :class:`ConnectionOptions`
56
+ constants (``adbc.connection.*`` / ``spanner.*``).
57
+ autocommit:
58
+ Toggles PEP 249 autocommit; ``False`` (the default) puts the driver into
59
+ its buffer-and-commit manual-transaction mode.
60
+ """
61
+ # The driver manager builds and owns the database/connection handles here and
62
+ # tears them down if the connection fails, so no manual cleanup is needed.
63
+ return adbc_driver_manager.dbapi.connect(
64
+ driver=_driver_path(),
65
+ entrypoint=ENTRYPOINT,
66
+ db_kwargs=dict(db_kwargs) if db_kwargs else None,
67
+ conn_kwargs=conn_kwargs,
68
+ autocommit=autocommit,
69
+ )
@@ -0,0 +1,284 @@
1
+ """Generate and install an ADBC *driver manifest* for the bundled Spanner driver.
2
+
3
+ A driver manifest is a small TOML file that lets any ADBC driver manager find a
4
+ driver **by name** instead of by an explicit path to a shared library::
5
+
6
+ adbc_driver_manager.dbapi.connect(driver="spanner", uri="spanner:///...")
7
+
8
+ and — because a driver manager with no ``driver`` option falls back to the
9
+ *scheme* of the ``uri`` option, and this driver's connection URIs already use
10
+ the ``spanner://`` scheme — by URI alone::
11
+
12
+ adbc_driver_manager.dbapi.connect(uri="spanner:///projects/p/instances/i/databases/d")
13
+
14
+ See the ADBC specification, ``docs/source/format/driver_manifests.rst``.
15
+
16
+ Why this is not a plain data file in the wheel
17
+ ----------------------------------------------
18
+
19
+ The manifest's ``Driver.shared`` value is passed to ``dlopen``/``LoadLibraryExW``
20
+ **verbatim**: neither the C nor the Rust driver manager resolves it relative to
21
+ the manifest file. So a manifest must contain the *absolute* path of the shared
22
+ library, which is only known once the wheel is installed. A manifest shipped
23
+ pre-baked in the wheel would therefore point nowhere. Instead this module writes
24
+ one at install time::
25
+
26
+ python -m spanner_adbc.manifest install
27
+ # or, equivalently, the console script:
28
+ spanner-adbc-install-manifest
29
+
30
+ By default it writes to a directory the driver manager already searches (see
31
+ :func:`default_install_dir`).
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import argparse
37
+ import os
38
+ import pathlib
39
+ import platform
40
+ import sys
41
+ import sysconfig
42
+ import typing
43
+
44
+ from . import ENTRYPOINT, __version__, _driver_path
45
+
46
+ __all__ = [
47
+ "MANIFEST_NAME",
48
+ "default_install_dir",
49
+ "install",
50
+ "manifest_text",
51
+ "platform_tuple",
52
+ ]
53
+
54
+ #: The manifest's file name. The *base name* is what a driver manager matches
55
+ #: against the driver name (and against a connection URI's scheme), so calling
56
+ #: it anything else breaks ``driver="spanner"``.
57
+ MANIFEST_NAME = "spanner.toml"
58
+
59
+ # Architecture names from the spec's authoritative platform-tuple table, keyed by
60
+ # the values `platform.machine()` reports. Only the architectures this project
61
+ # builds for are listed; anything else is reported as an error rather than
62
+ # guessed at.
63
+ _ARCHES = {
64
+ "x86_64": "amd64",
65
+ "amd64": "amd64",
66
+ "aarch64": "arm64",
67
+ "arm64": "arm64",
68
+ }
69
+
70
+ _OSES = {
71
+ "linux": "linux",
72
+ "darwin": "macos",
73
+ "win32": "windows",
74
+ "cygwin": "windows",
75
+ "freebsd": "freebsd",
76
+ "openbsd": "openbsd",
77
+ }
78
+
79
+
80
+ def _is_musl() -> bool:
81
+ """Whether this interpreter runs against musl libc rather than glibc.
82
+
83
+ The driver manager appends ``_musl`` to the platform tuple when *it* was
84
+ compiled against musl. The driver manager reaching this code is the
85
+ ``adbc_driver_manager`` wheel installed next to this package, so it matches
86
+ the interpreter's own libc.
87
+ """
88
+ if sys.platform != "linux":
89
+ return False
90
+ # The platform tag is the most direct signal: CPython built on Alpine
91
+ # reports e.g. "linux-x86_64" but its wheel tags are musllinux_*, and
92
+ # `sysconfig.get_platform()` on a musl build carries "musl" on the
93
+ # distributions that set it.
94
+ if "musl" in (sysconfig.get_platform() or ""):
95
+ return True
96
+ try:
97
+ # glibc reports e.g. ("glibc", "2.35"); a musl build reports ("", "").
98
+ name, _version = platform.libc_ver()
99
+ except OSError: # pragma: no cover - platform.libc_ver is very forgiving
100
+ return False
101
+ return not name
102
+
103
+
104
+ def platform_tuple() -> str:
105
+ """The ``<os>_<arch>[_<env>]`` tuple the driver manager looks up.
106
+
107
+ Mirrors ``InternalAdbcCurrentArch()`` in the C driver manager
108
+ (``c/driver_manager/adbc_driver_manager.cc``) and ``arch_triplet()`` in the
109
+ Rust one.
110
+ """
111
+ os_name = _OSES.get(sys.platform)
112
+ if os_name is None and sys.platform.startswith("freebsd"):
113
+ os_name = "freebsd"
114
+ machine = platform.machine().lower()
115
+ arch = _ARCHES.get(machine)
116
+ if os_name is None or arch is None:
117
+ raise RuntimeError(
118
+ "spanner_adbc: cannot derive an ADBC platform tuple for "
119
+ f"sys.platform={sys.platform!r} machine={platform.machine()!r}; "
120
+ "this platform is not one the Spanner driver ships binaries for."
121
+ )
122
+ suffix = "_musl" if os_name == "linux" and _is_musl() else ""
123
+ return f"{os_name}_{arch}{suffix}"
124
+
125
+
126
+ def _toml_string(value: str) -> str:
127
+ """Render a TOML basic string.
128
+
129
+ Basic (double-quoted) strings are used rather than literal ones because a
130
+ Windows install path contains backslashes *and* may contain a single quote,
131
+ which a TOML literal string cannot escape.
132
+ """
133
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
134
+ return f'"{escaped}"'
135
+
136
+
137
+ def manifest_text(library: typing.Union[str, "os.PathLike[str]", None] = None) -> str:
138
+ """Render a driver manifest for this installation.
139
+
140
+ Parameters
141
+ ----------
142
+ library:
143
+ Absolute path to the driver shared library. Defaults to the library
144
+ bundled in this wheel.
145
+
146
+ Notes
147
+ -----
148
+ ``Driver.shared`` is emitted as a plain *string* rather than a table of
149
+ platform tuples. The spec allows either ("``Driver.shared`` ... must either
150
+ be a string (single path) or a table of platform-specific paths"), and a
151
+ manifest generated for one installed wheel describes exactly one platform,
152
+ so the string form cannot disagree with the driver manager about how this
153
+ platform is spelled.
154
+ """
155
+ path = pathlib.Path(library) if library is not None else pathlib.Path(_driver_path())
156
+ path = path.resolve()
157
+ return f"""\
158
+ # ADBC driver manifest for Google Cloud Spanner.
159
+ #
160
+ # Generated by `python -m spanner_adbc.manifest install` for the library
161
+ # bundled in the spanner-adbc wheel. Regenerate it after upgrading,
162
+ # reinstalling or moving the Python environment: the path below is absolute and
163
+ # is passed to the dynamic loader verbatim.
164
+ #
165
+ # Platform tuple at generation time: {platform_tuple()}
166
+
167
+ manifest_version = 1
168
+
169
+ name = "Google Cloud Spanner"
170
+ publisher = "fornwall"
171
+ license = "Apache-2.0"
172
+ version = {_toml_string(__version__)}
173
+ url = "https://github.com/fornwall/spanner-adbc"
174
+ source = "spanner-adbc (Python wheel)"
175
+
176
+ [ADBC]
177
+ version = "1.1.0"
178
+
179
+ [Driver]
180
+ entrypoint = {_toml_string(ENTRYPOINT)}
181
+ shared = {_toml_string(str(path))}
182
+ """
183
+
184
+
185
+ def default_install_dir() -> pathlib.Path:
186
+ """The directory to install the manifest into by default.
187
+
188
+ Inside a virtual environment this is ``<sys.prefix>/etc/adbc/drivers``,
189
+ which the Python driver manager adds to its search paths automatically
190
+ (``adbc_driver_manager/_lib.pyx`` does this whenever
191
+ ``sys.prefix != sys.base_prefix``). That keeps the manifest scoped to the
192
+ same environment as the wheel it points into.
193
+
194
+ Outside a venv it is the driver manager's *user* config directory, which is
195
+ searched when the ``SEARCH_USER`` load flag is set (it is, by default):
196
+
197
+ * Linux: ``$XDG_CONFIG_HOME/adbc/drivers``, else ``~/.config/adbc/drivers``
198
+ * macOS: ``~/Library/Application Support/ADBC/Drivers``
199
+ * Windows: ``%LOCALAPPDATA%\\ADBC\\Drivers``
200
+ """
201
+ if sys.prefix != sys.base_prefix:
202
+ return pathlib.Path(sys.prefix) / "etc" / "adbc" / "drivers"
203
+
204
+ if sys.platform == "win32":
205
+ local = os.environ.get("LOCALAPPDATA")
206
+ if not local:
207
+ raise RuntimeError(
208
+ "spanner_adbc: %LOCALAPPDATA% is not set, so the driver "
209
+ "manager's user config directory cannot be located; pass an "
210
+ "explicit directory instead."
211
+ )
212
+ return pathlib.Path(local) / "ADBC" / "Drivers"
213
+
214
+ if sys.platform == "darwin":
215
+ return pathlib.Path.home() / "Library" / "Application Support" / "ADBC" / "Drivers"
216
+
217
+ xdg = os.environ.get("XDG_CONFIG_HOME")
218
+ base = pathlib.Path(xdg) if xdg else pathlib.Path.home() / ".config"
219
+ return base / "adbc" / "drivers"
220
+
221
+
222
+ def install(
223
+ directory: typing.Union[str, "os.PathLike[str]", None] = None,
224
+ *,
225
+ library: typing.Union[str, "os.PathLike[str]", None] = None,
226
+ ) -> pathlib.Path:
227
+ """Write ``spanner.toml`` into *directory* and return the path written.
228
+
229
+ *directory* defaults to :func:`default_install_dir` and is created if it
230
+ does not exist. An existing manifest is overwritten, so re-running this
231
+ after an upgrade is the supported way to refresh a stale path.
232
+ """
233
+ target_dir = pathlib.Path(directory) if directory is not None else default_install_dir()
234
+ target_dir.mkdir(parents=True, exist_ok=True)
235
+ target = target_dir / MANIFEST_NAME
236
+ target.write_text(manifest_text(library), encoding="utf-8")
237
+ return target
238
+
239
+
240
+ def main(argv: typing.Optional[typing.Sequence[str]] = None) -> int:
241
+ parser = argparse.ArgumentParser(
242
+ prog="python -m spanner_adbc.manifest",
243
+ description=(
244
+ "Install an ADBC driver manifest so the Spanner driver resolves by "
245
+ 'name (driver="spanner") or by URI scheme (uri="spanner:///...").'
246
+ ),
247
+ )
248
+ sub = parser.add_subparsers(dest="command")
249
+
250
+ p_install = sub.add_parser("install", help="write the manifest (default)")
251
+ p_install.add_argument(
252
+ "-d",
253
+ "--dir",
254
+ dest="directory",
255
+ default=None,
256
+ help=(
257
+ f"directory to write {MANIFEST_NAME} into (default: a directory the "
258
+ "driver manager searches; see the `path` subcommand)"
259
+ ),
260
+ )
261
+ sub.add_parser("print", help="print the manifest to stdout without writing it")
262
+ sub.add_parser("path", help="print the default install path and exit")
263
+
264
+ args = parser.parse_args(argv)
265
+ command = args.command or "install"
266
+
267
+ if command == "print":
268
+ sys.stdout.write(manifest_text())
269
+ return 0
270
+ if command == "path":
271
+ print(default_install_dir() / MANIFEST_NAME)
272
+ return 0
273
+
274
+ written = install(getattr(args, "directory", None))
275
+ print(f"Wrote {written}")
276
+ print(
277
+ 'The Spanner driver now resolves as driver="spanner" and from '
278
+ 'uri="spanner:///..." for this environment.'
279
+ )
280
+ return 0
281
+
282
+
283
+ if __name__ == "__main__": # pragma: no cover - exercised via the console script
284
+ raise SystemExit(main())
spanner_adbc/py.typed ADDED
File without changes
Binary file
@@ -0,0 +1,361 @@
1
+ Metadata-Version: 2.4
2
+ Name: spanner-adbc
3
+ Version: 0.8.0
4
+ Summary: ADBC (Arrow Database Connectivity) driver for Google Cloud Spanner
5
+ Author-email: Fredrik Fornwall <fredrik@fornwall.net>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/fornwall/spanner-adbc
8
+ Project-URL: Repository, https://github.com/fornwall/spanner-adbc
9
+ Keywords: adbc,arrow,spanner,database,gcp
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Rust
18
+ Classifier: Topic :: Database
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: adbc-driver-manager>=1.0
23
+ Provides-Extra: dbapi
24
+ Requires-Dist: adbc-driver-manager[dbapi]>=1.0; extra == "dbapi"
25
+ Requires-Dist: pyarrow>=14.0.1; extra == "dbapi"
26
+ Dynamic: license-file
27
+
28
+ # spanner-adbc
29
+
30
+ [![PyPI version](https://img.shields.io/pypi/v/spanner-adbc.svg)](https://pypi.org/project/spanner-adbc/)
31
+ [![Python versions](https://img.shields.io/pypi/pyversions/spanner-adbc.svg)](https://pypi.org/project/spanner-adbc/)
32
+ [![Wheel](https://img.shields.io/pypi/wheel/spanner-adbc.svg)](https://pypi.org/project/spanner-adbc/#files)
33
+ [![License](https://img.shields.io/pypi/l/spanner-adbc.svg)](https://github.com/fornwall/spanner-adbc/blob/main/LICENSE)
34
+ [![Build](https://github.com/fornwall/spanner-adbc/actions/workflows/libraries.yml/badge.svg)](https://github.com/fornwall/spanner-adbc/actions/workflows/libraries.yml)
35
+
36
+ A Python [ADBC](https://arrow.apache.org/adbc/) driver for **Google Cloud Spanner**.
37
+
38
+ Query Spanner through [DBAPI 2.0](https://peps.python.org/pep-0249/) and fetch
39
+ [Apache Arrow](https://arrow.apache.org/) results for pandas, Polars, DuckDB, or PyArrow.
40
+
41
+ ## Install
42
+
43
+ Requires Python 3.11 or later. Wheels bundle the native driver; see
44
+ [Supported platforms](#supported-platforms) for OS requirements.
45
+
46
+ ```sh
47
+ pip install "spanner-adbc[dbapi]" # includes PyArrow and pandas
48
+ ```
49
+
50
+ For the low-level ADBC API without DataFrame dependencies, install `spanner-adbc`.
51
+ The Python import name is `spanner_adbc`.
52
+
53
+ ## Quickstart
54
+
55
+ This example assumes an existing `Singers` table with `SingerId` and `FirstName` columns.
56
+
57
+ ```python
58
+ import spanner_adbc.dbapi as spanner
59
+ from spanner_adbc import DatabaseOptions
60
+
61
+ with spanner.connect(
62
+ db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
63
+ ) as conn:
64
+ with conn.cursor() as cur:
65
+ cur.execute("SELECT SingerId, FirstName FROM Singers")
66
+ df = cur.fetch_df() # -> pandas.DataFrame
67
+ ```
68
+
69
+ Use GoogleSQL `@name` parameters with `cur.execute(...)`; `?` placeholders are unsupported.
70
+ `fetchone()` and `fetchall()` return Python rows; Arrow and DataFrame helpers are shown below.
71
+
72
+ ## Driver manifest (`driver="spanner"`)
73
+
74
+ The package's `spanner.connect()` loads its bundled library directly. To use the generic
75
+ `adbc_driver_manager` by driver name, install an [ADBC driver manifest][manifests].
76
+ Use a current driver manager; Python manifest support requires
77
+ [version 1.8.0 or later](https://arrow.apache.org/blog/2025/09/12/adbc-20-release/).
78
+
79
+ ```sh
80
+ pip install --upgrade adbc-driver-manager
81
+ python -m spanner_adbc.manifest install
82
+ ```
83
+
84
+ The equivalent console command is `spanner-adbc-install-manifest`.
85
+
86
+ ```python docs-test: skip
87
+ import adbc_driver_manager.dbapi
88
+
89
+ with adbc_driver_manager.dbapi.connect(
90
+ driver="spanner",
91
+ uri="spanner:///projects/my-project/instances/my-instance/databases/my-db",
92
+ ) as conn:
93
+ ...
94
+ ```
95
+
96
+ Current driver managers can also infer `spanner` from the URI scheme when `driver` is omitted.
97
+
98
+ - Re-run the installer after upgrading, reinstalling, or moving the environment: the manifest
99
+ contains the library's absolute path.
100
+ - The default directory is `<sys.prefix>/etc/adbc/drivers` inside a virtual environment, or the
101
+ platform's user configuration directory otherwise. `python -m spanner_adbc.manifest path`
102
+ prints the target path.
103
+ - Use `install --dir /path/to/drivers` for a custom directory on `ADBC_DRIVER_PATH`.
104
+ - For a standalone shared library, edit the `Driver.shared` paths in the repository's
105
+ [spanner.toml][manifest-file].
106
+
107
+ [manifests]: https://arrow.apache.org/adbc/current/format/driver_manifests.html
108
+ [manifest-file]: https://github.com/fornwall/spanner-adbc/blob/main/spanner.toml
109
+
110
+ ## Authentication
111
+
112
+ With no credential options, the driver uses
113
+ [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials):
114
+ run `gcloud auth application-default login` locally, set `GOOGLE_APPLICATION_CREDENTIALS`, or use
115
+ an attached service account on Google Cloud.
116
+
117
+ Pass explicit credentials in `db_kwargs` alongside `DatabaseOptions.URI.value`:
118
+
119
+ | Option | Value |
120
+ | --- | --- |
121
+ | `DatabaseOptions.KEYFILE` | Credential JSON file path |
122
+ | `DatabaseOptions.KEYFILE_JSON` | Credential JSON contents |
123
+ | `DatabaseOptions.ACCESS_TOKEN` | OAuth bearer token; never refreshed |
124
+ | `DatabaseOptions.IMPERSONATE_TARGET_PRINCIPAL` | Service account to impersonate using ADC or explicit base credentials |
125
+
126
+ Use each enum member's `.value` as the key. `ACCESS_TOKEN` cannot be combined with key-file
127
+ credentials or impersonation. See the [option reference][options] for scopes and other auth settings.
128
+
129
+ For the [Spanner emulator](https://cloud.google.com/spanner/docs/emulator), use anonymous mode:
130
+
131
+ ```python docs-test: skip
132
+ import spanner_adbc.dbapi as spanner
133
+ from spanner_adbc import DatabaseOptions
134
+
135
+ with spanner.connect(db_kwargs={
136
+ DatabaseOptions.URI.value: "spanner:///projects/p/instances/i/databases/d",
137
+ DatabaseOptions.ENDPOINT.value: "localhost:9010",
138
+ DatabaseOptions.EMULATOR.value: "true",
139
+ }) as conn:
140
+ ...
141
+ ```
142
+
143
+ Emulator mode rejects explicit credential options; ambient ADC is ignored.
144
+
145
+ ## Options
146
+
147
+ | `connect()` argument | Purpose |
148
+ | --- | --- |
149
+ | `db_kwargs` | Database options, including the required `uri` |
150
+ | `conn_kwargs` | Connection options |
151
+ | `autocommit` | `False` by default; see [Transactions](#transactions) |
152
+
153
+ Use `DatabaseOptions`, `ConnectionOptions`, and `StatementOptions` enums, or raw option keys.
154
+ The [option reference][options] lists types, defaults, and accepted values. Set cursor options with
155
+ `conn.cursor(adbc_stmt_kwargs={...})` or `cur.adbc_statement.set_options(**{...})`.
156
+
157
+ [options]: https://github.com/fornwall/spanner-adbc/blob/main/docs/options.md
158
+
159
+ ```python
160
+ import spanner_adbc.dbapi as spanner
161
+ from spanner_adbc import ConnectionOptions, DatabaseOptions, StatementOptions
162
+
163
+ with spanner.connect(
164
+ db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/p/instances/i/databases/d"},
165
+ conn_kwargs={ConnectionOptions.READ_STALENESS.value: "max:10s"},
166
+ autocommit=True, # bounded staleness on a single-use read
167
+ ) as conn:
168
+ with conn.cursor(
169
+ adbc_stmt_kwargs={StatementOptions.ROWS_PER_BATCH.value: "1024"}
170
+ ) as cur:
171
+ cur.execute("SELECT * FROM Singers")
172
+ ```
173
+
174
+ In manual query transactions, `max:<duration>` and `min:<timestamp>` bounds become exact
175
+ staleness and a fixed read timestamp respectively. Set `ConnectionOptions.READONLY.value` to
176
+ `"true"` to reject DML, DDL, and ingest; rollback remains available.
177
+
178
+ ## Transactions
179
+
180
+ DBAPI defaults to `autocommit=False`. A manual transaction accepts either queries or writes,
181
+ chosen by its first query or write; mixing them raises `adbc_driver_manager.ProgrammingError` until
182
+ commit or rollback. Queries share a snapshot. DML is buffered until `conn.commit()`, so queries
183
+ cannot read buffered writes.
184
+
185
+ DDL always executes immediately: rollback cannot undo it, and it runs before buffered DML.
186
+ Use `autocommit=True` for immediately committed DML, including `THEN RETURN` statements.
187
+ See [transactions](https://github.com/fornwall/spanner-adbc/blob/main/docs/transactions.md) for
188
+ bulk-ingest and partitioned-DML behavior.
189
+
190
+ ```python
191
+ import spanner_adbc.dbapi as spanner
192
+ from adbc_driver_manager import ProgrammingError
193
+ from spanner_adbc import DatabaseOptions
194
+
195
+ with spanner.connect(
196
+ db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
197
+ ) as conn:
198
+ with conn.cursor() as cur:
199
+ # DDL applies immediately.
200
+ cur.execute("DROP TABLE IF EXISTS Albums")
201
+ cur.execute("CREATE TABLE Albums (Id INT64 NOT NULL) PRIMARY KEY (Id)")
202
+
203
+ cur.execute("INSERT INTO Albums (Id) VALUES (1)") # a DML transaction: buffered
204
+ # Commit before querying the inserted row.
205
+ try:
206
+ cur.execute("SELECT COUNT(*) FROM Albums")
207
+ raise AssertionError("expected the guarded query to raise")
208
+ except ProgrammingError:
209
+ pass
210
+ conn.commit()
211
+
212
+ with conn.cursor() as cur:
213
+ cur.execute("SELECT COUNT(*) FROM Albums")
214
+ assert cur.fetchone()[0] == 1
215
+ conn.rollback() # end the query snapshot
216
+ ```
217
+
218
+ ## Working with DataFrames
219
+
220
+ The quickstart uses pandas. The examples below use the same `Singers` table with `SingerId` and
221
+ `FirstName` columns. Install `polars` or `duckdb` separately for those examples.
222
+
223
+ **pyarrow — results as a native Arrow table:**
224
+
225
+ ```python
226
+ import spanner_adbc.dbapi as spanner
227
+ from spanner_adbc import DatabaseOptions
228
+
229
+ with spanner.connect(
230
+ db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
231
+ ) as conn:
232
+ with conn.cursor() as cur:
233
+ cur.execute("SELECT SingerId, FirstName FROM Singers ORDER BY SingerId")
234
+ table = cur.fetch_arrow_table() # -> pyarrow.Table
235
+ ```
236
+
237
+ **polars — read straight from the connection:**
238
+
239
+ ```python
240
+ import polars as pl
241
+ import spanner_adbc.dbapi as spanner
242
+ from spanner_adbc import DatabaseOptions
243
+
244
+ with spanner.connect(
245
+ db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
246
+ ) as conn:
247
+ df = pl.read_database(
248
+ "SELECT SingerId, FirstName FROM Singers ORDER BY SingerId",
249
+ connection=conn, # an ADBC connection, not a URI
250
+ )
251
+ ```
252
+
253
+ **DuckDB — query the fetched Arrow table in-process:**
254
+
255
+ ```python
256
+ import duckdb
257
+ import spanner_adbc.dbapi as spanner
258
+ from spanner_adbc import DatabaseOptions
259
+
260
+ with spanner.connect(
261
+ db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
262
+ ) as conn:
263
+ with conn.cursor() as cur:
264
+ cur.execute("SELECT SingerId, FirstName FROM Singers")
265
+ singers = cur.fetch_arrow_table()
266
+
267
+ # DuckDB can query the Arrow table by variable name.
268
+ top = duckdb.sql("SELECT COUNT(*) AS n, MIN(FirstName) AS first FROM singers").fetchone()
269
+ ```
270
+
271
+ ## Bulk insert a DataFrame
272
+
273
+ `cur.adbc_ingest(table, data, mode=...)` inserts Arrow-compatible data in bulk. Convert pandas
274
+ DataFrames to Arrow as shown here:
275
+
276
+ ```python
277
+ import pandas as pd
278
+ import pyarrow as pa
279
+ import spanner_adbc.dbapi as spanner
280
+ from spanner_adbc import DatabaseOptions
281
+
282
+ frame = pd.DataFrame({"SingerId": [10, 11], "FirstName": ["Carol", "Dave"]})
283
+
284
+ with spanner.connect(
285
+ db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
286
+ autocommit=True, # apply immediately; returns the row count
287
+ ) as conn:
288
+ with conn.cursor() as cur:
289
+ # `append` inserts into an existing table (the default mode is `create`).
290
+ rows = cur.adbc_ingest("Singers", pa.Table.from_pandas(frame), mode="append")
291
+ ```
292
+
293
+ The `mode` selects how the target table is handled:
294
+
295
+ - `create` — create the table from the data's Arrow schema first, erroring if it already exists (the default).
296
+ - `append` — insert into an existing table.
297
+ - `create_append` — create the table only if it is absent, then insert.
298
+ - `replace` — drop any existing table, recreate it from the schema, then insert.
299
+
300
+ Create modes omit a primary key, so Spanner supplies a [hidden `rowid` column][no-pk].
301
+ For an explicit primary key, create the table with SQL first and use `mode="append"`.
302
+ In manual mode, inserts need `conn.commit()`; table creation or replacement still happens immediately.
303
+ Autocommit loads may commit in multiple chunks, so a failed load can leave rows written.
304
+
305
+ [no-pk]: https://cloud.google.com/spanner/docs/primary-key-default-value#tables-without-primary-keys
306
+
307
+ ## Partitioned reads and Data Boost
308
+
309
+ A large scan can be split into independent partitions and read in parallel — optionally on Spanner's
310
+ serverless [Data Boost] compute, so the work is isolated from your provisioned instance. This uses
311
+ the ADBC partitioned-execution extension (`adbc_execute_partitions` / `adbc_read_partition`):
312
+
313
+ ```python
314
+ import spanner_adbc.dbapi as spanner
315
+ from spanner_adbc import DatabaseOptions, StatementOptions
316
+
317
+ with spanner.connect(
318
+ db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
319
+ ) as conn:
320
+ with conn.cursor() as cur:
321
+ # Optional statement options, set on the underlying ADBC statement:
322
+ cur.adbc_statement.set_options(**{
323
+ StatementOptions.DATA_BOOST.value: "true", # run on Data Boost
324
+ })
325
+ partitions, schema = cur.adbc_execute_partitions("SELECT SingerId FROM Singers")
326
+
327
+ # Each descriptor is opaque bytes; it can be shipped to another worker,
328
+ # process, or connection and read independently.
329
+ for token in partitions:
330
+ with conn.cursor() as cur:
331
+ cur.adbc_read_partition(token)
332
+ table = cur.fetch_arrow_table()
333
+ ...
334
+ ```
335
+
336
+ Spanner decides partitionability from the query plan. Simple scans are suitable; joins, ordering,
337
+ and aggregation can prevent partitioning. See the [partitionability rules][parallel-reads].
338
+
339
+ Descriptors contain SQL and transaction identifiers and are unauthenticated. Only read descriptors
340
+ from trusted sources: they execute using the receiving connection's credentials.
341
+
342
+ [Data Boost]: https://cloud.google.com/spanner/docs/databoost/databoost-overview
343
+ [parallel-reads]: https://cloud.google.com/spanner/docs/reads#read_data_in_parallel
344
+
345
+ ## Supported platforms
346
+
347
+ `pip` selects a wheel matching the OS and architecture:
348
+
349
+ | Platform | Wheel tag | Minimum requirement |
350
+ | -------------- | ------------------------ | -------------------------------------------- |
351
+ | Linux x86-64 | `manylinux_2_35_x86_64` | glibc >= 2.35 |
352
+ | Linux aarch64 | `manylinux_2_35_aarch64` | glibc >= 2.35 |
353
+ | Linux x86-64 musl | `musllinux_1_2_x86_64` | musl libc >= 1.2 |
354
+ | Linux aarch64 musl | `musllinux_1_2_aarch64` | musl libc >= 1.2 |
355
+ | macOS arm64 | `macosx_11_0_arm64` | macOS >= 11.0 |
356
+ | macOS x86-64 | `macosx_10_15_x86_64` | macOS >= 10.15 |
357
+ | Windows x86-64 | `win_amd64` | 64-bit Windows |
358
+ | Windows arm64 | `win_arm64` | ARM64 Windows |
359
+
360
+ Building for an older OS requires a compatible native driver and Python dependencies.
361
+ For local builds and releases, see [CONTRIBUTING.md](https://github.com/fornwall/spanner-adbc/blob/main/CONTRIBUTING.md).
@@ -0,0 +1,13 @@
1
+ spanner_adbc/__init__.py,sha256=JaM_rJCT-CHATYa7-uJ8YwPUbbiYGbGRw-smVB_B6nw,3131
2
+ spanner_adbc/_options.py,sha256=E7CmaEoTXI2aTdeqcezKCm8NpabOdIk-w9mIHztr6-A,10169
3
+ spanner_adbc/_version.py,sha256=iPlYCcIzuzW7T2HKDkmYlMkRI51dBLfNRxPPiWrfw9U,22
4
+ spanner_adbc/dbapi.py,sha256=386EflvG4UZuOhAuPXurbaR4IS7YLZJ4PKx3JjAQm48,2676
5
+ spanner_adbc/manifest.py,sha256=gzkRLvApR8Qr_pSOt3eJ28exp8UuwH38EbhiNgjNFF8,10214
6
+ spanner_adbc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ spanner_adbc/spanner_adbc.dll,sha256=ZaCvm9ysnwMcJZp-cmK_4iIH9_0XyTejALTdXWkU_BQ,16002560
8
+ spanner_adbc-0.8.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
9
+ spanner_adbc-0.8.0.dist-info/METADATA,sha256=swV8k52jxUjSLmpSrShJHl4vE7DX4yTMTJQ2dwMEA-8,15354
10
+ spanner_adbc-0.8.0.dist-info/WHEEL,sha256=QE-niYB-ZRQVWczDB301OM3v6K9Rt_4BEsv6l0aMysk,98
11
+ spanner_adbc-0.8.0.dist-info/entry_points.txt,sha256=S9FfsSQaBxlssjyACrt16kcxWdYq3ptxDVXCuYkCF58,77
12
+ spanner_adbc-0.8.0.dist-info/top_level.txt,sha256=sBmJlDBf448Pz4G5lANkoLq6qyRvZ8tawyxyOqDmlFk,13
13
+ spanner_adbc-0.8.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_arm64
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ spanner-adbc-install-manifest = spanner_adbc.manifest:main
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1 @@
1
+ spanner_adbc