Skip to content

Commit 07c66aa

Browse files
committed
feat: adds time_zone to external config and load job
1 parent 37e4e0e commit 07c66aa

File tree

5 files changed

+75
-0
lines changed

5 files changed

+75
-0
lines changed

google/cloud/bigquery/external_config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,23 @@ def schema(self, value):
848848
prop = {"fields": [field.to_api_repr() for field in value]}
849849
self._properties["schema"] = prop
850850

851+
@property
852+
def time_zone(self) -> Optional[str]:
853+
"""Optional[str]: Time zone used when parsing timestamp values that do not
854+
have specific time zone information (e.g. 2024-04-20 12:34:56). The expected
855+
format is an IANA timezone string (e.g. America/Los_Angeles).
856+
857+
See:
858+
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#ExternalDataConfiguration.FIELDS.time_zone
859+
"""
860+
861+
result = self._properties.get("timeZone")
862+
return typing.cast(str, result)
863+
864+
@time_zone.setter
865+
def time_zone(self, value: Optional[str]):
866+
self._properties["timeZone"] = value
867+
851868
@property
852869
def connection_id(self):
853870
"""Optional[str]: [Experimental] ID of a BigQuery Connection API

google/cloud/bigquery/job/load.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,20 @@ def source_format(self):
548548
def source_format(self, value):
549549
self._set_sub_prop("sourceFormat", value)
550550

551+
@property
552+
def time_zone(self) -> Optional[str]:
553+
"""Optional[str]: Default time zone that will apply when parsing timestamp
554+
values that have no specific time zone.
555+
556+
See:
557+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#JobConfigurationLoad.FIELDS.time_zone
558+
"""
559+
return self._get_sub_prop("timeZone")
560+
561+
@time_zone.setter
562+
def time_zone(self, value: Optional[str]):
563+
self._set_sub_prop("timeZone", value)
564+
551565
@property
552566
def time_partitioning(self):
553567
"""Optional[google.cloud.bigquery.table.TimePartitioning]: Specifies time-based
@@ -889,6 +903,13 @@ def clustering_fields(self):
889903
"""
890904
return self.configuration.clustering_fields
891905

906+
@property
907+
def time_zone(self):
908+
"""See
909+
:attr:`google.cloud.bigquery.job.LoadJobConfig.time_zone`.
910+
"""
911+
return self.configuration.time_zone
912+
892913
@property
893914
def schema_update_options(self):
894915
"""See

tests/unit/job/test_load.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,14 @@ def _setUpConstants(self):
3838
self.OUTPUT_ROWS = 345
3939
self.REFERENCE_FILE_SCHEMA_URI = "gs://path/to/reference"
4040

41+
self.TIME_ZONE = "UTC"
42+
4143
def _make_resource(self, started=False, ended=False):
4244
resource = super(TestLoadJob, self)._make_resource(started, ended)
4345
config = resource["configuration"]["load"]
4446
config["sourceUris"] = [self.SOURCE1]
47+
48+
config["timeZone"] = self.TIME_ZONE
4549
config["destinationTable"] = {
4650
"projectId": self.PROJECT,
4751
"datasetId": self.DS_ID,
@@ -152,6 +156,10 @@ def _verifyResourceProperties(self, job, resource):
152156
)
153157
else:
154158
self.assertIsNone(job.destination_encryption_configuration)
159+
if "timeZone" in config:
160+
self.assertEqual(job.time_zone, config["timeZone"])
161+
else:
162+
self.assertIsNone(job.time_zone)
155163

156164
def test_ctor(self):
157165
client = _make_client(project=self.PROJECT)
@@ -195,6 +203,8 @@ def test_ctor(self):
195203
self.assertIsNone(job.schema_update_options)
196204
self.assertIsNone(job.reference_file_schema_uri)
197205

206+
self.assertIsNone(job.time_zone)
207+
198208
def test_ctor_w_config(self):
199209
from google.cloud.bigquery.schema import SchemaField
200210
from google.cloud.bigquery.job import LoadJobConfig
@@ -571,6 +581,7 @@ def test_begin_w_alternate_client(self):
571581
]
572582
},
573583
"schemaUpdateOptions": [SchemaUpdateOption.ALLOW_FIELD_ADDITION],
584+
"timeZone": self.TIME_ZONE,
574585
}
575586
RESOURCE["configuration"]["load"] = LOAD_CONFIGURATION
576587
conn1 = make_connection()
@@ -599,6 +610,9 @@ def test_begin_w_alternate_client(self):
599610
config.write_disposition = WriteDisposition.WRITE_TRUNCATE
600611
config.schema_update_options = [SchemaUpdateOption.ALLOW_FIELD_ADDITION]
601612
config.reference_file_schema_uri = "gs://path/to/reference"
613+
614+
config.time_zone = self.TIME_ZONE
615+
602616
with mock.patch(
603617
"google.cloud.bigquery.opentelemetry_tracing._get_final_span_attributes"
604618
) as final_attributes:

tests/unit/job/test_load_config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,22 @@ def test_write_disposition_setter(self):
828828
config._properties["load"]["writeDisposition"], write_disposition
829829
)
830830

831+
def test_time_zone_missing(self):
832+
config = self._get_target_class()()
833+
self.assertIsNone(config.time_zone)
834+
835+
def test_time_zone_hit(self):
836+
time_zone = "UTC"
837+
config = self._get_target_class()()
838+
config._properties["load"]["timeZone"] = time_zone
839+
self.assertEqual(config.time_zone, time_zone)
840+
841+
def test_time_zone_setter(self):
842+
time_zone = "America/New_York"
843+
config = self._get_target_class()()
844+
config.time_zone = time_zone
845+
self.assertEqual(config._properties["load"]["timeZone"], time_zone)
846+
831847
def test_parquet_options_missing(self):
832848
config = self._get_target_class()()
833849
self.assertIsNone(config.parquet_options)

tests/unit/test_external_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@
2626
class TestExternalConfig(unittest.TestCase):
2727
SOURCE_URIS = ["gs://foo", "gs://bar"]
2828

29+
TIME_ZONE = "America/Los_Angeles"
30+
2931
BASE_RESOURCE = {
3032
"sourceFormat": "",
3133
"sourceUris": SOURCE_URIS,
3234
"maxBadRecords": 17,
3335
"autodetect": True,
3436
"ignoreUnknownValues": False,
3537
"compression": "compression",
38+
"timeZone": TIME_ZONE,
3639
}
3740

3841
def test_from_api_repr_base(self):
@@ -79,6 +82,7 @@ def test_to_api_repr_base(self):
7982
ec.connection_id = "path/to/connection"
8083
ec.schema = [schema.SchemaField("full_name", "STRING", mode="REQUIRED")]
8184

85+
ec.time_zone = self.TIME_ZONE
8286
exp_schema = {
8387
"fields": [{"name": "full_name", "type": "STRING", "mode": "REQUIRED"}]
8488
}
@@ -92,6 +96,7 @@ def test_to_api_repr_base(self):
9296
"compression": "compression",
9397
"connectionId": "path/to/connection",
9498
"schema": exp_schema,
99+
"timeZone": self.TIME_ZONE,
95100
}
96101
self.assertEqual(got_resource, exp_resource)
97102

@@ -128,6 +133,8 @@ def _verify_base(self, ec):
128133
self.assertEqual(ec.max_bad_records, 17)
129134
self.assertEqual(ec.source_uris, self.SOURCE_URIS)
130135

136+
self.assertEqual(ec.time_zone, self.TIME_ZONE)
137+
131138
def test_to_api_repr_source_format(self):
132139
ec = external_config.ExternalConfig("CSV")
133140
got = ec.to_api_repr()

0 commit comments

Comments
 (0)