Skip to content
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Commit 4704e7a

Browse files
authored
Merge branch 'main' into cleanup-resources-at-startup
2 parents 799e51a + 8585747 commit 4704e7a

6 files changed

Lines changed: 175 additions & 12 deletions

File tree

‎README.rst‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ the BigQuery client the following PyPI packages need to be installed:
117117

118118
.. code-block:: console
119119
120-
pip install google-cloud-bigquery[opentelemetry] opentelemetry-exporter-google-cloud
120+
pip install google-cloud-bigquery[opentelemetry] opentelemetry-exporter-gcp-trace
121121
122122
After installation, OpenTelemetry can be used in the BigQuery
123123
client and in BigQuery jobs. First, however, an exporter must be
@@ -128,12 +128,11 @@ example of this can be found here:
128128
129129
from opentelemetry import trace
130130
from opentelemetry.sdk.trace import TracerProvider
131-
from opentelemetry.sdk.trace.export import BatchExportSpanProcessor
131+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
132132
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
133+
tracer_provider = TracerProvider()
134+
tracer_provider = BatchSpanProcessor(CloudTraceSpanExporter())
133135
trace.set_tracer_provider(TracerProvider())
134-
trace.get_tracer_provider().add_span_processor(
135-
BatchExportSpanProcessor(CloudTraceSpanExporter())
136-
)
137136
138137
In this example all tracing data will be published to the Google
139138
`Cloud Trace`_ console. For more information on OpenTelemetry, please consult the `OpenTelemetry documentation`_.

‎google/cloud/bigquery/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
from google.cloud.bigquery.routine import RemoteFunctionOptions
9797
from google.cloud.bigquery.schema import PolicyTagList
9898
from google.cloud.bigquery.schema import SchemaField
99+
from google.cloud.bigquery.schema import FieldElementType
99100
from google.cloud.bigquery.standard_sql import StandardSqlDataType
100101
from google.cloud.bigquery.standard_sql import StandardSqlField
101102
from google.cloud.bigquery.standard_sql import StandardSqlStructType
@@ -158,6 +159,7 @@
158159
"RemoteFunctionOptions",
159160
# Shared helpers
160161
"SchemaField",
162+
"FieldElementType",
161163
"PolicyTagList",
162164
"UDFResource",
163165
"ExternalConfig",

‎google/cloud/bigquery/schema.py‎

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

1717
import collections
1818
import enum
19-
from typing import Any, Dict, Iterable, Optional, Union
19+
from typing import Any, Dict, Iterable, Optional, Union, cast
2020

2121
from google.cloud.bigquery import standard_sql
2222
from google.cloud.bigquery.enums import StandardSqlTypeNames
@@ -66,6 +66,46 @@ class _DefaultSentinel(enum.Enum):
6666
_DEFAULT_VALUE = _DefaultSentinel.DEFAULT_VALUE
6767

6868

69+
class FieldElementType(object):
70+
"""Represents the type of a field element.
71+
72+
Args:
73+
element_type (str): The type of a field element.
74+
"""
75+
76+
def __init__(self, element_type: str):
77+
self._properties = {}
78+
self._properties["type"] = element_type.upper()
79+
80+
@property
81+
def element_type(self):
82+
return self._properties.get("type")
83+
84+
@classmethod
85+
def from_api_repr(cls, api_repr: Optional[dict]) -> Optional["FieldElementType"]:
86+
"""Factory: construct a FieldElementType given its API representation.
87+
88+
Args:
89+
api_repr (Dict[str, str]): field element type as returned from
90+
the API.
91+
92+
Returns:
93+
google.cloud.bigquery.FieldElementType:
94+
Python object, as parsed from ``api_repr``.
95+
"""
96+
if not api_repr:
97+
return None
98+
return cls(api_repr["type"].upper())
99+
100+
def to_api_repr(self) -> dict:
101+
"""Construct the API resource representation of this field element type.
102+
103+
Returns:
104+
Dict[str, str]: Field element type represented as an API resource.
105+
"""
106+
return self._properties
107+
108+
69109
class SchemaField(object):
70110
"""Describe a single field within a table schema.
71111
@@ -117,6 +157,12 @@ class SchemaField(object):
117157
- Struct or array composed with the above allowed functions, for example:
118158
119159
"[CURRENT_DATE(), DATE '2020-01-01'"]
160+
161+
range_element_type: FieldElementType, str, Optional
162+
The subtype of the RANGE, if the type of this field is RANGE. If
163+
the type is RANGE, this field is required. Possible values for the
164+
field element type of a RANGE include `DATE`, `DATETIME` and
165+
`TIMESTAMP`.
120166
"""
121167

122168
def __init__(
@@ -131,6 +177,7 @@ def __init__(
131177
precision: Union[int, _DefaultSentinel] = _DEFAULT_VALUE,
132178
scale: Union[int, _DefaultSentinel] = _DEFAULT_VALUE,
133179
max_length: Union[int, _DefaultSentinel] = _DEFAULT_VALUE,
180+
range_element_type: Union[FieldElementType, str, None] = None,
134181
):
135182
self._properties: Dict[str, Any] = {
136183
"name": name,
@@ -152,6 +199,11 @@ def __init__(
152199
self._properties["policyTags"] = (
153200
policy_tags.to_api_repr() if policy_tags is not None else None
154201
)
202+
if isinstance(range_element_type, str):
203+
self._properties["rangeElementType"] = {"type": range_element_type}
204+
if isinstance(range_element_type, FieldElementType):
205+
self._properties["rangeElementType"] = range_element_type.to_api_repr()
206+
155207
self._fields = tuple(fields)
156208

157209
@staticmethod
@@ -186,6 +238,12 @@ def from_api_repr(cls, api_repr: dict) -> "SchemaField":
186238
if policy_tags is not None and policy_tags is not _DEFAULT_VALUE:
187239
policy_tags = PolicyTagList.from_api_repr(policy_tags)
188240

241+
if api_repr.get("rangeElementType"):
242+
range_element_type = cast(dict, api_repr.get("rangeElementType"))
243+
element_type = range_element_type.get("type")
244+
else:
245+
element_type = None
246+
189247
return cls(
190248
field_type=field_type,
191249
fields=[cls.from_api_repr(f) for f in fields],
@@ -197,6 +255,7 @@ def from_api_repr(cls, api_repr: dict) -> "SchemaField":
197255
precision=cls.__get_int(api_repr, "precision"),
198256
scale=cls.__get_int(api_repr, "scale"),
199257
max_length=cls.__get_int(api_repr, "maxLength"),
258+
range_element_type=element_type,
200259
)
201260

202261
@property
@@ -252,6 +311,18 @@ def max_length(self):
252311
"""Optional[int]: Maximum length for the STRING or BYTES field."""
253312
return self._properties.get("maxLength")
254313

314+
@property
315+
def range_element_type(self):
316+
"""Optional[FieldElementType]: The subtype of the RANGE, if the
317+
type of this field is RANGE.
318+
319+
Must be set when ``type`` is `"RANGE"`. Must be one of `"DATE"`,
320+
`"DATETIME"` or `"TIMESTAMP"`.
321+
"""
322+
if self._properties.get("rangeElementType"):
323+
ret = self._properties.get("rangeElementType")
324+
return FieldElementType.from_api_repr(ret)
325+
255326
@property
256327
def fields(self):
257328
"""Optional[tuple]: Subfields contained in this field.

‎tests/data/schema.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@
8383
"mode" : "NULLABLE",
8484
"name" : "FavoriteNumber",
8585
"type" : "NUMERIC"
86+
},
87+
{
88+
"mode" : "NULLABLE",
89+
"name" : "TimeRange",
90+
"type" : "RANGE",
91+
"rangeElementType": {
92+
"type": "DATETIME"
93+
}
8694
}
8795
]
8896
}

‎tests/system/test_client.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1781,19 +1781,13 @@ def test_dbapi_fetch_w_bqstorage_client_large_result_set(self):
17811781
)
17821782

17831783
result_rows = [cursor.fetchone(), cursor.fetchone(), cursor.fetchone()]
1784-
17851784
field_name = operator.itemgetter(0)
17861785
fetched_data = [sorted(row.items(), key=field_name) for row in result_rows]
17871786
# Since DB API is not thread safe, only a single result stream should be
17881787
# requested by the BQ storage client, meaning that results should arrive
17891788
# in the sorted order.
17901789

17911790
expected_data = [
1792-
[
1793-
("by", "pg"),
1794-
("id", 1),
1795-
("timestamp", datetime.datetime(2006, 10, 9, 18, 21, 51, tzinfo=UTC)),
1796-
],
17971791
[
17981792
("by", "phyllis"),
17991793
("id", 2),
@@ -1804,6 +1798,11 @@ def test_dbapi_fetch_w_bqstorage_client_large_result_set(self):
18041798
("id", 3),
18051799
("timestamp", datetime.datetime(2006, 10, 9, 18, 40, 33, tzinfo=UTC)),
18061800
],
1801+
[
1802+
("by", "onebeerdave"),
1803+
("id", 4),
1804+
("timestamp", datetime.datetime(2006, 10, 9, 18, 47, 42, tzinfo=UTC)),
1805+
],
18071806
]
18081807

18091808
self.assertEqual(fetched_data, expected_data)

‎tests/unit/test_schema.py‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,36 @@ def test_constructor_subfields(self):
9797
self.assertEqual(field.fields[0], sub_field1)
9898
self.assertEqual(field.fields[1], sub_field2)
9999

100+
def test_constructor_range(self):
101+
from google.cloud.bigquery.schema import FieldElementType
102+
103+
field = self._make_one(
104+
"test",
105+
"RANGE",
106+
mode="REQUIRED",
107+
description="Testing",
108+
range_element_type=FieldElementType("DATETIME"),
109+
)
110+
self.assertEqual(field.name, "test")
111+
self.assertEqual(field.field_type, "RANGE")
112+
self.assertEqual(field.mode, "REQUIRED")
113+
self.assertEqual(field.description, "Testing")
114+
self.assertEqual(field.range_element_type.element_type, "DATETIME")
115+
116+
def test_constructor_range_str(self):
117+
field = self._make_one(
118+
"test",
119+
"RANGE",
120+
mode="REQUIRED",
121+
description="Testing",
122+
range_element_type="DATETIME",
123+
)
124+
self.assertEqual(field.name, "test")
125+
self.assertEqual(field.field_type, "RANGE")
126+
self.assertEqual(field.mode, "REQUIRED")
127+
self.assertEqual(field.description, "Testing")
128+
self.assertEqual(field.range_element_type.element_type, "DATETIME")
129+
100130
def test_to_api_repr(self):
101131
from google.cloud.bigquery.schema import PolicyTagList
102132

@@ -160,6 +190,7 @@ def test_from_api_repr(self):
160190
self.assertEqual(field.fields[0].name, "bar")
161191
self.assertEqual(field.fields[0].field_type, "INTEGER")
162192
self.assertEqual(field.fields[0].mode, "NULLABLE")
193+
self.assertEqual(field.range_element_type, None)
163194

164195
def test_from_api_repr_policy(self):
165196
field = self._get_target_class().from_api_repr(
@@ -178,6 +209,23 @@ def test_from_api_repr_policy(self):
178209
self.assertEqual(field.fields[0].field_type, "INTEGER")
179210
self.assertEqual(field.fields[0].mode, "NULLABLE")
180211

212+
def test_from_api_repr_range(self):
213+
field = self._get_target_class().from_api_repr(
214+
{
215+
"mode": "nullable",
216+
"description": "test_range",
217+
"name": "foo",
218+
"type": "range",
219+
"rangeElementType": {"type": "DATETIME"},
220+
}
221+
)
222+
self.assertEqual(field.name, "foo")
223+
self.assertEqual(field.field_type, "RANGE")
224+
self.assertEqual(field.mode, "NULLABLE")
225+
self.assertEqual(field.description, "test_range")
226+
self.assertEqual(len(field.fields), 0)
227+
self.assertEqual(field.range_element_type.element_type, "DATETIME")
228+
181229
def test_from_api_repr_defaults(self):
182230
field = self._get_target_class().from_api_repr(
183231
{"name": "foo", "type": "record"}
@@ -192,8 +240,10 @@ def test_from_api_repr_defaults(self):
192240
# _properties.
193241
self.assertIsNone(field.description)
194242
self.assertIsNone(field.policy_tags)
243+
self.assertIsNone(field.range_element_type)
195244
self.assertNotIn("description", field._properties)
196245
self.assertNotIn("policyTags", field._properties)
246+
self.assertNotIn("rangeElementType", field._properties)
197247

198248
def test_name_property(self):
199249
name = "lemon-ness"
@@ -566,6 +616,40 @@ def test___repr__evaluable_with_policy_tags(self):
566616
assert field == evaled_field
567617

568618

619+
class TestFieldElementType(unittest.TestCase):
620+
@staticmethod
621+
def _get_target_class():
622+
from google.cloud.bigquery.schema import FieldElementType
623+
624+
return FieldElementType
625+
626+
def _make_one(self, *args):
627+
return self._get_target_class()(*args)
628+
629+
def test_constructor(self):
630+
element_type = self._make_one("DATETIME")
631+
self.assertEqual(element_type.element_type, "DATETIME")
632+
self.assertEqual(element_type._properties["type"], "DATETIME")
633+
634+
def test_to_api_repr(self):
635+
element_type = self._make_one("DATETIME")
636+
self.assertEqual(element_type.to_api_repr(), {"type": "DATETIME"})
637+
638+
def test_from_api_repr(self):
639+
api_repr = {"type": "DATETIME"}
640+
expected_element_type = self._make_one("DATETIME")
641+
self.assertEqual(
642+
expected_element_type.element_type,
643+
self._get_target_class().from_api_repr(api_repr).element_type,
644+
)
645+
646+
def test_from_api_repr_empty(self):
647+
self.assertEqual(None, self._get_target_class().from_api_repr({}))
648+
649+
def test_from_api_repr_none(self):
650+
self.assertEqual(None, self._get_target_class().from_api_repr(None))
651+
652+
569653
# TODO: dedup with the same class in test_table.py.
570654
class _SchemaBase(object):
571655
def _verify_field(self, field, r_field):

0 commit comments

Comments
 (0)