Merge remote-tracking branch 'remotes/origin/dev' into bug/stt-stats

pull/199/head
Chris Veilleux 2019-12-03 13:00:46 -06:00
commit d6f4f74e25
9 changed files with 267 additions and 36 deletions

View File

@ -8,7 +8,7 @@ behave = "*"
pyhamcrest = "*"
[packages]
flask = "*"
flask = "<1.1"
requests = "*"
selene = {editable = true,path = "./../../shared"}
SpeechRecognition = "*"

View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "d0f209f40deb313debbdf78ca1d590913f454e8cb98e2be96447acd0f00043df"
"sha256": "3e875b3d36c7ad28b2c052508172f60330e1c9f5e5e8edd668ba5677adae750a"
},
"pipfile-spec": 6,
"requires": {
@ -18,10 +18,10 @@
"default": {
"certifi": {
"hashes": [
"sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
],
"version": "==2019.6.16"
"version": "==2019.9.11"
},
"chardet": {
"hashes": [
@ -53,11 +53,11 @@
},
"flask": {
"hashes": [
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
"sha256:1a21ccca71cee5e55b6a367cc48c6eb47e3c447f76e64d41f3f3f931c17e7c96",
"sha256:ed1330220a321138de53ec7c534c3d90cf2f7af938c7880fc3da13aa46bf870f"
],
"index": "pypi",
"version": "==1.1.1"
"version": "==1.0.4"
},
"idna": {
"hashes": [
@ -175,16 +175,18 @@
},
"python-http-client": {
"hashes": [
"sha256:7e430f4b9dd2b621b0051f6a362f103447ea8e267594c602a5c502a0c694ee38"
"sha256:01f58f1871612fdce6a4545df7c867a6d1457695652a7ca48d5c22e5bf57628d",
"sha256:c2776054245db376ea26c859b80e9280b1a470b96ed998d60d35951f89bbbe79",
"sha256:e455ae0dfd5819ac483f7fecf08ab8693048d9dc47a0a6fe0d4aebf86d9d1d17"
],
"version": "==3.1.0"
"version": "==3.2.1"
},
"redis": {
"hashes": [
"sha256:0607faf60d44768e17f65e506fe390679b54be6fd6d5f0c2d28f3ebf4f0535e7",
"sha256:9c96c5bf11a8c47eb33cefdefd41c47cf1ff68db41c51b56b3ec7938b7c627f7"
"sha256:98a22fb750c9b9bb46e75e945dc3f61d0ab30d06117cbb21ff9cd1d315fedd3b",
"sha256:c504251769031b0dd7dd5cf786050a6050197c6de0d37778c80c08cb04ae8275"
],
"version": "==3.3.7"
"version": "==3.3.8"
},
"requests": {
"hashes": [
@ -214,10 +216,11 @@
},
"sendgrid": {
"hashes": [
"sha256:297d33363a70df9b39419210e1273b165d487730e85c495695e0015bc626db71",
"sha256:8b82c8c801dde8180a567913a9f80d8a63f38e39f209edde302b6df899b4bca1"
"sha256:a9999878aad90e32d7b62464454adc70bcef40085c729355ea58717bb0ea0dbd",
"sha256:cb0b21a83a54bc99d9befda1ea7b4f15fe8db362a152458e58abc5ce23d6d828",
"sha256:f04fee009c750b47ab984f3c4a735facacc7fba902052d597f7e60b601e56bcc"
],
"version": "==6.0.5"
"version": "==6.1.0"
},
"six": {
"hashes": [
@ -235,18 +238,18 @@
},
"stripe": {
"hashes": [
"sha256:344cd691a542f08c508b9d12ac201da46b7f0f21a0a7f72f56199b3baee795eb",
"sha256:e07efa567ae0831fe351ddb49de074aa1681569fd234d4f1dc0a9f7f4c017820"
"sha256:f5b27b45bb5d7fe8c7e524a2bd4372fbf32e5e2d42aafa8e84802801faff28d2",
"sha256:f80e76dc17ead135a992fd9b03ee4ef3a49a958501d482f8fd11431ba3287870"
],
"index": "pypi",
"version": "==2.35.0"
"version": "==2.36.2"
},
"urllib3": {
"hashes": [
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
],
"version": "==1.25.3"
"version": "==1.25.6"
},
"uwsgi": {
"hashes": [
@ -257,10 +260,10 @@
},
"werkzeug": {
"hashes": [
"sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4",
"sha256:a13b74dd3c45f758d4ebdb224be8f1ab8ef58b3c0ffc1783a8c7d9f4f50227e6"
"sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7",
"sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"
],
"version": "==0.15.5"
"version": "==0.16.0"
},
"wrapt": {
"hashes": [
@ -280,9 +283,9 @@
},
"parse": {
"hashes": [
"sha256:1b68657434d371e5156048ca4a0c5aea5afc6ca59a2fea4dd1a575354f617142"
"sha256:a5fca7000c6588d77bc65c28f3f21bfce03b5e44daa8f9f07c17fe364990d717"
],
"version": "==1.12.0"
"version": "==1.12.1"
},
"parse-type": {
"hashes": [

View File

@ -40,6 +40,7 @@ from .endpoints.device_skill_manifest import DeviceSkillManifestEndpoint
from .endpoints.device_skill_settings import DeviceSkillSettingsEndpoint
from .endpoints.device_skill_settings import DeviceSkillSettingsEndpointV2
from .endpoints.device_subscription import DeviceSubscriptionEndpoint
from .endpoints.geolocation import GeolocationEndpoint
from .endpoints.google_stt import GoogleSTTEndpoint
from .endpoints.oauth_callback import OauthCallbackEndpoint
from .endpoints.open_weather_map import OpenWeatherMapEndpoint
@ -54,51 +55,48 @@ public = Flask(__name__)
public.config.from_object(get_base_config())
public.config['GOOGLE_STT_KEY'] = os.environ['GOOGLE_STT_KEY']
public.config['SELENE_CACHE'] = SeleneCache()
public.response_class = SeleneResponse
public.register_blueprint(selene_api)
public.add_url_rule(
'/v1/device/<string:device_id>/skill/<string:skill_gid>',
view_func=DeviceSkillSettingsEndpoint.as_view('device_skill_delete_api'),
methods=['DELETE']
)
public.add_url_rule(
'/v1/device/<string:device_id>/skill',
view_func=DeviceSkillSettingsEndpoint.as_view('device_skill_api'),
methods=['GET', 'PUT']
)
public.add_url_rule(
'/v1/device/<string:device_id>/skill/settings',
view_func=DeviceSkillSettingsEndpointV2.as_view('skill_settings_api'),
methods=['GET']
)
public.add_url_rule(
'/v1/device/<string:device_id>/settingsMeta',
view_func=SkillSettingsMetaEndpoint.as_view('device_user_skill_api'),
methods=['PUT']
)
public.add_url_rule(
'/v1/device/<string:device_id>',
view_func=DeviceEndpoint.as_view('device_api'),
methods=['GET', 'PATCH']
)
public.add_url_rule(
'/v1/device/<string:device_id>/setting',
view_func=DeviceSettingEndpoint.as_view('device_settings_api'),
methods=['GET']
)
public.add_url_rule(
'/v1/device/<string:device_id>/subscription',
view_func=DeviceSubscriptionEndpoint.as_view('device_subscription_api'),
methods=['GET']
)
public.add_url_rule(
'/v1/geolocation',
view_func=GeolocationEndpoint.as_view('location_api'),
methods=['GET']
)
public.add_url_rule(
'/v1/wa',
view_func=WolframAlphaEndpoint.as_view('wolfram_alpha_api'),

View File

@ -0,0 +1,137 @@
"""Call this endpoint to retrieve the timezone for a given location"""
from dataclasses import asdict
from http import HTTPStatus
from logging import getLogger
from selene.api import PublicEndpoint
from selene.data.geography import CityRepository
ONE_HUNDRED_MILES = 100
_log = getLogger()
class GeolocationEndpoint(PublicEndpoint):
def __init__(self):
super().__init__()
self.device_id = None
self.request_geolocation = None
self.cities = None
self._city_repo = None
@property
def city_repo(self):
"""Lazy load the CityRepository."""
if self._city_repo is None:
self._city_repo = CityRepository(self.db)
return self._city_repo
def get(self):
"""Handle a HTTP GET request."""
self.request_geolocation = self.request.args['location'].lower()
response_geolocation = self._get_geolocation()
return dict(data=response_geolocation), HTTPStatus.OK
def _get_geolocation(self):
"""Try our best to find a geolocation matching the request."""
self._get_cities()
if self.cities:
selected_geolocation = self._select_geolocation_from_cities()
else:
selected_geolocation = self.city_repo.get_biggest_city_in_region(
self.request_geolocation
)
if selected_geolocation is None:
selected_geolocation = self.city_repo.get_biggest_city_in_country(
self.request_geolocation
)
if selected_geolocation is not None:
selected_geolocation.latitude = float(
selected_geolocation.latitude
)
selected_geolocation.longitude = float(
selected_geolocation.longitude
)
return selected_geolocation
def _get_cities(self):
"""Retrieve a list of cities matching the requested location.
City names can be a single word (e.g. Seattle) or multiple words
(e.g. Kansas City). Query the database for all permutations of words
in the location passed in the request. For example, a request for
"Kansas City Missouri" will pass "Kansas" and "Kansas City" and
"Kansas City Missouri"
This logic assumes that it will not find a match when a city and
region/country are included in the request. For example, a request for
"Kansas City Missouri" should only find a match for "Kansas City".
"""
possible_city_names = []
geolocation_words = self.request_geolocation.split()
for index, word in enumerate(geolocation_words):
possible_city_name = ' '.join(geolocation_words[:index + 1])
possible_city_names.append(possible_city_name)
self.cities = self.city_repo.get_geographic_location_by_city(
possible_city_names
)
def _select_geolocation_from_cities(self):
"""Select one of the cities returned by the database.
If a single match is found, select it. If multiple matches are found,
return the city with the biggest population. If multiple matches are
found and a region or country is included in the requested location,
attempt to match based on the extra criteria.
"""
selected_geolocation = None
if len(self.cities) == 1:
selected_geolocation = self.cities[0]
elif len(self.cities) > 1:
biggest_city = self.cities[0]
if biggest_city.city.lower() == self.request_geolocation:
selected_geolocation = biggest_city
else:
city_in_region = self._get_city_for_requested_region()
city_in_country = self._get_city_for_requested_country()
selected_geolocation = city_in_region or city_in_country
return selected_geolocation
def _get_city_for_requested_region(self):
"""If a region is in the request, get the city in that region.
Example:
A request for "Kansas City Missouri" should return the city of
Kansas City in the state of Missouri
"""
city_in_requested_region = None
for city in self.cities:
location_without_city = self.request_geolocation[len(city.city):]
if city.region.lower() in location_without_city.strip():
city_in_requested_region = city
break
return city_in_requested_region
def _get_city_for_requested_country(self):
"""If a country is in the request, get the city in that country.
Examples:
A request for "Sydney Australia" should return the city of Syndey
in the country of Australia.
"""
selected_city = None
for city in self.cities:
location_without_city = self.request_geolocation[len(city.city):]
if city.country.lower() in location_without_city.strip():
selected_city = city
break
return selected_city

View File

@ -27,3 +27,13 @@ class City(object):
longitude: str
name: str
timezone: str
@dataclass
class GeographicLocation(object):
city: str
country: str
region: str
latitude: str
longitude: str
timezone: str

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from ..entity.city import City
from ..entity.city import City, GeographicLocation
from ...repository_base import RepositoryBase
@ -33,3 +33,29 @@ class CityRepository(RepositoryBase):
db_result = self.cursor.select_all(db_request)
return [City(**row) for row in db_result]
def get_geographic_location_by_city(self, possible_city_names: list):
"""Return a list of all cities matching the list of possibilities"""
city_names = [nm.lower() for nm in possible_city_names]
return self._select_all_into_dataclass(
GeographicLocation,
sql_file_name='get_geographic_location_by_city.sql',
args=dict(possible_city_names=tuple(city_names))
)
def get_biggest_city_in_region(self, region_name):
"""Return the geolocation of the most populous city in a region."""
return self._select_one_into_dataclass(
GeographicLocation,
sql_file_name='get_biggest_city_in_region.sql',
args=dict(region=region_name.lower())
)
def get_biggest_city_in_country(self, country_name):
"""Return the geolocation of the most populous city in a country."""
return self._select_one_into_dataclass(
GeographicLocation,
sql_file_name='get_biggest_city_in_country.sql',
args=dict(country=country_name.lower())
)

View File

@ -0,0 +1,20 @@
SELECT
cty.latitude,
cty.longitude,
cty.name AS city,
cntry.name AS country,
r.name AS region,
t.name AS timezone
FROM
geography.city cty
INNER JOIN geography.region r ON cty.region_id = r.id
INNER JOIN geography.country cntry ON r.country_id = cntry.id
INNER JOIN geography.timezone t ON cty.timezone_id = t.id
WHERE
lower(cntry.name) = %(country)s
AND cty.population IS NOT NULL
ORDER BY
cty.population DESC
LIMIT
1

View File

@ -0,0 +1,20 @@
SELECT
cty.latitude,
cty.longitude,
cty.name AS city,
cntry.name AS country,
r.name AS region,
t.name AS timezone
FROM
geography.city cty
INNER JOIN geography.region r ON cty.region_id = r.id
INNER JOIN geography.country cntry ON r.country_id = cntry.id
INNER JOIN geography.timezone t ON cty.timezone_id = t.id
WHERE
lower(r.name) = %(region)s
AND cty.population IS NOT NULL
ORDER BY
cty.population DESC
LIMIT
1

View File

@ -0,0 +1,17 @@
SELECT
cty.latitude,
cty.longitude,
cty.name AS city,
cntry.name AS country,
r.name AS region,
t.name AS timezone
FROM
geography.city cty
INNER JOIN geography.region r ON cty.region_id = r.id
INNER JOIN geography.country cntry ON r.country_id = cntry.id
INNER JOIN geography.timezone t ON cty.timezone_id = t.id
WHERE
lower(cty.name) IN %(possible_city_names)s
ORDER BY
cty.population DESC