Skip to content

Commit 1d484cf

Browse files
committed
feat: adds datetime_format as an option
1 parent 7d31828 commit 1d484cf

File tree

5 files changed

+64
-0
lines changed

5 files changed

+64
-0
lines changed

google/cloud/bigquery/external_config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,20 @@ def date_format(self) -> Optional[str]:
862862
def date_format(self, value: Optional[str]):
863863
self._properties["dateFormat"] = value
864864

865+
@property
866+
def datetime_format(self) -> Optional[str]:
867+
"""Optional[str]: Date format used for parsing DATETIME values.
868+
(Valid for CSV and NEWLINE_DELIMITED_JSON)
869+
See:
870+
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#ExternalDataConfiguration.FIELDS.datetime_format
871+
"""
872+
result = self._properties.get("datetimeFormat")
873+
return typing.cast(str, result)
874+
875+
@datetime_format.setter
876+
def datetime_format(self, value: Optional[str]):
877+
self._properties["datetimeFormat"] = value
878+
865879
@property
866880
def time_zone(self) -> Optional[str]:
867881
"""Optional[str]: Time zone used when parsing timestamp values that do not

google/cloud/bigquery/job/load.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,19 @@ def date_format(self) -> Optional[str]:
561561
def date_format(self, value: Optional[str]):
562562
self._set_sub_prop("dateFormat", value)
563563

564+
@property
565+
def datetime_format(self) -> Optional[str]:
566+
"""Optional[str]: Date format used for parsing DATETIME values.
567+
This option is valid for CSV and JSON sources.
568+
See:
569+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#JobConfigurationLoad.FIELDS.datetime_format
570+
"""
571+
return self._get_sub_prop("datetimeFormat")
572+
573+
@datetime_format.setter
574+
def datetime_format(self, value: Optional[str]):
575+
self._set_sub_prop("datetimeFormat", value)
576+
564577
@property
565578
def time_zone(self) -> Optional[str]:
566579
"""Optional[str]: Default time zone that will apply when parsing timestamp
@@ -923,6 +936,13 @@ def date_format(self):
923936
"""
924937
return self.configuration.date_format
925938

939+
@property
940+
def datetime_format(self):
941+
"""See
942+
:attr:`google.cloud.bigquery.job.LoadJobConfig.datetime_format`.
943+
"""
944+
return self.configuration.datetime_format
945+
926946
@property
927947
def time_zone(self):
928948
"""See

tests/unit/job/test_load.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,15 @@ def _setUpConstants(self):
3838
self.OUTPUT_ROWS = 345
3939
self.REFERENCE_FILE_SCHEMA_URI = "gs://path/to/reference"
4040
self.DATE_FORMAT = "%Y-%m-%d"
41+
self.DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
4142
self.TIME_ZONE = "UTC"
4243

4344
def _make_resource(self, started=False, ended=False):
4445
resource = super(TestLoadJob, self)._make_resource(started, ended)
4546
config = resource["configuration"]["load"]
4647
config["sourceUris"] = [self.SOURCE1]
4748
config["dateFormat"] = self.DATE_FORMAT
49+
config["datetimeFormat"] = self.DATETIME_FORMAT
4850
config["timeZone"] = self.TIME_ZONE
4951
config["destinationTable"] = {
5052
"projectId": self.PROJECT,
@@ -159,6 +161,10 @@ def _verifyResourceProperties(self, job, resource):
159161
self.assertEqual(job.date_format, config["dateFormat"])
160162
else:
161163
self.assertIsNone(job.date_format)
164+
if "datetimeFormat" in config:
165+
self.assertEqual(job.datetime_format, config["datetimeFormat"])
166+
else:
167+
self.assertIsNone(job.datetime_format)
162168
if "timeZone" in config:
163169
self.assertEqual(job.time_zone, config["timeZone"])
164170
else:
@@ -206,6 +212,7 @@ def test_ctor(self):
206212
self.assertIsNone(job.schema_update_options)
207213
self.assertIsNone(job.reference_file_schema_uri)
208214
self.assertIsNone(job.date_format)
215+
self.assertIsNone(job.datetime_format)
209216
self.assertIsNone(job.time_zone)
210217

211218
def test_ctor_w_config(self):
@@ -603,6 +610,7 @@ def test_begin_w_alternate_client(self):
603610
},
604611
"schemaUpdateOptions": [SchemaUpdateOption.ALLOW_FIELD_ADDITION],
605612
"dateFormat": self.DATE_FORMAT,
613+
"datetimeFormat": self.DATETIME_FORMAT,
606614
"timeZone": self.TIME_ZONE,
607615
}
608616
RESOURCE["configuration"]["load"] = LOAD_CONFIGURATION
@@ -633,6 +641,7 @@ def test_begin_w_alternate_client(self):
633641
config.schema_update_options = [SchemaUpdateOption.ALLOW_FIELD_ADDITION]
634642
config.reference_file_schema_uri = "gs://path/to/reference"
635643
config.date_format = self.DATE_FORMAT
644+
config.datetime_format = self.DATETIME_FORMAT
636645
config.time_zone = self.TIME_ZONE
637646

638647
with mock.patch(

tests/unit/job/test_load_config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,22 @@ def test_date_format_setter(self):
844844
config.date_format = date_format
845845
self.assertEqual(config._properties["load"]["dateFormat"], date_format)
846846

847+
def test_datetime_format_missing(self):
848+
config = self._get_target_class()()
849+
self.assertIsNone(config.datetime_format)
850+
851+
def test_datetime_format_hit(self):
852+
datetime_format = "%Y-%m-%dT%H:%M:%S"
853+
config = self._get_target_class()()
854+
config._properties["load"]["datetimeFormat"] = datetime_format
855+
self.assertEqual(config.datetime_format, datetime_format)
856+
857+
def test_datetime_format_setter(self):
858+
datetime_format = "YYYY/MM/DD HH24:MI:SS"
859+
config = self._get_target_class()()
860+
config.datetime_format = datetime_format
861+
self.assertEqual(config._properties["load"]["datetimeFormat"], datetime_format)
862+
847863
def test_time_zone_missing(self):
848864
config = self._get_target_class()()
849865
self.assertIsNone(config.time_zone)

tests/unit/test_external_config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
class TestExternalConfig(unittest.TestCase):
2727
SOURCE_URIS = ["gs://foo", "gs://bar"]
2828
DATE_FORMAT = "MM/DD/YYYY"
29+
DATETIME_FORMAT = "MM/DD/YYYY HH24:MI:SS"
2930
TIME_ZONE = "America/Los_Angeles"
3031

3132
BASE_RESOURCE = {
@@ -36,6 +37,7 @@ class TestExternalConfig(unittest.TestCase):
3637
"ignoreUnknownValues": False,
3738
"compression": "compression",
3839
"dateFormat": DATE_FORMAT,
40+
"datetimeFormat": DATETIME_FORMAT,
3941
"timeZone": TIME_ZONE,
4042
}
4143

@@ -84,6 +86,7 @@ def test_to_api_repr_base(self):
8486
ec.schema = [schema.SchemaField("full_name", "STRING", mode="REQUIRED")]
8587

8688
ec.date_format = self.DATE_FORMAT
89+
ec.datetime_format = self.DATETIME_FORMAT
8790
ec.time_zone = self.TIME_ZONE
8891
exp_schema = {
8992
"fields": [{"name": "full_name", "type": "STRING", "mode": "REQUIRED"}]
@@ -99,6 +102,7 @@ def test_to_api_repr_base(self):
99102
"connectionId": "path/to/connection",
100103
"schema": exp_schema,
101104
"dateFormat": self.DATE_FORMAT,
105+
"datetimeFormat": self.DATETIME_FORMAT,
102106
"timeZone": self.TIME_ZONE,
103107
}
104108
self.assertEqual(got_resource, exp_resource)
@@ -136,6 +140,7 @@ def _verify_base(self, ec):
136140
self.assertEqual(ec.max_bad_records, 17)
137141
self.assertEqual(ec.source_uris, self.SOURCE_URIS)
138142
self.assertEqual(ec.date_format, self.DATE_FORMAT)
143+
self.assertEqual(ec.datetime_format, self.DATETIME_FORMAT)
139144
self.assertEqual(ec.time_zone, self.TIME_ZONE)
140145

141146
def test_to_api_repr_source_format(self):

0 commit comments

Comments
 (0)