diff --git a/tests/app/test_app.py b/tests/app/test_app.py index dd551f7..d683f6a 100644 --- a/tests/app/test_app.py +++ b/tests/app/test_app.py @@ -14,8 +14,9 @@ class TestRoutes: @pytest.mark.asyncio - async def test_get_weather(self) -> None: - weather = await get_weather(CITY) + @pytest.mark.parametrize("include_hourly", [True, False]) + async def test_get_weather_hourly(self, include_hourly: bool) -> None: + weather = await get_weather(CITY, include_hourly=include_hourly) assert weather.city == CITY assert weather.temperature > 0 @@ -25,16 +26,19 @@ async def test_get_weather(self) -> None: first_daily_forecast = daily_forecast[0] assert first_daily_forecast.forecast_date == datetime.date.today() assert first_daily_forecast.temperature > 0 - assert len(first_daily_forecast.hourly_forecasts) > 0 + if not include_hourly: + assert first_daily_forecast.hourly_forecasts is None + else: + assert len(first_daily_forecast.hourly_forecasts) > 0 # type: ignore [arg-type] - first_hourly_forecast = first_daily_forecast.hourly_forecasts[0] - assert isinstance(first_hourly_forecast, HourlyForecast) - assert first_hourly_forecast.forecast_time is not None - assert first_hourly_forecast.temperature > 0 - assert first_hourly_forecast.description is not None + first_hourly_forecast = first_daily_forecast.hourly_forecasts[0] # type: ignore [index] + assert isinstance(first_hourly_forecast, HourlyForecast) + assert first_hourly_forecast.forecast_time is not None + assert first_hourly_forecast.temperature > 0 + assert first_hourly_forecast.description is not None - def test_weather_route(self) -> None: - response = client.get(f"/?city={CITY}") + def test_daily_weather_route(self) -> None: + response = client.get(f"/daily?city={CITY}") assert response.status_code == 200 resp_json = response.json() assert resp_json.get("city") == CITY @@ -50,25 +54,37 @@ def test_weather_route(self) -> None: == datetime.date.today().isoformat() ) assert first_daily_forecast.get("temperature") > 0 - assert len(first_daily_forecast.get("hourly_forecasts")) > 0 - - first_hourly_forecast = first_daily_forecast.get("hourly_forecasts")[0] - assert isinstance(first_hourly_forecast, dict) - assert first_hourly_forecast.get("forecast_time") is not None - assert first_hourly_forecast.get("temperature") > 0 # type: ignore - assert first_hourly_forecast.get("description") is not None + assert first_daily_forecast.get("hourly_forecasts") is None - def test_secure_weather_route(self) -> None: - response = client.get(f"/secure?city={CITY}", headers={"x-key": "wrong_key"}) + def test_hourly_weather_route_with_invalid_key(self) -> None: + response = client.get(f"/hourly?city={CITY}", headers={"x-key": "wrong_key"}) assert response.status_code == 403 resp_json = response.json() assert resp_json.get("detail") == f"Invalid API Key; Try '{API_KEY}'" - response = client.get(f"/secure?city={CITY}", headers={"x-key": API_KEY}) + def test_hourly_weather_route_with_valid_key(self) -> None: + response = client.get(f"/hourly?city={CITY}", headers={"x-key": API_KEY}) assert response.status_code == 200 resp_json = response.json() assert resp_json.get("city") == CITY assert resp_json.get("temperature") > 0 + assert len(resp_json.get("daily_forecasts")) > 0 + daily_forecasts = resp_json.get("daily_forecasts") + assert isinstance(daily_forecasts, list) + + first_daily_forecast = daily_forecasts[0] + assert ( + first_daily_forecast.get("forecast_date") + == datetime.date.today().isoformat() + ) + assert first_daily_forecast.get("temperature") > 0 + assert len(first_daily_forecast.get("hourly_forecasts")) > 0 + + first_hourly_forecast = first_daily_forecast.get("hourly_forecasts")[0] + assert isinstance(first_hourly_forecast, dict) + assert first_hourly_forecast.get("forecast_time") is not None + assert first_hourly_forecast.get("temperature") > 0 # type: ignore + assert first_hourly_forecast.get("description") is not None def test_openapi(self) -> None: expected = { @@ -78,11 +94,11 @@ def test_openapi(self) -> None: {"url": "http://localhost:8000", "description": "Weather app server"} ], "paths": { - "/": { + "/daily": { "get": { - "summary": "Get Weather Route", - "description": "Get weather forecast for a given city", - "operationId": "get_weather_route__get", + "summary": "Get Daily Weather", + "description": "Get daily weather forecast for a given city", + "operationId": "get_daily_weather_daily_get", "parameters": [ { "name": "city", @@ -120,11 +136,11 @@ def test_openapi(self) -> None: }, } }, - "/secure": { + "/hourly": { "get": { - "summary": "Secure Get Weather Route", - "description": "Get weather forecast for a given city with security", - "operationId": "secure_get_weather_route_secure_get", + "summary": "Get Hourly Weather", + "description": "Get hourly weather forecast for a given city", + "operationId": "get_hourly_weather_hourly_get", "security": [{"APIKeyHeader": []}], "parameters": [ { @@ -175,19 +191,20 @@ def test_openapi(self) -> None: }, "temperature": {"type": "integer", "title": "Temperature"}, "hourly_forecasts": { - "items": { - "$ref": "#/components/schemas/HourlyForecast" - }, - "type": "array", + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/HourlyForecast" + }, + "type": "array", + }, + {"type": "null"}, + ], "title": "Hourly Forecasts", }, }, "type": "object", - "required": [ - "forecast_date", - "temperature", - "hourly_forecasts", - ], + "required": ["forecast_date", "temperature"], "title": "DailyForecast", }, "HTTPValidationError": { diff --git a/weatherapi/app.py b/weatherapi/app.py index 23e3011..c099794 100644 --- a/weatherapi/app.py +++ b/weatherapi/app.py @@ -1,7 +1,7 @@ import datetime import logging from os import environ -from typing import Annotated, List +from typing import Annotated, List, Optional import python_weather from fastapi import Depends, FastAPI, HTTPException, Query @@ -40,7 +40,7 @@ class HourlyForecast(BaseModel): class DailyForecast(BaseModel): forecast_date: datetime.date temperature: int - hourly_forecasts: List[HourlyForecast] + hourly_forecasts: Optional[List[HourlyForecast]] = None class Weather(BaseModel): @@ -49,7 +49,7 @@ class Weather(BaseModel): daily_forecasts: List[DailyForecast] -async def get_weather(city: str) -> Weather: +async def get_weather(city: str, include_hourly: bool = False) -> Weather: async with python_weather.Client(unit=python_weather.METRIC) as client: # fetch a weather forecast from a city weather = await client.get(city) @@ -57,14 +57,18 @@ async def get_weather(city: str) -> Weather: daily_forecasts = [] # get the weather forecast for a few days for daily in weather.daily_forecasts: - hourly_forecasts = [ - HourlyForecast( - forecast_time=hourly.time, - temperature=hourly.temperature, - description=hourly.description, - ) - for hourly in daily.hourly_forecasts - ] + hourly_forecasts = ( + [ + HourlyForecast( + forecast_time=hourly.time, + temperature=hourly.temperature, + description=hourly.description, + ) + for hourly in daily.hourly_forecasts + ] + if include_hourly + else None + ) daily_forecasts.append( DailyForecast( forecast_date=daily.date, @@ -82,18 +86,18 @@ async def get_weather(city: str) -> Weather: return weather_response -@app.get("/", description="Get weather forecast for a given city") -async def get_weather_route( +@app.get("/daily", description="Get daily weather forecast for a given city") +async def get_daily_weather( city: Annotated[str, Query(description="city for which forecast is requested")], ) -> Weather: - return await get_weather(city) + return await get_weather(city, include_hourly=False) -@app.get("/secure", description="Get weather forecast for a given city with security") -async def secure_get_weather_route( +@app.get("/hourly", description="Get hourly weather forecast for a given city") +async def get_hourly_weather( city: Annotated[str, Query(description="city for which forecast is requested")], key: str = Depends(header_scheme), ) -> Weather: if key != API_KEY: raise HTTPException(status_code=403, detail=f"Invalid API Key; Try '{API_KEY}'") - return await get_weather(city) + return await get_weather(city, include_hourly=True)