Skip to content

Commit 7d31828

Browse files
authored
feat: adds date_format to load job and external config (#2231)
* feat: adds date_format to load job and external config * adds date_format to new to/from_api_repr tests
1 parent d44bf02 commit 7d31828

File tree

5 files changed

+67
-7
lines changed

5 files changed

+67
-7
lines changed

google/cloud/bigquery/external_config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,20 @@ 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 date_format(self) -> Optional[str]:
853+
"""Optional[str]: Format used to parse DATE values. Supports C-style and SQL-style values.
854+
855+
See:
856+
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#ExternalDataConfiguration.FIELDS.date_format
857+
"""
858+
result = self._properties.get("dateFormat")
859+
return typing.cast(str, result)
860+
861+
@date_format.setter
862+
def date_format(self, value: Optional[str]):
863+
self._properties["dateFormat"] = value
864+
851865
@property
852866
def time_zone(self) -> Optional[str]:
853867
"""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
@@ -548,6 +548,19 @@ def source_format(self):
548548
def source_format(self, value):
549549
self._set_sub_prop("sourceFormat", value)
550550

551+
@property
552+
def date_format(self) -> Optional[str]:
553+
"""Optional[str]: Date format used for parsing DATE values.
554+
555+
See:
556+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#JobConfigurationLoad.FIELDS.date_format
557+
"""
558+
return self._get_sub_prop("dateFormat")
559+
560+
@date_format.setter
561+
def date_format(self, value: Optional[str]):
562+
self._set_sub_prop("dateFormat", value)
563+
551564
@property
552565
def time_zone(self) -> Optional[str]:
553566
"""Optional[str]: Default time zone that will apply when parsing timestamp
@@ -903,6 +916,13 @@ def clustering_fields(self):
903916
"""
904917
return self.configuration.clustering_fields
905918

919+
@property
920+
def date_format(self):
921+
"""See
922+
:attr:`google.cloud.bigquery.job.LoadJobConfig.date_format`.
923+
"""
924+
return self.configuration.date_format
925+
906926
@property
907927
def time_zone(self):
908928
"""See

tests/unit/job/test_load.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ def _setUpConstants(self):
3737
self.OUTPUT_BYTES = 23456
3838
self.OUTPUT_ROWS = 345
3939
self.REFERENCE_FILE_SCHEMA_URI = "gs://path/to/reference"
40-
40+
self.DATE_FORMAT = "%Y-%m-%d"
4141
self.TIME_ZONE = "UTC"
4242

4343
def _make_resource(self, started=False, ended=False):
4444
resource = super(TestLoadJob, self)._make_resource(started, ended)
4545
config = resource["configuration"]["load"]
4646
config["sourceUris"] = [self.SOURCE1]
47-
47+
config["dateFormat"] = self.DATE_FORMAT
4848
config["timeZone"] = self.TIME_ZONE
4949
config["destinationTable"] = {
5050
"projectId": self.PROJECT,
@@ -147,7 +147,6 @@ def _verifyResourceProperties(self, job, resource):
147147
)
148148
else:
149149
self.assertIsNone(job.reference_file_schema_uri)
150-
151150
if "destinationEncryptionConfiguration" in config:
152151
self.assertIsNotNone(job.destination_encryption_configuration)
153152
self.assertEqual(
@@ -156,6 +155,10 @@ def _verifyResourceProperties(self, job, resource):
156155
)
157156
else:
158157
self.assertIsNone(job.destination_encryption_configuration)
158+
if "dateFormat" in config:
159+
self.assertEqual(job.date_format, config["dateFormat"])
160+
else:
161+
self.assertIsNone(job.date_format)
159162
if "timeZone" in config:
160163
self.assertEqual(job.time_zone, config["timeZone"])
161164
else:
@@ -202,7 +205,7 @@ def test_ctor(self):
202205
self.assertIsNone(job.clustering_fields)
203206
self.assertIsNone(job.schema_update_options)
204207
self.assertIsNone(job.reference_file_schema_uri)
205-
208+
self.assertIsNone(job.date_format)
206209
self.assertIsNone(job.time_zone)
207210

208211
def test_ctor_w_config(self):
@@ -599,6 +602,7 @@ def test_begin_w_alternate_client(self):
599602
]
600603
},
601604
"schemaUpdateOptions": [SchemaUpdateOption.ALLOW_FIELD_ADDITION],
605+
"dateFormat": self.DATE_FORMAT,
602606
"timeZone": self.TIME_ZONE,
603607
}
604608
RESOURCE["configuration"]["load"] = LOAD_CONFIGURATION
@@ -628,7 +632,7 @@ def test_begin_w_alternate_client(self):
628632
config.write_disposition = WriteDisposition.WRITE_TRUNCATE
629633
config.schema_update_options = [SchemaUpdateOption.ALLOW_FIELD_ADDITION]
630634
config.reference_file_schema_uri = "gs://path/to/reference"
631-
635+
config.date_format = self.DATE_FORMAT
632636
config.time_zone = self.TIME_ZONE
633637

634638
with mock.patch(

tests/unit/job/test_load_config.py

Lines changed: 19 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_date_format_missing(self):
832+
config = self._get_target_class()()
833+
self.assertIsNone(config.date_format)
834+
835+
def test_date_format_hit(self):
836+
date_format = "%Y-%m-%d"
837+
config = self._get_target_class()()
838+
config._properties["load"]["dateFormat"] = date_format
839+
self.assertEqual(config.date_format, date_format)
840+
841+
def test_date_format_setter(self):
842+
date_format = "YYYY/MM/DD"
843+
config = self._get_target_class()()
844+
config.date_format = date_format
845+
self.assertEqual(config._properties["load"]["dateFormat"], date_format)
846+
831847
def test_time_zone_missing(self):
832848
config = self._get_target_class()()
833849
self.assertIsNone(config.time_zone)
@@ -942,6 +958,7 @@ def test_column_name_character_map_none(self):
942958
},
943959
"useAvroLogicalTypes": True,
944960
"writeDisposition": "WRITE_TRUNCATE",
961+
"dateFormat": "%Y-%m-%d",
945962
"timeZone": "America/New_York",
946963
"parquetOptions": {"enableListInference": True},
947964
"columnNameCharacterMap": "V2",
@@ -983,6 +1000,7 @@ def test_from_api_repr(self):
9831000
)
9841001
self.assertTrue(config.use_avro_logical_types)
9851002
self.assertEqual(config.write_disposition, WriteDisposition.WRITE_TRUNCATE)
1003+
self.assertEqual(config.date_format, "%Y-%m-%d")
9861004
self.assertEqual(config.time_zone, "America/New_York")
9871005
self.assertTrue(config.parquet_options.enable_list_inference)
9881006
self.assertEqual(config.column_name_character_map, ColumnNameCharacterMap.V2)
@@ -1017,6 +1035,7 @@ def test_to_api_repr(self):
10171035
)
10181036
config.use_avro_logical_types = True
10191037
config.write_disposition = WriteDisposition.WRITE_TRUNCATE
1038+
config.date_format = "%Y-%m-%d"
10201039
config.time_zone = "America/New_York"
10211040
parquet_options = ParquetOptions()
10221041
parquet_options.enable_list_inference = True

tests/unit/test_external_config.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
class TestExternalConfig(unittest.TestCase):
2727
SOURCE_URIS = ["gs://foo", "gs://bar"]
28-
28+
DATE_FORMAT = "MM/DD/YYYY"
2929
TIME_ZONE = "America/Los_Angeles"
3030

3131
BASE_RESOURCE = {
@@ -35,6 +35,7 @@ class TestExternalConfig(unittest.TestCase):
3535
"autodetect": True,
3636
"ignoreUnknownValues": False,
3737
"compression": "compression",
38+
"dateFormat": DATE_FORMAT,
3839
"timeZone": TIME_ZONE,
3940
}
4041

@@ -82,6 +83,7 @@ def test_to_api_repr_base(self):
8283
ec.connection_id = "path/to/connection"
8384
ec.schema = [schema.SchemaField("full_name", "STRING", mode="REQUIRED")]
8485

86+
ec.date_format = self.DATE_FORMAT
8587
ec.time_zone = self.TIME_ZONE
8688
exp_schema = {
8789
"fields": [{"name": "full_name", "type": "STRING", "mode": "REQUIRED"}]
@@ -96,6 +98,7 @@ def test_to_api_repr_base(self):
9698
"compression": "compression",
9799
"connectionId": "path/to/connection",
98100
"schema": exp_schema,
101+
"dateFormat": self.DATE_FORMAT,
99102
"timeZone": self.TIME_ZONE,
100103
}
101104
self.assertEqual(got_resource, exp_resource)
@@ -132,7 +135,7 @@ def _verify_base(self, ec):
132135
self.assertEqual(ec.ignore_unknown_values, False)
133136
self.assertEqual(ec.max_bad_records, 17)
134137
self.assertEqual(ec.source_uris, self.SOURCE_URIS)
135-
138+
self.assertEqual(ec.date_format, self.DATE_FORMAT)
136139
self.assertEqual(ec.time_zone, self.TIME_ZONE)
137140

138141
def test_to_api_repr_source_format(self):

0 commit comments

Comments
 (0)