Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4fedcf1

Browse filesBrowse files
Gurov Ilyac24t
Gurov Ilya
authored andcommitted
feat: refactor connect() function, cover it with unit tests (#462)
1 parent 96f2223 commit 4fedcf1
Copy full SHA for 4fedcf1

File tree

Expand file treeCollapse file tree

5 files changed

+253
-113
lines changed
Filter options
Expand file treeCollapse file tree

5 files changed

+253
-113
lines changed

‎django_spanner/base.py

Copy file name to clipboardExpand all lines: django_spanner/base.py
+59-57Lines 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

Copy file name to clipboardExpand all lines: setup.cfg
+3-1Lines 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

Copy file name to clipboardExpand all lines: spanner_dbapi/__init__.py
+94-55Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.