Skip to content

Commit aedbb01

Browse files
committed
Starter code for async web chapter.
1 parent 413ee93 commit aedbb01

24 files changed

+369
-0
lines changed

Diff for: .gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,10 @@ src/09-built-on-asyncio/.idea/09-built-on-asyncio.iml
127127
src/09-built-on-asyncio/.idea/misc.xml
128128
src/09-built-on-asyncio/.idea/modules.xml
129129
src/09-built-on-asyncio/.idea/workspace.xml
130+
src/10-async-web/.idea/10-async-web.iml
131+
src/10-async-web/.idea/misc.xml
132+
src/10-async-web/.idea/modules.xml
133+
src/10-async-web/.idea/webResources.xml
134+
src/10-async-web/.idea/workspace.xml
135+
src/10-async-web/.idea/dictionaries/mkennedy.xml
136+
Project_Default.xml

Diff for: src/10-async-web/.idea/vcs.xml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/10-async-web/acityscape_api/app.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import flask
2+
from views import city_api
3+
from views import home
4+
from config import settings
5+
import services.weather_service
6+
import services.sun_service
7+
import services.location_service
8+
9+
app = flask.Flask(__name__)
10+
is_debug = True
11+
12+
app.register_blueprint(home.blueprint)
13+
app.register_blueprint(city_api.blueprint)
14+
15+
16+
def configure_app():
17+
mode = 'dev' if is_debug else 'prod'
18+
data = settings.load(mode)
19+
20+
services.weather_service.global_init(data.get('weather_key'))
21+
services.sun_service.use_cached_data = data.get('use_cached_data')
22+
services.location_service.use_cached_data = data.get('use_cached_data')
23+
24+
print("Using cached data? {}".format(data.get('use_cached_data')))
25+
26+
27+
def run_web_app():
28+
app.run(debug=is_debug, port=5001)
29+
30+
31+
configure_app()
32+
33+
if __name__ == '__main__':
34+
run_web_app()

Diff for: src/10-async-web/acityscape_api/config/dev.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dev": true,
3+
"use_cached_data": false,
4+
"weather_key": "684e93644281bf1398569ccf91aca62d"
5+
}

Diff for: src/10-async-web/acityscape_api/config/prod.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dev": false,
3+
"use_cached_data": false,
4+
"weather_key": "684e93644281bf1398569ccf91aca62d"
5+
}

Diff for: src/10-async-web/acityscape_api/config/settings.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import os
2+
import json
3+
4+
5+
def load(mode='dev') -> dict:
6+
file = os.path.join(os.path.dirname(__file__), f"{mode}.json")
7+
if not os.path.exists(file):
8+
raise Exception(f"Config not found for {mode}.")
9+
10+
with open(file, 'r', encoding='utf-8') as fin:
11+
return json.load(fin)

Diff for: src/10-async-web/acityscape_api/requirements.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Web framework requirements
2+
flask
3+
4+
# Calling services requirements
5+
requests

Diff for: src/10-async-web/acityscape_api/services/event_service.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import random
2+
import time
3+
from typing import Tuple
4+
import requests
5+
6+
use_cached_data = False
7+
8+
measured_latency_in_sec = [
9+
0.28844, 0.334694, 0.33468, 0.343911, 0.339515, 0.344329, 0.341594, 0.352366,
10+
0.535646, 0.527148, 0.533472, 0.53351, 0.523462]
11+
12+
13+
def get_lat_long(zip_code: str, country: str) -> Tuple[float, float]:
14+
key = f'{zip_code}, {country}'
15+
url = f'http://www.datasciencetoolkit.org/street2coordinates/{key.replace(" ", "+")}'
16+
17+
if use_cached_data:
18+
time.sleep(random.choice(measured_latency_in_sec))
19+
return 45.50655, -122.733888
20+
else:
21+
resp = requests.get(url)
22+
resp.raise_for_status()
23+
24+
data = resp.json()
25+
26+
city_data = data.get(f'{zip_code}, {country}', dict())
27+
return city_data.get('latitude', 0.00), city_data.get('longitude', 0.00)
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import datetime
2+
import random
3+
import time
4+
5+
import requests
6+
7+
measured_latency_in_sec = [0.399203, 0.7046, 0.422959, 0.741911, 0.404674]
8+
use_cached_data = False
9+
10+
11+
def for_today(latitude: float, longitude: float) -> dict:
12+
url = f'https://api.sunrise-sunset.org/json?lat={latitude}&lng={longitude}'
13+
14+
if use_cached_data: # Set in config/dev.json or config/prod.json
15+
time.sleep(random.choice(measured_latency_in_sec))
16+
return {'sunrise': '06:04:09 AM', 'sunset': '08:28:48 PM', 'solar_noon': '01:16:28 PM',
17+
'day_length': '14:24:39', 'civil_twilight_begin': '05:31:10 AM', 'civil_twilight_end': '09:01:47 PM',
18+
'nautical_twilight_begin': '04:49:54 AM', 'nautical_twilight_end': '09:43:03 PM',
19+
'astronomical_twilight_begin': '04:03:13 AM', 'astronomical_twilight_end': '10:29:44 PM'}
20+
else:
21+
resp = requests.get(url)
22+
resp.raise_for_status()
23+
24+
sun_data = resp.json().get('results', {})
25+
for k, v in list(sun_data.items()):
26+
if 'AM' not in v and 'PM' not in v:
27+
continue
28+
29+
sun_data[k] = datetime.datetime.strftime(__utc_to_local(v), '%I:%M:%S %p')
30+
31+
return sun_data
32+
33+
34+
def __utc_to_local(date_text: str) -> datetime.datetime:
35+
# Not perfect, but works most of the time.
36+
utc = datetime.datetime.strptime(date_text, '%I:%M:%S %p')
37+
now_timestamp = time.time()
38+
offset = datetime.datetime.fromtimestamp(now_timestamp) - datetime.datetime.utcfromtimestamp(now_timestamp)
39+
return utc + offset
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import requests
2+
3+
__api_key = ''
4+
5+
6+
def global_init(api_key: str):
7+
global __api_key
8+
__api_key = api_key
9+
10+
11+
def get_current(zip_code: str, country_code: str) -> dict:
12+
url = f'https://api.openweathermap.org/data/2.5/weather?zip={zip_code},{country_code}&appid={__api_key}'
13+
resp = requests.get(url)
14+
resp.raise_for_status()
15+
16+
return resp.json()

Diff for: src/10-async-web/acityscape_api/views/city_api.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import flask
2+
from services import weather_service, sun_service, location_service
3+
4+
blueprint = flask.blueprints.Blueprint(__name__, __name__)
5+
6+
7+
@blueprint.route('/api/weather/<zip_code>/<country>', methods=['GET'])
8+
def weather(zip_code: str, country: str):
9+
weather_data = weather_service.get_current(zip_code, country)
10+
if not weather_data:
11+
flask.abort(404)
12+
return flask.jsonify(weather_data)
13+
14+
15+
@blueprint.route('/api/sun/<zip_code>/<country>', methods=['GET'])
16+
def sun(zip_code: str, country: str):
17+
lat, long = location_service.get_lat_long(zip_code, country)
18+
sun_data = sun_service.for_today(lat, long)
19+
if not sun_data:
20+
flask.abort(404)
21+
return flask.jsonify(sun_data)

Diff for: src/10-async-web/acityscape_api/views/home.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import flask
2+
3+
blueprint = flask.blueprints.Blueprint(__name__, __name__)
4+
5+
6+
@blueprint.route('/')
7+
def index():
8+
return "Welcome to the city_scape API. " \
9+
"Use /api/sun/[zipcode]/[country code (e.g. us)] and" \
10+
"/api/weather/[zipcode]/[country code (e.g. us)] for API calls."
11+
12+
13+
@blueprint.errorhandler(404)
14+
def not_found(_):
15+
return flask.Response("The page was not found.", status=404)

Diff for: src/10-async-web/cityscape_api/app.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import flask
2+
from views import city_api
3+
from views import home
4+
from config import settings
5+
import services.weather_service
6+
import services.sun_service
7+
import services.location_service
8+
9+
app = flask.Flask(__name__)
10+
is_debug = True
11+
12+
app.register_blueprint(home.blueprint)
13+
app.register_blueprint(city_api.blueprint)
14+
15+
16+
def configure_app():
17+
mode = 'dev' if is_debug else 'prod'
18+
data = settings.load(mode)
19+
20+
services.weather_service.global_init(data.get('weather_key'))
21+
services.sun_service.use_cached_data = data.get('use_cached_data')
22+
services.location_service.use_cached_data = data.get('use_cached_data')
23+
24+
print("Using cached data? {}".format(data.get('use_cached_data')))
25+
26+
27+
def run_web_app():
28+
app.run(debug=is_debug, port=5001)
29+
30+
31+
configure_app()
32+
33+
if __name__ == '__main__':
34+
run_web_app()

Diff for: src/10-async-web/cityscape_api/config/dev.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dev": true,
3+
"use_cached_data": false,
4+
"weather_key": "684e93644281bf1398569ccf91aca62d"
5+
}

Diff for: src/10-async-web/cityscape_api/config/prod.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dev": false,
3+
"use_cached_data": false,
4+
"weather_key": "684e93644281bf1398569ccf91aca62d"
5+
}

Diff for: src/10-async-web/cityscape_api/config/settings.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import os
2+
import json
3+
4+
5+
def load(mode='dev') -> dict:
6+
file = os.path.join(os.path.dirname(__file__), f"{mode}.json")
7+
if not os.path.exists(file):
8+
raise Exception(f"Config not found for {mode}.")
9+
10+
with open(file, 'r', encoding='utf-8') as fin:
11+
return json.load(fin)

Diff for: src/10-async-web/cityscape_api/requirements.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Web framework requirements
2+
flask
3+
4+
# Calling services requirements
5+
requests

Diff for: src/10-async-web/cityscape_api/services/event_service.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import random
2+
import time
3+
from typing import Tuple
4+
import requests
5+
6+
use_cached_data = False
7+
8+
measured_latency_in_sec = [
9+
0.28844, 0.334694, 0.33468, 0.343911, 0.339515, 0.344329, 0.341594, 0.352366,
10+
0.535646, 0.527148, 0.533472, 0.53351, 0.523462]
11+
12+
13+
def get_lat_long(zip_code: str, country: str) -> Tuple[float, float]:
14+
key = f'{zip_code}, {country}'
15+
url = f'http://www.datasciencetoolkit.org/street2coordinates/{key.replace(" ", "+")}'
16+
17+
if use_cached_data:
18+
time.sleep(random.choice(measured_latency_in_sec))
19+
return 45.50655, -122.733888
20+
else:
21+
resp = requests.get(url)
22+
resp.raise_for_status()
23+
24+
data = resp.json()
25+
26+
city_data = data.get(f'{zip_code}, {country}', dict())
27+
return city_data.get('latitude', 0.00), city_data.get('longitude', 0.00)
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import datetime
2+
import random
3+
import time
4+
5+
import requests
6+
7+
measured_latency_in_sec = [0.399203, 0.7046, 0.422959, 0.741911, 0.404674]
8+
use_cached_data = False
9+
10+
11+
def for_today(latitude: float, longitude: float) -> dict:
12+
url = f'https://api.sunrise-sunset.org/json?lat={latitude}&lng={longitude}'
13+
14+
if use_cached_data: # Set in config/dev.json or config/prod.json
15+
time.sleep(random.choice(measured_latency_in_sec))
16+
return {'sunrise': '06:04:09 AM', 'sunset': '08:28:48 PM', 'solar_noon': '01:16:28 PM',
17+
'day_length': '14:24:39', 'civil_twilight_begin': '05:31:10 AM', 'civil_twilight_end': '09:01:47 PM',
18+
'nautical_twilight_begin': '04:49:54 AM', 'nautical_twilight_end': '09:43:03 PM',
19+
'astronomical_twilight_begin': '04:03:13 AM', 'astronomical_twilight_end': '10:29:44 PM'}
20+
else:
21+
resp = requests.get(url)
22+
resp.raise_for_status()
23+
24+
sun_data = resp.json().get('results', {})
25+
for k, v in list(sun_data.items()):
26+
if 'AM' not in v and 'PM' not in v:
27+
continue
28+
29+
sun_data[k] = datetime.datetime.strftime(__utc_to_local(v), '%I:%M:%S %p')
30+
31+
return sun_data
32+
33+
34+
def __utc_to_local(date_text: str) -> datetime.datetime:
35+
# Not perfect, but works most of the time.
36+
utc = datetime.datetime.strptime(date_text, '%I:%M:%S %p')
37+
now_timestamp = time.time()
38+
offset = datetime.datetime.fromtimestamp(now_timestamp) - datetime.datetime.utcfromtimestamp(now_timestamp)
39+
return utc + offset
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import requests
2+
3+
__api_key = ''
4+
5+
6+
def global_init(api_key: str):
7+
global __api_key
8+
__api_key = api_key
9+
10+
11+
def get_current(zip_code: str, country_code: str) -> dict:
12+
url = f'https://api.openweathermap.org/data/2.5/weather?zip={zip_code},{country_code}&appid={__api_key}'
13+
resp = requests.get(url)
14+
resp.raise_for_status()
15+
16+
return resp.json()

Diff for: src/10-async-web/cityscape_api/views/city_api.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import flask
2+
from services import weather_service, sun_service, location_service
3+
4+
blueprint = flask.blueprints.Blueprint(__name__, __name__)
5+
6+
7+
@blueprint.route('/api/weather/<zip_code>/<country>', methods=['GET'])
8+
def weather(zip_code: str, country: str):
9+
weather_data = weather_service.get_current(zip_code, country)
10+
if not weather_data:
11+
flask.abort(404)
12+
return flask.jsonify(weather_data)
13+
14+
15+
@blueprint.route('/api/sun/<zip_code>/<country>', methods=['GET'])
16+
def sun(zip_code: str, country: str):
17+
lat, long = location_service.get_lat_long(zip_code, country)
18+
sun_data = sun_service.for_today(lat, long)
19+
if not sun_data:
20+
flask.abort(404)
21+
return flask.jsonify(sun_data)

Diff for: src/10-async-web/cityscape_api/views/home.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import flask
2+
3+
blueprint = flask.blueprints.Blueprint(__name__, __name__)
4+
5+
6+
@blueprint.route('/')
7+
def index():
8+
return "Welcome to the city_scape API. " \
9+
"Use /api/sun/[zipcode]/[country code (e.g. us)] and" \
10+
"/api/weather/[zipcode]/[country code (e.g. us)] for API calls."
11+
12+
13+
@blueprint.errorhandler(404)
14+
def not_found(_):
15+
return flask.Response("The page was not found.", status=404)

0 commit comments

Comments
 (0)