diff --git a/Rain_alert/.env.example b/Rain_alert/.env.example new file mode 100644 index 00000000..a092f2e0 --- /dev/null +++ b/Rain_alert/.env.example @@ -0,0 +1,5 @@ +OPENWEATHER_API_KEY=Open-Weather-API +TWILIO_ACCOUNT_SID=Your-Account-SID +TWILIO_AUTH_TOKEN=Your-Twilio-Auth-Token +TWILIO_NUMBER=Your-Twilio-Phone-Number +USER_NUMBER=User-Number \ No newline at end of file diff --git a/Rain_alert/.gitignore b/Rain_alert/.gitignore new file mode 100644 index 00000000..7c8c1516 --- /dev/null +++ b/Rain_alert/.gitignore @@ -0,0 +1,5 @@ +venv/ +__pycache__/ +.vscode/ +creds.py +.env \ No newline at end of file diff --git a/Rain_alert/helper.py b/Rain_alert/helper.py new file mode 100644 index 00000000..47b9db41 --- /dev/null +++ b/Rain_alert/helper.py @@ -0,0 +1,137 @@ +""" +A module for helper functions. +""" + +from dotenv import load_dotenv +from os import getenv +from requests import get +from requests.exceptions import HTTPError +from samples import wx_sample_response +from twilio.rest import Client + + +load_dotenv() # Take environment variables from .env + +def get_weather_data(lat: float, lon: float, exclude: str) -> dict: + """ + Get current weather from the given city's latitude and longitude. + + Parameters: + lat (float): Angular distance of place north/south of equater + lonv(float): Angular distance of place east/west of greenwich meridian + Return: + weather_data (dict): All weather information about the city + """ + + URL = f"https://api.openweathermap.org/data/3.0/onecall?lat={lat}&lon={lon}&exclude={exclude}&appid={getenv('OPENWEATHER_API')}" + test_url = "https://api.github.com" + + #f"api.openweathermap.org/data/2.5/weather?q=London,uk&APPID={api_key}" + + # Catch any errors that may arise trying to GET the Weather data + try: + response = get(URL) + response.raise_for_status() + except HTTPError as http_err: + print(f"HTTP Error occured: {http_err}") + except Exception as e: + print(f"Other error occured: {e}") + else: + return response.json() # If request was a success + + +def get_user_location() -> tuple[str, str]: + """ + Get current user location and return coordinates (lat, lon) + + Parameters: + None + Return: + location (tuple): Location of user in latitude and longitude + """ + + url = "https://ipinfo.io/" + try: + response = get(url) + response.raise_for_status() + except HTTPError as http_err: + print(f"HTTP Error occured: {http_err}") + except Exception as e: + print(f"Other error occured: {e}") + else: + json_format = response.json() # Make response into json for referencing + location_str = tuple(json_format['loc'].split(",")) # Get lat, long, make them a tuple + # Convert the str location to a float and give it back + return tuple(map(float, location_str)) + + +def process_weather(weather_data: dict) -> str: + """ + Process weather from given location, give rain warning if needed + + Parameters: + weather_data (dict): Weather information from a given location + Return: + weather_forecast (str): Warning to user if there is rain + """ + warning_words = [ + "rain", + "drizzle", + "precipitation", + "thunderstorms", + "showers", + "hail", + "sleet", + "snow", + "snowflake" + ] + + # Get weather data from the dictionary at 'data' key element 0 + dict_wx_data = weather_data['data'][0] + # Get dictionary with weather data we are interested on + weather_dict = dict_wx_data['weather'][0] + # Issue the warning + for item in weather_dict: + for warning in warning_words: + if type(weather_dict[item]) == int: # Skip integeer items + continue + else: + if warning in weather_dict[item].lower(): + return "Rain is expected in you area (Time Expected: 1 hour), get to shelter." + return "Clear blue sky, go to the swimming pool" + + +def send_sms(message: str) -> dict: + """ + Send SMS to verified number from predefined number. + + Parameters: + message (str): Message we want to send to the user + Return: + response (dict): Response from the attempt to send the message + """ + + client = Client(str(getenv('TWILIO_ACCOUNT_SID')), str(getenv('TWILIO_AUTH_TOKEN'))) + + message = client.messages.create( + from_= str(getenv('TWILIO_NUMBER')), + body = message, + to = str(getenv('USER_NUMBER')) + ) + + return message.sid + + +if __name__ == "__main__": + # Test cases + # Test data + # Johannesburg (lat, lon) + # lat = -26.204103 + # lon = 28.047304 + # exclude = "hourly" + # print(f"Hardcoded: ({lat}, {lon})") + # print(f"Retrieval: {get_user_location()}") + # print(get_weather_data(lat, lon, exclude)) + # message = "Hey, Sakhile, alien technology is here! Ayinabungozi lento." + # print(send_sms(message)) + print(process_weather(wx_sample_response)) \ No newline at end of file diff --git a/Rain_alert/main.py b/Rain_alert/main.py index e69de29b..8eeeb45c 100644 --- a/Rain_alert/main.py +++ b/Rain_alert/main.py @@ -0,0 +1,37 @@ +""" +A module to check rain forecast and arn the user through Twilio SMS +""" + +from helper import ( + get_user_location, + get_weather_data, + send_sms, + process_weather +) + + +def main() -> None: + """ + Main driver function for the program. + + Parameters + None + Return: + None + """ + # Get user location + location = get_user_location() + # Uunpack tuple to latitude and longitude + lat, lon = location + # Get hourly weather data + exclude = 'hourly' + hourly_forcast = get_weather_data(lat, lon, exclude) + # Process that data and formulate warning for the user + warning = process_weather(hourly_forcast) + # Send the user a warning SMS + send_sms(warning) + + +if __name__ == "__main__": + # Start the program + main() diff --git a/Rain_alert/requirements.txt b/Rain_alert/requirements.txt new file mode 100644 index 00000000..b93584b2 --- /dev/null +++ b/Rain_alert/requirements.txt @@ -0,0 +1,25 @@ +aiohappyeyeballs==2.4.3 +aiohttp==3.10.10 +aiohttp-retry==2.9.1 +aiosignal==1.3.1 +async-timeout==4.0.3 +attrs==24.2.0 +certifi==2024.8.30 +charset-normalizer==3.4.0 +click==8.1.7 +decorator==5.1.1 +frozenlist==1.5.0 +future==1.0.0 +geocoder==1.38.1 +idna==3.10 +multidict==6.1.0 +propcache==0.2.0 +PyJWT==2.9.0 +python-dotenv==1.0.1 +ratelim==0.1.6 +requests==2.32.3 +six==1.16.0 +twilio==9.3.6 +typing_extensions==4.12.2 +urllib3==2.2.3 +yarl==1.17.1 diff --git a/Rain_alert/samples.py b/Rain_alert/samples.py new file mode 100644 index 00000000..08709cea --- /dev/null +++ b/Rain_alert/samples.py @@ -0,0 +1,65 @@ +# Sample response from Openweather API +wx_sample_response = { + "lat": 52.2297, + "lon": 21.0122, + "timezone": "Sakhile/Mars", + "timezone_offset": 3600, + "data": [ + { + "dt": 1645888976, + "sunrise": 1645853361, + "sunset": 1645891727, + "temp": 279.13, + "feels_like": 276.44, + "pressure": 1029, + "humidity": 64, + "dew_point": 272.88, + "uvi": 0.06, + "clouds": 0, + "visibility": 10000, + "wind_speed": 3.6, + "wind_deg": 340, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ] + } + ] +} + +sample_dict_wx_data = { + "dt": 1645888976, + "sunrise": 1645853361, + "sunset": 1645891727, + "temp": 279.13, + "feels_like": 276.44, + "pressure": 1029, + "humidity": 64, + "dew_point": 272.88, + "uvi": 0.06, + "clouds": 0, + "visibility": 10000, + "wind_speed": 3.6, + "wind_deg": 340, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ] +} + +# Sample response from Twilio +# 400 - BAD REQUEST - The data given in the POST or PUT failed validation. Inspect the response body for details. +res = { + "code": 21608, + "message": "The number +27082581XXXX is unverified. Trial accounts cannot send messages to unverified numbers; verify +27082581XXXX at twilio.com/user/account/phone-numbers/verified, or purchase a Twilio number to send messages to unverified numbers", + "more_info": "https://www.twilio.com/docs/errors/21608", + "status": 400 +} \ No newline at end of file