Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bfde221

Browse files
author
Gurov Ilya
authored
feat: refactor connect() function, cover it with unit tests (#462)
1 parent 5551b58 commit bfde221

File tree

5 files changed

+253
-113
lines changed

5 files changed

+253
-113
lines changed

django_spanner/base.py

Lines changed: 59 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,79 +18,81 @@
1818

1919

2020
class DatabaseWrapper(BaseDatabaseWrapper):
21-
vendor = 'spanner'
22-
display_name = 'Cloud Spanner'
21+
vendor = "spanner"
22+
display_name = "Cloud Spanner"
2323

2424
# Mapping of Field objects to their column types.
2525
# https://cloud.google.com/spanner/docs/data-types#date-type
2626
data_types = {
27-
'AutoField': 'INT64',
28-
'BigAutoField': 'INT64',
29-
'BinaryField': 'BYTES(MAX)',
30-
'BooleanField': 'BOOL',
31-
'CharField': 'STRING(%(max_length)s)',
32-
'DateField': 'DATE',
33-
'DateTimeField': 'TIMESTAMP',
34-
'DecimalField': 'FLOAT64',
35-
'DurationField': 'INT64',
36-
'EmailField': 'STRING(%(max_length)s)',
37-
'FileField': 'STRING(%(max_length)s)',
38-
'FilePathField': 'STRING(%(max_length)s)',
39-
'FloatField': 'FLOAT64',
40-
'IntegerField': 'INT64',
41-
'BigIntegerField': 'INT64',
42-
'IPAddressField': 'STRING(15)',
43-
'GenericIPAddressField': 'STRING(39)',
44-
'NullBooleanField': 'BOOL',
45-
'OneToOneField': 'INT64',
46-
'PositiveIntegerField': 'INT64',
47-
'PositiveSmallIntegerField': 'INT64',
48-
'SlugField': 'STRING(%(max_length)s)',
49-
'SmallAutoField': 'INT64',
50-
'SmallIntegerField': 'INT64',
51-
'TextField': 'STRING(MAX)',
52-
'TimeField': 'TIMESTAMP',
53-
'UUIDField': 'STRING(32)',
27+
"AutoField": "INT64",
28+
"BigAutoField": "INT64",
29+
"BinaryField": "BYTES(MAX)",
30+
"BooleanField": "BOOL",
31+
"CharField": "STRING(%(max_length)s)",
32+
"DateField": "DATE",
33+
"DateTimeField": "TIMESTAMP",
34+
"DecimalField": "FLOAT64",
35+
"DurationField": "INT64",
36+
"EmailField": "STRING(%(max_length)s)",
37+
"FileField": "STRING(%(max_length)s)",
38+
"FilePathField": "STRING(%(max_length)s)",
39+
"FloatField": "FLOAT64",
40+
"IntegerField": "INT64",
41+
"BigIntegerField": "INT64",
42+
"IPAddressField": "STRING(15)",
43+
"GenericIPAddressField": "STRING(39)",
44+
"NullBooleanField": "BOOL",
45+
"OneToOneField": "INT64",
46+
"PositiveIntegerField": "INT64",
47+
"PositiveSmallIntegerField": "INT64",
48+
"SlugField": "STRING(%(max_length)s)",
49+
"SmallAutoField": "INT64",
50+
"SmallIntegerField": "INT64",
51+
"TextField": "STRING(MAX)",
52+
"TimeField": "TIMESTAMP",
53+
"UUIDField": "STRING(32)",
5454
}
5555
operators = {
56-
'exact': '= %s',
57-
'iexact': 'REGEXP_CONTAINS(%s, %%%%s)',
56+
"exact": "= %s",
57+
"iexact": "REGEXP_CONTAINS(%s, %%%%s)",
5858
# contains uses REGEXP_CONTAINS instead of LIKE to allow
5959
# DatabaseOperations.prep_for_like_query() to do regular expression
6060
# escaping. prep_for_like_query() is called for all the lookups that
6161
# use REGEXP_CONTAINS except regex/iregex (see
6262
# django.db.models.lookups.PatternLookup).
63-
'contains': 'REGEXP_CONTAINS(%s, %%%%s)',
64-
'icontains': 'REGEXP_CONTAINS(%s, %%%%s)',
65-
'gt': '> %s',
66-
'gte': '>= %s',
67-
'lt': '< %s',
68-
'lte': '<= %s',
63+
"contains": "REGEXP_CONTAINS(%s, %%%%s)",
64+
"icontains": "REGEXP_CONTAINS(%s, %%%%s)",
65+
"gt": "> %s",
66+
"gte": ">= %s",
67+
"lt": "< %s",
68+
"lte": "<= %s",
6969
# Using REGEXP_CONTAINS instead of STARTS_WITH and ENDS_WITH for the
7070
# same reasoning as described above for 'contains'.
71-
'startswith': 'REGEXP_CONTAINS(%s, %%%%s)',
72-
'endswith': 'REGEXP_CONTAINS(%s, %%%%s)',
73-
'istartswith': 'REGEXP_CONTAINS(%s, %%%%s)',
74-
'iendswith': 'REGEXP_CONTAINS(%s, %%%%s)',
75-
'regex': 'REGEXP_CONTAINS(%s, %%%%s)',
76-
'iregex': 'REGEXP_CONTAINS(%s, %%%%s)',
71+
"startswith": "REGEXP_CONTAINS(%s, %%%%s)",
72+
"endswith": "REGEXP_CONTAINS(%s, %%%%s)",
73+
"istartswith": "REGEXP_CONTAINS(%s, %%%%s)",
74+
"iendswith": "REGEXP_CONTAINS(%s, %%%%s)",
75+
"regex": "REGEXP_CONTAINS(%s, %%%%s)",
76+
"iregex": "REGEXP_CONTAINS(%s, %%%%s)",
7777
}
7878

7979
# pattern_esc is used to generate SQL pattern lookup clauses when the
8080
# right-hand side of the lookup isn't a raw string (it might be an
8181
# expression or the result of a bilateral transformation). In those cases,
8282
# special characters for REGEXP_CONTAINS operators (e.g. \, *, _) must be
8383
# escaped on database side.
84-
pattern_esc = r'REPLACE(REPLACE(REPLACE({}, "\\", "\\\\"), "%%", r"\%%"), "_", r"\_")'
84+
pattern_esc = (
85+
r'REPLACE(REPLACE(REPLACE({}, "\\", "\\\\"), "%%", r"\%%"), "_", r"\_")'
86+
)
8587
# These are all no-ops in favor of using REGEXP_CONTAINS in the customized
8688
# lookups.
8789
pattern_ops = {
88-
'contains': '',
89-
'icontains': '',
90-
'startswith': '',
91-
'istartswith': '',
92-
'endswith': '',
93-
'iendswith': '',
90+
"contains": "",
91+
"icontains": "",
92+
"startswith": "",
93+
"istartswith": "",
94+
"endswith": "",
95+
"iendswith": "",
9496
}
9597

9698
Database = Database
@@ -104,19 +106,19 @@ class DatabaseWrapper(BaseDatabaseWrapper):
104106

105107
@property
106108
def instance(self):
107-
return spanner.Client().instance(self.settings_dict['INSTANCE'])
109+
return spanner.Client().instance(self.settings_dict["INSTANCE"])
108110

109111
@property
110112
def _nodb_connection(self):
111113
raise NotImplementedError('Spanner does not have a "no db" connection.')
112114

113115
def get_connection_params(self):
114116
return {
115-
'project': self.settings_dict['PROJECT'],
116-
'instance': self.settings_dict['INSTANCE'],
117-
'database': self.settings_dict['NAME'],
118-
'user_agent': 'django_spanner/0.0.1',
119-
**self.settings_dict['OPTIONS'],
117+
"project": self.settings_dict["PROJECT"],
118+
"instance_id": self.settings_dict["INSTANCE"],
119+
"database_id": self.settings_dict["NAME"],
120+
"user_agent": "django_spanner/0.0.1",
121+
**self.settings_dict["OPTIONS"],
120122
}
121123

122124
def get_new_connection(self, conn_params):
@@ -137,7 +139,7 @@ def is_usable(self):
137139
return False
138140
try:
139141
# Use a cursor directly, bypassing Django's utilities.
140-
self.connection.cursor().execute('SELECT 1')
142+
self.connection.cursor().execute("SELECT 1")
141143
except Database.Error:
142144
return False
143145
else:

setup.cfg

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
max-line-length = 119
33

44
[isort]
5+
use_parentheses=True
56
combine_as_imports = true
67
default_section = THIRDPARTY
78
include_trailing_comma = true
9+
force_grid_wrap=0
810
line_length = 79
9-
multi_line_output = 5
11+
multi_line_output = 3

spanner_dbapi/__init__.py

Lines changed: 94 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,83 +4,122 @@
44
# license that can be found in the LICENSE file or at
55
# https://developers.google.com/open-source/licenses/bsd
66

7-
from google.cloud import spanner_v1 as spanner
7+
"""Connection-based DB API for Cloud Spanner."""
8+
9+
from google.cloud import spanner_v1
810

911
from .connection import Connection
10-
# These need to be included in the top-level package for PEP-0249 DB API v2.
1112
from .exceptions import (
12-
DatabaseError, DataError, Error, IntegrityError, InterfaceError,
13-
InternalError, NotSupportedError, OperationalError, ProgrammingError,
13+
DatabaseError,
14+
DataError,
15+
Error,
16+
IntegrityError,
17+
InterfaceError,
18+
InternalError,
19+
NotSupportedError,
20+
OperationalError,
21+
ProgrammingError,
1422
Warning,
1523
)
1624
from .parse_utils import get_param_types
1725
from .types import (
18-
BINARY, DATETIME, NUMBER, ROWID, STRING, Binary, Date, DateFromTicks, Time,
19-
TimeFromTicks, Timestamp, TimestampFromTicks,
26+
BINARY,
27+
DATETIME,
28+
NUMBER,
29+
ROWID,
30+
STRING,
31+
Binary,
32+
Date,
33+
DateFromTicks,
34+
Time,
35+
TimeFromTicks,
36+
Timestamp,
37+
TimestampFromTicks,
2038
)
2139
from .version import google_client_info
2240

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

3350

34-
def connect(project=None, instance=None, database=None, credentials_uri=None, user_agent=None):
51+
def connect(instance_id, database_id, project=None, credentials=None, user_agent=None):
3552
"""
36-
Connect to Cloud Spanner.
53+
Create a connection to Cloud Spanner database.
3754
38-
Args:
39-
project: The id of a project that already exists.
40-
instance: The id of an instance that already exists.
41-
database: The name of a database that already exists.
42-
credentials_uri: An optional string specifying where to retrieve the service
43-
account JSON for the credentials to connect to Cloud Spanner.
55+
:type instance_id: :class:`str`
56+
:param instance_id: ID of the instance to connect to.
4457
45-
Returns:
46-
The Connection object associated to the Cloud Spanner instance.
58+
:type database_id: :class:`str`
59+
:param database_id: The name of the database to connect to.
4760
48-
Raises:
49-
Error if it encounters any unexpected inputs.
50-
"""
51-
if not project:
52-
raise Error("'project' is required.")
53-
if not instance:
54-
raise Error("'instance' is required.")
55-
if not database:
56-
raise Error("'database' is required.")
61+
:type project: :class:`str`
62+
:param project: (Optional) The ID of the project which owns the
63+
instances, tables and data. If not provided, will
64+
attempt to determine from the environment.
5765
58-
client_kwargs = {
59-
'project': project,
60-
'client_info': google_client_info(user_agent),
61-
}
62-
if credentials_uri:
63-
client = spanner.Client.from_service_account_json(credentials_uri, **client_kwargs)
64-
else:
65-
client = spanner.Client(**client_kwargs)
66+
:type credentials: :class:`google.auth.credentials.Credentials`
67+
:param credentials: (Optional) The authorization credentials to attach to requests.
68+
These credentials identify this application to the service.
69+
If none are specified, the client will attempt to ascertain
70+
the credentials from the environment.
71+
72+
:rtype: :class:`google.cloud.spanner_dbapi.connection.Connection`
73+
:returns: Connection object associated with the given Cloud Spanner resource.
74+
75+
:raises: :class:`ValueError` in case of given instance/database
76+
doesn't exist.
77+
"""
78+
client = spanner_v1.Client(
79+
project=project,
80+
credentials=credentials,
81+
client_info=google_client_info(user_agent),
82+
)
6683

67-
client_instance = client.instance(instance)
68-
if not client_instance.exists():
69-
raise ProgrammingError("instance '%s' does not exist." % instance)
84+
instance = client.instance(instance_id)
85+
if not instance.exists():
86+
raise ValueError("instance '%s' does not exist." % instance_id)
7087

71-
db = client_instance.database(database, pool=spanner.pool.BurstyPool())
72-
if not db.exists():
73-
raise ProgrammingError("database '%s' does not exist." % database)
88+
database = instance.database(database_id, pool=spanner_v1.pool.BurstyPool())
89+
if not database.exists():
90+
raise ValueError("database '%s' does not exist." % database_id)
7491

75-
return Connection(db)
92+
return Connection(database)
7693

7794

7895
__all__ = [
79-
'DatabaseError', 'DataError', 'Error', 'IntegrityError', 'InterfaceError',
80-
'InternalError', 'NotSupportedError', 'OperationalError', 'ProgrammingError',
81-
'Warning', 'DEFAULT_USER_AGENT', 'apilevel', 'connect', 'paramstyle', 'threadsafety',
82-
'get_param_types',
83-
'Binary', 'Date', 'DateFromTicks', 'Time', 'TimeFromTicks', 'Timestamp',
84-
'TimestampFromTicks',
85-
'BINARY', 'STRING', 'NUMBER', 'DATETIME', 'ROWID', 'TimestampStr',
96+
"DatabaseError",
97+
"DataError",
98+
"Error",
99+
"IntegrityError",
100+
"InterfaceError",
101+
"InternalError",
102+
"NotSupportedError",
103+
"OperationalError",
104+
"ProgrammingError",
105+
"Warning",
106+
"DEFAULT_USER_AGENT",
107+
"apilevel",
108+
"connect",
109+
"paramstyle",
110+
"threadsafety",
111+
"get_param_types",
112+
"Binary",
113+
"Date",
114+
"DateFromTicks",
115+
"Time",
116+
"TimeFromTicks",
117+
"Timestamp",
118+
"TimestampFromTicks",
119+
"BINARY",
120+
"STRING",
121+
"NUMBER",
122+
"DATETIME",
123+
"ROWID",
124+
"TimestampStr",
86125
]

0 commit comments

Comments
 (0)