├── tests ├── __init__.py ├── conftest.py ├── utils_test.py ├── network_test.py ├── options_test.py ├── integration_test.py └── speedtest_test.py ├── requirements.txt ├── requirements-dev.txt ├── setup.py ├── tox.ini ├── .gitignore ├── cf_speedtest ├── __init__.py ├── __main__.py ├── options.py ├── speedtest.py └── locations.py ├── .github └── workflows │ └── main.yml ├── LICENSE ├── setup.cfg ├── .pre-commit-config.yaml └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests[socks] 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pre-commit 2 | pytest 3 | requests[socks] 4 | types-requests 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from setuptools import setup 4 | setup() 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py37,py38,py39,py310,py311,py312 3 | 4 | [testenv] 5 | deps = pytest 6 | commands = 7 | pytest 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .vscode 3 | /cf_speedtest.egg-info 4 | /dist 5 | /build 6 | /.tox/dist 7 | /.tox/log 8 | /cf_speedtest/utime.exe 9 | -------------------------------------------------------------------------------- /cf_speedtest/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | if sys.hexversion < 0x3060000: 6 | sys.exit('Python 3.6+ required') 7 | -------------------------------------------------------------------------------- /cf_speedtest/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from cf_speedtest.speedtest import main 4 | 5 | if __name__ == '__main__': 6 | raise SystemExit(main()) 7 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | 7 | 8 | @pytest.fixture 9 | def mock_time(): 10 | with patch('cf_speedtest.speedtest.time') as mock_time: 11 | mock_time.time.return_value = 1234567890.0 12 | yield mock_time 13 | 14 | 15 | @pytest.fixture 16 | def mock_requests_session(): 17 | with patch('cf_speedtest.speedtest.REQ_SESSION') as mock_session: 18 | yield mock_session 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | main: 10 | strategy: 11 | matrix: 12 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] 13 | os: [ubuntu-latest] 14 | arch: [x64] 15 | 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: install tox 23 | run: python -m pip install --upgrade tox 24 | - name: install cf_speedtest dependencies 25 | run: python -m pip install -r requirements.txt 26 | - name: run tox 27 | run: tox -e py 28 | -------------------------------------------------------------------------------- /tests/utils_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from cf_speedtest import speedtest 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 'data, percentile, expected', [ 10 | ([1, 2, 3, 4, 5], 50, 3), 11 | ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 90, 9), 12 | ([1, 1, 1, 1, 1], 100, 1), 13 | ([1, 2, 3, 4, 5], 0, 1), 14 | ], 15 | ) 16 | def test_percentile(data, percentile, expected): 17 | assert speedtest.percentile(data, percentile) == expected 18 | 19 | 20 | @pytest.mark.parametrize( 21 | 'server_timing, expected', [ 22 | ('dur=1234.5', 1.2345), 23 | ('key=value;dur=5678.9', 5.6789), 24 | ('invalid', 0.0), 25 | ('dur=1000', 1.0), 26 | ('start=0;dur=500;desc="Backend"', 0.5), 27 | ], 28 | ) 29 | def test_get_server_timing(server_timing, expected): 30 | assert speedtest.get_server_timing(server_timing) == expected 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 wilbo007 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = cf_speedtest 3 | version = 0.1.9 4 | description = Command-line internet speed test 5 | long_description = README.md 6 | long_description_content_type = text/markdown 7 | author = wilbo007 8 | author_email = wilbo007@protonmail.ch 9 | url = https://github.com/12932/cf-speedtest 10 | license = MIT 11 | classifiers = 12 | License :: OSI Approved :: MIT License 13 | Programming Language :: Python 14 | Programming Language :: Python :: 3.6 15 | Programming Language :: Python :: 3.7 16 | Programming Language :: Python :: 3.8 17 | Programming Language :: Python :: 3.9 18 | Programming Language :: Python :: 3.10 19 | Programming Language :: Python :: 3.11 20 | Programming Language :: Python :: 3.12 21 | 22 | [options] 23 | python_requires = >=3.6.0 24 | packages = find: 25 | install_requires = requests[socks]>=2.22.0 26 | include_package_data = True 27 | 28 | [options.packages.find] 29 | exclude = 30 | tests 31 | *.tests 32 | *.tests.* 33 | tests.* 34 | 35 | [options.entry_points] 36 | console_scripts = 37 | cf-speedtest = cf_speedtest.speedtest:main 38 | cf_speedtest = cf_speedtest.speedtest:main 39 | -------------------------------------------------------------------------------- /tests/network_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from unittest.mock import MagicMock 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cf_speedtest import speedtest 9 | 10 | 11 | @pytest.fixture 12 | def mock_requests_session(): 13 | with patch('cf_speedtest.speedtest.REQ_SESSION') as mock_session: 14 | yield mock_session 15 | 16 | 17 | def test_get_our_country(mock_requests_session): 18 | mock_response = MagicMock() 19 | mock_response.text = 'loc=GB\nother=value' 20 | mock_requests_session.get.return_value = mock_response 21 | 22 | assert speedtest.get_our_country() == 'GB' 23 | 24 | 25 | def test_preamble_unit(mock_requests_session): 26 | mock_response = MagicMock() 27 | mock_response.headers = { 28 | 'cf-meta-ip': '1.2.3.4', 29 | 'cf-meta-colo': 'LAX', 30 | } 31 | mock_requests_session.get.return_value = mock_response 32 | 33 | with patch('cf_speedtest.speedtest.get_our_country', return_value='US'): 34 | result = speedtest.preamble() 35 | assert '1.2.3.4' in result 36 | assert 'LAX' in result 37 | assert 'US' in result 38 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: double-quote-string-fixer 10 | - id: name-tests-test 11 | - id: requirements-txt-fixer 12 | - repo: https://github.com/asottile/reorder_python_imports 13 | rev: v3.9.0 14 | hooks: 15 | - id: reorder-python-imports 16 | args: [--py37-plus, --add-import, "from __future__ import annotations"] 17 | - repo: https://github.com/asottile/add-trailing-comma 18 | rev: v2.4.0 19 | hooks: 20 | - id: add-trailing-comma 21 | args: [--py36-plus] 22 | - repo: https://github.com/asottile/pyupgrade 23 | rev: v3.3.2 24 | hooks: 25 | - id: pyupgrade 26 | args: [--py37-plus] 27 | - repo: https://github.com/pre-commit/mirrors-autopep8 28 | rev: v2.0.2 29 | hooks: 30 | - id: autopep8 31 | - repo: https://github.com/PyCQA/flake8 32 | rev: 6.0.0 33 | hooks: 34 | - id: flake8 35 | args: 36 | - --max-line-length=120 37 | -------------------------------------------------------------------------------- /tests/options_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from cf_speedtest import options 6 | from cf_speedtest import speedtest 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 'input_str, expected', [ 11 | ('yes', True), 12 | ('no', False), 13 | ('true', True), 14 | ('false', False), 15 | ('1', True), 16 | ('0', False), 17 | ('YES', True), 18 | ('NO', False), 19 | ('True', True), 20 | ('False', False), 21 | ('y', True), 22 | ('n', False), 23 | ], 24 | ) 25 | def test_str_to_bool_valid(input_str, expected): 26 | assert options.str_to_bool(input_str) == expected 27 | 28 | 29 | @pytest.mark.parametrize('input_str', ['invalid', 'maybe', '2', '-1']) 30 | def test_str_to_bool_invalid(input_str): 31 | with pytest.raises(speedtest.argparse.ArgumentTypeError): 32 | options.str_to_bool(input_str) 33 | 34 | 35 | @pytest.mark.parametrize( 36 | 'input_str, expected', [ 37 | ('0', 0), 38 | ('50', 50), 39 | ('100', 100), 40 | ], 41 | ) 42 | def test_valid_percentile_valid(input_str, expected): 43 | assert options.valid_percentile(input_str) == expected 44 | 45 | 46 | @pytest.mark.parametrize('input_str', ['-1', '101', 'invalid', '50.5']) 47 | def test_valid_percentile_invalid(input_str): 48 | with pytest.raises(speedtest.argparse.ArgumentTypeError): 49 | options.valid_percentile(input_str) 50 | -------------------------------------------------------------------------------- /tests/integration_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import csv 4 | import os 5 | 6 | import pytest 7 | 8 | from cf_speedtest import speedtest 9 | 10 | 11 | @pytest.mark.integration 12 | def test_country(): 13 | country = speedtest.get_our_country() 14 | assert isinstance(country, str) 15 | assert len(country) == 2 # Assuming country codes are always 2 characters 16 | 17 | 18 | @pytest.mark.integration 19 | def test_preamble(): 20 | preamble_text = speedtest.preamble() 21 | assert isinstance(preamble_text, str) 22 | assert 'Your IP:' in preamble_text 23 | assert 'Server loc:' in preamble_text 24 | 25 | 26 | @pytest.mark.integration 27 | def test_main(): 28 | assert speedtest.main() == 0 29 | 30 | 31 | @pytest.mark.integration 32 | @pytest.mark.skip(reason='will fail without proxy') 33 | def test_proxy(): 34 | assert speedtest.main(['--proxy', '100.24.216.83:80']) == 0 35 | 36 | 37 | @pytest.mark.integration 38 | def test_nossl(): 39 | assert speedtest.main(['--verifyssl', 'False']) == 0 40 | 41 | 42 | @pytest.mark.integration 43 | def test_csv_output(): 44 | temp_file = 'test_output.csv' 45 | 46 | assert speedtest.main(['--output', temp_file]) == 0 47 | 48 | assert os.path.exists(temp_file) 49 | assert os.path.getsize(temp_file) > 0 50 | 51 | with open(temp_file) as csvfile: 52 | try: 53 | csv.reader(csvfile) 54 | next(csv.reader(csvfile)) 55 | except csv.Error: 56 | pytest.fail('The output file is not a valid CSV') 57 | 58 | os.remove(temp_file) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cf_speedtest 2 | 3 | ## A simple internet speed test tool/library, which uses https://speed.cloudflare.com (provided by Cloudflare) 4 | 5 | [![PyPI version](https://img.shields.io/pypi/v/cf-speedtest.svg)](https://pypi.python.org/pypi/cf-speedtest) 6 | [![PyPI pyversions](https://img.shields.io/pypi/pyversions/cf-speedtest.svg)](https://pypi.python.org/pypi/cf-speedtest) 7 | [![PyPI status](https://img.shields.io/pypi/status/cf-speedtest.svg)](https://pypi.python.org/pypi/cf-speedtest) 8 | [![PyPI downloads](https://img.shields.io/pypi/dm/cf-speedtest.svg)](https://pypi.python.org/pypi/cf-speedtest) 9 | 10 | ## Installation: 11 | ```bash 12 | $ pip install -U cf-speedtest 13 | ``` 14 | 15 | ## Basic CLI usage: 16 | 17 | - ### Running a normal speedtest: 18 | - `cf_speedtest` 19 | 20 | - ### Without verifying SSL: 21 | - `cf_speedtest --verifyssl=false` 22 | 23 | - ### Specify a [percentile](https://en.wikipedia.org/wiki/Percentile) of measurements to be considered your speed (default 90): 24 | - `cf_speedtest --percentile 80` 25 | 26 | - ### Output measurements to a CSV file: 27 | - `cf_speedtest --output speed_data.csv` 28 | 29 | - ### Specify a SOCKS/HTTP proxy to use (with or without authentication): 30 | - `cf_speedtest --proxy socks5://127.0.0.1:1080` 31 | - `cf_speedtest --proxy socks5://admin:admin@127.0.0.1:1080` 32 | - `cf_speedtest --proxy http://127.0.0.1:8181` 33 | - `cf_speedtest --proxy http://admin:admin@127.0.0.1:8181` 34 | - `cf_speedtest --proxy 127.0.0.1:8181` 35 | 36 | ## Programmatic usage: 37 | - TODO 38 | 39 | #### TODO: 40 | - Programmatic usage 41 | - Multi-threaded speedtest 42 | - Continuous mode 43 | 44 | #### Disclaimers: 45 | - This library is purely single-threaded 46 | - This library works entirely over HTTP(S), which has some overhead 47 | - Latency is measured with HTTP requests 48 | - Cloudflare has a global network, but you may be connected to a distant PoP due to ISP peering and other factors 49 | -------------------------------------------------------------------------------- /cf_speedtest/options.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | 5 | 6 | def str_to_bool(s: str) -> bool: 7 | if isinstance(s, bool): 8 | return s 9 | if s.lower() in ('yes', 'true', 't', 'y', '1'): 10 | return True 11 | elif s.lower() in ('no', 'false', 'f', 'n', '0'): 12 | return False 13 | else: 14 | raise argparse.ArgumentTypeError( 15 | f'Boolean value expected, received {s!r}', 16 | ) 17 | 18 | 19 | def valid_percentile(s: str) -> int: 20 | try: 21 | value = int(s) 22 | except ValueError: 23 | raise argparse.ArgumentTypeError( 24 | f'Expected integer between 0 and 100, received {s!r}', 25 | ) 26 | 27 | if not (0 <= value <= 100): 28 | raise argparse.ArgumentTypeError( 29 | f'Expected integer between 0 and 100, received {s}', 30 | ) 31 | 32 | return value 33 | 34 | 35 | def add_run_options(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: 36 | parser.add_argument( 37 | '--output', '-o', 38 | type=str, 39 | help='The file to output the csv data of measurements to', 40 | ) 41 | 42 | parser.add_argument( 43 | '--percentile', '-p', 44 | default=90, 45 | type=valid_percentile, 46 | help=( 47 | 'The percentile of measurements to be considered download speed ' 48 | ' where default is 90 https://en.wikipedia.org/wiki/Percentile' 49 | ), 50 | ) 51 | 52 | parser.add_argument( 53 | '--verifyssl', '-k', 54 | default=True, 55 | type=str_to_bool, 56 | help=( 57 | 'Whether to verify that the server connection is secure by validating the server ' 58 | 'certificate has the correct name and verifies successfully using this machines certificate store' 59 | ), 60 | ) 61 | 62 | parser.add_argument( 63 | '--proxy', '-x', 64 | default=None, 65 | type=str, 66 | help=( 67 | 'Use the specified proxy. Supports HTTP/HTTPS/SOCKS5 with or without authentication' 68 | ), 69 | ) 70 | 71 | parser.add_argument( 72 | '--testpatience', 73 | type=int, 74 | default=20, 75 | help='The longest time to wait for an individual test to run', 76 | ) 77 | 78 | return parser 79 | -------------------------------------------------------------------------------- /tests/speedtest_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cf_speedtest import speedtest 9 | 10 | 11 | @pytest.mark.parametrize( 12 | 'test_type, bytes_to_xfer, iteration_count, expected_len', [ 13 | ('down', 1000, 3, 3), 14 | ('up', 1000, 5, 5), 15 | ('invalid', 1000, 2, 0), 16 | ], 17 | ) 18 | def test_run_tests(test_type, bytes_to_xfer, iteration_count, expected_len): 19 | with patch('cf_speedtest.speedtest.download_test', return_value=(1000, 0.1)), \ 20 | patch('cf_speedtest.speedtest.upload_test', return_value=(1000, 0.2)): 21 | 22 | results = speedtest.run_tests( 23 | test_type, bytes_to_xfer, iteration_count, 24 | ) 25 | assert len(results) == expected_len 26 | 27 | if test_type == 'down': 28 | assert results[0] == 80000 # (1000 / 0.1) * 8 29 | elif test_type == 'up': 30 | assert results[0] == 40000 # (1000 / 0.2) * 8 31 | 32 | 33 | @patch('cf_speedtest.speedtest.run_tests') 34 | @patch('cf_speedtest.speedtest.latency_test') 35 | @patch('cf_speedtest.speedtest.preamble') 36 | def test_run_standard_test(mock_preamble, mock_latency_test, mock_run_tests): 37 | mock_latency_test.return_value = 0.05 38 | mock_run_tests.side_effect = [ 39 | [100000000, 200000000], # download 40 | [50000000, 100000000], # upload 41 | ] 42 | 43 | results = speedtest.run_standard_test( 44 | [1000000], measurement_percentile=90, verbose=True, 45 | ) 46 | 47 | assert results['download_speed'] == 200000000 48 | assert results['upload_speed'] == 100000000 49 | assert len(results['latency_measurements']) == 20 50 | 51 | 52 | @pytest.mark.parametrize( 53 | 'args, expected_exit_code', [ 54 | (['--percentile', '90', '--verifyssl', 'False', '--testpatience', '10'], 0), 55 | (['--proxy', '100.24.216.83:80'], 0), 56 | (['--output', 'test_output.csv'], 0), 57 | ], 58 | ) 59 | def test_main_unit(args, expected_exit_code, mock_requests_session): 60 | with patch('cf_speedtest.speedtest.run_standard_test') as mock_run_test: 61 | mock_run_test.return_value = { 62 | 'download_speed': 100000000, 63 | 'upload_speed': 50000000, 64 | 'download_stdev': 1000000, 65 | 'upload_stdev': 500000, 66 | 'latency_measurements': [10, 20, 30], 67 | 'download_measurements': [90000000, 100000000, 110000000], 68 | 'upload_measurements': [45000000, 50000000, 55000000], 69 | } 70 | 71 | assert speedtest.main(args) == expected_exit_code 72 | 73 | 74 | @pytest.mark.parametrize( 75 | 'proxy, expected_dict', [ 76 | ( 77 | '100.24.216.83:80', { 78 | 'http': 'http://100.24.216.83:80', 'https': 'http://100.24.216.83:80', 79 | }, 80 | ), 81 | ( 82 | 'socks5://127.0.0.1:9150', 83 | {'http': 'socks5://127.0.0.1:9150', 'https': 'socks5://127.0.0.1:9150'}, 84 | ), 85 | ( 86 | 'http://user:pass@10.10.1.10:3128', 87 | { 88 | 'http': 'http://user:pass@10.10.1.10:3128', 89 | 'https': 'http://user:pass@10.10.1.10:3128', 90 | }, 91 | ), 92 | ], 93 | ) 94 | def test_proxy_unit(proxy, expected_dict): 95 | with patch('cf_speedtest.speedtest.run_standard_test') as mock_run_test: 96 | mock_run_test.return_value = { 97 | 'download_speed': 100000000, 98 | 'upload_speed': 50000000, 99 | 'download_stdev': 1000000, 100 | 'upload_stdev': 500000, 101 | 'latency_measurements': [10, 20, 30], 102 | 'download_measurements': [90000000, 100000000, 110000000], 103 | 'upload_measurements': [45000000, 50000000, 55000000], 104 | } 105 | 106 | speedtest.main(['--proxy', proxy]) 107 | assert speedtest.PROXY_DICT == expected_dict 108 | 109 | 110 | def test_output_file(mock_time): 111 | output_file = 'test_output.csv' 112 | 113 | with patch('cf_speedtest.speedtest.run_standard_test') as mock_run_test, \ 114 | patch('builtins.open', create=True) as mock_open: 115 | mock_run_test.return_value = { 116 | 'download_speed': 100000000, 117 | 'upload_speed': 50000000, 118 | 'download_stdev': 1000000, 119 | 'upload_stdev': 500000, 120 | 'latency_measurements': [10, 20, 30], 121 | 'download_measurements': [90000000, 100000000, 110000000], 122 | 'upload_measurements': [45000000, 50000000, 55000000], 123 | } 124 | 125 | speedtest.main(['--output', output_file]) 126 | 127 | mock_open.assert_called_with(output_file, 'w') 128 | 129 | if os.path.exists(output_file): 130 | os.remove(output_file) 131 | -------------------------------------------------------------------------------- /cf_speedtest/speedtest.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import argparse 5 | import math 6 | import statistics 7 | import time 8 | from timeit import default_timer as timer 9 | 10 | import requests 11 | import urllib3 12 | 13 | import cf_speedtest.locations as locations 14 | import cf_speedtest.options as options 15 | 16 | REQ_SESSION = requests.Session() 17 | 18 | CGI_ENDPOINT = 'https://speed.cloudflare.com/cdn-cgi/trace' 19 | DOWNLOAD_ENDPOINT = 'https://speed.cloudflare.com/__down?measId=0&bytes={}' 20 | UPLOAD_ENDPOINT = 'https://speed.cloudflare.com/__up?measId=0' 21 | 22 | UPLOAD_HEADERS = { 23 | 'Connection': 'keep-alive', 24 | 'DNT': '1', 25 | 'Content-Type': 'text/plain;charset=UTF-8', 26 | 'Accept': '*/*', 27 | } 28 | 29 | PROXY_DICT = None 30 | VERIFY_SSL = True 31 | OUTPUT_FILE = None 32 | 33 | # Could use python's statistics library, but quantiles are only available 34 | # in version 3.8 and above 35 | 36 | 37 | def percentile(data: list, percentile: int) -> float: 38 | size = len(data) 39 | if percentile == 0: 40 | return min(data) 41 | return sorted(data)[int(math.ceil((size * percentile) / 100)) - 1] 42 | 43 | # returns ms of how long cloudflare took to process the request, this is in the Server-Timing header 44 | 45 | 46 | def get_server_timing(server_timing: str) -> float: 47 | for part in server_timing.split(';'): 48 | if 'dur=' in part: 49 | try: 50 | return float(part.split('=')[1]) / 1000 51 | except (IndexError, ValueError): 52 | try: 53 | return float(part.split(',')[0].split('=')[1]) / 1000 54 | except (IndexError, ValueError): 55 | pass 56 | return 0.0 57 | 58 | # given an amount of bytes, upload it and return the elapsed seconds taken 59 | 60 | 61 | def upload_test(total_bytes: int) -> int | float: 62 | start = timer() 63 | 64 | r = REQ_SESSION.post( 65 | UPLOAD_ENDPOINT, data=bytearray( 66 | total_bytes, 67 | ), headers=UPLOAD_HEADERS, verify=VERIFY_SSL, proxies=PROXY_DICT, 68 | ) 69 | r.raise_for_status() 70 | total_time_taken = timer() - start 71 | 72 | # trust what the server says as time taken 73 | server_time_taken = get_server_timing(r.headers['Server-Timing']) 74 | 75 | if OUTPUT_FILE: 76 | with open(OUTPUT_FILE, 'a', encoding='utf-8') as datafile: 77 | datafile.write( 78 | f'{time.time()},up,{total_bytes},{total_time_taken},{server_time_taken}\n', 79 | ) 80 | 81 | return total_bytes, server_time_taken 82 | 83 | # given an amount of bytes, download it and return the elapsed seconds taken 84 | 85 | 86 | def download_test(total_bytes: int) -> int | float: 87 | endpoint = DOWNLOAD_ENDPOINT.format(total_bytes) 88 | start = timer() 89 | 90 | r = REQ_SESSION.get(endpoint, verify=VERIFY_SSL, proxies=PROXY_DICT) 91 | r.raise_for_status() 92 | 93 | total_time_taken = timer() - start 94 | 95 | content_size = len(r.content) 96 | server_time_taken = get_server_timing(r.headers['Server-Timing']) 97 | 98 | if OUTPUT_FILE: 99 | with open(OUTPUT_FILE, 'a', encoding='utf-8') as datafile: 100 | datafile.write( 101 | f'{time.time()},down,{content_size},{total_time_taken},{server_time_taken}\n', 102 | ) 103 | 104 | return content_size, total_time_taken - server_time_taken 105 | 106 | # calculates http "latency" by measuring download of an empty payload 107 | 108 | 109 | def latency_test() -> float: 110 | endpoint = DOWNLOAD_ENDPOINT.format(0) 111 | 112 | start = timer() 113 | r = REQ_SESSION.get(endpoint, verify=VERIFY_SSL, proxies=PROXY_DICT) 114 | r.raise_for_status() 115 | 116 | total_time_taken = timer() - start 117 | server_time_taken = get_server_timing(r.headers['Server-Timing']) 118 | 119 | if OUTPUT_FILE: 120 | with open(OUTPUT_FILE, 'a', encoding='utf-8') as datafile: 121 | datafile.write( 122 | f'{time.time()},down,{len(r.content)},{total_time_taken},{server_time_taken}\n', 123 | ) 124 | 125 | return total_time_taken - server_time_taken 126 | 127 | # See https://speed.cloudflare.com/cdn-cgi/trace 128 | 129 | 130 | def get_our_country() -> str: 131 | r = REQ_SESSION.get(CGI_ENDPOINT, verify=VERIFY_SSL, proxies=PROXY_DICT) 132 | r.raise_for_status() 133 | 134 | cgi_data = r.text 135 | cgi_dict = { 136 | k: v for k, v in [ 137 | line.split( 138 | '=', 139 | ) for line in cgi_data.splitlines() 140 | ] 141 | } 142 | 143 | return cgi_dict.get('loc') or 'Unknown' 144 | 145 | 146 | def preamble() -> str: 147 | r = REQ_SESSION.get( 148 | DOWNLOAD_ENDPOINT.format( 149 | 0, 150 | ), verify=VERIFY_SSL, proxies=PROXY_DICT, 151 | ) 152 | r.raise_for_status() 153 | 154 | our_ip = r.headers.get('cf-meta-ip') 155 | colo = r.headers.get('cf-meta-colo') 156 | server_city = colo 157 | server_country = next( 158 | ( 159 | loc.get( 160 | 'cca2', 161 | ) or 'Unknown' for loc in locations.SERVER_LOCATIONS if loc['iata'] == colo.upper() 162 | ), 'Unknown', 163 | ) 164 | preamble_str = f'Your IP:\t{our_ip} ({get_our_country()})\nServer loc:\t{server_city} ({colo}) - ({server_country})' 165 | 166 | return preamble_str 167 | 168 | # runs x amount of y-byte tests, given a test_type ("down" or "up") 169 | # returns a list of measurements in bits per second 170 | 171 | 172 | def run_tests(test_type: str, bytes_to_xfer: int, iteration_count: int = 8) -> list: 173 | measurements = [] 174 | 175 | for i in range(0, iteration_count): 176 | if test_type == 'down': 177 | xferd_bytes_total, seconds_taken = download_test(bytes_to_xfer) 178 | elif test_type == 'up': 179 | xferd_bytes_total, seconds_taken = upload_test(bytes_to_xfer) 180 | else: 181 | return measurements 182 | 183 | bits_per_second = (int(xferd_bytes_total) / seconds_taken) * 8 184 | measurements.append(bits_per_second) 185 | 186 | return measurements 187 | 188 | # runs a standard test of upload and download, similar to what 189 | # simulates what is ran on speed.cloudflare.com 190 | 191 | 192 | def run_standard_test( 193 | measurement_sizes: list, 194 | measurement_percentile: int = 90, 195 | verbose: bool = False, 196 | test_patience: int = 15, 197 | ) -> dict: 198 | LATENCY_MEASUREMENTS = [] 199 | DOWNLOAD_MEASUREMENTS = [] 200 | UPLOAD_MEASUREMENTS = [] 201 | 202 | if verbose: 203 | print(preamble(), '\n') 204 | 205 | latency_test() # ignore first request as it contains http connection setup 206 | for i in range(0, 20): 207 | LATENCY_MEASUREMENTS.append(latency_test() * 1000) 208 | 209 | # Assume the median latency is our latency (just like the website) 210 | latency = percentile(LATENCY_MEASUREMENTS, 50) 211 | jitter = statistics.stdev(LATENCY_MEASUREMENTS) 212 | if verbose: 213 | print(f"{'Latency:':<16} {latency:.2f} ms") 214 | print(f"{'Jitter:':<16} {jitter:.2f} ms") 215 | print('Running speed tests...\n') 216 | 217 | first_dl_test, first_ul_test = True, True 218 | continue_dl_test, continue_ul_test = True, True 219 | current_up_speed_mbps = 0 220 | current_down_speed_mbps = 0 221 | 222 | # The SLOWEST test should take no longer than 30 seconds 223 | for i in range(0, len(measurement_sizes)): 224 | measurement = measurement_sizes[i] 225 | download_test_count = (-2 * i + 12) # this is how the website does it 226 | upload_test_count = (-2 * i + 10) # this is how the website does it 227 | total_download_bytes = measurement * download_test_count 228 | total_upload_bytes = measurement * upload_test_count 229 | 230 | if not first_dl_test: 231 | if current_down_speed_mbps * test_patience < total_download_bytes / 125000: 232 | continue_dl_test = False 233 | else: 234 | first_dl_test = False 235 | 236 | if continue_dl_test: 237 | # print(f"Testing download ({measurement / 1_000_000:.2f}MiB) ({download_test_count} time(s))") 238 | DOWNLOAD_MEASUREMENTS += run_tests( 239 | 'down', 240 | measurement, download_test_count, 241 | ) 242 | current_down_speed_mbps = percentile( 243 | DOWNLOAD_MEASUREMENTS, measurement_percentile, 244 | ) / 1_000_000 245 | if verbose: 246 | # print(f"Current down: {current_down_speed_mbps:.2f} Mbit/sec") 247 | print( 248 | f"{'Current speeds:':<24} {'Down: '}{current_down_speed_mbps:.2f} Mbit/sec\t" 249 | f"{'Up: '}{current_up_speed_mbps:.2f} Mbit/sec", 250 | ) 251 | 252 | if not first_ul_test: 253 | if current_up_speed_mbps * test_patience < total_upload_bytes / 125_000: 254 | continue_ul_test = False 255 | else: 256 | first_ul_test = False 257 | 258 | if continue_ul_test: 259 | # print(f"Testing upload ({measurement / 1_000_000:.2f}MiB) ({upload_test_count} time(s))") 260 | UPLOAD_MEASUREMENTS += run_tests( 261 | 'up', 262 | measurement, upload_test_count, 263 | ) 264 | current_up_speed_mbps = percentile( 265 | UPLOAD_MEASUREMENTS, measurement_percentile, 266 | ) / 1_000_000 267 | if verbose: 268 | # print(f"Current up: {current_up_speed_mbps:.2f} Mbit/sec") 269 | print( 270 | f"{'Current speeds:':<24} {'Down: '}{current_down_speed_mbps:.2f} Mbit/sec\t" 271 | f"{'Up: '}{current_up_speed_mbps:.2f} Mbit/sec", 272 | ) 273 | 274 | # all raw measurements are in bits per second 275 | pctile_download = percentile(DOWNLOAD_MEASUREMENTS, measurement_percentile) 276 | pctile_upload = percentile(UPLOAD_MEASUREMENTS, measurement_percentile) 277 | download_stdev = statistics.stdev(DOWNLOAD_MEASUREMENTS) 278 | upload_stdev = statistics.stdev(UPLOAD_MEASUREMENTS) 279 | 280 | return { 281 | 'download_measurements': DOWNLOAD_MEASUREMENTS, 282 | 'upload_measurements': UPLOAD_MEASUREMENTS, 283 | 'latency_measurements': LATENCY_MEASUREMENTS, 284 | 'download_speed': pctile_download, 285 | 'upload_speed': pctile_upload, 286 | 'download_stdev': download_stdev, 287 | 'upload_stdev': upload_stdev, 288 | } 289 | 290 | 291 | def main(argv=None) -> int: 292 | global PROXY_DICT, VERIFY_SSL, OUTPUT_FILE 293 | # disable annoying warning when using verify=False in requests.get("x", verify=False) 294 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 295 | 296 | parser = argparse.ArgumentParser() 297 | parser_with_args = options.add_run_options(parser) 298 | 299 | args = parser_with_args.parse_args(argv) 300 | percentile = args.percentile 301 | VERIFY_SSL = args.verifyssl 302 | OUTPUT_FILE = args.output 303 | patience = args.testpatience 304 | proxy = args.proxy 305 | 306 | # clear the output file 307 | if OUTPUT_FILE: 308 | open(args.output, 'w').close() 309 | 310 | # set up proxy dictionary 311 | if proxy: 312 | if proxy.startswith('socks'): 313 | PROXY_DICT = {'http': f'{proxy}', 'https': f'{proxy}'} 314 | elif proxy.startswith('http'): 315 | PROXY_DICT = {'http': f'{proxy}', 'https': f'{proxy}'} 316 | else: 317 | PROXY_DICT = { 318 | 'http': f'http://{proxy}', 319 | 'https': f'http://{proxy}', 320 | } 321 | else: 322 | PROXY_DICT = None 323 | 324 | # Taken from speed.cloudflare.com 325 | # byte count for each test, ranging from 100KB to 250MB 326 | measurement_sizes =\ 327 | [ 328 | 100_000, 329 | 1_000_000, 330 | 10_000_000, 331 | 25_000_000, 332 | 100_000_000, 333 | 250_000_000, 334 | ] 335 | 336 | speeds = run_standard_test(measurement_sizes, percentile, True, patience) 337 | 338 | d = speeds['download_speed'] 339 | u = speeds['upload_speed'] 340 | d_s = speeds['download_stdev'] # noqa 341 | u_s = speeds['upload_stdev'] # noqa 342 | 343 | print( 344 | f"{args.percentile}{'th percentile results:':<24} Down: {d/1_000_000:.2f} Mbit/sec\t" 345 | f'Up: {u/1_000_000:.2f} Mbit/sec', 346 | ) 347 | 348 | return 0 349 | 350 | 351 | if __name__ == '__main__': 352 | raise SystemExit(main()) 353 | -------------------------------------------------------------------------------- /cf_speedtest/locations.py: -------------------------------------------------------------------------------- 1 | # https://speed.cloudflare.com/locations 2 | from __future__ import annotations 3 | SERVER_LOCATIONS = [ 4 | { 5 | 'iata': 'TIA', 6 | 'lat': 41.4146995544, 7 | 'lon': 19.7206001282, 8 | 'cca2': 'AL', 9 | 'region': 'Europe', 10 | 'city': 'Tirana', 11 | }, 12 | { 13 | 'iata': 'ALG', 14 | 'lat': 36.6910018921, 15 | 'lon': 3.2154099941, 16 | 'cca2': 'DZ', 17 | 'region': 'Africa', 18 | 'city': 'Algiers', 19 | }, 20 | { 21 | 'iata': 'AAE', 22 | 'lat': 36.85596, 23 | 'lon': 7.79207, 24 | 'cca2': 'DZ', 25 | 'region': 'Africa', 26 | 'city': 'Annaba', 27 | }, 28 | { 29 | 'iata': 'ORN', 30 | 'lat': 35.6911, 31 | 'lon': -0.6416, 32 | 'cca2': 'DZ', 33 | 'region': 'Africa', 34 | 'city': 'Oran', 35 | }, 36 | { 37 | 'iata': 'LAD', 38 | 'lat': -8.8583698273, 39 | 'lon': 13.2312002182, 40 | 'cca2': 'AO', 41 | 'region': 'Africa', 42 | 'city': 'Luanda', 43 | }, 44 | { 45 | 'iata': 'EZE', 46 | 'lat': -34.8222, 47 | 'lon': -58.5358, 48 | 'cca2': 'AR', 49 | 'region': 'South America', 50 | 'city': 'Buenos Aires', 51 | }, 52 | { 53 | 'iata': 'COR', 54 | 'lat': -31.31, 55 | 'lon': -64.208333, 56 | 'cca2': 'AR', 57 | 'region': 'South America', 58 | 'city': 'Córdoba', 59 | }, 60 | { 61 | 'iata': 'NQN', 62 | 'lat': -38.9490013123, 63 | 'lon': -68.1557006836, 64 | 'cca2': 'AR', 65 | 'region': 'South America', 66 | 'city': 'Neuquen', 67 | }, 68 | { 69 | 'iata': 'EVN', 70 | 'lat': 40.1473007202, 71 | 'lon': 44.3959007263, 72 | 'cca2': 'AM', 73 | 'region': 'Middle East', 74 | 'city': 'Yerevan', 75 | }, 76 | { 77 | 'iata': 'ADL', 78 | 'lat': -34.9431729, 79 | 'lon': 138.5335637, 80 | 'cca2': 'AU', 81 | 'region': 'Oceania', 82 | 'city': 'Adelaide', 83 | }, 84 | { 85 | 'iata': 'BNE', 86 | 'lat': -27.3841991425, 87 | 'lon': 153.117004394, 88 | 'cca2': 'AU', 89 | 'region': 'Oceania', 90 | 'city': 'Brisbane', 91 | }, 92 | { 93 | 'iata': 'CBR', 94 | 'lat': -35.3069000244, 95 | 'lon': 149.1950073242, 96 | 'cca2': 'AU', 97 | 'region': 'Oceania', 98 | 'city': 'Canberra', 99 | }, 100 | { 101 | 'iata': 'HBA', 102 | 'lat': -42.883209, 103 | 'lon': 147.331665, 104 | 'cca2': 'AU', 105 | 'region': 'Oceania', 106 | 'city': 'Hobart', 107 | }, 108 | { 109 | 'iata': 'MEL', 110 | 'lat': -37.6733016968, 111 | 'lon': 144.843002319, 112 | 'cca2': 'AU', 113 | 'region': 'Oceania', 114 | 'city': 'Melbourne', 115 | }, 116 | { 117 | 'iata': 'PER', 118 | 'lat': -31.9402999878, 119 | 'lon': 115.967002869, 120 | 'cca2': 'AU', 121 | 'region': 'Oceania', 122 | 'city': 'Perth', 123 | }, 124 | { 125 | 'iata': 'SYD', 126 | 'lat': -33.9460983276, 127 | 'lon': 151.177001953, 128 | 'cca2': 'AU', 129 | 'region': 'Oceania', 130 | 'city': 'Sydney', 131 | }, 132 | { 133 | 'iata': 'VIE', 134 | 'lat': 48.1102981567, 135 | 'lon': 16.5697002411, 136 | 'cca2': 'AT', 137 | 'region': 'Europe', 138 | 'city': 'Vienna', 139 | }, 140 | { 141 | 'iata': 'LLK', 142 | 'lat': 38.7463989258, 143 | 'lon': 48.8180007935, 144 | 'cca2': 'AZ', 145 | 'region': 'Middle East', 146 | 'city': 'Astara', 147 | }, 148 | { 149 | 'iata': 'GYD', 150 | 'lat': 40.4674987793, 151 | 'lon': 50.0466995239, 152 | 'cca2': 'AZ', 153 | 'region': 'Middle East', 154 | 'city': 'Baku', 155 | }, 156 | { 157 | 'iata': 'BAH', 158 | 'lat': 26.2707996368, 159 | 'lon': 50.6335983276, 160 | 'cca2': 'BH', 161 | 'region': 'Middle East', 162 | 'city': 'Manama', 163 | }, 164 | { 165 | 'iata': 'CGP', 166 | 'lat': 22.2495995, 167 | 'lon': 91.8133011, 168 | 'cca2': 'BD', 169 | 'region': 'Asia Pacific', 170 | 'city': 'Chittagong', 171 | }, 172 | { 173 | 'iata': 'DAC', 174 | 'lat': 23.843347, 175 | 'lon': 90.397783, 176 | 'cca2': 'BD', 177 | 'region': 'Asia Pacific', 178 | 'city': 'Dhaka', 179 | }, 180 | { 181 | 'iata': 'JSR', 182 | 'lat': 23.1837997437, 183 | 'lon': 89.1607971191, 184 | 'cca2': 'BD', 185 | 'region': 'Asia Pacific', 186 | 'city': 'Jashore', 187 | }, 188 | { 189 | 'iata': 'BGI', 190 | 'lat': 13.103562, 191 | 'lon': -59.603226, 192 | 'cca2': 'BB', 193 | 'region': 'North America', 194 | 'city': 'Bridgetown', 195 | }, 196 | { 197 | 'iata': 'MSQ', 198 | 'lat': 53.9006, 199 | 'lon': 27.599, 200 | 'cca2': 'BY', 201 | 'region': 'Europe', 202 | 'city': 'Minsk', 203 | }, 204 | { 205 | 'iata': 'BRU', 206 | 'lat': 50.9014015198, 207 | 'lon': 4.4844398499, 208 | 'cca2': 'BE', 209 | 'region': 'Europe', 210 | 'city': 'Brussels', 211 | }, 212 | { 213 | 'iata': 'PBH', 214 | 'lat': 27.4712, 215 | 'lon': 89.6339, 216 | 'cca2': 'BT', 217 | 'region': 'Asia Pacific', 218 | 'city': 'Thimphu', 219 | }, 220 | { 221 | 'iata': 'LPB', 222 | 'lat': -16.4897, 223 | 'lon': -68.1193, 224 | 'cca2': 'BO', 225 | 'region': 'South America', 226 | 'city': 'La Paz', 227 | }, 228 | { 229 | 'iata': 'GBE', 230 | 'lat': -24.6282, 231 | 'lon': 25.9231, 232 | 'cca2': 'BW', 233 | 'region': 'Africa', 234 | 'city': 'Gaborone', 235 | }, 236 | { 237 | 'iata': 'QWJ', 238 | 'lat': -22.738, 239 | 'lon': -47.334, 240 | 'cca2': 'BR', 241 | 'region': 'South America', 242 | 'city': 'Americana', 243 | }, 244 | { 245 | 'iata': 'ARU', 246 | 'lat': -21.1413002014, 247 | 'lon': -50.4247016907, 248 | 'cca2': 'BR', 249 | 'region': 'South America', 250 | 'city': 'Aracatuba', 251 | }, 252 | { 253 | 'iata': 'BEL', 254 | 'lat': -1.4563, 255 | 'lon': -48.5013, 256 | 'cca2': 'BR', 257 | 'region': 'South America', 258 | 'city': 'Belém', 259 | }, 260 | { 261 | 'iata': 'CNF', 262 | 'lat': -19.624444, 263 | 'lon': -43.971944, 264 | 'cca2': 'BR', 265 | 'region': 'South America', 266 | 'city': 'Belo Horizonte', 267 | }, 268 | { 269 | 'iata': 'BNU', 270 | 'lat': -26.89245, 271 | 'lon': -49.07696, 272 | 'cca2': 'BR', 273 | 'region': 'South America', 274 | 'city': 'Blumenau', 275 | }, 276 | { 277 | 'iata': 'BSB', 278 | 'lat': -15.79824, 279 | 'lon': -47.90859, 280 | 'cca2': 'BR', 281 | 'region': 'South America', 282 | 'city': 'Brasilia', 283 | }, 284 | { 285 | 'iata': 'CFC', 286 | 'lat': -26.7762, 287 | 'lon': -51.0125, 288 | 'cca2': 'BR', 289 | 'region': 'South America', 290 | 'city': 'Cacador', 291 | }, 292 | { 293 | 'iata': 'VCP', 294 | 'lat': -22.90662, 295 | 'lon': -47.08576, 296 | 'cca2': 'BR', 297 | 'region': 'South America', 298 | 'city': 'Campinas', 299 | }, 300 | { 301 | 'iata': 'CAW', 302 | 'lat': -21.698299408, 303 | 'lon': -41.301700592, 304 | 'cca2': 'BR', 305 | 'region': 'South America', 306 | 'city': 'Campos dos Goytacazes', 307 | }, 308 | { 309 | 'iata': 'XAP', 310 | 'lat': -27.1341991425, 311 | 'lon': -52.6566009521, 312 | 'cca2': 'BR', 313 | 'region': 'South America', 314 | 'city': 'Chapeco', 315 | }, 316 | { 317 | 'iata': 'CGB', 318 | 'lat': -15.59611, 319 | 'lon': -56.09667, 320 | 'cca2': 'BR', 321 | 'region': 'South America', 322 | 'city': 'Cuiaba', 323 | }, 324 | { 325 | 'iata': 'CWB', 326 | 'lat': -25.5284996033, 327 | 'lon': -49.1758003235, 328 | 'cca2': 'BR', 329 | 'region': 'South America', 330 | 'city': 'Curitiba', 331 | }, 332 | { 333 | 'iata': 'FLN', 334 | 'lat': -27.6702785492, 335 | 'lon': -48.5525016785, 336 | 'cca2': 'BR', 337 | 'region': 'South America', 338 | 'city': 'Florianopolis', 339 | }, 340 | { 341 | 'iata': 'FOR', 342 | 'lat': -3.7762799263, 343 | 'lon': -38.5326004028, 344 | 'cca2': 'BR', 345 | 'region': 'South America', 346 | 'city': 'Fortaleza', 347 | }, 348 | { 349 | 'iata': 'GYN', 350 | 'lat': -16.69727, 351 | 'lon': -49.26851, 352 | 'cca2': 'BR', 353 | 'region': 'South America', 354 | 'city': 'Goiania', 355 | }, 356 | { 357 | 'iata': 'ITJ', 358 | 'lat': -27.6116676331, 359 | 'lon': -48.6727790833, 360 | 'cca2': 'BR', 361 | 'region': 'South America', 362 | 'city': 'Itajai', 363 | }, 364 | { 365 | 'iata': 'JOI', 366 | 'lat': -26.304408, 367 | 'lon': -48.846383, 368 | 'cca2': 'BR', 369 | 'region': 'South America', 370 | 'city': 'Joinville', 371 | }, 372 | { 373 | 'iata': 'JDO', 374 | 'lat': -7.2242, 375 | 'lon': -39.313, 376 | 'cca2': 'BR', 377 | 'region': 'South America', 378 | 'city': 'Juazeiro do Norte', 379 | }, 380 | { 381 | 'iata': 'MAO', 382 | 'lat': -3.11286, 383 | 'lon': -60.01949, 384 | 'cca2': 'BR', 385 | 'region': 'South America', 386 | 'city': 'Manaus', 387 | }, 388 | { 389 | 'iata': 'PMW', 390 | 'lat': -10.2915000916, 391 | 'lon': -48.3569984436, 392 | 'cca2': 'BR', 393 | 'region': 'South America', 394 | 'city': 'Palmas', 395 | }, 396 | { 397 | 'iata': 'POA', 398 | 'lat': -29.9944000244, 399 | 'lon': -51.1713981628, 400 | 'cca2': 'BR', 401 | 'region': 'South America', 402 | 'city': 'Porto Alegre', 403 | }, 404 | { 405 | 'iata': 'REC', 406 | 'lat': -8.1264896393, 407 | 'lon': -34.9235992432, 408 | 'cca2': 'BR', 409 | 'region': 'South America', 410 | 'city': 'Recife', 411 | }, 412 | { 413 | 'iata': 'RAO', 414 | 'lat': -21.1363887787, 415 | 'lon': -47.7766685486, 416 | 'cca2': 'BR', 417 | 'region': 'South America', 418 | 'city': 'Ribeirao Preto', 419 | }, 420 | { 421 | 'iata': 'GIG', 422 | 'lat': -22.8099994659, 423 | 'lon': -43.2505569458, 424 | 'cca2': 'BR', 425 | 'region': 'South America', 426 | 'city': 'Rio de Janeiro', 427 | }, 428 | { 429 | 'iata': 'SJP', 430 | 'lat': -20.807157, 431 | 'lon': -49.378994, 432 | 'cca2': 'BR', 433 | 'region': 'South America', 434 | 'city': 'São José do Rio Preto', 435 | }, 436 | { 437 | 'iata': 'SJK', 438 | 'lat': -23.1791, 439 | 'lon': -45.8872, 440 | 'cca2': 'BR', 441 | 'region': 'South America', 442 | 'city': 'São José dos Campos', 443 | }, 444 | { 445 | 'iata': 'GRU', 446 | 'lat': -23.4355564117, 447 | 'lon': -46.4730567932, 448 | 'cca2': 'BR', 449 | 'region': 'South America', 450 | 'city': 'São Paulo', 451 | }, 452 | { 453 | 'iata': 'SOD', 454 | 'lat': -23.54389, 455 | 'lon': -46.63445, 456 | 'cca2': 'BR', 457 | 'region': 'South America', 458 | 'city': 'Sorocaba', 459 | }, 460 | { 461 | 'iata': 'NVT', 462 | 'lat': -26.8251, 463 | 'lon': -49.2695, 464 | 'cca2': 'BR', 465 | 'region': 'South America', 466 | 'city': 'Timbo', 467 | }, 468 | { 469 | 'iata': 'UDI', 470 | 'lat': -18.8836116791, 471 | 'lon': -48.225276947, 472 | 'cca2': 'BR', 473 | 'region': 'South America', 474 | 'city': 'Uberlandia', 475 | }, 476 | { 477 | 'iata': 'VIX', 478 | 'lat': -20.64871, 479 | 'lon': -41.90857, 480 | 'cca2': 'BR', 481 | 'region': 'South America', 482 | 'city': 'Vitoria', 483 | }, 484 | { 485 | 'iata': 'BWN', 486 | 'lat': 4.903052, 487 | 'lon': 114.939819, 488 | 'cca2': 'BN', 489 | 'region': 'Asia Pacific', 490 | 'city': 'Bandar Seri Begawan', 491 | }, 492 | { 493 | 'iata': 'SOF', 494 | 'lat': 42.6966934204, 495 | 'lon': 23.4114360809, 496 | 'cca2': 'BG', 497 | 'region': 'Europe', 498 | 'city': 'Sofia', 499 | }, 500 | { 501 | 'iata': 'OUA', 502 | 'lat': 12.3531999588, 503 | 'lon': -1.5124200583, 504 | 'cca2': 'BF', 505 | 'region': 'Africa', 506 | 'city': 'Ouagadougou', 507 | }, 508 | { 509 | 'iata': 'PNH', 510 | 'lat': 11.5466003418, 511 | 'lon': 104.84400177, 512 | 'cca2': 'KH', 513 | 'region': 'Asia Pacific', 514 | 'city': 'Phnom Penh', 515 | }, 516 | { 517 | 'iata': 'YYC', 518 | 'lat': 51.113899231, 519 | 'lon': -114.019996643, 520 | 'cca2': 'CA', 521 | 'region': 'North America', 522 | 'city': 'Calgary', 523 | }, 524 | { 525 | 'iata': 'YVR', 526 | 'lat': 49.193901062, 527 | 'lon': -123.183998108, 528 | 'cca2': 'CA', 529 | 'region': 'North America', 530 | 'city': 'Vancouver', 531 | }, 532 | { 533 | 'iata': 'YWG', 534 | 'lat': 49.9099998474, 535 | 'lon': -97.2398986816, 536 | 'cca2': 'CA', 537 | 'region': 'North America', 538 | 'city': 'Winnipeg', 539 | }, 540 | { 541 | 'iata': 'YHZ', 542 | 'lat': 44.64601, 543 | 'lon': -63.66844, 544 | 'cca2': 'CA', 545 | 'region': 'North America', 546 | 'city': 'Halifax', 547 | }, 548 | { 549 | 'iata': 'YOW', 550 | 'lat': 45.3224983215, 551 | 'lon': -75.6691970825, 552 | 'cca2': 'CA', 553 | 'region': 'North America', 554 | 'city': 'Ottawa', 555 | }, 556 | { 557 | 'iata': 'YYZ', 558 | 'lat': 43.6772003174, 559 | 'lon': -79.6305999756, 560 | 'cca2': 'CA', 561 | 'region': 'North America', 562 | 'city': 'Toronto', 563 | }, 564 | { 565 | 'iata': 'YUL', 566 | 'lat': 45.4706001282, 567 | 'lon': -73.7407989502, 568 | 'cca2': 'CA', 569 | 'region': 'North America', 570 | 'city': 'Montréal', 571 | }, 572 | { 573 | 'iata': 'YXE', 574 | 'lat': 52.1707992554, 575 | 'lon': -106.699996948, 576 | 'cca2': 'CA', 577 | 'region': 'North America', 578 | 'city': 'Saskatoon', 579 | }, 580 | { 581 | 'iata': 'ARI', 582 | 'lat': -18.348611, 583 | 'lon': -70.338889, 584 | 'cca2': 'CL', 585 | 'region': 'South America', 586 | 'city': 'Arica', 587 | }, 588 | { 589 | 'iata': 'SCL', 590 | 'lat': -33.3930015564, 591 | 'lon': -70.7857971191, 592 | 'cca2': 'CL', 593 | 'region': 'South America', 594 | 'city': 'Santiago', 595 | }, 596 | { 597 | 'iata': 'BAQ', 598 | 'lat': 10.8896, 599 | 'lon': -74.7808, 600 | 'cca2': 'CO', 601 | 'region': 'South America', 602 | 'city': 'Barranquilla', 603 | }, 604 | { 605 | 'iata': 'BOG', 606 | 'lat': 4.70159, 607 | 'lon': -74.1469, 608 | 'cca2': 'CO', 609 | 'region': 'South America', 610 | 'city': 'Bogota', 611 | }, 612 | { 613 | 'iata': 'MDE', 614 | 'lat': 6.16454, 615 | 'lon': -75.4231, 616 | 'cca2': 'CO', 617 | 'region': 'South America', 618 | 'city': 'Medellín', 619 | }, 620 | { 621 | 'iata': 'FIH', 622 | 'lat': -4.3857498169, 623 | 'lon': 15.4446001053, 624 | 'cca2': 'CD', 625 | 'region': 'Africa', 626 | 'city': 'Kinshasa', 627 | }, 628 | { 629 | 'iata': 'SJO', 630 | 'lat': 9.9938602448, 631 | 'lon': -84.2088012695, 632 | 'cca2': 'CR', 633 | 'region': 'South America', 634 | 'city': 'San José', 635 | }, 636 | { 637 | 'iata': 'ABJ', 638 | 'lat': 5.292598, 639 | 'lon': -3.999133, 640 | 'cca2': 'CI', 641 | 'region': 'Africa', 642 | 'city': 'Abidjan', 643 | }, 644 | { 645 | 'iata': 'ASK', 646 | 'lat': 6.842178, 647 | 'lon': -5.259932, 648 | 'cca2': 'CI', 649 | 'region': 'Africa', 650 | 'city': 'Yamoussoukro', 651 | }, 652 | { 653 | 'iata': 'ZAG', 654 | 'lat': 45.7429008484, 655 | 'lon': 16.0687999725, 656 | 'cca2': 'HR', 657 | 'region': 'Europe', 658 | 'city': 'Zagreb', 659 | }, 660 | { 661 | 'iata': 'LCA', 662 | 'lat': 34.8750991821, 663 | 'lon': 33.6249008179, 664 | 'cca2': 'CY', 665 | 'region': 'Europe', 666 | 'city': 'Nicosia', 667 | }, 668 | { 669 | 'iata': 'PRG', 670 | 'lat': 50.1007995605, 671 | 'lon': 14.2600002289, 672 | 'cca2': 'CZ', 673 | 'region': 'Europe', 674 | 'city': 'Prague', 675 | }, 676 | { 677 | 'iata': 'CPH', 678 | 'lat': 55.6179008484, 679 | 'lon': 12.6560001373, 680 | 'cca2': 'DK', 681 | 'region': 'Europe', 682 | 'city': 'Copenhagen', 683 | }, 684 | { 685 | 'iata': 'JIB', 686 | 'lat': 11.5473003387, 687 | 'lon': 43.1595001221, 688 | 'cca2': 'DJ', 689 | 'region': 'Africa', 690 | 'city': 'Djibouti', 691 | }, 692 | { 693 | 'iata': 'STI', 694 | 'lat': 19.4060993195, 695 | 'lon': -70.6046981812, 696 | 'cca2': 'DO', 697 | 'region': 'North America', 698 | 'city': 'Santiago de los Caballeros', 699 | }, 700 | { 701 | 'iata': 'SDQ', 702 | 'lat': 18.4297008514, 703 | 'lon': -69.6688995361, 704 | 'cca2': 'DO', 705 | 'region': 'North America', 706 | 'city': 'Santo Domingo', 707 | }, 708 | { 709 | 'iata': 'GYE', 710 | 'lat': -2.1894, 711 | 'lon': -79.8891, 712 | 'cca2': 'EC', 713 | 'region': 'South America', 714 | 'city': 'Guayaquil', 715 | }, 716 | { 717 | 'iata': 'UIO', 718 | 'lat': -0.1291666667, 719 | 'lon': -78.3575, 720 | 'cca2': 'EC', 721 | 'region': 'South America', 722 | 'city': 'Quito', 723 | }, 724 | { 725 | 'iata': 'CAI', 726 | 'lat': 30.1219005585, 727 | 'lon': 31.4055995941, 728 | 'cca2': 'EG', 729 | 'region': 'Africa', 730 | 'city': 'Cairo', 731 | }, 732 | { 733 | 'iata': 'TLL', 734 | 'lat': 59.4132995605, 735 | 'lon': 24.8327999115, 736 | 'cca2': 'EE', 737 | 'region': 'Europe', 738 | 'city': 'Tallinn', 739 | }, 740 | { 741 | 'iata': 'SUV', 742 | 'lat': -18.11319, 743 | 'lon': 178.43859, 744 | 'cca2': 'FJ', 745 | 'region': 'Oceania', 746 | 'city': 'Suva', 747 | }, 748 | { 749 | 'iata': 'HEL', 750 | 'lat': 60.317199707, 751 | 'lon': 24.963300705, 752 | 'cca2': 'FI', 753 | 'region': 'Europe', 754 | 'city': 'Helsinki', 755 | }, 756 | { 757 | 'iata': 'BOD', 758 | 'lat': 44.82946, 759 | 'lon': -0.58355, 760 | 'cca2': 'FR', 761 | 'region': 'Europe', 762 | 'city': 'Bordeaux', 763 | }, 764 | { 765 | 'iata': 'LYS', 766 | 'lat': 45.7263, 767 | 'lon': 5.0908, 768 | 'cca2': 'FR', 769 | 'region': 'Europe', 770 | 'city': 'Lyon', 771 | }, 772 | { 773 | 'iata': 'MRS', 774 | 'lat': 43.439271922, 775 | 'lon': 5.2214241028, 776 | 'cca2': 'FR', 777 | 'region': 'Europe', 778 | 'city': 'Marseille', 779 | }, 780 | { 781 | 'iata': 'CDG', 782 | 'lat': 49.0127983093, 783 | 'lon': 2.5499999523, 784 | 'cca2': 'FR', 785 | 'region': 'Europe', 786 | 'city': 'Paris', 787 | }, 788 | { 789 | 'iata': 'PPT', 790 | 'lat': -17.5536994934, 791 | 'lon': -149.606994629, 792 | 'cca2': 'PF', 793 | 'region': 'Oceania', 794 | 'city': 'Tahiti', 795 | }, 796 | { 797 | 'iata': 'TBS', 798 | 'lat': 41.6692008972, 799 | 'lon': 44.95470047, 800 | 'cca2': 'GE', 801 | 'region': 'Europe', 802 | 'city': 'Tbilisi', 803 | }, 804 | { 805 | 'iata': 'TXL', 806 | 'lat': 52.5597000122, 807 | 'lon': 13.2876996994, 808 | 'cca2': 'DE', 809 | 'region': 'Europe', 810 | 'city': 'Berlin', 811 | }, 812 | { 813 | 'iata': 'DUS', 814 | 'lat': 51.2895011902, 815 | 'lon': 6.7667798996, 816 | 'cca2': 'DE', 817 | 'region': 'Europe', 818 | 'city': 'Düsseldorf', 819 | }, 820 | { 821 | 'iata': 'FRA', 822 | 'lat': 50.0264015198, 823 | 'lon': 8.543129921, 824 | 'cca2': 'DE', 825 | 'region': 'Europe', 826 | 'city': 'Frankfurt', 827 | }, 828 | { 829 | 'iata': 'HAM', 830 | 'lat': 53.6304016113, 831 | 'lon': 9.9882297516, 832 | 'cca2': 'DE', 833 | 'region': 'Europe', 834 | 'city': 'Hamburg', 835 | }, 836 | { 837 | 'iata': 'MUC', 838 | 'lat': 48.3538017273, 839 | 'lon': 11.7861003876, 840 | 'cca2': 'DE', 841 | 'region': 'Europe', 842 | 'city': 'Munich', 843 | }, 844 | { 845 | 'iata': 'STR', 846 | 'lat': 48.783333, 847 | 'lon': 9.183333, 848 | 'cca2': 'DE', 849 | 'region': 'Europe', 850 | 'city': 'Stuttgart', 851 | }, 852 | { 853 | 'iata': 'ACC', 854 | 'lat': 5.614818, 855 | 'lon': -0.205874, 856 | 'cca2': 'GH', 857 | 'region': 'Africa', 858 | 'city': 'Accra', 859 | }, 860 | { 861 | 'iata': 'ATH', 862 | 'lat': 37.9364013672, 863 | 'lon': 23.9444999695, 864 | 'cca2': 'GR', 865 | 'region': 'Europe', 866 | 'city': 'Athens', 867 | }, 868 | { 869 | 'iata': 'SKG', 870 | 'lat': 40.5196990967, 871 | 'lon': 22.9708995819, 872 | 'cca2': 'GR', 873 | 'region': 'Europe', 874 | 'city': 'Thessaloniki', 875 | }, 876 | { 877 | 'iata': 'GND', 878 | 'lat': 12.007116, 879 | 'lon': -61.7882288, 880 | 'cca2': 'GD', 881 | 'region': 'South America', 882 | 'city': "St. George's", 883 | }, 884 | { 885 | 'iata': 'GUM', 886 | 'lat': 13.4834003448, 887 | 'lon': 144.796005249, 888 | 'cca2': 'GU', 889 | 'region': 'Asia Pacific', 890 | 'city': 'Hagatna', 891 | }, 892 | { 893 | 'iata': 'GUA', 894 | 'lat': 14.5832996368, 895 | 'lon': -90.5274963379, 896 | 'cca2': 'GT', 897 | 'region': 'North America', 898 | 'city': 'Guatemala City', 899 | }, 900 | { 901 | 'iata': 'GEO', 902 | 'lat': 6.825648, 903 | 'lon': -58.163756, 904 | 'cca2': 'GY', 905 | 'region': 'South America', 906 | 'city': 'Georgetown', 907 | }, 908 | { 909 | 'iata': 'TGU', 910 | 'lat': 14.0608, 911 | 'lon': -87.2172, 912 | 'cca2': 'HN', 913 | 'region': 'South America', 914 | 'city': 'Tegucigalpa', 915 | }, 916 | { 917 | 'iata': 'HKG', 918 | 'lat': 22.3089008331, 919 | 'lon': 113.915000916, 920 | 'cca2': 'HK', 921 | 'region': 'Asia Pacific', 922 | 'city': 'Hong Kong', 923 | }, 924 | { 925 | 'iata': 'BUD', 926 | 'lat': 47.4369010925, 927 | 'lon': 19.2555999756, 928 | 'cca2': 'HU', 929 | 'region': 'Europe', 930 | 'city': 'Budapest', 931 | }, 932 | { 933 | 'iata': 'KEF', 934 | 'lat': 63.9850006104, 935 | 'lon': -22.6056003571, 936 | 'cca2': 'IS', 937 | 'region': 'Europe', 938 | 'city': 'Reykjavík', 939 | }, 940 | { 941 | 'iata': 'AMD', 942 | 'lat': 23.0225, 943 | 'lon': 72.5714, 944 | 'cca2': 'IN', 945 | 'region': 'Asia Pacific', 946 | 'city': 'Ahmedabad', 947 | }, 948 | { 949 | 'iata': 'BLR', 950 | 'lat': 13.7835719, 951 | 'lon': 76.6165937, 952 | 'cca2': 'IN', 953 | 'region': 'Asia Pacific', 954 | 'city': 'Bangalore', 955 | }, 956 | { 957 | 'iata': 'BBI', 958 | 'lat': 20.2961, 959 | 'lon': 85.8245, 960 | 'cca2': 'IN', 961 | 'region': 'Asia Pacific', 962 | 'city': 'Bhubaneswar', 963 | }, 964 | { 965 | 'iata': 'IXC', 966 | 'lat': 30.673500061, 967 | 'lon': 76.7884979248, 968 | 'cca2': 'IN', 969 | 'region': 'Asia Pacific', 970 | 'city': 'Chandigarh', 971 | }, 972 | { 973 | 'iata': 'MAA', 974 | 'lat': 12.9900054932, 975 | 'lon': 80.1692962646, 976 | 'cca2': 'IN', 977 | 'region': 'Asia Pacific', 978 | 'city': 'Chennai', 979 | }, 980 | { 981 | 'iata': 'HYD', 982 | 'lat': 17.2313175201, 983 | 'lon': 78.4298553467, 984 | 'cca2': 'IN', 985 | 'region': 'Asia Pacific', 986 | 'city': 'Hyderabad', 987 | }, 988 | { 989 | 'iata': 'CNN', 990 | 'lat': 11.915858, 991 | 'lon': 75.55094, 992 | 'cca2': 'IN', 993 | 'region': 'Asia Pacific', 994 | 'city': 'Kannur', 995 | }, 996 | { 997 | 'iata': 'KNU', 998 | 'lat': 26.4499, 999 | 'lon': 80.3319, 1000 | 'cca2': 'IN', 1001 | 'region': 'Asia Pacific', 1002 | 'city': 'Kanpur', 1003 | }, 1004 | { 1005 | 'iata': 'COK', 1006 | 'lat': 9.9312, 1007 | 'lon': 76.2673, 1008 | 'cca2': 'IN', 1009 | 'region': 'Asia Pacific', 1010 | 'city': 'Kochi', 1011 | }, 1012 | { 1013 | 'iata': 'CCU', 1014 | 'lat': 22.6476933, 1015 | 'lon': 88.4349249, 1016 | 'cca2': 'IN', 1017 | 'region': 'Asia Pacific', 1018 | 'city': 'Kolkata', 1019 | }, 1020 | { 1021 | 'iata': 'BOM', 1022 | 'lat': 19.0886993408, 1023 | 'lon': 72.8678970337, 1024 | 'cca2': 'IN', 1025 | 'region': 'Asia Pacific', 1026 | 'city': 'Mumbai', 1027 | }, 1028 | { 1029 | 'iata': 'NAG', 1030 | 'lat': 21.1610714, 1031 | 'lon': 79.0024702, 1032 | 'cca2': 'IN', 1033 | 'region': 'Asia Pacific', 1034 | 'city': 'Nagpur', 1035 | }, 1036 | { 1037 | 'iata': 'DEL', 1038 | 'lat': 28.5664997101, 1039 | 'lon': 77.1031036377, 1040 | 'cca2': 'IN', 1041 | 'region': 'Asia Pacific', 1042 | 'city': 'New Delhi', 1043 | }, 1044 | { 1045 | 'iata': 'PAT', 1046 | 'lat': 25.591299057, 1047 | 'lon': 85.0879974365, 1048 | 'cca2': 'IN', 1049 | 'region': 'Asia Pacific', 1050 | 'city': 'Patna', 1051 | }, 1052 | { 1053 | 'iata': 'DPS', 1054 | 'lat': -8.748169899, 1055 | 'lon': 115.1669998169, 1056 | 'cca2': 'ID', 1057 | 'region': 'Asia Pacific', 1058 | 'city': 'Denpasar', 1059 | }, 1060 | { 1061 | 'iata': 'CGK', 1062 | 'lat': -6.1275229, 1063 | 'lon': 106.6515118, 1064 | 'cca2': 'ID', 1065 | 'region': 'Asia Pacific', 1066 | 'city': 'Jakarta', 1067 | }, 1068 | { 1069 | 'iata': 'JOG', 1070 | 'lat': -7.7881798744, 1071 | 'lon': 110.4319992065, 1072 | 'cca2': 'ID', 1073 | 'region': 'Asia Pacific', 1074 | 'city': 'Yogyakarta', 1075 | }, 1076 | { 1077 | 'iata': 'BGW', 1078 | 'lat': 33.2625007629, 1079 | 'lon': 44.2346000671, 1080 | 'cca2': 'IQ', 1081 | 'region': 'Middle East', 1082 | 'city': 'Baghdad', 1083 | }, 1084 | { 1085 | 'iata': 'BSR', 1086 | 'lat': 30.5491008759, 1087 | 'lon': 47.6621017456, 1088 | 'cca2': 'IQ', 1089 | 'region': 'Middle East', 1090 | 'city': 'Basra', 1091 | }, 1092 | { 1093 | 'iata': 'EBL', 1094 | 'lat': 36.1901, 1095 | 'lon': 43.993, 1096 | 'cca2': 'IQ', 1097 | 'region': 'Middle East', 1098 | 'city': 'Erbil', 1099 | }, 1100 | { 1101 | 'iata': 'NJF', 1102 | 'lat': 31.989722, 1103 | 'lon': 44.404167, 1104 | 'cca2': 'IQ', 1105 | 'region': 'Middle East', 1106 | 'city': 'Najaf', 1107 | }, 1108 | { 1109 | 'iata': 'XNH', 1110 | 'lat': 30.9358005524, 1111 | 'lon': 46.0900993347, 1112 | 'cca2': 'IQ', 1113 | 'region': 'Middle East', 1114 | 'city': 'Nasiriyah', 1115 | }, 1116 | { 1117 | 'iata': 'ISU', 1118 | 'lat': 35.5668, 1119 | 'lon': 45.4161, 1120 | 'cca2': 'IQ', 1121 | 'region': 'Middle East', 1122 | 'city': 'Sulaymaniyah', 1123 | }, 1124 | { 1125 | 'iata': 'ORK', 1126 | 'lat': 51.8413009644, 1127 | 'lon': -8.491109848, 1128 | 'cca2': 'IE', 1129 | 'region': 'Europe', 1130 | 'city': 'Cork', 1131 | }, 1132 | { 1133 | 'iata': 'DUB', 1134 | 'lat': 53.4212989807, 1135 | 'lon': -6.270070076, 1136 | 'cca2': 'IE', 1137 | 'region': 'Europe', 1138 | 'city': 'Dublin', 1139 | }, 1140 | { 1141 | 'iata': 'HFA', 1142 | 'lat': 32.78492, 1143 | 'lon': 34.96069, 1144 | 'cca2': 'IL', 1145 | 'region': 'Middle East', 1146 | 'city': 'Haifa', 1147 | }, 1148 | { 1149 | 'iata': 'TLV', 1150 | 'lat': 32.0113983154, 1151 | 'lon': 34.8866996765, 1152 | 'cca2': 'IL', 1153 | 'region': 'Middle East', 1154 | 'city': 'Tel Aviv', 1155 | }, 1156 | { 1157 | 'iata': 'MXP', 1158 | 'lat': 45.6305999756, 1159 | 'lon': 8.7281103134, 1160 | 'cca2': 'IT', 1161 | 'region': 'Europe', 1162 | 'city': 'Milan', 1163 | }, 1164 | { 1165 | 'iata': 'PMO', 1166 | 'lat': 38.16114, 1167 | 'lon': 13.31546, 1168 | 'cca2': 'IT', 1169 | 'region': 'Europe', 1170 | 'city': 'Palermo', 1171 | }, 1172 | { 1173 | 'iata': 'FCO', 1174 | 'lat': 41.8045005798, 1175 | 'lon': 12.2508001328, 1176 | 'cca2': 'IT', 1177 | 'region': 'Europe', 1178 | 'city': 'Rome', 1179 | }, 1180 | { 1181 | 'iata': 'KIN', 1182 | 'lat': 17.9951, 1183 | 'lon': -76.7846, 1184 | 'cca2': 'JM', 1185 | 'region': 'North America', 1186 | 'city': 'Kingston', 1187 | }, 1188 | { 1189 | 'iata': 'FUK', 1190 | 'lat': 33.5902, 1191 | 'lon': 130.4017, 1192 | 'cca2': 'JP', 1193 | 'region': 'Asia Pacific', 1194 | 'city': 'Fukuoka', 1195 | }, 1196 | { 1197 | 'iata': 'OKA', 1198 | 'lat': 26.1958, 1199 | 'lon': 127.646, 1200 | 'cca2': 'JP', 1201 | 'region': 'Asia Pacific', 1202 | 'city': 'Naha', 1203 | }, 1204 | { 1205 | 'iata': 'KIX', 1206 | 'lat': 34.4272994995, 1207 | 'lon': 135.244003296, 1208 | 'cca2': 'JP', 1209 | 'region': 'Asia Pacific', 1210 | 'city': 'Osaka', 1211 | }, 1212 | { 1213 | 'iata': 'NRT', 1214 | 'lat': 35.7647018433, 1215 | 'lon': 140.386001587, 1216 | 'cca2': 'JP', 1217 | 'region': 'Asia Pacific', 1218 | 'city': 'Tokyo', 1219 | }, 1220 | { 1221 | 'iata': 'AMM', 1222 | 'lat': 31.7226009369, 1223 | 'lon': 35.9931983948, 1224 | 'cca2': 'JO', 1225 | 'region': 'Middle East', 1226 | 'city': 'Amman', 1227 | }, 1228 | { 1229 | 'iata': 'ALA', 1230 | 'lat': 43.3521003723, 1231 | 'lon': 77.0404968262, 1232 | 'cca2': 'KZ', 1233 | 'region': 'Asia Pacific', 1234 | 'city': 'Almaty', 1235 | }, 1236 | { 1237 | 'iata': 'MBA', 1238 | 'lat': -4.0348300934, 1239 | 'lon': 39.5942001343, 1240 | 'cca2': 'KE', 1241 | 'region': 'Africa', 1242 | 'city': 'Mombasa', 1243 | }, 1244 | { 1245 | 'iata': 'NBO', 1246 | 'lat': -1.319239974, 1247 | 'lon': 36.9277992249, 1248 | 'cca2': 'KE', 1249 | 'region': 'Africa', 1250 | 'city': 'Nairobi', 1251 | }, 1252 | { 1253 | 'iata': 'ICN', 1254 | 'lat': 37.4691009521, 1255 | 'lon': 126.450996399, 1256 | 'cca2': 'KR', 1257 | 'region': 'Asia Pacific', 1258 | 'city': 'Seoul', 1259 | }, 1260 | { 1261 | 'iata': 'KWI', 1262 | 'lat': 29.226600647, 1263 | 'lon': 47.9688987732, 1264 | 'cca2': 'KW', 1265 | 'region': 'Middle East', 1266 | 'city': 'Kuwait City', 1267 | }, 1268 | { 1269 | 'iata': 'VTE', 1270 | 'lat': 17.9757, 1271 | 'lon': 102.5683, 1272 | 'cca2': 'LA', 1273 | 'region': 'Asia Pacific', 1274 | 'city': 'Vientiane', 1275 | }, 1276 | { 1277 | 'iata': 'RIX', 1278 | 'lat': 56.9235992432, 1279 | 'lon': 23.9710998535, 1280 | 'cca2': 'LV', 1281 | 'region': 'Europe', 1282 | 'city': 'Riga', 1283 | }, 1284 | { 1285 | 'iata': 'BEY', 1286 | 'lat': 33.8208999634, 1287 | 'lon': 35.4883995056, 1288 | 'cca2': 'LB', 1289 | 'region': 'Middle East', 1290 | 'city': 'Beirut', 1291 | }, 1292 | { 1293 | 'iata': 'VNO', 1294 | 'lat': 54.6341018677, 1295 | 'lon': 25.2858009338, 1296 | 'cca2': 'LT', 1297 | 'region': 'Europe', 1298 | 'city': 'Vilnius', 1299 | }, 1300 | { 1301 | 'iata': 'LUX', 1302 | 'lat': 49.6265983582, 1303 | 'lon': 6.211520195, 1304 | 'cca2': 'LU', 1305 | 'region': 'Europe', 1306 | 'city': 'Luxembourg City', 1307 | }, 1308 | { 1309 | 'iata': 'MFM', 1310 | 'lat': 22.1495990753, 1311 | 'lon': 113.592002869, 1312 | 'cca2': 'MO', 1313 | 'region': 'Asia Pacific', 1314 | 'city': 'Macau', 1315 | }, 1316 | { 1317 | 'iata': 'TNR', 1318 | 'lat': -18.91368, 1319 | 'lon': 47.53613, 1320 | 'cca2': 'MG', 1321 | 'region': 'Africa', 1322 | 'city': 'Antananarivo', 1323 | }, 1324 | { 1325 | 'iata': 'JHB', 1326 | 'lat': 1.635848, 1327 | 'lon': 103.665943, 1328 | 'cca2': 'MY', 1329 | 'region': 'Asia Pacific', 1330 | 'city': 'Johor Bahru', 1331 | }, 1332 | { 1333 | 'iata': 'KUL', 1334 | 'lat': 2.745579958, 1335 | 'lon': 101.709999084, 1336 | 'cca2': 'MY', 1337 | 'region': 'Asia Pacific', 1338 | 'city': 'Kuala Lumpur', 1339 | }, 1340 | { 1341 | 'iata': 'MLE', 1342 | 'lat': 4.1748, 1343 | 'lon': 73.50888, 1344 | 'cca2': 'MV', 1345 | 'region': 'Asia Pacific', 1346 | 'city': 'Male', 1347 | }, 1348 | { 1349 | 'iata': 'MRU', 1350 | 'lat': -20.4302005768, 1351 | 'lon': 57.6836013794, 1352 | 'cca2': 'MU', 1353 | 'region': 'Africa', 1354 | 'city': 'Port Louis', 1355 | }, 1356 | { 1357 | 'iata': 'GDL', 1358 | 'lat': 20.5217990875, 1359 | 'lon': -103.3109970093, 1360 | 'cca2': 'MX', 1361 | 'region': 'North America', 1362 | 'city': 'Guadalajara', 1363 | }, 1364 | { 1365 | 'iata': 'MEX', 1366 | 'lat': 19.4363002777, 1367 | 'lon': -99.0720977783, 1368 | 'cca2': 'MX', 1369 | 'region': 'North America', 1370 | 'city': 'Mexico City', 1371 | }, 1372 | { 1373 | 'iata': 'QRO', 1374 | 'lat': 20.6173000336, 1375 | 'lon': -100.185997009, 1376 | 'cca2': 'MX', 1377 | 'region': 'North America', 1378 | 'city': 'Queretaro', 1379 | }, 1380 | { 1381 | 'iata': 'KIV', 1382 | 'lat': 46.9277000427, 1383 | 'lon': 28.9309997559, 1384 | 'cca2': 'MD', 1385 | 'region': 'Europe', 1386 | 'city': 'Chișinău', 1387 | }, 1388 | { 1389 | 'iata': 'ULN', 1390 | 'lat': 47.8431015015, 1391 | 'lon': 106.766998291, 1392 | 'cca2': 'MN', 1393 | 'region': 'Asia Pacific', 1394 | 'city': 'Ulaanbaatar', 1395 | }, 1396 | { 1397 | 'iata': 'CMN', 1398 | 'lat': 33.3675003052, 1399 | 'lon': -7.5899701118, 1400 | 'cca2': 'MA', 1401 | 'region': 'Africa', 1402 | 'city': 'Casablanca', 1403 | }, 1404 | { 1405 | 'iata': 'MPM', 1406 | 'lat': -25.9207992554, 1407 | 'lon': 32.5726013184, 1408 | 'cca2': 'MZ', 1409 | 'region': 'Africa', 1410 | 'city': 'Maputo', 1411 | }, 1412 | { 1413 | 'iata': 'MDL', 1414 | 'lat': 21.7051697, 1415 | 'lon': 95.9695206, 1416 | 'cca2': 'MM', 1417 | 'region': 'Asia Pacific', 1418 | 'city': 'Mandalay', 1419 | }, 1420 | { 1421 | 'iata': 'RGN', 1422 | 'lat': 16.9073009491, 1423 | 'lon': 96.1332015991, 1424 | 'cca2': 'MM', 1425 | 'region': 'Asia Pacific', 1426 | 'city': 'Yangon', 1427 | }, 1428 | { 1429 | 'iata': 'WDH', 1430 | 'lat': -22.565587, 1431 | 'lon': 17.085334, 1432 | 'cca2': 'NA', 1433 | 'region': 'Africa', 1434 | 'city': 'Windhoek', 1435 | }, 1436 | { 1437 | 'iata': 'KTM', 1438 | 'lat': 27.6965999603, 1439 | 'lon': 85.3591003418, 1440 | 'cca2': 'NP', 1441 | 'region': 'Asia Pacific', 1442 | 'city': 'Kathmandu', 1443 | }, 1444 | { 1445 | 'iata': 'AMS', 1446 | 'lat': 52.3086013794, 1447 | 'lon': 4.7638897896, 1448 | 'cca2': 'NL', 1449 | 'region': 'Europe', 1450 | 'city': 'Amsterdam', 1451 | }, 1452 | { 1453 | 'iata': 'NOU', 1454 | 'lat': -22.0146007538, 1455 | 'lon': 166.212997436, 1456 | 'cca2': 'NC', 1457 | 'region': 'Oceania', 1458 | 'city': 'Noumea', 1459 | }, 1460 | { 1461 | 'iata': 'AKL', 1462 | 'lat': -37.0080986023, 1463 | 'lon': 174.792007446, 1464 | 'cca2': 'NZ', 1465 | 'region': 'Oceania', 1466 | 'city': 'Auckland', 1467 | }, 1468 | { 1469 | 'iata': 'CHC', 1470 | 'lat': -43.4893989563, 1471 | 'lon': 172.5319976807, 1472 | 'cca2': 'NZ', 1473 | 'region': 'Oceania', 1474 | 'city': 'Christchurch', 1475 | }, 1476 | { 1477 | 'iata': 'LOS', 1478 | 'lat': 6.5773701668, 1479 | 'lon': 3.321160078, 1480 | 'cca2': 'NG', 1481 | 'region': 'Africa', 1482 | 'city': 'Lagos', 1483 | }, 1484 | { 1485 | 'iata': 'SKP', 1486 | 'lat': 41.9616012573, 1487 | 'lon': 21.6214008331, 1488 | 'cca2': 'MK', 1489 | 'region': 'Europe', 1490 | 'city': 'Skopje', 1491 | }, 1492 | { 1493 | 'iata': 'OSL', 1494 | 'lat': 60.193901062, 1495 | 'lon': 11.100399971, 1496 | 'cca2': 'NO', 1497 | 'region': 'Europe', 1498 | 'city': 'Oslo', 1499 | }, 1500 | { 1501 | 'iata': 'MCT', 1502 | 'lat': 23.5932998657, 1503 | 'lon': 58.2844009399, 1504 | 'cca2': 'OM', 1505 | 'region': 'Middle East', 1506 | 'city': 'Muscat', 1507 | }, 1508 | { 1509 | 'iata': 'ISB', 1510 | 'lat': 33.6166992188, 1511 | 'lon': 73.0991973877, 1512 | 'cca2': 'PK', 1513 | 'region': 'Asia Pacific', 1514 | 'city': 'Islamabad', 1515 | }, 1516 | { 1517 | 'iata': 'KHI', 1518 | 'lat': 24.9064998627, 1519 | 'lon': 67.1607971191, 1520 | 'cca2': 'PK', 1521 | 'region': 'Asia Pacific', 1522 | 'city': 'Karachi', 1523 | }, 1524 | { 1525 | 'iata': 'LHE', 1526 | 'lat': 31.5216007233, 1527 | 'lon': 74.4036026001, 1528 | 'cca2': 'PK', 1529 | 'region': 'Asia Pacific', 1530 | 'city': 'Lahore', 1531 | }, 1532 | { 1533 | 'iata': 'ZDM', 1534 | 'lat': 32.2719, 1535 | 'lon': 35.0194, 1536 | 'cca2': 'PS', 1537 | 'region': 'Middle East', 1538 | 'city': 'Ramallah', 1539 | }, 1540 | { 1541 | 'iata': 'PTY', 1542 | 'lat': 9.0713596344, 1543 | 'lon': -79.3834991455, 1544 | 'cca2': 'PA', 1545 | 'region': 'South America', 1546 | 'city': 'Panama City', 1547 | }, 1548 | { 1549 | 'iata': 'ASU', 1550 | 'lat': -25.2399997711, 1551 | 'lon': -57.5200004578, 1552 | 'cca2': 'PY', 1553 | 'region': 'South America', 1554 | 'city': 'Asunción', 1555 | }, 1556 | { 1557 | 'iata': 'LIM', 1558 | 'lat': -12.021900177, 1559 | 'lon': -77.1143035889, 1560 | 'cca2': 'PE', 1561 | 'region': 'South America', 1562 | 'city': 'Lima', 1563 | }, 1564 | { 1565 | 'iata': 'CGY', 1566 | 'lat': 8.4156198502, 1567 | 'lon': 124.611000061, 1568 | 'cca2': 'PH', 1569 | 'region': 'Asia Pacific', 1570 | 'city': 'Cagayan de Oro', 1571 | }, 1572 | { 1573 | 'iata': 'CEB', 1574 | 'lat': 10.3074998856, 1575 | 'lon': 123.978996277, 1576 | 'cca2': 'PH', 1577 | 'region': 'Asia Pacific', 1578 | 'city': 'Cebu', 1579 | }, 1580 | { 1581 | 'iata': 'MNL', 1582 | 'lat': 14.508600235, 1583 | 'lon': 121.019996643, 1584 | 'cca2': 'PH', 1585 | 'region': 'Asia Pacific', 1586 | 'city': 'Manila', 1587 | }, 1588 | { 1589 | 'iata': 'CRK', 1590 | 'lat': 15.1859, 1591 | 'lon': 120.5599, 1592 | 'cca2': 'PH', 1593 | 'region': 'Asia Pacific', 1594 | 'city': 'Tarlac City', 1595 | }, 1596 | { 1597 | 'iata': 'WAW', 1598 | 'lat': 52.1656990051, 1599 | 'lon': 20.9671001434, 1600 | 'cca2': 'PL', 1601 | 'region': 'Europe', 1602 | 'city': 'Warsaw', 1603 | }, 1604 | { 1605 | 'iata': 'LIS', 1606 | 'lat': 38.7812995911, 1607 | 'lon': -9.1359195709, 1608 | 'cca2': 'PT', 1609 | 'region': 'Europe', 1610 | 'city': 'Lisbon', 1611 | }, 1612 | { 1613 | 'iata': 'SJU', 1614 | 'lat': 18.411391, 1615 | 'lon': -66.102793, 1616 | 'cca2': 'PR', 1617 | 'region': 'North America', 1618 | 'city': 'San Juan', 1619 | }, 1620 | { 1621 | 'iata': 'DOH', 1622 | 'lat': 25.2605946, 1623 | 'lon': 51.6137665, 1624 | 'cca2': 'QA', 1625 | 'region': 'Middle East', 1626 | 'city': 'Doha', 1627 | }, 1628 | { 1629 | 'iata': 'RUN', 1630 | 'lat': -20.8871002197, 1631 | 'lon': 55.5102996826, 1632 | 'cca2': 'RE', 1633 | 'region': 'Africa', 1634 | 'city': 'Saint-Denis', 1635 | }, 1636 | { 1637 | 'iata': 'OTP', 1638 | 'lat': 44.5722007751, 1639 | 'lon': 26.1021995544, 1640 | 'cca2': 'RO', 1641 | 'region': 'Europe', 1642 | 'city': 'Bucharest', 1643 | }, 1644 | { 1645 | 'iata': 'KJA', 1646 | 'lat': 56.0153, 1647 | 'lon': 92.8932, 1648 | 'cca2': 'RU', 1649 | 'region': 'Asia Pacific', 1650 | 'city': 'Krasnoyarsk', 1651 | }, 1652 | { 1653 | 'iata': 'DME', 1654 | 'lat': 55.4087982178, 1655 | 'lon': 37.9062995911, 1656 | 'cca2': 'RU', 1657 | 'region': 'Europe', 1658 | 'city': 'Moscow', 1659 | }, 1660 | { 1661 | 'iata': 'LED', 1662 | 'lat': 59.8003005981, 1663 | 'lon': 30.2625007629, 1664 | 'cca2': 'RU', 1665 | 'region': 'Europe', 1666 | 'city': 'Saint Petersburg', 1667 | }, 1668 | { 1669 | 'iata': 'KLD', 1670 | 'lat': 56.8587, 1671 | 'lon': 35.9176, 1672 | 'cca2': 'RU', 1673 | 'region': 'Europe', 1674 | 'city': 'Tver', 1675 | }, 1676 | { 1677 | 'iata': 'SVX', 1678 | 'lat': 56.8431, 1679 | 'lon': 60.6454, 1680 | 'cca2': 'RU', 1681 | 'region': 'Asia Pacific', 1682 | 'city': 'Yekaterinburg', 1683 | }, 1684 | { 1685 | 'iata': 'KGL', 1686 | 'lat': -1.9686299563, 1687 | 'lon': 30.1394996643, 1688 | 'cca2': 'RW', 1689 | 'region': 'Africa', 1690 | 'city': 'Kigali', 1691 | }, 1692 | { 1693 | 'iata': 'DMM', 1694 | 'lat': 26.471200943, 1695 | 'lon': 49.7979011536, 1696 | 'cca2': 'SA', 1697 | 'region': 'Middle East', 1698 | 'city': 'Dammam', 1699 | }, 1700 | { 1701 | 'iata': 'JED', 1702 | 'lat': 21.679599762, 1703 | 'lon': 39.15650177, 1704 | 'cca2': 'SA', 1705 | 'region': 'Middle East', 1706 | 'city': 'Jeddah', 1707 | }, 1708 | { 1709 | 'iata': 'RUH', 1710 | 'lat': 24.9575996399, 1711 | 'lon': 46.6987991333, 1712 | 'cca2': 'SA', 1713 | 'region': 'Middle East', 1714 | 'city': 'Riyadh', 1715 | }, 1716 | { 1717 | 'iata': 'DKR', 1718 | 'lat': 14.7412099, 1719 | 'lon': -17.4889771, 1720 | 'cca2': 'SN', 1721 | 'region': 'Africa', 1722 | 'city': 'Dakar', 1723 | }, 1724 | { 1725 | 'iata': 'BEG', 1726 | 'lat': 44.8184013367, 1727 | 'lon': 20.3090991974, 1728 | 'cca2': 'RS', 1729 | 'region': 'Europe', 1730 | 'city': 'Belgrade', 1731 | }, 1732 | { 1733 | 'iata': 'SIN', 1734 | 'lat': 1.3501900434, 1735 | 'lon': 103.994003296, 1736 | 'cca2': 'SG', 1737 | 'region': 'Asia Pacific', 1738 | 'city': 'Singapore', 1739 | }, 1740 | { 1741 | 'iata': 'BTS', 1742 | 'lat': 48.1486, 1743 | 'lon': 17.1077, 1744 | 'cca2': 'SK', 1745 | 'region': 'Europe', 1746 | 'city': 'Bratislava', 1747 | }, 1748 | { 1749 | 'iata': 'CPT', 1750 | 'lat': -33.9648017883, 1751 | 'lon': 18.6016998291, 1752 | 'cca2': 'ZA', 1753 | 'region': 'Africa', 1754 | 'city': 'Cape Town', 1755 | }, 1756 | { 1757 | 'iata': 'DUR', 1758 | 'lat': -29.6144444444, 1759 | 'lon': 31.1197222222, 1760 | 'cca2': 'ZA', 1761 | 'region': 'Africa', 1762 | 'city': 'Durban', 1763 | }, 1764 | { 1765 | 'iata': 'JNB', 1766 | 'lat': -26.133333, 1767 | 'lon': 28.25, 1768 | 'cca2': 'ZA', 1769 | 'region': 'Africa', 1770 | 'city': 'Johannesburg', 1771 | }, 1772 | { 1773 | 'iata': 'BCN', 1774 | 'lat': 41.2971000671, 1775 | 'lon': 2.0784599781, 1776 | 'cca2': 'ES', 1777 | 'region': 'Europe', 1778 | 'city': 'Barcelona', 1779 | }, 1780 | { 1781 | 'iata': 'MAD', 1782 | 'lat': 40.4936, 1783 | 'lon': -3.56676, 1784 | 'cca2': 'ES', 1785 | 'region': 'Europe', 1786 | 'city': 'Madrid', 1787 | }, 1788 | { 1789 | 'iata': 'CMB', 1790 | 'lat': 7.1807599068, 1791 | 'lon': 79.8841018677, 1792 | 'cca2': 'LK', 1793 | 'region': 'Asia Pacific', 1794 | 'city': 'Colombo', 1795 | }, 1796 | { 1797 | 'iata': 'PBM', 1798 | 'lat': 5.452831, 1799 | 'lon': -55.187783, 1800 | 'cca2': 'SR', 1801 | 'region': 'South America', 1802 | 'city': 'Paramaribo', 1803 | }, 1804 | { 1805 | 'iata': 'GOT', 1806 | 'lat': 57.6627998352, 1807 | 'lon': 12.279800415, 1808 | 'cca2': 'SE', 1809 | 'region': 'Europe', 1810 | 'city': 'Gothenburg', 1811 | }, 1812 | { 1813 | 'iata': 'ARN', 1814 | 'lat': 59.6519012451, 1815 | 'lon': 17.9186000824, 1816 | 'cca2': 'SE', 1817 | 'region': 'Europe', 1818 | 'city': 'Stockholm', 1819 | }, 1820 | { 1821 | 'iata': 'GVA', 1822 | 'lat': 46.2380981445, 1823 | 'lon': 6.1089501381, 1824 | 'cca2': 'CH', 1825 | 'region': 'Europe', 1826 | 'city': 'Geneva', 1827 | }, 1828 | { 1829 | 'iata': 'ZRH', 1830 | 'lat': 47.4646987915, 1831 | 'lon': 8.5491695404, 1832 | 'cca2': 'CH', 1833 | 'region': 'Europe', 1834 | 'city': 'Zurich', 1835 | }, 1836 | { 1837 | 'iata': 'KHH', 1838 | 'lat': 22.5771007538, 1839 | 'lon': 120.3499984741, 1840 | 'cca2': 'TW', 1841 | 'region': 'Asia Pacific', 1842 | 'city': 'Kaohsiung City', 1843 | }, 1844 | { 1845 | 'iata': 'TPE', 1846 | 'lat': 25.0776996613, 1847 | 'lon': 121.233001709, 1848 | 'cca2': 'TW', 1849 | 'region': 'Asia Pacific', 1850 | 'city': 'Taipei', 1851 | }, 1852 | { 1853 | 'iata': 'DAR', 1854 | 'lat': -6.8781099319, 1855 | 'lon': 39.2025985718, 1856 | 'cca2': 'TZ', 1857 | 'region': 'Africa', 1858 | 'city': 'Dar es Salaam', 1859 | }, 1860 | { 1861 | 'iata': 'BKK', 1862 | 'lat': 13.6810998917, 1863 | 'lon': 100.747001648, 1864 | 'cca2': 'TH', 1865 | 'region': 'Asia Pacific', 1866 | 'city': 'Bangkok', 1867 | }, 1868 | { 1869 | 'iata': 'CNX', 1870 | 'lat': 18.7667999268, 1871 | 'lon': 98.962600708, 1872 | 'cca2': 'TH', 1873 | 'region': 'Asia Pacific', 1874 | 'city': 'Chiang Mai', 1875 | }, 1876 | { 1877 | 'iata': 'URT', 1878 | 'lat': 9.1325998306, 1879 | 'lon': 99.135597229, 1880 | 'cca2': 'TH', 1881 | 'region': 'Asia Pacific', 1882 | 'city': 'Surat Thani', 1883 | }, 1884 | { 1885 | 'iata': 'POS', 1886 | 'lat': 10.5953998566, 1887 | 'lon': -61.3372001648, 1888 | 'cca2': 'TT', 1889 | 'region': 'South America', 1890 | 'city': 'Port of Spain', 1891 | }, 1892 | { 1893 | 'iata': 'TUN', 1894 | 'lat': 36.8510017395, 1895 | 'lon': 10.2271995544, 1896 | 'cca2': 'TN', 1897 | 'region': 'Africa', 1898 | 'city': 'Tunis', 1899 | }, 1900 | { 1901 | 'iata': 'IST', 1902 | 'lat': 40.9768981934, 1903 | 'lon': 28.8145999908, 1904 | 'cca2': 'TR', 1905 | 'region': 'Europe', 1906 | 'city': 'Istanbul', 1907 | }, 1908 | { 1909 | 'iata': 'ADB', 1910 | 'lat': 38.32377, 1911 | 'lon': 27.14317, 1912 | 'cca2': 'TR', 1913 | 'region': 'Europe', 1914 | 'city': 'Izmir', 1915 | }, 1916 | { 1917 | 'iata': 'EBB', 1918 | 'lat': 0.3152, 1919 | 'lon': 32.5816, 1920 | 'cca2': 'UG', 1921 | 'region': 'Africa', 1922 | 'city': 'KAMPALA', 1923 | }, 1924 | { 1925 | 'iata': 'KBP', 1926 | 'lat': 50.3450012207, 1927 | 'lon': 30.8946990967, 1928 | 'cca2': 'UA', 1929 | 'region': 'Europe', 1930 | 'city': 'Kyiv', 1931 | }, 1932 | { 1933 | 'iata': 'DXB', 1934 | 'lat': 25.2527999878, 1935 | 'lon': 55.3643989563, 1936 | 'cca2': 'AE', 1937 | 'region': 'Middle East', 1938 | 'city': 'Dubai', 1939 | }, 1940 | { 1941 | 'iata': 'EDI', 1942 | 'lat': 55.9500007629, 1943 | 'lon': -3.3724999428, 1944 | 'cca2': 'GB', 1945 | 'region': 'Europe', 1946 | 'city': 'Edinburgh', 1947 | }, 1948 | { 1949 | 'iata': 'LHR', 1950 | 'lat': 51.4706001282, 1951 | 'lon': -0.4619410038, 1952 | 'cca2': 'GB', 1953 | 'region': 'Europe', 1954 | 'city': 'London', 1955 | }, 1956 | { 1957 | 'iata': 'MAN', 1958 | 'lat': 53.3536987305, 1959 | 'lon': -2.2749500275, 1960 | 'cca2': 'GB', 1961 | 'region': 'Europe', 1962 | 'city': 'Manchester', 1963 | }, 1964 | { 1965 | 'iata': 'MGM', 1966 | 'lat': 32.30059814, 1967 | 'lon': -86.39399719, 1968 | 'cca2': 'US', 1969 | 'region': 'North America', 1970 | 'city': 'Montgomery', 1971 | }, 1972 | { 1973 | 'iata': 'ANC', 1974 | 'lat': 61.158555, 1975 | 'lon': -149.890208, 1976 | 'cca2': 'US', 1977 | 'region': 'North America', 1978 | 'city': 'Anchorage', 1979 | }, 1980 | { 1981 | 'iata': 'PHX', 1982 | 'lat': 33.434299469, 1983 | 'lon': -112.012001038, 1984 | 'cca2': 'US', 1985 | 'region': 'North America', 1986 | 'city': 'Phoenix', 1987 | }, 1988 | { 1989 | 'iata': 'LAX', 1990 | 'lat': 33.94250107, 1991 | 'lon': -118.4079971, 1992 | 'cca2': 'US', 1993 | 'region': 'North America', 1994 | 'city': 'Los Angeles', 1995 | }, 1996 | { 1997 | 'iata': 'SMF', 1998 | 'lat': 38.695400238, 1999 | 'lon': -121.591003418, 2000 | 'cca2': 'US', 2001 | 'region': 'North America', 2002 | 'city': 'Sacramento', 2003 | }, 2004 | { 2005 | 'iata': 'SAN', 2006 | 'lat': 32.7336006165, 2007 | 'lon': -117.190002441, 2008 | 'cca2': 'US', 2009 | 'region': 'North America', 2010 | 'city': 'San Diego', 2011 | }, 2012 | { 2013 | 'iata': 'SFO', 2014 | 'lat': 37.6189994812, 2015 | 'lon': -122.375, 2016 | 'cca2': 'US', 2017 | 'region': 'North America', 2018 | 'city': 'San Francisco', 2019 | }, 2020 | { 2021 | 'iata': 'SJC', 2022 | 'lat': 37.3625984192, 2023 | 'lon': -121.929000855, 2024 | 'cca2': 'US', 2025 | 'region': 'North America', 2026 | 'city': 'San Jose', 2027 | }, 2028 | { 2029 | 'iata': 'DEN', 2030 | 'lat': 39.8616981506, 2031 | 'lon': -104.672996521, 2032 | 'cca2': 'US', 2033 | 'region': 'North America', 2034 | 'city': 'Denver', 2035 | }, 2036 | { 2037 | 'iata': 'JAX', 2038 | 'lat': 30.4941005707, 2039 | 'lon': -81.6878967285, 2040 | 'cca2': 'US', 2041 | 'region': 'North America', 2042 | 'city': 'Jacksonville', 2043 | }, 2044 | { 2045 | 'iata': 'MIA', 2046 | 'lat': 25.7931995392, 2047 | 'lon': -80.2906036377, 2048 | 'cca2': 'US', 2049 | 'region': 'North America', 2050 | 'city': 'Miami', 2051 | }, 2052 | { 2053 | 'iata': 'TLH', 2054 | 'lat': 30.3964996338, 2055 | 'lon': -84.3503036499, 2056 | 'cca2': 'US', 2057 | 'region': 'North America', 2058 | 'city': 'Tallahassee', 2059 | }, 2060 | { 2061 | 'iata': 'TPA', 2062 | 'lat': 27.9755001068, 2063 | 'lon': -82.533203125, 2064 | 'cca2': 'US', 2065 | 'region': 'North America', 2066 | 'city': 'Tampa', 2067 | }, 2068 | { 2069 | 'iata': 'ATL', 2070 | 'lat': 33.6366996765, 2071 | 'lon': -84.4281005859, 2072 | 'cca2': 'US', 2073 | 'region': 'North America', 2074 | 'city': 'Atlanta', 2075 | }, 2076 | { 2077 | 'iata': 'HNL', 2078 | 'lat': 21.3187007904, 2079 | 'lon': -157.9219970703, 2080 | 'cca2': 'US', 2081 | 'region': 'North America', 2082 | 'city': 'Honolulu', 2083 | }, 2084 | { 2085 | 'iata': 'ORD', 2086 | 'lat': 41.97859955, 2087 | 'lon': -87.90480042, 2088 | 'cca2': 'US', 2089 | 'region': 'North America', 2090 | 'city': 'Chicago', 2091 | }, 2092 | { 2093 | 'iata': 'IND', 2094 | 'lat': 39.717300415, 2095 | 'lon': -86.2944030762, 2096 | 'cca2': 'US', 2097 | 'region': 'North America', 2098 | 'city': 'Indianapolis', 2099 | }, 2100 | { 2101 | 'iata': 'BGR', 2102 | 'lat': 44.8081, 2103 | 'lon': -68.795, 2104 | 'cca2': 'US', 2105 | 'region': 'North America', 2106 | 'city': 'Bangor', 2107 | }, 2108 | { 2109 | 'iata': 'BOS', 2110 | 'lat': 42.36429977, 2111 | 'lon': -71.00520325, 2112 | 'cca2': 'US', 2113 | 'region': 'North America', 2114 | 'city': 'Boston', 2115 | }, 2116 | { 2117 | 'iata': 'DTW', 2118 | 'lat': 42.2123985291, 2119 | 'lon': -83.3534011841, 2120 | 'cca2': 'US', 2121 | 'region': 'North America', 2122 | 'city': 'Detroit', 2123 | }, 2124 | { 2125 | 'iata': 'MSP', 2126 | 'lat': 44.8819999695, 2127 | 'lon': -93.2218017578, 2128 | 'cca2': 'US', 2129 | 'region': 'North America', 2130 | 'city': 'Minneapolis', 2131 | }, 2132 | { 2133 | 'iata': 'MCI', 2134 | 'lat': 39.2975997925, 2135 | 'lon': -94.7138977051, 2136 | 'cca2': 'US', 2137 | 'region': 'North America', 2138 | 'city': 'Kansas City', 2139 | }, 2140 | { 2141 | 'iata': 'STL', 2142 | 'lat': 38.7486991882, 2143 | 'lon': -90.3700027466, 2144 | 'cca2': 'US', 2145 | 'region': 'North America', 2146 | 'city': 'St. Louis', 2147 | }, 2148 | { 2149 | 'iata': 'OMA', 2150 | 'lat': 41.3031997681, 2151 | 'lon': -95.8940963745, 2152 | 'cca2': 'US', 2153 | 'region': 'North America', 2154 | 'city': 'Omaha', 2155 | }, 2156 | { 2157 | 'iata': 'LAS', 2158 | 'lat': 36.08010101, 2159 | 'lon': -115.1520004, 2160 | 'cca2': 'US', 2161 | 'region': 'North America', 2162 | 'city': 'Las Vegas', 2163 | }, 2164 | { 2165 | 'iata': 'EWR', 2166 | 'lat': 40.6925010681, 2167 | 'lon': -74.1687011719, 2168 | 'cca2': 'US', 2169 | 'region': 'North America', 2170 | 'city': 'Newark', 2171 | }, 2172 | { 2173 | 'iata': 'ABQ', 2174 | 'lat': 35.0844, 2175 | 'lon': -106.6504, 2176 | 'cca2': 'US', 2177 | 'region': 'North America', 2178 | 'city': 'Albuquerque', 2179 | }, 2180 | { 2181 | 'iata': 'BUF', 2182 | 'lat': 42.94049835, 2183 | 'lon': -78.73220062, 2184 | 'cca2': 'US', 2185 | 'region': 'North America', 2186 | 'city': 'Buffalo', 2187 | }, 2188 | { 2189 | 'iata': 'CLT', 2190 | 'lat': 35.2140007019, 2191 | 'lon': -80.9430999756, 2192 | 'cca2': 'US', 2193 | 'region': 'North America', 2194 | 'city': 'Charlotte', 2195 | }, 2196 | { 2197 | 'iata': 'RDU', 2198 | 'lat': 35.93543, 2199 | 'lon': -78.88075, 2200 | 'cca2': 'US', 2201 | 'region': 'North America', 2202 | 'city': 'Durham', 2203 | }, 2204 | { 2205 | 'iata': 'CLE', 2206 | 'lat': 41.50069, 2207 | 'lon': -81.68412, 2208 | 'cca2': 'US', 2209 | 'region': 'North America', 2210 | 'city': 'Cleveland', 2211 | }, 2212 | { 2213 | 'iata': 'CMH', 2214 | 'lat': 39.9980010986, 2215 | 'lon': -82.8918991089, 2216 | 'cca2': 'US', 2217 | 'region': 'North America', 2218 | 'city': 'Columbus', 2219 | }, 2220 | { 2221 | 'iata': 'OKC', 2222 | 'lat': 35.46655, 2223 | 'lon': -97.65373, 2224 | 'cca2': 'US', 2225 | 'region': 'North America', 2226 | 'city': 'Oklahoma City', 2227 | }, 2228 | { 2229 | 'iata': 'PDX', 2230 | 'lat': 45.58869934, 2231 | 'lon': -122.5979996, 2232 | 'cca2': 'US', 2233 | 'region': 'North America', 2234 | 'city': 'Portland', 2235 | }, 2236 | { 2237 | 'iata': 'PHL', 2238 | 'lat': 39.8718986511, 2239 | 'lon': -75.2410964966, 2240 | 'cca2': 'US', 2241 | 'region': 'North America', 2242 | 'city': 'Philadelphia', 2243 | }, 2244 | { 2245 | 'iata': 'PIT', 2246 | 'lat': 40.49150085, 2247 | 'lon': -80.23290253, 2248 | 'cca2': 'US', 2249 | 'region': 'North America', 2250 | 'city': 'Pittsburgh', 2251 | }, 2252 | { 2253 | 'iata': 'FSD', 2254 | 'lat': 43.540819819502, 2255 | 'lon': -96.65511577730963, 2256 | 'cca2': 'US', 2257 | 'region': 'North America', 2258 | 'city': 'Sioux Falls', 2259 | }, 2260 | { 2261 | 'iata': 'MEM', 2262 | 'lat': 35.0424003601, 2263 | 'lon': -89.9766998291, 2264 | 'cca2': 'US', 2265 | 'region': 'North America', 2266 | 'city': 'Memphis', 2267 | }, 2268 | { 2269 | 'iata': 'BNA', 2270 | 'lat': 36.1245002747, 2271 | 'lon': -86.6781997681, 2272 | 'cca2': 'US', 2273 | 'region': 'North America', 2274 | 'city': 'Nashville', 2275 | }, 2276 | { 2277 | 'iata': 'AUS', 2278 | 'lat': 30.1975, 2279 | 'lon': -97.6664, 2280 | 'cca2': 'US', 2281 | 'region': 'North America', 2282 | 'city': 'Austin', 2283 | }, 2284 | { 2285 | 'iata': 'DFW', 2286 | 'lat': 32.8968009949, 2287 | 'lon': -97.0380020142, 2288 | 'cca2': 'US', 2289 | 'region': 'North America', 2290 | 'city': 'Dallas', 2291 | }, 2292 | { 2293 | 'iata': 'IAH', 2294 | 'lat': 29.9843997955, 2295 | 'lon': -95.3414001465, 2296 | 'cca2': 'US', 2297 | 'region': 'North America', 2298 | 'city': 'Houston', 2299 | }, 2300 | { 2301 | 'iata': 'MFE', 2302 | 'lat': 26.17580032, 2303 | 'lon': -98.23860168, 2304 | 'cca2': 'US', 2305 | 'region': 'North America', 2306 | 'city': 'McAllen', 2307 | }, 2308 | { 2309 | 'iata': 'SAT', 2310 | 'lat': 29.429461, 2311 | 'lon': -98.487061, 2312 | 'cca2': 'US', 2313 | 'region': 'North America', 2314 | 'city': 'San Antonio', 2315 | }, 2316 | { 2317 | 'iata': 'SLC', 2318 | 'lat': 40.7883987427, 2319 | 'lon': -111.977996826, 2320 | 'cca2': 'US', 2321 | 'region': 'North America', 2322 | 'city': 'Salt Lake City', 2323 | }, 2324 | { 2325 | 'iata': 'IAD', 2326 | 'lat': 38.94449997, 2327 | 'lon': -77.45580292, 2328 | 'cca2': 'US', 2329 | 'region': 'North America', 2330 | 'city': 'Ashburn', 2331 | }, 2332 | { 2333 | 'iata': 'ORF', 2334 | 'lat': 36.8945999146, 2335 | 'lon': -76.2012023926, 2336 | 'cca2': 'US', 2337 | 'region': 'North America', 2338 | 'city': 'Norfolk', 2339 | }, 2340 | { 2341 | 'iata': 'RIC', 2342 | 'lat': 37.5051994324, 2343 | 'lon': -77.3197021484, 2344 | 'cca2': 'US', 2345 | 'region': 'North America', 2346 | 'city': 'Richmond', 2347 | }, 2348 | { 2349 | 'iata': 'SEA', 2350 | 'lat': 47.4490013123, 2351 | 'lon': -122.308998108, 2352 | 'cca2': 'US', 2353 | 'region': 'North America', 2354 | 'city': 'Seattle', 2355 | }, 2356 | { 2357 | 'iata': 'TAS', 2358 | 'lat': 41.257900238, 2359 | 'lon': 69.2811965942, 2360 | 'cca2': 'UZ', 2361 | 'region': 'Asia Pacific', 2362 | 'city': 'Tashkent', 2363 | }, 2364 | { 2365 | 'iata': 'DAD', 2366 | 'lat': 16.02636, 2367 | 'lon': 108.20869, 2368 | 'cca2': 'VN', 2369 | 'region': 'Asia Pacific', 2370 | 'city': 'Da Nang', 2371 | }, 2372 | { 2373 | 'iata': 'HAN', 2374 | 'lat': 21.221200943, 2375 | 'lon': 105.806999206, 2376 | 'cca2': 'VN', 2377 | 'region': 'Asia Pacific', 2378 | 'city': 'Hanoi', 2379 | }, 2380 | { 2381 | 'iata': 'SGN', 2382 | 'lat': 10.8187999725, 2383 | 'lon': 106.652000427, 2384 | 'cca2': 'VN', 2385 | 'region': 'Asia Pacific', 2386 | 'city': 'Ho Chi Minh City', 2387 | }, 2388 | { 2389 | 'iata': 'HRE', 2390 | 'lat': -17.9318008423, 2391 | 'lon': 31.0928001404, 2392 | 'cca2': 'ZW', 2393 | 'region': 'Africa', 2394 | 'city': 'Harare', 2395 | }, 2396 | ] 2397 | --------------------------------------------------------------------------------