Skip to content

Commit 172b1a7

Browse files
authored
Merge pull request #10 from wook3024/develop
v0.1.0
2 parents 3837552 + 8bb2d47 commit 172b1a7

20 files changed

+672
-31
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ repos:
3535
rev: 5.10.1
3636
hooks:
3737
- id: isort
38-
name: isort (python)
38+
args: ["--profile", "black"]

README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,36 @@
1-
# FastAPI template
2-
3-
## Quickstart
4-
5-
### Build and start containers
6-
1+
# WeatherBot API
2+
---
3+
## Quickstart
4+
가상환경 또는 컨테이너를 사용해 실행합니다.
5+
### container (build and start containers)
76
```sh
87
docker-compose up -d --build
98
```
10-
11-
### API server health check
12-
9+
### venv (setup environments and run server)
10+
```sh
11+
1. pip install pip-tools
12+
2. pip-sync requirements/dev.txt
13+
3. sh run_server.sh
14+
```
15+
---
16+
## Health check
17+
서버 상태를 체크합니다.
1318
```sh
1419
curl -X GET http://localhost:8000/livez
1520
```
21+
---
22+
## Unittest
23+
가상환경 또는 컨테이너를 사용해 테스트합니다.
24+
### container
25+
```sh
26+
docker exec -it web bash -c "pytest -vv tests"
27+
```
28+
### venv
29+
```sh
30+
pytest -vv tests
31+
```
32+
---
33+
## Coinfig
34+
앱의 config를 관리합니다.
35+
- app.conf.log: 로그 설정 config를 관리합니다.
36+
- app.conf.config.service: 전체적인 서비스와 관련된 config를 관리합니다.

app/conf/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
defaults:
22
- log: default
3-
- service: dev
3+
- service: default

app/conf/service/default.yaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
app:
2+
title: FastAPI
3+
version: 0.1.0
4+
host: 127.0.0.1
5+
port: 8000
6+
reload: True
7+
timeout: 1.5
8+
9+
weather:
10+
base_url: https://thirdparty-weather-api-v2.droom.workers.dev
11+
historical_endpoint: /historical/hourly
12+
current_endpoint: /current
13+
api_key: CMRJW4WT7V3QA5AOIGPBC
14+
weather_map:
15+
- "sun"
16+
- "foggy"
17+
- "rain"
18+
- "snow"
19+
- "others"
20+
base_hot_temp: 15
21+
base_cold_temp: -2
22+
base_rainfall: 100
23+
base_warm_temp: 30
24+
historical_time_unit: -6
25+
26+
message:
27+
greeting:
28+
snow: "눈이 포슬포슬 내립니다."
29+
heavy_snow: "폭설이 내리고 있어요."
30+
rain: "비가 오고 있습니다."
31+
heavy_rain: "폭우가 내리고 있어요."
32+
foggy: "날씨가 약간은 칙칙해요."
33+
sunny: "따사로운 햇살을 맞으세요."
34+
cold: "날이 참 춥네요."
35+
clear: "날씨가 참 맑습니다."
36+
temperature:
37+
min_max: "최고기온은 {}도, 최저기온은 {}도 입니다."
38+
hotter: "어제보다 {}도 더 덥습니다."
39+
less_hot: "어제보다 {}도 덜 덥습니다."
40+
similarly_hot: "어제와 비슷하게 덥습니다."
41+
colder: "어제보다 {}도 더 춥습니다."
42+
less_cold: "어제보다 {}도 덜 춥습니다."
43+
similarly_cold: "어제와 비슷하게 춥습니다."
44+
headsup:
45+
heavy_snow: "내일 폭설이 내릴 수도 있으니 외출 시 주의하세요."
46+
snow: "눈이 내릴 예정이니 외출 시 주의하세요."
47+
heavy_rain: "폭우가 내릴 예정이에요. 우산을 미리 챙겨두세요."
48+
rain: "며칠동안 비 소식이 있어요."
49+
clear: "날씨는 대체로 평온할 예정이에요."

app/conf/service/dev.yaml

Lines changed: 0 additions & 6 deletions
This file was deleted.

app/main.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import asyncio
12
import time
2-
import uvicorn
33

4+
import uvicorn
45
from fastapi import FastAPI, Request
6+
from fastapi.encoders import jsonable_encoder
7+
from fastapi.exceptions import RequestValidationError
58
from fastapi.responses import ORJSONResponse
9+
from starlette.status import HTTP_400_BAD_REQUEST, HTTP_408_REQUEST_TIMEOUT
610

7-
from .routes import index
8-
from . import logger, cfg
11+
from . import cfg, logger
12+
from .routes import index, weather
913

1014

1115
def create_app() -> FastAPI:
@@ -15,21 +19,42 @@ def create_app() -> FastAPI:
1519
default_response_class=ORJSONResponse,
1620
)
1721

18-
@app.middleware("http")
19-
async def add_process_time_header(request: Request, call_next):
22+
app.include_router(router=index.router, tags=["Index"])
23+
app.include_router(router=weather.router, tags=["Weather"])
24+
25+
return app
26+
27+
28+
app = create_app()
29+
30+
31+
@app.middleware("http")
32+
async def timeout_middleware(request: Request, call_next):
33+
try:
2034
start_time = time.time()
21-
response = await call_next(request)
35+
return await asyncio.wait_for(
36+
call_next(request), timeout=cfg.service.app.timeout
37+
)
38+
except asyncio.TimeoutError:
39+
return ORJSONResponse(
40+
status_code=HTTP_408_REQUEST_TIMEOUT,
41+
content=jsonable_encoder(
42+
{"detail": "Request processing time excedeed limit"}
43+
),
44+
)
45+
finally:
2246
process_time = time.time() - start_time
2347
logger.info(
2448
"{0} process time: {1:.8f}s".format(call_next.__name__, process_time)
2549
)
26-
return response
27-
28-
app.include_router(router=index.router, tags=["Index"])
29-
return app
3050

3151

32-
app = create_app()
52+
@app.exception_handler(RequestValidationError)
53+
async def validation_exception_handler(request: Request, exc: RequestValidationError):
54+
return ORJSONResponse(
55+
status_code=HTTP_400_BAD_REQUEST,
56+
content=jsonable_encoder({"detail": exc.errors()}),
57+
)
3358

3459

3560
if __name__ == "__main__":

app/routes/index.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from fastapi import APIRouter
2-
from fastapi.responses import RedirectResponse, PlainTextResponse
1+
import asyncio
32

3+
from fastapi import APIRouter
4+
from fastapi.responses import PlainTextResponse, RedirectResponse
45

56
router = APIRouter()
67

@@ -24,3 +25,8 @@ async def livez() -> PlainTextResponse:
2425
sPlainTextResponse: Health check response
2526
"""
2627
return PlainTextResponse("\n", status_code=200)
28+
29+
30+
@router.get("/timeout", include_in_schema=False)
31+
async def route_for_test(sleep_time: float) -> None:
32+
await asyncio.sleep(sleep_time)

app/routes/weather.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import asyncio
2+
from typing import Dict, Optional
3+
from urllib.parse import urljoin
4+
5+
from fastapi import APIRouter
6+
from fastapi.encoders import jsonable_encoder
7+
from fastapi.responses import ORJSONResponse
8+
from httpx import AsyncClient
9+
10+
from .. import cfg, schemas
11+
from ..utils.weather import Greeting, HeadsUp, Temperature
12+
13+
router = APIRouter()
14+
15+
16+
async def request_weather_data(
17+
client: AsyncClient,
18+
lon: float,
19+
lat: float,
20+
endpoint: str,
21+
hour_offset: Optional[int] = None,
22+
) -> Dict:
23+
response = await client.get(
24+
url=urljoin(
25+
base=cfg.service.weather.base_url,
26+
url=endpoint,
27+
),
28+
params={
29+
"api_key": cfg.service.weather.api_key,
30+
"lat": lat,
31+
"lon": lon,
32+
"hour_offset": hour_offset,
33+
},
34+
)
35+
weather = response.json()
36+
return weather
37+
38+
39+
@router.get("/summary", response_model=schemas.SummaryResponse)
40+
async def summary(lon: float, lat: float) -> ORJSONResponse:
41+
async with AsyncClient() as client:
42+
cur_weather, pre_weather = await asyncio.gather(
43+
request_weather_data(
44+
client=client,
45+
lon=lon,
46+
lat=lat,
47+
endpoint=cfg.service.weather.current_endpoint,
48+
),
49+
request_weather_data(
50+
client=client,
51+
lon=lon,
52+
lat=lat,
53+
endpoint=cfg.service.weather.historical_endpoint,
54+
hour_offset=-24,
55+
),
56+
)
57+
greeting_message, temp_message, headsup_message = await asyncio.gather(
58+
Greeting.get_greeting_message(schemas.CurrentWeatherResponse(**cur_weather)),
59+
Temperature.get_temp_message(
60+
lat=lat,
61+
lon=lon,
62+
cur_temp=cur_weather.get("temp", float("inf")),
63+
pre_temp=pre_weather.get("temp", float("inf")),
64+
hour_offset=-24,
65+
),
66+
HeadsUp.get_headsup_message(lat, lon),
67+
)
68+
return ORJSONResponse(
69+
content=jsonable_encoder(
70+
{
71+
"summary": {
72+
"greeting": greeting_message,
73+
"temperature": temp_message,
74+
"heads_up": headsup_message,
75+
}
76+
}
77+
)
78+
)

app/schemas/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from .weather import (
2+
CurrentWeatherRequest,
3+
CurrentWeatherResponse,
4+
ForecastWeatherRequest,
5+
ForecastWeatherResponse,
6+
HistoricalWeatherRequest,
7+
HistoricalWeatherResponse,
8+
SummaryResponse,
9+
)
10+
11+
__all__ = [
12+
"SummaryResponse",
13+
"CurrentWeatherRequest",
14+
"CurrentWeatherResponse",
15+
"ForecastWeatherRequest",
16+
"ForecastWeatherResponse",
17+
"HistoricalWeatherRequest",
18+
"HistoricalWeatherResponse",
19+
]

app/schemas/weather.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from pydantic import BaseModel
2+
3+
4+
class Summary(BaseModel):
5+
greeting: str
6+
temperature: str
7+
heads_up: str
8+
9+
10+
class SummaryResponse(BaseModel):
11+
summary: Summary
12+
13+
14+
class Coordinate(BaseModel):
15+
lat: float
16+
lon: float
17+
18+
19+
class CurrentWeatherRequest(Coordinate):
20+
...
21+
22+
23+
class CurrentWeatherResponse(BaseModel):
24+
timestamp: float
25+
code: int
26+
temp: float
27+
rain1h: int
28+
29+
30+
class ForecastWeatherRequest(Coordinate):
31+
hour_offset: int
32+
33+
34+
class ForecastWeatherResponse(BaseModel):
35+
timestamp: float
36+
code: int
37+
min_temp: float
38+
max_temp: float
39+
rain: int
40+
41+
42+
class HistoricalWeatherRequest(Coordinate):
43+
hour_offset: int
44+
45+
46+
class HistoricalWeatherResponse(BaseModel):
47+
timestamp: float
48+
code: int
49+
rain1h: int

0 commit comments

Comments
 (0)