new public API endpoint to extrapolate a geolocation from a string.

pull/198/head
Chris Veilleux 2019-09-25 17:26:29 -05:00
parent b2b99e9dbd
commit 8e0f6c53ea
9 changed files with 301 additions and 76 deletions

View File

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

133
api/public/Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "d0f209f40deb313debbdf78ca1d590913f454e8cb98e2be96447acd0f00043df"
"sha256": "3e875b3d36c7ad28b2c052508172f60330e1c9f5e5e8edd668ba5677adae750a"
},
"pipfile-spec": 6,
"requires": {
@ -18,10 +18,10 @@
"default": {
"certifi": {
"hashes": [
"sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
"sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
],
"version": "==2019.3.9"
"version": "==2019.9.11"
},
"chardet": {
"hashes": [
@ -39,10 +39,10 @@
},
"deprecated": {
"hashes": [
"sha256:2f293eb0eee34b1fcf3da530fe8fc4b0d71d43ddc2dc78e2ffb444b6c0868557",
"sha256:749f6cdcfbdc3f79258f8154bad43fced95adc632c337675d0385959895894bc"
"sha256:a515c4cf75061552e0284d123c3066fbbe398952c87333a92b8fc3dd8e4f9cc1",
"sha256:b07b414c8aac88f60c1d837d21def7e83ba711052e03b3cbaff27972567a8f8d"
],
"version": "==1.2.5"
"version": "==1.2.6"
},
"facebook-sdk": {
"hashes": [
@ -53,11 +53,11 @@
},
"flask": {
"hashes": [
"sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3",
"sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"
"sha256:1a21ccca71cee5e55b6a367cc48c6eb47e3c447f76e64d41f3f3f931c17e7c96",
"sha256:ed1330220a321138de53ec7c534c3d90cf2f7af938c7880fc3da13aa46bf870f"
],
"index": "pypi",
"version": "==1.0.3"
"version": "==1.0.4"
},
"idna": {
"hashes": [
@ -122,42 +122,42 @@
},
"psycopg2-binary": {
"hashes": [
"sha256:007ca0df127b1862fc010125bc4100b7a630efc6841047bd11afceadb4754611",
"sha256:03c49e02adf0b4d68f422fdbd98f7a7c547beb27e99a75ed02298f85cb48406a",
"sha256:0a1232cdd314e08848825edda06600455ad2a7adaa463ebfb12ece2d09f3370e",
"sha256:131c80d0958c89273d9720b9adf9df1d7600bb3120e16019a7389ab15b079af5",
"sha256:2de34cc3b775724623f86617d2601308083176a495f5b2efc2bbb0da154f483a",
"sha256:2eddc31500f73544a2a54123d4c4b249c3c711d31e64deddb0890982ea37397a",
"sha256:484f6c62bdc166ee0e5be3aa831120423bf399786d1f3b0304526c86180fbc0b",
"sha256:4c2d9369ed40b4a44a8ccd6bc3a7db6272b8314812d2d1091f95c4c836d92e06",
"sha256:70f570b5fa44413b9f30dbc053d17ef3ce6a4100147a10822f8662e58d473656",
"sha256:7a2b5b095f3bd733aab101c89c0e1a3f0dfb4ebdc26f6374805c086ffe29d5b2",
"sha256:804914a669186e2843c1f7fbe12b55aad1b36d40a28274abe6027deffad9433d",
"sha256:8520c03172da18345d012949a53617a963e0191ccb3c666f23276d5326af27b5",
"sha256:90da901fc33ea393fc644607e4a3916b509387e9339ec6ebc7bfded45b7a0ae9",
"sha256:a582416ad123291a82c300d1d872bdc4136d69ad0b41d57dc5ca3df7ef8e3088",
"sha256:ac8c5e20309f4989c296d62cac20ee456b69c41fd1bc03829e27de23b6fa9dd0",
"sha256:b2cf82f55a619879f8557fdaae5cec7a294fac815e0087c4f67026fdf5259844",
"sha256:b59d6f8cfca2983d8fdbe457bf95d2192f7b7efdb2b483bf5fa4e8981b04e8b2",
"sha256:be08168197021d669b9964bd87628fa88f910b1be31e7010901070f2540c05fd",
"sha256:be0f952f1c365061041bad16e27e224e29615d4eb1fb5b7e7760a1d3d12b90b6",
"sha256:c1c9a33e46d7c12b9c96cf2d4349d783e3127163fd96254dcd44663cf0a1d438",
"sha256:d18c89957ac57dd2a2724ecfe9a759912d776f96ecabba23acb9ecbf5c731035",
"sha256:d7e7b0ff21f39433c50397e60bf0995d078802c591ca3b8d99857ea18a7496ee",
"sha256:da0929b2bf0d1f365345e5eb940d8713c1d516312e010135b14402e2a3d2404d",
"sha256:de24a4962e361c512d3e528ded6c7480eab24c655b8ca1f0b761d3b3650d2f07",
"sha256:e45f93ff3f7dae2202248cf413a87aeb330821bf76998b3cf374eda2fc893dd7",
"sha256:f046aeae1f7a845041b8661bb7a52449202b6c5d3fb59eb4724e7ca088811904",
"sha256:f1dc2b7b2748084b890f5d05b65a47cd03188824890e9a60818721fd492249fb",
"sha256:fcbe7cf3a786572b73d2cd5f34ed452a5f5fac47c9c9d1e0642c457a148f9f88"
"sha256:080c72714784989474f97be9ab0ddf7b2ad2984527e77f2909fcd04d4df53809",
"sha256:110457be80b63ff4915febb06faa7be002b93a76e5ba19bf3f27636a2ef58598",
"sha256:171352a03b22fc099f15103959b52ee77d9a27e028895d7e5fde127aa8e3bac5",
"sha256:19d013e7b0817087517a4b3cab39c084d78898369e5c46258aab7be4f233d6a1",
"sha256:249b6b21ae4eb0f7b8423b330aa80fab5f821b9ffc3f7561a5e2fd6bb142cf5d",
"sha256:2ac0731d2d84b05c7bb39e85b7e123c3a0acd4cda631d8d542802c88deb9e87e",
"sha256:2b6d561193f0dc3f50acfb22dd52ea8c8dfbc64bcafe3938b5f209cc17cb6f00",
"sha256:2bd23e242e954214944481124755cbefe7c2cf563b1a54cd8d196d502f2578bf",
"sha256:3e1239242ca60b3725e65ab2f13765fc199b03af9eaf1b5572f0e97bdcee5b43",
"sha256:3eb70bb697abbe86b1d2b1316370c02ba320bfd1e9e35cf3b9566a855ea8e4e5",
"sha256:51a2fc7e94b98bd1bb5d4570936f24fc2b0541b63eccadf8fdea266db8ad2f70",
"sha256:52f1bdafdc764b7447e393ed39bb263eccb12bfda25a4ac06d82e3a9056251f6",
"sha256:5b3581319a3951f1e866f4f6c5e42023db0fae0284273b82e97dfd32c51985cd",
"sha256:63c1b66e3b2a3a336288e4bcec499e0dc310cd1dceaed1c46fa7419764c68877",
"sha256:8123a99f24ecee469e5c1339427bcdb2a33920a18bb5c0d58b7c13f3b0298ba3",
"sha256:85e699fcabe7f817c0f0a412d4e7c6627e00c412b418da7666ff353f38e30f67",
"sha256:8dbff4557bbef963697583366400822387cccf794ccb001f1f2307ed21854c68",
"sha256:908d21d08d6b81f1b7e056bbf40b2f77f8c499ab29e64ec5113052819ef1c89b",
"sha256:af39d0237b17d0a5a5f638e9dffb34013ce2b1d41441fd30283e42b22d16858a",
"sha256:af51bb9f055a3f4af0187149a8f60c9d516cf7d5565b3dac53358796a8fb2a5b",
"sha256:b2ecac57eb49e461e86c092761e6b8e1fd9654dbaaddf71a076dcc869f7014e2",
"sha256:cd37cc170678a4609becb26b53a2bc1edea65177be70c48dd7b39a1149cabd6e",
"sha256:d17e3054b17e1a6cb8c1140f76310f6ede811e75b7a9d461922d2c72973f583e",
"sha256:d305313c5a9695f40c46294d4315ed3a07c7d2b55e48a9010dad7db7a66c8b7f",
"sha256:dd0ef0eb1f7dd18a3f4187226e226a7284bda6af5671937a221766e6ef1ee88f",
"sha256:e1adff53b56db9905db48a972fb89370ad5736e0450b96f91bcf99cadd96cfd7",
"sha256:f0d43828003c82dbc9269de87aa449e9896077a71954fbbb10a614c017e65737",
"sha256:f78e8b487de4d92640105c1389e5b90be3496b1d75c90a666edd8737cc2dbab7"
],
"version": "==2.8.2"
"version": "==2.8.3"
},
"pygithub": {
"hashes": [
"sha256:2ce91d4990efbfb7a0a1abd06e2106a3998a4a5810a4bdd3c1b6be5499918e9b"
"sha256:db415a5aeb5ab1e4a3263b1a091b4f9ffbd85a12a06a0303d5bf083ce7c1b2c8"
],
"version": "==1.43.7"
"version": "==1.43.8"
},
"pyhamcrest": {
"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:6946b5dca72e86103edc8033019cc3814c031232d339d5f4533b02ea85685175",
"sha256:8ca418d2ddca1b1a850afa1680a7d2fd1f3322739271de4b704e0d4668449273"
"sha256:98a22fb750c9b9bb46e75e945dc3f61d0ab30d06117cbb21ff9cd1d315fedd3b",
"sha256:c504251769031b0dd7dd5cf786050a6050197c6de0d37778c80c08cb04ae8275"
],
"version": "==3.2.1"
"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:362472a0b69f1791629d75c4829be3af92dc8c9254812af5670dcbcde704bb1f",
"sha256:92d6691382e0abf314759863c48a4830b5bfd3a935193ee6c3e5621ab25740ba"
"sha256:f5b27b45bb5d7fe8c7e524a2bd4372fbf32e5e2d42aafa8e84802801faff28d2",
"sha256:f80e76dc17ead135a992fd9b03ee4ef3a49a958501d482f8fd11431ba3287870"
],
"index": "pypi",
"version": "==2.29.4"
"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,16 +260,16 @@
},
"werkzeug": {
"hashes": [
"sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c",
"sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6"
"sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7",
"sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"
],
"version": "==0.15.4"
"version": "==0.16.0"
},
"wrapt": {
"hashes": [
"sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533"
"sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"
],
"version": "==1.11.1"
"version": "==1.11.2"
}
},
"develop": {
@ -280,16 +283,16 @@
},
"parse": {
"hashes": [
"sha256:1b68657434d371e5156048ca4a0c5aea5afc6ca59a2fea4dd1a575354f617142"
"sha256:a5fca7000c6588d77bc65c28f3f21bfce03b5e44daa8f9f07c17fe364990d717"
],
"version": "==1.12.0"
"version": "==1.12.1"
},
"parse-type": {
"hashes": [
"sha256:6e906a66f340252e4c324914a60d417d33a4bea01292ea9bbf68b4fc123be8c9",
"sha256:f596bdc75d3dd93036fbfe3d04127da9f6df0c26c36e01e76da85adef4336b3c"
"sha256:089a471b06327103865dfec2dd844230c3c658a4a1b5b4c8b6c16c8f77577f9e",
"sha256:7f690b18d35048c15438d6d0571f9045cffbec5907e0b1ccf006f889e3a38c0b"
],
"version": "==0.4.2"
"version": "==0.5.2"
},
"pyhamcrest": {
"hashes": [

View File

@ -21,6 +21,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
@ -35,51 +36,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/location',
view_func=GeolocationEndpoint.as_view('location_api'),
methods=['GET']
)
public.add_url_rule(
'/v1/wa',
view_func=WolframAlphaEndpoint.as_view('wolfram_alpha_api'),
@ -165,4 +163,4 @@ public.add_url_rule(
# path. Use case: GET /device/{uuid} with empty uuid. Core today uses the 401
# to validate if it needs to perform a pairing process. We should fix that in a
# future version because we have to return 404 when we call a non existent path
public.before_request(check_oauth_token)
# public.before_request(check_oauth_token)

View File

@ -0,0 +1,131 @@
"""Call this endpoint to retrieve the timezone for a given location"""
from dataclasses import asdict
from http import HTTPStatus
from selene.api import PublicEndpoint
from selene.data.geography import CityRepository
ONE_HUNDRED_MILES = 100
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']
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 = asdict(selected_geolocation)
del(selected_geolocation['longitude'])
del(selected_geolocation['latitude'])
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 == 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 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 in location_without_city.strip():
selected_city = city
break
return selected_city

View File

@ -8,3 +8,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

@ -1,4 +1,4 @@
from ..entity.city import City
from ..entity.city import City, GeographicLocation
from ...repository_base import RepositoryBase
@ -14,3 +14,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