Skip to content
31 changes: 25 additions & 6 deletions weather_mv/loader_pipeline/bq.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import json
import logging
import os
import re
import typing as t
from pprint import pformat

Expand Down Expand Up @@ -130,6 +131,12 @@ def validate_arguments(cls, known_args: argparse.Namespace, pipeline_args: t.Lis
pipeline_options = PipelineOptions(pipeline_args)
pipeline_options_dict = pipeline_options.get_all_options()

if known_args.output_table:
# checking if the output table is in format (<project>.<dataset>.<table>).
output_table_pattern = r'^[\w-]+\.[\w-]+\.[\w-]+$'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the []s necessary? Can it just be? r'^\w+\.\w+\.\w+$' (Please test my hand-written regex :))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

project_ids can contain hypens https://cloud.google.com/resource-manager/docs/creating-managing-projects
\w+ doesn't match when word is like my-project.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that makes sense! Then, would it be [\w\-]+? IIRC, [] and - together are often used to express a range.

if not bool(re.match(output_table_pattern, known_args.output_table)):
raise RuntimeError("output_table is not in correct format (<project>.<dataset_id>.<table_id>). ")

if known_args.area:
assert len(known_args.area) == 4, 'Must specify exactly 4 lat/long values for area: N, W, S, E boundaries.'

Expand All @@ -149,10 +156,20 @@ def validate_arguments(cls, known_args: argparse.Namespace, pipeline_args: t.Lis
logger.info('Region validation completed successfully.')

def __post_init__(self):
"""Initializes Sink by creating a BigQuery table based on user input."""
"""Initializes BigQuery table based on user input."""
self.project, self.dataset_id, self.table_id = self.output_table.split('.')
self.table = None

if self.zarr:
self.xarray_open_dataset_kwargs = self.zarr_kwargs
with open_dataset(self.first_uri, self.xarray_open_dataset_kwargs,

def create_bq_table(self, uri: str) -> str:
Comment thread
alxmrs marked this conversation as resolved.
"""Create a big query table for the first uri. After table is created, subsequent uris are returned."""
# Skip table creation.
if self.table:
return uri

with open_dataset(uri, self.xarray_open_dataset_kwargs,
self.disable_grib_schema_normalization, self.tif_metadata_for_datetime,
is_zarr=self.zarr) as open_ds:
# Define table from user input
Expand All @@ -170,12 +187,13 @@ def __post_init__(self):
if self.dry_run:
logger.debug('Created the BigQuery table with schema...')
logger.debug(f'\n{pformat(table_schema)}')
return
return uri

# Create the table in BigQuery
try:
table = bigquery.Table(self.output_table, schema=table_schema)
self.table = bigquery.Client().create_table(table, exists_ok=True)
return uri
except Exception as e:
logger.error(f'Unable to create table in BigQuery: {e}')
raise
Expand Down Expand Up @@ -243,6 +261,7 @@ def expand(self, paths):
"""Extract rows of variables from data paths into a BigQuery table."""
extracted_rows = (
paths
| 'CreateTable' >> beam.Map(self.create_bq_table)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concern: I am pretty sure this will try to create a BQ table for every element of paths. Remember, self.tables won't refer to the state of the class, since global state is not really a think for parallel steps like this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

| 'PrepareCoordinates' >> beam.FlatMap(self.prepare_coordinates)
| beam.Reshuffle()
| 'ExtractRows' >> beam.FlatMapTuple(self.extract_rows)
Expand All @@ -252,9 +271,9 @@ def expand(self, paths):
(
extracted_rows
| 'WriteToBigQuery' >> WriteToBigQuery(
project=self.table.project,
dataset=self.table.dataset_id,
table=self.table.table_id,
project=self.project,
dataset=self.dataset_id,
table=self.table_id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we wanted to create the BQ table in the pipeline, a simpler solution would be to change this step's disposition: https://beam.apache.org/documentation/io/built-in/google-bigquery/#create-disposition

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, we may want to keep your step since we create an opinionated schema...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alxmrs
i tried putting the create table as a transform in the pipeline but faced many issues

  • I tried the sample transform as you suggested but there is no way to ensure table creation happens before the usual pipeline flow. (apache_beam python sdk doesn't have support for Wait.on which could have made this possible)
  • I also tried stateful processing but as we are windowing out pub sub reads, and a state is discarded when the window is expired, the solution didn't work.

I think there is potential in using the create-disposition flag in WriteToBigQuery. Can you please elaborate on what do you mean by opinionated schema

cc: @mahrsee1997

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you mention it, I agree that using the create disposition is easiest! We need to make sure that we pass in our computed schema (say, from the init) into this transform instead of having it automatically make the schema. Thinking it over now, that's what my concern was about: I wasn't sure if we'd get the schema we wanted if it computed it automatically.

@mahrsee1997 mahrsee1997 Jul 4, 2023

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was surfing web for some other issue & found this. It might be helpful.

https://beam.apache.org/releases/pydoc/current/apache_beam.io.gcp.bigquery.html#schemas

write_disposition=BigQueryDisposition.WRITE_APPEND,
create_disposition=BigQueryDisposition.CREATE_NEVER)
)
Expand Down
8 changes: 6 additions & 2 deletions weather_mv/loader_pipeline/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ def pattern_to_uris(match_pattern: str, is_zarr: bool = False) -> t.Iterable[str

def pipeline(known_args: argparse.Namespace, pipeline_args: t.List[str]) -> None:
all_uris = list(pattern_to_uris(known_args.uris, known_args.zarr))
if not all_uris:
if not all_uris and not known_args.topic:
raise FileNotFoundError(f"File pattern '{known_args.uris}' matched no objects")

# First URI is useful to get an example data shard. It also can be a Zarr path.
known_args.first_uri = next(iter(all_uris))
if all_uris:
known_args.first_uri = next(iter(all_uris))
else:
# If it's a streaming pipeline, it will allow first_uri to be empty.
known_args.first_uri = None

with beam.Pipeline(argv=pipeline_args) as p:
if known_args.topic or known_args.subscription:
Expand Down