Skip to content
This repository has been archived by the owner on Nov 29, 2021. It is now read-only.

Commit

Permalink
🏎 Switch to FastAPI (#44)
Browse files Browse the repository at this point in the history
* 🏎 Switch to FastAPI

* πŸ› Fix datetime import

* πŸ“š Improve docs

* ⬆️ Update dependencies

* πŸ” Update Python runtime

* πŸ”§ Switch to GitHub Actions

* πŸ”§ Install dependencies in test
  • Loading branch information
jerr0328 authored Jul 17, 2020
1 parent 472432f commit bc0475b
Show file tree
Hide file tree
Showing 14 changed files with 552 additions and 347 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: Tests
on:
push:
branches:
- main
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.8"
- run: pip install -r requirements.txt -r requirements-dev.txt
name: Install dependencies
- run: pytest -v
name: Run tests
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ repos:
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.0.7
rev: v5.1.1
hooks:
- id: isort
7 changes: 0 additions & 7 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
web: gunicorn isitsnowinginberlin:APP --log-file -
web: uvicorn isitsnowinginberlin.main:app --host=0.0.0.0 --port=${PORT:-5000}
134 changes: 0 additions & 134 deletions isitsnowinginberlin.py

This file was deleted.

Empty file added isitsnowinginberlin/__init__.py
Empty file.
137 changes: 137 additions & 0 deletions isitsnowinginberlin/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""
Copyright 2020 Jeremy Mayeres
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import asyncio
import json
import logging
import os
from datetime import datetime

import aioredis
import httpx
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi_camelcase import CamelModel
from pydantic import Field

logging.basicConfig()
logger = logging.getLogger("isitsnowinginberlin")
app = FastAPI(title="Is it Snowing in Berlin")

BERLIN_COORDS = "52.52,13.37"
UPDATE_DELAY_SECONDS = 600 # Update delay in seconds (10 minutes)
DARKSKY_URL = (
f"https://api.darksky.net/forecast/{os.getenv('DARKSKY_KEY')}/{BERLIN_COORDS}"
)
DARKSKY_PAYLOAD = {"units": "si", "exclude": "[minutely,hourly,flags,alerts]"}
REDIS_URL = os.getenv("REDIS_URL")
CACHE_KEY = "cached-wx"


class SnowingResponse(CamelModel):
is_snowing: bool = Field(..., description="Flag if snowing")
data_updated: int = Field(
..., description="Timestamp (seconds) of when the data was updated"
)
temperature: float = Field(..., description="Temperature in Celsius")


class WillSnowResponse(CamelModel):
will_snow: bool = Field(..., description="Flag if will snow")
data_updated: int = Field(
..., description="Timestamp (seconds) of when the data was updated"
)


async def store_weather(wx: dict):
logger.info("Updating cache")
wx_str = json.dumps(wx)
await app.redis.set(CACHE_KEY, wx_str, expire=UPDATE_DELAY_SECONDS)


async def update_weather() -> dict:
async with httpx.AsyncClient() as client:
r = await client.get(DARKSKY_URL, params=DARKSKY_PAYLOAD)
r.raise_for_status()
wx = r.json()
asyncio.create_task(store_weather(wx))
return wx


async def get_weather() -> dict:
cached_str = await app.redis.get(CACHE_KEY, encoding="utf-8")
if cached_str:
logger.info("Using cached weather")
return json.loads(cached_str)
else:
logger.info("Need to fetch weather")
return await update_weather()


def is_snowing(wx: dict) -> bool:
return wx["currently"]["icon"] == "snow"


def will_snow(wx: dict) -> bool:
now = datetime.now()
next_forecast = next(
data
for data in wx["daily"]["data"]
if now < datetime.fromtimestamp(data["time"])
)
return next_forecast["icon"] == "snow"


@app.on_event("startup")
async def setup():
app.redis = await aioredis.create_redis_pool(REDIS_URL)


@app.get("/api/rawWeather")
async def api_raw_weather():
"""Return the data retrieved from the weather service."""
return await get_weather()


@app.get("/api/isSnowing", response_model=SnowingResponse)
async def api_is_snowing():
"""Find out if it's snowing in Berlin."""
wx = await get_weather()
return {
"isSnowing": is_snowing(wx),
"dataUpdated": wx["currently"]["time"],
"temperature": wx["currently"]["temperature"],
}


@app.get("/api/willSnow", response_model=WillSnowResponse)
async def api_will_snow():
"""Find out if it will snow in Berlin soon."""
wx = await get_weather()
return {"willSnow": will_snow(wx), "dataUpdated": wx["currently"]["time"]}


@app.get("/tomorrow/", include_in_schema=False)
async def tomorrow():
return FileResponse("tomorrow/index.html")


@app.get("/", include_in_schema=False)
async def main():
return FileResponse("index.html")


app.mount("/static", StaticFiles(directory="static"), name="static")
2 changes: 1 addition & 1 deletion requirements-dev.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-r requirements.txt
-c requirements.txt
pytest
black
isort
Expand Down
Loading

0 comments on commit bc0475b

Please sign in to comment.