Merge remote-tracking branch 'remotes/origin/dev' into bug/stt-stats
commit
d6f4f74e25
|
@ -8,7 +8,7 @@ behave = "*"
|
|||
pyhamcrest = "*"
|
||||
|
||||
[packages]
|
||||
flask = "*"
|
||||
flask = "<1.1"
|
||||
requests = "*"
|
||||
selene = {editable = true,path = "./../../shared"}
|
||||
SpeechRecognition = "*"
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
Loading…
Reference in New Issue