diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 9e95801..b90095a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -17,7 +17,13 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install pytest + pip install -r dev-requirements.txt - name: Test with pytest run: | - pytest + set -o pipefail + python -m pytest --junitxml=pytest.xml --cov-report=term-missing --cov=src tests/ | tee pytest-coverage.txt + - name: Pytest coverage comment + uses: MishaKav/pytest-coverage-comment@v1.1.47 + with: + pytest-coverage-path: ./pytest-coverage.txt + junitxml-path: ./pytest.xml diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..d98c126 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,3 @@ +ruff==0.4.6 +pytest==8.2.1 +pytest-cov==5.0.0 diff --git a/makefile b/makefile new file mode 100644 index 0000000..c49405c --- /dev/null +++ b/makefile @@ -0,0 +1,13 @@ +.PHONY: lint format + +lint: + ruff check . && ruff check . --diff + +format: + ruff check . --fix && ruff format . + +test: + pytest -s -x --cov=src -vv + +post_test: + coverage html diff --git a/pyproject.toml b/pyproject.toml index d253baf..d00f2ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,3 +2,23 @@ [tool.pytest.ini_options] pythonpath = "." addopts = '-p no:warnings' # disable pytest warnings + +# ruff global settings +[tool.ruff] +line-length = 79 + +# linter +[tool.ruff.lint] +# I: isort +# F: Pyflakes +# E: Pycodestyle Error +# W: Pycodestyle Warning +# P: Pylint +# PT: flake8-pytest-style +preview = true +select = ['I', 'F', 'E', 'W', 'PL', 'PT'] + +# formatter +[tool.ruff.format] +preview = true +quote-style = 'double' diff --git a/src/api.py b/src/api.py index 9bcb808..da61dd6 100644 --- a/src/api.py +++ b/src/api.py @@ -2,12 +2,15 @@ Functions that make API calls stored here """ -from geopy.geocoders import Nominatim +from http import HTTPStatus + import openmeteo_requests +import pandas as pd +import requests import requests_cache +from geopy.geocoders import Nominatim from retry_requests import retry -import requests -import pandas as pd + from src import helper @@ -17,8 +20,8 @@ def get_coordinates(args): If no location is specified, default_location() finds the users coordinates """ for arg in args: - arg = str(arg) - if arg.startswith("location=") or arg.startswith("loc="): + arg_str = str(arg) + if arg_str.startswith("location=") or arg_str.startswith("loc="): address = arg.split("=")[1] geolocator = Nominatim(user_agent="cli-surf") location = geolocator.geocode(address) @@ -35,7 +38,7 @@ def default_location(): """ response = requests.get("https://ipinfo.io/json", timeout=10) - if response.status_code == 200: + if response.status_code == HTTPStatus.OK: data = response.json() location = data["loc"].split(",") lat = location[0] @@ -98,7 +101,8 @@ def ocean_information(lat, long, decimal, unit="imperial"): except ValueError: return "No data" - # Process first location. Add a for-loop for multiple locations or weather models + # Process first location. + # Add a for-loop for multiple locations or weather models response = responses[0] # Current values. The order of variables needs to be the same as requested. @@ -125,7 +129,11 @@ def forecast(lat, long, decimal, days=0): params = { "latitude": lat, "longitude": long, - "daily": ["wave_height_max", "wave_direction_dominant", "wave_period_max"], + "daily": [ + "wave_height_max", + "wave_direction_dominant", + "wave_period_max", + ], "length_unit": "imperial", "timezone": "auto", "forecast_days": days, diff --git a/src/art.py b/src/art.py index aca6a54..aceccad 100644 --- a/src/art.py +++ b/src/art.py @@ -24,13 +24,12 @@ def print_wave(show_wave, show_large_wave, color): print("Not a valid color") color = "blue" if int(show_wave) == 1: - print( colors[color] + """ .-``'. .` .` -_.-' '._ +_.-' '._ """ + colors["end"] ) diff --git a/src/cli.py b/src/cli.py index 41caf6c..121456e 100644 --- a/src/cli.py +++ b/src/cli.py @@ -2,14 +2,12 @@ Main module """ -import sys import os +import sys sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from src import helper -from src import api -from src import art +from src import api, helper def run(lat=0, long=0): @@ -39,7 +37,7 @@ def run(lat=0, long=0): # Makes calls to the apis(ocean, UV) and returns the values data = api.gather_data(lat, long, arguments) ocean_data = data[0] - uv_index = data[1] + # uv_index = data[1] data_dict = data[2] # Non-JSON output diff --git a/src/helper.py b/src/helper.py index 240f757..9731f10 100644 --- a/src/helper.py +++ b/src/helper.py @@ -3,9 +3,8 @@ """ import json -from src import api -from src import art -import pandas as pd + +from src import api, art def arguments_dictionary(lat, long, city, args): @@ -47,11 +46,12 @@ def get_forecast_days(args): """ Checks to see if forecast in cli args. Defaults to 0 """ + MAX_VALUE = 7 for arg in args: - arg = str(arg) - if arg.startswith("forecast=") or arg.startswith("fc="): - forecast = int(arg.split("=")[1]) - if forecast < 0 or forecast > 7: + arg_str = str(arg) + if arg_str.startswith("forecast=") or arg_str.startswith("fc="): + forecast = int(arg_str.split("=")[1]) + if forecast < 0 or forecast > MAX_VALUE: print("Must choose a non-negative number >= 7 in forecast!") break return forecast @@ -67,7 +67,6 @@ def print_location(city, show_city): print("\n") -# def print_output(uv_index, ocean_data, show_uv, show_height, show_direction, show_period): def print_output(ocean_data_dict): """ Prints output @@ -119,9 +118,9 @@ def get_color(args): Gets the color in the cli args """ for arg in args: - arg = str(arg) - if arg.startswith("color=") or arg.startswith("c="): - color_name = arg.split("=")[1] + arg_str = str(arg) + if arg_str.startswith("color=") or arg_str.startswith("c="): + color_name = arg_str.split("=")[1] return color_name return "blue" @@ -138,7 +137,8 @@ def round_decimal(round_list, decimal): def set_output_values(args, ocean): """ - Takes a list of command line arguments(args) and sets the appropritate values + Takes a list of command line arguments(args) + and sets the appropritate values in the ocean dictionary(show_wave = 1, etc) """ if "hide_wave" in args or "hw" in args: @@ -175,7 +175,8 @@ def json_output(data_dict): def print_outputs(lat, long, coordinates, ocean_data, arguments): """ - Basically the main printing function, calls all the other printing functions + Basically the main printing function, + calls all the other printing functions """ print("\n") if coordinates == "No data": @@ -186,11 +187,15 @@ def print_outputs(lat, long, coordinates, ocean_data, arguments): else: print_location(arguments["city"], arguments["show_city"]) art.print_wave( - arguments["show_wave"], arguments["show_large_wave"], arguments["color"] + arguments["show_wave"], + arguments["show_large_wave"], + arguments["color"], ) print_output(arguments) print("\n") - forecast = api.forecast(lat, long, arguments["decimal"], arguments["forecast_days"]) + forecast = api.forecast( + lat, long, arguments["decimal"], arguments["forecast_days"] + ) print_forecast(arguments, forecast) diff --git a/src/send_email.py b/src/send_email.py index e6b83c9..56bc221 100644 --- a/src/send_email.py +++ b/src/send_email.py @@ -3,10 +3,11 @@ """ import os -import subprocess import smtplib -from email.mime.text import MIMEText +import subprocess from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + from dotenv import load_dotenv # Load environment variables from .env file @@ -35,7 +36,10 @@ def send_user_email(): Sends user an email """ SURF = subprocess.run( - ["curl", os.getenv("COMMAND")], capture_output=True, text=True, check=True + ["curl", os.getenv("COMMAND")], + capture_output=True, + text=True, + check=True, ) if SURF.returncode == 0: # Check if command executed successfully BODY = SURF.stdout diff --git a/src/server.py b/src/server.py index dd19335..ce4d91d 100644 --- a/src/server.py +++ b/src/server.py @@ -2,14 +2,21 @@ Flask Server! """ -from flask import Flask, send_file, send_from_directory, request, render_template -from flask_cors import CORS -from dotenv import load_dotenv, dotenv_values +import asyncio import os import subprocess -import asyncio import urllib.parse +from dotenv import dotenv_values, load_dotenv +from flask import ( + Flask, + render_template, + request, + send_file, + send_from_directory, +) +from flask_cors import CORS + # Load environment variables from .env file load_dotenv(override=True) diff --git a/tests/test_code.py b/tests/test_code.py index 80e1f59..7fd8686 100644 --- a/tests/test_code.py +++ b/tests/test_code.py @@ -4,13 +4,13 @@ Run pytest: pytest """ -import sys -from unittest.mock import patch import io import time +from unittest.mock import patch + from src import cli -from src.helper import extract_decimal from src.api import get_coordinates, get_uv, ocean_information +from src.helper import extract_decimal def test_invalid_input(): @@ -20,7 +20,8 @@ def test_invalid_input(): with patch("sys.stdout", new=io.StringIO()) as fake_stdout: extract_decimal(["decimal=NotADecimal"]) printed_output = fake_stdout.getvalue().strip() - assert printed_output == "Invalid value for decimal. Please provide an integer." + expected = "Invalid value for decimal. Please provide an integer." + assert printed_output == expected def test_default_input(): @@ -53,9 +54,11 @@ def test_main_output(): Main() returns a dictionary of: location, height, period, etc. This functions checks if the dictionary is returned and is populated """ - # Hardcode lat and long for location. If not, when test are ran in Github Actions + # Hardcode lat and long for location. + # If not, when test are ran in Github Actions # We get an error(because server probably isn't near ocean) + expected = 5 data_dict = cli.run(36.95, -121.97) print(data_dict) time.sleep(5) - assert len(data_dict) >= 5 + assert len(data_dict) >= expected