Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,70 @@
"""Spanner Python Driver."""

import logging
from typing import Final

from . import version as package_version
from .connection import Connection, connect
from .cursor import Cursor
from .dbapi import apilevel, paramstyle, threadsafety
from .errors import (
DatabaseError,
DataError,
Error,
IntegrityError,
InterfaceError,
InternalError,
NotSupportedError,
OperationalError,
ProgrammingError,
Warning,
)
from .types import (
BINARY,
DATETIME,
NUMBER,
ROWID,
STRING,
Binary,
Date,
DateFromTicks,
Time,
TimeFromTicks,
Timestamp,
TimestampFromTicks,
)

__version__ = package_version.__version__
__version__: Final[str] = "0.0.1"

logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

__all__: list[str] = [
"BINARY",
"Binary",
"Connection",
"Cursor",
"DATETIME",
"DataError",
"DatabaseError",
"Date",
"DateFromTicks",
"Error",
"IntegrityError",
"InterfaceError",
"InternalError",
"NUMBER",
"NotSupportedError",
"OperationalError",
"ProgrammingError",
"ROWID",
"STRING",
"Time",
"TimeFromTicks",
"Timestamp",
"TimestampFromTicks",
"Warning",
"apilevel",
"connect",
"paramstyle",
"threadsafety",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from typing import Any

from google.cloud.spannerlib.pool import Pool

from . import errors
from .cursor import Cursor

logger = logging.getLogger(__name__)


def check_not_closed(function):
"""`Connection` class methods decorator.

Raise an exception if the connection is closed.

:raises: :class:`InterfaceError` if the connection is closed.
"""

def wrapper(connection, *args, **kwargs):
if connection._closed:
raise errors.InterfaceError("Connection is closed")

return function(connection, *args, **kwargs)

return wrapper


class Connection:
"""Connection to a Google Cloud Spanner database.

This class provides a connection to the Spanner database and adheres to
PEP 249 (Python Database API Specification v2.0).
"""

def __init__(self, internal_connection: Any):
"""
Args:
internal_connection: An instance of
google.cloud.spannerlib.Connection
"""
self._internal_conn = internal_connection
self._closed = False
self._messages: list[Any] = []

@property
def messages(self) -> list[Any]:
"""Return the list of messages sent to the client by the database."""
return self._messages

@check_not_closed
def cursor(self) -> Cursor:
"""Return a new Cursor Object using the connection.

Returns:
Cursor: A cursor object.
"""
return Cursor(self)

@check_not_closed
def begin(self) -> None:
"""Begin a new transaction."""
logger.debug("Beginning transaction")
try:
self._internal_conn.begin_transaction()
except Exception as e:
raise errors.map_spanner_error(e)

@check_not_closed
def commit(self) -> None:
"""Commit any pending transaction to the database.

This is a no-op if there is no active client transaction.
"""
logger.debug("Committing transaction")
try:
self._internal_conn.commit()
except Exception as e:
logger.debug(f"Commit failed {e}")
raise errors.map_spanner_error(e)

@check_not_closed
def rollback(self) -> None:
"""Rollback any pending transaction to the database.

This is a no-op if there is no active client transaction.
"""
logger.debug("Rolling back transaction")
try:
self._internal_conn.rollback()
except Exception as e:
logger.debug(f"Rollback failed {e}")
raise errors.map_spanner_error(e)

def close(self) -> None:
"""Close the connection now.

The connection will be unusable from this point forward; an Error (or
subclass) exception will be raised if any operation is attempted with
the connection. The same applies to all cursor objects trying to use
the connection.
"""
if self._closed:
raise errors.InterfaceError("Connection is already closed")

logger.debug("Closing connection")
self._internal_conn.close()
self._closed = True

def __enter__(self) -> "Connection":
return self

def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
self.close()


def connect(connection_string: str) -> Connection:
logger.debug(f"Connecting to {connection_string}")
# Create the pool
pool = Pool.create_pool(connection_string)

# Create the low-level connection
internal_conn = pool.create_connection()

return Connection(internal_conn)
Loading