"""Snowflake connector class for the Apple Music Streams' tasks.""" from snowflake_connector.etl_connector import SQLLoader from feed_ingestion.common.fact_analytics_sf.base_executor \ import SnowflakeSQLExecutorFA from feed_ingestion.flows.apple_music_streams import config from feed_ingestion.flows.apple_music_streams.utils import \ get_fact_analytics_report from feed_ingestion.flows.apple_music_streams.vendor_accounts import \ get_vendors from feed_ingestion.util.snowflake.errors import JSONParserLoading sql_loader = SQLLoader(__file__) class AppleMusicStreams(SnowflakeSQLExecutorFA): """Helper class to abstract Snowflake operations. This class inherits from SnowflakeSQLExecutor class, which provides basic set of methods. This class extends SnowflakeSQLExecutor with some specific methods, which are useful to encapsulate some flow specific operations. """ @property def feed_name(self): """Name of the feed. Should match dir name of this feed, feed_name in config.py of a feed. Returns: str: Feed name """ return config.feed_name @property def feedid(self): """Id of the feed. Should match dir name of this feed, feed_name in config.py of a feed. Returns: str: Feed name """ return config.feedid @property def staging_raw_table(self): """Name of the staging_raw table for the feed. Returns: str: staging_raw_{feed} table name. """ return config.reports['amStreamsSummary']['staging_raw_table'] @property def library_events_table(self): """Name of the library_events table for the feed. Returns: str: library_events table name. """ return config.reports['amLibraryEvents']['staging_raw_table'] @property def nonroyalty_table(self): """Name of the nonroyalty streams table for the feed. Returns: str: nonroyalty table name. """ return config.reports['amNonRoyaltyStreams']['staging_raw_table'] @property def storeid(self): """Storeid of feed data. Should match dim_store and feed config value. Returns: integer: Feed's storeid. """ return config.store_id def get_staging_raw_table_for_fact_analytics(self, date, licensor): """Name of the staging_raw table for fact_analytics and dim updates. Returns: str: staging_raw_{feed} table name. """ return config.reports[ get_fact_analytics_report(date, licensor)]['staging_raw_table'] def get_query_name(self, query_name, sql_loader, licensor): """Name of the staging_raw table for fact_analytics and dim updates. Returns: str: staging_raw_{feed} table name. """ try: sql_loader.load_query(f'{query_name}_{licensor}') return f'{query_name}_{licensor}' except FileNotFoundError: return query_name def staging_raw_location(self, report_name): """Database and schema where a report's staging_raw table lives. Reports flagged with 'is_consumer_reporting' keep their staging_raw table in the consumer reporting database; everything else stays on the flow's db/schema. Args: report_name (str): Name of the report being ingested. Returns: tuple: (db, schema) for the report's staging_raw table. """ if config.reports[report_name].get('is_consumer_reporting'): return config.consumer_sf['db'], config.consumer_sf['schema'] return self.sf_config['db'], self.sf_config['schema'] def update_dimension_table(self, date, table_name): """Update dimension table with the new data. Args: date (str): Date of the data being process (YYYY-MM-DD). table_name (str): A table to update (corresponding query should be placed in the queries/ folder of the flow). """ sql_loader = SQLLoader(__file__, date=date) params = { 'staging_raw_table': # todo change 'awal' after backfill # it is only amStreamsSummary now self.get_staging_raw_table_for_fact_analytics(date, 'awal'), 'consumer_db': config.consumer_sf['db'], 'consumer_schema': config.consumer_sf['schema'], } for licensor in config.active_licensors: params['{}_vendor_ids'.format(licensor)] = get_vendors( date, licensor, get_fact_analytics_report(date, licensor)) return self._update_dimension_table( date, table_name, sql_loader, **params) def create_temp_staging_raw_table(self, temp_staging_raw_table, **kwargs): """Create a temporary staging raw table. Args: temp_staging_raw_table (str): A table name in Snowflake. kwargs (dict): Custom arguments with report type. """ sql_loader = SQLLoader(__file__, date=kwargs['date']) query_name = config.create_temp_table_query_name.format( report=kwargs['report_name']) query_name = self.get_query_name( query_name, sql_loader, kwargs['licensor']) params = dict( db=self.sf_config['db'], schema=self.sf_config['schema'], table_name=temp_staging_raw_table) self.execute_query(sql_loader, query_name, params) def load_temp_staging_raw_table( self, temp_staging_raw_table, aws, key_dir, **kwargs): """Load temp staging raw table. Args: temp_staging_raw_table (str): A table name in Snowflake. aws (dict): AWS credentials to fill a template of COPY SQL statement. key_dir (str): Custom arguments. """ sql_loader = SQLLoader(__file__, date=kwargs['date']) aws_params = self.get_aws_params() params = dict( db=self.sf_config['db'], schema=self.sf_config['schema'], table_name=temp_staging_raw_table, s3_path=key_dir, file_pattern=kwargs['file_pattern'], on_error_action='SKIP_FILE_{}'.format(kwargs['error_limit']), **aws_params,) return [JSONParserLoading(*e) for e in self.fetchall_query( sql_loader, 'load_temp_table', params)] def load_staging_raw_table( self, date, staging_raw_table, temp_staging_raw_table, **kwargs): """Create a temporary staging raw table. Args: temp_staging_raw_table (str): A table name in Snowflake. kwargs (dict): Custom arguments with report type. """ sql_loader = SQLLoader(__file__, date=date) query_name = config.load_query_name.format( report=kwargs['report_name']) query_name = self.get_query_name( query_name, sql_loader, kwargs['licensor']) params = dict( db=self.sf_config['db'], schema=self.sf_config['schema'], consumer_db=config.consumer_sf['db'], consumer_schema=config.consumer_sf['schema'], staging_raw_table=staging_raw_table, download_date=date, processed_datetime=kwargs['processed_datetime'], filename=kwargs['filename'], temp_table_amcontent=kwargs['temp_table_amcontent'], temp_table_amsubreference=kwargs['temp_table_amsubreference'], vendor=kwargs['vendor'], licensor=kwargs['licensor'], temp_table_name=temp_staging_raw_table, apple_id_mapping_table=kwargs['apple_id_mapping_table']) self.execute_query(sql_loader, query_name, params) def load_fact_data(self, date, **kwargs): """Load matched data into fact_analytics. Args: date (str): Date of the data being process (YYYY-MM-DD). """ sql_loader = SQLLoader(__file__, date=date) licensor = kwargs['licensor'] consumer_db, consumer_schema = self.staging_raw_location( get_fact_analytics_report(date, licensor)) params = dict( db=self.sf_config['db'], schema=self.sf_config['schema'], consumer_db=consumer_db, consumer_schema=consumer_schema, fact_table=self.fact_table, staging_raw_table=self.get_staging_raw_table_for_fact_analytics( date, licensor), reportdate=date, feedid=self.feedid, storeid=self.storeid, vendor_ids=get_vendors( date, licensor, get_fact_analytics_report(date, licensor)), licensor=licensor) query_name = self.get_query_name( 'load_fact_analytics', sql_loader, kwargs['licensor']) self.execute_query(sql_loader, query_name, params) def delete_from_aggregated_skips_and_saves(self, date, licensor): """Delete from aggregated_skips_and_saves. Args: date (str): Date of the data being process (YYYY-MM-DD). licensor (str): Name of the licensor to ingest. """ self.execute_query( sql_loader, 'delete_from_aggregated_skips_and_saves', params=dict( db=self.sf_config['db'], schema=self.sf_config['schema'], reportdate=date, feedid=self.feedid, vendor_ids=get_vendors(date, licensor))) def merge_skips_into_aggregated_skips_and_saves(self, date, licensor): """Merge skips to aggregated_skips_and_saves. Args: date (str): Date of the data being process (YYYY-MM-DD). licensor (str): Name of the licensor to ingest. """ sql_loader = SQLLoader(__file__, date=date) self.execute_query( sql_loader, 'merge_skips_into_aggregated_skips_and_saves', params=dict( db=self.sf_config['db'], schema=self.sf_config['schema'], nonroyalty_table=self.nonroyalty_table, reportdate=date, storeid=self.storeid, feedid=self.feedid, vendor_ids=get_vendors(date, licensor), licensor=licensor ) ) def load_saves_into_aggregated_skips_and_saves(self, date, licensor): """Load saves to aggregated_skips_and_saves. Args: date (str): Date of the data being process (YYYY-MM-DD). licensor (str): Name of the licensor to ingest. """ self.execute_query( sql_loader, 'load_saves_into_aggregated_skips_and_saves', params=dict( db=self.sf_config['db'], schema=self.sf_config['schema'], library_events_table=self.library_events_table, reportdate=date, storeid=self.storeid, feedid=self.feedid, vendor_ids=get_vendors(date, licensor), licensor=licensor ) ) def update_streams_in_skips_and_saves_aggregated_streams( self, date, licensor): """Update streams number in aggregated_skips_and_saves. Args: date (str): Date of the data being process (YYYY-MM-DD). licensor (str): Name of the licensor to ingest (sme, theorchard). """ self.execute_query( sql_loader, 'update_streams_in_skips_and_saves_aggregated_streams', params=dict( db=self.sf_config['db'], schema=self.sf_config['schema'], fact_table=self.fact_table, reportdate=date, storeid=self.storeid, feedid=self.feedid, licensor=licensor)) def load_fact_error_data(self, date, **kwargs): """Load unmatched data into fact_analytics_error. Args: date (str): Date of the data being process (YYYY-MM-DD). """ sql_loader = SQLLoader(__file__, date=date) licensor = kwargs['licensor'] consumer_db, consumer_schema = self.staging_raw_location( get_fact_analytics_report(date, licensor)) params = dict( db=self.sf_config['db'], schema=self.sf_config['schema'], consumer_db=consumer_db, consumer_schema=consumer_schema, fact_error_table=self.fact_error_table, staging_raw_table=self.get_staging_raw_table_for_fact_analytics( date, licensor), reportdate=date, feedid=self.feedid, storeid=self.storeid, vendor_ids=get_vendors( date, licensor, get_fact_analytics_report(date, licensor)), licensor=licensor) query_name = self.get_query_name( 'load_fact_analytics_error', sql_loader, kwargs['licensor']) self.execute_query(sql_loader, query_name, params) def _delete_from_fact_table(self, table, date, **kwargs): """Delete rows in fact table with the current run date. This is required for the workflow to be idempotent, and to avoid row duplication. Before we'll load rows for a specific day to the fact tables, we have to delete rows which were added by previous (allegedly unsuccessful workflow run). Args: table (str): Either fact_analytics, either fact_analytics_error. date (str): Date of the data being process (YYYY-MM-DD). kwargs (dict): Custom arguments. """ sql_template = sql_loader.load_query('delete_from_fact_table') params = dict( db=self.sf_config['db'], schema=self.sf_config['schema'], fact_table=table, reportdate=date, feedid=self.feedid, storeid=self.storeid, **kwargs) sql_template, non_identifier_params = ( self.validator.format_identifiers(sql_template, params)) return self.fetchone(sql_template, params=non_identifier_params)[0]