From 4086e7445c70a8163f8f63ca5afd6e5244ef3650 Mon Sep 17 00:00:00 2001 From: Stephen Hoover Date: Sat, 8 Oct 2016 13:58:35 -0500 Subject: [PATCH] ENH Generalize to other bike share networks Multiple bikeshare networks use the same API as Divvy. Move the Divvy-specific settings and language into the configuration file so that this skill can switch to any network. --- README.md | 8 ++++++-- divvy/handle.py | 44 ++++++++++++++++++++++++++------------------ divvy/location.py | 27 ++++++++++++++------------- lambda_function.py | 6 ++++-- 4 files changed, 50 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 1f0a9d1..195a2a3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ # Chicago Bikeshare A skill for Amazon Alexa
-http://www.alexaskillstore.com/ ## Overview @@ -65,7 +64,12 @@ in the environment, but only v1.3. This Skill needs v1.4.) The skill requires an additional `config.py` file in the "divvy" folder. This file should define the following attributes at global level: - APP_ID : The unique ID of the Skill which uses the Lambda -- divvy_api : Web address of the Divvy API. As of September 2016, this is https://feeds.divvybikes.com/stations/stations.json. +- network_name : The name of the bike sharing network, e.g. "Divvy" or "CoGo" +- default_state : The two letter state code in which the network operates, e.g. "IL" +- default_city : The name of the city in which the network operates, e.g. "Chicago" +- time_zone : The local time zone, e.g. "US/Central" or "US/Eastern" +- sample_station : A valid station name for use in the help prompt +- divvy_api : Web address of the bike sharing network's API. As of September 2016, the Divvy network's API is https://feeds.divvybikes.com/stations/stations.json. - maps_api : Web address of the Google Maps Geocoding API (used when users store addresses). As of September 2016, this is https://maps.googleapis.com/maps/api/geocode/. - maps_api_key : Token which allows access to the Google Maps Geocoding API - aws_region : Region in which you have your database diff --git a/divvy/handle.py b/divvy/handle.py index a6faa19..1210f01 100644 --- a/divvy/handle.py +++ b/divvy/handle.py @@ -152,15 +152,15 @@ def intent(req, session): elif intent['name'] == 'AMAZON.HelpIntent': return reply.build("You can ask me how many bikes or docks are " "at a specific station, or else just ask the " - "status of a station. Use the Divvy station " - "name, such as " - "\"Milwaukee Avenue and Rockwell Street\". " + "status of a station. Use the %s station " + "name, such as \"%s\". " "If you only remember one cross-street, you " "can ask me to list all stations on a particular " "street. If you've told me to \"add an address\", " "I can remember that and use it when you " "ask me to \"check my commute\". " - "What should I do?", + "What should I do?" % + (config.network_name, config.sample_station), persist=session['attributes'], is_end=False) else: @@ -170,8 +170,8 @@ def intent(req, session): def _time_string(): - """Return a string representing local Chicago time""" - os.environ['TZ'] = 'US/Central' # Chicago! + """Return a string representing local time""" + os.environ['TZ'] = config.time_zone time.tzset() return time.asctime() @@ -185,12 +185,12 @@ def _station_from_intent(intent, stations): JSON following the Alexa "IntentRequest" schema with name "CheckBikeIntent" stations : list of dict - JSON following the Divvy "stationBeanList" schema + JSON following the bikeshare "stationBeanList" schema Returns ------- dict - A single station information JSON from the Divvy API response + A single station information JSON from the bikeshare API response Raises ------ @@ -341,7 +341,8 @@ def check_commute(intent, session): nearest_st[1]['stationName'])) return reply.build(utter, - card_title="Your Divvy Commute Status", + card_title=("Your %s Commute Status" % + config.network_name), card_text='\n'.join(card_text), is_end=True) @@ -478,22 +479,27 @@ def add_address(intent, session): return add_address(intent, session) elif sess_data['next_step'] == 'check_address': if sess_data['zip_code']: - # Assume that Divvy subscribers are always interested - # in Illinois addresses, but not necessarily Chicago. - addr = '%s, IL, %s' % (sess_data['spoken_address'], + # Assume that network subscribers are always interested + # in in-state addresses, but not necessarily in the city. + addr = '%s, %s, %s' % (sess_data['spoken_address'], + config.default_state, sess_data['zip_code']) else: - # Without a zip code, assume Chicago + # Without a zip code, assume the network's home city # to add necessary specificity. - addr = '%s, Chicago, IL' % sess_data['spoken_address'] + addr = '%s, %s, %s' % (sess_data['spoken_address'], + config.default_city, + config.default_state) lat, lon, full_address = geocoding.get_lat_lon(addr) if full_address.endswith(", USA"): # We don't need to keep the country name. full_address = full_address[:-5] - if full_address.lower().startswith("chicago, il"): + if full_address.lower().startswith("%s, %s" % + (config.default_city.lower(), + config.default_state.lower())): # If the geocoding fails to find a specific address, - # it will return a generic "Chicago" location. + # it will return a generic city location. sess_data['next_step'] = 'num_and_name' return reply.build("I'm sorry, I heard the address \"%s\", " "but I can't figure out where that is. " @@ -744,7 +750,8 @@ def list_stations(intent, session): sta_name = location.text_to_speech(possible[0]['stationName']) return reply.build("There's only one: the %s " "station." % sta_name, - card_title="Divvy Stations on %s" % street_name, + card_title=("%s Stations on %s" % + (config.network_name, street_name)), card_text=("One station on %s: %s" % (street_name, possible[0]['stationName'])), is_end=True) @@ -759,6 +766,7 @@ def list_stations(intent, session): (len(possible), street_name, '\n'.join(p['stationName'] for p in possible))) return reply.build(speech, - card_title="Divvy Stations on %s" % street_name, + card_title=("%s Stations on %s" % + (config.network_name, street_name)), card_text=card_text, is_end=True) diff --git a/divvy/location.py b/divvy/location.py index f0a4971..b8a3551 100644 --- a/divvy/location.py +++ b/divvy/location.py @@ -1,6 +1,6 @@ """Match spoken station names and addresses to the stored station information which comes back from -the Divvy API. +the bikeshare network's API. """ import difflib import logging @@ -10,7 +10,7 @@ # Create a couple of lookup tables to go # between the name format given to us by -# Divvy and the transcription of spoken words. +# the API and the transcription of spoken words. ABBREV = {' st ': ' street ', ' pl ': ' place ', ' ave ': ' avenue ', @@ -20,7 +20,8 @@ ' ln ': ' lane ', ' pkwy ': ' parkway ', ' ter ': ' terrace ', - ' ct ': ' court '} + ' ct ': ' court ', + ' mt ': ' mount '} DIRECTIONS = {' n ': ' north ', ' w ': ' west ', @@ -57,14 +58,14 @@ def _check_possible(possible, first, second=None): def matching_station_list(stations, first, second=None, exact=False): - """Filter the Divvy station list based on locations + """Filter the station list based on locations May return multiple stations Parameters ---------- stations : list of dict - The 'stationBeanList' from the Divvy API response + The 'stationBeanList' from the bikeshare API response first : str The first component of a station name or address, e.g. "Larrabee" or "Larrabee Street". @@ -79,7 +80,7 @@ def matching_station_list(stations, first, second=None, exact=False): Returns ------- list of dict - List of station status JSONs from the Divvy API response + List of station status JSONs from the bikeshare API response """ possible = [] first = speech_to_text(first) @@ -165,12 +166,12 @@ def _fuzzy_match_two(first, second, stations): def find_station(stations, first, second=None, exact=False): - """Filter the Divvy station list to find a single station + """Filter the station list to find a single station Parameters ---------- stations : list of dict - The 'stationBeanList' from the Divvy API response + The 'stationBeanList' from the bikeshare API response first : str The first component of a station name or address, e.g. "Larrabee" or "Larrabee Street". @@ -184,7 +185,7 @@ def find_station(stations, first, second=None, exact=False): Returns ------- dict - A single station status JSON from the Divvy API response + A single station status JSON from the bikeshare API response Raises ------ @@ -196,7 +197,7 @@ def find_station(stations, first, second=None, exact=False): def speech_to_text(address): - """Standardize speech input to look like Divvy station names + """Standardize speech input to look like station names in the network """ # Add a space, since we look for spaces after abbreviations address = address.lower() + ' ' @@ -224,9 +225,9 @@ def text_to_speech(address): return address.strip() -def get_stations(divvy_api): - """Query the Divvy API and return the station list""" - resp = requests.get(divvy_api) +def get_stations(bike_api): + """Query the bikeshare API and return the station list""" + resp = requests.get(bike_api) stations = resp.json()['stationBeanList'] return stations diff --git a/lambda_function.py b/lambda_function.py index 0bfc615..9afdeab 100644 --- a/lambda_function.py +++ b/lambda_function.py @@ -24,13 +24,15 @@ def lambda_handler(event, context): if event['request']['type'] == "IntentRequest": return handle.intent(event['request'], event['session']) elif event['request']['type'] == "LaunchRequest": - return reply.build("Ask me a question about a Divvy station.", + return reply.build("Ask me a question about a " + "%s station." % config.network_name, is_end=False) elif event['request']['type'] == "SessionEndedRequest": return reply.build("Bike safe!", is_end=True) else: # I don't think there's any other kinds of requests. - return reply.build("Ask me a question about a Divvy station.", + return reply.build("Ask me a question about a " + "%s station." % config.network_name, is_end=False) except Exception as err: # NOQA log.exception('Unhandled exception for event\n%s\n' % str(event))