Skip to content

Commit 73398e9

Browse files
committed
dbapi: update, document and test extract_connection_params
* Update README.md as well as the Python docs for `extract_connection_params` showing the format as well as examples * Added tests for each code path as well as parity tests for `SPANNER_URL` vs dict key=value pairs Fixes #27
1 parent f73e1fe commit 73398e9

File tree

3 files changed

+173
-26
lines changed

3 files changed

+173
-26
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,47 @@ description (Column(name='date', type_code=5), Column(name='isoyear', type_code=
8787
Time spent 1.6195518970489502
8888
```
8989

90+
### Django connection params
91+
92+
Settings should be a dict containing either:
93+
94+
a) 'SPANNER_URL' as the key and expecting a URL of the form:
95+
```
96+
cloudspanner:[//host[:port]]/project/<project_id>/instances/
97+
<instance-id>/databases/<database-name>?property-name=property-value
98+
```
99+
For example:
100+
```python
101+
DATABASE={
102+
'default': {
103+
"SPANNER_URL": "cloudspanner:/projects/appdev/instances/dev1/databases/db1?"
104+
"instance_config=projects/appdev/instanceConfigs/regional-us-west2"
105+
}
106+
}
107+
```
108+
109+
b) Otherwise expects parameters whose keys are capitalized and
110+
are of the form:
111+
```python
112+
{
113+
"NAME": "<database_name>",
114+
"INSTANCE": "<instance_name>",
115+
"AUTOCOMMIT": True or False,
116+
"READONLY": True or False,
117+
"PROJECT_ID": "<project_id>",
118+
"INSTANCE_CONFIG": "[instance configuration if using a brand new database]",
119+
}
120+
```
121+
122+
for example:
123+
124+
```python
125+
{
126+
"NAME": "db1",
127+
"INSTANCE": "dev1",
128+
"AUTOCOMMIT": True,
129+
"READONLY": False,
130+
"PROJECT_ID": "appdev",
131+
"INSTANCE_CONFIG": "projects/appdev/instanceConfigs/regional-us-west2",
132+
}
133+
```

spanner/dbapi/parse_utils.py

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def parse_spanner_url(spanner_url):
5656
for k, v in amap.items():
5757
combined[k] = v
5858

59-
return combined
59+
return filter_out_unset_keys(combined)
6060

6161

6262
def parse_properties(kv_pairs, sep=';'):
@@ -75,6 +75,22 @@ def parse_properties(kv_pairs, sep=';'):
7575
else:
7676
kvp[kvs[0]] = kvs[1:]
7777

78+
as_bools = ['autocommit', 'readonly']
79+
for as_bool_key in as_bools:
80+
value = kvp.get(as_bool_key, None)
81+
if value is None:
82+
continue
83+
84+
as_bool_value = True
85+
if value == '1' or value == '0':
86+
as_bool_value = value == '1'
87+
elif value == 'True' or value == 'true':
88+
as_bool_value = True
89+
elif value == 'False' or value == 'False':
90+
as_bool_value = False
91+
92+
kvp[as_bool_key] = as_bool_value
93+
7894
return kvp
7995

8096

@@ -102,31 +118,69 @@ def parse_projectid_instance_dbname(url_path):
102118

103119

104120
def extract_connection_params(settings_dict):
105-
# We'll expect settings in the form of either:
106-
# {
107-
# "NAME": "spanner",
108-
# "INSTANCE": "instance",
109-
# "AUTOCOMMIT": True or False,
110-
# "READ_ONLY": True or False,
111-
# "PROJECT_ID": "<project_id>",
112-
# }
113-
#
114-
# OR
115-
# {
116-
# "SPANNER_URL": "cloudspanner:[//host[:port]]/project/<project_id>/instances/
117-
# <instance-id>/databases/<database-name>?property-name=property-value
118-
# }
119-
if settings_dict['SPANNER_URL']:
120-
return parse_spanner_url(settings_dict['SPANNER_URL'])
121+
"""
122+
Examines settings_dict and depending on the provided
123+
keys will try to retrieve Cloud Spanner connection parameters.
124+
125+
Args:
126+
settings_dict: a dict containing either:
127+
a) 'SPANNER_URL' as the key and expecting a URL of the
128+
form:
129+
"cloudspanner:[//host[:port]]/project/<project_id>/
130+
instances/<instance-id>/databases/<database-name>?
131+
property-name=property-value"
132+
for example:
133+
{
134+
"SPANNER_URL": "cloudspanner:/projects/appdev/instances/dev1/databases/db1?"
135+
"instance_config=projects/appdev/instanceConfigs/regional-us-west2"
136+
}
137+
138+
b) Otherwise expects parameters whose keys are capitalized and
139+
are of the form:
140+
{
141+
"NAME": "<database_name>",
142+
"INSTANCE": "<instance_name>",
143+
"AUTOCOMMIT": True or False,
144+
"READONLY": True or False,
145+
"PROJECT_ID": "<project_id>",
146+
"INSTANCE_CONFIG": "[instance configuration if using a brand new database]",
147+
}
148+
149+
Returns:
150+
A dict of the form:
151+
{
152+
"autocommit": <True otherwise omitted if zero-value>,
153+
"database": <database name otherwise omitted if zero-value>,
154+
"instance": <instance name otherwise omitted if zero-value>,
155+
"instance_config": <instance configuration otherwise omitted if zero-value>,
156+
"project_id": <project_id otherwise omitted if zero-value>,
157+
"readonly": <True otherwise omitted if zero-value>
158+
}
159+
"""
160+
161+
spanner_url = settings_dict.get('SPANNER_URL', None)
162+
if spanner_url:
163+
return parse_spanner_url(spanner_url)
121164
else:
122-
return dict(
123-
auto_commit=settings_dict['AUTO_COMMIT'],
124-
database=settings_dict['NAME'] or 'spanner',
125-
instance=settings_dict['INSTANCE'],
126-
project_id=resolve_project_id(settings_dict['PROJECT_ID']),
127-
read_only=settings_dict['READ_ONLY'],
165+
all_unfiltered = dict(
166+
autocommit=settings_dict.get('AUTOCOMMIT'),
167+
database=settings_dict.get('NAME'),
168+
instance=settings_dict.get('INSTANCE'),
169+
instance_config=settings_dict.get('INSTANCE_CONFIG'),
170+
project_id=resolve_project_id(settings_dict.get('PROJECT_ID')),
171+
readonly=settings_dict.get('READONLY'),
128172
)
129173

174+
# Filter them to remove any unnecessary
175+
# None's whose keys have no associated value.
176+
return filter_out_unset_keys(all_unfiltered)
177+
178+
179+
def filter_out_unset_keys(unfiltered):
180+
# Filter them to remove any unnecessary
181+
# None's whose keys have no associated value.
182+
return {key: value for key, value in unfiltered.items() if value}
183+
130184

131185
reINSTANCE_CONFIG = re.compile('^projects/([^/]+)/instanceConfigs/([^/]+)$', re.UNICODE)
132186

tests/spanner/dbapi/test_parse_utils.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
from unittest import TestCase
1616

1717
from spanner.dbapi.exceptions import Error
18-
from spanner.dbapi.parse_utils import parse_spanner_url
18+
from spanner.dbapi.parse_utils import (
19+
extract_connection_params, parse_spanner_url,
20+
)
1921

2022

2123
class ParseUtilsTests(TestCase):
@@ -81,7 +83,54 @@ def test_with_host_with_properties(self):
8183
project_id='test-project-012345',
8284
instance='test-instance',
8385
database='dev-db',
84-
autocommit='true',
85-
readonly='true',
86+
autocommit=True,
87+
readonly=True,
8688
)
8789
self.assertEqual(got, want)
90+
91+
def test_extract_connection_params(self):
92+
cases = [
93+
(
94+
dict(
95+
INSTANCE='instance',
96+
INSTANCE_CONFIG='projects/proj/instanceConfigs/regional-us-west2',
97+
NAME='db',
98+
PROJECT_ID='project',
99+
AUTOCOMMIT=True,
100+
READONLY=True,
101+
),
102+
dict(
103+
database='db',
104+
instance='instance',
105+
instance_config='projects/proj/instanceConfigs/regional-us-west2',
106+
project_id='project',
107+
autocommit=True,
108+
readonly=True,
109+
),
110+
),
111+
]
112+
113+
for case in cases:
114+
din, want = case
115+
got = extract_connection_params(din)
116+
self.assertEqual(got, want, 'unequal dicts')
117+
118+
def test_SPANNER_URL_vs_dictParity(self):
119+
by_spanner_url = dict(
120+
SPANNER_URL='cloudspanner:/projects/proj/instances/django-dev1/databases/db1?'
121+
'instance_config=projects/proj/instanceConfigs/regional-us-west2;'
122+
'autocommit=true;readonly=true'
123+
)
124+
by_dict = dict(
125+
INSTANCE='django-dev1',
126+
NAME='db1',
127+
INSTANCE_CONFIG='projects/proj/instanceConfigs/regional-us-west2',
128+
PROJECT_ID='proj',
129+
AUTOCOMMIT=True,
130+
READONLY=True,
131+
)
132+
133+
got_by_spanner_url = extract_connection_params(by_spanner_url)
134+
got_by_dict = extract_connection_params(by_dict)
135+
136+
self.assertEqual(got_by_spanner_url, got_by_dict, 'No parity between equivalent configs')

0 commit comments

Comments
 (0)