Skip to content

Commit ffdc737

Browse files
committed
incorporate suggestions
1 parent cfdfbd0 commit ffdc737

File tree

5 files changed

+643
-755
lines changed

5 files changed

+643
-755
lines changed

google/cloud/spanner_v1/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
from .types.type import Type
6464
from .types.type import TypeAnnotationCode
6565
from .types.type import TypeCode
66-
from .data_types import JsonObject
66+
from .data_types import JsonObject, Interval
6767
from .transaction import BatchTransactionId, DefaultTransactionOptions
6868

6969
from google.cloud.spanner_v1 import param_types
@@ -145,6 +145,7 @@
145145
"TypeCode",
146146
# Custom spanner related data types
147147
"JsonObject",
148+
"Interval",
148149
# google.cloud.spanner_v1.services
149150
"SpannerClient",
150151
"SpannerAsyncClient",

google/cloud/spanner_v1/_helpers.py

Lines changed: 1 addition & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
import time
2121
import base64
2222
import threading
23-
import re
24-
from dataclasses import dataclass
2523

2624
from google.protobuf.struct_pb2 import ListValue
2725
from google.protobuf.struct_pb2 import Value
@@ -33,7 +31,7 @@
3331
from google.cloud._helpers import _date_from_iso8601_date
3432
from google.cloud.spanner_v1 import TypeCode
3533
from google.cloud.spanner_v1 import ExecuteSqlRequest
36-
from google.cloud.spanner_v1 import JsonObject
34+
from google.cloud.spanner_v1 import JsonObject, Interval
3735
from google.cloud.spanner_v1 import TransactionOptions
3836
from google.cloud.spanner_v1.request_id_header import with_request_id
3937
from google.rpc.error_details_pb2 import RetryInfo
@@ -198,152 +196,6 @@ def _datetime_to_rfc3339_nanoseconds(value):
198196
return "{}.{}Z".format(value.isoformat(sep="T", timespec="seconds"), nanos)
199197

200198

201-
@dataclass
202-
class Interval:
203-
"""Represents a Spanner INTERVAL type.
204-
205-
An interval is a combination of months, days and nanoseconds.
206-
Internally, Spanner supports Interval value with the following range of individual fields:
207-
months: [-120000, 120000]
208-
days: [-3660000, 3660000]
209-
nanoseconds: [-316224000000000000000, 316224000000000000000]
210-
"""
211-
212-
months: int = 0
213-
days: int = 0
214-
nanos: int = 0
215-
216-
def __str__(self) -> str:
217-
"""Returns the ISO8601 duration format string representation."""
218-
result = ["P"]
219-
220-
# Handle years and months
221-
if self.months:
222-
is_negative = self.months < 0
223-
abs_months = abs(self.months)
224-
years, months = divmod(abs_months, 12)
225-
if years:
226-
result.append(f"{'-' if is_negative else ''}{years}Y")
227-
if months:
228-
result.append(f"{'-' if is_negative else ''}{months}M")
229-
230-
# Handle days
231-
if self.days:
232-
result.append(f"{self.days}D")
233-
234-
# Handle time components
235-
if self.nanos:
236-
result.append("T")
237-
nanos = abs(self.nanos)
238-
is_negative = self.nanos < 0
239-
240-
# Convert to hours, minutes, seconds
241-
nanos_per_hour = 3600000000000
242-
hours, nanos = divmod(nanos, nanos_per_hour)
243-
if hours:
244-
if is_negative:
245-
result.append("-")
246-
result.append(f"{hours}H")
247-
248-
nanos_per_minute = 60000000000
249-
minutes, nanos = divmod(nanos, nanos_per_minute)
250-
if minutes:
251-
if is_negative:
252-
result.append("-")
253-
result.append(f"{minutes}M")
254-
255-
nanos_per_second = 1000000000
256-
seconds, nanos_fraction = divmod(nanos, nanos_per_second)
257-
258-
if seconds or nanos_fraction:
259-
if is_negative:
260-
result.append("-")
261-
if seconds:
262-
result.append(str(seconds))
263-
elif nanos_fraction:
264-
result.append("0")
265-
266-
if nanos_fraction:
267-
nano_str = f"{nanos_fraction:09d}"
268-
trimmed = nano_str.rstrip("0")
269-
if len(trimmed) <= 3:
270-
while len(trimmed) < 3:
271-
trimmed += "0"
272-
elif len(trimmed) <= 6:
273-
while len(trimmed) < 6:
274-
trimmed += "0"
275-
else:
276-
while len(trimmed) < 9:
277-
trimmed += "0"
278-
result.append(f".{trimmed}")
279-
result.append("S")
280-
281-
if len(result) == 1:
282-
result.append("0Y") # Special case for zero interval
283-
284-
return "".join(result)
285-
286-
@classmethod
287-
def from_str(cls, s: str) -> "Interval":
288-
"""Parse an ISO8601 duration format string into an Interval."""
289-
pattern = r"^P(-?\d+Y)?(-?\d+M)?(-?\d+D)?(T(-?\d+H)?(-?\d+M)?(-?((\d+([.,]\d{1,9})?)|([.,]\d{1,9}))S)?)?$"
290-
match = re.match(pattern, s)
291-
if not match or len(s) == 1:
292-
raise ValueError(f"Invalid interval format: {s}")
293-
294-
parts = match.groups()
295-
if not any(parts[:3]) and not parts[3]:
296-
raise ValueError(
297-
f"Invalid interval format: at least one component (Y/M/D/H/M/S) is required: {s}"
298-
)
299-
300-
if parts[3] == "T" and not any(parts[4:7]):
301-
raise ValueError(
302-
f"Invalid interval format: time designator 'T' present but no time components specified: {s}"
303-
)
304-
305-
def parse_num(s: str, suffix: str) -> int:
306-
if not s:
307-
return 0
308-
return int(s.rstrip(suffix))
309-
310-
years = parse_num(parts[0], "Y")
311-
months = parse_num(parts[1], "M")
312-
total_months = years * 12 + months
313-
314-
days = parse_num(parts[2], "D")
315-
316-
nanos = 0
317-
if parts[3]: # Has time component
318-
# Convert hours to nanoseconds
319-
hours = parse_num(parts[4], "H")
320-
nanos += hours * 3600000000000
321-
322-
# Convert minutes to nanoseconds
323-
minutes = parse_num(parts[5], "M")
324-
nanos += minutes * 60000000000
325-
326-
# Handle seconds and fractional seconds
327-
if parts[6]:
328-
seconds = parts[6].rstrip("S")
329-
if "," in seconds:
330-
seconds = seconds.replace(",", ".")
331-
332-
if "." in seconds:
333-
sec_parts = seconds.split(".")
334-
whole_seconds = sec_parts[0] if sec_parts[0] else "0"
335-
nanos += int(whole_seconds) * 1000000000
336-
frac = sec_parts[1][:9].ljust(9, "0")
337-
frac_nanos = int(frac)
338-
if seconds.startswith("-"):
339-
frac_nanos = -frac_nanos
340-
nanos += frac_nanos
341-
else:
342-
nanos += int(seconds) * 1000000000
343-
344-
return cls(months=total_months, days=days, nanos=nanos)
345-
346-
347199
def _make_value_pb(value):
348200
"""Helper for :func:`_make_list_value_pbs`.
349201

google/cloud/spanner_v1/data_types.py

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
import json
1818
import types
19-
19+
import re
20+
from dataclasses import dataclass
2021
from google.protobuf.message import Message
2122
from google.protobuf.internal.enum_type_wrapper import EnumTypeWrapper
2223

@@ -97,6 +98,152 @@ def serialize(self):
9798
return json.dumps(self, sort_keys=True, separators=(",", ":"))
9899

99100

101+
@dataclass
102+
class Interval:
103+
"""Represents a Spanner INTERVAL type.
104+
105+
An interval is a combination of months, days and nanoseconds.
106+
Internally, Spanner supports Interval value with the following range of individual fields:
107+
months: [-120000, 120000]
108+
days: [-3660000, 3660000]
109+
nanoseconds: [-316224000000000000000, 316224000000000000000]
110+
"""
111+
112+
months: int = 0
113+
days: int = 0
114+
nanos: int = 0
115+
116+
def __str__(self) -> str:
117+
"""Returns the ISO8601 duration format string representation."""
118+
result = ["P"]
119+
120+
# Handle years and months
121+
if self.months:
122+
is_negative = self.months < 0
123+
abs_months = abs(self.months)
124+
years, months = divmod(abs_months, 12)
125+
if years:
126+
result.append(f"{'-' if is_negative else ''}{years}Y")
127+
if months:
128+
result.append(f"{'-' if is_negative else ''}{months}M")
129+
130+
# Handle days
131+
if self.days:
132+
result.append(f"{self.days}D")
133+
134+
# Handle time components
135+
if self.nanos:
136+
result.append("T")
137+
nanos = abs(self.nanos)
138+
is_negative = self.nanos < 0
139+
140+
# Convert to hours, minutes, seconds
141+
nanos_per_hour = 3600000000000
142+
hours, nanos = divmod(nanos, nanos_per_hour)
143+
if hours:
144+
if is_negative:
145+
result.append("-")
146+
result.append(f"{hours}H")
147+
148+
nanos_per_minute = 60000000000
149+
minutes, nanos = divmod(nanos, nanos_per_minute)
150+
if minutes:
151+
if is_negative:
152+
result.append("-")
153+
result.append(f"{minutes}M")
154+
155+
nanos_per_second = 1000000000
156+
seconds, nanos_fraction = divmod(nanos, nanos_per_second)
157+
158+
if seconds or nanos_fraction:
159+
if is_negative:
160+
result.append("-")
161+
if seconds:
162+
result.append(str(seconds))
163+
elif nanos_fraction:
164+
result.append("0")
165+
166+
if nanos_fraction:
167+
nano_str = f"{nanos_fraction:09d}"
168+
trimmed = nano_str.rstrip("0")
169+
if len(trimmed) <= 3:
170+
while len(trimmed) < 3:
171+
trimmed += "0"
172+
elif len(trimmed) <= 6:
173+
while len(trimmed) < 6:
174+
trimmed += "0"
175+
else:
176+
while len(trimmed) < 9:
177+
trimmed += "0"
178+
result.append(f".{trimmed}")
179+
result.append("S")
180+
181+
if len(result) == 1:
182+
result.append("0Y") # Special case for zero interval
183+
184+
return "".join(result)
185+
186+
@classmethod
187+
def from_str(cls, s: str) -> "Interval":
188+
"""Parse an ISO8601 duration format string into an Interval."""
189+
pattern = r"^P(-?\d+Y)?(-?\d+M)?(-?\d+D)?(T(-?\d+H)?(-?\d+M)?(-?((\d+([.,]\d{1,9})?)|([.,]\d{1,9}))S)?)?$"
190+
match = re.match(pattern, s)
191+
if not match or len(s) == 1:
192+
raise ValueError(f"Invalid interval format: {s}")
193+
194+
parts = match.groups()
195+
if not any(parts[:3]) and not parts[3]:
196+
raise ValueError(
197+
f"Invalid interval format: at least one component (Y/M/D/H/M/S) is required: {s}"
198+
)
199+
200+
if parts[3] == "T" and not any(parts[4:7]):
201+
raise ValueError(
202+
f"Invalid interval format: time designator 'T' present but no time components specified: {s}"
203+
)
204+
205+
def parse_num(s: str, suffix: str) -> int:
206+
if not s:
207+
return 0
208+
return int(s.rstrip(suffix))
209+
210+
years = parse_num(parts[0], "Y")
211+
months = parse_num(parts[1], "M")
212+
total_months = years * 12 + months
213+
214+
days = parse_num(parts[2], "D")
215+
216+
nanos = 0
217+
if parts[3]: # Has time component
218+
# Convert hours to nanoseconds
219+
hours = parse_num(parts[4], "H")
220+
nanos += hours * 3600000000000
221+
222+
# Convert minutes to nanoseconds
223+
minutes = parse_num(parts[5], "M")
224+
nanos += minutes * 60000000000
225+
226+
# Handle seconds and fractional seconds
227+
if parts[6]:
228+
seconds = parts[6].rstrip("S")
229+
if "," in seconds:
230+
seconds = seconds.replace(",", ".")
231+
232+
if "." in seconds:
233+
sec_parts = seconds.split(".")
234+
whole_seconds = sec_parts[0] if sec_parts[0] else "0"
235+
nanos += int(whole_seconds) * 1000000000
236+
frac = sec_parts[1][:9].ljust(9, "0")
237+
frac_nanos = int(frac)
238+
if seconds.startswith("-"):
239+
frac_nanos = -frac_nanos
240+
nanos += frac_nanos
241+
else:
242+
nanos += int(seconds) * 1000000000
243+
244+
return cls(months=total_months, days=days, nanos=nanos)
245+
246+
100247
def _proto_message(bytes_val, proto_message_object):
101248
"""Helper for :func:`get_proto_message`.
102249
parses serialized protocol buffer bytes data into proto message.

tests/system/test_session_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2919,7 +2919,7 @@ def get_param_info(param_names, database_dialect):
29192919

29202920

29212921
def test_interval(sessions_database, database_dialect, not_emulator):
2922-
from google.cloud.spanner_v1._helpers import Interval
2922+
from google.cloud.spanner_v1 import Interval
29232923

29242924
def setup_table():
29252925
if database_dialect == DatabaseDialect.POSTGRESQL:

0 commit comments

Comments
 (0)