This is an automated email from the ASF dual-hosted git repository.
turbaszek pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/master by this push:
new 3994030 Refactor BigQuery operators (#8858)
3994030 is described below
commit 3994030ea678727daaf9c2bfed0ca94a096f8d2a
Author: Tomek Urbaszek <[email protected]>
AuthorDate: Tue May 26 10:03:23 2020 +0200
Refactor BigQuery operators (#8858)
* Refactor BigQueryCreateEmptyTableOperator
* Refactor BigQueryCreateExternalTableOperator
* Refactor BigQueryDeleteDatasetOperator
* Refactor BigQueryCreateEmptyDatasetOperator
* Refactor BigQueryGetDataOperator
* BigQueryGetDatasetTablesOperator
* Refactor BigQueryPatchDatasetOperator
* Refactor BigQueryUpdateDatasetOperator
* Refactor BigQueryDeleteTableOperator
* Refactor BigQueryUpsertTableOperator
* Apply cr suggestions
* fixup! Apply cr suggestions
---
airflow/providers/google/cloud/hooks/bigquery.py | 44 +-
.../providers/google/cloud/operators/bigquery.py | 483 ++++++++++++---------
.../providers/google/cloud/hooks/test_bigquery.py | 2 +-
.../google/cloud/operators/test_bigquery.py | 100 ++---
4 files changed, 363 insertions(+), 266 deletions(-)
diff --git a/airflow/providers/google/cloud/hooks/bigquery.py
b/airflow/providers/google/cloud/hooks/bigquery.py
index 73cba36..6155bc4 100644
--- a/airflow/providers/google/cloud/hooks/bigquery.py
+++ b/airflow/providers/google/cloud/hooks/bigquery.py
@@ -233,6 +233,7 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
retry: Optional[Retry] = DEFAULT_RETRY,
num_retries: Optional[int] = None,
location: Optional[str] = None,
+ exists_ok: bool = True
) -> Table:
"""
Creates a new, empty table in the dataset.
@@ -293,6 +294,8 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
:type encryption_configuration: dict
:param num_retries: Maximum number of retries in case of connection
problems.
:type num_retries: int
+ :param exists_ok: If ``True``, ignore "already exists" errors when
creating the table.
+ :type exists_ok: bool
:return: Created table
"""
if num_retries:
@@ -333,16 +336,19 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
table = Table.from_api_repr(table_resource)
return self.get_client(project_id=project_id,
location=location).create_table(
table=table,
- exists_ok=True,
+ exists_ok=exists_ok,
retry=retry
)
@GoogleBaseHook.fallback_to_default_project_id
- def create_empty_dataset(self,
- dataset_id: Optional[str] = None,
- project_id: Optional[str] = None,
- location: Optional[str] = None,
- dataset_reference: Optional[Dict[str, Any]] =
None) -> None:
+ def create_empty_dataset(
+ self,
+ dataset_id: Optional[str] = None,
+ project_id: Optional[str] = None,
+ location: Optional[str] = None,
+ dataset_reference: Optional[Dict[str, Any]] = None,
+ exists_ok: bool = True,
+ ) -> None:
"""
Create a new empty dataset:
https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets/insert
@@ -358,6 +364,8 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
:param dataset_reference: Dataset reference that could be provided
with request body. More info:
https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets#resource
:type dataset_reference: dict
+ :param exists_ok: If ``True``, ignore "already exists" errors when
creating the DATASET.
+ :type exists_ok: bool
"""
dataset_reference = dataset_reference or {"datasetReference": {}}
@@ -388,8 +396,10 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
if location:
dataset_reference["location"] = dataset_reference.get("location",
location)
- dataset = Dataset.from_api_repr(dataset_reference)
- self.get_client(location=location).create_dataset(dataset=dataset,
exists_ok=True)
+ dataset: Dataset = Dataset.from_api_repr(dataset_reference)
+ self.log.info('Creating dataset: %s in project: %s ',
dataset.dataset_id, dataset.project)
+ self.get_client(location=location).create_dataset(dataset=dataset,
exists_ok=exists_ok)
+ self.log.info('Dataset created successfully.')
@GoogleBaseHook.fallback_to_default_project_id
def get_dataset_tables(
@@ -397,7 +407,6 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
dataset_id: str,
project_id: Optional[str] = None,
max_results: Optional[int] = None,
- page_token: Optional[str] = None,
retry: Retry = DEFAULT_RETRY,
) -> List[Dict[str, Any]]:
"""
@@ -413,19 +422,17 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
:type project_id: str
:param max_results: (Optional) the maximum number of tables to return.
:type max_results: int
- :param page_token: (Optional) page token, returned from a previous
call,
- identifying the result set.
- :type page_token: str
:param retry: How to retry the RPC.
:type retry: google.api_core.retry.Retry
:return: List of tables associated with the dataset.
"""
+ self.log.info('Start getting tables list from dataset: %s.%s',
project_id, dataset_id)
tables = self.get_client().list_tables(
dataset=DatasetReference(project=project_id,
dataset_id=dataset_id),
max_results=max_results,
- page_token=page_token,
retry=retry,
)
+ # Convert to a list (consumes all values)
return [t.reference.to_api_repr() for t in tables]
@GoogleBaseHook.fallback_to_default_project_id
@@ -480,7 +487,7 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
project_id: Optional[str] = None,
) -> None:
"""
- Creates a new external table in the dataset with the data in Google
+ Creates a new external table in the dataset with the data from Google
Cloud Storage. See here:
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#resource
@@ -619,7 +626,12 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
table.encryption_configuration =
EncryptionConfiguration.from_api_repr(encryption_configuration)
self.log.info('Creating external table: %s',
external_project_dataset_table)
- self.create_empty_table(table_resource=table.to_api_repr(),
project_id=project_id, location=location)
+ self.create_empty_table(
+ table_resource=table.to_api_repr(),
+ project_id=project_id,
+ location=location,
+ exists_ok=True
+ )
self.log.info('External table created successfully: %s',
external_project_dataset_table)
@GoogleBaseHook.fallback_to_default_project_id
@@ -894,6 +906,7 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
if value and not spec_value:
dataset_resource["datasetReference"][key] = value
+ self.log.info('Start updating dataset')
dataset = self.get_client(project_id=project_id).update_dataset(
dataset=Dataset.from_api_repr(dataset_resource),
fields=fields,
@@ -937,6 +950,7 @@ class BigQueryHook(GoogleBaseHook, DbApiHook):
service = self.get_service()
dataset_project_id = project_id or self.project_id
+ self.log.info('Start patching dataset: %s:%s', dataset_project_id,
dataset_id)
dataset = (
service.datasets() # pylint: disable=no-member
.patch(
diff --git a/airflow/providers/google/cloud/operators/bigquery.py
b/airflow/providers/google/cloud/operators/bigquery.py
index b93b757..8d36d25 100644
--- a/airflow/providers/google/cloud/operators/bigquery.py
+++ b/airflow/providers/google/cloud/operators/bigquery.py
@@ -26,7 +26,8 @@ import warnings
from typing import Any, Dict, Iterable, List, Optional, SupportsAbs, Union
import attr
-from googleapiclient.errors import HttpError
+from google.api_core.exceptions import Conflict
+from google.cloud.bigquery import TableReference
from airflow.exceptions import AirflowException
from airflow.models import BaseOperator, BaseOperatorLink
@@ -36,7 +37,7 @@ from airflow.providers.google.cloud.hooks.bigquery import
BigQueryHook
from airflow.providers.google.cloud.hooks.gcs import GCSHook, _parse_gcs_url
from airflow.utils.decorators import apply_defaults
-BIGQUERY_JOB_DETAILS_LINK_FMT =
'https://console.cloud.google.com/bigquery?j={job_id}'
+BIGQUERY_JOB_DETAILS_LINK_FMT =
"https://console.cloud.google.com/bigquery?j={job_id}"
_DEPRECATION_MSG = "The bigquery_conn_id parameter has been deprecated. " \
"You should pass the gcp_conn_id parameter."
@@ -72,7 +73,7 @@ class BigQueryConsoleIndexableLink(BaseOperatorLink):
@property
def name(self) -> str:
- return 'BigQuery Console #{index}'.format(index=self.index + 1)
+ return f'BigQuery Console #{self.index + 1}'
def get_link(self, operator, dttm):
ti = TaskInstance(task=operator, execution_date=dttm)
@@ -343,21 +344,23 @@ class BigQueryGetDataOperator(BaseOperator):
:param location: The location used for the operation.
:type location: str
"""
- template_fields = ('dataset_id', 'table_id', 'max_results')
+ template_fields = ('dataset_id', 'table_id', 'max_results',
'selected_fields')
ui_color = BigQueryUIColors.QUERY.value
@apply_defaults
- def __init__(self,
- dataset_id: str,
- table_id: str,
- max_results: int = 100,
- selected_fields: Optional[str] = None,
- gcp_conn_id: str = 'google_cloud_default',
- bigquery_conn_id: Optional[str] = None,
- delegate_to: Optional[str] = None,
- location: Optional[str] = None,
- *args,
- **kwargs) -> None:
+ def __init__(
+ self,
+ dataset_id: str,
+ table_id: str,
+ max_results: int = 100,
+ selected_fields: Optional[str] = None,
+ gcp_conn_id: str = 'google_cloud_default',
+ bigquery_conn_id: Optional[str] = None,
+ delegate_to: Optional[str] = None,
+ location: Optional[str] = None,
+ *args,
+ **kwargs
+ ) -> None:
super().__init__(*args, **kwargs)
if bigquery_conn_id:
@@ -375,8 +378,7 @@ class BigQueryGetDataOperator(BaseOperator):
self.location = location
def execute(self, context):
- self.log.info('Fetching Data from:')
- self.log.info('Dataset: %s ; Table: %s ; Max Results: %s',
+ self.log.info('Fetching Data from %s.%s max results: %s',
self.dataset_id, self.table_id, self.max_results)
hook = BigQueryHook(
@@ -392,7 +394,7 @@ class BigQueryGetDataOperator(BaseOperator):
location=self.location
)
- self.log.info('Total Extracted rows: %s', len(rows))
+ self.log.info('Total extracted rows: %s', len(rows))
table_data = [row.values() for row in rows]
return table_data
@@ -647,6 +649,10 @@ class BigQueryCreateEmptyTableOperator(BaseOperator):
:type dataset_id: str
:param table_id: The Name of the table to be created. (templated)
:type table_id: str
+ :param table_resource: Table resource as described in documentation:
+ https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#Table
+ If provided all other parameters are ignored.
+ :type table_resource: Dict[str, Any]
:param schema_fields: If set, the schema field list as defined here:
https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs#configuration.load.schema
@@ -741,28 +747,38 @@ class BigQueryCreateEmptyTableOperator(BaseOperator):
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#clustering.fields
:type cluster_fields: list
"""
- template_fields = ('dataset_id', 'table_id', 'project_id',
- 'gcs_schema_object', 'labels', 'view')
+ template_fields = (
+ 'dataset_id',
+ 'table_id',
+ 'project_id',
+ 'gcs_schema_object',
+ 'labels',
+ 'view'
+ )
+
ui_color = BigQueryUIColors.TABLE.value
# pylint: disable=too-many-arguments
@apply_defaults
- def __init__(self,
- dataset_id: str,
- table_id: str,
- project_id: Optional[str] = None,
- schema_fields: Optional[List] = None,
- gcs_schema_object: Optional[str] = None,
- time_partitioning: Optional[Dict] = None,
- bigquery_conn_id: str = 'google_cloud_default',
- google_cloud_storage_conn_id: str = 'google_cloud_default',
- delegate_to: Optional[str] = None,
- labels: Optional[Dict] = None,
- view: Optional[Dict] = None,
- encryption_configuration: Optional[Dict] = None,
- location: Optional[str] = None,
- cluster_fields: Optional[List[str]] = None,
- *args, **kwargs) -> None:
+ def __init__(
+ self,
+ dataset_id: str,
+ table_id: str,
+ table_resource: Optional[Dict[str, Any]] = None,
+ project_id: Optional[str] = None,
+ schema_fields: Optional[List] = None,
+ gcs_schema_object: Optional[str] = None,
+ time_partitioning: Optional[Dict] = None,
+ bigquery_conn_id: str = 'google_cloud_default',
+ google_cloud_storage_conn_id: str = 'google_cloud_default',
+ delegate_to: Optional[str] = None,
+ labels: Optional[Dict] = None,
+ view: Optional[Dict] = None,
+ encryption_configuration: Optional[Dict] = None,
+ location: Optional[str] = None,
+ cluster_fields: Optional[List[str]] = None,
+ *args, **kwargs
+ ) -> None:
super().__init__(*args, **kwargs)
self.project_id = project_id
@@ -779,16 +795,17 @@ class BigQueryCreateEmptyTableOperator(BaseOperator):
self.encryption_configuration = encryption_configuration
self.location = location
self.cluster_fields = cluster_fields
+ self.table_resource = table_resource
def execute(self, context):
- bq_hook = BigQueryHook(gcp_conn_id=self.bigquery_conn_id,
- delegate_to=self.delegate_to,
- location=self.location)
+ bq_hook = BigQueryHook(
+ gcp_conn_id=self.bigquery_conn_id,
+ delegate_to=self.delegate_to,
+ location=self.location
+ )
if not self.schema_fields and self.gcs_schema_object:
-
gcs_bucket, gcs_object = _parse_gcs_url(self.gcs_schema_object)
-
gcs_hook = GCSHook(
google_cloud_storage_conn_id=self.google_cloud_storage_conn_id,
delegate_to=self.delegate_to)
@@ -799,9 +816,8 @@ class BigQueryCreateEmptyTableOperator(BaseOperator):
schema_fields = self.schema_fields
try:
- self.log.info('Creating Table %s:%s.%s',
- self.project_id, self.dataset_id, self.table_id)
- bq_hook.create_empty_table(
+ self.log.info('Creating table')
+ table = bq_hook.create_empty_table(
project_id=self.project_id,
dataset_id=self.dataset_id,
table_id=self.table_id,
@@ -810,22 +826,20 @@ class BigQueryCreateEmptyTableOperator(BaseOperator):
cluster_fields=self.cluster_fields,
labels=self.labels,
view=self.view,
- encryption_configuration=self.encryption_configuration
+ encryption_configuration=self.encryption_configuration,
+ table_resource=self.table_resource,
+ exists_ok=False,
)
- self.log.info('Table created successfully: %s:%s.%s',
- self.project_id, self.dataset_id, self.table_id)
- except HttpError as err:
- if err.resp.status != 409:
- raise
- else:
- self.log.info('Table %s:%s.%s already exists.',
self.project_id,
- self.dataset_id, self.table_id)
+ self.log.info('Table %s.%s.%s created successfully',
+ table.project, table.dataset_id, table.table_id)
+ except Conflict:
+ self.log.info('Table %s.%s already exists.', self.dataset_id,
self.table_id)
# pylint: disable=too-many-instance-attributes
class BigQueryCreateExternalTableOperator(BaseOperator):
"""
- Creates a new external table in the dataset with the data in Google Cloud
+ Creates a new external table in the dataset with the data from Google Cloud
Storage.
The schema to be used for the BigQuery table may be specified in one of
@@ -836,8 +850,7 @@ class BigQueryCreateExternalTableOperator(BaseOperator):
:param bucket: The bucket to point the external table to. (templated)
:type bucket: str
:param source_objects: List of Google Cloud Storage URIs to point
- table to. (templated)
- If source_format is 'DATASTORE_BACKUP', the list must only contain a
single URI.
+ table to. If source_format is 'DATASTORE_BACKUP', the list must only
contain a single URI.
:type source_objects: list
:param destination_project_dataset_table: The dotted
``(<project>.)<dataset>.<table>``
BigQuery table to load data into (templated). If ``<project>`` is not
included,
@@ -852,6 +865,10 @@ class BigQueryCreateExternalTableOperator(BaseOperator):
{"name": "salary", "type": "INTEGER", "mode":
"NULLABLE"}]
Should not be set when source_format is 'DATASTORE_BACKUP'.
+ :param table_resource: Table resource as described in documentation:
+ https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#Table
+ If provided all other parameters are ignored. External schema from
object will be resolved.
+ :type table_resource: Dict[str, Any]
:type schema_fields: list
:param schema_object: If set, a GCS object path pointing to a .json file
that
contains the schema for the table. (templated)
@@ -906,34 +923,43 @@ class BigQueryCreateExternalTableOperator(BaseOperator):
:param location: The location used for the operation.
:type location: str
"""
- template_fields = ('bucket', 'source_objects',
- 'schema_object', 'destination_project_dataset_table',
'labels')
+ template_fields = (
+ 'bucket',
+ 'source_objects',
+ 'schema_object',
+ 'destination_project_dataset_table',
+ 'labels',
+ 'table_resource',
+ )
ui_color = BigQueryUIColors.TABLE.value
- # pylint: disable=too-many-arguments
+ # pylint: disable=too-many-arguments,too-many-locals
@apply_defaults
- def __init__(self,
- bucket: str,
- source_objects: List,
- destination_project_dataset_table: str,
- schema_fields: Optional[List] = None,
- schema_object: Optional[str] = None,
- source_format: str = 'CSV',
- compression: str = 'NONE',
- skip_leading_rows: int = 0,
- field_delimiter: str = ',',
- max_bad_records: int = 0,
- quote_character: Optional[str] = None,
- allow_quoted_newlines: bool = False,
- allow_jagged_rows: bool = False,
- bigquery_conn_id: str = 'google_cloud_default',
- google_cloud_storage_conn_id: str = 'google_cloud_default',
- delegate_to: Optional[str] = None,
- src_fmt_configs: Optional[dict] = None,
- labels: Optional[Dict] = None,
- encryption_configuration: Optional[Dict] = None,
- location: Optional[str] = None,
- *args, **kwargs) -> None:
+ def __init__(
+ self,
+ bucket: str,
+ source_objects: List,
+ destination_project_dataset_table: str,
+ table_resource: Optional[Dict[str, Any]] = None,
+ schema_fields: Optional[List] = None,
+ schema_object: Optional[str] = None,
+ source_format: str = 'CSV',
+ compression: str = 'NONE',
+ skip_leading_rows: int = 0,
+ field_delimiter: str = ',',
+ max_bad_records: int = 0,
+ quote_character: Optional[str] = None,
+ allow_quoted_newlines: bool = False,
+ allow_jagged_rows: bool = False,
+ bigquery_conn_id: str = 'google_cloud_default',
+ google_cloud_storage_conn_id: str = 'google_cloud_default',
+ delegate_to: Optional[str] = None,
+ src_fmt_configs: Optional[dict] = None,
+ labels: Optional[Dict] = None,
+ encryption_configuration: Optional[Dict] = None,
+ location: Optional[str] = None,
+ *args, **kwargs
+ ) -> None:
super().__init__(*args, **kwargs)
# GCS config
@@ -942,6 +968,34 @@ class BigQueryCreateExternalTableOperator(BaseOperator):
self.schema_object = schema_object
# BQ config
+ kwargs_passed = any([
+ destination_project_dataset_table,
+ schema_fields,
+ source_format,
+ compression,
+ skip_leading_rows,
+ field_delimiter,
+ max_bad_records,
+ quote_character,
+ allow_quoted_newlines,
+ allow_jagged_rows,
+ src_fmt_configs,
+ labels,
+ encryption_configuration,
+ ])
+
+ if not table_resource:
+ warnings.warn(
+ "Passing table parameters via keywords arguments will be
deprecated. "
+ "Please use provide table definition using `table_resource`
parameter."
+ "You can still use external `schema_object`. ",
+ DeprecationWarning, stacklevel=2
+ )
+
+ if table_resource and kwargs_passed:
+ raise ValueError("You provided both `table_resource` and exclusive
keywords arguments.")
+
+ self.table_resource = table_resource
self.destination_project_dataset_table =
destination_project_dataset_table
self.schema_fields = schema_fields
self.source_format = source_format
@@ -957,30 +1011,41 @@ class BigQueryCreateExternalTableOperator(BaseOperator):
self.google_cloud_storage_conn_id = google_cloud_storage_conn_id
self.delegate_to = delegate_to
- self.src_fmt_configs = src_fmt_configs if src_fmt_configs is not None
else dict()
+ self.src_fmt_configs = src_fmt_configs or dict()
self.labels = labels
self.encryption_configuration = encryption_configuration
self.location = location
def execute(self, context):
- bq_hook = BigQueryHook(gcp_conn_id=self.bigquery_conn_id,
- delegate_to=self.delegate_to,
- location=self.location)
+ bq_hook = BigQueryHook(
+ gcp_conn_id=self.bigquery_conn_id,
+ delegate_to=self.delegate_to,
+ location=self.location
+ )
if not self.schema_fields and self.schema_object and
self.source_format != 'DATASTORE_BACKUP':
gcs_hook = GCSHook(
google_cloud_storage_conn_id=self.google_cloud_storage_conn_id,
delegate_to=self.delegate_to)
- schema_fields = json.loads(gcs_hook.download(
- self.bucket,
- self.schema_object).decode("utf-8"))
+ schema_object = gcs_hook.download(self.bucket, self.schema_object)
+ schema_fields = json.loads(schema_object.decode("utf-8"))
else:
schema_fields = self.schema_fields
- source_uris = ['gs://{}/{}'.format(self.bucket, source_object)
- for source_object in self.source_objects]
+ if schema_fields and self.table_resource:
+ self.table_resource["externalDataConfiguration"]["schema"] =
schema_fields
+
+ if self.table_resource:
+ tab_ref =
TableReference.from_string(self.destination_project_dataset_table)
+ bq_hook.create_empty_table(
+ table_resource=self.table_resource,
+ project_id=tab_ref.project,
+ table_id=tab_ref.table_id,
+ dataset_id=tab_ref.dataset_id,
+ )
+ else:
+ source_uris = [f"gs://{self.bucket}/{source_object}" for
source_object in self.source_objects]
- try:
bq_hook.create_external_table(
external_project_dataset_table=self.destination_project_dataset_table,
schema_fields=schema_fields,
@@ -997,9 +1062,6 @@ class BigQueryCreateExternalTableOperator(BaseOperator):
labels=self.labels,
encryption_configuration=self.encryption_configuration
)
- except HttpError as err:
- if err.resp.status != 409:
- raise
class BigQueryDeleteDatasetOperator(BaseOperator):
@@ -1037,14 +1099,16 @@ class BigQueryDeleteDatasetOperator(BaseOperator):
ui_color = BigQueryUIColors.DATASET.value
@apply_defaults
- def __init__(self,
- dataset_id: str,
- project_id: Optional[str] = None,
- delete_contents: bool = False,
- gcp_conn_id: str = 'google_cloud_default',
- bigquery_conn_id: Optional[str] = None,
- delegate_to: Optional[str] = None,
- *args, **kwargs) -> None:
+ def __init__(
+ self,
+ dataset_id: str,
+ project_id: Optional[str] = None,
+ delete_contents: bool = False,
+ gcp_conn_id: str = 'google_cloud_default',
+ bigquery_conn_id: Optional[str] = None,
+ delegate_to: Optional[str] = None,
+ *args, **kwargs
+ ) -> None:
if bigquery_conn_id:
warnings.warn(
"The bigquery_conn_id parameter has been deprecated. You
should pass "
@@ -1062,8 +1126,10 @@ class BigQueryDeleteDatasetOperator(BaseOperator):
def execute(self, context):
self.log.info('Dataset id: %s Project id: %s', self.dataset_id,
self.project_id)
- bq_hook = BigQueryHook(gcp_conn_id=self.gcp_conn_id,
- delegate_to=self.delegate_to)
+ bq_hook = BigQueryHook(
+ gcp_conn_id=self.gcp_conn_id,
+ delegate_to=self.delegate_to
+ )
bq_hook.delete_dataset(
project_id=self.project_id,
@@ -1074,17 +1140,14 @@ class BigQueryDeleteDatasetOperator(BaseOperator):
class BigQueryCreateEmptyDatasetOperator(BaseOperator):
"""
- This operator is used to create new dataset for your Project in Big query.
+ This operator is used to create new dataset for your Project in BigQuery.
https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets#resource
:param project_id: The name of the project where we want to create the
dataset.
- Don't need to provide, if projectId in dataset_reference.
:type project_id: str
- :param dataset_id: The id of dataset. Don't need to provide,
- if datasetId in dataset_reference.
+ :param dataset_id: The id of dataset. Don't need to provide, if datasetId
in dataset_reference.
:type dataset_id: str
- :param location: (Optional) The geographic location where the dataset
should reside.
- There is no default value but the dataset will be created in US if
nothing is provided.
+ :param location: The geographic location where the dataset should reside.
:type location: str
:param dataset_reference: Dataset reference that could be provided with
request body.
More info:
@@ -1108,12 +1171,12 @@ class BigQueryCreateEmptyDatasetOperator(BaseOperator):
:type location: str
"""
- template_fields = ('dataset_id', 'project_id')
+ template_fields = ('dataset_id', 'project_id', 'dataset_reference')
ui_color = BigQueryUIColors.DATASET.value
@apply_defaults
def __init__(self,
- dataset_id: str,
+ dataset_id: Optional[str] = None,
project_id: Optional[str] = None,
dataset_reference: Optional[Dict] = None,
location: Optional[str] = None,
@@ -1138,24 +1201,23 @@ class BigQueryCreateEmptyDatasetOperator(BaseOperator):
super().__init__(*args, **kwargs)
def execute(self, context):
- self.log.info('Dataset id: %s Project id: %s', self.dataset_id,
self.project_id)
-
- bq_hook = BigQueryHook(gcp_conn_id=self.gcp_conn_id,
- delegate_to=self.delegate_to,
- location=self.location)
+ bq_hook = BigQueryHook(
+ gcp_conn_id=self.gcp_conn_id,
+ delegate_to=self.delegate_to,
+ location=self.location
+ )
try:
- self.log.info('Creating Dataset: %s in project: %s ',
self.dataset_id, self.project_id)
bq_hook.create_empty_dataset(
project_id=self.project_id,
dataset_id=self.dataset_id,
dataset_reference=self.dataset_reference,
- location=self.location)
- self.log.info('Dataset created successfully.')
- except HttpError as err:
- if err.resp.status != 409:
- raise
- self.log.info('Dataset %s already exists.', self.dataset_id)
+ location=self.location,
+ exists_ok=False,
+ )
+ except Conflict:
+ dataset_id = self.dataset_reference.get("datasetReference",
{}).get("datasetId", self.dataset_id)
+ self.log.info('Dataset %s already exists.', dataset_id)
class BigQueryGetDatasetOperator(BaseOperator):
@@ -1213,50 +1275,44 @@ class BigQueryGetDatasetTablesOperator(BaseOperator):
:type project_id: str
:param max_results: (Optional) the maximum number of tables to return.
:type max_results: int
- :param page_token: (Optional) page token, returned from a previous call,
- identifying the result set.
- :type page_token: str
:param gcp_conn_id: (Optional) The connection ID used to connect to Google
Cloud Platform.
:type gcp_conn_id: str
:param delegate_to: (Optional) The account to impersonate, if any.
For this to work, the service account making the request must have
domain-wide
delegation enabled.
:type delegate_to: str
-
- :rtype: dict
- .. seealso::
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables/list#response-body
"""
template_fields = ('dataset_id', 'project_id')
ui_color = BigQueryUIColors.DATASET.value
@apply_defaults
- def __init__(self,
- dataset_id: str,
- project_id: Optional[str] = None,
- max_results: Optional[int] = None,
- page_token: Optional[str] = None,
- gcp_conn_id: Optional[str] = 'google_cloud_default',
- delegate_to: Optional[str] = None,
- *args, **kwargs) -> None:
+ def __init__(
+ self,
+ dataset_id: str,
+ project_id: Optional[str] = None,
+ max_results: Optional[int] = None,
+ gcp_conn_id: Optional[str] = 'google_cloud_default',
+ delegate_to: Optional[str] = None,
+ *args, **kwargs
+ ) -> None:
self.dataset_id = dataset_id
self.project_id = project_id
self.max_results = max_results
- self.page_token = page_token
self.gcp_conn_id = gcp_conn_id
self.delegate_to = delegate_to
super().__init__(*args, **kwargs)
def execute(self, context):
- bq_hook = BigQueryHook(gcp_conn_id=self.gcp_conn_id,
- delegate_to=self.delegate_to)
-
- self.log.info('Start getting tables list from dataset: %s:%s',
self.project_id, self.dataset_id)
+ bq_hook = BigQueryHook(
+ gcp_conn_id=self.gcp_conn_id,
+ delegate_to=self.delegate_to,
+ )
return bq_hook.get_dataset_tables(
dataset_id=self.dataset_id,
project_id=self.project_id,
max_results=self.max_results,
- page_token=self.page_token)
+ )
class BigQueryPatchDatasetOperator(BaseOperator):
@@ -1283,13 +1339,20 @@ class BigQueryPatchDatasetOperator(BaseOperator):
ui_color = BigQueryUIColors.DATASET.value
@apply_defaults
- def __init__(self,
- dataset_id: str,
- dataset_resource: dict,
- project_id: Optional[str] = None,
- gcp_conn_id: str = 'google_cloud_default',
- delegate_to: Optional[str] = None,
- *args, **kwargs) -> None:
+ def __init__(
+ self,
+ dataset_id: str,
+ dataset_resource: dict,
+ project_id: Optional[str] = None,
+ gcp_conn_id: str = 'google_cloud_default',
+ delegate_to: Optional[str] = None,
+ *args, **kwargs,
+ ) -> None:
+
+ warnings.warn(
+ "This operator is deprecated. Please use
BigQueryUpdateDatasetOperator.",
+ DeprecationWarning, stacklevel=3
+ )
self.dataset_id = dataset_id
self.project_id = project_id
self.gcp_conn_id = gcp_conn_id
@@ -1298,22 +1361,25 @@ class BigQueryPatchDatasetOperator(BaseOperator):
super().__init__(*args, **kwargs)
def execute(self, context):
- bq_hook = BigQueryHook(gcp_conn_id=self.gcp_conn_id,
- delegate_to=self.delegate_to)
-
- self.log.info('Start patching dataset: %s:%s', self.project_id,
self.dataset_id)
+ bq_hook = BigQueryHook(
+ gcp_conn_id=self.gcp_conn_id,
+ delegate_to=self.delegate_to,
+ )
return bq_hook.patch_dataset(
dataset_id=self.dataset_id,
dataset_resource=self.dataset_resource,
- project_id=self.project_id)
+ project_id=self.project_id,
+ )
class BigQueryUpdateDatasetOperator(BaseOperator):
"""
This operator is used to update dataset for your Project in BigQuery.
- The update method replaces the entire dataset resource, whereas the patch
- method only replaces fields that are provided in the submitted dataset
resource.
+ Use ``fields`` to specify which fields of dataset to update. If a field
+ is listed in ``fields`` and is ``None`` in dataset, it will be deleted.
+ If no ``fields`` are provided then all fields of provided
``dataset_reources``
+ will be used.
:param dataset_id: The id of dataset. Don't need to provide,
if datasetId in dataset_reference.
@@ -1321,6 +1387,8 @@ class BigQueryUpdateDatasetOperator(BaseOperator):
:param dataset_resource: Dataset resource that will be provided with
request body.
https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets#resource
:type dataset_resource: dict
+ :param fields: The properties of dataset to change (e.g. "friendly_name").
+ :type fields: Sequence[str]
:param project_id: The name of the project where we want to create the
dataset.
Don't need to provide, if projectId in dataset_reference.
:type project_id: str
@@ -1334,27 +1402,30 @@ class BigQueryUpdateDatasetOperator(BaseOperator):
ui_color = BigQueryUIColors.DATASET.value
@apply_defaults
- def __init__(self,
- dataset_resource: dict,
- dataset_id: Optional[str] = None,
- project_id: Optional[str] = None,
- gcp_conn_id: str = 'google_cloud_default',
- delegate_to: Optional[str] = None,
- *args, **kwargs) -> None:
+ def __init__(
+ self,
+ dataset_resource: dict,
+ fields: Optional[List[str]] = None,
+ dataset_id: Optional[str] = None,
+ project_id: Optional[str] = None,
+ gcp_conn_id: str = 'google_cloud_default',
+ delegate_to: Optional[str] = None,
+ *args, **kwargs
+ ) -> None:
self.dataset_id = dataset_id
self.project_id = project_id
+ self.fields = fields
self.gcp_conn_id = gcp_conn_id
self.dataset_resource = dataset_resource
self.delegate_to = delegate_to
super().__init__(*args, **kwargs)
def execute(self, context):
- bq_hook = BigQueryHook(gcp_conn_id=self.gcp_conn_id,
- delegate_to=self.delegate_to)
-
- self.log.info('Start updating dataset: %s:%s', self.project_id,
self.dataset_id)
-
- fields = list(self.dataset_resource.keys())
+ bq_hook = BigQueryHook(
+ gcp_conn_id=self.gcp_conn_id,
+ delegate_to=self.delegate_to,
+ )
+ fields = self.fields or list(self.dataset_resource.keys())
dataset = bq_hook.update_dataset(
dataset_resource=self.dataset_resource,
@@ -1392,15 +1463,17 @@ class BigQueryDeleteTableOperator(BaseOperator):
ui_color = BigQueryUIColors.TABLE.value
@apply_defaults
- def __init__(self,
- deletion_dataset_table: str,
- gcp_conn_id: str = 'google_cloud_default',
- bigquery_conn_id: Optional[str] = None,
- delegate_to: Optional[str] = None,
- ignore_if_missing: bool = False,
- location: Optional[str] = None,
- *args,
- **kwargs) -> None:
+ def __init__(
+ self,
+ deletion_dataset_table: str,
+ gcp_conn_id: str = 'google_cloud_default',
+ bigquery_conn_id: Optional[str] = None,
+ delegate_to: Optional[str] = None,
+ ignore_if_missing: bool = False,
+ location: Optional[str] = None,
+ *args,
+ **kwargs,
+ ) -> None:
super().__init__(*args, **kwargs)
if bigquery_conn_id:
@@ -1417,12 +1490,15 @@ class BigQueryDeleteTableOperator(BaseOperator):
def execute(self, context):
self.log.info('Deleting: %s', self.deletion_dataset_table)
- hook = BigQueryHook(gcp_conn_id=self.gcp_conn_id,
- delegate_to=self.delegate_to,
- location=self.location)
- hook.run_table_delete(
- deletion_dataset_table=self.deletion_dataset_table,
- ignore_if_missing=self.ignore_if_missing)
+ hook = BigQueryHook(
+ gcp_conn_id=self.gcp_conn_id,
+ delegate_to=self.delegate_to,
+ location=self.location
+ )
+ hook.delete_table(
+ table_id=self.deletion_dataset_table,
+ not_found_ok=self.ignore_if_missing
+ )
class BigQueryUpsertTableOperator(BaseOperator):
@@ -1451,20 +1527,22 @@ class BigQueryUpsertTableOperator(BaseOperator):
:param location: The location used for the operation.
:type location: str
"""
- template_fields = ('dataset_id', 'table_resource',)
+ template_fields = ('dataset_id', 'table_resource')
ui_color = BigQueryUIColors.TABLE.value
@apply_defaults
- def __init__(self,
- dataset_id: str,
- table_resource: dict,
- project_id: Optional[str] = None,
- gcp_conn_id: str = 'google_cloud_default',
- bigquery_conn_id: Optional[str] = None,
- delegate_to: Optional[str] = None,
- location: Optional[str] = None,
- *args,
- **kwargs) -> None:
+ def __init__(
+ self,
+ dataset_id: str,
+ table_resource: dict,
+ project_id: Optional[str] = None,
+ gcp_conn_id: str = 'google_cloud_default',
+ bigquery_conn_id: Optional[str] = None,
+ delegate_to: Optional[str] = None,
+ location: Optional[str] = None,
+ *args,
+ **kwargs,
+ ) -> None:
super().__init__(*args, **kwargs)
if bigquery_conn_id:
@@ -1482,10 +1560,13 @@ class BigQueryUpsertTableOperator(BaseOperator):
def execute(self, context):
self.log.info('Upserting Dataset: %s with table_resource: %s',
self.dataset_id, self.table_resource)
- hook = BigQueryHook(bigquery_conn_id=self.gcp_conn_id,
- delegate_to=self.delegate_to,
- location=self.location)
+ hook = BigQueryHook(
+ bigquery_conn_id=self.gcp_conn_id,
+ delegate_to=self.delegate_to,
+ location=self.location,
+ )
hook.run_table_upsert(
dataset_id=self.dataset_id,
table_resource=self.table_resource,
- project_id=self.project_id)
+ project_id=self.project_id,
+ )
diff --git a/tests/providers/google/cloud/hooks/test_bigquery.py
b/tests/providers/google/cloud/hooks/test_bigquery.py
index bdb9c15..8c34690 100644
--- a/tests/providers/google/cloud/hooks/test_bigquery.py
+++ b/tests/providers/google/cloud/hooks/test_bigquery.py
@@ -891,7 +891,6 @@ class TestTableOperations(_BigQueryBaseTestClass):
mock_client.return_value.list_tables.assert_called_once_with(
dataset=dataset_reference,
max_results=None,
- page_token=None,
retry=DEFAULT_RETRY,
)
for res, exp in zip(result, table_list):
@@ -1598,6 +1597,7 @@ class TestBigQueryWithKMS(_BigQueryBaseTestClass):
table_resource=body,
project_id=PROJECT_ID,
location=None,
+ exists_ok=True,
)
@mock.patch("airflow.providers.google.cloud.hooks.bigquery.BigQueryHook.insert_job")
diff --git a/tests/providers/google/cloud/operators/test_bigquery.py
b/tests/providers/google/cloud/operators/test_bigquery.py
index 8a019bb..6c032eb 100644
--- a/tests/providers/google/cloud/operators/test_bigquery.py
+++ b/tests/providers/google/cloud/operators/test_bigquery.py
@@ -65,14 +65,15 @@ class
TestBigQueryCreateEmptyTableOperator(unittest.TestCase):
@mock.patch('airflow.providers.google.cloud.operators.bigquery.BigQueryHook')
def test_execute(self, mock_hook):
- operator = BigQueryCreateEmptyTableOperator(task_id=TASK_ID,
- dataset_id=TEST_DATASET,
-
project_id=TEST_GCP_PROJECT_ID,
- table_id=TEST_TABLE_ID)
+ operator = BigQueryCreateEmptyTableOperator(
+ task_id=TASK_ID,
+ dataset_id=TEST_DATASET,
+ project_id=TEST_GCP_PROJECT_ID,
+ table_id=TEST_TABLE_ID
+ )
operator.execute(None)
- mock_hook.return_value \
- .create_empty_table \
+ mock_hook.return_value.create_empty_table \
.assert_called_once_with(
dataset_id=TEST_DATASET,
project_id=TEST_GCP_PROJECT_ID,
@@ -82,20 +83,23 @@ class
TestBigQueryCreateEmptyTableOperator(unittest.TestCase):
cluster_fields=None,
labels=None,
view=None,
- encryption_configuration=None
+ encryption_configuration=None,
+ table_resource=None,
+ exists_ok=False,
)
@mock.patch('airflow.providers.google.cloud.operators.bigquery.BigQueryHook')
def test_create_view(self, mock_hook):
- operator = BigQueryCreateEmptyTableOperator(task_id=TASK_ID,
- dataset_id=TEST_DATASET,
-
project_id=TEST_GCP_PROJECT_ID,
- table_id=TEST_TABLE_ID,
- view=VIEW_DEFINITION)
+ operator = BigQueryCreateEmptyTableOperator(
+ task_id=TASK_ID,
+ dataset_id=TEST_DATASET,
+ project_id=TEST_GCP_PROJECT_ID,
+ table_id=TEST_TABLE_ID,
+ view=VIEW_DEFINITION
+ )
operator.execute(None)
- mock_hook.return_value \
- .create_empty_table \
+ mock_hook.return_value.create_empty_table \
.assert_called_once_with(
dataset_id=TEST_DATASET,
project_id=TEST_GCP_PROJECT_ID,
@@ -105,7 +109,9 @@ class
TestBigQueryCreateEmptyTableOperator(unittest.TestCase):
cluster_fields=None,
labels=None,
view=VIEW_DEFINITION,
- encryption_configuration=None
+ encryption_configuration=None,
+ table_resource=None,
+ exists_ok=False,
)
@mock.patch('airflow.providers.google.cloud.operators.bigquery.BigQueryHook')
@@ -133,18 +139,18 @@ class
TestBigQueryCreateEmptyTableOperator(unittest.TestCase):
"field": "date_hired"
}
cluster_fields = ["date_birth"]
- operator = BigQueryCreateEmptyTableOperator(task_id=TASK_ID,
- dataset_id=TEST_DATASET,
-
project_id=TEST_GCP_PROJECT_ID,
- table_id=TEST_TABLE_ID,
-
schema_fields=schema_fields,
-
time_partitioning=time_partitioning,
-
cluster_fields=cluster_fields
- )
+ operator = BigQueryCreateEmptyTableOperator(
+ task_id=TASK_ID,
+ dataset_id=TEST_DATASET,
+ project_id=TEST_GCP_PROJECT_ID,
+ table_id=TEST_TABLE_ID,
+ schema_fields=schema_fields,
+ time_partitioning=time_partitioning,
+ cluster_fields=cluster_fields
+ )
operator.execute(None)
- mock_hook.return_value \
- .create_empty_table \
+ mock_hook.return_value.create_empty_table \
.assert_called_once_with(
dataset_id=TEST_DATASET,
project_id=TEST_GCP_PROJECT_ID,
@@ -154,7 +160,9 @@ class
TestBigQueryCreateEmptyTableOperator(unittest.TestCase):
cluster_fields=cluster_fields,
labels=None,
view=None,
- encryption_configuration=None
+ encryption_configuration=None,
+ table_resource=None,
+ exists_ok=False,
)
@@ -228,14 +236,13 @@ class
TestBigQueryCreateEmptyDatasetOperator(unittest.TestCase):
)
operator.execute(None)
- mock_hook.return_value \
- .create_empty_dataset \
- .assert_called_once_with(
- dataset_id=TEST_DATASET,
- project_id=TEST_GCP_PROJECT_ID,
- location=TEST_DATASET_LOCATION,
- dataset_reference={}
- )
+ mock_hook.return_value.create_empty_dataset.assert_called_once_with(
+ dataset_id=TEST_DATASET,
+ project_id=TEST_GCP_PROJECT_ID,
+ location=TEST_DATASET_LOCATION,
+ dataset_reference={},
+ exists_ok=False,
+ )
class TestBigQueryGetDatasetOperator(unittest.TestCase):
@@ -705,7 +712,7 @@ class TestBigQueryTableDeleteOperator(unittest.TestCase):
@mock.patch('airflow.providers.google.cloud.operators.bigquery.BigQueryHook')
def test_execute(self, mock_hook):
ignore_if_missing = True
- deletion_dataset_table = '{}.{}'.format(TEST_DATASET, TEST_TABLE_ID)
+ deletion_dataset_table = f'{TEST_DATASET}.{TEST_TABLE_ID}'
operator = BigQueryDeleteTableOperator(
task_id=TASK_ID,
@@ -714,12 +721,10 @@ class TestBigQueryTableDeleteOperator(unittest.TestCase):
)
operator.execute(None)
- mock_hook.return_value \
- .run_table_delete \
- .assert_called_once_with(
- deletion_dataset_table=deletion_dataset_table,
- ignore_if_missing=ignore_if_missing
- )
+ mock_hook.return_value.delete_table.assert_called_once_with(
+ table_id=deletion_dataset_table,
+ not_found_ok=ignore_if_missing
+ )
class TestBigQueryGetDatasetTablesOperator(unittest.TestCase):
@@ -733,14 +738,11 @@ class
TestBigQueryGetDatasetTablesOperator(unittest.TestCase):
)
operator.execute(None)
- mock_hook.return_value \
- .get_dataset_tables \
- .assert_called_once_with(
- dataset_id=TEST_DATASET,
- project_id=TEST_GCP_PROJECT_ID,
- max_results=2,
- page_token=None
- )
+ mock_hook.return_value.get_dataset_tables.assert_called_once_with(
+ dataset_id=TEST_DATASET,
+ project_id=TEST_GCP_PROJECT_ID,
+ max_results=2,
+ )
class TestBigQueryConnIdDeprecationWarning(unittest.TestCase):