Skip to content

Commit a316406

Browse files
feat: Implement initial Google Cloud Spanner DB-API 2.0 driver (#16157)
feat: Implement initial Google Cloud Spanner DB-API 2.0 driver with core components and comprehensive unit and system tests. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/google-cloud-python/issues) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) Fixes #16120 🦕 --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent ef856b0 commit a316406

File tree

17 files changed

+2380
-2
lines changed

17 files changed

+2380
-2
lines changed

packages/google-cloud-spanner-dbapi-driver/google/cloud/spanner_driver/__init__.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,71 @@
1414
"""Spanner Python Driver."""
1515

1616
import logging
17+
from typing import Final
1718

18-
from . import version as package_version
19+
from .connection import Connection, connect
20+
from .cursor import Cursor
1921
from .dbapi import apilevel, paramstyle, threadsafety
22+
from .errors import (
23+
DatabaseError,
24+
DataError,
25+
Error,
26+
IntegrityError,
27+
InterfaceError,
28+
InternalError,
29+
NotSupportedError,
30+
OperationalError,
31+
ProgrammingError,
32+
Warning,
33+
)
34+
from .types import (
35+
BINARY,
36+
DATETIME,
37+
NUMBER,
38+
ROWID,
39+
STRING,
40+
Binary,
41+
Date,
42+
DateFromTicks,
43+
Time,
44+
TimeFromTicks,
45+
Timestamp,
46+
TimestampFromTicks,
47+
)
48+
from .version import __version__ as _version
2049

21-
__version__ = package_version.__version__
50+
__version__: Final[str] = _version
2251

2352
logger = logging.getLogger(__name__)
2453
logger.addHandler(logging.NullHandler())
2554

2655
__all__: list[str] = [
56+
"BINARY",
57+
"Binary",
58+
"Connection",
59+
"Cursor",
60+
"DATETIME",
61+
"DataError",
62+
"DatabaseError",
63+
"Date",
64+
"DateFromTicks",
65+
"Error",
66+
"IntegrityError",
67+
"InterfaceError",
68+
"InternalError",
69+
"NUMBER",
70+
"NotSupportedError",
71+
"OperationalError",
72+
"ProgrammingError",
73+
"ROWID",
74+
"STRING",
75+
"Time",
76+
"TimeFromTicks",
77+
"Timestamp",
78+
"TimestampFromTicks",
79+
"Warning",
2780
"apilevel",
81+
"connect",
2882
"paramstyle",
2983
"threadsafety",
3084
]
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import logging
15+
from typing import Any
16+
17+
from google.cloud.spannerlib.pool import Pool # type: ignore[import-untyped]
18+
19+
from . import errors
20+
from .cursor import Cursor
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
def check_not_closed(function):
26+
"""`Connection` class methods decorator.
27+
28+
Raise an exception if the connection is closed.
29+
30+
:raises: :class:`InterfaceError` if the connection is closed.
31+
"""
32+
33+
def wrapper(connection, *args, **kwargs):
34+
if connection._closed:
35+
raise errors.InterfaceError("Connection is closed")
36+
37+
return function(connection, *args, **kwargs)
38+
39+
return wrapper
40+
41+
42+
class Connection:
43+
"""Connection to a Google Cloud Spanner database.
44+
45+
This class provides a connection to the Spanner database and adheres to
46+
PEP 249 (Python Database API Specification v2.0).
47+
"""
48+
49+
def __init__(self, internal_connection: Any):
50+
"""
51+
Args:
52+
internal_connection: An instance of
53+
google.cloud.spannerlib.Connection
54+
"""
55+
self._internal_conn = internal_connection
56+
self._closed = False
57+
self._messages: list[Any] = []
58+
59+
@property
60+
def messages(self) -> list[Any]:
61+
"""Return the list of messages sent to the client by the database."""
62+
return self._messages
63+
64+
@check_not_closed
65+
def cursor(self) -> Cursor:
66+
"""Return a new Cursor Object using the connection.
67+
68+
Returns:
69+
Cursor: A cursor object.
70+
"""
71+
return Cursor(self)
72+
73+
@check_not_closed
74+
def begin(self) -> None:
75+
"""Begin a new transaction."""
76+
logger.debug("Beginning transaction")
77+
try:
78+
self._internal_conn.begin_transaction()
79+
except Exception as e:
80+
raise errors.map_spanner_error(e)
81+
82+
@check_not_closed
83+
def commit(self) -> None:
84+
"""Commit any pending transaction to the database.
85+
86+
This is a no-op if there is no active client transaction.
87+
"""
88+
logger.debug("Committing transaction")
89+
try:
90+
self._internal_conn.commit()
91+
except Exception as e:
92+
logger.debug(f"Commit failed {e}")
93+
raise errors.map_spanner_error(e)
94+
95+
@check_not_closed
96+
def rollback(self) -> None:
97+
"""Rollback any pending transaction to the database.
98+
99+
This is a no-op if there is no active client transaction.
100+
"""
101+
logger.debug("Rolling back transaction")
102+
try:
103+
self._internal_conn.rollback()
104+
except Exception as e:
105+
logger.debug(f"Rollback failed {e}")
106+
raise errors.map_spanner_error(e)
107+
108+
def close(self) -> None:
109+
"""Close the connection now.
110+
111+
The connection will be unusable from this point forward; an Error (or
112+
subclass) exception will be raised if any operation is attempted with
113+
the connection. The same applies to all cursor objects trying to use
114+
the connection.
115+
"""
116+
if self._closed:
117+
raise errors.InterfaceError("Connection is already closed")
118+
119+
logger.debug("Closing connection")
120+
self._internal_conn.close()
121+
self._closed = True
122+
123+
def __enter__(self) -> "Connection":
124+
return self
125+
126+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
127+
self.close()
128+
129+
130+
def connect(connection_string: str) -> Connection:
131+
logger.debug(f"Connecting to {connection_string}")
132+
# Create the pool
133+
pool = Pool.create_pool(connection_string)
134+
135+
# Create the low-level connection
136+
internal_conn = pool.create_connection()
137+
138+
return Connection(internal_conn)

0 commit comments

Comments
 (0)