diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f978e55..f02a18c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ Please set up your development environment by referring to the `Setup` section i - The [PEP 8](https://realpython.com/python-pep8/) styling convention is used. - This is achieved using the `ruff` Linter and Formatter. - The Linter and Formatter are automatically executed before committing via pre-commit. - - If you want to run the Linter and Formatter at any time, execute `pre-commit run --all-files`. + - If you want to run the Linter and Formatter at any time, execute `pre-commit run --all-files`. Or, `make format` and `make run` can be ran. ### Testing > [!NOTE] diff --git a/README.md b/README.md index 70a13db..c8c95ac 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,9 @@ Note that the Flask server must be running in order to send emails. cli-surf_website gif

-Although this application was made with the cli in mind, there is a frontend. +Although this application was made with the cli in mind, there are two frontends (experimenting at the moment). + +**HTML/JS/CSS Frontend** `http://localhost:8000/home` **or** `:/home` if the application is running on a different host or you have changed the default port. @@ -170,6 +172,14 @@ You may need to change `IP_ADDRESS` in `.env` to match the ip of the host runnin Now, running `python3 server.py` will launch the website! +**Streamlit Frontend** + +[Streamlit](https://streamlit.io/) is also an option that we are experimenting with. + +To run streamlit: `streamlit run src/dev_streamlit.py` + +You will be able to find the frontend here: `http://localhost:8502` + ## 📈 Contributing diff --git a/docs/cheat_sheet.md b/docs/cheat_sheet.md index 30f6897..4bda305 100644 --- a/docs/cheat_sheet.md +++ b/docs/cheat_sheet.md @@ -18,7 +18,7 @@ When developing, these commands may come in handy: | `poetry add ` | Add a new dependency to Poetry | | `poetry add --group dev ` | Add a new developer dependency to Poetry | | `poetry show` | List all available dependencies with descriptions | -| `poetry run pre-commit run --all-files` | Run the Linter & Formatter | +| `pre-commit run --all-files` | Run the Linter & Formatter | ## [Mkdocs Commands](https://www.mkdocs.org/user-guide/) @@ -30,12 +30,16 @@ When developing, these commands may come in handy: | Argument | Description| | -------- | ------- | +| `make install` | Install dependencies and activates the virtual environment. | | `make run` | Runs `server.py` | | `make run_docker` | Runs `docker compose up -d` | | `make test` | Runs pytest | | `make test_docker` | Runs pytest on Docker | | `make output_coverage` | Outputs the coverage of the tests | | `make send_email` | Runs `send_email.py` | +| `make lint` | Runs the ruff linter | +| `make format` | Runs the ruff formatter | +| `make clean` | Cleans up files generated during testing (`.coverage`, `pytest_cache`, etc.) | ## [Git](https://education.github.com/git-cheat-sheet-education.pdf) @@ -51,4 +55,5 @@ When developing, these commands may come in handy: | `git checkout -b ` | Creates a new branch, `branch`, and switches into it | | `git branch -d ` | Delete a local branch | | `git push -u origin ` | Pushes a local branch to the upstream remote repo | -| `git log --branches --not --remotes` | View commits that have not yet been pushed | \ No newline at end of file +| `git log --branches --not --remotes` | View commits that have not yet been pushed | +| `git fetch origin pull/ID/head:BRANCH_NAME` | Checking out a PR branch with `ID` and `BRANCH_NAME` | \ No newline at end of file diff --git a/docs/structure.md b/docs/structure.md index 031e1e3..0147e46 100644 --- a/docs/structure.md +++ b/docs/structure.md @@ -28,6 +28,9 @@ More in-depth structure: . ├── compose.yaml ├── CONTRIBUTING.md +├── dist +│   ├── cli_surf-0.1.0-py3-none-any.whl +│   └── cli_surf-0.1.0.tar.gz ├── Dockerfile ├── docs │   ├── cheat_sheet.md @@ -55,6 +58,7 @@ More in-depth structure: │   ├── api.py │   ├── art.py │   ├── cli.py +│   ├── dev_streamlit.py │   ├── gpt.py │   ├── helper.py │   ├── __init__.py @@ -63,6 +67,7 @@ More in-depth structure: │   ├── server.py │   ├── settings.py │   ├── static +│   ├── streamlit_helper.py │   └── templates └── tests ├── __init__.py @@ -73,6 +78,6 @@ More in-depth structure: ├── test_helper.py └── test_server.py -9 directories, 38 files +10 directories, 42 files ``` diff --git a/docs/styling.md b/docs/styling.md index 39f5a9a..5e55032 100644 --- a/docs/styling.md +++ b/docs/styling.md @@ -7,4 +7,6 @@ This is achieved using the ruff Linter and Formatter. The Linter and Formatter are automatically executed before committing via pre-commit. -If you want to run the Linter and Formatter at any time, execute `poetry run pre-commit run --all-files`. +If you want to run the Linter and Formatter at any time, execute `pre-commit run --all-files`. + +You may also run `make lint` or `make format` to run the linter/formatter on its own. diff --git a/src/api.py b/src/api.py index 641ef06..12b730f 100644 --- a/src/api.py +++ b/src/api.py @@ -16,8 +16,9 @@ def get_coordinates(args): """ - Takes a location(city or address) and returns the coordinates: [lat, long] - If no location is specified, default_location() finds the users coordinates + Takes a location (city or address) and returns the coordinates: [lat, long] + If no location is specified or the location is invalid, default_location() + finds the user's coordinates. """ for arg in args: arg_str = str(arg) @@ -27,13 +28,18 @@ def get_coordinates(args): location = geolocator.geocode(address) if location is not None: return [location.latitude, location.longitude, location] - return "No data" + else: + print( + f"Invalid location '{address}' provided. " + "Using default location." + ) + return default_location() return default_location() def default_location(): """ - If no location specified in cli, find users location + If no location specified in cli, find user's location Make a GET request to the API endpoint """ response = requests.get("https://ipinfo.io/json", timeout=10) diff --git a/src/cli.py b/src/cli.py index 191fb91..3f2a12b 100644 --- a/src/cli.py +++ b/src/cli.py @@ -15,12 +15,15 @@ gpt_info = [api_key, model] -def run(lat=0, long=0): +def run(lat=0, long=0, args=None): """ Main function """ # Seperates the cli args into a list - args = helper.seperate_args(sys.argv) + if args is None: + args = helper.seperate_args(sys.argv) + else: + args = helper.seperate_args(args) # return coordinates, lat, long, city location = api.seperate_args_and_get_location(args) @@ -41,10 +44,12 @@ def run(lat=0, long=0): # Non-JSON output if arguments["json_output"] == 0: - helper.print_outputs( + # Response prints all the outputs & returns the GPT response + response = helper.print_outputs( city, ocean_data_dict, arguments, gpt_prompt, gpt_info ) - return ocean_data_dict + # Returns ocean data, GPT response + return ocean_data_dict, response else: # print the output in json format! json_output = helper.json_output(ocean_data_dict) diff --git a/src/dev_streamlit.py b/src/dev_streamlit.py new file mode 100644 index 0000000..05ebb22 --- /dev/null +++ b/src/dev_streamlit.py @@ -0,0 +1,86 @@ +import sys +import time +from pathlib import Path + +import streamlit as st +from streamlit_folium import st_folium + +sys.path.append(str(Path(__file__).parent.parent)) + +from src import streamlit_helper as sl_help + +# NOTE: This file is for testing purposes. Do not use it in production. + +# Page Configuration ### +title = "cli-surf" +st.set_page_config( + page_title=title, + page_icon="🌊", + layout="wide", +) +st.title(title) + +# Page Content ### + +# sidebar +st.sidebar.markdown( + """ +# MENU +- [weather](#weather) +- [csv upload](#csv-upload) +- [graph](#graph) +- [data sheet](#data-sheet) +""", + unsafe_allow_html=True, +) + +latest_iteration = st.empty() +bar = st.progress(0) + +for i in range(100): + latest_iteration.text(f"Iteration {i + 1}") + bar.progress(i + 1) + time.sleep(0.01) + +st.caption("Enter a surf spot to see the map and forecast!") + + +# Toggles in a horizontal line +col1, col2 = st.columns(2) + +with col1: + gpt = st.toggle("Activate GPT") +with col2: + map = st.toggle("Show Map", value=True) + +extra_args = sl_help.extra_args(gpt) + +# User input location +location = st.text_input("Surf Spot", placeholder="Enter surf spot!") + +graph_type = st.radio( + "Choose graph type", + ["Height/Period :ocean:", "Direction :world_map:"], + index=None, +) + +# Checks if location has been entered. +# If True, gathers surf report and displays map +if location: + get_report = sl_help.get_report(location, extra_args) + report_dict, gpt_response, lat, long = get_report + + # Displays the map + # TODO: Configure map to only show nearby surf spots based on the lat/long + if map: + map_data = sl_help.map_data(lat, long) + st_folium(map_data, width=725) + + # Writes the GPT response + if gpt_response is not None: + st.write(gpt_response) + + # Displays the line graph + st.write("# Surf Conditions") + df = sl_help.graph_data(report_dict, graph_type) + st.line_chart(df.rename(columns={"date": "index"}).set_index("index")) diff --git a/src/helper.py b/src/helper.py index 66387e0..fa65303 100644 --- a/src/helper.py +++ b/src/helper.py @@ -232,9 +232,11 @@ def print_outputs(city, data_dict, arguments, gpt_prompt, gpt_info): # Prints the forecast(if activated in CLI args) print_forecast(arguments, forecast) # Checks if GPT in args, prints GPT response if True + gpt_response = None if arguments["gpt"] == 1: gpt_response = print_gpt(data_dict, gpt_prompt, gpt_info) print(gpt_response) + return gpt_response def set_location(location): diff --git a/src/streamlit_helper.py b/src/streamlit_helper.py new file mode 100644 index 0000000..9703981 --- /dev/null +++ b/src/streamlit_helper.py @@ -0,0 +1,90 @@ +""" +Helper functions for the streamlit frontend +""" + +import sys +from pathlib import Path + +import folium +import pandas as pd + +sys.path.append(str(Path(__file__).parent.parent)) + +from src import cli + + +def extra_args(gpt): + """ + By default, the location is the only argument when cli.run() + is ran. Extra args outputs and other arguments the user wants, + like using the GPT function + """ + # Arguments + extra_args = "" + + if gpt: + extra_args += ",gpt" + + return extra_args + + +def get_report(location, extra_args): + """ + Executes cli.run(), retrns the report dictionary, + gpt response, lat and long + """ + gpt_response = None + args = "location=" + location + if extra_args: + args += extra_args + surf_report = cli.run(args=["placeholder", args]) + report_dict, gpt_response = surf_report[0], surf_report[1] + lat, long = report_dict["Lat"], report_dict["Long"] + + return report_dict, gpt_response, lat, long + + +def map_data(lat, long): + """ + Gathers data for the map + Docs: https://folium.streamlit.app/ + """ + m = folium.Map(location=[lat, long], zoom_start=16) + folium.Marker( + [lat, long], popup="Surf Spot!", tooltip="Get out there!" + ).add_to(m) + + return m + + +def graph_data(report_dict, graph_type="Height/Period :ocean:"): + """ + Gathers the forecasted dates, heights, period and stores them in a pandas + dataframe. Will be used to display the line chart + """ + forecasted_dates = [ + forecast["date"] for forecast in report_dict["Forecast"] + ] + forecasted_heights = [ + forecast["height"] for forecast in report_dict["Forecast"] + ] + forecasted_periods = [ + forecast["period"] for forecast in report_dict["Forecast"] + ] + forecasted_directions = [ + forecast["direction"] for forecast in report_dict["Forecast"] + ] + # table + if graph_type == "Height/Period :ocean:" or graph_type is None: + df = pd.DataFrame({ + "date": forecasted_dates, + "heights": forecasted_heights, + "periods": forecasted_periods, + }) + else: + df = pd.DataFrame({ + "date": forecasted_dates, + "directions": forecasted_directions, + }) + + return df diff --git a/tests/test_cli.py b/tests/test_cli.py index 867d289..edea3a5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -18,6 +18,6 @@ def test_cli_output(): # 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) - data_dict = cli.run(36.95, -121.97) + data_dict = cli.run(36.95, -121.97)[0] time.sleep(5) assert len(data_dict) >= expected