From 4a5cf58edb9198f02fbcf4abdf5848c21bdb877c Mon Sep 17 00:00:00 2001 From: Tyler Haas Date: Tue, 5 Oct 2021 15:38:24 -0700 Subject: [PATCH 01/10] Add dummy forecast endpoint --- services/backend/api/api.py | 21 +++++++++++++++++++++ services/backend/api/models.py | 25 +++++++++++++++++++++++++ services/backend/tests/test_api.py | 13 +++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 services/backend/api/models.py diff --git a/services/backend/api/api.py b/services/backend/api/api.py index 4a859c8..ec5a7c0 100644 --- a/services/backend/api/api.py +++ b/services/backend/api/api.py @@ -1,4 +1,8 @@ from fastapi import FastAPI +from .models import Condition, Forecast +from typing import Dict, List, Optional +from datetime import datetime, timedelta +import random app = FastAPI() @@ -6,3 +10,20 @@ @app.get("/") async def get_root(): return {"data": "I am a WeatherApp"} + + +@app.get("/forecast/{location}", response_model=Dict[str, List[Forecast]]) +async def get_forecast_by_location( + location: str, start_date: Optional[datetime] = None, days: int = 5 +): + if not start_date: + start_date = datetime.now().date() + forecasts = [] + for delta in range(days): + forecast_date = start_date + timedelta(days=delta) + # Just get a random condition and temp + condition = random.choice(list(Condition)) + temp = round(random.uniform(23, 105), 1) + forecast = Forecast(date=forecast_date, condition=condition, temp=temp) + forecasts.append(forecast) + return {location: forecasts} diff --git a/services/backend/api/models.py b/services/backend/api/models.py new file mode 100644 index 0000000..ebe1ebb --- /dev/null +++ b/services/backend/api/models.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel +from datetime import date +from enum import Enum + + +class Condition(str, Enum): + sunny = "sunny" + cloudy = "cloudy" + partial_cloudy = "partial-cloudy" + partial_sunny = "partial-sunny" + windy = "windy" + lightning = "lightning" + snow = "snow" + rain = "rain" + light_right = "light-rain" + heavy_rain = "heavy-rain" + + +class Forecast(BaseModel): + date: date + condition: Condition + temp: float + + class Config: + use_enum_values = True diff --git a/services/backend/tests/test_api.py b/services/backend/tests/test_api.py index 20ecd29..b9ee8c0 100644 --- a/services/backend/tests/test_api.py +++ b/services/backend/tests/test_api.py @@ -7,3 +7,16 @@ def test_root(): response = test_app.get("/") assert response.status_code == 200 + + +def test_forecast(): + test_location = "San Francisco, CA" + days = 10 + response = test_app.get(f"/forecast/{test_location}?days={days}") + assert ( + response.status_code == 200 + ), f"Expected response 200, got {response.status_code}: {response.reason}." + data = response.json() + assert test_location in data + forecast = data[test_location] + assert len(forecast) == days From 4902577d272d322ded336f960c910bf0aa69259c Mon Sep 17 00:00:00 2001 From: Tyler Haas Date: Tue, 5 Oct 2021 15:41:02 -0700 Subject: [PATCH 02/10] Add CORS --- services/backend/api/api.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/services/backend/api/api.py b/services/backend/api/api.py index ec5a7c0..7fa1dbe 100644 --- a/services/backend/api/api.py +++ b/services/backend/api/api.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from .models import Condition, Forecast from typing import Dict, List, Optional from datetime import datetime, timedelta @@ -6,6 +7,16 @@ app = FastAPI() +origins = ["http://localhost", "http://localhost:3000"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + @app.get("/") async def get_root(): From 6e902b2ff39a49f64e22a3d4e8356da6d610b198 Mon Sep 17 00:00:00 2001 From: Tyler Haas Date: Tue, 5 Oct 2021 16:19:46 -0700 Subject: [PATCH 03/10] Simplify forecast response model --- services/backend/api/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/backend/api/api.py b/services/backend/api/api.py index 7fa1dbe..2b441cc 100644 --- a/services/backend/api/api.py +++ b/services/backend/api/api.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from .models import Condition, Forecast -from typing import Dict, List, Optional +from typing import List, Optional from datetime import datetime, timedelta import random @@ -23,7 +23,7 @@ async def get_root(): return {"data": "I am a WeatherApp"} -@app.get("/forecast/{location}", response_model=Dict[str, List[Forecast]]) +@app.get("/forecast/{location}", response_model=List[Forecast]) async def get_forecast_by_location( location: str, start_date: Optional[datetime] = None, days: int = 5 ): @@ -37,4 +37,4 @@ async def get_forecast_by_location( temp = round(random.uniform(23, 105), 1) forecast = Forecast(date=forecast_date, condition=condition, temp=temp) forecasts.append(forecast) - return {location: forecasts} + return forecasts From ffabe2f37e4627305453e2e230b499fc51fecc13 Mon Sep 17 00:00:00 2001 From: Tyler Haas Date: Tue, 5 Oct 2021 16:20:38 -0700 Subject: [PATCH 04/10] Add weather data fetch from backend --- .../src/components/WeatherForecast.js | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/services/frontend/src/components/WeatherForecast.js b/services/frontend/src/components/WeatherForecast.js index 3a61ef3..d18d238 100644 --- a/services/frontend/src/components/WeatherForecast.js +++ b/services/frontend/src/components/WeatherForecast.js @@ -1,34 +1,29 @@ import WeatherWidgetContainer from "./WeatherWidgetContainer"; import Header from "./Header"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import PropTypes from "prop-types"; export const WeatherForecast = ({ locationData, defaultExpanded }) => { - // Placeholder weather data - const weatherData = [ - { - temp: 51, - cond: "cloudy", - }, - { - temp: 48, - cond: "rain", - }, - { - temp: 58, - cond: "windy", - }, - { - temp: 62, - cond: "partial-cloudy", - }, - { - temp: 71, - cond: "sunny", - }, - ]; const [isExpanded, setIsExpanded] = useState(defaultExpanded); + const [weatherData, setWeatherData] = useState(null); + const [isLoaded, setIsLoaded] = useState(false); + + useEffect(() => { + fetch(`http://localhost:8000/forecast/foo`) + .then(res => res.json()) + .then( + (result) => { + // console.log(result); + setWeatherData(result); + setIsLoaded(true); + }, + (error) => { + console.log(error); + // TODO: Add isError state handling + } + ) + }, [locationData]); const toggleExpanded = () => { setIsExpanded(!isExpanded); @@ -42,7 +37,7 @@ export const WeatherForecast = ({ locationData, defaultExpanded }) => { onClick={toggleExpanded} >
- + {isLoaded ? :

"Loading..."

} ) : (
Date: Tue, 5 Oct 2021 16:20:54 -0700 Subject: [PATCH 05/10] Rename condition prop --- services/frontend/src/components/WeatherWidget.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/frontend/src/components/WeatherWidget.js b/services/frontend/src/components/WeatherWidget.js index 6f7faa7..8e76c5c 100644 --- a/services/frontend/src/components/WeatherWidget.js +++ b/services/frontend/src/components/WeatherWidget.js @@ -12,7 +12,7 @@ import { faSlash, } from "@fortawesome/free-solid-svg-icons"; -const WeatherWidget = ({ temp, cond }) => { +const WeatherWidget = ({ temp, condition }) => { const getIconFromString = (conditionString) => { let returnVal; switch (conditionString) { @@ -53,7 +53,7 @@ const WeatherWidget = ({ temp, cond }) => {

- +

@@ -67,7 +67,7 @@ const WeatherWidget = ({ temp, cond }) => { WeatherWidget.propTypes = { temp: PropTypes.number, - cond: PropTypes.string, + condition: PropTypes.string, }; export default WeatherWidget; From 2283863f5bb06fe3dd126ca491d3472bd020f03f Mon Sep 17 00:00:00 2001 From: Tyler Haas Date: Tue, 5 Oct 2021 16:21:21 -0700 Subject: [PATCH 06/10] Refac to accept weather data from backend --- .../src/components/WeatherWidgetContainer.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/services/frontend/src/components/WeatherWidgetContainer.js b/services/frontend/src/components/WeatherWidgetContainer.js index 8131db3..f8a19f1 100644 --- a/services/frontend/src/components/WeatherWidgetContainer.js +++ b/services/frontend/src/components/WeatherWidgetContainer.js @@ -1,18 +1,11 @@ import PropTypes from "prop-types"; import WeatherWidget from "./WeatherWidget"; -const WeatherWidgetContainer = ({ weatherObjArray, widgetCount }) => { +const WeatherWidgetContainer = ({ weatherObjArray }) => { const createWeatherWidgets = (objArray) => { const widgetArray = []; - for (let index = 0; index < widgetCount; index++) { - let obj = objArray[index]; - if (!obj) { - console.log(`Object ${index} was missing!`); - obj = { temp: null, cond: null }; - } - const widget = ( - - ); + for (const [index, obj] of objArray.entries()) { + const widget = ; widgetArray.push(widget); } return widgetArray; @@ -29,10 +22,9 @@ WeatherWidgetContainer.propTypes = { weatherObjArray: PropTypes.arrayOf( PropTypes.shape({ temp: PropTypes.number, - cond: PropTypes.string, + condition: PropTypes.string, }) ), - widgetCount: PropTypes.number, }; WeatherWidgetContainer.defaultProps = { From 019b9565b1e5b9f3ebacbd726db6543903d40a6f Mon Sep 17 00:00:00 2001 From: Tyler Haas Date: Tue, 5 Oct 2021 16:38:00 -0700 Subject: [PATCH 07/10] Replace dummy url param --- services/frontend/src/components/WeatherForecast.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/frontend/src/components/WeatherForecast.js b/services/frontend/src/components/WeatherForecast.js index d18d238..2a1810d 100644 --- a/services/frontend/src/components/WeatherForecast.js +++ b/services/frontend/src/components/WeatherForecast.js @@ -10,7 +10,7 @@ export const WeatherForecast = ({ locationData, defaultExpanded }) => { const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { - fetch(`http://localhost:8000/forecast/foo`) + fetch(`http://localhost:8000/forecast/${locationData.city}, ${locationData.state}`) .then(res => res.json()) .then( (result) => { From e42d7eb4afa92d3bb563f93e9c2dd59cbe4a3fb8 Mon Sep 17 00:00:00 2001 From: Tyler Haas Date: Tue, 5 Oct 2021 16:40:32 -0700 Subject: [PATCH 08/10] Remove outdated test assert --- services/backend/tests/test_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/services/backend/tests/test_api.py b/services/backend/tests/test_api.py index b9ee8c0..b8a4809 100644 --- a/services/backend/tests/test_api.py +++ b/services/backend/tests/test_api.py @@ -17,6 +17,5 @@ def test_forecast(): response.status_code == 200 ), f"Expected response 200, got {response.status_code}: {response.reason}." data = response.json() - assert test_location in data forecast = data[test_location] assert len(forecast) == days From 38773aa65e9eb7f337d8f9b07f1061f32e14fa28 Mon Sep 17 00:00:00 2001 From: Tyler Haas Date: Tue, 5 Oct 2021 16:41:54 -0700 Subject: [PATCH 09/10] Fix test error --- services/backend/tests/test_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/backend/tests/test_api.py b/services/backend/tests/test_api.py index b8a4809..dbcd1dc 100644 --- a/services/backend/tests/test_api.py +++ b/services/backend/tests/test_api.py @@ -17,5 +17,4 @@ def test_forecast(): response.status_code == 200 ), f"Expected response 200, got {response.status_code}: {response.reason}." data = response.json() - forecast = data[test_location] - assert len(forecast) == days + assert len(data) == days From da7bdd35ea9d1bda0a46e29744b0ec4fa69dd3b1 Mon Sep 17 00:00:00 2001 From: Tyler Haas Date: Tue, 5 Oct 2021 16:46:13 -0700 Subject: [PATCH 10/10] Formatting --- .../src/components/WeatherForecast.js | 33 +++++++++++-------- .../src/components/WeatherWidgetContainer.js | 4 ++- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/services/frontend/src/components/WeatherForecast.js b/services/frontend/src/components/WeatherForecast.js index 2a1810d..11a4a94 100644 --- a/services/frontend/src/components/WeatherForecast.js +++ b/services/frontend/src/components/WeatherForecast.js @@ -4,25 +4,26 @@ import { useState, useEffect } from "react"; import PropTypes from "prop-types"; export const WeatherForecast = ({ locationData, defaultExpanded }) => { - const [isExpanded, setIsExpanded] = useState(defaultExpanded); const [weatherData, setWeatherData] = useState(null); const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { - fetch(`http://localhost:8000/forecast/${locationData.city}, ${locationData.state}`) - .then(res => res.json()) - .then( - (result) => { - // console.log(result); - setWeatherData(result); - setIsLoaded(true); - }, - (error) => { - console.log(error); - // TODO: Add isError state handling - } + fetch( + `http://localhost:8000/forecast/${locationData.city}, ${locationData.state}` ) + .then((res) => res.json()) + .then( + (result) => { + // console.log(result); + setWeatherData(result); + setIsLoaded(true); + }, + (error) => { + console.log(error); + // TODO: Add isError state handling + } + ); }, [locationData]); const toggleExpanded = () => { @@ -37,7 +38,11 @@ export const WeatherForecast = ({ locationData, defaultExpanded }) => { onClick={toggleExpanded} >
- {isLoaded ? :

"Loading..."

} + {isLoaded ? ( + + ) : ( +

"Loading..."

+ )}
) : (
{ const createWeatherWidgets = (objArray) => { const widgetArray = []; for (const [index, obj] of objArray.entries()) { - const widget = ; + const widget = ( + + ); widgetArray.push(widget); } return widgetArray;