Evaluate the following classes

'''
A module that encapsulates a web scraper. This module scrapes data from a website.
'''
from html.parser import HTMLParser
import urllib.request
from datetime import datetime, timedelta
import logging
from dateutil.parser import parse

class WeatherScraper(HTMLParser):
    """A parser for extracting temperature values from a website."""

    logger = logging.getLogger("main." + __name__)

    def __init__(self):
        try:
            super().__init__()
            self.is_tbody = False
            self.is_td = False
            self.is_tr = False
            self.last_page = False
            self.counter = 0
            self.daily_temps = {}
            self.weather = {}
            self.row_date = ""
        except Exception as error:
            self.logger.error("scrape:init:%s", error)

    def is_valid_date(self, date_str):
        """Check if a given string is a valid date."""

        try:
            parse(date_str, default=datetime(1900, 1, 1))
            return True
        except ValueError:
            return False

    def is_numeric(self, temp_str):
        """Check if given temperature string can be converted to a float."""

        try:
            float(temp_str)
            return True
        except ValueError:
            return False

    def handle_starttag(self, tag, attrs):
        """Handle the opening tags."""

        try:
            if tag == "tbody":
                self.is_tbody = True
            if tag == "tr" and self.is_tbody:
                self.is_tr = True
            if tag == "td" and self.is_tr:
                self.counter += 1
                self.is_td = True

            # Only parses the valid dates, all other values are excluded.
            if tag == "abbr" and self.is_tr and self.is_valid_date(attrs[0][1]):
                self.row_date = str(datetime.strptime(attrs[0][1], "%B %d, %Y").date())

            # if len(attrs) == 2:
            #    if attrs[1][1] == "previous disabled":
            #        self.last_page = True

        except Exception as error:
            self.logger.error("scrape:starttag:%s", error)

    def handle_endtag(self, tag):
        """Handle the closing tags."""

        try:
            if tag == "td":
                self.is_td = False
            if tag == "tr":
                self.counter = 0
                self.is_tr = False
        except Exception as error:
            self.logger.error("scrape:end:%s", error)

    def handle_data(self, data):
        """Handle the data inside the tags."""

        if data.startswith("Daily Data Report for January 2022"):
            self.last_page = True

        try:
            if self.is_tbody and self.is_td and self.counter <= 3 and data.strip():
                if self.counter == 1 and self.is_numeric(data.strip()):
                    self.daily_temps["Max"] = float(data.strip())
                if self.counter == 2 and self.is_numeric(data.strip()):
                    self.daily_temps["Min"] = float(data.strip())
                if self.counter == 3 and self.is_numeric(data.strip()):
                    self.daily_temps["Mean"] = float(data.strip())
                    self.weather[self.row_date] = self.daily_temps
                    self.daily_temps = {}
        except Exception as error:
            self.logger.error("scrape:data:%s", error)

    def get_data(self):
        """Fetch the weather data and return it as a dictionary of dictionaries."""

        current_date = datetime.now()

        while not self.last_page:
            try:
                url = f"https://climate.weather.gc.ca/climate_data/daily_data_e.html?StationID=27174&timeframe=2&StartYear=1840&EndYear=2018&Day={current_date.day}&Year={current_date.year}&Month={current_date.month}"

                with urllib.request.urlopen(url) as response:
                    html = response.read().decode()
                self.feed(html)

                # Subtracts one day from the current date and assigns the
                # resulting date back to the current_date variable.
                current_date -= timedelta(days=1)
            except Exception as error:
                self.logger.error("scrape:get_data:%s", error)

        return self.weather

# Test program.
if __name__ == "__main__":
    print_data = WeatherScraper().get_data()
    for k, v in print_data.items():
        print(k, v)


'''
A Module that creates and modifies a database. In this case, the data is weather information
scraped from a webpage.
'''
import sqlite3
import logging
from dateutil import parser
from scrape_weather import WeatherScraper


class DBOperations:
    """Class for performing operations on a SQLite database"""

    def __init__(self, dbname):
        """
        Constructor for DBOperations class.

        Parameters:
        - dbname: str, the name of the SQLite database file to use
        """
        self.dbname = dbname
        self.logger = logging.getLogger(__name__)

    def initialize_db(self):
        """
        Initialize the SQLite database by creating the weather_data table.

        This method should be called every time the program runs.
        """
        with self.get_cursor() as cursor:
            try:
                cursor.execute('''
                    CREATE TABLE IF NOT EXISTS weather_data (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        sample_date TEXT UNIQUE,
                        location TEXT,
                        min_temp REAL,
                        max_temp REAL,
                        avg_temp REAL
                    )
                ''')
                self.logger.info("Initialized database successfully.")
            except sqlite3.Error as error:
                self.logger.error("An error occurred while creating the table: %s", error)

    def save_data(self, data):
        """
        Save weather data to the SQLite database.

        If the data already exists in the database, it will not be duplicated.

        Parameters:
        - data: dict, the weather data to save to the database. Must have keys for
          sample_date, location, min_temp, max_temp, and avg_temp.
        """
    # Initialize the database
        data_base = DBOperations("../mydatabase.db")
        data_base.initialize_db()
        
        # Process the data and prepare the rows
        rows = []
        for date, temps in data.items():
            row = (
                date,
                "Winnipeg",
                temps["Max"],
                temps["Min"],
                temps["Mean"]
            )
            rows.append(row)

        # Save the data to the database
        with data_base.get_cursor() as cursor:
            try:
                cursor.executemany('''
                    INSERT OR IGNORE INTO weather_data
                    (sample_date, location, min_temp, max_temp, avg_temp)
                    VALUES (?, ?, ?, ?, ?)
                ''', rows)
                data_base.logger.info("Inserted %s rows into the database.", len(rows))
            except sqlite3.Error as error:
                data_base.logger.error("An error occurred while inserting data: %s", error)

    def fetch_data(self, location):
        """
        Fetch weather data from the SQLite database for a specified location.

        Parameters:
        - location: str, the location to fetch weather data for

        Returns:
        - A list of tuples containing the weather data for the specified location,
          where each tuple has the format (sample_date, min_temp, max_temp, avg_temp).
          Returns an empty list if no data is found for the specified location.
        """
        with self.get_cursor() as cursor:
            try:
                cursor.execute('''
                    SELECT sample_date, min_temp, max_temp, avg_temp
                    FROM weather_data
                    WHERE location = ?
                ''', (location,))
                data = cursor.fetchall()
                self.logger.info("Data fetched successfully.")
                return data
            except sqlite3.Error as error:
                self.logger.error("An error occurred while fetching data from the database: %s",
                                  error)
                return []

    def fetch_data_year_to_year(self, first_year, last_year):
        '''
        Fetch weather data from the SQLite database for a specified year range.
        
        Parameters:
        - first_year: int, the first year in the range.
        - end_year: int, the final year in the range.
        
        Returns:
        - A list of data that falls in the range of years specified by the user.'''

        month_data = {1:[], 2:[], 3:[], 4:[], 5:[], 6:[],
                      7:[], 8:[], 9:[], 10:[], 11:[], 12:[]}
        start_year = f'{first_year}-01-01'
        end_year = f'{last_year}-01-01'

        with self.get_cursor() as cursor:
            try:
                for row in cursor.execute('''
                    SELECT sample_date, avg_temp
                    FROM weather_data
                    WHERE sample_date BETWEEN ? AND ?
                    ORDER BY sample_date''',(start_year, end_year)):

                    month = parser.parse(row[0]).month
                    month_data[month].append(row[1])

                self.logger.info("Data fetched successfully.")
                return month_data

            except sqlite3.Error as error:
                self.logger.error("An error occurred while fetching data from the database: %s",
                                  error)
                return []

    def fetch_data_single_month(self, month, year):
        '''
        Fetch weather data from the SQLite database for a specified month and year.
        
        Parameters:
        - month: int, the month to search for.
        - year: int, the year to search for.
        
        Returns:
        - A list of temperatures for the month and year the user searched for'''

        temperatures = {}

        with self.get_cursor() as cursor:
            try:
                for row in cursor.execute('''
                SELECT sample_date, avg_temp
                FROM weather_data
                WHERE sample_date LIKE ?||'-'||'0'||?||'-'||'%'
                ORDER BY sample_date''',(year, month)):
                    temperatures[row[0]] = row[1]
                return temperatures
            except sqlite3.Error as error:
                self.logger.error("An error occurred while fetching data from the database: %s",
                                  error)
                return []



    def purge_data(self):
        """
        Purge all weather data from the SQLite database.
        """
        with self.get_cursor() as cursor:
            try:
                cursor.execute('DELETE FROM weather_data')
                self.logger.info("Data purged successfully.")
            except sqlite3.Error as error:
                self.logger.error("An error occurred while purging data from the database: %s",
                                   error)

    def get_cursor(self):
        """
        Get a cursor to use for database operations.

        Returns:
        - A cursor object for the SQLite database.
        """
        return DBCM(self.dbname)


class DBCM:
    '''
    A class that represents a connection to a database.
    '''
    def __init__(self, dbname):
        self.dbname = dbname
        self.logger = logging.getLogger(__name__)

    def __enter__(self):
        try:
            self.conn = sqlite3.connect(self.dbname)
            self.cursor = self.conn.cursor()
            self.logger.info("Connection to database established successfully.")
            return self.cursor
        except sqlite3.Error as error:
            self.logger.error("An error occurred while connecting to the database: %s", error)
            return None

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            self.conn.rollback()
        else:
            try:
                self.conn.commit()
                self.logger.info("Changes committed successfully.")
            except sqlite3.Error as error:
                self.logger.error("An error occurred while committing changes to the database: %s",
                                  error)
        try:
            self.cursor.close()
            self.conn.close()
            self.logger.info("Connection to database closed successfully.")
        except sqlite3.Error as error:
            self.logger.error("An error occurred while closing the database connection: %s", error)


def main():
    '''
    The main method.
    '''

    # Initialize the database
    data_base = DBOperations("mydatabase.db")
    data_base.initialize_db()

    # Get the weather data
    scraper = WeatherScraper()
    data = scraper.get_data()

    # Process the data and prepare the rows
    rows = []
    for date, temps in data.items():
        row = (
            date,
            "Winnipeg",
            temps["Max"],
            temps["Min"],
            temps["Mean"]
        )
        rows.append(row)

    # Save the data to the database
    with data_base.get_cursor() as cursor:
        try:
            cursor.executemany('''
                INSERT OR IGNORE INTO weather_data
                (sample_date, location, min_temp, max_temp, avg_temp)
                VALUES (?, ?, ?, ?, ?)
            ''', rows)
            data_base.logger.info("Inserted %s rows into the database.", len(rows))
        except sqlite3.Error as error:
            data_base.logger.error("An error occurred while inserting data: %s", error)

if __name__ == '__main__':
    main()


from plot_operations import PlotOperations
import sys
import logging
from datetime import datetime
from scrape_weather import WeatherScraper
from db_operations import DBOperations
from plot_operations import PlotOperations

class WeatherProcessor:
    def __init__(self):
        self.db_operations = DBOperations("../mydatabase.db")
        self.create_plot = PlotOperations()
        self.scrape_weather = WeatherScraper()

    def main_menu(self):
        while True:
            print("\nWeather Data Processor")
            print("1. Download a full set of weather data")
            print("2. Update weather data")
            print("3. Generate box plot based on year range")
            print("4. Generate line plot based on a month and year")
            print("5. Exit")
            selection = input("Make a selection: ")

            if selection == "1":
                self.download_full_set()
                input("Press Enter to continue...")
            elif selection == "2":
                self.update_weather_data()
                input("Press Enter to continue...")
            elif selection == "3":
                self.generate_box_plot()
                input("Press Enter to continue...")
            elif selection == "4":
                self.generate_line_plot()
                input("Press Enter to continue...")
            elif selection == "5":
                sys.exit(0)
            else:
                print("Invalid choice. Select another option.")

    def download_full_set(self):
        data = self.scrape_weather.get_data()
        self.db_operations.save_data(data)

    #TODO
    def update_weather_data(self):
        return None

    def generate_box_plot(self):
        start_year = input("Enter the year to start gathering data from: ")
        end_year = input("Enter the year to end the search for data at: ")

        data = self.db_operations.fetch_data_year_to_year(start_year, end_year)
        self.create_plot.create_box_plot(start_year, end_year, data)

    def generate_line_plot(self):
        month = input("Enter a month to search for weather data (January = 1, February = 2): ")
        year = input("Enter the year of the month you wish to view data from: ")

        data = self.db_operations.fetch_data_single_month(month, year)
        self.create_plot.create_line_plot(data)

if __name__ == '__main__':
    processor = WeatherProcessor()
    processor.main_menu()


'''
A module that creates graphs from data pulled from a database.
'''
import logging
import matplotlib.pyplot as plt
from db_operations import DBOperations

class PlotOperations():
    '''A class that plots data based on user input.'''
    logger = logging.getLogger("main." + __name__)

    def create_box_plot(self, start_year, end_year, data):
        '''
        A function that creates a box plot of data pulled from the database based off of user input.
        
        Parameters:
        - start_year: int - The year in which the user wants the range to begin.
        - end_year: int - The year in which the user wants the range to end.'''
        try:
            data_to_plot = list(data.values())

            plt.boxplot(data_to_plot) #Feed the data
            plt.title(f'Monthly temperature distribution for: {start_year} to {end_year}')
            plt.xlabel('Month') # Label the x-axis
            plt.ylabel('Temperature (Celcius)') # Label the y-axis
            plt.show() # Show the graph

        except Exception as error:
            self.logger.error("PlotOps:boxplot:%s", error)

    def create_line_plot(self, data):
        """
        Creates a line plot based on the data provided by the user.
        
        Parameters:
        - data: dict - A collection of data stored in a dictionary."""

        try:
            dates = list(data.keys()) # Dates are the keys in the dictionary
            temps = list(data.values()) # Temperatures are the values in the dictionary

            plt.plot(dates, temps) # Feed the data
            plt.title('Daily Avg Temperatures') # Create the title
            plt.xlabel('Days of Month') # Label the x axis
            plt.ylabel('Avg Daily Temp') # Label the y axis

            # Create text rotation on the x axis so they all fit properly
            plt.xticks(rotation = 50, horizontalalignment = 'right')
            plt.show() # Show the graph

        except Exception as error:
            self.logger.error("PlotOps:lineplot:%s", error)

# db = DBOperations("mydatabase.db")
# data = db.fetch_data_year_to_year(1996, 2023)
# print(data)
# PlotOperations().create_box_plot(1996, 2023, data)