Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor connect() function, cover it with unit tests #462

Merged
merged 9 commits into from Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion setup.cfg
Expand Up @@ -2,8 +2,10 @@
max-line-length = 119

[isort]
use_parentheses=True
combine_as_imports = true
default_section = THIRDPARTY
include_trailing_comma = true
force_grid_wrap=0
line_length = 79
multi_line_output = 5
multi_line_output = 3
149 changes: 94 additions & 55 deletions spanner_dbapi/__init__.py
Expand Up @@ -4,83 +4,122 @@
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

from google.cloud import spanner_v1 as spanner
"""Connection-based DB API for Cloud Spanner."""

from google.cloud import spanner_v1
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved

from .connection import Connection
# These need to be included in the top-level package for PEP-0249 DB API v2.
from .exceptions import (
DatabaseError, DataError, Error, IntegrityError, InterfaceError,
InternalError, NotSupportedError, OperationalError, ProgrammingError,
DatabaseError,
DataError,
Error,
IntegrityError,
InterfaceError,
InternalError,
NotSupportedError,
OperationalError,
ProgrammingError,
Warning,
)
from .parse_utils import get_param_types
from .types import (
BINARY, DATETIME, NUMBER, ROWID, STRING, Binary, Date, DateFromTicks, Time,
TimeFromTicks, Timestamp, TimestampFromTicks,
BINARY,
DATETIME,
NUMBER,
ROWID,
STRING,
Binary,
Date,
DateFromTicks,
Time,
TimeFromTicks,
Timestamp,
TimestampFromTicks,
)
from .version import google_client_info

# Globals that MUST be defined ###
apilevel = "2.0" # Implements the Python Database API specification 2.0 version.
# We accept arguments in the format '%s' aka ANSI C print codes.
# as per https://www.python.org/dev/peps/pep-0249/#paramstyle
paramstyle = 'format'
# Threads may share the module but not connections. This is a paranoid threadsafety level,
# but it is necessary for starters to use when debugging failures. Eventually once transactions
# are working properly, we'll update the threadsafety level.
apilevel = "2.0" # supports DP-API 2.0 level.
paramstyle = "format" # ANSI C printf format codes, e.g. ...WHERE name=%s.

# Threads may share the module, but not connections. This is a paranoid threadsafety
# level, but it is necessary for starters to use when debugging failures.
# Eventually once transactions are working properly, we'll update the
# threadsafety level.
threadsafety = 1


def connect(project=None, instance=None, database=None, credentials_uri=None, user_agent=None):
def connect(instance_id, database_id, project=None, credentials=None, user_agent=None):
"""
Connect to Cloud Spanner.
Create a connection to Cloud Spanner database.

Args:
project: The id of a project that already exists.
instance: The id of an instance that already exists.
database: The name of a database that already exists.
credentials_uri: An optional string specifying where to retrieve the service
account JSON for the credentials to connect to Cloud Spanner.
:type instance_id: :class:`str`
:param instance_id: ID of the instance to connect to.
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved

Returns:
The Connection object associated to the Cloud Spanner instance.
:type database_id: :class:`str`
:param database_id: The name of the database to connect to.

Raises:
Error if it encounters any unexpected inputs.
"""
if not project:
raise Error("'project' is required.")
if not instance:
raise Error("'instance' is required.")
if not database:
raise Error("'database' is required.")
:type project: :class:`str`
:param project: (Optional) The ID of the project which owns the
instances, tables and data. If not provided, will
attempt to determine from the environment.

client_kwargs = {
'project': project,
'client_info': google_client_info(user_agent),
}
if credentials_uri:
client = spanner.Client.from_service_account_json(credentials_uri, **client_kwargs)
else:
client = spanner.Client(**client_kwargs)
:type credentials: :class:`google.auth.credentials.Credentials`
:param credentials: (Optional) The authorization credentials to attach to requests.
These credentials identify this application to the service.
If none are specified, the client will attempt to ascertain
the credentials from the environment.

:rtype: :class:`google.cloud.spanner_dbapi.connection.Connection`
:returns: Connection object associated with the given Cloud Spanner resource.

:raises: :class:`ProgrammingError` in case of given instance/database
doesn't exist.
"""
client = spanner_v1.Client(
project=project,
credentials=credentials,
client_info=google_client_info(user_agent),
)

client_instance = client.instance(instance)
if not client_instance.exists():
raise ProgrammingError("instance '%s' does not exist." % instance)
instance = client.instance(instance_id)
if not instance.exists():
raise ProgrammingError("instance '%s' does not exist." % instance_id)
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved

db = client_instance.database(database, pool=spanner.pool.BurstyPool())
if not db.exists():
raise ProgrammingError("database '%s' does not exist." % database)
database = instance.database(database_id, pool=spanner_v1.pool.BurstyPool())
if not database.exists():
raise ProgrammingError("database '%s' does not exist." % database_id)

return Connection(db)
return Connection(database)


__all__ = [
'DatabaseError', 'DataError', 'Error', 'IntegrityError', 'InterfaceError',
'InternalError', 'NotSupportedError', 'OperationalError', 'ProgrammingError',
'Warning', 'DEFAULT_USER_AGENT', 'apilevel', 'connect', 'paramstyle', 'threadsafety',
'get_param_types',
'Binary', 'Date', 'DateFromTicks', 'Time', 'TimeFromTicks', 'Timestamp',
'TimestampFromTicks',
'BINARY', 'STRING', 'NUMBER', 'DATETIME', 'ROWID', 'TimestampStr',
"DatabaseError",
"DataError",
"Error",
"IntegrityError",
"InterfaceError",
"InternalError",
"NotSupportedError",
"OperationalError",
"ProgrammingError",
"Warning",
"DEFAULT_USER_AGENT",
"apilevel",
"connect",
"paramstyle",
"threadsafety",
"get_param_types",
"Binary",
"Date",
"DateFromTicks",
"Time",
"TimeFromTicks",
"Timestamp",
"TimestampFromTicks",
"BINARY",
"STRING",
"NUMBER",
"DATETIME",
"ROWID",
"TimestampStr",
]
115 changes: 115 additions & 0 deletions tests/spanner_dbapi/test_connect.py
@@ -0,0 +1,115 @@
# Copyright 2020 Google LLC
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

"""connect() module function unit tests."""

import unittest
from unittest import mock


def _make_credentials():
import google.auth.credentials

class _CredentialsWithScopes(
google.auth.credentials.Credentials, google.auth.credentials.Scoped
):
pass

return mock.Mock(spec=_CredentialsWithScopes)


class Testconnect(unittest.TestCase):
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved
def _callFUT(self, *args, **kw):
from spanner_dbapi import connect
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved

return connect(*args, **kw)

def test_connect(self):
from google.api_core.gapic_v1.client_info import ClientInfo
from spanner_dbapi.connection import Connection

PROJECT = "test-project"
USER_AGENT = "user-agent"
CREDENTIALS = _make_credentials()
CLIENT_INFO = ClientInfo(user_agent=USER_AGENT)
c24t marked this conversation as resolved.
Show resolved Hide resolved

with mock.patch("spanner_dbapi.spanner_v1.Client") as client_mock:
with mock.patch(
"spanner_dbapi.google_client_info", return_value=CLIENT_INFO
) as client_info_mock:

connection = self._callFUT(
"test-instance", "test-database", PROJECT, CREDENTIALS, USER_AGENT
)

self.assertIsInstance(connection, Connection)
client_info_mock.assert_called_once_with(USER_AGENT)

client_mock.assert_called_once_with(
project=PROJECT, credentials=CREDENTIALS, client_info=CLIENT_INFO
)

def test_instance_not_found(self):
from spanner_dbapi.exceptions import ProgrammingError

with mock.patch(
"google.cloud.spanner_v1.instance.Instance.exists", return_value=False
) as exists_mock:

with self.assertRaises(ProgrammingError):
self._callFUT("test-instance", "test-database")

exists_mock.assert_called_once()

def test_database_not_found(self):
from spanner_dbapi.exceptions import ProgrammingError

with mock.patch(
"google.cloud.spanner_v1.instance.Instance.exists", return_value=True
):
with mock.patch(
"google.cloud.spanner_v1.database.Database.exists", return_value=False
) as exists_mock:

with self.assertRaises(ProgrammingError):
self._callFUT("test-instance", "test-database")

exists_mock.assert_called_once()

def test_connect_instance_id(self):
from spanner_dbapi.connection import Connection

INSTANCE = "test-instance"

with mock.patch(
"google.cloud.spanner_v1.client.Client.instance"
) as instance_mock:
connection = self._callFUT(INSTANCE, "test-database")

instance_mock.assert_called_once_with(INSTANCE)

self.assertIsInstance(connection, Connection)

def test_connect_database_id(self):
from spanner_dbapi.connection import Connection

DATABASE = "test-database"

with mock.patch(
"google.cloud.spanner_v1.instance.Instance.database"
) as database_mock:
with mock.patch(
"google.cloud.spanner_v1.instance.Instance.exists", return_value=True
):
connection = self._callFUT("test-instance", DATABASE)

database_mock.assert_called_once_with(DATABASE, pool=mock.ANY)

self.assertIsInstance(connection, Connection)


if __name__ == "__main__":
unittest.main()
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion tox.ini
Expand Up @@ -21,4 +21,4 @@ deps =
isort
commands =
flake8
isort --recursive --check-only --diff
isort --recursive --check-only --diff .