From 8e0f6c53ea37d6bdb687ba3fcd431f6f357eedfb Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Wed, 25 Sep 2019 17:26:29 -0500 Subject: [PATCH 1/7] new public API endpoint to extrapolate a geolocation from a string. --- api/public/Pipfile | 2 +- api/public/Pipfile.lock | 133 +++++++++--------- api/public/public_api/api.py | 16 +-- .../public_api/endpoints/geolocation.py | 131 +++++++++++++++++ shared/selene/data/geography/entity/city.py | 10 ++ .../selene/data/geography/repository/city.py | 28 +++- .../sql/get_biggest_city_in_country.sql | 20 +++ .../sql/get_biggest_city_in_region.sql | 20 +++ .../sql/get_geographic_location_by_city.sql | 17 +++ 9 files changed, 301 insertions(+), 76 deletions(-) create mode 100644 api/public/public_api/endpoints/geolocation.py create mode 100644 shared/selene/data/geography/repository/sql/get_biggest_city_in_country.sql create mode 100644 shared/selene/data/geography/repository/sql/get_biggest_city_in_region.sql create mode 100644 shared/selene/data/geography/repository/sql/get_geographic_location_by_city.sql diff --git a/api/public/Pipfile b/api/public/Pipfile index 32a06164..315b583a 100644 --- a/api/public/Pipfile +++ b/api/public/Pipfile @@ -8,7 +8,7 @@ behave = "*" pyhamcrest = "*" [packages] -flask = "*" +flask = "<1.1" requests = "*" selene = {editable = true,path = "./../../shared"} SpeechRecognition = "*" diff --git a/api/public/Pipfile.lock b/api/public/Pipfile.lock index ab5909fd..e2b6ebc7 100644 --- a/api/public/Pipfile.lock +++ b/api/public/Pipfile.lock @@ -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": [ diff --git a/api/public/public_api/api.py b/api/public/public_api/api.py index f8e5ca67..5e4ca031 100644 --- a/api/public/public_api/api.py +++ b/api/public/public_api/api.py @@ -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//skill/', view_func=DeviceSkillSettingsEndpoint.as_view('device_skill_delete_api'), methods=['DELETE'] ) - public.add_url_rule( '/v1/device//skill', view_func=DeviceSkillSettingsEndpoint.as_view('device_skill_api'), methods=['GET', 'PUT'] ) - public.add_url_rule( '/v1/device//skill/settings', view_func=DeviceSkillSettingsEndpointV2.as_view('skill_settings_api'), methods=['GET'] ) - public.add_url_rule( '/v1/device//settingsMeta', view_func=SkillSettingsMetaEndpoint.as_view('device_user_skill_api'), methods=['PUT'] ) - public.add_url_rule( '/v1/device/', view_func=DeviceEndpoint.as_view('device_api'), methods=['GET', 'PATCH'] ) - public.add_url_rule( '/v1/device//setting', view_func=DeviceSettingEndpoint.as_view('device_settings_api'), methods=['GET'] ) - public.add_url_rule( '/v1/device//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) diff --git a/api/public/public_api/endpoints/geolocation.py b/api/public/public_api/endpoints/geolocation.py new file mode 100644 index 00000000..b52a29d7 --- /dev/null +++ b/api/public/public_api/endpoints/geolocation.py @@ -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 diff --git a/shared/selene/data/geography/entity/city.py b/shared/selene/data/geography/entity/city.py index 30259a4b..c7d948ec 100644 --- a/shared/selene/data/geography/entity/city.py +++ b/shared/selene/data/geography/entity/city.py @@ -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 diff --git a/shared/selene/data/geography/repository/city.py b/shared/selene/data/geography/repository/city.py index d146658d..e0b20343 100644 --- a/shared/selene/data/geography/repository/city.py +++ b/shared/selene/data/geography/repository/city.py @@ -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()) + ) diff --git a/shared/selene/data/geography/repository/sql/get_biggest_city_in_country.sql b/shared/selene/data/geography/repository/sql/get_biggest_city_in_country.sql new file mode 100644 index 00000000..f1c7f988 --- /dev/null +++ b/shared/selene/data/geography/repository/sql/get_biggest_city_in_country.sql @@ -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 + diff --git a/shared/selene/data/geography/repository/sql/get_biggest_city_in_region.sql b/shared/selene/data/geography/repository/sql/get_biggest_city_in_region.sql new file mode 100644 index 00000000..611c7977 --- /dev/null +++ b/shared/selene/data/geography/repository/sql/get_biggest_city_in_region.sql @@ -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 + diff --git a/shared/selene/data/geography/repository/sql/get_geographic_location_by_city.sql b/shared/selene/data/geography/repository/sql/get_geographic_location_by_city.sql new file mode 100644 index 00000000..e460047f --- /dev/null +++ b/shared/selene/data/geography/repository/sql/get_geographic_location_by_city.sql @@ -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 + From 71a78b8d556e1c0724418932e0481cd2708e4ce4 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Thu, 26 Sep 2019 12:03:13 -0500 Subject: [PATCH 2/7] renamed geolocation endpoint url --- api/public/public_api/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/public/public_api/api.py b/api/public/public_api/api.py index 5e4ca031..1747c2a2 100644 --- a/api/public/public_api/api.py +++ b/api/public/public_api/api.py @@ -74,7 +74,7 @@ public.add_url_rule( methods=['GET'] ) public.add_url_rule( - '/v1/location', + '/v1/geolocation', view_func=GeolocationEndpoint.as_view('location_api'), methods=['GET'] ) From 41789eca3f36d7b81db825ef5a166fd80aab0e25 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Thu, 26 Sep 2019 16:12:40 -0500 Subject: [PATCH 3/7] un-comment the token check that was commented for testing --- api/public/public_api/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/public/public_api/api.py b/api/public/public_api/api.py index 1747c2a2..dd2521d2 100644 --- a/api/public/public_api/api.py +++ b/api/public/public_api/api.py @@ -163,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) From a113282ec10f6be9b169017ed3917d5699cf624b Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Thu, 26 Sep 2019 16:13:24 -0500 Subject: [PATCH 4/7] better handle requests in lower-case characters --- api/public/public_api/endpoints/geolocation.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/api/public/public_api/endpoints/geolocation.py b/api/public/public_api/endpoints/geolocation.py index b52a29d7..6a111410 100644 --- a/api/public/public_api/endpoints/geolocation.py +++ b/api/public/public_api/endpoints/geolocation.py @@ -1,12 +1,15 @@ """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): @@ -89,7 +92,7 @@ class GeolocationEndpoint(PublicEndpoint): selected_geolocation = self.cities[0] elif len(self.cities) > 1: biggest_city = self.cities[0] - if biggest_city.city == self.request_geolocation: + if biggest_city.city.lower() == self.request_geolocation.lower(): selected_geolocation = biggest_city else: city_in_region = self._get_city_for_requested_region() @@ -108,7 +111,7 @@ class GeolocationEndpoint(PublicEndpoint): 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(): + if city.region.lower() in location_without_city.strip().lower(): city_in_requested_region = city break @@ -124,7 +127,7 @@ class GeolocationEndpoint(PublicEndpoint): 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(): + if city.country.lower() in location_without_city.strip().lower(): selected_city = city break From 06c8afe45be21eb8343763a14d27e72f3b91e311 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Thu, 26 Sep 2019 16:16:13 -0500 Subject: [PATCH 5/7] minor refactor --- api/public/public_api/endpoints/geolocation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/public/public_api/endpoints/geolocation.py b/api/public/public_api/endpoints/geolocation.py index 6a111410..9086ef8c 100644 --- a/api/public/public_api/endpoints/geolocation.py +++ b/api/public/public_api/endpoints/geolocation.py @@ -29,7 +29,7 @@ class GeolocationEndpoint(PublicEndpoint): def get(self): """Handle a HTTP GET request.""" - self.request_geolocation = self.request.args['location'] + self.request_geolocation = self.request.args['location'].lower() response_geolocation = self._get_geolocation() return dict(data=response_geolocation), HTTPStatus.OK @@ -92,7 +92,7 @@ class GeolocationEndpoint(PublicEndpoint): selected_geolocation = self.cities[0] elif len(self.cities) > 1: biggest_city = self.cities[0] - if biggest_city.city.lower() == self.request_geolocation.lower(): + if biggest_city.city.lower() == self.request_geolocation: selected_geolocation = biggest_city else: city_in_region = self._get_city_for_requested_region() @@ -111,7 +111,7 @@ class GeolocationEndpoint(PublicEndpoint): 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().lower(): + if city.region.lower() in location_without_city.strip(): city_in_requested_region = city break @@ -127,7 +127,7 @@ class GeolocationEndpoint(PublicEndpoint): 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().lower(): + if city.country.lower() in location_without_city.strip(): selected_city = city break From 97e003d41c199893ec680c5e75c2901e41e17c8e Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Mon, 21 Oct 2019 20:36:05 -0500 Subject: [PATCH 6/7] removed code that prevented geographic coordinates from being passed to core. the weather skill uses coordinates to make API calls. --- api/public/public_api/endpoints/geolocation.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/api/public/public_api/endpoints/geolocation.py b/api/public/public_api/endpoints/geolocation.py index 9086ef8c..2a659aac 100644 --- a/api/public/public_api/endpoints/geolocation.py +++ b/api/public/public_api/endpoints/geolocation.py @@ -49,11 +49,6 @@ class GeolocationEndpoint(PublicEndpoint): 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): From d34bdd9692898133795b0e254d34469c52f92e78 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Wed, 6 Nov 2019 14:33:28 -0600 Subject: [PATCH 7/7] converted decimals to float for serialization --- api/public/public_api/endpoints/geolocation.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/public/public_api/endpoints/geolocation.py b/api/public/public_api/endpoints/geolocation.py index 2a659aac..db92c1a7 100644 --- a/api/public/public_api/endpoints/geolocation.py +++ b/api/public/public_api/endpoints/geolocation.py @@ -49,6 +49,14 @@ class GeolocationEndpoint(PublicEndpoint): 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):