Skip to content

Commit

Permalink
Split routes into hourly and daily forecasts
Browse files Browse the repository at this point in the history
  • Loading branch information
kumaranvpl committed Sep 9, 2024
1 parent 1d4971b commit bea3251
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 54 deletions.
91 changes: 54 additions & 37 deletions tests/app/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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 = {
Expand All @@ -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",
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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": {
Expand Down
38 changes: 21 additions & 17 deletions weatherapi/app.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -49,22 +49,26 @@ 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)

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,
Expand All @@ -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)

0 comments on commit bea3251

Please sign in to comment.