adbc-driver-spanner 0.5.0__py3-none-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.
- adbc_driver_spanner/__init__.py +116 -0
- adbc_driver_spanner/_version.py +1 -0
- adbc_driver_spanner/adbc_spanner.dll +0 -0
- adbc_driver_spanner/dbapi.py +58 -0
- adbc_driver_spanner/py.typed +0 -0
- adbc_driver_spanner-0.5.0.dist-info/METADATA +190 -0
- adbc_driver_spanner-0.5.0.dist-info/RECORD +9 -0
- adbc_driver_spanner-0.5.0.dist-info/WHEEL +5 -0
- adbc_driver_spanner-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
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:`adbc_driver_spanner.dbapi.connect` instead of the low-level
|
|
10
|
+
:func:`connect` here.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import functools
|
|
14
|
+
import pathlib
|
|
15
|
+
import typing
|
|
16
|
+
|
|
17
|
+
import adbc_driver_manager
|
|
18
|
+
|
|
19
|
+
from ._version import __version__
|
|
20
|
+
|
|
21
|
+
__all__ = ["connect", "option_kwargs", "ENTRYPOINT", "__version__"]
|
|
22
|
+
|
|
23
|
+
#: C entrypoint exported by the shared library (see src/ffi.rs).
|
|
24
|
+
ENTRYPOINT = "AdbcSpannerInit"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def option_kwargs(
|
|
28
|
+
database: typing.Optional[str] = None,
|
|
29
|
+
*,
|
|
30
|
+
endpoint: typing.Optional[str] = None,
|
|
31
|
+
emulator: bool = False,
|
|
32
|
+
keyfile: typing.Optional[str] = None,
|
|
33
|
+
keyfile_json: typing.Optional[str] = None,
|
|
34
|
+
db_kwargs: typing.Optional[typing.Mapping[str, str]] = None,
|
|
35
|
+
) -> typing.Dict[str, str]:
|
|
36
|
+
"""Translate the friendly connection kwargs into ``adbc.spanner.*`` options.
|
|
37
|
+
|
|
38
|
+
Shared by :func:`connect` and :func:`adbc_driver_spanner.dbapi.connect` so the
|
|
39
|
+
two entry points map parameters identically. ``db_kwargs`` is an escape hatch
|
|
40
|
+
for raw option keys and is merged last.
|
|
41
|
+
"""
|
|
42
|
+
options: typing.Dict[str, str] = {}
|
|
43
|
+
# Friendly kwargs -> the driver's option keys (see src/lib.rs).
|
|
44
|
+
if database is not None:
|
|
45
|
+
options["adbc.spanner.database"] = database
|
|
46
|
+
if endpoint is not None:
|
|
47
|
+
options["adbc.spanner.endpoint"] = endpoint
|
|
48
|
+
if emulator:
|
|
49
|
+
options["adbc.spanner.emulator"] = "true"
|
|
50
|
+
if keyfile is not None:
|
|
51
|
+
options["adbc.spanner.keyfile"] = keyfile
|
|
52
|
+
if keyfile_json is not None:
|
|
53
|
+
options["adbc.spanner.keyfile_json"] = keyfile_json
|
|
54
|
+
if db_kwargs:
|
|
55
|
+
options.update(db_kwargs)
|
|
56
|
+
return options
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def connect(
|
|
60
|
+
database: typing.Optional[str] = None,
|
|
61
|
+
*,
|
|
62
|
+
endpoint: typing.Optional[str] = None,
|
|
63
|
+
emulator: bool = False,
|
|
64
|
+
keyfile: typing.Optional[str] = None,
|
|
65
|
+
keyfile_json: typing.Optional[str] = None,
|
|
66
|
+
db_kwargs: typing.Optional[typing.Mapping[str, str]] = None,
|
|
67
|
+
) -> adbc_driver_manager.AdbcDatabase:
|
|
68
|
+
"""Create a low-level ADBC database handle for Spanner.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
database:
|
|
73
|
+
Fully-qualified database path,
|
|
74
|
+
``projects/<p>/instances/<i>/databases/<d>``.
|
|
75
|
+
endpoint:
|
|
76
|
+
Override the Spanner gRPC endpoint (e.g. an emulator ``host:port``).
|
|
77
|
+
emulator:
|
|
78
|
+
Use anonymous credentials and talk to the emulator. When
|
|
79
|
+
``SPANNER_EMULATOR_HOST`` is set the driver detects the emulator on its
|
|
80
|
+
own, so this is only needed to force it explicitly.
|
|
81
|
+
keyfile / keyfile_json:
|
|
82
|
+
Service-account credentials, as a path or inline JSON. Omit both to use
|
|
83
|
+
Application Default Credentials.
|
|
84
|
+
db_kwargs:
|
|
85
|
+
Escape hatch for raw ``adbc.spanner.*`` option keys, merged last.
|
|
86
|
+
|
|
87
|
+
For a DBAPI 2.0 connection, prefer :func:`adbc_driver_spanner.dbapi.connect`.
|
|
88
|
+
"""
|
|
89
|
+
options = option_kwargs(
|
|
90
|
+
database,
|
|
91
|
+
endpoint=endpoint,
|
|
92
|
+
emulator=emulator,
|
|
93
|
+
keyfile=keyfile,
|
|
94
|
+
keyfile_json=keyfile_json,
|
|
95
|
+
db_kwargs=db_kwargs,
|
|
96
|
+
)
|
|
97
|
+
# ** unpacking accepts the dotted, non-identifier option keys; they land in
|
|
98
|
+
# AdbcDatabase's **kwargs and are forwarded as ADBC options.
|
|
99
|
+
return adbc_driver_manager.AdbcDatabase(
|
|
100
|
+
driver=_driver_path(), entrypoint=ENTRYPOINT, **options
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@functools.cache
|
|
105
|
+
def _driver_path() -> str:
|
|
106
|
+
"""Absolute path to the shared library bundled in this wheel."""
|
|
107
|
+
here = pathlib.Path(__file__).resolve().parent
|
|
108
|
+
for name in ("libadbc_spanner.so", "libadbc_spanner.dylib", "adbc_spanner.dll"):
|
|
109
|
+
candidate = here / name
|
|
110
|
+
if candidate.is_file():
|
|
111
|
+
return str(candidate)
|
|
112
|
+
raise RuntimeError(
|
|
113
|
+
"adbc_driver_spanner: no bundled Spanner driver library found next to "
|
|
114
|
+
f"{here}. This usually means a source/sdist install without a matching "
|
|
115
|
+
"platform wheel; install a prebuilt wheel for your platform instead."
|
|
116
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
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 adbc_driver_spanner.dbapi as spanner
|
|
8
|
+
with spanner.connect(database="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 ENTRYPOINT, _driver_path, option_kwargs
|
|
21
|
+
|
|
22
|
+
__all__ = ["connect"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def connect(
|
|
26
|
+
database: typing.Optional[str] = None,
|
|
27
|
+
*,
|
|
28
|
+
endpoint: typing.Optional[str] = None,
|
|
29
|
+
emulator: bool = False,
|
|
30
|
+
keyfile: typing.Optional[str] = None,
|
|
31
|
+
keyfile_json: typing.Optional[str] = None,
|
|
32
|
+
db_kwargs: typing.Optional[typing.Mapping[str, str]] = None,
|
|
33
|
+
conn_kwargs: typing.Optional[typing.Mapping[str, str]] = None,
|
|
34
|
+
autocommit: bool = False,
|
|
35
|
+
) -> adbc_driver_manager.dbapi.Connection:
|
|
36
|
+
"""Open a DBAPI 2.0 connection to a Spanner database.
|
|
37
|
+
|
|
38
|
+
Accepts the same connection parameters as
|
|
39
|
+
:func:`adbc_driver_spanner.connect`; ``conn_kwargs`` sets raw
|
|
40
|
+
``adbc.connection.*`` options and ``autocommit`` toggles PEP 249 autocommit.
|
|
41
|
+
"""
|
|
42
|
+
options = option_kwargs(
|
|
43
|
+
database,
|
|
44
|
+
endpoint=endpoint,
|
|
45
|
+
emulator=emulator,
|
|
46
|
+
keyfile=keyfile,
|
|
47
|
+
keyfile_json=keyfile_json,
|
|
48
|
+
db_kwargs=db_kwargs,
|
|
49
|
+
)
|
|
50
|
+
# The driver manager builds and owns the database/connection handles here and
|
|
51
|
+
# tears them down if the connection fails, so no manual cleanup is needed.
|
|
52
|
+
return adbc_driver_manager.dbapi.connect(
|
|
53
|
+
driver=_driver_path(),
|
|
54
|
+
entrypoint=ENTRYPOINT,
|
|
55
|
+
db_kwargs=options,
|
|
56
|
+
conn_kwargs=conn_kwargs,
|
|
57
|
+
autocommit=autocommit,
|
|
58
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adbc-driver-spanner
|
|
3
|
+
Version: 0.5.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/adbc-spanner
|
|
8
|
+
Project-URL: Repository, https://github.com/fornwall/adbc-spanner
|
|
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 :: Rust
|
|
14
|
+
Classifier: Topic :: Database
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: adbc-driver-manager
|
|
18
|
+
Provides-Extra: dbapi
|
|
19
|
+
Requires-Dist: pyarrow>=8; extra == "dbapi"
|
|
20
|
+
|
|
21
|
+
# adbc-driver-spanner
|
|
22
|
+
|
|
23
|
+
A Python [ADBC](https://arrow.apache.org/adbc/) driver for **Google Cloud Spanner**.
|
|
24
|
+
|
|
25
|
+
It bundles the prebuilt native driver (a Rust cdylib) and exposes it through
|
|
26
|
+
[`adbc_driver_manager`](https://pypi.org/project/adbc-driver-manager/), so you
|
|
27
|
+
get a standard DBAPI 2.0 connection whose results come back as Apache Arrow —
|
|
28
|
+
ready for pandas, polars, DuckDB, or PyArrow with no per-row conversion.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pip install adbc-driver-spanner
|
|
34
|
+
# for the DataFrame / Arrow helpers used below:
|
|
35
|
+
pip install adbc-driver-spanner[dbapi] pandas
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Prebuilt wheels are published for Linux (x86-64, aarch64), macOS (arm64,
|
|
39
|
+
x86-64), and Windows (x86-64, arm64).
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import adbc_driver_spanner.dbapi as spanner
|
|
45
|
+
|
|
46
|
+
with spanner.connect(
|
|
47
|
+
database="projects/my-project/instances/my-instance/databases/my-db",
|
|
48
|
+
) as conn:
|
|
49
|
+
with conn.cursor() as cur:
|
|
50
|
+
cur.execute("SELECT SingerId, FirstName FROM Singers")
|
|
51
|
+
df = cur.fetch_df() # -> pandas.DataFrame
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Connection options
|
|
55
|
+
|
|
56
|
+
Options mirror the driver's `adbc.spanner.*` keys:
|
|
57
|
+
|
|
58
|
+
| kwarg | driver option |
|
|
59
|
+
| -------------- | --------------------------- |
|
|
60
|
+
| `database=` | `adbc.spanner.database` |
|
|
61
|
+
| `endpoint=` | `adbc.spanner.endpoint` |
|
|
62
|
+
| `emulator=` | `adbc.spanner.emulator` |
|
|
63
|
+
| `keyfile=` | `adbc.spanner.keyfile` |
|
|
64
|
+
| `keyfile_json=`| `adbc.spanner.keyfile_json` |
|
|
65
|
+
|
|
66
|
+
Credentials default to Application Default Credentials; pass `keyfile=` /
|
|
67
|
+
`keyfile_json=` for a service account, or point at the emulator:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# docs-test: skip
|
|
71
|
+
spanner.connect(database="projects/p/instances/i/databases/d",
|
|
72
|
+
endpoint="localhost:9010", emulator=True)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Cookbook
|
|
76
|
+
|
|
77
|
+
Every snippet below is executed against the Spanner emulator in CI, so they stay
|
|
78
|
+
correct. They assume a `Singers(SingerId INT64, FirstName STRING)` table.
|
|
79
|
+
|
|
80
|
+
Two things to know:
|
|
81
|
+
|
|
82
|
+
- **DBAPI is autocommit-off by default**, so **DML and ingest need a
|
|
83
|
+
`conn.commit()`** (or pass `autocommit=True`). Reads need neither.
|
|
84
|
+
- The DataFrame / Arrow paths need the `[dbapi]` extra (pyarrow).
|
|
85
|
+
|
|
86
|
+
**pyarrow — zero-copy Arrow:**
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import adbc_driver_spanner.dbapi as spanner
|
|
90
|
+
|
|
91
|
+
with spanner.connect(
|
|
92
|
+
database="projects/my-project/instances/my-instance/databases/my-db",
|
|
93
|
+
) as conn:
|
|
94
|
+
with conn.cursor() as cur:
|
|
95
|
+
cur.execute("SELECT SingerId, FirstName FROM Singers ORDER BY SingerId")
|
|
96
|
+
table = cur.fetch_arrow_table() # -> pyarrow.Table
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**polars — read straight from the connection:**
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import polars as pl
|
|
103
|
+
import adbc_driver_spanner.dbapi as spanner
|
|
104
|
+
|
|
105
|
+
with spanner.connect(
|
|
106
|
+
database="projects/my-project/instances/my-instance/databases/my-db",
|
|
107
|
+
) as conn:
|
|
108
|
+
df = pl.read_database(
|
|
109
|
+
"SELECT SingerId, FirstName FROM Singers ORDER BY SingerId",
|
|
110
|
+
connection=conn, # an ADBC connection, not a URI
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**DuckDB — query the fetched Arrow table in-process:**
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import duckdb
|
|
118
|
+
import adbc_driver_spanner.dbapi as spanner
|
|
119
|
+
|
|
120
|
+
with spanner.connect(
|
|
121
|
+
database="projects/my-project/instances/my-instance/databases/my-db",
|
|
122
|
+
) as conn:
|
|
123
|
+
with conn.cursor() as cur:
|
|
124
|
+
cur.execute("SELECT SingerId, FirstName FROM Singers")
|
|
125
|
+
singers = cur.fetch_arrow_table()
|
|
126
|
+
|
|
127
|
+
# `singers` is a pyarrow.Table; DuckDB queries it by variable name, no copy.
|
|
128
|
+
top = duckdb.sql("SELECT COUNT(*) AS n, MIN(FirstName) AS first FROM singers").fetchone()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Insert a DataFrame (bulk ingest):**
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import pandas as pd
|
|
135
|
+
import pyarrow as pa
|
|
136
|
+
import adbc_driver_spanner.dbapi as spanner
|
|
137
|
+
|
|
138
|
+
frame = pd.DataFrame({"SingerId": [10, 11], "FirstName": ["Carol", "Dave"]})
|
|
139
|
+
|
|
140
|
+
with spanner.connect(
|
|
141
|
+
database="projects/my-project/instances/my-instance/databases/my-db",
|
|
142
|
+
autocommit=True, # apply immediately; returns the row count
|
|
143
|
+
) as conn:
|
|
144
|
+
with conn.cursor() as cur:
|
|
145
|
+
# The target table must already exist — only append mode is supported.
|
|
146
|
+
rows = cur.adbc_ingest("Singers", pa.Table.from_pandas(frame), mode="append")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Partitioned reads and Data Boost
|
|
150
|
+
|
|
151
|
+
A large scan can be split into independent partitions and read in parallel —
|
|
152
|
+
optionally on Spanner's serverless [Data Boost] compute, so the work is isolated
|
|
153
|
+
from your provisioned instance. This uses the ADBC partitioned-execution
|
|
154
|
+
extension (`adbc_execute_partitions` / `adbc_read_partition`):
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import adbc_driver_spanner.dbapi as spanner
|
|
158
|
+
|
|
159
|
+
with spanner.connect(
|
|
160
|
+
database="projects/my-project/instances/my-instance/databases/my-db",
|
|
161
|
+
) as conn:
|
|
162
|
+
with conn.cursor() as cur:
|
|
163
|
+
# Optional statement options, set on the underlying ADBC statement:
|
|
164
|
+
cur.adbc_statement.set_options(**{
|
|
165
|
+
"adbc.spanner.data_boost_enabled": "true", # run on Data Boost
|
|
166
|
+
"adbc.spanner.max_partitions": "8", # cap the partition count
|
|
167
|
+
})
|
|
168
|
+
partitions, schema = cur.adbc_execute_partitions("SELECT SingerId FROM Singers")
|
|
169
|
+
|
|
170
|
+
# Each descriptor is opaque bytes; it can be shipped to another worker,
|
|
171
|
+
# process, or connection and read independently.
|
|
172
|
+
for token in partitions:
|
|
173
|
+
with conn.cursor() as cur:
|
|
174
|
+
cur.adbc_read_partition(token)
|
|
175
|
+
table = cur.fetch_arrow_table()
|
|
176
|
+
...
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The Data Boost choice is baked into each descriptor, so it is honoured wherever
|
|
180
|
+
the partition is read. Only single-table scans are partitionable — queries with
|
|
181
|
+
an `ORDER BY` or aggregation are not.
|
|
182
|
+
|
|
183
|
+
[Data Boost]: https://cloud.google.com/spanner/docs/databoost/databoost-overview
|
|
184
|
+
|
|
185
|
+
## How this package is built
|
|
186
|
+
|
|
187
|
+
The wheel is **data-only**: it does not compile anything at install time and
|
|
188
|
+
links nothing against Python. CI (`.github/workflows/libraries.yml`) builds the
|
|
189
|
+
native library per platform, drops it into `adbc_driver_spanner/`, and packages
|
|
190
|
+
a `py3-none-<platform>` wheel. See that workflow for the release wiring.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
adbc_driver_spanner/__init__.py,sha256=2zV0i1fbiJyGuuYAAKTG504WIGZJ-wKPT9xdEtMb4rk,4209
|
|
2
|
+
adbc_driver_spanner/_version.py,sha256=LBK46heutvn3KmsCrKIYu8RQikbfnjZaj2xFrXaeCzQ,22
|
|
3
|
+
adbc_driver_spanner/adbc_spanner.dll,sha256=XGFb7Vll8Vtf2-73GmKUp3uS6Zczik9yW59SVY6lQMs,18796032
|
|
4
|
+
adbc_driver_spanner/dbapi.py,sha256=obq1KQu9EI727oWonG39VtEQ1sv6sNezGtReOVafvOA,2104
|
|
5
|
+
adbc_driver_spanner/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
adbc_driver_spanner-0.5.0.dist-info/METADATA,sha256=lZnZC_9gz__KRyJykOdXsSXmVg_NHhv28q-bIcuQIFA,6668
|
|
7
|
+
adbc_driver_spanner-0.5.0.dist-info/WHEEL,sha256=hVx9elvUDfBjRmbl8JwIcXXikst35RZTZK9nspfI_28,98
|
|
8
|
+
adbc_driver_spanner-0.5.0.dist-info/top_level.txt,sha256=OQpyk3MKXSGZElB1wpO5_JMeFvzz3zAHYza6ahRQmb4,20
|
|
9
|
+
adbc_driver_spanner-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adbc_driver_spanner
|