• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jyablonski / python_docker / 14283764026

05 Apr 2025 04:30PM UTC coverage: 83.114% (-0.2%) from 83.333%
14283764026

push

github

web-flow
Ingestion v1.13.8 (#92)

### Description
UV Update

## Added
- UV for Package Management
- [migrate-to-uv](https://github.com/mkniewallner/migrate-to-uv) Package
was used for the Poetry -> UV Migration
- `scripts/backfill.py` Script for Boxscore + PBP Backfills

## Updated
- `Dockerfile` to include UV for Package Management
- Adjusted various Exceptions & Exception handling to include best
practices

## Deleted
- Poetry gahbage

0 of 21 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

822 of 989 relevant lines covered (83.11%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.49
/src/utils.py
1
from datetime import date, datetime, timedelta
1✔
2
import hashlib
1✔
3
import json
1✔
4
import logging
1✔
5
import os
1✔
6
import re
1✔
7
import time
1✔
8
from typing import Any, Callable
1✔
9

10
import awswrangler as wr
1✔
11
from bs4 import BeautifulSoup
1✔
12
from nltk.sentiment import SentimentIntensityAnalyzer
1✔
13
import numpy as np
1✔
14
import pandas as pd
1✔
15
import praw
1✔
16
import requests
1✔
17
from sqlalchemy.engine.base import Connection, Engine
1✔
18
import sentry_sdk
1✔
19

20
# import tweepy
21

22
sentry_sdk.init(os.environ.get("SENTRY_TOKEN"), traces_sample_rate=1.0)
1✔
23
sentry_sdk.set_user({"email": "jyablonski9@gmail.com"})
1✔
24

25

26
def time_function(func: Callable[..., Any]) -> Callable[..., Any]:
1✔
27
    """
28
    Decorator function used to record the execution time of any
29
    function it's applied to.
30

31
    Args:
32
        func (Callable): Function to track the execution time on.
33

34
    Returns:
35
        Callable[..., Any]: The wrapped function that records
36
            the execution time.
37
    """
38

39
    def wrapper(*args, **kwargs):
1✔
40
        start_time = time.time()
1✔
41
        result = func(*args, **kwargs)
1✔
42
        total_func_time = round(time.time() - start_time, 2)
1✔
43
        logging.info(f"{func.__name__} took {total_func_time} seconds")
1✔
44

45
        return result
1✔
46

47
    return wrapper
1✔
48

49

50
def filter_spread(value: str) -> str:
1✔
51
    """
52
    Filter out 3-digit values from the `spread` column
53
    in the Scrape Odds Function such as `-108` or `-112`
54

55
    Parameters:
56
        value (str): The original value from the spread column.
57

58
    Returns:
59
        The spread value without any 3-digit values present
60
    """
61
    parts = value.split()
1✔
62
    filtered_parts = [
1✔
63
        (
64
            part
65
            if (part[0] in ["+", "-"] and float(part[1:]) <= 25)
66
            or (part.isdigit() and int(part) <= 25)
67
            else ""
68
        )
69
        for part in parts
70
    ]
71
    result = " ".join(filtered_parts).strip()
1✔
72

73
    # this last part strips out a couple extra white spaces
74
    return re.sub(r"\s+", " ", result)
1✔
75

76

77
def get_season_type(todays_date: date | None = None) -> str:
1✔
78
    """
79
    Function to generate Season Type for a given Date.
80
    **2025-03-16 NOTE** this has been deprecated as this logic
81
    belongs in the dbt project
82

83
    Args:
84
        todays_date (date): The Date to generate a Season Type for.  Defaults to
85
            today's date.
86

87
    Returns:
88
        The Season Type for Given Date
89
    """
90
    if todays_date is None:
1✔
UNCOV
91
        todays_date = datetime.now().date()
×
92

93
    if todays_date < datetime(2025, 4, 15).date():
1✔
94
        season_type = "Regular Season"
1✔
95
    elif (todays_date >= datetime(2025, 4, 16).date()) & (
1✔
96
        todays_date < datetime(2025, 4, 21).date()
97
    ):
98
        season_type = "Play-In"
1✔
99
    else:
100
        season_type = "Playoffs"
1✔
101

102
    return season_type
1✔
103

104

105
def check_schedule(date: datetime.date) -> bool:
1✔
106
    """
107
    Small Function used in Boxscores + PBP Functions to check if
108
    there are any games scheduled for a given date.
109

110
    Args:
111
        date (datetime.date): The Date to check for games on.
112

113
    Returns:
114
        Boolean: True if there are games scheduled, False if not.
115
    """
116
    schedule_endpoint = f"https://api.jyablonski.dev/schedule?date={date}"
1✔
117
    schedule_data = requests.get(schedule_endpoint).json()
1✔
118

119
    return True if len(schedule_data) > 0 else False
1✔
120

121

122
def add_sentiment_analysis(df: pd.DataFrame, sentiment_col: str) -> pd.DataFrame:
1✔
123
    """
124
    Function to add Sentiment Analysis columns to a DataFrame via nltk Vader Lexicon.
125

126
    Args:
127
        df (pd.DataFrame): The Pandas DataFrame
128

129
        sentiment_col (str): The Column in the DataFrame to run Sentiment Analysis on
130
            (comments / tweets etc).
131

132
    Returns:
133
        The same DataFrame but with the Sentiment Analysis columns attached.
134
    """
135
    try:
1✔
136
        analyzer = SentimentIntensityAnalyzer()
1✔
137
        df["compound"] = [
1✔
138
            analyzer.polarity_scores(x)["compound"] for x in df[sentiment_col]
139
        ]
140
        df["neg"] = [analyzer.polarity_scores(x)["neg"] for x in df[sentiment_col]]
1✔
141
        df["neu"] = [analyzer.polarity_scores(x)["neu"] for x in df[sentiment_col]]
1✔
142
        df["pos"] = [analyzer.polarity_scores(x)["pos"] for x in df[sentiment_col]]
1✔
143
        df["sentiment"] = np.where(df["compound"] > 0, 1, 0)
1✔
144
        return df
1✔
NEW
145
    except Exception as e:
×
146
        logging.error(f"Error Occurred while adding Sentiment Analysis, {e}")
×
147
        sentry_sdk.capture_exception(e)
×
NEW
148
        raise
×
149

150

151
def get_leading_zeroes(value: int) -> str:
1✔
152
    """
153
    Function to add leading zeroes to a month (1 (January) -> 01).
154
    Used in the the `write_to_s3` function.
155

156
    Args:
157
        value (int): The value integer (created from `datetime.now().month`)
158

159
    Returns:
160
        The same value integer with a leading 0 if it is less than 10
161
            (Nov/Dec aka 11/12 unaffected).
162
    """
163
    if len(str(value)) > 1:
1✔
164
        return str(value)
1✔
165
    else:
166
        return f"0{value}"
1✔
167

168

169
def clean_player_names(name: str) -> str:
1✔
170
    """
171
    Function to remove suffixes from a player name.
172

173
    Args:
174
        name (str): The raw player name you wish to alter.
175

176
    Returns:
177
        str: Cleaned Name w/ no suffix bs
178
    """
179
    try:
1✔
180
        cleaned_name = (
1✔
181
            name.replace(" Jr.", "")
182
            .replace(" Sr.", "")
183
            .replace(" III", "")  # III HAS TO GO FIRST, OVER II
184
            .replace(" II", "")  # or else Robert Williams III -> Robert WilliamsI
185
            .replace(" IV", "")
186
        )
187
        return cleaned_name
1✔
NEW
188
    except Exception as e:
×
189
        logging.error(f"Error Occurred with Clean Player Names, {e}")
×
190
        sentry_sdk.capture_exception(e)
×
NEW
191
        raise
×
192

193

194
@time_function
1✔
195
def get_player_stats_data(feature_flags_df: pd.DataFrame) -> pd.DataFrame:
1✔
196
    """
197
    Web Scrape function w/ BS4 that grabs aggregate season stats
198

199
    Args:
200
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to
201
            check whether to run this function or not
202

203
    Returns:
204
        DataFrame of Player Aggregate Season stats
205
    """
206
    feature_flag = "stats"
1✔
207
    feature_flag_check = check_feature_flag(
1✔
208
        flag=feature_flag, flags_df=feature_flags_df
209
    )
210

211
    if feature_flag_check is False:
1✔
212
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
213
        df = pd.DataFrame()
×
214
        return df
×
215

216
    # stats = stats.rename(columns={"fg%": "fg_pct", "3p%": "3p_pct",
217
    # "2p%": "2p_pct", "efg%": "efg_pct", "ft%": "ft_pct"})
218
    try:
1✔
219
        year_stats = 2025
1✔
220
        url = f"https://www.basketball-reference.com/leagues/NBA_{year_stats}_per_game.html"
1✔
221
        html = requests.get(url).content
1✔
222
        soup = BeautifulSoup(html, "html.parser")
1✔
223
        headers = [th.getText() for th in soup.findAll("tr", limit=2)[0].findAll("th")]
1✔
224
        headers = headers[1:]
1✔
225
        rows = soup.findAll("tr")[1:]
1✔
226
        player_stats = [
1✔
227
            [td.getText() for td in rows[i].findAll("td")] for i in range(len(rows))
228
        ]
229
        stats = pd.DataFrame(player_stats, columns=headers)
1✔
230
        stats["PTS"] = pd.to_numeric(stats["PTS"])
1✔
231
        stats = stats.query("Player == Player").reset_index()
1✔
232
        stats["Player"] = (
1✔
233
            stats["Player"]
234
            .str.normalize("NFKD")
235
            .str.encode("ascii", errors="ignore")
236
            .str.decode("utf-8")
237
        )
238
        stats.columns = stats.columns.str.lower()
1✔
239
        stats["scrape_date"] = datetime.now().date()
1✔
240
        stats = stats.drop(columns=["index", "awards"], axis=1)
1✔
241
        logging.info(
1✔
242
            "General Stats Transformation Function Successful, "
243
            f"retrieving {len(stats)} updated rows"
244
        )
245
        return stats
1✔
NEW
246
    except Exception as error:
×
247
        logging.error(f"General Stats Extraction Function Failed, {error}")
×
248
        sentry_sdk.capture_exception(error)
×
249
        df = pd.DataFrame()
×
250
        return df
×
251

252

253
@time_function
1✔
254
def get_boxscores_data(
1✔
255
    feature_flags_df: pd.DataFrame,
256
    month: int = (datetime.now() - timedelta(1)).month,
257
    day: int = (datetime.now() - timedelta(1)).day,
258
    year: int = (datetime.now() - timedelta(1)).year,
259
) -> pd.DataFrame:
260
    """
261
    Function that grabs box scores from a given date in mmddyyyy
262
    format - defaults to yesterday.  values can be ex. 1 or 01.
263
    Can't use `read_html` for this so this is raw web scraping baby.
264

265
    Args:
266
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to
267
            check whether to run this function or not
268

269
        month (int): month value of the game played (0 - 12)
270

271
        day (int): day value of the game played (1 - 31)
272

273
        year (int): year value of the game played (2021)
274

275
    Returns:
276
        DataFrame of Player Aggregate Season stats
277
    """
278
    day = get_leading_zeroes(value=day)
1✔
279
    month = get_leading_zeroes(value=month)
1✔
280

281
    feature_flag = "boxscores"
1✔
282
    feature_flag_check = check_feature_flag(
1✔
283
        flag=feature_flag, flags_df=feature_flags_df
284
    )
285

286
    if feature_flag_check is False:
1✔
287
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
288
        df = pd.DataFrame()
×
289
        return df
×
290

291
    url = f"https://www.basketball-reference.com/friv/dailyleaders.fcgi?month={month}&day={day}&year={year}&type=all"
1✔
292
    date = f"{year}-{month}-{day}"
1✔
293

294
    try:
1✔
295
        html = requests.get(url).content
1✔
296
        soup = BeautifulSoup(html, "html.parser")
1✔
297
        headers = [th.getText() for th in soup.findAll("tr", limit=2)[0].findAll("th")]
1✔
298
        headers = headers[1:]
1✔
299
        headers[1] = "Team"
1✔
300
        headers[2] = "Location"
1✔
301
        headers[3] = "Opponent"
1✔
302
        headers[4] = "Outcome"
1✔
303
        headers[6] = "FGM"
1✔
304
        headers[8] = "FGPercent"
1✔
305
        headers[9] = "threePFGMade"
1✔
306
        headers[10] = "threePAttempted"
1✔
307
        headers[11] = "threePointPercent"
1✔
308
        headers[14] = "FTPercent"
1✔
309
        headers[15] = "OREB"
1✔
310
        headers[16] = "DREB"
1✔
311
        headers[24] = "PlusMinus"
1✔
312

313
        rows = soup.findAll("tr")[1:]
1✔
314
        player_stats = [
1✔
315
            [td.getText() for td in rows[i].findAll("td")] for i in range(len(rows))
316
        ]
317

318
        df = pd.DataFrame(player_stats, columns=headers)
1✔
319

320
        df[
1✔
321
            [
322
                "FGM",
323
                "FGA",
324
                "FGPercent",
325
                "threePFGMade",
326
                "threePAttempted",
327
                "threePointPercent",
328
                "OREB",
329
                "DREB",
330
                "TRB",
331
                "AST",
332
                "STL",
333
                "BLK",
334
                "TOV",
335
                "PF",
336
                "PTS",
337
                "PlusMinus",
338
                "GmSc",
339
            ]
340
        ] = df[
341
            [
342
                "FGM",
343
                "FGA",
344
                "FGPercent",
345
                "threePFGMade",
346
                "threePAttempted",
347
                "threePointPercent",
348
                "OREB",
349
                "DREB",
350
                "TRB",
351
                "AST",
352
                "STL",
353
                "BLK",
354
                "TOV",
355
                "PF",
356
                "PTS",
357
                "PlusMinus",
358
                "GmSc",
359
            ]
360
        ].apply(
361
            pd.to_numeric
362
        )
363
        df["date"] = str(year) + "-" + str(month) + "-" + str(day)
1✔
364
        df["date"] = pd.to_datetime(df["date"])
1✔
365
        df["Location"] = df["Location"].apply(lambda x: "A" if x == "@" else "H")
1✔
366
        df["Team"] = df["Team"].str.replace("PHO", "PHX")
1✔
367
        df["Team"] = df["Team"].str.replace("CHO", "CHA")
1✔
368
        df["Team"] = df["Team"].str.replace("BRK", "BKN")
1✔
369
        df["Opponent"] = df["Opponent"].str.replace("PHO", "PHX")
1✔
370
        df["Opponent"] = df["Opponent"].str.replace("CHO", "CHA")
1✔
371
        df["Opponent"] = df["Opponent"].str.replace("BRK", "BKN")
1✔
372
        df = df.query("Player == Player").reset_index(drop=True)
1✔
373
        df["Player"] = (
1✔
374
            df["Player"]
375
            .str.normalize("NFKD")  # this is removing all accented characters
376
            .str.encode("ascii", errors="ignore")
377
            .str.decode("utf-8")
378
        )
379
        df["scrape_date"] = datetime.now().date()
1✔
380
        df.columns = df.columns.str.lower()
1✔
381
        logging.info(
1✔
382
            "Box Score Transformation Function Successful, "
383
            f"retrieving {len(df)} rows for {date}"
384
        )
385
        return df
1✔
386
    except IndexError:
1✔
387

388
        # if no boxscores available, check the schedule. this will log an error
389
        # if there are games played and the data isnt available yet, or log a
390
        # message that no games were found bc there were no games played on that date
391
        is_games_played = check_schedule(date=date)
1✔
392

393
        if is_games_played:
1✔
394
            logging.error(
1✔
395
                "Box Scores Function Failed, Box Scores aren't available yet "
396
                f"for {date}"
397
            )
398
        else:
399
            logging.info(
1✔
400
                f"Box Scores Function Warning, no games played on {date} so "
401
                "no data available"
402
            )
403

404
        return pd.DataFrame()
1✔
405

NEW
406
    except Exception as error:
×
407
        logging.error(f"Box Scores Function Failed, {error}")
×
408
        sentry_sdk.capture_exception(error)
×
409
        df = pd.DataFrame()
×
410
        return df
×
411

412

413
@time_function
1✔
414
def get_opp_stats_data(feature_flags_df: pd.DataFrame) -> pd.DataFrame:
1✔
415
    """
416
    Web Scrape function w/ pandas read_html that grabs all
417
        regular season opponent team stats
418

419
    Args:
420
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame
421
            to check whether to run this function or not
422

423
    Returns:
424
        Pandas DataFrame of all current team opponent stats
425
    """
426
    feature_flag = "opp_stats"
1✔
427
    feature_flag_check = check_feature_flag(
1✔
428
        flag=feature_flag, flags_df=feature_flags_df
429
    )
430

431
    if feature_flag_check is False:
1✔
432
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
433
        df = pd.DataFrame()
×
434
        return df
×
435

436
    year = (datetime.now() - timedelta(1)).year
1✔
437
    month = (datetime.now() - timedelta(1)).month
1✔
438
    day = (datetime.now() - timedelta(1)).day
1✔
439
    year_stats = 2025
1✔
440

441
    try:
1✔
442
        url = f"https://www.basketball-reference.com/leagues/NBA_{year_stats}.html"
1✔
443
        df = pd.read_html(url)[5]
1✔
444
        df = df[["Team", "FG%", "3P%", "3P", "PTS"]]
1✔
445
        df = df.rename(
1✔
446
            columns={
447
                df.columns[0]: "team",
448
                df.columns[1]: "fg_percent_opp",
449
                df.columns[2]: "threep_percent_opp",
450
                df.columns[3]: "threep_made_opp",
451
                df.columns[4]: "ppg_opp",
452
            }
453
        )
454
        df = df.query('team != "League Average"')
1✔
455
        df = df.reset_index(drop=True)
1✔
456
        df["scrape_date"] = datetime.now().date()
1✔
457
        logging.info(
1✔
458
            "Opp Stats Transformation Function Successful, "
459
            f"retrieving {len(df)} rows for {year}-{month}-{day}"
460
        )
461
        return df
1✔
NEW
462
    except Exception as error:
×
463
        logging.error(f"Opp Stats Web Scrape Function Failed, {error}")
×
464
        sentry_sdk.capture_exception(error)
×
465
        df = pd.DataFrame()
×
466
        return df
×
467

468

469
@time_function
1✔
470
def get_injuries_data(feature_flags_df: pd.DataFrame) -> pd.DataFrame:
1✔
471
    """
472
    Web Scrape function w/ pandas read_html that grabs all current injuries
473

474
    Args:
475
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to check
476
            whether to run this function or not
477

478
    Returns:
479
        Pandas DataFrame of all current player injuries & their associated team
480
    """
481
    feature_flag = "injuries"
1✔
482
    feature_flag_check = check_feature_flag(
1✔
483
        flag=feature_flag, flags_df=feature_flags_df
484
    )
485

486
    if feature_flag_check is False:
1✔
487
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
488
        df = pd.DataFrame()
×
489
        return df
×
490

491
    try:
1✔
492
        url = "https://www.basketball-reference.com/friv/injuries.fcgi"
1✔
493
        df = pd.read_html(url)[0]
1✔
494
        df = df.rename(columns={"Update": "Date"})
1✔
495
        df.columns = df.columns.str.lower()
1✔
496
        df["scrape_date"] = datetime.now().date()
1✔
497
        df["player"] = (
1✔
498
            df["player"]
499
            .str.normalize("NFKD")  # this is removing all accented characters
500
            .str.encode("ascii", errors="ignore")
501
            .str.decode("utf-8")
502
        )
503
        df["player"] = df["player"].apply(clean_player_names)
1✔
504
        df = df.drop_duplicates()
1✔
505
        logging.info(
1✔
506
            f"Injury Transformation Function Successful, retrieving {len(df)} rows"
507
        )
508
        return df
1✔
NEW
509
    except Exception as error:
×
510
        logging.error(f"Injury Web Scrape Function Failed, {error}")
×
511
        sentry_sdk.capture_exception(error)
×
512
        df = pd.DataFrame()
×
513
        return df
×
514

515

516
@time_function
1✔
517
def get_transactions_data(feature_flags_df: pd.DataFrame) -> pd.DataFrame:
1✔
518
    """
519
    Web Scrape function w/ BS4 that retrieves NBA Trades, signings, waivers etc.
520

521
    Args:
522
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to check whether
523
            to run this function or not
524

525
    Returns:
526
        Pandas DataFrame of all season transactions, trades, player waives etc.
527
    """
528
    feature_flag = "transactions"
1✔
529
    feature_flag_check = check_feature_flag(
1✔
530
        flag=feature_flag, flags_df=feature_flags_df
531
    )
532

533
    if feature_flag_check is False:
1✔
534
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
535
        df = pd.DataFrame()
×
536
        return df
×
537

538
    try:
1✔
539
        url = "https://www.basketball-reference.com/leagues/NBA_2025_transactions.html"
1✔
540
        html = requests.get(url).content
1✔
541
        soup = BeautifulSoup(html, "html.parser")
1✔
542
        # theres a bunch of garbage in the first 50 rows - no matter what
543
        trs = soup.findAll("li")[70:]
1✔
544
        rows = []
1✔
545
        mylist = []
1✔
546
        for tr in trs:
1✔
547
            date = tr.find("span")
1✔
548
            # needed bc span can be null (multi <p> elements per span)
549
            if date is not None:
1✔
550
                date = date.text
1✔
551
            data = tr.findAll("p")
1✔
552
            for p in data:
1✔
553
                mylist.append(p.text)
1✔
554
            data3 = [date] + [mylist]
1✔
555
            rows.append(data3)
1✔
556
            mylist = []
1✔
557

558
        transactions = pd.DataFrame(rows)
1✔
559
        transactions.columns = ["Date", "Transaction"]
1✔
560
        transactions = transactions.query(
1✔
561
            'Date == Date & Date != ""'
562
        ).reset_index()  # filters out nulls and empty values
563
        transactions = transactions.explode("Transaction")
1✔
564
        transactions["Date"] = transactions["Date"].str.replace(
1✔
565
            "\\?", "October 1, 2024", regex=True  # bad data 10-14-21
566
        )
567
        transactions["Date"] = pd.to_datetime(transactions["Date"])
1✔
568
        transactions.columns = transactions.columns.str.lower()
1✔
569
        transactions = transactions[["date", "transaction"]]
1✔
570
        transactions["scrape_date"] = datetime.now().date()
1✔
571
        transactions = transactions.drop_duplicates()
1✔
572
        logging.info(
1✔
573
            "Transactions Transformation Function Successful, "
574
            f"retrieving {len(transactions)} rows"
575
        )
576
        return transactions
1✔
NEW
577
    except Exception as error:
×
578
        logging.error(f"Transaction Web Scrape Function Failed, {error}")
×
579
        sentry_sdk.capture_exception(error)
×
580
        df = pd.DataFrame()
×
581
        return df
×
582

583

584
@time_function
1✔
585
def get_advanced_stats_data(feature_flags_df: pd.DataFrame) -> pd.DataFrame:
1✔
586
    """
587
    Web Scrape function w/ pandas read_html that grabs all team advanced stats
588

589
    Args:
590
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to check
591
            whether to run this function or not
592

593
    Returns:
594
        DataFrame of all current Team Advanced Stats
595
    """
596
    feature_flag = "adv_stats"
1✔
597
    feature_flag_check = check_feature_flag(
1✔
598
        flag=feature_flag, flags_df=feature_flags_df
599
    )
600

601
    if feature_flag_check is False:
1✔
602
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
603
        df = pd.DataFrame()
×
604
        return df
×
605

606
    year_stats = 2025
1✔
607
    try:
1✔
608
        url = f"https://www.basketball-reference.com/leagues/NBA_{year_stats}.html"
1✔
609
        df = pd.read_html(url)
1✔
610
        df = pd.DataFrame(df[10])
1✔
611
        df.drop(columns=df.columns[0], axis=1, inplace=True)
1✔
612
        df.columns = [
1✔
613
            "Team",
614
            "Age",
615
            "W",
616
            "L",
617
            "PW",
618
            "PL",
619
            "MOV",
620
            "SOS",
621
            "SRS",
622
            "ORTG",
623
            "DRTG",
624
            "NRTG",
625
            "Pace",
626
            "FTr",
627
            "3PAr",
628
            "TS%",
629
            "bby1",  # the bby columns are because of hierarchical html formatting
630
            "eFG%",
631
            "TOV%",
632
            "ORB%",
633
            "FT/FGA",
634
            "bby2",
635
            "eFG%_opp",
636
            "TOV%_opp",
637
            "DRB%_opp",
638
            "FT/FGA_opp",
639
            "bby3",
640
            "Arena",
641
            "Attendance",
642
            "Att/Game",
643
        ]
644
        df.drop(["bby1", "bby2", "bby3"], axis=1, inplace=True)
1✔
645
        df = df.query('Team != "League Average"').reset_index()
1✔
646
        # Playoff teams get a * next to them ??  fkn stupid, filter it out.
647
        df["Team"] = df["Team"].str.replace("\\*", "", regex=True)
1✔
648
        df["scrape_date"] = datetime.now().date()
1✔
649
        df.columns = df.columns.str.lower()
1✔
650
        logging.info(
1✔
651
            """
652
            Advanced Stats Transformation Function Successful,
653
            retrieving updated data for 30 Teams
654
            """
655
        )
656
        return df
1✔
NEW
657
    except Exception as error:
×
658
        logging.error(f"Advanced Stats Web Scrape Function Failed, {error}")
×
659
        sentry_sdk.capture_exception(error)
×
660
        df = pd.DataFrame()
×
661
        return df
×
662

663

664
@time_function
1✔
665
def get_shooting_stats_data(feature_flags_df: pd.DataFrame) -> pd.DataFrame:
1✔
666
    """
667
    Web Scrape function w/ pandas read_html that grabs all raw shooting stats
668

669
    Args:
670
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to check whether
671
            to run this function or not
672

673
    Returns:
674
        DataFrame of raw shooting stats
675
    """
676
    feature_flag = "shooting_stats"
1✔
677
    feature_flag_check = check_feature_flag(
1✔
678
        flag=feature_flag, flags_df=feature_flags_df
679
    )
680

681
    if feature_flag_check is False:
1✔
682
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
683
        df = pd.DataFrame()
×
684
        return df
×
685

686
    year_stats = 2025
1✔
687
    try:
1✔
688
        url = f"https://www.basketball-reference.com/leagues/NBA_{year_stats}_shooting.html"
1✔
689
        df = pd.read_html(url)[0]
1✔
690
        df.columns = df.columns.to_flat_index()
1✔
691
        df = df.rename(
1✔
692
            columns={
693
                df.columns[1]: "player",
694
                df.columns[6]: "mp",
695
                df.columns[8]: "avg_shot_distance",
696
                df.columns[10]: "pct_fga_2p",
697
                df.columns[11]: "pct_fga_0_3",
698
                df.columns[12]: "pct_fga_3_10",
699
                df.columns[13]: "pct_fga_10_16",
700
                df.columns[14]: "pct_fga_16_3p",
701
                df.columns[15]: "pct_fga_3p",
702
                df.columns[17]: "fg_pct_0_3",
703
                df.columns[18]: "fg_pct_3_10",
704
                df.columns[19]: "fg_pct_10_16",
705
                df.columns[20]: "fg_pct_16_3p",
706
                df.columns[22]: "pct_2pfg_ast",
707
                df.columns[23]: "pct_3pfg_ast",
708
                df.columns[24]: "dunk_pct_tot_fg",
709
                df.columns[25]: "dunks",
710
                df.columns[26]: "corner_3_ast_pct",
711
                df.columns[27]: "corner_3pm_pct",
712
                df.columns[28]: "heaves_att",
713
                df.columns[29]: "heaves_makes",
714
            }
715
        )[
716
            [
717
                "player",
718
                "mp",
719
                "avg_shot_distance",
720
                "pct_fga_2p",
721
                "pct_fga_0_3",
722
                "pct_fga_3_10",
723
                "pct_fga_10_16",
724
                "pct_fga_16_3p",
725
                "pct_fga_3p",
726
                "fg_pct_0_3",
727
                "fg_pct_3_10",
728
                "fg_pct_10_16",
729
                "fg_pct_16_3p",
730
                "pct_2pfg_ast",
731
                "pct_3pfg_ast",
732
                "dunk_pct_tot_fg",
733
                "dunks",
734
                "corner_3_ast_pct",
735
                "corner_3pm_pct",
736
                "heaves_att",
737
                "heaves_makes",
738
            ]
739
        ]
740
        df = df.query('player != "Player"').copy()
1✔
741
        df["mp"] = pd.to_numeric(df["mp"])
1✔
742
        df = (
1✔
743
            df.sort_values(["mp"], ascending=False)
744
            .groupby("player")
745
            .first()
746
            .reset_index()
747
            .drop("mp", axis=1)
748
        )
749
        df["player"] = (
1✔
750
            df["player"]
751
            .str.normalize("NFKD")  # this is removing all accented characters
752
            .str.encode("ascii", errors="ignore")
753
            .str.decode("utf-8")
754
        )
755
        df["player"] = df["player"].apply(clean_player_names)
1✔
756
        df["scrape_date"] = datetime.now().date()
1✔
757
        df["scrape_ts"] = datetime.now()
1✔
758
        logging.info(
1✔
759
            "Shooting Stats Transformation Function Successful, "
760
            f"retrieving {len(df)} rows"
761
        )
762
        return df
1✔
NEW
763
    except Exception as error:
×
764
        logging.error(f"Shooting Stats Web Scrape Function Failed, {error}")
×
765
        sentry_sdk.capture_exception(error)
×
766
        df = pd.DataFrame()
×
767
        return df
×
768

769

770
@time_function
1✔
771
def scrape_odds(feature_flags_df: pd.DataFrame) -> pd.DataFrame:
1✔
772
    """
773
    Function to web scrape Gambling Odds from cover.com
774

775
    Args:
776
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to check whether
777
            to run this function or not
778

779
    Returns:
780
        DataFrame of Gambling Odds for Today's Games
781
    """
782
    feature_flag = "odds"
1✔
783
    feature_flag_check = check_feature_flag(
1✔
784
        flag=feature_flag, flags_df=feature_flags_df
785
    )
786

787
    if feature_flag_check is False:
1✔
788
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
789
        df = pd.DataFrame()
×
790
        return df
×
791

792
    try:
1✔
793
        url = "https://www.covers.com/sport/basketball/nba/odds"
1✔
794
        df = pd.read_html(url)
1✔
795
        odds = df[0]
1✔
796
        odds["spread"] = df[3].iloc[:, 4]  # 5th column in df[3]
1✔
797
        # Select columns by index: First column (index 0),
798
        # 5th column (index 4), and 'spread'
799
        odds = odds.iloc[:, [0, 4, -1]]
1✔
800
        # Rename the selected columns
801
        odds = odds.rename(
1✔
802
            columns={
803
                odds.columns[0]: "datetime1",  # Rename first column
804
                odds.columns[1]: "moneyline",  # Rename second column
805
            }
806
        )
807
        # filter out any records not from today
808
        odds = odds.query(
1✔
809
            "datetime1 != 'FINAL' and datetime1 == datetime1 and datetime1.str.contains('Today')",
810
            engine="python",
811
        ).copy()
812
        # PK is a pick em game, so we'll set the spread to -1.0
813
        odds["spread"] = odds["spread"].str.replace("PK", "-1.0")
1✔
814
        if len(odds) == 0:
1✔
815
            logging.info("No Odds Records available for today's games")
×
816
            return []
×
817

818
        odds["spread"] = odds["spread"].apply(filter_spread)
1✔
819
        odds["spread"] = odds["spread"].apply(lambda x: " ".join(x.split()))
1✔
820
        odds["datetime1"] = odds["datetime1"].str.replace("Today, ", "")
1✔
821
        odds_final = odds[["datetime1", "spread", "moneyline"]].copy()
1✔
822

823
        # \b: Word boundary anchor, ensures that the match occurs at a word boundary.
824
        # (: Start of a capturing group.
825
        # [A-Z]: Character class matching any uppercase letter from 'A' to 'Z'.
826
        # {2,3}: Quantifier specifying that the preceding character class [A-Z]
827
        #       should appear 2 to 3 times.
828
        # ): End of the capturing group.
829
        # \b: Word boundary anchor, again ensuring that the match occurs at a word boundary.
830

831
        pattern = r"\b([A-Z]{2,3})\b"
1✔
832

833
        odds_final["team"] = (
1✔
834
            odds_final["datetime1"]
835
            .str.extractall(pattern)
836
            .unstack()
837
            .apply(lambda x: " ".join(x.dropna()), axis=1)
838
        )
839

840
        # turning the space separated elements in a list, then exploding that list
841
        odds_final["team"] = odds_final["team"].str.split(" ", n=1, expand=False)
1✔
842
        odds_final["spread"] = odds_final["spread"].str.split(" ", n=1, expand=False)
1✔
843
        odds_final["moneyline"] = odds_final["moneyline"].str.split(
1✔
844
            " ", n=1, expand=False
845
        )
846
        odds_final = odds_final.explode(["team", "spread", "moneyline"]).reset_index()
1✔
847
        odds_final = odds_final.drop("index", axis=1)
1✔
848
        odds_final["date"] = datetime.now().date()
1✔
849
        odds_final["spread"] = odds_final[
1✔
850
            "spread"
851
        ].str.strip()  # strip trailing and leading spaces
852
        odds_final["moneyline"] = odds_final["moneyline"].str.strip()
1✔
853
        odds_final["time"] = odds_final["datetime1"].str.split().str[1]
1✔
854
        odds_final["datetime1"] = pd.to_datetime(
1✔
855
            (datetime.now().date().strftime("%Y-%m-%d") + " " + odds_final["time"]),
856
            format="%Y-%m-%d %H:%M",
857
        )
858

859
        odds_final["total"] = 200
1✔
860
        odds_final["team"] = odds_final["team"].str.replace("BK", "BKN")
1✔
861
        odds_final["moneyline"] = odds_final["moneyline"].str.replace(
1✔
862
            "\\+", "", regex=True
863
        )
864
        odds_final["moneyline"] = odds_final["moneyline"].astype("int")
1✔
865
        odds_final = odds_final[
1✔
866
            ["team", "spread", "total", "moneyline", "date", "datetime1"]
867
        ]
868
        logging.info(
1✔
869
            f"Odds Scrape Successful, returning {len(odds_final)} records "
870
            f"from {len(odds_final) // 2} games Today"
871
        )
872
        return odds_final
1✔
NEW
873
    except Exception as e:
×
874
        logging.error(f"Odds Function Web Scrape Failed, {e}")
×
875
        sentry_sdk.capture_exception(e)
×
876
        df = pd.DataFrame()
×
877
        return df
×
878

879

880
# def get_odds_data() -> pd.DataFrame:
881
#     """
882
#     *********** DEPRECATED AS OF 2022-10-19 ***********
883

884
#     Web Scrape function w/ pandas read_html that grabs current day's
885
#         nba odds in raw format. There are 2 objects [0], [1] if the days
886
#         are split into 2.  AWS ECS operates in UTC time so the game start
887
#         times are actually 5-6+ hours ahead of what they actually are, so
888
#         there are 2 html tables.
889

890
#     Args:
891
#         None
892

893
#     Returns:
894
#         Pandas DataFrame of NBA moneyline + spread odds for upcoming games
895
#    for that day
896
#     """
897
#     year = (datetime.now() - timedelta(1)).year
898

899
#     try:
900
#         url = "https://sportsbook.draftkings.com/leagues/basketball/nba"
901
#         df = pd.read_html(url)
902
#         if len(df) == 0:
903
#             logging.info("Odds Transformation Failed, no Odds Data available.")
904
#             df = pd.DataFrame()
905
#             return df
906
#         else:
907
#             try:
908
#                 data1 = df[0].copy()
909
#                 data1.columns.values[0] = "Tomorrow"
910
#                 date_try = str(year) + " " + data1.columns[0]
911
#                 data1["date"] = np.where(
912
#                     date_try == "2022 Tomorrow",
913
#                     datetime.now().date(),  # if the above is true, then return this
914
#                     str(year) + " " + data1.columns[0],  # if false then return this
915
#                 )
916
#                 # )
917
#                 date_try = data1["date"].iloc[0]
918
#                 data1.reset_index(drop=True)
919
#                 data1["Tomorrow"] = data1["Tomorrow"].str.replace(
920
#                     "LA Clippers", "LAC Clippers", regex=True
921
#                 )
922

923
#                 data1["Tomorrow"] = data1["Tomorrow"].str.replace(
924
#                     "AM", "AM ", regex=True
925
#                 )
926
#                 data1["Tomorrow"] = data1["Tomorrow"].str.replace(
927
#                     "PM", "PM ", regex=True
928
#                 )
929
#                 data1["Time"] = data1["Tomorrow"].str.split().str[0]
930
#                 data1["datetime1"] = (
931
#                     pd.to_datetime(date_try.strftime("%Y-%m-%d") + " "
932
#                                   + data1["Time"])
933
#                     - timedelta(hours=6)
934
#                     + timedelta(days=1)
935
#                 )
936
#                 if len(df) > 1:  # if more than 1 day's data appears then do this
937
#                     data2 = df[1].copy()
938
#                     data2.columns.values[0] = "Tomorrow"
939
#                     data2.reset_index(drop=True)
940
#                     data2["Tomorrow"] = data2["Tomorrow"].str.replace(
941
#                         "LA Clippers", "LAC Clippers", regex=True
942
#                     )
943
#                     data2["Tomorrow"] = data2["Tomorrow"].str.replace(
944
#                         "AM", "AM ", regex=True
945
#                     )
946
#                     data2["Tomorrow"] = data2["Tomorrow"].str.replace(
947
#                         "PM", "PM ", regex=True
948
#                     )
949
#                     data2["Time"] = data2["Tomorrow"].str.split().str[0]
950
#                     data2["datetime1"] = (
951
#                         pd.to_datetime(
952
#                             date_try.strftime("%Y-%m-%d") + " " + data2["Time"]
953
#                         )
954
#                         - timedelta(hours=6)
955
#                         + timedelta(days=1)
956
#                     )
957
#                     data2["date"] = data2["datetime1"].dt.date
958

959
#                     data = pd.concat([data1, data2])
960
#                     data["SPREAD"] = data["SPREAD"].str[:-4]
961
#                     data["TOTAL"] = data["TOTAL"].str[:-4]
962
#                     data["TOTAL"] = data["TOTAL"].str[2:]
963
#                     data["Tomorrow"] = data["Tomorrow"].str.split().str[1:2]
964
#                     data["Tomorrow"] = pd.DataFrame(
965
#                         [
966
#                             str(line).strip("[").strip("]").replace("'", "")
967
#                             for line in data["Tomorrow"]
968
#                         ]
969
#                     )
970
#                     data["SPREAD"] = data["SPREAD"].str.replace("pk", "-1",
971
#                           regex=True)
972
#                     data["SPREAD"] = data["SPREAD"].str.replace("+", "", regex=True)
973
#                     data.columns = data.columns.str.lower()
974
#                     data = data[
975
#                         [
976
#                             "tomorrow",
977
#                             "spread",
978
#                             "total",
979
#                             "moneyline",
980
#                             "date",
981
#                             "datetime1",
982
#                         ]
983
#                     ]
984
#                     data = data.rename(columns={data.columns[0]: "team"})
985
#                     data = data.query(
986
#                         "date == date.min()"
987
#                     )  # only grab games from upcoming day
988
#                     logging.info(
989
#                         f"""Odds Transformation Function Successful {len(df)} day, \
990
#                         retrieving {len(data)} rows"""
991
#                     )
992
#                     return data
993
#                 else:  # if there's only 1 day of data then just use that
994
#                     data = data1.reset_index(drop=True)
995
#                     data["SPREAD"] = data["SPREAD"].str[:-4]
996
#                     data["TOTAL"] = data["TOTAL"].str[:-4]
997
#                     data["TOTAL"] = data["TOTAL"].str[2:]
998
#                     data["Tomorrow"] = data["Tomorrow"].str.split().str[1:2]
999
#                     data["Tomorrow"] = pd.DataFrame(
1000
#                         [
1001
#                             str(line).strip("[").strip("]").replace("'", "")
1002
#                             for line in data["Tomorrow"]
1003
#                         ]
1004
#                     )
1005
#                     data["SPREAD"] = data["SPREAD"].str.replace("pk", "-1",
1006
#                        regex=True)
1007
#                     data["SPREAD"] = data["SPREAD"].str.replace("+", "", regex=True)
1008
#                     data.columns = data.columns.str.lower()
1009
#                     data = data[
1010
#                         [
1011
#                             "tomorrow",
1012
#                             "spread",
1013
#                             "total",
1014
#                             "moneyline",
1015
#                             "date",
1016
#                             "datetime1",
1017
#                         ]
1018
#                     ]
1019
#                     data = data.rename(columns={data.columns[0]: "team"})
1020
#                     data = data.query(
1021
#                         "date == date.min()"
1022
#                     )  # only grab games from upcoming day
1023
#                     logging.info(
1024
#                         f"""Odds Transformation Successful {len(df)} day, \
1025
#                         retrieving {len(data)} rows"""
1026
#                     )
1027
#                     return data
1028
#             except Exception as error:
1029
#                 logging.error(
1030
#                     f"Odds Transformation Failed for {len(df)} day objects, {error}"
1031
#                 )
1032
#                 sentry_sdk.capture_exception(error)
1033
#                 data = pd.DataFrame()
1034
#                 return data
1035
#     except (
1036
#         BaseException,
1037
#         ValueError,
1038
#     ) as error:  # valueerror fucked shit up apparently idfk
1039
#         logging.error(f"Odds Function Web Scrape Failed, {error}")
1040
#         sentry_sdk.capture_exception(error)
1041
#         df = pd.DataFrame()
1042
#         return df
1043

1044

1045
@time_function
1✔
1046
def get_reddit_data(feature_flags_df: pd.DataFrame, sub: str = "nba") -> pd.DataFrame:
1✔
1047
    """
1048
    Web Scrape function w/ PRAW that grabs top ~27 top posts from a given subreddit.
1049
    Left sub as an argument in case I want to scrape multi subreddits in the future
1050
    (r/nba, r/nbadiscussion, r/sportsbook etc)
1051

1052
    Args:
1053
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to check whether
1054
            to run this function or not
1055

1056
        sub (string): subreddit to query
1057

1058
    Returns:
1059
        Pandas DataFrame of all current top posts on r/nba
1060
    """
1061
    feature_flag = "reddit_posts"
×
1062
    feature_flag_check = check_feature_flag(
×
1063
        flag=feature_flag, flags_df=feature_flags_df
1064
    )
1065

1066
    if feature_flag_check is False:
×
1067
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
1068
        df = pd.DataFrame()
×
1069
        return df
×
1070

1071
    reddit = praw.Reddit(
×
1072
        client_id=os.environ.get("reddit_accesskey"),
1073
        client_secret=os.environ.get("reddit_secretkey"),
1074
        user_agent="praw-app",
1075
        username=os.environ.get("reddit_user"),
1076
        password=os.environ.get("reddit_pw"),
1077
    )
1078
    try:
×
1079
        subreddit = reddit.subreddit(sub)
×
1080
        posts = []
×
1081
        for post in subreddit.hot(limit=27):
×
1082
            posts.append(
×
1083
                [
1084
                    post.title,
1085
                    post.score,
1086
                    post.id,
1087
                    post.url,
1088
                    str(f"https://www.reddit.com{post.permalink}"),
1089
                    post.num_comments,
1090
                    post.selftext,
1091
                    datetime.now().date(),
1092
                    datetime.now(),
1093
                ]
1094
            )
1095
        posts = pd.DataFrame(
×
1096
            posts,
1097
            columns=[
1098
                "title",
1099
                "score",
1100
                "id",
1101
                "url",
1102
                "reddit_url",
1103
                "num_comments",
1104
                "body",
1105
                "scrape_date",
1106
                "scrape_time",
1107
            ],
1108
        )
1109
        posts.columns = posts.columns.str.lower()
×
1110

1111
        logging.info(
×
1112
            "Reddit Scrape Successful, grabbing 27 Recent "
1113
            f"popular posts from r/{sub} subreddit"
1114
        )
1115
        return posts
×
NEW
1116
    except Exception as error:
×
1117
        logging.error(f"Reddit Scrape Function Failed, {error}")
×
1118
        sentry_sdk.capture_exception(error)
×
1119
        data = pd.DataFrame()
×
1120
        return data
×
1121

1122

1123
@time_function
1✔
1124
def get_reddit_comments(
1✔
1125
    feature_flags_df: pd.DataFrame, urls: pd.Series
1126
) -> pd.DataFrame:
1127
    """
1128
    Web Scrape function w/ PRAW that iteratively extracts comments from provided
1129
    reddit post urls.
1130

1131
    Args:
1132
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to check whether
1133
            to run this function or not
1134

1135
        urls (Series): The (reddit) urls to extract comments from
1136

1137
    Returns:
1138
        Pandas DataFrame of all comments from the provided reddit urls
1139
    """
1140
    feature_flag = "reddit_comments"
1✔
1141
    feature_flag_check = check_feature_flag(
1✔
1142
        flag=feature_flag, flags_df=feature_flags_df
1143
    )
1144

1145
    if feature_flag_check is False:
1✔
1146
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
1147
        df = pd.DataFrame()
×
1148
        return df
×
1149

1150
    reddit = praw.Reddit(
1✔
1151
        client_id=os.environ.get("reddit_accesskey"),
1152
        client_secret=os.environ.get("reddit_secretkey"),
1153
        user_agent="praw-app",
1154
        username=os.environ.get("reddit_user"),
1155
        password=os.environ.get("reddit_pw"),
1156
    )
1157
    author_list = []
1✔
1158
    comment_list = []
1✔
1159
    score_list = []
1✔
1160
    flair_list1 = []
1✔
1161
    flair_list2 = []
1✔
1162
    edited_list = []
1✔
1163
    url_list = []
1✔
1164

1165
    try:
1✔
1166
        for i in urls:
1✔
1167
            submission = reddit.submission(url=i)
1✔
1168
            submission.comments.replace_more(limit=0)
1✔
1169
            # this removes all the "more comment" stubs
1170
            # to grab ALL comments use limit=None, but it will take 100x longer
1171
            for comment in submission.comments.list():
1✔
1172
                author_list.append(comment.author)
×
1173
                comment_list.append(comment.body)
×
1174
                score_list.append(comment.score)
×
1175
                flair_list1.append(comment.author_flair_css_class)
×
1176
                flair_list2.append(comment.author_flair_text)
×
1177
                edited_list.append(comment.edited)
×
1178
                url_list.append(i)
×
1179

1180
        df = pd.DataFrame(
1✔
1181
            {
1182
                "author": author_list,
1183
                "comment": comment_list,
1184
                "score": score_list,
1185
                "url": url_list,
1186
                "flair1": flair_list1,
1187
                "flair2": flair_list2,
1188
                "edited": edited_list,
1189
                "scrape_date": datetime.now().date(),
1190
                "scrape_ts": datetime.now(),
1191
            }
1192
        )
1193

1194
        df = df.query('author != "None"')  # remove deleted comments rip
1✔
1195
        df["author"] = df["author"].astype(str)
1✔
1196
        df = df.sort_values("score").groupby(["author", "comment", "url"]).tail(1)
1✔
1197
        df = add_sentiment_analysis(df, "comment")
1✔
1198

1199
        df["edited"] = np.where(
1✔
1200
            df["edited"] is False, 0, 1
1201
        )  # if edited, then 1, else 0
1202
        df["md5_pk"] = df.apply(
1✔
1203
            lambda x: hashlib.md5(
1204
                (str(x["author"]) + str(x["comment"]) + str(x["url"])).encode("utf8")
1205
            ).hexdigest(),
1206
            axis=1,
1207
        )
1208
        # this hash function lines up with the md5 function in postgres
1209
        # this is needed for the upsert to work on it.
1210
        logging.info(
1✔
1211
            f"Reddit Comment Extraction Success, retrieving {len(df)} "
1212
            f"total comments from {len(urls)} total urls"
1213
        )
1214
        return df
1✔
NEW
1215
    except Exception as e:
×
1216
        logging.error(f"Reddit Comment Extraction Failed for url {i}, {e}")
×
1217
        sentry_sdk.capture_exception(e)
×
1218
        df = pd.DataFrame()
×
1219
        return df
×
1220

1221

1222
# def scrape_tweets_tweepy(
1223
#     search_parameter: str, count: int, result_type: str
1224
# ) -> pd.DataFrame:
1225
#     """
1226
#     Web Scrape function w/ Tweepy to scrape Tweets made within last ~ 7 days
1227

1228
#     Args:
1229
#         search_parameter (str): The string you're interested in finding Tweets for
1230

1231
#         count (int): Number of tweets to grab
1232

1233
#         result_type (str): Either mixed, recent, or popular.
1234

1235
#     Returns:
1236
#         Pandas DataFrame of recent Tweets
1237
#     """
1238
#     auth = tweepy.OAuthHandler(
1239
#         os.environ.get("twitter_consumer_api_key"),
1240
#         os.environ.get("twitter_consumer_api_secret"),
1241
#     )
1242

1243
#     api = tweepy.API(auth, wait_on_rate_limit=True)
1244

1245
#     full_tweet_df = pd.DataFrame()
1246
#     try:
1247
#         for tweet in tweepy.Cursor(  # result_type can be mixed, recent, or popular.
1248
#             api.search_tweets, search_parameter, count=count, result_type=result_type
1249
#         ).items(count):
1250
#             df = {
1251
#                 "api_created_at": tweet._json["created_at"],
1252
#                 "tweet_id": tweet._json["id_str"],
1253
#                 "username": tweet._json["user"]["screen_name"],
1254
#                 "user_id": tweet._json["user"]["id"],
1255
#                 "tweet": tweet._json["text"],
1256
#                 "likes": tweet._json["favorite_count"],
1257
#                 "retweets": tweet._json["retweet_count"],
1258
#                 "language": tweet._json["lang"],
1259
#                 "scrape_ts": datetime.now(),
1260
#                 "profile_img": tweet._json["user"]["profile_image_url"],
1261
#                 "url": f"https://twitter.com/twitter/statuses/{tweet._json['id']}",
1262
#             }
1263
#             full_tweet_df = pd.concat([df, full_tweet_df])
1264

1265
#         df = add_sentiment_analysis(df, "tweet")
1266
#         logging.info(f"Twitter Scrape Successful, retrieving {len(df)} Tweets")
1267
#         return df
1268
#     except Exception as e:
1269
#         logging.error(f"Error Occurred for Scrape Tweets Tweepy, {e}")
1270
#         sentry_sdk.capture_exception(e)
1271
#         df = pd.DataFrame()
1272
#         return df
1273

1274

1275
# @time_function
1276
# def scrape_tweets_combo(feature_flags_df: pd.DataFrame) -> pd.DataFrame:
1277
#     """
1278
#     Web Scrape function to scrape Tweepy Tweets for both popular & mixed tweets
1279

1280
#     Args:
1281
#         feature_flags_df (pd.DataFrame): Feature Flags DataFrame to check whether
1282
#             to run this function or not
1283

1284
#     Returns:
1285
#         Pandas DataFrame of both popular and mixed tweets.
1286
#     """
1287
#     feature_flag = "twitter"
1288
#     feature_flag_check = check_feature_flag(
1289
#         flag=feature_flag, flags_df=feature_flags_df
1290
#     )
1291

1292
#     if feature_flag_check is False:
1293
#         logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
1294
#         df = pd.DataFrame()
1295
#         return df
1296

1297
#     try:
1298
#         df1 = scrape_tweets_tweepy("nba", 1000, "popular")
1299
#         df2 = scrape_tweets_tweepy("nba", 5000, "mixed")
1300

1301
#         # so the scrape_ts column screws up with filtering duplicates out so
1302
#         # this code ignores that column to correctly drop the duplicates
1303
#         df_combo = pd.concat([df1, df2])
1304
#         df_combo = df_combo.drop_duplicates(
1305
#             subset=df_combo.columns.difference(
1306
#                 ["scrape_ts", "likes", "retweets", "tweet"]
1307
#             )
1308
#         )
1309

1310
#         logging.info(
1311
#             f"Grabbing {len(df1)} Popular Tweets and {len(df2)} Mixed Tweets "
1312
#             f"for {len(df_combo)} Total, {(len(df1) + len(df2) - len(df_combo))} "
1313
#             "were duplicates"
1314
#         )
1315
#         return df_combo
1316
#     except Exception as e:
1317
#         logging.error(f"Error Occurred for Scrape Tweets Combo, {e}")
1318
#         sentry_sdk.capture_exception(e)
1319
#         df = pd.DataFrame()
1320
#         return df
1321

1322

1323
@time_function
1✔
1324
def get_pbp_data(feature_flags_df: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame:
1✔
1325
    """
1326
    Web Scrape function w/ pandas read_html that uses aliases via boxscores function
1327
    to scrape the pbp data iteratively for each game played the previous day.
1328
    It assumes there is a location column in the df being passed in.
1329

1330
    Args:
1331
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to check whether
1332
            to run this function or not
1333

1334
        df (DataFrame) - The Boxscores DataFrame
1335

1336
    Returns:
1337
        All PBP Data for the games in the input df
1338

1339
    """
1340
    feature_flag = "pbp"
1✔
1341
    feature_flag_check = check_feature_flag(
1✔
1342
        flag=feature_flag, flags_df=feature_flags_df
1343
    )
1344

1345
    if feature_flag_check is False:
1✔
1346
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
1347
        df = pd.DataFrame()
×
1348
        return df
×
1349

1350
    if len(df) > 0:
1✔
1351
        game_date = df["date"][0]
1✔
1352
    else:
1353
        df = pd.DataFrame()
×
1354
        logging.warning(
×
1355
            "PBP Transformation Function Failed, "
1356
            f"no data available for {datetime.now().date()}"
1357
        )
1358
        return df
×
1359
    try:
1✔
1360
        if len(df) > 0:
1✔
1361
            yesterday_hometeams = (
1✔
1362
                df.query('location == "H"')[["team"]].drop_duplicates().dropna()
1363
            )
1364
            yesterday_hometeams["team"] = yesterday_hometeams["team"].str.replace(
1✔
1365
                "PHX", "PHO"
1366
            )
1367
            yesterday_hometeams["team"] = yesterday_hometeams["team"].str.replace(
1✔
1368
                "CHA", "CHO"
1369
            )
1370
            yesterday_hometeams["team"] = yesterday_hometeams["team"].str.replace(
1✔
1371
                "BKN", "BRK"
1372
            )
1373

1374
            away_teams = (
1✔
1375
                df.query('location == "A"')[["team", "opponent"]]
1376
                .drop_duplicates()
1377
                .dropna()
1378
            )
1379
            away_teams = away_teams.rename(
1✔
1380
                columns={
1381
                    away_teams.columns[0]: "AwayTeam",
1382
                    away_teams.columns[1]: "HomeTeam",
1383
                }
1384
            )
1385
        else:
1386
            yesterday_hometeams = []
×
1387

1388
        if len(yesterday_hometeams) > 0:
1✔
1389
            try:
1✔
1390
                newdate = str(
1✔
1391
                    df["date"].drop_duplicates()[0].date()
1392
                )  # this assumes all games in the boxscores df are 1 date
1393
                newdate = pd.to_datetime(newdate).strftime(
1✔
1394
                    "%Y%m%d"
1395
                )  # formatting into url format.
1396
                pbp_list = pd.DataFrame()
1✔
1397
                for i in yesterday_hometeams["team"]:
1✔
1398
                    url = f"https://www.basketball-reference.com/boxscores/pbp/{newdate}0{i}.html"
1✔
1399
                    df = pd.read_html(url)[0]
1✔
1400
                    df.columns = df.columns.map("".join)
1✔
1401
                    df = df.rename(
1✔
1402
                        columns={
1403
                            df.columns[0]: "Time",
1404
                            df.columns[1]: "descriptionPlayVisitor",
1405
                            df.columns[2]: "AwayScore",
1406
                            df.columns[3]: "Score",
1407
                            df.columns[4]: "HomeScore",
1408
                            df.columns[5]: "descriptionPlayHome",
1409
                        }
1410
                    )
1411
                    conditions = [
1✔
1412
                        (
1413
                            df["HomeScore"].str.contains("Jump ball:", na=False)
1414
                            & df["Time"].str.contains("12:00.0")
1415
                        ),
1416
                        (
1417
                            df["HomeScore"].str.contains(
1418
                                "Start of 2nd quarter", na=False
1419
                            )
1420
                        ),
1421
                        (
1422
                            df["HomeScore"].str.contains(
1423
                                "Start of 3rd quarter", na=False
1424
                            )
1425
                        ),
1426
                        (
1427
                            df["HomeScore"].str.contains(
1428
                                "Start of 4th quarter", na=False
1429
                            )
1430
                        ),
1431
                        (
1432
                            df["HomeScore"].str.contains(
1433
                                "Start of 1st overtime", na=False
1434
                            )
1435
                        ),
1436
                        (
1437
                            df["HomeScore"].str.contains(
1438
                                "Start of 2nd overtime", na=False
1439
                            )
1440
                        ),
1441
                        (
1442
                            df["HomeScore"].str.contains(
1443
                                "Start of 3rd overtime", na=False
1444
                            )
1445
                        ),
1446
                        (
1447
                            df["HomeScore"].str.contains(
1448
                                "Start of 4th overtime", na=False
1449
                            )
1450
                        ),  # if more than 4 ots then rip
1451
                    ]
1452
                    values = [
1✔
1453
                        "1st Quarter",
1454
                        "2nd Quarter",
1455
                        "3rd Quarter",
1456
                        "4th Quarter",
1457
                        "1st OT",
1458
                        "2nd OT",
1459
                        "3rd OT",
1460
                        "4th OT",
1461
                    ]
1462
                    df["Quarter"] = np.select(conditions, values, default=None)
1✔
1463
                    df["Quarter"] = df["Quarter"].ffill()
1✔
1464
                    df = df.query(
1✔
1465
                        'Time != "Time" & '
1466
                        'Time != "2nd Q" & '
1467
                        'Time != "3rd Q" & '
1468
                        'Time != "4th Q" & '
1469
                        'Time != "1st OT" & '
1470
                        'Time != "2nd OT" & '
1471
                        'Time != "3rd OT" & '
1472
                        'Time != "4th OT"'
1473
                    ).copy()
1474
                    # use COPY to get rid of the fucking goddamn warning
1475
                    df["HomeTeam"] = i
1✔
1476
                    df["HomeTeam"] = df["HomeTeam"].str.replace("PHO", "PHX")
1✔
1477
                    df["HomeTeam"] = df["HomeTeam"].str.replace("CHO", "CHA")
1✔
1478
                    df["HomeTeam"] = df["HomeTeam"].str.replace("BRK", "BKN")
1✔
1479
                    df = df.merge(away_teams)
1✔
1480
                    df[["scoreAway", "scoreHome"]] = df["Score"].str.split(
1✔
1481
                        "-", expand=True, n=1
1482
                    )
1483
                    df["scoreAway"] = pd.to_numeric(df["scoreAway"], errors="coerce")
1✔
1484
                    df["scoreAway"] = df["scoreAway"].ffill()
1✔
1485
                    df["scoreAway"] = df["scoreAway"].fillna(0)
1✔
1486
                    df["scoreHome"] = pd.to_numeric(df["scoreHome"], errors="coerce")
1✔
1487
                    df["scoreHome"] = df["scoreHome"].ffill()
1✔
1488

1489
                    df["scoreHome"] = df["scoreHome"].fillna(0)
1✔
1490
                    df["marginScore"] = df["scoreHome"] - df["scoreAway"]
1✔
1491
                    df["Date"] = game_date
1✔
1492
                    df["scrape_date"] = datetime.now().date()
1✔
1493
                    df = df.rename(
1✔
1494
                        columns={
1495
                            df.columns[0]: "timeQuarter",
1496
                            df.columns[6]: "numberPeriod",
1497
                        }
1498
                    )
1499
                    pbp_list = pd.concat([df, pbp_list])
1✔
1500
                    df = pd.DataFrame()
1✔
1501
                pbp_list.columns = pbp_list.columns.str.lower()
1✔
1502
                pbp_list = pbp_list.query(
1✔
1503
                    "(awayscore.notnull()) | (homescore.notnull())", engine="python"
1504
                )
1505
                logging.info(
1✔
1506
                    "PBP Data Transformation Function Successful, "
1507
                    f"retrieving {len(pbp_list)} rows for {game_date}"
1508
                )
1509
                # filtering only scoring plays here, keep other all other rows in future
1510
                # for lineups stuff etc.
1511
                return pbp_list
1✔
NEW
1512
            except Exception as error:
×
1513
                logging.error(f"PBP Transformation Function Logic Failed, {error}")
×
1514
                sentry_sdk.capture_exception(error)
×
1515
                df = pd.DataFrame()
×
1516
                return df
×
1517
        else:
1518
            df = pd.DataFrame()
×
1519
            logging.error(
×
1520
                "PBP Transformation Function Failed, no data available "
1521
                f"for {game_date}"
1522
            )
1523
            return df
×
NEW
1524
    except Exception as error:
×
1525
        logging.error(f"PBP Data Transformation Function Failed, {error}")
×
1526
        sentry_sdk.capture_exception(error)
×
1527
        data = pd.DataFrame()
×
1528
        return data
×
1529

1530

1531
@time_function
1✔
1532
def schedule_scraper(
1✔
1533
    feature_flags_df: pd.DataFrame,
1534
    year: str,
1535
    month_list: list[str] = [
1536
        "october",
1537
        "november",
1538
        "december",
1539
        "january",
1540
        "february",
1541
        "march",
1542
        "april",
1543
    ],
1544
) -> pd.DataFrame:
1545
    """
1546
    Web Scrape Function to scrape Schedule data by iterating through a list of months
1547

1548
    Args:
1549
        feature_flags_df (pd.DataFrame): Feature Flags DataFrame to check whether
1550
            to run this function or not
1551

1552
        year (str) - The year to scrape
1553

1554
        month_list (list) - List of full-month names to scrape
1555

1556
    Returns:
1557
        DataFrame of Schedule Data to be stored.
1558

1559
    """
1560
    current_date = (
1✔
1561
        datetime.now().date()
1562
    )  # DO NOT REMOVE, used in df.query function later
1563
    feature_flag = "schedule"
1✔
1564
    feature_flag_check = check_feature_flag(
1✔
1565
        flag=feature_flag, flags_df=feature_flags_df
1566
    )
1567

1568
    if feature_flag_check is False:
1✔
1569
        logging.info(f"Feature Flag {feature_flag} is disabled, skipping function")
×
1570
        df = pd.DataFrame()
×
1571
        return df
×
1572

1573
    try:
1✔
1574
        schedule_df = pd.DataFrame()
1✔
1575
        completed_months = []
1✔
1576
        for month in month_list:
1✔
1577
            url = f"https://www.basketball-reference.com/leagues/NBA_{year}_games-{month}.html"
1✔
1578
            html = requests.get(url).content
1✔
1579
            soup = BeautifulSoup(html, "html.parser")
1✔
1580

1581
            headers = [th.getText() for th in soup.findAll("tr")[0].findAll("th")]
1✔
1582
            headers[6] = "boxScoreLink"
1✔
1583
            headers[7] = "isOT"
1✔
1584
            headers = headers[1:]
1✔
1585

1586
            rows = soup.findAll("tr")[1:]
1✔
1587
            date_info = [
1✔
1588
                [th.getText() for th in rows[i].findAll("th")] for i in range(len(rows))
1589
            ]
1590

1591
            game_info = [
1✔
1592
                [td.getText() for td in rows[i].findAll("td")] for i in range(len(rows))
1593
            ]
1594
            date_info = [i[0] for i in date_info]
1✔
1595

1596
            schedule = pd.DataFrame(game_info, columns=headers)
1✔
1597
            schedule["Date"] = date_info
1✔
1598

1599
            logging.info(
1✔
1600
                f"Schedule Function Completed for {month}, retrieving {len(schedule)} "
1601
                "rows"
1602
            )
1603
            completed_months.append(month)
1✔
1604
            schedule_df = pd.concat([schedule, schedule_df])
1✔
1605

1606
    except IndexError:
×
1607
        logging.info(
×
1608
            f"{month} currently has no data in basketball-reference, "
1609
            f"stopping the function and returning data for {' '.join(completed_months)}"
1610
        )
1611
    finally:
1612
        if not schedule_df.empty:
1✔
1613
            schedule_df = schedule_df[
1✔
1614
                ["Start (ET)", "Visitor/Neutral", "Home/Neutral", "Date"]
1615
            ]
1616
            schedule_df["proper_date"] = pd.to_datetime(
1✔
1617
                schedule_df["Date"], format="%a, %b %d, %Y"
1618
            ).dt.date
1619
            schedule_df.columns = schedule_df.columns.str.lower()
1✔
1620
            schedule_df = schedule_df.rename(
1✔
1621
                columns={
1622
                    "start (et)": "start_time",
1623
                    "visitor/neutral": "away_team",
1624
                    "home/neutral": "home_team",
1625
                }
1626
            )
1627
            # filtering the data to only rows beyond the current date because we already have
1628
            # the historical records
1629
            schedule_df = schedule_df[schedule_df["proper_date"] >= current_date]
1✔
1630
            return schedule_df
1✔
1631
        else:
1632
            return pd.DataFrame()
×
1633

1634

1635
def write_to_s3(
1✔
1636
    file_name: str,
1637
    df: pd.DataFrame,
1638
    date: datetime.date = datetime.now().date(),
1639
    bucket: str = os.environ.get("S3_BUCKET"),
1640
) -> None:
1641
    """
1642
    S3 Function using awswrangler to write file.  Only supports parquet right now.
1643

1644
    Args:
1645
        file_name (str): The base name of the file (boxscores, opp_stats)
1646

1647
        df (pd.DataFrame): The Pandas DataFrame to write to S3
1648

1649
        bucket (str): The Bucket to write to.  Defaults to `os.environ.get('S3_BUCKET')`
1650

1651
        date (datetime.date): Date to partition the data by.
1652
            Defaults to `datetime.now().date()`
1653

1654
    Returns:
1655
        Writes the Pandas DataFrame to an S3 File.
1656

1657
    """
1658
    year_partition = date.year
1✔
1659
    month_partition = get_leading_zeroes(value=date.month)
1✔
1660
    file_name_jn = f"{file_name}-{date}"
1✔
1661
    try:
1✔
1662
        if len(df) == 0:
1✔
1663
            logging.info(f"Not storing {file_name} to s3 because it's empty.")
×
1664
            pass
×
1665
        else:
1666
            wr.s3.to_parquet(
1✔
1667
                df=df,
1668
                path=f"s3://{bucket}/{file_name}/validated/year={year_partition}/month={month_partition}/{file_name_jn}.parquet",
1669
                index=False,
1670
            )
1671
            logging.info(
1✔
1672
                f"Storing {len(df)} {file_name} rows to S3 (s3://{bucket}/{file_name}/validated/{year_partition}/{file_name_jn}.parquet)"
1673
            )
1674
            pass
1✔
NEW
1675
    except Exception as error:
×
1676
        logging.error(f"S3 Storage Function Failed {file_name}, {error}")
×
1677
        sentry_sdk.capture_exception(error)
×
1678
        pass
×
1679

1680

1681
def write_to_sql(con, table_name: str, df: pd.DataFrame, table_type: str) -> None:
1✔
1682
    """
1683
    Simple Wrapper Function to write a Pandas DataFrame to SQL
1684

1685
    Args:
1686
        con (SQL Connection): The connection to the SQL DB.
1687

1688
        table_name (str): The Table name to write to SQL as.
1689

1690
        df (DataFrame): The Pandas DataFrame to store in SQL
1691

1692
        table_type (str): Whether the table should replace or append to an
1693
            existing SQL Table under that name
1694

1695
    Returns:
1696
        Writes the Pandas DataFrame to a Table in the Schema we connected to.
1697

1698
    """
1699
    try:
1✔
1700
        if len(df) == 0:
1✔
1701
            logging.info(f"{table_name} is empty, not writing to SQL")
×
1702
        else:
1703
            df.to_sql(
1✔
1704
                con=con,
1705
                name=table_name,
1706
                index=False,
1707
                if_exists=table_type,
1708
            )
1709
            logging.info(
1✔
1710
                f"Writing {len(df)} {table_name} rows to aws_{table_name}_source to SQL"
1711
            )
1712

1713
        return None
1✔
NEW
1714
    except Exception as error:
×
1715
        logging.error(f"SQL Write Script Failed, {error}")
×
1716
        sentry_sdk.capture_exception(error)
×
1717

1718

1719
# deprecated as of 2023-10-17 rip
1720
# def send_aws_email(logs: pd.DataFrame) -> None:
1721
#     """
1722
#     Email function utilizing boto3, has to be set up with SES in AWS
1723
#     and env variables passed in via Terraform.
1724

1725
#     The actual email code is copied from aws/boto3 and the subject &
1726
#     message should go in the subject / body_html variables.
1727

1728
#     Args:
1729
#         logs (DataFrame): The log file name generated by the script.
1730

1731
#     Returns:
1732
#         Sends an email out upon every script execution, including errors (if any)
1733
#     """
1734
#     sender = os.environ.get("USER_EMAIL")
1735
#     recipient = os.environ.get("USER_EMAIL")
1736
#     aws_region = "us-east-1"
1737
#     subject = f"""
1738
#     NBA ELT PIPELINE - {str(len(logs))} Alert Fails for {str(datetime.now().date())}
1739
#     """
1740
#     body_html = f"""\
1741
# <h3>Errors:</h3>
1742
#                    {logs.to_html()}"""
1743

1744
#     charset = "UTF-8"
1745
#     client = boto3.client("ses", region_name=aws_region)
1746
#     try:
1747
#         response = client.send_email(
1748
#             Destination={
1749
#                 "ToAddresses": [
1750
#                     recipient,
1751
#                 ],
1752
#             },
1753
#             Message={
1754
#                 "Body": {
1755
#                     "Html": {
1756
#                         "Charset": charset,
1757
#                         "Data": body_html,
1758
#                     },
1759
#                     "Text": {
1760
#                         "Charset": charset,
1761
#                         "Data": body_html,
1762
#                     },
1763
#                 },
1764
#                 "Subject": {
1765
#                     "Charset": charset,
1766
#                     "Data": subject,
1767
#                 },
1768
#             },
1769
#             Source=sender,
1770
#         )
1771
#     except ClientError as e:
1772
#         logging.error(e.response["Error"]["Message"])
1773
#         raise e
1774
#     else:
1775
#         logging.info(f"Email sent! Message ID: {response['MessageId']}")
1776
#         return None
1777

1778

1779
# DEPRECATING this as of 2022-04-25 - i send emails everyday now regardless
1780
# of pass or fail
1781
# def execute_email_function(logs: pd.DataFrame) -> None:
1782
#     """
1783
#     Email function that executes the email function upon script finishing.
1784
#     This is really not necessary; originally thought i wouldn't email
1785
#     if no errors would found but now i send it everyday regardless.
1786

1787
#     Args:
1788
#         logs (DataFrame): The log file name generated by the script.
1789

1790
#     Returns:
1791
#         Holds the actual send_email logic and executes if invoked as a
1792
#             script (aka on ECS)
1793
#     """
1794
#     try:
1795
#         if len(logs) > 0:
1796
#             logging.info("Sending Email")
1797
#             send_aws_email(logs)
1798
#         elif len(logs) == 0:
1799
#             logging.info("No Errors!")
1800
#             send_aws_email(logs)
1801
#     except Exception as error:
1802
#         logging.error(f"Failed Email Alert, {error}")
1803
#         sentry_sdk.capture_exception(error)
1804

1805

1806
def get_feature_flags(connection: Connection | Engine) -> pd.DataFrame:
1✔
1807
    flags = pd.read_sql_query(sql="select * from marts.feature_flags;", con=connection)
1✔
1808

1809
    logging.info(f"Retrieving {len(flags)} Feature Flags")
1✔
1810
    return flags
1✔
1811

1812

1813
def check_feature_flag(flag: str, flags_df: pd.DataFrame) -> bool:
1✔
1814
    flags_df = flags_df.query(f"flag == '{flag}'")
1✔
1815

1816
    if len(flags_df) > 0 and flags_df["is_enabled"].iloc[0] == 1:
1✔
1817
        return True
1✔
1818
    else:
1819
        return False
1✔
1820

1821

1822
def query_logs(log_file: str = "logs/example.log") -> list:
1✔
1823
    """
1824
    Small Function to read Logs CSV File and grab Errors
1825

1826
    Args:
1827
        log_file (str): Optional String of the Log File Name
1828

1829
    Returns:
1830
        list of Error Messages to be passed into Slack Function
1831
    """
1832
    logs = pd.read_csv(log_file, sep=r"\\t", engine="python", header=None)
1✔
1833
    logs = logs.rename(columns={0: "errors"})
1✔
1834
    logs = logs.query("errors.str.contains('Failed')", engine="python")
1✔
1835
    logs = logs["errors"].to_list()
1✔
1836

1837
    logging.info(f"Returning {len(logs)} Failed Logs")
1✔
1838
    return logs
1✔
1839

1840

1841
def write_to_slack(
1✔
1842
    errors: list, webhook_url: str = os.environ.get("WEBHOOK_URL", default="default")
1843
) -> int | None:
1844
    """ "
1845
    Function to write Errors out to Slack.  Requires a pre-configured `webhook_url`
1846
    to be setup.
1847

1848
    Args:
1849
        errors (list): The list of Failed Tasks + their associated errors
1850

1851
        webhook_url (str): Optional Parameter to specify the Webhook to send the
1852
            errors to.  Defaults to `os.environ.get("WEBHOOK_URL")`
1853

1854
    Returns:
1855
        None, but writes the Errors to Slack if there are any
1856
    """
1857
    try:
1✔
1858
        date = datetime.now().date()
1✔
1859
        num_errors = len(errors)
1✔
1860
        str_dump = "\n".join(errors)
1✔
1861

1862
        if num_errors > 0:
1✔
1863
            response = requests.post(
1✔
1864
                webhook_url,
1865
                data=json.dumps(
1866
                    {
1867
                        "text": (
1868
                            f"\U0001f6d1 {num_errors} Errors during NBA ELT "
1869
                            f"Ingestion on {date}: \n {str_dump}"
1870
                        )
1871
                    }
1872
                ),
1873
                headers={"Content-Type": "application/json"},
1874
            )
1875
            logging.info(
1✔
1876
                f"Wrote Errors to Slack, Reponse Code {response.status_code}. "
1877
                "Exiting ..."
1878
            )
1879
            return response.status_code
1✔
1880
        else:
1881
            logging.info("No Error Logs, not writing to Slack.  Exiting out ...")
1✔
1882
            return None
1✔
NEW
1883
    except Exception as e:
×
NEW
1884
        logging.error(f"Error Writing to Slack, {e}")
×
NEW
1885
        raise
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc