Skip to content

Commit 78aa052

Browse files
committed
feat: Implement initial Google Cloud Spanner DB-API 2.0 driver with core components and comprehensive unit and system tests.
1 parent a45d741 commit 78aa052

17 files changed

Lines changed: 2376 additions & 3 deletions

File tree

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

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,70 @@
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+
)
2048

21-
__version__ = package_version.__version__
49+
__version__: Final[str] = "0.0.1"
2250

2351
logger = logging.getLogger(__name__)
2452
logger.addHandler(logging.NullHandler())
2553

2654
__all__: list[str] = [
2755
"apilevel",
28-
"paramstyle",
2956
"threadsafety",
57+
"paramstyle",
58+
"Connection",
59+
"connect",
60+
"Cursor",
61+
"Date",
62+
"Time",
63+
"Timestamp",
64+
"DateFromTicks",
65+
"TimeFromTicks",
66+
"TimestampFromTicks",
67+
"Binary",
68+
"STRING",
69+
"BINARY",
70+
"NUMBER",
71+
"DATETIME",
72+
"ROWID",
73+
"InterfaceError",
74+
"ProgrammingError",
75+
"OperationalError",
76+
"DatabaseError",
77+
"DataError",
78+
"NotSupportedError",
79+
"IntegrityError",
80+
"InternalError",
81+
"Warning",
82+
"Error",
3083
]
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
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+
# raise errors.map_spanner_error(e)
93+
logger.debug(f"Commit failed {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+
# raise errors.map_spanner_error(e)
106+
logger.debug(f"Rollback failed {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, **kwargs: Any) -> 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)