Merge pull request #198 from MycroftAI/feature/timezone_endpoint
Feature/timezone endpointpull/199/head^2
commit
30c27fc23f
|
@ -8,7 +8,7 @@ behave = "*"
|
||||||
pyhamcrest = "*"
|
pyhamcrest = "*"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
flask = "*"
|
flask = "<1.1"
|
||||||
requests = "*"
|
requests = "*"
|
||||||
selene = {editable = true,path = "./../../shared"}
|
selene = {editable = true,path = "./../../shared"}
|
||||||
SpeechRecognition = "*"
|
SpeechRecognition = "*"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "d0f209f40deb313debbdf78ca1d590913f454e8cb98e2be96447acd0f00043df"
|
"sha256": "3e875b3d36c7ad28b2c052508172f60330e1c9f5e5e8edd668ba5677adae750a"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -18,10 +18,10 @@
|
||||||
"default": {
|
"default": {
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
|
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
|
||||||
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
|
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
|
||||||
],
|
],
|
||||||
"version": "==2019.6.16"
|
"version": "==2019.9.11"
|
||||||
},
|
},
|
||||||
"chardet": {
|
"chardet": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -53,11 +53,11 @@
|
||||||
},
|
},
|
||||||
"flask": {
|
"flask": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
|
"sha256:1a21ccca71cee5e55b6a367cc48c6eb47e3c447f76e64d41f3f3f931c17e7c96",
|
||||||
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
|
"sha256:ed1330220a321138de53ec7c534c3d90cf2f7af938c7880fc3da13aa46bf870f"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.1.1"
|
"version": "==1.0.4"
|
||||||
},
|
},
|
||||||
"idna": {
|
"idna": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -175,16 +175,18 @@
|
||||||
},
|
},
|
||||||
"python-http-client": {
|
"python-http-client": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7e430f4b9dd2b621b0051f6a362f103447ea8e267594c602a5c502a0c694ee38"
|
"sha256:01f58f1871612fdce6a4545df7c867a6d1457695652a7ca48d5c22e5bf57628d",
|
||||||
|
"sha256:c2776054245db376ea26c859b80e9280b1a470b96ed998d60d35951f89bbbe79",
|
||||||
|
"sha256:e455ae0dfd5819ac483f7fecf08ab8693048d9dc47a0a6fe0d4aebf86d9d1d17"
|
||||||
],
|
],
|
||||||
"version": "==3.1.0"
|
"version": "==3.2.1"
|
||||||
},
|
},
|
||||||
"redis": {
|
"redis": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0607faf60d44768e17f65e506fe390679b54be6fd6d5f0c2d28f3ebf4f0535e7",
|
"sha256:98a22fb750c9b9bb46e75e945dc3f61d0ab30d06117cbb21ff9cd1d315fedd3b",
|
||||||
"sha256:9c96c5bf11a8c47eb33cefdefd41c47cf1ff68db41c51b56b3ec7938b7c627f7"
|
"sha256:c504251769031b0dd7dd5cf786050a6050197c6de0d37778c80c08cb04ae8275"
|
||||||
],
|
],
|
||||||
"version": "==3.3.7"
|
"version": "==3.3.8"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -214,10 +216,11 @@
|
||||||
},
|
},
|
||||||
"sendgrid": {
|
"sendgrid": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:297d33363a70df9b39419210e1273b165d487730e85c495695e0015bc626db71",
|
"sha256:a9999878aad90e32d7b62464454adc70bcef40085c729355ea58717bb0ea0dbd",
|
||||||
"sha256:8b82c8c801dde8180a567913a9f80d8a63f38e39f209edde302b6df899b4bca1"
|
"sha256:cb0b21a83a54bc99d9befda1ea7b4f15fe8db362a152458e58abc5ce23d6d828",
|
||||||
|
"sha256:f04fee009c750b47ab984f3c4a735facacc7fba902052d597f7e60b601e56bcc"
|
||||||
],
|
],
|
||||||
"version": "==6.0.5"
|
"version": "==6.1.0"
|
||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -235,18 +238,18 @@
|
||||||
},
|
},
|
||||||
"stripe": {
|
"stripe": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:344cd691a542f08c508b9d12ac201da46b7f0f21a0a7f72f56199b3baee795eb",
|
"sha256:f5b27b45bb5d7fe8c7e524a2bd4372fbf32e5e2d42aafa8e84802801faff28d2",
|
||||||
"sha256:e07efa567ae0831fe351ddb49de074aa1681569fd234d4f1dc0a9f7f4c017820"
|
"sha256:f80e76dc17ead135a992fd9b03ee4ef3a49a958501d482f8fd11431ba3287870"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.35.0"
|
"version": "==2.36.2"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
|
||||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
|
||||||
],
|
],
|
||||||
"version": "==1.25.3"
|
"version": "==1.25.6"
|
||||||
},
|
},
|
||||||
"uwsgi": {
|
"uwsgi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -257,10 +260,10 @@
|
||||||
},
|
},
|
||||||
"werkzeug": {
|
"werkzeug": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4",
|
"sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7",
|
||||||
"sha256:a13b74dd3c45f758d4ebdb224be8f1ab8ef58b3c0ffc1783a8c7d9f4f50227e6"
|
"sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"
|
||||||
],
|
],
|
||||||
"version": "==0.15.5"
|
"version": "==0.16.0"
|
||||||
},
|
},
|
||||||
"wrapt": {
|
"wrapt": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -280,9 +283,9 @@
|
||||||
},
|
},
|
||||||
"parse": {
|
"parse": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1b68657434d371e5156048ca4a0c5aea5afc6ca59a2fea4dd1a575354f617142"
|
"sha256:a5fca7000c6588d77bc65c28f3f21bfce03b5e44daa8f9f07c17fe364990d717"
|
||||||
],
|
],
|
||||||
"version": "==1.12.0"
|
"version": "==1.12.1"
|
||||||
},
|
},
|
||||||
"parse-type": {
|
"parse-type": {
|
||||||
"hashes": [
|
"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 DeviceSkillSettingsEndpoint
|
||||||
from .endpoints.device_skill_settings import DeviceSkillSettingsEndpointV2
|
from .endpoints.device_skill_settings import DeviceSkillSettingsEndpointV2
|
||||||
from .endpoints.device_subscription import DeviceSubscriptionEndpoint
|
from .endpoints.device_subscription import DeviceSubscriptionEndpoint
|
||||||
|
from .endpoints.geolocation import GeolocationEndpoint
|
||||||
from .endpoints.google_stt import GoogleSTTEndpoint
|
from .endpoints.google_stt import GoogleSTTEndpoint
|
||||||
from .endpoints.oauth_callback import OauthCallbackEndpoint
|
from .endpoints.oauth_callback import OauthCallbackEndpoint
|
||||||
from .endpoints.open_weather_map import OpenWeatherMapEndpoint
|
from .endpoints.open_weather_map import OpenWeatherMapEndpoint
|
||||||
|
@ -54,51 +55,48 @@ public = Flask(__name__)
|
||||||
public.config.from_object(get_base_config())
|
public.config.from_object(get_base_config())
|
||||||
public.config['GOOGLE_STT_KEY'] = os.environ['GOOGLE_STT_KEY']
|
public.config['GOOGLE_STT_KEY'] = os.environ['GOOGLE_STT_KEY']
|
||||||
public.config['SELENE_CACHE'] = SeleneCache()
|
public.config['SELENE_CACHE'] = SeleneCache()
|
||||||
|
|
||||||
public.response_class = SeleneResponse
|
public.response_class = SeleneResponse
|
||||||
public.register_blueprint(selene_api)
|
public.register_blueprint(selene_api)
|
||||||
|
|
||||||
public.add_url_rule(
|
public.add_url_rule(
|
||||||
'/v1/device/<string:device_id>/skill/<string:skill_gid>',
|
'/v1/device/<string:device_id>/skill/<string:skill_gid>',
|
||||||
view_func=DeviceSkillSettingsEndpoint.as_view('device_skill_delete_api'),
|
view_func=DeviceSkillSettingsEndpoint.as_view('device_skill_delete_api'),
|
||||||
methods=['DELETE']
|
methods=['DELETE']
|
||||||
)
|
)
|
||||||
|
|
||||||
public.add_url_rule(
|
public.add_url_rule(
|
||||||
'/v1/device/<string:device_id>/skill',
|
'/v1/device/<string:device_id>/skill',
|
||||||
view_func=DeviceSkillSettingsEndpoint.as_view('device_skill_api'),
|
view_func=DeviceSkillSettingsEndpoint.as_view('device_skill_api'),
|
||||||
methods=['GET', 'PUT']
|
methods=['GET', 'PUT']
|
||||||
)
|
)
|
||||||
|
|
||||||
public.add_url_rule(
|
public.add_url_rule(
|
||||||
'/v1/device/<string:device_id>/skill/settings',
|
'/v1/device/<string:device_id>/skill/settings',
|
||||||
view_func=DeviceSkillSettingsEndpointV2.as_view('skill_settings_api'),
|
view_func=DeviceSkillSettingsEndpointV2.as_view('skill_settings_api'),
|
||||||
methods=['GET']
|
methods=['GET']
|
||||||
)
|
)
|
||||||
|
|
||||||
public.add_url_rule(
|
public.add_url_rule(
|
||||||
'/v1/device/<string:device_id>/settingsMeta',
|
'/v1/device/<string:device_id>/settingsMeta',
|
||||||
view_func=SkillSettingsMetaEndpoint.as_view('device_user_skill_api'),
|
view_func=SkillSettingsMetaEndpoint.as_view('device_user_skill_api'),
|
||||||
methods=['PUT']
|
methods=['PUT']
|
||||||
)
|
)
|
||||||
|
|
||||||
public.add_url_rule(
|
public.add_url_rule(
|
||||||
'/v1/device/<string:device_id>',
|
'/v1/device/<string:device_id>',
|
||||||
view_func=DeviceEndpoint.as_view('device_api'),
|
view_func=DeviceEndpoint.as_view('device_api'),
|
||||||
methods=['GET', 'PATCH']
|
methods=['GET', 'PATCH']
|
||||||
)
|
)
|
||||||
|
|
||||||
public.add_url_rule(
|
public.add_url_rule(
|
||||||
'/v1/device/<string:device_id>/setting',
|
'/v1/device/<string:device_id>/setting',
|
||||||
view_func=DeviceSettingEndpoint.as_view('device_settings_api'),
|
view_func=DeviceSettingEndpoint.as_view('device_settings_api'),
|
||||||
methods=['GET']
|
methods=['GET']
|
||||||
)
|
)
|
||||||
|
|
||||||
public.add_url_rule(
|
public.add_url_rule(
|
||||||
'/v1/device/<string:device_id>/subscription',
|
'/v1/device/<string:device_id>/subscription',
|
||||||
view_func=DeviceSubscriptionEndpoint.as_view('device_subscription_api'),
|
view_func=DeviceSubscriptionEndpoint.as_view('device_subscription_api'),
|
||||||
methods=['GET']
|
methods=['GET']
|
||||||
)
|
)
|
||||||
|
public.add_url_rule(
|
||||||
|
'/v1/geolocation',
|
||||||
|
view_func=GeolocationEndpoint.as_view('location_api'),
|
||||||
|
methods=['GET']
|
||||||
|
)
|
||||||
public.add_url_rule(
|
public.add_url_rule(
|
||||||
'/v1/wa',
|
'/v1/wa',
|
||||||
view_func=WolframAlphaEndpoint.as_view('wolfram_alpha_api'),
|
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
|
longitude: str
|
||||||
name: str
|
name: str
|
||||||
timezone: 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
|
# 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/>.
|
# 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
|
from ...repository_base import RepositoryBase
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,3 +33,29 @@ class CityRepository(RepositoryBase):
|
||||||
db_result = self.cursor.select_all(db_request)
|
db_result = self.cursor.select_all(db_request)
|
||||||
|
|
||||||
return [City(**row) for row in db_result]
|
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