# Import python packages import pandas as pd import numpy as np import json import re import time import datetime as dt from dateutil.relativedelta import relativedelta, FR import pytz import requests from bs4 import BeautifulSoup import html import boto3 import os def setup_email_params(): send_the_email = True send_only_to_myself = False client = boto3.client('ses',region_name='us-east-1') email_params = { 'client':client, 'send_the_email':send_the_email, 'send_only_to_myself':send_only_to_myself } return email_params def init_secrets(): try: import os sf_user = os.environ['SNOWFLAKE_USER'] sf_account = os.environ['SNOWFLAKE_ACCOUNT'] sf_warehouse = os.environ['SNOWFLAKE_WAREHOUSE'] sf_password = os.environ['PEM_KEY_PASSWORD'] sf_pem_key = os.environ['PEM_KEY'] except: pass try: session = boto3.session.Session() client = session.client( service_name='secretsmanager', region_name='us-east-1' ) sf_user = client.get_secret_value(SecretId="dev/awal-ar/SNOWFLAKE_USER")["SecretString"] sf_account = client.get_secret_value(SecretId="dev/awal-ar/SNOWFLAKE_ACCOUNT")["SecretString"] sf_warehouse = client.get_secret_value(SecretId="dev/awal-ar/SNOWFLAKE_WAREHOUSE")["SecretString"] sf_password = client.get_secret_value(SecretId="dev/awal-ar/PEM_KEY_PASSWORD")["SecretString"] sf_pem_key = client.get_secret_value(SecretId="dev/awal-ar/PEM_KEY")["SecretString"] except: pass from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization p_key = serialization.load_pem_private_key( sf_pem_key.encode('utf-8').decode('unicode_escape').encode("utf-8"), password=sf_password.encode('utf-8'), backend=default_backend() ) pkb = p_key.private_bytes( encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()) sf_secrets = { 'sf_user':sf_user, 'sf_account':sf_account, 'sf_warehouse':sf_warehouse, 'sf_password':sf_password, 'pkb':pkb } return sf_secrets def orcd_query(sql_query,sf_params): ctx = sf_params['snowflake'].connector.connect( user=sf_params['sf_user'], private_key=sf_params['pkb'], account=sf_params['sf_account'], warehouse=sf_params['sf_warehouse'] ) cs = ctx.cursor(sf_params['snowflake'].connector.DictCursor) try: cs.execute(sql_query) result = cs.fetchall() finally: cs.close() ctx.close() result = pd.DataFrame(result) return result def delphi_query(sql_query,sf_params): ctx = sf_params['snowflake'].connector.connect( user=sf_params['sf_user'], private_key=sf_params['pkb'], account='delphi', warehouse='AWAL_ANALYTICS_LARGE_WAREHOUSE', region='us-east-1' ) cs = ctx.cursor(sf_params['snowflake'].connector.DictCursor) try: cs.execute(sql_query) result = cs.fetchall() finally: cs.close() ctx.close() return pd.DataFrame(result) def upload_df_to_snowflake_table(df,table_name,sf_params): import streamlit # write_pandas won't run without streamlit package for some reason from snowflake.connector.pandas_tools import write_pandas ctx = sf_params['snowflake'].connector.connect( user=sf_params['sf_user'], private_key=sf_params['pkb'], account=sf_params['sf_account'], warehouse=sf_params['sf_warehouse'], database='awal', schema='awal_ar' ) try: write_pandas(ctx, df, table_name, auto_create_table=True) finally: ctx.close() def upload_temp_stage_to_delphi(stage_name,stage_data,sf_params): delphi_query(f'REMOVE @~/{stage_name}.csv.gz',sf_params) with open(f'/tmp/{stage_name}.csv', 'w') as f: for each_item in stage_data: f.write(f"{each_item}\n") file_path = os.path.abspath(f'/tmp/{stage_name}.csv') delphi_query(f'PUT file://{file_path} @~ AUTO_COMPRESS=TRUE',sf_params) def remove_off_limits_artists(df,sf_params): # artists off_limits_artists = orcd_query('select * from awal.awal_ar.off_limits_artists',sf_params)['ARTIST'].str.lower() df = df.loc[~df['NAME'].str.lower().isin(off_limits_artists)] # collabs off_limits_artists = orcd_query('select * from awal.awal_ar.off_limits_artists_collabs',sf_params) off_limits_artists = off_limits_artists['ARTIST'].str.lower().values.tolist() df = df.loc[~(df['NAME'].str.lower()).str.contains('|'.join(off_limits_artists))].reset_index(drop=True) # shows off_limits_shows = orcd_query('select * from awal.awal_ar.off_limits_shows',sf_params) off_limits_shows = off_limits_shows['NAME'].str.lower().values.tolist() df = df.loc[~(df['NAME'].str.lower()).str.contains('|'.join(off_limits_shows))].reset_index(drop=True) return df # DOD streaming growth (greatest gainers, steady growth) def group_songs(df_original,which_region): df = df_original.copy() day_columns = df_original.columns[df_original.columns.str.startswith(f'{which_region}_DAY')] df['STREAMING_CATEGORY'] = np.where( df[day_columns[-1]] > 1.3 * df[day_columns[-2]], # greatest gainers -- last day is at least 1.3x previous day # 'GG', np.where( df[day_columns[-1]] > df[day_columns[-2]], np.where( df[day_columns[-2]] > df[day_columns[-3]], # continued growth in last 3 days # 'SG', '' ), '' ) ) greatest_gainers = df.loc[df['STREAMING_CATEGORY']=='GG']['ARTIST'].tolist() steady_growth = df.loc[df['STREAMING_CATEGORY']=='SG']['ARTIST'].tolist() return greatest_gainers,steady_growth def get_songs_up_bw(result,n_building_days): tmp = result.loc[(result['GL_WTD_CHG']>(3000*n_building_days))].reset_index(drop=True) to_drop = tmp.loc[(tmp['GL_LW1']>1000000) & (tmp['GL_WTD_PCT'] < 10)].index if len(to_drop)>0: tmp.drop(to_drop,inplace=True) return tmp def get_followers_spotify(result,followers_dict,platform): tmp = result.merge(followers_dict[platform],how='left',on='SPOTIFY_ARTIST_ID') # filter to relevant results tmp = tmp.loc[tmp[f'{platform}_FOLLOWERS_7DAY_GROWTH']>2].reset_index(drop=True) tmp = tmp.loc[tmp['SPOTIFY_FOLLOWERS_7DAY']>150] tmp.sort_values(f'{platform}_FOLLOWERS_7DAY_GROWTH',ascending=False,inplace=True) if 'ENGAGEMENT_RATE' in tmp.columns: tmp.drop('ENGAGEMENT_RATE',axis=1,inplace=True) return tmp def get_followers_ig(result,followers_dict,platform): tmp = result.merge(followers_dict[platform],how='left',on='SPOTIFY_ARTIST_ID') # filter to relevant results tmp = tmp.loc[tmp[f'{platform}_FOLLOWERS_7DAY']>1000] to_drop = tmp.loc[(tmp[f'{platform}_TOTAL_FOLLOWERS']>100000) & (tmp[f'{platform}_FOLLOWERS_7DAY_GROWTH'] < 2)].index if len(to_drop)>0: tmp.drop(to_drop,inplace=True) return tmp def get_tiktok_engagement(result,followers_dict): # attach relevant columns # if tiktok URL is already there (via followers), then don't include it if 'TIKTOK_TOTAL_FOLLOWERS' in result.columns: tmp = result.merge(followers_dict['TIKTOK'][['SPOTIFY_ARTIST_ID','ENGAGEMENT_RATE']],how='left',on='SPOTIFY_ARTIST_ID') else: tmp = result.merge(followers_dict['TIKTOK'][['SPOTIFY_ARTIST_ID','TIKTOK_PROFILE','ENGAGEMENT_RATE']],how='left',on='SPOTIFY_ARTIST_ID') tmp.reset_index(drop=True,inplace=True) # filter to relevant results tmp = tmp.loc[tmp['ENGAGEMENT_RATE']>=20] return tmp def drop_sony_orchard(result): tmp = result.drop(result.loc[result['LABEL'].fillna('').str.lower().str.contains('sony')].index) tmp.drop(tmp.loc[tmp['LABEL'].fillna('').str.lower().str.contains('orchard')].index,inplace=True) tmp.reset_index(inplace=True,drop=True) return tmp def format_number(n): if n >= 1_000_000_000: return f"{n / 1_000_000_000:.1f}B" elif n >= 1_000_000: return f"{n / 1_000_000:.1f}M" elif n >= 1_000: return f"{n / 1_000:.1f}K" else: return str(n) # Define tiers def get_multiplier(value): if value < 25000: return 1.5 elif value < 50000: return 1.4 elif value < 100000: return 1.3 elif value < 250000: return 1.2 elif value < 500000: return 1.1 elif value < 1000000: return 1.05 else: return 1.01 def prep_and_send_email(email_params,destination_emails,email_subject,full_email): if email_params['send_the_email']: if email_params['send_only_to_myself']: destination_emails = ['joselyn.ho@awal.com'] response = email_params['client'].send_email( Source='awalresearch@dev.theorchard.io', Destination={ 'ToAddresses': destination_emails, }, Message={ 'Subject': { 'Data': email_subject, 'Charset': 'UTF-8' }, 'Body': { 'Html': { 'Data': full_email, 'Charset': 'UTF-8' } } }, SourceArn='arn:aws:ses:us-east-1:103233932089:identity/dev.theorchard.io', ReplyToAddresses=[ 'joselyn.ho@awal.com' ], ) return response['ResponseMetadata']['HTTPStatusCode'] == 200 else: # this is for local testing where no email is sent. Just opens a browser with the contents import webbrowser import os # Save the file file_path = os.path.abspath("preview_email.html") with open(file_path, "w", encoding="utf-8") as f: f.write(full_email) # Open it in the default web browser webbrowser.open(f"file://{file_path}") ########################### # Venues ########################### def print_shows(df_to_print): # combine date with URL and # days # df_to_print['DATE'] = '' + df_to_print['DATE'] + ' (sold out in ' + df_to_print['DAYS_TO_SELL_OUT'].astype(str) + ' days)' df_to_print['DATE'] = '' + df_to_print['DATE'] + '' # mark entries that just sold out df_to_print['DATE'] = np.where( df_to_print['NEWLY_SOLD_OUT']==True, '' + df_to_print['DATE'] + '', df_to_print['DATE'] ) # print number of shows sold out per artist/event df_to_print['TEXT'] = np.where( df_to_print['ALL_SOLD_OUT']==True, 'All shows sold out (' + df_to_print['N_SHOWS'].astype(str) + ')', df_to_print['N_SOLD_OUT'].astype(str) + ' sold-out shows out of ' + df_to_print['N_SHOWS'].astype(str) ) # bold the name and combine with the associated text df_to_print['TEXT'] = '' + df_to_print['NAME'] + ' - ' + df_to_print['TEXT'] + '
' # group multiple listings by artist, then sort alphabetically df_to_print = df_to_print.groupby(['TEXT'], as_index=False).agg(DATE=('DATE', '
'.join)) df_to_print.sort_values('TEXT',inplace=True,key=lambda col: col.str.lower()) # put all text into 1 string to print df_to_print['TEXT'] = df_to_print['TEXT'] + df_to_print['DATE'] df_to_print = df_to_print['TEXT'].str.cat(sep='

') df_to_print = '

' + df_to_print + '

' return df_to_print # Bowery Ballroom and Mercury Lounge use the same website def pull_bowery_mercury(HEADERS,which_venue): if which_venue=='Bowery Ballroom': url = 'https://mercuryeastpresents.com/boweryballroom/' pages = 4 elif which_venue=='Mercury Lounge': url = 'https://mercuryeastpresents.com/mercurylounge/' pages = 5 df = [] # 1 page at a time for n in np.arange(pages): # initial page = requests.get(url+'page/'+str(n+1)+'/',headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_calendar = soup.find_all('div',{'class':"tw-details-container"}) # organize entries for item in event_calendar: # get event name, url, and date event_name = item.find('a').string event_url = item.find('a')['href'] event_date = item.find('span',{'class':"tw-event-date"}).string # indicate whether the event is sold out if 'SOLD OUT' in event_name: is_sold_out = 1 else: is_sold_out = 0 # remove the *SOLD OUT* string for neatness event_name = event_name.replace("*SOLD OUT* ","") # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df # Hotel Cafe (LA) def pull_hotelcafe(HEADERS): df = [] which_months = [ '', # current month (today + relativedelta(months=1)).strftime('%Y-%m'), (today + relativedelta(months=2)).strftime('%Y-%m'), (today + relativedelta(months=3)).strftime('%Y-%m') ] for each_month in which_months: url = f'https://new.hotelcafe.com/events/month/{each_month}' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') long_string = soup.find('script',{'type':'application/ld+json'}).string.replace('\n','').replace('\t','') event_calendar = json.loads(long_string) for item in event_calendar: event_name = item['name'] event_url = item['url'] event_date = item['startDate'].split("T")[0] if 'offers' in item.keys(): is_sold_out = True if item['offers']['availability']=='SoldOut' else False else: is_sold_out = False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df # Elsewhere Zone 1 def pull_elsewhere(HEADERS): url = 'https://www.elsewhere.club/events' df = [] # initial pull page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_calendar = soup.find_all("time",{"class":"listing-compact_listing-compact__date__XDK6C font-t4"}) # organize entries for item in event_calendar: # get event name, url, and date event_date = item.string event_name = item.next_sibling.span.string event_url = 'https://www.elsewhere.club' + item.next_sibling.a['href'] # indicate whether the event is sold out if item.next_sibling.find('li',{'class':'listing-badge-list_listing-badge-list__sold-out__vwA99 badge'}) is not None: is_sold_out = 1 else: is_sold_out = 0 # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_cafedunord_swedishamericanhall(HEADERS): url = 'https://cafedunord.com/' df = [] # initial pull page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_calendar = soup.find_all('div',{'class':'row'}) # organize entries for item in event_calendar: try: # get event name, url, and date event_date = item.find('span',{'class':'tw-event-date'}).string event_name = item.find('div',{'class':['six columns']}).find('span').string event_url = item.find('div',{'class':['six columns']}).find('a')['href'] event_location = item.find('span',{'class':'tw-venue-name'}).string.replace('\n','').strip() except: continue # indicate whether the event is sold out if ( item.find('div',{'class':['three columns']}).find('a',{'class':'button button-primary tw-buy-tix-btn tw_soldout'}) or item.find('div',{'class':['three columns']}).find('a',{'class':'button button-primary tw-buy-tix-btn tw_wait list'}) ): is_sold_out = 1 else: is_sold_out = 0 # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out], 'VENUE': [event_location], 'REGION': ['San Francisco'] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) # this result will have duplicates - remove here df = df.drop_duplicates(['NAME','DATE']).reset_index(drop=True) cafedunord = df.loc[df['VENUE']=='Cafe Du Nord'].reset_index(drop=True) swedish = df.loc[df['VENUE']!='Cafe Du Nord'].reset_index(drop=True) ################# do this twice bc 2 venues ################## # count number of shows & number of sold out shows cafedunord['N_SHOWS'] = cafedunord.groupby('NAME')['IS_SOLD_OUT'].transform('count') cafedunord['N_SOLD_OUT'] = cafedunord.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out cafedunord['ALL_SOLD_OUT'] = cafedunord['N_SHOWS'] == cafedunord['N_SOLD_OUT'] ################# do this twice bc 2 venues ################## # count number of shows & number of sold out shows swedish['N_SHOWS'] = swedish.groupby('NAME')['IS_SOLD_OUT'].transform('count') swedish['N_SOLD_OUT'] = swedish.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out swedish['ALL_SOLD_OUT'] = swedish['N_SHOWS'] == swedish['N_SOLD_OUT'] return cafedunord,swedish def pull_tveye(HEADERS): df = [] # this website's listings are on separate pages so we have to check each page until there are no more pages. # i did try to get total number of pages but they only show 7 pages per view page_numbers = 20 # there shouldn't be more than this much # per page for which_page in np.arange(page_numbers): which_page_url = f'https://tveyenyc.com/calendar/?list1page={which_page+1}' # initial to get event urls page = requests.get(which_page_url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') # stop if this

has a message (would say "No events on sale at this time") if len(soup.find_all('p', class_="no-events"))>0: break events_raw = soup.find_all('p',class_="fs-18 bold mb-12 title") event_names = [event_raw.get_text() for event_raw in events_raw] event_urls = [event_raw.a['href'] for event_raw in events_raw] dates = soup.find_all('p',class_="fs-18 bold mt-1r date") event_dates = [each_date.get_text() for each_date in dates] # times = soup.find_all('p',class_="fs-12 doortime-showtime") # event_times = [each_time.get_text() for each_time in times] sold_out_status_raw = soup.find_all('a',class_="seetickets-buy-btn") sold_out_status = [each_status.get_text() for each_status in sold_out_status_raw] df.append(pd.DataFrame({ 'NAME': event_names, 'DATE': event_dates, # 'TIME': event_times, 'URL': event_urls, 'SOLD_OUT_STATUS': sold_out_status })) df = pd.concat(df) df.reset_index(drop=True,inplace=True) # df['DATE'] = df['DATE'] + ' ' + df['TIME'] df['IS_SOLD_OUT'] = np.where( df['SOLD_OUT_STATUS']=='Buy Tickets', 0, 1 ) df = df[['NAME','DATE','URL','IS_SOLD_OUT']] # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_theecho(HEADERS): url = f'https://www.theecho.com/shows' # initial page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_names_raw = soup.find_all('p',class_="chakra-text css-zvlevn") event_names = [event_name.get_text() for event_name in event_names_raw] event_dates_raw = soup.find_all('p',class_="chakra-text css-lfdvoo") event_dates = [event_date.get_text() for event_date in event_dates_raw] event_urls_raw = soup.find_all('a',class_="chakra-button css-1q88drx") event_urls = [event_url["href"] for event_url in event_urls_raw] df = pd.DataFrame({ 'NAME': event_names, 'DATE': event_dates, 'URL': event_urls }) # sold out based on whether the event name says so df['IS_SOLD_OUT'] = np.where( df['NAME'].str.upper().str.contains('SOLD OUT'), 1, 0 ) # need to remove " - SOLD OUT" from event_name otherwise won't match previous listing in snowflake table df['NAME'] = df['NAME'].str.replace(r' - SOLD OUT', '', case=False, regex=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_troubadour(HEADERS): url = 'https://troubadour.com/calendar/' # initial page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_names_raw = soup.find_all('p',class_="fs-12 bold m-0") event_names = [event_name.get_text() for event_name in event_names_raw] event_dates_raw = soup.find_all('div',class_="seetickets-buy-btn") sold_out_status = [event_date.find('a').get_text() for event_date in event_dates_raw] event_dates = [event_date.find('a').get('aria-label') for event_date in event_dates_raw] event_urls = [event_date.find('a').get('href') for event_date in event_dates_raw] df = pd.DataFrame({ 'NAME':event_names, 'DATE_STR':event_dates, 'SOLD_OUT_STR':sold_out_status, 'URL':event_urls }) df['DATE'] = df['DATE_STR'].str[-6:] df['IS_SOLD_OUT'] = np.where( df['SOLD_OUT_STR']=='Sold Out', 1, 0 ) df = df[['NAME','DATE','URL','IS_SOLD_OUT']] # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_basement(HEADERS): url = 'https://www.thebasementnashville.com/basement/calendar/' # initial page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') # Step 1: Find all script blocks with JavaScript script_tags = soup.find_all('script', type='text/javascript') target_script_content = None # Step 2: Identify the correct script tag that contains EventData for script in script_tags: if script.string and 'EventData.events.push' in script.string: target_script_content = script.string break # Stop once we've found the relevant one pattern = r'EventData\.events\.push\((\{.*?\})\);' matches = re.findall(pattern, target_script_content, re.DOTALL) events = [json.loads(match) for match in matches] event_names = [each_event['value'] for each_event in events] event_urls = [each_event['data']['url'] for each_event in events] df = pd.DataFrame({ 'NAME':event_names, 'URL':event_urls }) # sold out based on whether the event name says so df['IS_SOLD_OUT'] = np.where( df['NAME'].str.upper().str.contains('SOLD OUT'), 1, 0 ) # need to remove " - SOLD OUT" from event_name otherwise won't match previous listing in snowflake table df['NAME'] = df['NAME'].str.replace(r'SOLD OUT! ', '', case=False, regex=True) df['DATE'] = df['NAME'].str[-5:] df['NAME'] = df['NAME'].str[:-6] df = df[['NAME','DATE','URL','IS_SOLD_OUT']] # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_philamoca(HEADERS): url = 'https://www.philamoca.org/' # initial page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_names_raw = soup.find_all('span',class_="event__title") event_names = [event_name.get_text() for event_name in event_names_raw] event_dates_raw = soup.find_all('time',class_="event__date") event_dates = [event_date['datetime'] for event_date in event_dates_raw] sold_out_str_raw = soup.find_all('li',class_="event__detail event__detail--tickets") sold_out_str = [each_str.find('span',class_="event__detail-label").get_text() for each_str in sold_out_str_raw] urls_raw = soup.find_all('a',class_="event") event_urls = [each_url["href"] for each_url in urls_raw] df = pd.DataFrame({ 'NAME':event_names, 'DATE':event_dates, 'SOLD_OUT_STATUS':sold_out_str }) if len(event_urls) != len(event_names): # remove private events before attaching URL (because private events won't have URL avail) to_drop = df.loc[df['SOLD_OUT_STATUS'].str.upper().str.contains('INVITATION')].index df.drop(to_drop,inplace=True) df['URL'] = event_urls df['IS_SOLD_OUT'] = np.where( df['SOLD_OUT_STATUS'].str.contains('Sold Out'), 1, 0 ) df = df[['NAME','DATE','URL','IS_SOLD_OUT']] # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_johnnybrendas(HEADERS): url = 'https://johnnybrendas.com/events/' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_calendar = soup.find_all('div',{'class':'rhpSingleEvent'}) df = [] for item in event_calendar: event_name = item.find('a')['title'] event_url = item.find('a')['href'] event_date = item.find('div',{'class':"mb-0"}).string.strip('\n\t') is_sold_out = True if item.find('span', class_=['col-12', 'rhp-event-cta']).get('class', [])[1]=='sold-out' else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_theearl(HEADERS): urls = ['https://badearl.com/','https://badearl.com/?sf_paged=2','https://badearl.com/?sf_paged=3'] df = [] for which_url in urls: page = requests.get(which_url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_calendar = soup.find_all('div',{'class':'cl-element cl-element-section cl-element--instance-1023 show-listing-right'}) for item in event_calendar: event_name = item.find('div',{'class':'show-listing-headliner'}).string event_url = item.find('a',{'class':'cl-element-link__anchor'})['href'] event_date = item.find('p',{'class':'show-listing-date'}).string if item.find('div',{'class':'sold-out'}) is not None: is_sold_out = True else: is_sold_out = False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_aisle5(HEADERS): url = 'https://aisle5atl.com/calendar/' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_calendar = soup.find_all('div',{'class':'seetickets-list-event-content-container'}) df = [] for item in event_calendar: event_url = item.find('a')['href'] event_name = item.find('a').string event_date = item.find('p',{'class':'event-date'}).string is_sold_out = True if item.find('a',{'class':'button-soldout'}) is not None else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_eddiesattic(HEADERS): url = 'https://eddiesattic.com/api/plot/v1/listings?currentpage=1¬Loaded=false&listingsPerPage=48&_locale=user' page = requests.get(url,headers=HEADERS,timeout=5) event_calendar = page.json() df = [] for item in event_calendar: event_name = item['title'] event_url = item['permalink'] event_date = item['dateTime'].lstrip('').rstrip('') is_sold_out = True if item['ticket']['text']=='Sold out' else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_sinclairboston(HEADERS): url = 'https://www.sinclaircambridge.com/events/all' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content, 'html.parser') event_calendar = soup.find_all(class_="entry") df = [] for item in event_calendar: event_name = item.find('h3').find('a').string.replace('\t','').replace('\n','') event_url = item.find('a')['href'] event_date = item.find('span',{'class':'date'}).get_text(separator=" ", strip=True) ticket_status = item.find('a',{'class':'btn-tickets'})['title'] if ticket_status in ['Buy Tickets','Get Tickets','Box Office Only','Cancelled','Coming Soon']: is_sold_out = False else: is_sold_out = True # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_babysallright(HEADERS): urls = [ 'https://donyc.com/venues/baby-s-all-right/month_events/', 'https://donyc.com/venues/baby-s-all-right/month_events/?period=1', 'https://donyc.com/venues/baby-s-all-right/month_events/?period=2' ] df = [] for which_url in urls: page = requests.get(which_url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content, 'html.parser') event_calendar = soup.find_all('div',{'class':'ds-listing'}) for item in event_calendar: event_name = item.find('span',{'class':'ds-listing-event-title-text'}).string event_url = 'https://donyc.com' + item.find('div').parent['data-permalink'] pattern = r'/events/(\d{4}/\d{1,2}/\d{1,2})/' match = re.search(pattern, event_url) event_date = match.group(1) is_sold_out = True if item.find('li',{'class':'ds-listing-soldout'}) is not None else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) time.sleep(2) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_purgatory(HEADERS): urls = [ 'https://donyc.com/venues/purgatory/month_events/', 'https://donyc.com/venues/purgatory/month_events/?period=1', 'https://donyc.com/venues/purgatory/month_events/?period=2' ] df = [] for which_url in urls: page = requests.get(which_url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content, 'html.parser') event_calendar = soup.find_all('div',{'class':'ds-listing'}) for item in event_calendar: event_name = item.find('span',{'class':'ds-listing-event-title-text'}).string event_url = 'https://donyc.com' + item.find('div').parent['data-permalink'] pattern = r'/events/(\d{4}/\d{1,2}/\d{1,2})/' match = re.search(pattern, event_url) event_date = match.group(1) is_sold_out = True if item.find('li',{'class':'ds-listing-soldout'}) is not None else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) time.sleep(2) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_schubas_lh(HEADERS): url = 'https://lh-st.com' df = [] page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content, 'html.parser') event_calendar = soup.find_all('div',{'class':'card'}) for item in event_calendar: event_venue = item.find('div').parent['data-venue'] event_url = item.find('a')['href'] event_date = item.find('span',{'class':'date'}).string + ' - ' + event_venue event_name = item.find('h4',{'class':'card-title'}).string event_id = item.find('div').parent['id'] # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'ID': [event_id] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) id_list = '%2C'.join(df['ID'].astype(str)) temp_headers = {'User-Agent': 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148','SessionId':'RUFBQUFPMkd0NmlvVXNPeE5lUjRTWVliNkJHNGdHaHVDQTRBNVlHQ1VYazZqbU1UcENLdEsvWnFYQW9BdzN2MUU0VHRyYmFkaXNobG5VLy9YMjFBSS93Z3VzQT0='} url = 'https://tickets.lh-st.com/api/v1/products?ids=' + id_list page = requests.get(url,headers=temp_headers,timeout=5) page = page.json() md = pd.DataFrame({ 'ID':[item['frontendId'] for item in page], 'IS_SOLD_OUT':[item['isSoldOut'] for item in page] }) # merge by id df = df.merge(md,how='inner',on='ID') df.drop('ID',axis=1,inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_emptybottle(HEADERS): url = 'https://app.ticketmaster.com/discovery/v2/events.json?size=200&apikey=GmC9AB6l4pDhA5yhg4dgD3G0AEDK8wmL&venueId=rZ7HnEZ178gfg&venueId=KovZpZAId16A&venueId=rZ7HnEZ17aJ47&source=ticketmaster,ticketweb' page = requests.get(url,headers=HEADERS,timeout=5) event_calendar = json.loads(page.content)['_embedded']['events'] df = [] for item in event_calendar: event_name = item['name'] event_date = item['dates']['start']['localDate'] event_url = item['url'] is_sold_out = True if 'sold out' in event_name.lower() else False event_name = event_name.replace('*SOLD OUT* ','') # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_bellwether(HEADERS): url = 'https://thebellwetherla.com/listing/' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_calendar = soup.find_all('div',{'class':'content-information'}) df = [] for item in event_calendar: event_date = item.find('div',{'class':'date-show'})['content'] event_name = item.find('h2',{'class':'show-title'}).string event_url = item.find('div',{'class':'entry'}).find('a')['href'] is_sold_out = True if 'sold out' in item.find('a',{'class':'button'}).string.lower() else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_broadberryco(HEADERS): url = 'https://thebroadberry.com/events/' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_calendar = soup.find_all('div',{'class':'eventWrapper'}) df = [] for item in event_calendar: event_url = item.find('a')['href'] event_name = item.find('a')['title'] event_date = item.find('div',{'id':'eventDate'}).string.replace('\n\t ','') is_sold_out = True if 'sold out' in item.find('a',{'class':'btn'}).string.lower() else False event_venue = item.find('a',{'class':'venueLink'})['title'] # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out], 'WHICH_VENUE': [event_venue] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) df = df.loc[df['WHICH_VENUE'].isin(['Richmond Music Hall','The Broadberry'])] df['DATE'] = df['DATE'] + ' - ' + df['WHICH_VENUE'] df.drop('WHICH_VENUE',axis=1,inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_thecamel(HEADERS): url = 'https://www.thecamel.org/shows' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') event_calendar = soup.find_all('div',{'class':'uui-layout88_item'}) df = [] for item in event_calendar: event_url = 'https://www.thecamel.org/shows' + item.find('a')['href'] event_date = item.find('div',{'class':'event-month-multi'}).string event_name = item.find('h3',{'class':'uui-heading-xxsmall-2'}).string is_sold_out = True if len(item.find_all('img',{'loading':'lazy'})[1]['class'])==1 else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_thecrocodile(HEADERS): df = [] for which_page in [1,2,3,4,5]: url = f'https://www.ticketweb.com/venue/the-crocodile-seattle-wa/10352?page={which_page}' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') long_string = soup.find('script',{'type':'application/ld+json'}).string.replace('\n','').replace('\t','') event_calendar = json.loads(long_string) for item in event_calendar: event_name = item['name'] event_url = item['url'] event_date = item['startDate'].split("T")[0] is_sold_out = True if item['offers']['availability']=='SoldOut' else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_themint(HEADERS): url = 'https://www.ticketweb.com/venue/the-mint-los-angeles-ca/206385' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') long_string = soup.find('script',{'type':'application/ld+json'}).string.replace('\n','').replace('\t','') event_calendar = json.loads(long_string) df = [] for item in event_calendar: event_name = item['name'] event_url = item['url'] event_date = item['startDate'].split("T")[0] is_sold_out = True if item['offers']['availability']=='SoldOut' else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_themusicbox(HEADERS): df = [] for which_page in [1,2,3]: url = f'https://www.ticketweb.com/venue/music-box-san-diego-ca/420435?page={which_page}' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') long_string = soup.find('script',{'type':'application/ld+json'}).string.replace('\n','').replace('\t','') event_calendar = json.loads(long_string) for item in event_calendar: event_name = item['name'] event_url = item['url'] event_date = item['startDate'].split("T")[0] is_sold_out = True if item['offers']['availability']=='SoldOut' else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_somasidestage(HEADERS): df = [] url = 'https://www.ticketweb.com/venue/soma-sidestage-san-diego-ca/434165' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') long_string = soup.find('script',{'type':'application/ld+json'}).string.replace('\n','').replace('\t','') event_calendar = json.loads(long_string) for item in event_calendar: event_name = item['name'] event_url = item['url'] event_date = item['startDate'].split("T")[0] is_sold_out = True if item['offers']['availability']=='SoldOut' else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def pull_middleeastupstairs(HEADERS): df = [] for which_page in [1,2,3]: url = f'https://www.ticketweb.com/venue/middle-east-upstairs-cambridge-ma/18394?page={which_page}' page = requests.get(url,headers=HEADERS,timeout=5) soup = BeautifulSoup(page.content,'html.parser') long_string = soup.find('script',{'type':'application/ld+json'}).string.replace('\n','').replace('\t','') event_calendar = json.loads(long_string) for item in event_calendar: event_name = item['name'] event_url = item['url'] event_date = item['startDate'].split("T")[0] is_sold_out = True if item['offers']['availability']=='SoldOut' else False # append result df.append( pd.DataFrame({ 'NAME': [event_name], 'DATE': [event_date], 'URL': [event_url], 'IS_SOLD_OUT': [is_sold_out] }) ) df = pd.concat(df) df.reset_index(drop=True,inplace=True) df.drop_duplicates(inplace=True) # count number of shows & number of sold out shows df['N_SHOWS'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('count') df['N_SOLD_OUT'] = df.groupby('NAME')['IS_SOLD_OUT'].transform('sum') # Indicate whether all events from the same artist are sold out df['ALL_SOLD_OUT'] = df['N_SHOWS'] == df['N_SOLD_OUT'] return df def setup_logger(): import logging logger = logging.getLogger() logger.setLevel(logging.INFO) # Add a logger handler if none exists if not logger.hasHandlers(): handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) return logger ################################################### ################################################### def handler(event,context): logger = setup_logger() logger.info('Starting handler function...') import snowflake.connector sf_params = init_secrets() sf_params['snowflake'] = snowflake email_params = setup_email_params() ################################################### ################################################### # READ/ORGANIZE DATA ################################################### ################################################### destination_email_list = orcd_query('select * from awal.awal_ar.email_alerts',sf_params) run_artist_engagement = True run_sold_out_shows = True if run_artist_engagement: # Main result adj_result = orcd_query(f""" select a.* from awal.awal_ar.discovery_artist_table_cleaned a left join awal.awal_ar.discovery_daily_streams d1 on d1.song_id=a.biggest_song_id left join awal.awal_ar.discovery_daily_streams d2 on d2.song_id=a.latest_song_id WHERE 1=1 and ((d1.gl_day_7>5000) or (d2.gl_day_7>5000)) and NOT ( -- ARTIST ( -- completely contain special characters REGEXP_LIKE(ARTIST_NAME, '^[Ͱ-Ͽ]+$') -- greek or REGEXP_LIKE(ARTIST_NAME, '^[Ѐ-ӿ]+$') -- russian or REGEXP_LIKE(ARTIST_NAME, '^[Ā-ɏ]+$') -- turkish or REGEXP_LIKE(ARTIST_NAME, '^[Ⴀ-ჿ]+$') -- georgian or REGEXP_LIKE(ARTIST_NAME, '^[՛-֏]+$') -- armenian or REGEXP_LIKE(ARTIST_NAME, '^[ក-៿]+$') -- cambodian or REGEXP_LIKE(ARTIST_NAME, '^[က-႟]+$') -- burmese or REGEXP_LIKE(ARTIST_NAME, '^[஀-௿]+$') -- tamil or REGEXP_LIKE(ARTIST_NAME, '^[ऀ-ॿ]+$') -- hindi/sanskrit -- contain special characters or REGEXP_LIKE(ARTIST_NAME, '.*[؀-ۿ].*') -- arabic or REGEXP_LIKE(ARTIST_NAME, '.*[֐-׿].*') -- hebrew or REGEXP_LIKE(ARTIST_NAME, '.*[ᄀ-ᇿ㄰-㆏가-힣].*') -- korean or REGEXP_LIKE(ARTIST_NAME, '.*[一-鿿].*') -- chinese or REGEXP_LIKE(ARTIST_NAME, '.*[ぁ-ゟ゠-ヿ一-鿿].*') -- japanese or REGEXP_LIKE(ARTIST_NAME, '.*[ĩứưẤÅåắăặạậệâỗốờơộșȘ±¨].*') -- Vietnamese / Portuguese ) ) and ( (COUNTRY_OF_ORIGIN not in ('TR','RU')) or (COUNTRY_OF_ORIGIN is null) ) """,sf_params) adj_result.rename(columns={'ARTIST_NAME':'ARTIST'},inplace=True) latest_date_main = adj_result['LATEST_DATE_MAIN'].iloc[0] date_pulled_main = adj_result['DATE_PULLED_MAIN'].iloc[0] date_pulled_weekly_streams = adj_result['DATE_PULLED_WEEKLY_STREAMS'].iloc[0] date_pulled_daily_streams = adj_result['DATE_PULLED_DAILY_STREAMS'].iloc[0] adj_result.drop(['LATEST_DATE_MAIN','DATE_PULLED_MAIN','DATE_PULLED_WEEKLY_STREAMS','DATE_PULLED_DAILY_STREAMS'],axis=1,inplace=True) adj_result['LATEST_SONG_DATA'] = f"https://app.luminatedata.com/song/" + adj_result['LATEST_SONG_ID'].astype('str') + f"?g=AA&d=YTD&stf=Q018U0UsSU5ULFBDfENOLFZJfC8%3D&ssf=Lw%3D%3D&psf=Lw%3D%3D&a=ST&b=CM&sd=2025-01-03&ed={date_pulled_main}&stgl=R0xCLVBWL1NFfEJTLENOfEJTLENNfEJT&psgl=R0xCLVNULw%3D%3D&ga=&m=VVNNLy9OQVRJT05BTA%3D%3D" adj_result['BIGGEST_SONG_DATA'] = f"https://app.luminatedata.com/song/" + adj_result['BIGGEST_SONG_ID'].astype('str') + f"?g=AA&d=YTD&stf=Q018U0UsSU5ULFBDfENOLFZJfC8%3D&ssf=Lw%3D%3D&psf=Lw%3D%3D&a=ST&b=CM&sd=2025-01-03&ed={date_pulled_main}&stgl=R0xCLVBWL1NFfEJTLENOfEJTLENNfEJT&psgl=R0xCLVNULw%3D%3D&ga=&m=VVNNLy9OQVRJT05BTA%3D%3D" ################################### # # active awal core releases # awal_core_active = orcd_query(f""" # with pre_table as ( # select # s.song_id, # a.spotify_artist_id, # a.title, # a.INGESTION_COMPLETED_DATE ingestion_date # from awal.awal_ar.awal_core_active a # join awal.awal_ar.isrc_map_song_id s on s.isrc=a.isrc # where a.spotify_artist_id is not null # ), # ranked AS ( # SELECT *, # ROW_NUMBER() OVER (PARTITION BY song_id ORDER BY ingestion_date DESC) AS rn # FROM pre_table # ) # SELECT spotify_artist_id,title,ingestion_date # FROM ranked # WHERE rn = 1; # """) # awal_core_active['INGESTION_DATE'] = pd.to_datetime(awal_core_active['INGESTION_DATE']).dt.strftime('%m/%d/%y') # awal_core_active['CORE'] = np.where( # awal_core_active['INGESTION_DATE'].isna(), # awal_core_active['TITLE'], # awal_core_active['TITLE'] + ' (as of ' + awal_core_active['INGESTION_DATE'] + ')' # ) # awal_core_active.drop(['TITLE','INGESTION_DATE'],axis=1,inplace=True) # awal_core_active = awal_core_active.groupby(['SPOTIFY_ARTIST_ID'], as_index=False).agg(CORE=('CORE', ', '.join)) ################################### adj_result['LATEST_SONG_WTD'] = adj_result['LATEST_SONG_WTD'] *100 / adj_result['GL_WTD_TP'] adj_result['BIGGEST_SONG_WTD'] = adj_result['BIGGEST_SONG_WTD'] *100 / adj_result['GL_WTD_TP'] # # daily streams plot # for region in ['GL']: # adj_result[region+'_DAILY'] = adj_result[[ # region+'_DAY_1',region+'_DAY_2',region+'_DAY_3',region+'_DAY_4',region+'_DAY_5',region+'_DAY_6',region+'_DAY_7' # ]].values.tolist() # # weekly streams plot # for region in ['GL']: # adj_result[region+'_WEEKLY'] = adj_result[[ # region+'_LW4',region+'_LW3',region+'_LW2',region+'_LW1' # ]].values.tolist() # CHG # # LW adj_result['GL_LW_CHG'] = adj_result['GL_LW1'] - adj_result['GL_LW2'] adj_result['GL_LW2_ADJUSTED'] = adj_result['GL_LW2'].clip(lower=0.0001) adj_result['GL_LW_PCT'] = adj_result['GL_LW_CHG'] *100 / adj_result['GL_LW2_ADJUSTED'] # cap % at 999% adj_result['GL_LW_PCT'].clip(lower=-999,upper=999,inplace=True) # WTD adj_result['GL_WTD_CHG'] = adj_result['GL_WTD_TP'] - adj_result['GL_WTD_LP'] adj_result['GL_WTD_LP_ADJUSTED'] = adj_result['GL_WTD_LP'].clip(lower=0.0001) adj_result['GL_WTD_PCT'] = adj_result['GL_WTD_CHG'] *100 / adj_result['GL_WTD_LP_ADJUSTED'] # cap % at 999% adj_result['GL_WTD_PCT'].clip(lower=-999,upper=999,inplace=True) #################### SOCIALS #################### followers_dict={} followers_dates={} for platform in ['SPOTIFY','INSTAGRAM','TIKTOK']: followers_dict[platform] = orcd_query(f""" select * from awal.awal_ar.discovery_{platform}_followers """,sf_params) followers_dates[platform] = followers_dict[platform]['DATE_UPDATED'].iloc[0] followers_dict[platform].drop(['DATE_UPDATED'],axis=1,inplace=True) followers_dict[platform].rename(columns={ 'TOTAL_FOLLOWERS':f'{platform}_TOTAL_FOLLOWERS', 'FOLLOWERS_7DAY':f'{platform}_FOLLOWERS_7DAY', 'FOLLOWERS_7DAY_GROWTH':f'{platform}_FOLLOWERS_7DAY_GROWTH', },inplace=True) try: # spotify doesn't have this followers_dict[platform]['URL'] = followers_dict[platform]['URL'].str.rstrip('/') + '/' followers_dict[platform].rename(columns={ 'URL':f'{platform}_PROFILE', },inplace=True) except: pass followers_dict[platform][f'{platform}_FOLLOWERS_7DAY_GROWTH'] = followers_dict[platform][f'{platform}_FOLLOWERS_7DAY_GROWTH'] * 100 # cap % at 999% followers_dict[platform][f'{platform}_FOLLOWERS_7DAY_GROWTH'].clip(lower=-999,upper=999,inplace=True) ## Tiktok engagement via chartmetric calc tiktok_engagement = orcd_query('select * from awal.awal_ar.discovery_tiktok_engagement',sf_params) followers_dict['TIKTOK'] = followers_dict['TIKTOK'].merge(tiktok_engagement,how='outer',on='SPOTIFY_ARTIST_ID') followers_dict['TIKTOK']['TIKTOK_PROFILE'] = followers_dict['TIKTOK']['TIKTOK_PROFILE'].fillna(followers_dict['TIKTOK']['URL']) followers_dict['TIKTOK'] = followers_dict['TIKTOK'].drop(columns='URL') ######################################## # # get core applicants # applicant_ids = orcd_query("select * from awal.awal_ar.awal_applicants where STATUS not like '%Rejected%'") # applicant_ids['STATUS'] = applicant_ids['STATUS'].str.replace('Artist ','') # applicant_ids.rename(columns={'LATEST_UPDATE':'CORE_LATEST_UPDATE'},inplace=True) # rearrange columns adj_result = adj_result[[ 'SPOTIFY_ARTIST_ID','ARTIST', 'LABEL','TRACK_URL','N_SONGS', 'GL_WTD_TP','GL_WTD_PCT','GL_WTD_CHG', # 'GL_DAILY', 'GL_DAY_1','GL_DAY_2','GL_DAY_3','GL_DAY_4','GL_DAY_5','GL_DAY_6','GL_DAY_7', 'GL_LW1','GL_LW_PCT','GL_LW_CHG', # 'GL_WEEKLY', 'GL_LW2','GL_LW3','GL_LW4', 'LATEST_SONG','LATEST_SONG_WTD','LATEST_RELEASE_DATE','LATEST_SONG_DATA', 'BIGGEST_SONG', 'BIGGEST_SONG_WTD','BIGGEST_RELEASE_DATE','BIGGEST_SONG_DATA', 'GENRE', 'PREVIEW_URL','COUNTRY_OF_ORIGIN' ]] adj_result.sort_values('GL_WTD_PCT',ascending=False,inplace=True) if date_pulled_main==date_pulled_weekly_streams==date_pulled_daily_streams: data_is_accurate = True else: data_is_accurate = False # daily growth (streams) # greatest_gainers_us,steady_growth_us = group_songs(adj_result,'US') greatest_gainers_gl,steady_growth_gl = group_songs(adj_result,'GL') greatest_gainers = [] # greatest_gainers.extend(greatest_gainers_us) greatest_gainers.extend(greatest_gainers_gl) greatest_gainers = list(set(greatest_gainers)) # drop duplicates steady_growth = [] # steady_growth.extend(steady_growth_us) steady_growth.extend(steady_growth_gl) steady_growth = list(set(steady_growth)) # drop duplicates ## Setup dates ## # NOTE - Daily/weekly streams are pulled based on the main table so as long as the pull-script ran, then LW1 or DAY_7 correspond to the expected dates. # Just need to check that all my tables were updated on the same day. if data_is_accurate==False: logger.info('Data not up to date. Skipping this section.') else: # current building week today = date_pulled_main bw_start_date_tp = today - relativedelta(days=today.weekday()) + relativedelta(days=4, weeks=-1) bw_end_date_tp = latest_date_main # today - relativedelta(days=2) # luminate latest day typically 2 days behind n_building_days = (bw_end_date_tp - bw_start_date_tp).days + 1 # LP (last week) bw_start_date_lw = bw_start_date_tp + relativedelta(weeks=-1) bw_end_date_lw = bw_start_date_lw + relativedelta(days=6) # LP (2 weeks ago) bw_start_date_2w = bw_start_date_lw + relativedelta(weeks=-1) bw_end_date_2w = bw_start_date_2w + relativedelta(days=6) # LP (3 weeks ago) bw_start_date_3w = bw_start_date_2w + relativedelta(weeks=-1) bw_end_date_3w = bw_start_date_3w + relativedelta(days=6) # LP (4 weeks ago) bw_start_date_4w = bw_start_date_3w + relativedelta(weeks=-1) bw_end_date_4w = bw_start_date_4w + relativedelta(days=6) ################################################### ################################################### # DAILY ARTIST EMAIL ################################################### ################################################### email_head = f"""\ A&R Research Report: Artists {today}

BW = Streams across the artist's relevant* catalog from {bw_start_date_tp.strftime("%a %m/%d")} to {latest_date_main.strftime("%a %m/%d")}

Highlighted artists = new this week

""" email_body_main = '' email_body_uk = '' ################################################### # Up in building week, growth in spotify followers, tiktok engagement ################################################### curr_view = get_songs_up_bw(adj_result,n_building_days) curr_view = get_followers_spotify(curr_view,followers_dict,'SPOTIFY') curr_view = drop_sony_orchard(curr_view) ig_group = get_followers_ig(curr_view,followers_dict,'INSTAGRAM') tt_group = get_tiktok_engagement(curr_view,followers_dict) curr_view = curr_view.loc[(curr_view['ARTIST'].isin(ig_group['ARTIST'])) | (curr_view['ARTIST'].isin(tt_group['ARTIST']))] # replace region from spotify with country_of_origin unless country_of_origin is missing. curr_view['COUNTRY_OF_ORIGIN'].fillna(curr_view['REGION'],inplace=True) curr_view.drop('REGION',axis=1,inplace=True) curr_view.rename(columns={'COUNTRY_OF_ORIGIN':'REGION'},inplace=True) curr_view = curr_view.merge(followers_dict['INSTAGRAM'][['SPOTIFY_ARTIST_ID','INSTAGRAM_PROFILE']],how='left',on='SPOTIFY_ARTIST_ID') curr_view['INSTAGRAM_PROFILE'] = curr_view['INSTAGRAM_PROFILE'].str.split('?').str[0].str.rstrip('/') curr_view = curr_view.merge(followers_dict['TIKTOK'][['SPOTIFY_ARTIST_ID','TIKTOK_PROFILE']],how='left',on='SPOTIFY_ARTIST_ID') curr_view['TIKTOK_PROFILE'] = curr_view['TIKTOK_PROFILE'].str.rstrip('/') curr_view.sort_values('GL_WTD_PCT',ascending=False,inplace=True) # store results to snowflake to keep track of new/old if len(curr_view) > 0: to_upload = curr_view[['ARTIST']].copy() to_upload['REPORT_DATE'] = today upload_df_to_snowflake_table(to_upload,'ARTIST_ENGAGEMENT_TRACKING',sf_params) # delete >7 days ago orcd_query(f"delete from awal.awal_ar.ARTIST_ENGAGEMENT_TRACKING where report_date<=$${today-relativedelta(days=7)}$$;",sf_params) # identify new entries new_today = orcd_query(f""" with counts as ( select artist, count(artist) as count from awal.awal_ar.ARTIST_ENGAGEMENT_TRACKING group by artist ), new_today as ( select m.artist from awal.awal_ar.ARTIST_ENGAGEMENT_TRACKING m join counts on counts.artist=m.artist where report_date like $${today}$$ and counts.count = 1 ) select artist from new_today; """,sf_params) if len(new_today) > 0: curr_view['NEW_TODAY'] = curr_view['ARTIST'].isin(new_today['ARTIST']).astype(int) else: curr_view['NEW_TODAY'] = 0 uk_artists = curr_view.loc[curr_view['REGION']=='GB'] email_body_main = email_body_main + f"



ENGAGEMENT + STREAMS ({len(curr_view)} artists)
Spotify follower growth, streaming growth, + Tiktok engagement OR IG follower growth
" email_body_uk = email_body_uk + f"


ENGAGEMENT + STREAMS ({len(uk_artists)} artists)
Spotify follower growth, streaming growth, + Tiktok engagement OR IG follower growth
" for idx,item in curr_view.iterrows(): artist = html.escape(item['ARTIST']) if pd.notna(item['ARTIST']) else "N/A" new_marker = """""" if item['NEW_TODAY']==1 else "" labels = html.escape(item['LABEL']) if pd.notna(item['LABEL']) else "N/A" url = item['TRACK_URL'] n_songs = item['N_SONGS'] genre = "N/A" if pd.isna(item['GENRE']) else item['GENRE'] region = "N/A" if pd.isna(item['REGION']) else item['REGION'] wtd = format_number(item['GL_WTD_TP']) if pd.notna(item['GL_WTD_TP']) else 0 wtd_pct = item['GL_WTD_PCT'] if pd.notna(item['GL_WTD_PCT']) else 0 lw = format_number(item['GL_LW1']) if pd.notna(item['GL_LW1']) else 0 last_release = html.escape(item['LATEST_SONG']) if pd.notna(item['LATEST_SONG']) else "N/A" last_release_pct = item['LATEST_SONG_WTD'] if pd.notna(item['LATEST_SONG_WTD']) else 0 last_release_date = item['LATEST_RELEASE_DATE'] last_release_data = item['LATEST_SONG_DATA'] top_song = html.escape(item['BIGGEST_SONG']) if pd.notna(item['BIGGEST_SONG']) else "N/A" top_song_pct = item['BIGGEST_SONG_WTD'] if pd.notna(item['BIGGEST_SONG_WTD']) else 0 top_song_date = item['BIGGEST_RELEASE_DATE'] top_song_data = item['BIGGEST_SONG_DATA'] spf = format_number(item['SPOTIFY_TOTAL_FOLLOWERS']) if pd.notna(item['SPOTIFY_TOTAL_FOLLOWERS']) else 0 spf_7day = format_number(item['SPOTIFY_FOLLOWERS_7DAY']) if pd.notna(item['SPOTIFY_FOLLOWERS_7DAY']) else 0 tiktok_page = "@" + item['TIKTOK_PROFILE'].split('@')[-1] + "" if pd.notna(item['TIKTOK_PROFILE']) else "N/A" instagram_page = "@" + item['INSTAGRAM_PROFILE'].split('/')[-1] + "" if pd.notna(item['INSTAGRAM_PROFILE']) else "N/A" email_body_curr = f"""\
{new_marker}{artist}
, {n_songs} songs | Aff. labels {labels} | Genres {genre} | Region {region}
BW {wtd}, +{wtd_pct:.0f}% | LW {lw}
Last release "{last_release}" ({last_release_date}), {last_release_pct:.0f}% of BW | Top release "{top_song}" ({top_song_date}), {top_song_pct:.0f}% of BW
Spotify followers {spf} (+{spf_7day} in the last week)
Tiktok {tiktok_page} | IG {instagram_page} """ email_body_main = email_body_main + email_body_curr + '
' if region == 'GB': email_body_uk = email_body_uk + email_body_curr + '
' email_head = email_head + "" email_footer = f"""


Relevant catalog = """ full_email_main = email_head + '' + email_body_main + email_footer + '' full_email_uk = email_head + '' + email_body_uk + email_footer + '' ### email_subject_main = f'A&R Research: Artist Engagement {today}' destination_emails = destination_email_list.loc[destination_email_list['CATEGORY']=='DAILY_ARTIST'] destination_emails = destination_emails['EMAILS'].tolist() email_outcome = prep_and_send_email(email_params,destination_emails,email_subject_main,full_email_main) if email_outcome: logger.info('Main Email sent.') else: logger.info('Error: Main Email did not send') ### uk_artists = [] if len(uk_artists) > 0: email_subject_uk = f'(UK) A&R Research Report: Artist Engagement {today}' destination_emails = destination_email_list.loc[destination_email_list['CATEGORY']=='DAILY_ARTIST_UK'] destination_emails = destination_emails['EMAILS'].tolist() email_outcome = prep_and_send_email(email_params,destination_emails,email_subject_uk,full_email_uk) if email_outcome: logger.info('UK Email sent.') else: logger.info('Error: UK Email did not send') else: logger.info('No results today for Artist Engagement email') ################################################### ################################################### # WEEKLY ARTIST EMAIL ################################################### ################################################### if today.weekday()==0: # monday email_head = f"""\ A&R Research Report: Artist Growth {today}

LW = Streams across the artist's relevant* catalog for the week ending {bw_end_date_lw.strftime("%m/%d/%y")}

""" email_body = '' ################################################### # Ranking top artist month growth ################################################### # Apply tier function to create a new multiplier column adj_result['multiplier'] = adj_result[['GL_LW3', 'GL_LW4']].mean(axis=1).apply(get_multiplier) adj_result['RATIO'] = adj_result['GL_LW1'] / adj_result['GL_LW4'] adj_result['LATEST_RELEASE_DATE'] = pd.to_datetime(adj_result['LATEST_RELEASE_DATE']) # 2. Growth in at least 3 of 4 steps step1 = adj_result['GL_LW3'] > adj_result['GL_LW4'] step2 = adj_result['GL_LW2'] > adj_result['GL_LW3'] step3 = adj_result['GL_LW1'] > adj_result['GL_LW2'] step4 = adj_result['GL_WTD_PCT'] > 0 growth_steps = step1.astype(int) + step2.astype(int) + step3.astype(int) + step4.astype(int) # 3. Ensure average of recent weeks is above the multiplier threshold recent_avg = adj_result[['GL_LW2', 'GL_LW1']].mean(axis=1) early_avg = adj_result[['GL_LW3', 'GL_LW4']].mean(axis=1) # Check that recent average is significantly higher than early average meaningful_growth = recent_avg > (adj_result['multiplier'] * early_avg) # 4. Final filter final_result = adj_result[ (growth_steps >= 2) # streams went up in 2 of 4 weeks & (adj_result['GL_LW1'] > early_avg) # LW1 greater than first 2 weeks & (adj_result['GL_LW2'] > early_avg) # LW2 greater than first 2 weeks & (adj_result['GL_LW3'] > 0.95 * adj_result['GL_LW4']) # LW3 cannot have dropped >5% from LW4 & (adj_result['GL_LW2'] > 0.95 * adj_result['GL_LW3']) # LW2 cannot have dropped >5% from LW3 & (adj_result['GL_LW1'] > 0.95 * adj_result['GL_LW2']) # LW1 cannot have dropped >5% from LW2 & meaningful_growth # avg of most recent 2 weeks at least x amount greater than avg of first 2 weeks & ( (adj_result['GL_LW_PCT'] > 0) | (adj_result['GL_WTD_PCT'] > 0) | (adj_result['GL_LW2'] > adj_result['GL_LW3']) ) # at least either LW2 or LW1 or BW is up & (adj_result['GL_WTD_PCT'] > -20) # BW can't be down more than 20% & (adj_result['GL_LW1'] > 12000 * adj_result['N_SONGS']) # LW1 did at least 12k streams per song, approx & (adj_result['GL_LW2'] > 12000 * adj_result['N_SONGS']) # LW2 did at least 12k streams per song, approx & (adj_result['GL_LW3'] > 12000 * adj_result['N_SONGS']) # LW3 did at least 12k streams per song, approx & (adj_result['GL_LW4'] > 7000 * adj_result['N_SONGS']) # LW4 did at least 7k streams per song, approx & (adj_result['GL_LW1'] < 2500000) # LW1 did under 2.5M streams & (adj_result['N_SONGS'] > 1) # more than 1 song hitting threshold & ( adj_result['LATEST_RELEASE_DATE'] > dt.datetime.strptime('2023-01-01', '%Y-%m-%d') ) # latest release date at least 2023 & ((~(adj_result['multiplier'] == 1.01)) | (adj_result['RATIO'] >= 1.12)) & ((~(adj_result['multiplier'] == 1.05)) | (adj_result['RATIO'] >= 1.15)) & ((~(adj_result['multiplier'] == 1.1)) | (adj_result['RATIO'] >= 1.27)) & ((~(adj_result['multiplier'] == 1.2)) | (adj_result['RATIO'] >= 1.28)) & ((~(adj_result['multiplier'] == 1.3)) | (adj_result['RATIO'] >= 1.4)) & ((~(adj_result['multiplier'] == 1.4)) | (adj_result['RATIO'] >= 1.7)) & ((~(adj_result['multiplier'] == 1.5)) | (adj_result['RATIO'] >= 2)) & (adj_result['LATEST_SONG_WTD']>5) ] curr_view = final_result.sort_values('RATIO',ascending=False) curr_view = drop_sony_orchard(curr_view) tiers = [ curr_view.loc[curr_view['RATIO']>=3], curr_view.loc[ (curr_view['RATIO']<3) & (curr_view['GL_LW1']<100000) ], curr_view.loc[ (curr_view['RATIO']<3) & (curr_view['GL_LW1'].between(100000,750000)) ], curr_view.loc[ (curr_view['RATIO']<3) & (curr_view['GL_LW1']>750000) ] ] tier_headers = [ 'GREATEST GAINERS: LAST 4 WEEKS', 'GROWTH: SMALL STREAMERS', 'GROWTH: MODERATE STREAMERS', 'GROWTH: LARGE STREAMERS (capped at 2.5M streams LW)' ] header_idx = 0 for curr_tier in tiers: email_body = email_body + f"


{tier_headers[header_idx]} ({len(curr_tier)} artists)

" header_idx+=1 curr_view = curr_tier.copy() for idx,item in curr_view.iterrows(): artist = html.escape(item['ARTIST']) if pd.notna(item['ARTIST']) else "N/A" labels = html.escape(item['LABEL']) if pd.notna(item['LABEL']) else "N/A" url = item['TRACK_URL'] n_songs = item['N_SONGS'] # genre = "N/A" if pd.isna(item['GENRE']) else item['GENRE'] # wtd = format_number(item['GL_WTD_TP']) if pd.notna(item['GL_WTD_TP']) else 0 # wtd_pct = item['GL_WTD_PCT'] if pd.notna(item['GL_WTD_PCT']) else 0 lw = format_number(item['GL_LW1']) if pd.notna(item['GL_LW1']) else 0 lw4 = format_number(item['GL_LW4']) if pd.notna(item['GL_LW4']) else 0 pct = item['RATIO'] if pd.notna(item['RATIO']) else 0 # last_release = html.escape(item['LATEST_SONG']) if pd.notna(item['LATEST_SONG']) else "N/A" # last_release_pct = item['LATEST_SONG_WTD'] if pd.notna(item['LATEST_SONG_WTD']) else 0 # last_release_date = item['LATEST_RELEASE_DATE'] # top_song = html.escape(item['BIGGEST_SONG']) if pd.notna(item['BIGGEST_SONG']) else "N/A" # top_song_pct = item['BIGGEST_SONG_WTD'] if pd.notna(item['BIGGEST_SONG_WTD']) else 0 # top_song_date = item['BIGGEST_RELEASE_DATE'] # spf = format_number(item['SPOTIFY_TOTAL_FOLLOWERS']) if pd.notna(item['SPOTIFY_TOTAL_FOLLOWERS']) else 0 # spf_7day = format_number(item['SPOTIFY_FOLLOWERS_7DAY']) if pd.notna(item['SPOTIFY_FOLLOWERS_7DAY']) else 0 # tiktok_page = "@" + item['TIKTOK_PROFILE'].split('@')[-1] + "" if pd.notna(item['TIKTOK_PROFILE']) else "N/A" # instagram_page = "@" + item['INSTAGRAM_PROFILE'].split('/')[-1] + "" if pd.notna(item['INSTAGRAM_PROFILE']) else "N/A" email_body_curr = f"""\ {artist}, {n_songs} songs | Aff. labels {labels} | LW {lw} ← 4W {lw4} """ email_body = email_body + email_body_curr + '
' email_head = email_head + "" email_footer = f"""


Relevant catalog = """ full_email = email_head + '' + email_body + email_footer + '' email_subject = f'A&R Research Report: Artist Growth {today}' destination_emails = destination_email_list.loc[destination_email_list['CATEGORY']=='WEEKLY_ARTIST'] destination_emails = destination_emails['EMAILS'].tolist() email_outcome = prep_and_send_email(email_params,destination_emails,email_subject,full_email) if email_outcome: logger.info('Email sent.') else: logger.info('Error: Email did not send') else: pass # get today's date based on the current time et_timezone = pytz.timezone('US/Eastern') today = dt.datetime.now(et_timezone).date() if run_sold_out_shows and ((today.weekday()==0) or (today.weekday()==3)): ################################################### ################################################### # SOLD OUT SHOWS ################################################### ################################################### logger.info('Sold out shows...') # variable to store the master result full_list = [] HEADERS = {'User-Agent': 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'} try: venue_name = 'Bowery Ballroom' logger.info(f'{venue_name}...') result = pull_bowery_mercury(HEADERS,venue_name) result['VENUE'] = venue_name result['REGION'] = 'NYC' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Mercury Lounge' logger.info(f'{venue_name}...') result = pull_bowery_mercury(HEADERS,venue_name) result['VENUE'] = venue_name result['REGION'] = 'NYC' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Elsewhere' logger.info(f'{venue_name}...') result = pull_elsewhere(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'NYC' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Babys All Right' logger.info(f'{venue_name}...') result = pull_babysallright(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'NYC' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'TV Eye' logger.info(f'{venue_name}...') result = pull_tveye(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'NYC' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Purgatory' logger.info(f'{venue_name}...') result = pull_purgatory(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'NYC' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Hotel Cafe' logger.info(f'{venue_name}...') result = pull_hotelcafe(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'LA' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Bellwether' logger.info(f'{venue_name}...') result = pull_bellwether(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'LA' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'The Echo/Echoplex' logger.info(f'{venue_name}...') result = pull_theecho(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'LA' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Troubadour' logger.info(f'{venue_name}...') result = pull_troubadour(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'LA' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'The Mint' logger.info(f'{venue_name}...') result = pull_themint(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'LA' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Cafe Du Nord' logger.info(f'{venue_name}...') result1,result2 = pull_cafedunord_swedishamericanhall(HEADERS) # don't need to attach venue and region here because already pre-attached in helper function full_list.append(result1) full_list.append(result2) logger.info(f'{len(result1) + len(result2)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'The Basement' logger.info(f'{venue_name}...') result = pull_basement(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Nashville' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'PhilaMOCA' logger.info(f'{venue_name}...') result = pull_philamoca(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Philadelphia' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Johnny Brendas' logger.info(f'{venue_name}...') result = pull_johnnybrendas(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Philadelphia' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'The Earl' logger.info(f'{venue_name}...') result = pull_theearl(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Atlanta' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Aisle 5' logger.info(f'{venue_name}...') result = pull_aisle5(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Atlanta' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Eddies Attic' logger.info(f'{venue_name}...') result = pull_eddiesattic(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Atlanta' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Middle East (Upstairs)' logger.info(f'{venue_name}...') result = pull_middleeastupstairs(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Boston' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'The Sinclair' logger.info(f'{venue_name}...') result = pull_sinclairboston(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Boston' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'The Music Box' logger.info(f'{venue_name}...') result = pull_themusicbox(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'San Diego' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Soma (Side Stage)' logger.info(f'{venue_name}...') result = pull_somasidestage(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'San Diego' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Schubas/Lincoln Hall' logger.info(f'{venue_name}...') result = pull_schubas_lh(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Chicago' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Empty Bottle' logger.info(f'{venue_name}...') result = pull_emptybottle(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Chicago' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'Broadberry/Richmond Music Hall' logger.info(f'{venue_name}...') result = pull_broadberryco(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Richmond, VA' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'The Camel' logger.info(f'{venue_name}...') result = pull_thecamel(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Richmond, VA' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') try: venue_name = 'The Crocodile' logger.info(f'{venue_name}...') result = pull_thecrocodile(HEADERS) result['VENUE'] = venue_name result['REGION'] = 'Seattle' full_list.append(result) logger.info(f'{len(result)} results') except Exception as e: logger.info(f'ERROR - {venue_name}') # concat all results full_list = pd.concat(full_list) full_list.reset_index(drop=True,inplace=True) # remove off-limits artists full_list = remove_off_limits_artists(full_list,sf_params) full_list.drop(full_list.loc[full_list['NAME'].str.contains('Private Event',na=False)].index,inplace=True) # remove apostrophes full_list['NAME'] = full_list['NAME'].str.replace("\'","") # add date full_list['DATE_UPDATED'] = today # In case an artist has a second show added on same day, rename the date with "show #2" dup_mask = full_list.duplicated(['NAME', 'DATE', 'VENUE']) full_list.loc[dup_mask, 'DATE'] = ( full_list.loc[dup_mask, 'DATE'].astype(str) + ' - show #2' ) # upload today's result upload_df_to_snowflake_table(full_list,'SOLD_OUT_SHOWS',sf_params) logger.info('Organizing...') ## Organizing sold out shows ## # only interested in the sold out shows is_sold_out = full_list.loc[full_list['IS_SOLD_OUT']==True] # Convert to list of tuples tuple_list = list(is_sold_out[['NAME','DATE','VENUE']].itertuples(index=False, name=None)) # Filter out any tuple containing None filtered_tuples = [row for row in tuple_list if all(v is not None for v in row)] # Convert to a string like: "(101, '2023-01-01'), (102, '2023-01-02')" values_str = ', '.join([f"({repr(event_name)}, {repr(event_date)}, {repr(event_venue)})" for event_name, event_date, event_venue in filtered_tuples]) historical = orcd_query(f""" select name, date, venue, is_sold_out, date_updated from awal.awal_ar.sold_out_shows where 1=1 and (NAME, DATE, VENUE) IN ({values_str}) and DATE_UPDATED != $${today}$$ """,sf_params) if len(historical) > 0: # here, newly sold out shows today would have IS_SOLD_OUT=0 for historical grouped_df = historical.groupby(['NAME','DATE','VENUE']).agg( IS_SOLD_OUT=('IS_SOLD_OUT', 'sum'), DAYS_TO_SELL_OUT=('NAME', 'count') ).reset_index() # merge left so can include new listings that may have sold out on day of df = is_sold_out.merge(grouped_df,how='left',on=['NAME','DATE','VENUE']) df['IS_SOLD_OUT_y'] = df['IS_SOLD_OUT_y'].fillna(0) df['DAYS_TO_SELL_OUT'] = df['DAYS_TO_SELL_OUT'].fillna(0) # newly_sold_out newly_sold_out = df.loc[df['IS_SOLD_OUT_y']==0] newly_sold_out['NEWLY_SOLD_OUT'] = 1 else: newly_sold_out = is_sold_out.copy() newly_sold_out['DAYS_TO_SELL_OUT'] = 0 newly_sold_out['NEWLY_SOLD_OUT'] = 1 # if there is a newly sold out show, we continue to organize and send the email if len(newly_sold_out) > 0: # merge back to is_sold_out is_sold_out = is_sold_out.merge(newly_sold_out[['NAME','DATE','VENUE','NEWLY_SOLD_OUT','DAYS_TO_SELL_OUT']],how='left',on=['NAME','DATE','VENUE']) is_sold_out['NEWLY_SOLD_OUT'] = is_sold_out['NEWLY_SOLD_OUT'].fillna(0) is_sold_out.sort_values(['REGION','VENUE','NAME'],inplace=True) email_body = '' for region in is_sold_out['REGION'].drop_duplicates(): # print region header here email_body = email_body + f'
{region}

' region_df = is_sold_out.loc[is_sold_out['REGION']==region] for venue in region_df['VENUE'].drop_duplicates(): # print venue header here email_body = email_body + f'{venue}
' if venue == 'Schubas/Lincoln Hall': email_body = email_body + 'Note - shows via AXS are not included here
' venue_df = region_df.loc[region_df['VENUE']==venue] # print shows here email_body = email_body + print_shows(venue_df) email_subject = f'A&R Research: Sold Out Shows {today}' email_head = f"""\ Sold Out Shows as of {today}

You are receiving this email because a show(s) just sold out (highlighted). If a venue does not appear here, it is either not included in our daily pull or there are no sold-out shows at this time.
NOTE - Signed artists may show up in this email as well as comedy acts, DJ events etc.


""" full_email = email_head + email_body destination_emails = destination_email_list.loc[destination_email_list['CATEGORY']=='SOLD_OUT_SHOWS'] destination_emails = destination_emails['EMAILS'].tolist() email_outcome = prep_and_send_email(email_params,destination_emails,email_subject,full_email) if email_outcome: logger.info('Email sent.') else: logger.info('Error: Email did not send') else: logger.info('No newly sold out shows - no need to send email') logger.info('Script complete') return { "statusCode": 200, "message": "Handler function ran successfully" } # if __name__ == "__main__": # handler({}, None)