diff --git a/ai.py b/ai.py index 66b97a0..ce1c18e 100644 --- a/ai.py +++ b/ai.py @@ -4,7 +4,8 @@ """ # pylint: disable=C0301 import os -from openai import OpenAI +import logging +from openai import OpenAI, OpenAIError def generate_ai_text(forecast): """ @@ -12,26 +13,47 @@ def generate_ai_text(forecast): """ # To authenticate with the model you will need to generate a personal access token (PAT) in your GitHub settings. # Create your PAT token by following instructions here: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens - client = OpenAI( - base_url="https://models.inference.ai.azure.com", - api_key=os.getenv("OPENAI_API_KEY"), - ) - - response = client.chat.completions.create( - messages=[ - { - "role": "system", - "content": "You are a young meteorologist explaining today's forecast in Syracuse NY in the morning(Say 'Good Morning Syracuse!'). Act like you're making a social media post. Make sure to Consildate the days into one description (don't do morning and night seperately). Make sure it's under 300 characters", - }, - { - "role": "user", - "content": forecast, - } - ], - model="gpt-4o", - temperature=1, - max_tokens=4096, - top_p=1 - ) - - print(response.choices[0].message.content) + try: + api_key=os.getenv("OPENAI_API_KEY") + if not api_key: + raise ValueError("Missing API Key") + + client = OpenAI( + base_url="https://models.inference.ai.azure.com", + api_key=api_key, + ) + + response = client.chat.completions.create( + messages=[ + { + "role": "system", + "content": "You are a young meteorologist explaining today's forecast in Syracuse NY in the morning(Say 'Good Morning Syracuse!'). Act like you're making a social media post. Make sure to Consildate the days into one description (don't do morning and night seperately). Make sure it's under 300 characters", + }, + { + "role": "user", + "content": forecast, + } + ], + model="gpt-4o", + temperature=1, + max_tokens=300, + top_p=1 + ) + + if not response or not response.choices or not response.choices[0].message: + raise ValueError("Unexpect API response format") + + ai_text = response.choices[0].message.content + return ai_text + + except ValueError as err: + logging.error("ValueError: %s", err) + return "Daily Weather Forecast Is down Today" + + except OpenAIError as err: + logging.error("OpenAIError: %s", err) + return "Daily Weather Forecast Is down Today" + + except TimeoutError as err: + logging.error("Request Time Out: %s", err) + return "Daily Weather Forecast Is down Today" diff --git a/bluesky.py b/bluesky.py index 8d461ff..effbe5c 100644 --- a/bluesky.py +++ b/bluesky.py @@ -5,8 +5,8 @@ import os import sys -import requests from dotenv import load_dotenv +from atproto import Client # Load enviornment variables from the .env file def load_bluesky_credentials(): @@ -23,22 +23,22 @@ def load_bluesky_credentials(): print(".env does not exist.") sys.exit(1) -def create_bluesky_session(username: str, password: str): +def create_bluesky_post(username: str, password: str, post_text: str): """ - Authenticate BlueSky credentials and create a session to recieve access token. - - returns: - - Access token (accessJwt) for authentication. + Creates post on BlueSky and returns its post link (URL) """ - url = "https://bsky.social/xrpc/com.atproto.server.createSession" - payload = {"identifier": username, "password": password} + client = Client() + + try: + client.login(username, password) + except Exception as e: + raise RuntimeError(f"Login failed: {e}") from e try: - response = requests.post(url, json=payload, timeout=10) - response.raise_for_status() - session = response.json() - return session["accessJwt"] - except requests.exceptions.RequestException as err: - print("Error during authentication:", err) - print("Response:", response.text if "response" in locals() else "No response") - sys.exit(1) + post = client.send_post(post_text) + post_id = post.uri.split('/')[-1] + except Exception as e: + raise RuntimeError(f"Post creation failed: {e}") from e + + url = f"https://bsky.app/profile/{username}/post/{post_id}" + return url diff --git a/main.py b/main.py index be5dbf8..410edc8 100644 --- a/main.py +++ b/main.py @@ -25,21 +25,22 @@ def main(): """ This runs the main module of BlueSkyTracker """ - print("Loading Credentials...") - bluesky_handle, bluesky_app_password = bluesky.load_bluesky_credentials() - - print("Athenticating...") - access_token = bluesky.create_bluesky_session(bluesky_handle, bluesky_app_password) - print("Authentication successful.") - - print("Fetching Forecast") + print("Fetching Forecast...") forecast = weather.fetch_weather_forecast() - print("Formatting Weather Data") + print("Formatting Weather Data...") forecast = weather.format_weather_data(forecast) - print("Generating AI Text") - ai.generate_ai_text(forecast) + print("Generating AI Text...") + post_text = ai.generate_ai_text(forecast) + + print("Loading BlueSky Credentials...") + bluesky_handle, bluesky_app_password = bluesky.load_bluesky_credentials() + + print("Creating Post...") + bluesky.create_bluesky_post(bluesky_handle, bluesky_app_password, post_text) + + print("Post Created Succesfully") if __name__ == "__main__": main() diff --git a/requirements.txt b/requirements.txt index 329150c..83cce53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ requests python-dotenv pylint -openai \ No newline at end of file +openai +atproto \ No newline at end of file diff --git a/tests/bluesky_auth_test.py b/tests/bluesky_auth_test.py index abcbaa6..25dba08 100644 --- a/tests/bluesky_auth_test.py +++ b/tests/bluesky_auth_test.py @@ -6,10 +6,8 @@ # pylint: disable=E0401 # pylint: disable=W0613 import unittest -from unittest.mock import patch, Mock -import requests +from unittest.mock import patch from bluesky import load_bluesky_credentials -from bluesky import create_bluesky_session class TestLoadCredentials(unittest.TestCase): """ @@ -43,60 +41,3 @@ def test_env_exist_with_valid_credentials(self, mock_load_dotenv, mock_getenv): """ credentials = load_bluesky_credentials() self.assertTrue(all(credentials)) - - - -class TestCreateBlueSkySession(unittest.TestCase): - """ - Testing the create_bluesky_session() method - """ - def setUp(self): - self.username = "ValidUsername" - self.password = "ValidPassword" - - @patch("bluesky.requests.post") - def test_successful_authentication(self, mock_post): - """" - Test if authentication was successful - """ - mock_response = Mock() - mock_response.json.return_value = {"accessJwt": "mocked_jwt_token"} - mock_response.status_code = 200 - mock_post.return_value = mock_response - - result = create_bluesky_session(username = self.username, password = self.password) - self.assertEqual(result, "mocked_jwt_token") - - @patch("bluesky.requests.post") - def test_unsuccesful_authentication(self, mock_post): - """ - Test if authentication was not successful - - Password or User was incorrect - """ - mock_response = Mock() - mock_response.status_code = 401 - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Client Error: Unauthorized") - mock_post.return_value = mock_response - - with self.assertRaises(SystemExit): - create_bluesky_session(username = self.username, password = self.password) - - @patch("bluesky.requests.post") - def test_invalid_request_error(self, mock_post): - """ - Test if authentication was not successful - - Bad Request Status Code 400 - """ - mock_response = Mock() - mock_response.status_code = 400 - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("400 Client Error: Bad Request") - mock_post.return_value = mock_response - - with self.assertRaises(SystemExit): - create_bluesky_session(self.username, self.password) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/weather_test.py b/tests/weather_test.py index 7b2304e..acb316f 100644 --- a/tests/weather_test.py +++ b/tests/weather_test.py @@ -6,7 +6,7 @@ import unittest from unittest.mock import patch, Mock import requests -from weather import fetch_weather_forecast +from weather import fetch_weather_forecast, format_weather_data class TestFetchWeatherForecast(unittest.TestCase): """ @@ -87,3 +87,41 @@ def test_api_500_error(self, mock_get): result = fetch_weather_forecast() self.assertIsNone(result) + +class TestFormatWeatherData(unittest.TestCase): + """ + Testing the format_weather_data method + """ + def test_basic_input(self): + """ + Testing basic Input Functionality Using Diff Tests + """ + sample_data = { + "day_1": [ + { + "name": "Monday", + "temperature": 75, + "temperatureUnit": "F", + "probabilityOfPrecipitation": 20, + "windSpeed": "10 mph", + "windDirection": "S", + "detailedForecast": "Sunny with some clouds" + }, + { + "name": "Monday Night", + "temperature": 55, + "temperatureUnit": "F", + "probabilityOfPrecipitation": 40, + "windSpeed": "5 mph", + "windDirection": "SW", + "detailedForecast": "Clear Skies" + } + ] + } + + result = format_weather_data(sample_data) + assert "Monday & Monday Night:" in result + assert "Temperature: 75F" in result + assert "Probability Of Precipitation: 40" in result + assert "Wind Speed: 5 mph, Direction SW" in result + assert "Detailed Forecast: Clear Skies" in result diff --git a/weather.py b/weather.py index 7d2ff7a..dadc726 100644 --- a/weather.py +++ b/weather.py @@ -59,5 +59,5 @@ def format_weather_data(weather_data): f"Wind Speed: {period['windSpeed']}, Direction {period['windDirection']}\n" f"Detailed Forecast: {period['detailedForecast']}\n\n" ) - print(weather_input) + return weather_input