Skip to content

Commit

Permalink
Merge pull request #6 from Zer0897/feature/async
Browse files Browse the repository at this point in the history
Migrating to Quart
  • Loading branch information
MarkKoz authored Jul 6, 2019
2 parents 0adbfcf + 786aac2 commit 81b9614
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 80 deletions.
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, ...]:
"""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

0 comments on commit 81b9614

Please sign in to comment.