From 93b7b7ada13e7c5a183265763c6d2a2c50f4e1e8 Mon Sep 17 00:00:00 2001 From: LG <76845820+im-coder-lg@users.noreply.github.com> Date: Sat, 6 May 2023 22:55:05 +0530 Subject: [PATCH] init: Basic GUI setup + upgrades (#5) * init: Basic GUI setup + upgrades * feat: workflows * Use classes and improve everything * More work * Add ruff, fix docs, improve tests * Final fixes @Futura-Py/reviewers I have finished updating the code to follow PEP standards closer and for better readability, we able to get this pushed to main? * Pull main * Implement rdbende's suggestions --------- Co-authored-by: Moosems <95927277+Moosems@users.noreply.github.com> --- .github/workflows/formatting.yml | 36 +++++ .gitignore | 6 + Justfile | 19 ++- README.md | 75 +++++++++- main.py | 237 +++++++++++++++---------------- pyproject.toml | 10 ++ requirements.txt | 5 +- tests/test_api.py | 30 +--- weather.spec | 1 - 9 files changed, 268 insertions(+), 151 deletions(-) create mode 100644 .github/workflows/formatting.yml create mode 100644 pyproject.toml diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml new file mode 100644 index 0000000..8a59951 --- /dev/null +++ b/.github/workflows/formatting.yml @@ -0,0 +1,36 @@ +name: Code Formatting + +on: + schedule: + - cron: 30 05 15 * * + workflow_dispatch: + + +jobs: + formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.8 + - uses: actions/setup-node@v2 + with: + node-version: '14' + - uses: extractions/setup-just@v1 + with: + just-version: 0.8 # optional semver specification, otherwise latest + - name: Code Formatting (App) + run: | + just format + - name: Code Formatting (website) + run: | + npm i -g prettier + cd docs + npx prettier --write . + - uses: fregante/setup-git-user@v1 + - name: Commit + run: | + git add . + git commit -m "style: Code Formatting Workflow" -m "Either triggered by cron or workflow_dispatch" + git push --force \ No newline at end of file diff --git a/.gitignore b/.gitignore index 63e603a..b4f5e80 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,9 @@ dmypy.json # Pyre type checker .pyre/ + +# Ruff +.ruff-cache/ + +# Mac stuff +.DS_Store \ No newline at end of file diff --git a/Justfile b/Justfile index cc16045..4a4231f 100644 --- a/Justfile +++ b/Justfile @@ -1,3 +1,6 @@ +ruff: + ruff . --fix + isort: python3 -m isort *.py python3 -m isort tests/*.py @@ -8,9 +11,23 @@ black: install: python3 -m pip install -r requirements.txt + pip3 install autopep8 + ruff --version run: python3 main.py run-tests: - python3 -m unittest tests/test_*.py \ No newline at end of file + ruff . + python3 -m unittest tests/test_*.py + +autopep8: + python3 -m autopep8 --in-place *.py + python3 -m autopep8 --in-place tests/test_*.py + + +format: + just black + just autopep8 + just isort + ruff . --fix \ No newline at end of file diff --git a/README.md b/README.md index 16db3ec..f9bf622 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,73 @@ -# Futura Weather -Coming very soon +

Future Weather

+ +A simple weather app that uses the OpenWeatherMap API to get the weather for a given city. + +## Installation + +To run this app, you will need Python 3 installed on your computer. Clone this repository and navigate to the directory containing the `weather_app.py` file. + +Use the following command to install the required packages: +```bash +pip install -r requirements.txt +``` + + +Once the packages are installed, you can run the app using the following command: + +```bash +python3 weather_app.py +``` +or +```bash +py3 weather_app.py +``` + + +## Usage + +When you run the app, a window will open with a search bar, a label to display the weather information, and two buttons. + +To search for the weather in a city, simply enter the name of the city in the search bar and click the "Search for City" button. The app will display the temperature in Celsius for that city in the label. + +To exit the app, click the "Exit" button. + +# Documentation + +Additionally, the code contains several functions, methods, and classes: + +### `self_return_decorator()` +A decorator function that allows for chaining of methods. This function takes a method as an argument and returns a new function that calls the original method and returns the instance of the class. + +### `App()` +A class that inherits from the Tk class and defines the main window of the app. The __init__ method sets up the menubar, window, and widgets. The about, resize_app, exit_app, and OWMCITY methods define the behavior of the corresponding buttons in the app. + +### `.about()` +A method that displays a message box with information about the app. + +### `.resize_app()` +A method that uses tkinter to detect the minimum size of the app, get the center of the screen, and place the app there. + +### `.exit_app()` +A method that exits the app. + +### `.OWMCITY()` +A method that sends a request to the OpenWeatherMap API to get the weather for a given city and displays the temperature in Celsius in the label. If the city is not found, the label displays an error message. + +# Final Notes + +## Contributing + +Contributions are welcome! If you find a bug or have an idea for a new feature, please open an issue or submit a pull request. +## Pre-Commit Actions + +Before making any commits, run the following command: + +```bash +black .; isort .; ruff . --fix +``` + +This will automatically format the code to follow the PEP 8 style guide and run several linters to catch any errors or warnings + +## License + +This project is licensed under the MIT License. See the LICENSE file for details. \ No newline at end of file diff --git a/main.py b/main.py index 850b373..b17e28e 100644 --- a/main.py +++ b/main.py @@ -1,121 +1,116 @@ -import os -import sys -from time import sleep - -import requests - -TOKEN = "c439e1209216cc7e7c73a3a8d1d12bfd" -BASE_URL = "https://api.openweathermap.org/data/2.5/weather?" -BREAK = "\n\n=====\n\n" -# TOKEN = os.getenv("TOKEN") - - -def breakliner(): - print(f"{BREAK:-^30}") - return - - -def clearScreen(): - if sys.platform == "windows": - os.system("cls") - else: - os.system("clear") - return - - -def exitQuery(): - exitQ = input("Exit? ") - if exitQ == "y": - exit() - elif exitQ == "n": - clearScreen() - OWMCITY() - -def data_processing(): - main = data["main"] - humidity = main["humidity"] - pressure = main["pressure"] - temp = main["temp"] - temp_min = main["temp_min"] - temp_max = main["temp_max"] - - def ktc(temp, temp_min, temp_max): - temp = temp - 273.15 - temp_min = temp_min - 273.15 - temp_max = temp_max - 273.15 - print("Temperature: ", temp, "°C") - print("Minimum Temperature: ", temp_min, "°C") - print("Maximum Temperature: ", temp_max, "°C") - - CITY = data["name"] - sys = data["sys"] - country = sys["country"] - CITY2 = CITY + "," + " " + country - if CITY == CITY2: - pass - else: - CITY = data["name"] - sys = data["sys"] - country = sys["country"] - CITY2 = CITY + "," + " " + country - - w_main = data["weather"][0]["main"] - w_desc = data["weather"][0]["description"] - pressure = main["pressure"] - visibility = data["visibility"] - visibility_new = visibility / 1000 - wind = data["wind"]["speed"] - - def printData(): - breakliner() - print(f"{CITY2:-^30}") - print(f"Weather: {w_main}:- {w_desc}") - ktc(temp, temp_min, temp_max) - print(f"Humidity: {humidity}%") - print(f"Pressure: {pressure} hPa") - print(f"Wind speed: {wind} m/s") - print(f"Visibility: {visibility} m (or) {visibility_new} km") - - printData() - -def OWMCITY(): - # City Name request - global CITY - CITY = input( - "Enter City Name (should be in compliance to OpenWeatherMap's City Index): " - ) - if CITY == "exit": - clearScreen() - print("Three") - sleep(1) - clearScreen() - print("Two") - sleep(1) - clearScreen() - print("One.") - sleep(1) - clearScreen() - print("Exiting...") - sleep(0.5) - exit() - - else: - pass - # The URL in actuality be like: - URL = BASE_URL + "q=" + CITY + "&appid=" + TOKEN - - # PROD: Request Response - global resp_PROD - resp_PROD = requests.get(URL) - if resp_PROD.status_code == 200: - pass - else: - raise TypeError("Oops, wrong city name or code?") - - global data - data = resp_PROD.json() - - data_processing() - exitQuery() - -OWMCITY() +from __future__ import annotations + +from platform import system +from tkinter import Menu, Tk, messagebox +from tkinter.ttk import Button, Entry, Frame, Label + +from requests import Response +from requests import get as requests_get + + +class App(Tk): + def __init__(self): + super().__init__() + self.withdraw() + + # Set up Menubar + if system() == "Darwin": + self.menubar = Menu(self) + # Apple menus have special names and special commands + self.app_menu = Menu(self.menubar, tearoff=0, name="apple") + self.menubar.add_cascade(label="App", menu=self.app_menu) + else: + self.menubar = Menu(self) + self.app_menu = Menu(self.menubar, tearoff=0) + self.menubar.add_cascade(label="App", menu=self.app_menu) + self.menubar.add_command(label="About Weather", command=self.about) + self.config(menu=self.menubar) + + # Set up window + self.title("Weather") + self.resizable(False, False) + self.configure(bg="white") + + # Set up widgets + self.main_frame = Frame(self, padding=10) + self.main_frame.pack() + + heading = Label(self.main_frame, text="Weather", font="Helvetica 13") + heading.grid(row=0, column=0, columnspan=2, padx=10, pady=10) + + self.searchbar = Entry(self.main_frame, width=42) + self.searchbar.grid(row=1, column=0, columnspan=2, padx=10, pady=10) + + self.label = Label(self.main_frame, text="", font=("Helvetica 13")) + self.label.grid(row=2, column=0, columnspan=2) + + Button(self.main_frame, text="Search for City", command=self.OWMCITY).grid( + row=3, column=0, padx=10, pady=10 + ) + Button(self.main_frame, text="Exit", command=self.exit_app).grid( + row=3, column=1, padx=10, pady=10 + ) + + self.resize_app() + self.deiconify() + + def about(self) -> App: + """Display a messagebox with information about the app.""" + messagebox.showinfo( + "About Weather", + "Weather is a simple weather app that uses the OpenWeatherMap API to get the weather for a given city.", + parent=self, + ) + return self + + def resize_app(self) -> App: + """Use tkinter to detect the minimum size of the app, get the center of the screen, and place the app there.""" + # Update widgets so minimum size is accurate + self.update_idletasks() + + # Get minimum size + minimum_width: int = self.winfo_reqwidth() + minimum_height: int = self.winfo_reqheight() + + # Get center of screen based on minimum size + x_coords = int(self.winfo_screenwidth() / 2 - minimum_width / 2) + y_coords = int(self.winfo_screenheight() / 2 - minimum_height / 2) - 20 + # `-20` should deal with Dock on macOS and looks good on other OS's + + # Place app and make the minimum size the actual minimum size (non-infringable) + self.geometry(f"{minimum_width}x{minimum_height}+{x_coords}+{y_coords}") + self.wm_minsize(minimum_width, minimum_height) + return self + + def exit_app(self) -> None: + """Exit the app.""" + self.destroy() + + def OWMCITY(self) -> App: + """Get the weather for a given city using the OpenWeatherMap API and display it in a label.""" + # Get API key + api_key: str = "c439e1209216cc7e7c73a3a8d1d12bfd" + + # Get city name + city: str = self.searchbar.get() + + # Send request to OpenWeatherMap API + response: Response = requests_get( + f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}" + ) + if response.status_code != 200: + self.label.configure(text="City not found") + return + + # Get temperature in Celsius + temperature_kelvin: float = response.json()["main"]["temp"] + temperature_celsius = temperature_kelvin - 273.15 + + # Put in label + self.label.configure(text=f"{temperature_celsius:.2f}°C") + return self + + +if __name__ == "__main__": + app = App() + app.mainloop() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bf2d022 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.black] +line-length = 88 + +[tool.isort] +line_length = 88 +profile = "black" +multi_line_output = 3 + +[tool.ruff] +line-length = 125 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 69ca46f..9034f54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ -pyowm requests -python-dotenv isort -black \ No newline at end of file +black +ruff \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py index 730e742..bf2fa0e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,36 +1,20 @@ -import os -import unittest -from time import sleep +from unittest import TestCase, main -import requests -from dotenv import load_dotenv +from requests import get TOKEN = "c439e1209216cc7e7c73a3a8d1d12bfd" CITY = "Norway" # test *only* BASE_URL = "https://api.openweathermap.org/data/2.5/weather?" URL = BASE_URL + "q=" + CITY + "&appid=" + TOKEN -resp_PROD = requests.get(URL) +resp_PROD = get(URL) -class testAPILoad(unittest.TestCase): - load_dotenv() - - def test_request(self): - TOKEN = os.getenv("TOKEN") - if resp_PROD.status_code == 200: - print(resp_PROD.status_code) - pass - else: - raise TypeError("Oops, wrong city name or code?") - sleep(5) - exit() - +class testAPILoad(TestCase): def test_data_processcode(self): - TOKEN = os.getenv("TOKEN") data = resp_PROD.json() main = data["main"] - humidity = main["humidity"] - pressure = main["pressure"] + main["humidity"] + main["pressure"] temp = main["temp"] temp_min = main["temp_min"] temp_max = main["temp_max"] @@ -48,4 +32,4 @@ def ktc(temp, temp_min, temp_max): if __name__ == "__main__": - unittest.main() + main() diff --git a/weather.spec b/weather.spec index 9f74a43..985e7e0 100644 --- a/weather.spec +++ b/weather.spec @@ -21,7 +21,6 @@ a = Analysis( pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - exe = EXE( pyz, a.scripts,