Guest User

Untitled

a guest
Mar 4th, 2021
385
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 7.92 KB | None | 0 0
  1. import argparse
  2. import datetime
  3. import json
  4. import logging
  5. import hashlib
  6.  
  7. import apache_beam as beam
  8. from apache_beam.options.pipeline_options import PipelineOptions
  9. import apache_beam.transforms.window as window
  10. from apache_beam import pvalue
  11.  
  12.  
  13. class GroupWindowsIntoBatches(beam.PTransform):
  14.     """
  15.    A composite transform that groups Pub/Sub messages based on publish
  16.    time and outputs a list of dictionaries, where each contains one message
  17.    and its publish timestamp.
  18.    """
  19.  
  20.     def __init__(self, window_size):
  21.         # Convert minutes into seconds.
  22.         self.window_size = int(window_size * 60)
  23.  
  24.     def expand(self, pcoll):
  25.         return (
  26.                 pcoll
  27.                 # Assigns window info to each Pub/Sub message based on its
  28.                 # publish timestamp.
  29.                 | "Window into Fixed Intervals" >> beam.WindowInto(window.FixedWindows(self.window_size))
  30.                 | "Add timestamps to messages" >> beam.ParDo(AddTimestamps())
  31.                 | "Add Dummy Key" >> beam.Map(lambda elem: (None, elem))
  32.                 | "Groupby" >> beam.GroupByKey()
  33.                 | "Abandon Dummy Key" >> beam.MapTuple(lambda _, val: val)
  34.         )
  35.  
  36.  
  37. class AddTimestamps(beam.DoFn):
  38.     def process(self, element, publish_time=beam.DoFn.TimestampParam):
  39.         """Processes each incoming windowed element by extracting the Pub/Sub
  40.        message and its publish timestamp into a dictionary. `publish_time`
  41.        defaults to the publish timestamp returned by the Pub/Sub server. It
  42.        is bound to each element by Beam at runtime.
  43.        """
  44.  
  45.         element["publish_time"] = datetime.datetime.utcfromtimestamp(float(publish_time)).strftime(
  46.             "%Y-%m-%d %H:%M:%S.%f")
  47.         yield element
  48.  
  49.  
  50. class WriteBatchesToGCS(beam.DoFn):
  51.     def __init__(self, output_path):
  52.         self.output_path = output_path
  53.  
  54.     def process(self, batch, window=beam.DoFn.WindowParam):
  55.         """Write one batch per file to a Google Cloud Storage bucket. """
  56.  
  57.         ts_format = "%H:%M"
  58.         window_start = window.start.to_utc_datetime().strftime(ts_format)
  59.         window_end = window.end.to_utc_datetime().strftime(ts_format)
  60.         filename = f"{self.output_path}{'-'.join([window_start, window_end])}"
  61.         with beam.io.gcp.gcsio.GcsIO().open(filename=filename, mode="w") as f:
  62.             for element in batch:
  63.                 f.write("{}\n".format(json.dumps(element)).encode("utf-8"))
  64.  
  65.  
  66. class Split(beam.DoFn):
  67.     # These tags will be used to tag the outputs of this DoFn.
  68.     OUTPUT_TAG_BQ = 'BigQuery'
  69.     OUTPUT_TAG_GCS = 'GCS'
  70.  
  71.     def __init__(self, required_fields, rule_value, rule_key, pii_fields):
  72.         self.required_fields = required_fields
  73.         self.rule_value = rule_value
  74.         self.rule_key = rule_key
  75.         self.pii_fields = pii_fields.split(',')
  76.  
  77.     def process(self, element):
  78.         """
  79.        tags the input as it processes the original PCollection, hashes pii
  80.        """
  81.         # load the message as json, hash the PII fields
  82.         element_raw = json.loads(element.decode("utf-8"))
  83.         salt = 'CairoTokyoLondon'
  84.         element = {k:(hashlib.md5(f"{str(v)}{salt}".encode()).hexdigest() if k in self.pii_fields else v) for (k,v) in element_raw.items()}
  85.  
  86.         # check if the message has the expected structure, check the rule
  87.         if set(element.keys()) == set(self.required_fields) and element[self.rule_key] == self.rule_value:
  88.             yield pvalue.TaggedOutput(self.OUTPUT_TAG_BQ, element)
  89.         else:
  90.             yield pvalue.TaggedOutput(self.OUTPUT_TAG_GCS, element)
  91.  
  92.  
  93. def run(input_subscription, output_path_gcs, output_table_bq, output_table_bq_schema, rule_key, rule_value,pii_fields,
  94.         window_size=1.0, pipeline_args=None):
  95.     required_fields = [i.split(':')[0] for i in output_table_bq_schema.split(',')]
  96.     required_fields.remove('publish_time')
  97.     pipeline_options = PipelineOptions(
  98.         pipeline_args, streaming=True, save_main_session=True, direct_running_mode='in_memory', direct_num_workers=2
  99.     )
  100.  
  101.     with beam.Pipeline(options=pipeline_options) as pipeline:
  102.         tagged_lines_result = (pipeline | "Read Pub/Sub" >> beam.io.ReadFromPubSub(subscription=input_subscription)
  103.                                | "Split to BQ and GCS" >> beam.ParDo(
  104.                     Split(required_fields=required_fields, rule_key=rule_key, rule_value=rule_value,
  105.                           pii_fields=pii_fields)).with_outputs(
  106.                     Split.OUTPUT_TAG_BQ,
  107.                     Split.OUTPUT_TAG_GCS))
  108.  
  109.         faulty_messages = tagged_lines_result[Split.OUTPUT_TAG_GCS] | "Window into GCS" >> GroupWindowsIntoBatches(
  110.             window_size) | "Write to GCS" >> beam.ParDo(
  111.             WriteBatchesToGCS(output_path_gcs))
  112.         accepted_messages = tagged_lines_result[Split.OUTPUT_TAG_BQ] | "Window into BQ" >> GroupWindowsIntoBatches(
  113.             window_size) | "FlatMap" >> beam.FlatMap(
  114.             lambda elements: elements) | "Write to BQ" >> beam.io.gcp.bigquery.WriteToBigQuery(table=output_table_bq,
  115.                                                                                                schema=(
  116.                                                                                                    output_table_bq_schema),
  117.                                                                                                write_disposition=beam.io.BigQueryDisposition.WRITE_APPEND,
  118.                                                                                                create_disposition=beam.io.BigQueryDisposition.CREATE_IF_NEEDED)
  119.  
  120.         pipeline.run().wait_until_finish()
  121.  
  122.  
  123. if __name__ == "__main__":  # noqa
  124.     logging.getLogger().setLevel(logging.INFO)
  125.  
  126.     parser = argparse.ArgumentParser()
  127.     parser.add_argument(
  128.         "--input_subscription",
  129.         dest='input_subscription',
  130.         help="The Cloud Pub/Sub subscription to read from.\n"
  131.              '"projects/<PROJECT_NAME>/subscriptions/<SUBSCRIPTION_NAME>".',
  132.     )
  133.     parser.add_argument(
  134.         "--window_size",
  135.         dest='window_size',
  136.         type=float,
  137.         default=1.0,
  138.         help="Output file's window size in number of minutes.",
  139.     )
  140.     parser.add_argument(
  141.         "--output_path_gcs",
  142.         dest='output_path_gcs',
  143.         required=True,
  144.         help="GCS Path of the output file including filename prefix.",
  145.     )
  146.     parser.add_argument(
  147.         "--output_table_bq",
  148.         dest='output_table_bq',
  149.         required=True,
  150.         help="BQ Table for output. Format: <project_id:dataset.table>",
  151.     )
  152.     parser.add_argument(
  153.         "--output_table_bq_schema",
  154.         dest='output_table_bq_schema',
  155.         required=True,
  156.         help="Output BQ Table Schema. Format: <col_name:type, col_name:type>",
  157.     )
  158.     parser.add_argument(
  159.         "--rule_key",
  160.         dest='rule_key',
  161.         required=True,
  162.         help="The key that should have hold <rule_value>. Used for determining whether the message should be accepted",
  163.     )
  164.     parser.add_argument(
  165.         "--rule_value",
  166.         dest='rule_value',
  167.         required=True,
  168.         help="The <rule_key> should hold this value. Used for determining whether the message should be accepted",
  169.     )
  170.     parser.add_argument(
  171.         "--pii_fields",
  172.         dest='pii_fields',
  173.         default='',
  174.         required=False,
  175.         help="The values in these keys will be hashed before loading to GCS or BQ.",
  176.     )
  177.     known_args, pipeline_args = parser.parse_known_args()
  178.  
  179.     run(
  180.         input_subscription=known_args.input_subscription,
  181.         output_path_gcs=known_args.output_path_gcs,
  182.         output_table_bq=known_args.output_table_bq,
  183.         window_size=known_args.window_size,
  184.         output_table_bq_schema=known_args.output_table_bq_schema,
  185.         rule_key=known_args.rule_key,
  186.         rule_value=known_args.rule_value,
  187.         pii_fields = known_args.pii_fields,
  188.         pipeline_args=pipeline_args,
  189.     )
  190.  
Advertisement
Add Comment
Please, Sign In to add comment