diff --git a/.gitignore b/.gitignore index 3ed6cf2..d027e2c 100644 --- a/.gitignore +++ b/.gitignore @@ -159,7 +159,7 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ *.cache.sqlite *.pyc \ No newline at end of file diff --git a/README.md b/README.md index 2ce4564..1937fa6 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,14 @@ Wave Period: 9.8 | show_rain_sum / srs | Show the rain sum | | show_precipitation_prob / spp | Show the max precipitation chance | | hide_uv / huv | Hide uv index | +| show_past_uv | Show past uv index | +| hide_past_uv | Hide past uv index | +| show_height_history | Show past wave height index | +| hide_height_history | Hide past wave height index | +| show_direction_history | Show past wave direction index | +| hide_direction_history | Hide past wave direction index | +| show_period_history | Show past wave period index | +| hide_period_history | Hide past wave period index | | hide_height / hh | Hide surf height | | hide_direction / hdir | Hide Swell direction | | hide_period / hp | Hide swell period | @@ -73,6 +81,7 @@ Wave Period: 9.8 * `curl localhost:8000` * `curl localhost:8000?location=new_york,hide_height,hide_wave,show_large_wave` * `curl localhost:8000?fc=3,hdate,loc=trestles` +* `curl localhost:8000?show_past_uv,show_height_history,show_direction_history,show_period_history` **For detailed information you can access the [help](https://github.com/ryansurf/cli-surf/blob/main/help.txt) page** diff --git a/src/api.py b/src/api.py index 8a9abe4..955f80d 100644 --- a/src/api.py +++ b/src/api.py @@ -2,6 +2,7 @@ Functions that make API calls stored here """ +from datetime import datetime, timedelta from http import HTTPStatus import numpy as np @@ -14,6 +15,8 @@ from src import helper +testing = 1 + def get_coordinates(args): """ @@ -90,6 +93,76 @@ def get_uv(lat, long, decimal, unit="imperial"): return current_uv_index +def get_uv_history(lat, long, decimal, unit="imperial"): + """ + Retrieve the UV index from one year ago for specified coordinates. + + This function accesses the Open-Meteo API to fetch the hourly UV index + for the given latitude and longitude. It formats the result based on the + specified decimal precision. + + Args: + lat (float): Latitude of the location. + long (float): Longitude of the location. + decimal (int): Number of decimal places to round the output. + unit (str): Unit of measurement ('imperial' or 'metric'). + Defaults to 'imperial'. + + Returns: + str: The historical UV index rounded to the specified decimal places, + or an error message if no data is found. + + API Documentation: + https://open-meteo.com/en/docs/air-quality-api + """ + # Set up the Open-Meteo API client with caching and retry on error + cache_session = requests_cache.CachedSession(".cache", expire_after=3600) + retry_session = retry(cache_session, retries=5, backoff_factor=0.2) + openmeteo = openmeteo_requests.Client(session=retry_session) + + # Calculate the date one year ago and the current hour + one_year_ago = datetime.now() - timedelta(days=365) + formatted_date_one_year_ago = one_year_ago.strftime("%Y-%m-%d") + current_hour = one_year_ago.hour + + # Define the API request parameters + url = "https://air-quality-api.open-meteo.com/v1/air-quality" + params = { + "latitude": lat, + "longitude": long, + "length_unit": unit, + "hourly": ["uv_index"], + "start_date": formatted_date_one_year_ago, + "end_date": formatted_date_one_year_ago, + "timezone": "auto", + } + + # For testing purposes if testing equals 1 it will continue + if testing == 1: + # Attempt to fetch the UV index data from the API + try: + responses = openmeteo.weather_api(url, params=params) + + except ValueError: + return "No data" + + # Process the first response (assuming a single location) + response = responses[0] + + # Extract hourly UV index values + hourly = response.Hourly() + hourly_uv_index = hourly.Variables(0).ValuesAsNumpy() + + else: + hourly_uv_index = [0.5678] * 24 + + # Retrieve the UV index for the current hour from one year ago + historical_uv_index = hourly_uv_index[current_hour] + + # Format the result to the specified decimal precision + return f"{historical_uv_index:.{decimal}f}" + + def ocean_information(lat, long, decimal, unit="imperial"): """ Get Ocean Data at coordinates @@ -129,6 +202,84 @@ def ocean_information(lat, long, decimal, unit="imperial"): return [current_wave_height, current_wave_direction, current_wave_period] +def ocean_information_history(lat, long, decimal, unit="imperial"): + """ + Retrieve ocean data from one year ago for specified coordinates. + + This function accesses the Open-Meteo API to fetch + hourly ocean data including wave height, + direction, and period for the specified latitude + and longitude. It formats the results based on the + specified decimal precision. + + Args: + lat (float): Latitude of the location. + long (float): Longitude of the location. + decimal (int): Number of decimal places to round the output. + unit (str): Unit of measurement ('imperial' or 'metric'). + Defaults to 'imperial'. + + Returns: + list: A list containing the wave height, direction, and period rounded + to the specified decimal places, + or an error message if no data is found. + + API Documentation: + https://open-meteo.com/en/docs/marine-weather-api + """ + # Set up the Open-Meteo API client with caching and retry on error + cache_session = requests_cache.CachedSession(".cache", expire_after=3600) + retry_session = retry(cache_session, retries=5, backoff_factor=0.2) + openmeteo = openmeteo_requests.Client(session=retry_session) + + # Calculate the date and current hour one year ago + one_year_ago = datetime.now() - timedelta(days=365) + formatted_date_one_year_ago = one_year_ago.strftime("%Y-%m-%d") + current_hour = one_year_ago.hour # Combined calculation here + + # Define the API request parameters + url = "https://marine-api.open-meteo.com/v1/marine" + params = { + "latitude": lat, + "longitude": long, + "hourly": ["wave_height", "wave_direction", "wave_period"], + "length_unit": unit, + "timezone": "auto", + "start_date": formatted_date_one_year_ago, + "end_date": formatted_date_one_year_ago, + } + + # For testing purposes if testing equals 1 it will continue + if testing == 1: + # Attempt to fetch the UV index data from the API + try: + responses = openmeteo.weather_api(url, params=params) + + except ValueError: + return "No data" + + # Process the first response (assuming a single location) + response = responses[0] + + # Extract hourly values for the specified metrics + hourly = response.Hourly() + hourly_wave_height = hourly.Variables(0).ValuesAsNumpy() + hourly_wave_direction = hourly.Variables(1).ValuesAsNumpy() + hourly_wave_period = hourly.Variables(2).ValuesAsNumpy() + + else: + hourly_wave_height = [0.5678] * 24 + hourly_wave_direction = [0.5678] * 24 + hourly_wave_period = [0.5678] * 24 + + # Retrieve data for the current hour from one year ago + return [ + f"{hourly_wave_height[current_hour]:.{decimal}f}", + f"{hourly_wave_direction[current_hour]:.{decimal}f}", + f"{hourly_wave_period[current_hour]:.{decimal}f}", + ] + + def current_wind_temp(lat, long, decimal, temp_unit="fahrenheit"): """ Gathers the wind and temperature data @@ -364,6 +515,7 @@ def gather_data(lat, long, arguments): ocean_data = ocean_information( lat, long, arguments["decimal"], arguments["unit"] ) + uv_index = get_uv(lat, long, arguments["decimal"], arguments["unit"]) wind_temp = current_wind_temp(lat, long, arguments["decimal"]) @@ -385,9 +537,27 @@ def gather_data(lat, long, arguments): "Long": long, "Location": arguments["city"], "Height": ocean_data[0], + "Height one year ago": ( + ocean_information_history( + lat, long, arguments["decimal"], arguments["unit"] + )[0] + ), "Swell Direction": ocean_data[1], + "Swell Direction one year ago": ( + ocean_information_history( + lat, long, arguments["decimal"], arguments["unit"] + )[1] + ), "Period": ocean_data[2], + "Period one year ago": ( + ocean_information_history( + lat, long, arguments["decimal"], arguments["unit"] + )[2] + ), "UV Index": uv_index, + "UV Index one year ago": ( + get_uv_history(lat, long, arguments["decimal"], arguments["unit"]) + ), "Air Temperature": air_temp, "Wind Speed": wind_speed, "Wind Direction": wind_dir, diff --git a/src/helper.py b/src/helper.py index 5a63a14..07a408c 100644 --- a/src/helper.py +++ b/src/helper.py @@ -24,9 +24,13 @@ def arguments_dictionary(lat, long, city, args): "show_wave": 1, "show_large_wave": 0, "show_uv": 1, + "show_past_uv": 0, "show_height": 1, "show_direction": 1, "show_period": 1, + "show_height_history": 0, + "show_direction_history": 0, + "show_period_history": 0, "show_city": 1, "show_date": 1, "show_air_temp": 0, @@ -63,12 +67,24 @@ def set_output_values(args, arguments_dictionary): # noqa "slw": ("show_large_wave", 1), "hide_uv": ("show_uv", 0), "huv": ("show_uv", 0), + "show_past_uv": ("show_past_uv", 1), + "spuv": ("show_past_uv", 1), + "hide_past_uv": ("show_past_uv", 0), "hide_height": ("show_height", 0), "hh": ("show_height", 0), + "show_height_history": ("show_height_history", 1), + "shh": ("show_height_history", 1), + "hide_height_history": ("show_height_history", 0), "hide_direction": ("show_direction", 0), "hdir": ("show_direction", 0), + "show_direction_history": ("show_direction_history", 1), + "sdh": ("show_direction_history", 1), + "hide_direction_history": ("show_direction_history", 0), "hide_period": ("show_period", 0), "hp": ("show_period", 0), + "show_period_history": ("show_period_history", 1), + "sph": ("show_period_history", 1), + "hide_period_history": ("show_period_history", 0), "hide_location": ("show_city", 0), "hl": ("show_city", 0), "hide_date": ("show_date", 0), @@ -149,9 +165,25 @@ def print_ocean_data(arguments_dict, ocean_data_dict): # List of tuples mapping argument keys to ocean data keys and labels mappings = [ ("show_uv", "UV Index", "UV index: "), + ("show_past_uv", "UV Index one year ago", "UV Index one year ago: "), ("show_height", "Height", "Wave Height: "), + ( + "show_height_history", + "Height one year ago", + "Wave Height one year ago: ", + ), ("show_direction", "Swell Direction", "Wave Direction: "), + ( + "show_direction_history", + "Swell Direction one year ago", + "Wave Direction one year ago: ", + ), ("show_period", "Period", "Wave Period: "), + ( + "show_period_history", + "Period one year ago", + "Wave Period one year ago:", + ), ("show_air_temp", "Air Temperature", "Air Temp: "), ("show_wind_speed", "Wind Speed", "Wind Speed: "), ("show_wind_direction", "Wind Direction", "Wind Direction: "), diff --git a/tests/test_api.py b/tests/test_api.py index 2aa186c..b4bdcd0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,12 +4,19 @@ Run pytest: pytest """ +from unittest.mock import patch + +import pytest +from openmeteo_requests.Client import OpenMeteoRequestsError + from src.api import ( forecast, gather_data, get_coordinates, get_uv, + get_uv_history, ocean_information, + ocean_information_history, seperate_args_and_get_location, ) from src.helper import arguments_dictionary @@ -77,3 +84,89 @@ def test_seperate_args_and_get_location(): assert isinstance(lat, (int, float)) assert isinstance(long, (int, float)) assert "Pleasure Point" in str(city) + + +def test_get_uv_history_basic_functionality(): + """ + Test the basic functionality of the get_uv_history function. + + This test verifies that the function returns a string + when provided with valid latitude and longitude coordinates. + """ + uv = get_uv_history(31.9505, 115.8605, 2) # Perth coordinates + assert isinstance(uv, str) + + +def test_get_uv_history_invalid_coordinates(): + """ + Test get_uv_history with invalid coordinates. + + This test checks that the function raises an OpenMeteoRequestsError + when provided with latitude and longitude values that are out of range. + """ + with pytest.raises(OpenMeteoRequestsError): + get_uv_history(1000, -2000, 2) + + +@patch("src.api.testing", new=0) # Set testing variable to 0 +def test_get_uv_history_api_response(): + """ + Test how get_uv_history handles API response. + + This test verifies that the function returns the expected values + when called with valid coordinates while patching the API call request. + """ + result = get_uv_history(31.9505, 115.8605, 1) + expected_result = "0.6" + assert result == expected_result + + +def test_ocean_information_history_basic_functionality(): + """ + Test the basic functionality of the ocean_information_history function. + + This test checks that the function returns actual values for + the wave data points when provided with valid coordinates. + """ + waves = ocean_information_history(31.9505, 115.8605, 2) + assert waves[0] is not None + assert waves[1] is not None + assert waves[2] is not None + + +def test_ocean_information_history_invalid_coordinates(): + """ + Test ocean_information_history with invalid coordinates. + + This test ensures that the function raises an OpenMeteoRequestsError + when provided with latitude and longitude values that are out of range. + """ + with pytest.raises(OpenMeteoRequestsError): + get_uv_history(1000, -2000, 2) + + +def test_ocean_information_history_response_format(): + """ + Test the response format of ocean_information_history. + + This test verifies that the function returns a list with a + specific number of elements when called with valid coordinates. + """ + waves = ocean_information_history(31.9505, 115.8605, 2) + expected_wave_count = 3 + assert isinstance(waves, list) + assert len(waves) > 0 + assert len(waves) == expected_wave_count + + +@patch("src.api.testing", new=0) # Set testing variable to 0 +def test_ocean_information_history(): + """ + Test how ocean_information_history handles API response. + + This test verifies that the function returns the expected values + when called with valid coordinates while patching the API call request. + """ + result = ocean_information_history(31.9505, 115.8605, 1) + expected_result = ["0.6", "0.6", "0.6"] + assert result == expected_result diff --git a/tests/test_helper.py b/tests/test_helper.py index 15319ea..9076939 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -8,6 +8,7 @@ from unittest.mock import patch from src import cli, helper +from src.helper import set_output_values def test_invalid_input(): @@ -59,3 +60,85 @@ def test_print_gpt(): gpt_info = [None, ""] gpt_response = helper.print_gpt(surf_data, gpt_prompt, gpt_info) assert "gpt works" in gpt_response + + +def test_set_output_values_show_past_uv(): + args = ["show_past_uv"] + arguments_dictionary = {} + expected = {"show_past_uv": 1} + result = set_output_values(args, arguments_dictionary) + assert result == expected + + +def test_set_output_values_hide_past_uv(): + args = ["hide_past_uv"] + arguments_dictionary = {} + expected = {"show_past_uv": 0} + result = set_output_values(args, arguments_dictionary) + assert result == expected + + +def test_set_output_values_show_height_history(): + args = ["show_height_history"] + arguments_dictionary = {} + expected = {"show_height_history": 1} + result = set_output_values(args, arguments_dictionary) + assert result == expected + + +def test_set_output_values_hide_height_history(): + args = ["hide_height_history"] + arguments_dictionary = {} + expected = {"show_height_history": 0} + result = set_output_values(args, arguments_dictionary) + assert result == expected + + +def test_set_output_values_show_direction_history(): + args = ["show_direction_history"] + arguments_dictionary = {} + expected = {"show_direction_history": 1} + result = set_output_values(args, arguments_dictionary) + assert result == expected + + +def test_set_output_values_hide_direction_history(): + args = ["hide_direction_history"] + arguments_dictionary = {} + expected = {"show_direction_history": 0} + result = set_output_values(args, arguments_dictionary) + assert result == expected + + +def test_set_output_values_show_period_history(): + args = ["show_period_history"] + arguments_dictionary = {} + expected = {"show_period_history": 1} + result = set_output_values(args, arguments_dictionary) + assert result == expected + + +def test_set_output_values_hide_period_history(): + args = ["hide_period_history"] + arguments_dictionary = {} + expected = {"show_period_history": 0} + result = set_output_values(args, arguments_dictionary) + assert result == expected + + +def test_set_output_values_combined_arguments(): + args = [ + "show_past_uv", + "show_height_history", + "show_direction_history", + "show_period_history", + ] + arguments_dictionary = {} + expected = { + "show_past_uv": 1, + "show_height_history": 1, + "show_direction_history": 1, + "show_period_history": 1, + } + result = set_output_values(args, arguments_dictionary) + assert result == expected