Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrating to Quart #6

Merged
merged 9 commits into from
Jul 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 190 additions & 35 deletions gentle_gnomes/poetry.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions gentle_gnomes/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ packages = [ { include = "src" } ]

[tool.poetry.dependencies]
python = "^3.7"
flask = "^1.0"
python-dotenv = "^0.10.3"
requests = "^2.22"
scipy = "^1.3"
aiohttp = "^3.5"
quart = "^0.9.1"
uvloop = "^0.12.2"

[tool.poetry.dev-dependencies]
pytest = "^4.6"
coverage = "^4.5"
flake8 = "^3.7"
pytest-asyncio = "^0.10.0"
pre-commit = "^1.17"
flake8-bugbear = "^19.3"
flake8-quotes = "^2.0"
Expand Down
18 changes: 12 additions & 6 deletions gentle_gnomes/src/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
from flask import Flask
import uvloop
from quart import Quart

from . import azavea
from . import view

uvloop.install()

def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
app = Quart(__name__, instance_relative_config=True)
app.config.from_mapping(SECRET_KEY='dev')
app.config.from_pyfile('config.py', silent=True)

if test_config is None:
app.config.from_pyfile('config.py', silent=True)
else:
if test_config is not None:
app.config.from_mapping(test_config)

app.register_blueprint(view.bp)

app.azavea = azavea.Client(app.config['AZAVEA_TOKEN'])

app.register_blueprint(view.bp)
@app.teardown_appcontext
async def teardown(*args):
await app.azavea.teardown()

return app
48 changes: 28 additions & 20 deletions gentle_gnomes/src/azavea.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import aiohttp
import typing as t

import requests
import asyncio as aio

BASE_URL = 'https://app.climate.azavea.com/api'

Expand All @@ -17,23 +17,31 @@ def __str__(self):
class Client:
"""Client for interacting with the Azavea Climate API."""

# Wait for async event loop to instanstiate
session: aiohttp.ClientSession = None

def __init__(self, token: str):
self.session = requests.Session()
self.session.headers = {'Authorization': f'Token {token}'}
self.headers = {'Authorization': f'Token {token}'}

async def _get(self, endpoint: str, **kwargs) -> t.Union[t.Dict, t.List]:
if self.session is None:
self.session = aiohttp.ClientSession(headers=self.headers)

async with self.session.get(BASE_URL + endpoint, **kwargs) as response:
return await response.json()

def _get(self, endpoint: str, **kwargs) -> t.Union[t.Dict, t.List]:
response = self.session.get(BASE_URL + endpoint, ** kwargs)
response.raise_for_status()
async def teardown(self):
if self.session is not None:
await self.session.close()

return response.json()

def get_cities(self, **kwargs) -> t.Iterator[City]:
async def get_cities(self, **kwargs) -> t.Iterator[City]:
"""Return all available cities."""
params = {'page': 1}
params.update(kwargs.get('params', {}))

while True:
cities = self._get('/city', params=params, **kwargs)
cities = await self._get('/city', params=params, **kwargs)

if not cities.get('next'):
break
Expand All @@ -43,7 +51,7 @@ def get_cities(self, **kwargs) -> t.Iterator[City]:
for city in cities['features']:
yield City(city['properties']['name'], city['properties']['admin'], city['id'])

def get_nearest_city(
async def get_nearest_city(
self,
lat: float,
lon: float,
Expand All @@ -57,24 +65,24 @@ def get_nearest_city(
'limit': limit,
}

cities = self._get('/city/nearest', params=params, **kwargs)
cities = await self._get('/city/nearest', params=params, **kwargs)

if cities['count'] > 0:
city = cities['features'][0]
return City(city['properties']['name'], city['properties']['admin'], city['id'])

def get_scenarios(self, **kwargs) -> t.List:
async def get_scenarios(self, **kwargs) -> t.List:
"""Return all available scenarios."""
return self._get('/scenario', **kwargs)
return await self._get('/scenario', **kwargs)

def get_indicators(self, **kwargs) -> t.Dict:
async def get_indicators(self, **kwargs) -> t.Dict:
"""Return the full list of indicators."""
return self._get('/indicator', **kwargs)
return await self._get('/indicator', **kwargs)

def get_indicator_details(self, indicator: str, **kwargs) -> t.Dict:
async def get_indicator_details(self, indicator: str, **kwargs) -> t.Dict:
"""Return the description and parameters of a specified indicator."""
return self._get(f'/indicator/{indicator}', **kwargs)
return await self._get(f'/indicator/{indicator}', **kwargs)

def get_indicator_data(self, city: int, scenario: str, indicator: str, **kwargs) -> t.Dict:
async def get_indicator_data(self, city: int, scenario: str, indicator: str, **kwargs) -> t.Dict:
"""Return derived climate indicator data for the requested indicator."""
return self._get(f'/climate-data/{city}/{scenario}/indicator/{indicator}', **kwargs)
return await self._get(f'/climate-data/{city}/{scenario}/indicator/{indicator}', **kwargs)
10 changes: 5 additions & 5 deletions gentle_gnomes/src/indicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Tuple

import numpy as np
from flask import current_app as app
from quart import current_app as app
from scipy import stats

from .azavea import City
Expand All @@ -19,14 +19,13 @@ class Indicator:
def __init__(self, name: str, city: City):
self.name = name
self.city = city
self._populate_data()

def _populate_data(self):
async def _populate_data(self):
items = []
count = 0

for scenario in ('historical', 'RCP85'):
response = app.azavea.get_indicator_data(self.city.id, scenario, self.name)
response = await app.azavea.get_indicator_data(self.city.id, scenario, self.name)
self.label = response['indicator']['label']
self.description = response['indicator']['description']
self.units = response['units']
Expand All @@ -50,13 +49,14 @@ def _calc_slope(x: np.ndarray, y: np.ndarray) -> float:
return slope


def get_top_indicators(city: City, n: int = 5) -> Tuple[Indicator, ...]:
async def get_top_indicators(city: City, n: int = 5) -> Tuple[Indicator, ...]:
intendednull marked this conversation as resolved.
Show resolved Hide resolved
"""Return the top n indicators with the highest rate of change."""
rates = Counter()
indicators = {}

for name in INDICATORS:
indicator = Indicator(name, city)
await self._populate_data()

rates[name] = indicator.rate
indicators[name] = indicator
Expand Down
21 changes: 11 additions & 10 deletions gentle_gnomes/src/view.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
import json

import flask
from flask import current_app as app
from flask import render_template
import quart
from quart import current_app as app
from quart import render_template

from . import indicator

bp = flask.Blueprint('view', __name__, url_prefix='/')
bp = quart.Blueprint('view', __name__, url_prefix='/')


@bp.route('/')
def index():
return render_template('view/index.html')
async def index():
return await render_template('view/index.html')


@bp.route('/search', methods=['POST'])
def search():
async def search():
try:
location = json.loads(flask.request.form['location'])
form = await quart.request.form
location = json.loads(form['location'])
latitude = location['lat']
longitude = location['lng']
except (json.JSONDecodeError, KeyError):
return render_template('view/results.html')

city = app.azavea.get_nearest_city(latitude, longitude)
city = await app.azavea.get_nearest_city(latitude, longitude)
if city:
with app.app_context():
results = indicator.get_top_indicators(city)
results = await indicator.get_top_indicators(city)
else:
results = None

Expand Down
6 changes: 6 additions & 0 deletions gentle_gnomes/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

from src import create_app
from src.azavea import Client


@pytest.fixture
Expand All @@ -17,3 +18,8 @@ def client(app):
@pytest.fixture
def runner(app):
return app.test_cli_runner()


@pytest.fixture
def azavea(app):
return Client(app.config['AZAVEA_TOKEN'])
6 changes: 6 additions & 0 deletions gentle_gnomes/tests/test_azavea.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import pytest


@pytest.mark.asyncio
async def test_request_data_exists(azavea):
assert await azavea.get_indicators()
9 changes: 7 additions & 2 deletions gentle_gnomes/tests/test_view.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
def test_index_response_200(client):
assert client.get('/').status_code == 200
import pytest


@pytest.mark.asyncio
async def test_index_response_200(client):
res = await client.get('/')
assert res.status_code == 200