Skip to content

Commit fb9733b

Browse files
committed
Prep spanner release.
0 parents  commit fb9733b

34 files changed

Lines changed: 10480 additions & 0 deletions

.coveragerc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[run]
2+
branch = True
3+
4+
[report]
5+
fail_under = 100
6+
show_missing = True
7+
exclude_lines =
8+
# Re-enable the standard pragma
9+
pragma: NO COVER
10+
# Ignore debug-only repr
11+
def __repr__

MANIFEST.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include README.rst
2+
graft google
3+
graft unit_tests
4+
global-exclude *.pyc

README.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Python Client for Cloud Spanner
2+
===============================
3+
4+
Python idiomatic client for `Cloud Spanner`_
5+
6+
Quick Start
7+
-----------
8+
9+
::
10+
11+
$ pip install --upgrade google-cloud-spanner

google/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
try:
16+
import pkg_resources
17+
pkg_resources.declare_namespace(__name__)
18+
except ImportError:
19+
import pkgutil
20+
__path__ = pkgutil.extend_path(__path__, __name__)

google/cloud/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
try:
16+
import pkg_resources
17+
pkg_resources.declare_namespace(__name__)
18+
except ImportError:
19+
import pkgutil
20+
__path__ = pkgutil.extend_path(__path__, __name__)

google/cloud/spanner/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2016 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Cloud Spanner API package."""
16+
17+
18+
from google.cloud.spanner.client import Client
19+
20+
from google.cloud.spanner.keyset import KeyRange
21+
from google.cloud.spanner.keyset import KeySet
22+
23+
from google.cloud.spanner.pool import AbstractSessionPool
24+
from google.cloud.spanner.pool import BurstyPool
25+
from google.cloud.spanner.pool import FixedSizePool

google/cloud/spanner/_fixtures.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2016 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test fixtures."""
16+
17+
18+
DDL = """\
19+
CREATE TABLE contacts (
20+
contact_id INT64,
21+
first_name STRING(1024),
22+
last_name STRING(1024),
23+
email STRING(1024) )
24+
PRIMARY KEY (contact_id);
25+
CREATE TABLE contact_phones (
26+
contact_id INT64,
27+
phone_type STRING(1024),
28+
phone_number STRING(1024) )
29+
PRIMARY KEY (contact_id, phone_type),
30+
INTERLEAVE IN PARENT contacts ON DELETE CASCADE;
31+
"""
32+
33+
DDL_STATEMENTS = [stmt.strip() for stmt in DDL.split(';') if stmt.strip()]

google/cloud/spanner/_helpers.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
# Copyright 2016 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Helper functions for Cloud Spanner."""
16+
17+
import datetime
18+
import math
19+
20+
import six
21+
22+
from google.gax import CallOptions
23+
from google.protobuf.struct_pb2 import ListValue
24+
from google.protobuf.struct_pb2 import Value
25+
from google.cloud.proto.spanner.v1 import type_pb2
26+
27+
from google.cloud._helpers import _date_from_iso8601_date
28+
from google.cloud._helpers import _datetime_to_rfc3339
29+
from google.cloud._helpers import _RFC3339_NANOS
30+
from google.cloud._helpers import _RFC3339_NO_FRACTION
31+
from google.cloud._helpers import UTC
32+
33+
34+
class TimestampWithNanoseconds(datetime.datetime):
35+
"""Track nanosecond in addition to normal datetime attrs.
36+
37+
nanosecond can be passed only as a keyword argument.
38+
"""
39+
__slots__ = ('_nanosecond',)
40+
41+
def __new__(cls, *args, **kw):
42+
nanos = kw.pop('nanosecond', 0)
43+
if nanos > 0:
44+
if 'microsecond' in kw:
45+
raise TypeError(
46+
"Specify only one of 'microsecond' or 'nanosecond'")
47+
kw['microsecond'] = nanos // 1000
48+
inst = datetime.datetime.__new__(cls, *args, **kw)
49+
inst._nanosecond = nanos or 0
50+
return inst
51+
52+
@property
53+
def nanosecond(self):
54+
"""Read-only: nanosecond precision."""
55+
return self._nanosecond
56+
57+
def rfc3339(self):
58+
"""RFC 3339-compliant timestamp.
59+
60+
:rtype: str
61+
:returns: Timestamp string according to RFC 3339 spec.
62+
"""
63+
if self._nanosecond == 0:
64+
return _datetime_to_rfc3339(self)
65+
nanos = str(self._nanosecond).rstrip('0')
66+
return '%s.%sZ' % (self.strftime(_RFC3339_NO_FRACTION), nanos)
67+
68+
@classmethod
69+
def from_rfc3339(cls, stamp):
70+
"""Parse RFC 3339-compliant timestamp, preserving nanoseconds.
71+
72+
:type stamp: str
73+
:param stamp: RFC 3339 stamp, with up to nanosecond precision
74+
75+
:rtype: :class:`TimestampWithNanoseconds`
76+
:returns: an instance matching the timestamp string
77+
"""
78+
with_nanos = _RFC3339_NANOS.match(stamp)
79+
if with_nanos is None:
80+
raise ValueError(
81+
'Timestamp: %r, does not match pattern: %r' % (
82+
stamp, _RFC3339_NANOS.pattern))
83+
bare = datetime.datetime.strptime(
84+
with_nanos.group('no_fraction'), _RFC3339_NO_FRACTION)
85+
fraction = with_nanos.group('nanos')
86+
if fraction is None:
87+
nanos = 0
88+
else:
89+
scale = 9 - len(fraction)
90+
nanos = int(fraction) * (10 ** scale)
91+
return cls(bare.year, bare.month, bare.day,
92+
bare.hour, bare.minute, bare.second,
93+
nanosecond=nanos, tzinfo=UTC)
94+
95+
96+
def _try_to_coerce_bytes(bytestring):
97+
"""Try to coerce a byte string into the right thing based on Python
98+
version and whether or not it is base64 encoded.
99+
100+
Return a text string or raise ValueError.
101+
"""
102+
# Attempt to coerce using google.protobuf.Value, which will expect
103+
# something that is utf-8 (and base64 consistently is).
104+
try:
105+
Value(string_value=bytestring)
106+
return bytestring
107+
except ValueError:
108+
raise ValueError('Received a bytes that is not base64 encoded. '
109+
'Ensure that you either send a Unicode string or a '
110+
'base64-encoded bytes.')
111+
112+
113+
# pylint: disable=too-many-return-statements
114+
def _make_value_pb(value):
115+
"""Helper for :func:`_make_list_value_pbs`.
116+
117+
:type value: scalar value
118+
:param value: value to convert
119+
120+
:rtype: :class:`~google.protobuf.struct_pb2.Value`
121+
:returns: value protobufs
122+
:raises: :exc:`ValueError` if value is not of a known scalar type.
123+
"""
124+
if value is None:
125+
return Value(null_value='NULL_VALUE')
126+
if isinstance(value, list):
127+
return Value(list_value=_make_list_value_pb(value))
128+
if isinstance(value, bool):
129+
return Value(bool_value=value)
130+
if isinstance(value, six.integer_types):
131+
return Value(string_value=str(value))
132+
if isinstance(value, float):
133+
if math.isnan(value):
134+
return Value(string_value='NaN')
135+
if math.isinf(value):
136+
return Value(string_value=str(value))
137+
return Value(number_value=value)
138+
if isinstance(value, TimestampWithNanoseconds):
139+
return Value(string_value=value.rfc3339())
140+
if isinstance(value, datetime.datetime):
141+
return Value(string_value=_datetime_to_rfc3339(value))
142+
if isinstance(value, datetime.date):
143+
return Value(string_value=value.isoformat())
144+
if isinstance(value, six.binary_type):
145+
value = _try_to_coerce_bytes(value)
146+
return Value(string_value=value)
147+
if isinstance(value, six.text_type):
148+
return Value(string_value=value)
149+
raise ValueError("Unknown type: %s" % (value,))
150+
# pylint: enable=too-many-return-statements
151+
152+
153+
def _make_list_value_pb(values):
154+
"""Construct of ListValue protobufs.
155+
156+
:type values: list of scalar
157+
:param values: Row data
158+
159+
:rtype: :class:`~google.protobuf.struct_pb2.ListValue`
160+
:returns: protobuf
161+
"""
162+
return ListValue(values=[_make_value_pb(value) for value in values])
163+
164+
165+
def _make_list_value_pbs(values):
166+
"""Construct a sequence of ListValue protobufs.
167+
168+
:type values: list of list of scalar
169+
:param values: Row data
170+
171+
:rtype: list of :class:`~google.protobuf.struct_pb2.ListValue`
172+
:returns: sequence of protobufs
173+
"""
174+
return [_make_list_value_pb(row) for row in values]
175+
176+
177+
# pylint: disable=too-many-branches
178+
def _parse_value_pb(value_pb, field_type):
179+
"""Convert a Value protobuf to cell data.
180+
181+
:type value_pb: :class:`~google.protobuf.struct_pb2.Value`
182+
:param value_pb: protobuf to convert
183+
184+
:type field_type: :class:`~google.cloud.proto.spanner.v1.type_pb2.Type`
185+
:param field_type: type code for the value
186+
187+
:rtype: varies on field_type
188+
:returns: value extracted from value_pb
189+
:raises: ValueError if uknown type is passed
190+
"""
191+
if value_pb.HasField('null_value'):
192+
return None
193+
if field_type.code == type_pb2.STRING:
194+
result = value_pb.string_value
195+
elif field_type.code == type_pb2.BYTES:
196+
result = value_pb.string_value.encode('utf8')
197+
elif field_type.code == type_pb2.BOOL:
198+
result = value_pb.bool_value
199+
elif field_type.code == type_pb2.INT64:
200+
result = int(value_pb.string_value)
201+
elif field_type.code == type_pb2.FLOAT64:
202+
if value_pb.HasField('string_value'):
203+
result = float(value_pb.string_value)
204+
else:
205+
result = value_pb.number_value
206+
elif field_type.code == type_pb2.DATE:
207+
result = _date_from_iso8601_date(value_pb.string_value)
208+
elif field_type.code == type_pb2.TIMESTAMP:
209+
result = TimestampWithNanoseconds.from_rfc3339(value_pb.string_value)
210+
elif field_type.code == type_pb2.ARRAY:
211+
result = [
212+
_parse_value_pb(item_pb, field_type.array_element_type)
213+
for item_pb in value_pb.list_value.values]
214+
elif field_type.code == type_pb2.STRUCT:
215+
result = [
216+
_parse_value_pb(item_pb, field_type.struct_type.fields[i].type)
217+
for (i, item_pb) in enumerate(value_pb.list_value.values)]
218+
else:
219+
raise ValueError("Unknown type: %s" % (field_type,))
220+
return result
221+
# pylint: enable=too-many-branches
222+
223+
224+
def _parse_list_value_pbs(rows, row_type):
225+
"""Convert a list of ListValue protobufs into a list of list of cell data.
226+
227+
:type rows: list of :class:`~google.protobuf.struct_pb2.ListValue`
228+
:param rows: row data returned from a read/query
229+
230+
:type row_type: :class:`~google.cloud.proto.spanner.v1.type_pb2.StructType`
231+
:param row_type: row schema specification
232+
233+
:rtype: list of list of cell data
234+
:returns: data for the rows, coerced into appropriate types
235+
"""
236+
result = []
237+
for row in rows:
238+
row_data = []
239+
for value_pb, field in zip(row.values, row_type.fields):
240+
row_data.append(_parse_value_pb(value_pb, field.type))
241+
result.append(row_data)
242+
return result
243+
244+
245+
class _SessionWrapper(object):
246+
"""Base class for objects wrapping a session.
247+
248+
:type session: :class:`~google.cloud.spanner.session.Session`
249+
:param session: the session used to perform the commit
250+
"""
251+
def __init__(self, session):
252+
self._session = session
253+
254+
255+
def _options_with_prefix(prefix, **kw):
256+
"""Create GAPIC options w/ prefix.
257+
258+
:type prefix: str
259+
:param prefix: appropriate resource path
260+
261+
:type kw: dict
262+
:param kw: other keyword arguments passed to the constructor
263+
264+
:rtype: :class:`~google.gax.CallOptions`
265+
:returns: GAPIC call options with supplied prefix
266+
"""
267+
return CallOptions(
268+
metadata=[('google-cloud-resource-prefix', prefix)], **kw)

0 commit comments

Comments
 (0)