diff --git a/octodns/record/geo.py b/octodns/record/geo.py new file mode 100644 index 0000000..9a91c0e --- /dev/null +++ b/octodns/record/geo.py @@ -0,0 +1,34 @@ +# +# +# + +from .geo_data import geo_data + + +class GeoCodes(object): + __COUNTRIES = None + + @classmethod + def validate(cls, code): + ''' + Validates an octoDNS geo code making sure that it is a valid and + corresponding: + * continent + * continent & country + * continent, country, & province + ''' + reasons = [] + + pieces = code.split('-') + n = len(pieces) + if n > 3: + reasons.append('Invalid geo code "{}"'.format(code)) + elif n > 0 and pieces[0] not in geo_data: + reasons.append('Unknown continent code "{}"'.format(code)) + elif n > 1 and pieces[1] not in geo_data[pieces[0]]: + reasons.append('Unknown country code "{}"'.format(code)) + elif n > 2 and \ + pieces[2] not in geo_data[pieces[0]][pieces[1]]['provinces']: + reasons.append('Unknown province code "{}"'.format(code)) + + return reasons diff --git a/octodns/record/geo_data.py b/octodns/record/geo_data.py new file mode 100644 index 0000000..5393db0 --- /dev/null +++ b/octodns/record/geo_data.py @@ -0,0 +1,316 @@ +# +# -*- coding: utf-8 -*- +# +# This file is generated using +# ./script/generate-geo-data > octodns/record/geo_data.py +# do not modify it directly +# + +geo_data = \ + {'AF': {'AO': {'name': 'Angola'}, + 'BF': {'name': 'Burkina Faso'}, + 'BI': {'name': 'Burundi'}, + 'BJ': {'name': 'Benin'}, + 'BW': {'name': 'Botswana'}, + 'CD': {'name': 'Congo, The Democratic Republic of the'}, + 'CF': {'name': 'Central African Republic'}, + 'CG': {'name': 'Congo'}, + 'CI': {'name': "Côte d'Ivoire"}, + 'CM': {'name': 'Cameroon'}, + 'CV': {'name': 'Cabo Verde'}, + 'DJ': {'name': 'Djibouti'}, + 'DZ': {'name': 'Algeria'}, + 'EG': {'name': 'Egypt'}, + 'EH': {'name': 'Western Sahara'}, + 'ER': {'name': 'Eritrea'}, + 'ET': {'name': 'Ethiopia'}, + 'GA': {'name': 'Gabon'}, + 'GH': {'name': 'Ghana'}, + 'GM': {'name': 'Gambia'}, + 'GN': {'name': 'Guinea'}, + 'GQ': {'name': 'Equatorial Guinea'}, + 'GW': {'name': 'Guinea-Bissau'}, + 'KE': {'name': 'Kenya'}, + 'KM': {'name': 'Comoros'}, + 'LR': {'name': 'Liberia'}, + 'LS': {'name': 'Lesotho'}, + 'LY': {'name': 'Libya'}, + 'MA': {'name': 'Morocco'}, + 'MG': {'name': 'Madagascar'}, + 'ML': {'name': 'Mali'}, + 'MR': {'name': 'Mauritania'}, + 'MU': {'name': 'Mauritius'}, + 'MW': {'name': 'Malawi'}, + 'MZ': {'name': 'Mozambique'}, + 'NA': {'name': 'Namibia'}, + 'NE': {'name': 'Niger'}, + 'NG': {'name': 'Nigeria'}, + 'RE': {'name': 'Réunion'}, + 'RW': {'name': 'Rwanda'}, + 'SC': {'name': 'Seychelles'}, + 'SD': {'name': 'Sudan'}, + 'SH': {'name': 'Saint Helena, Ascension and Tristan da Cunha'}, + 'SL': {'name': 'Sierra Leone'}, + 'SN': {'name': 'Senegal'}, + 'SO': {'name': 'Somalia'}, + 'SS': {'name': 'South Sudan'}, + 'ST': {'name': 'Sao Tome and Principe'}, + 'SZ': {'name': 'Swaziland'}, + 'TD': {'name': 'Chad'}, + 'TG': {'name': 'Togo'}, + 'TN': {'name': 'Tunisia'}, + 'TZ': {'name': 'Tanzania, United Republic of'}, + 'UG': {'name': 'Uganda'}, + 'YT': {'name': 'Mayotte'}, + 'ZA': {'name': 'South Africa'}, + 'ZM': {'name': 'Zambia'}, + 'ZW': {'name': 'Zimbabwe'}}, + 'AN': {'AQ': {'name': 'Antarctica'}, + 'BV': {'name': 'Bouvet Island'}, + 'HM': {'name': 'Heard Island and McDonald Islands'}, + 'TF': {'name': 'French Southern Territories'}}, + 'AS': {'AE': {'name': 'United Arab Emirates'}, + 'AF': {'name': 'Afghanistan'}, + 'AM': {'name': 'Armenia'}, + 'AZ': {'name': 'Azerbaijan'}, + 'BD': {'name': 'Bangladesh'}, + 'BH': {'name': 'Bahrain'}, + 'BN': {'name': 'Brunei Darussalam'}, + 'BT': {'name': 'Bhutan'}, + 'CC': {'name': 'Cocos (Keeling) Islands'}, + 'CN': {'name': 'China'}, + 'CX': {'name': 'Christmas Island'}, + 'CY': {'name': 'Cyprus'}, + 'GE': {'name': 'Georgia'}, + 'HK': {'name': 'Hong Kong'}, + 'ID': {'name': 'Indonesia'}, + 'IL': {'name': 'Israel'}, + 'IN': {'name': 'India'}, + 'IO': {'name': 'British Indian Ocean Territory'}, + 'IQ': {'name': 'Iraq'}, + 'IR': {'name': 'Iran, Islamic Republic of'}, + 'JO': {'name': 'Jordan'}, + 'JP': {'name': 'Japan'}, + 'KG': {'name': 'Kyrgyzstan'}, + 'KH': {'name': 'Cambodia'}, + 'KP': {'name': "Korea, Democratic People's Republic of"}, + 'KR': {'name': 'Korea, Republic of'}, + 'KW': {'name': 'Kuwait'}, + 'KZ': {'name': 'Kazakhstan'}, + 'LA': {'name': "Lao People's Democratic Republic"}, + 'LB': {'name': 'Lebanon'}, + 'LK': {'name': 'Sri Lanka'}, + 'MM': {'name': 'Myanmar'}, + 'MN': {'name': 'Mongolia'}, + 'MO': {'name': 'Macao'}, + 'MV': {'name': 'Maldives'}, + 'MY': {'name': 'Malaysia'}, + 'NP': {'name': 'Nepal'}, + 'OM': {'name': 'Oman'}, + 'PH': {'name': 'Philippines'}, + 'PK': {'name': 'Pakistan'}, + 'PS': {'name': 'Palestine, State of'}, + 'QA': {'name': 'Qatar'}, + 'SA': {'name': 'Saudi Arabia'}, + 'SG': {'name': 'Singapore'}, + 'SY': {'name': 'Syrian Arab Republic'}, + 'TH': {'name': 'Thailand'}, + 'TJ': {'name': 'Tajikistan'}, + 'TM': {'name': 'Turkmenistan'}, + 'TR': {'name': 'Turkey'}, + 'TW': {'name': 'Taiwan, Province of China'}, + 'UZ': {'name': 'Uzbekistan'}, + 'VN': {'name': 'Viet Nam'}, + 'YE': {'name': 'Yemen'}}, + 'EU': {'AD': {'name': 'Andorra'}, + 'AL': {'name': 'Albania'}, + 'AT': {'name': 'Austria'}, + 'AX': {'name': 'Åland Islands'}, + 'BA': {'name': 'Bosnia and Herzegovina'}, + 'BE': {'name': 'Belgium'}, + 'BG': {'name': 'Bulgaria'}, + 'BY': {'name': 'Belarus'}, + 'CH': {'name': 'Switzerland'}, + 'CZ': {'name': 'Czechia'}, + 'DE': {'name': 'Germany'}, + 'DK': {'name': 'Denmark'}, + 'EE': {'name': 'Estonia'}, + 'ES': {'name': 'Spain'}, + 'FI': {'name': 'Finland'}, + 'FO': {'name': 'Faroe Islands'}, + 'FR': {'name': 'France'}, + 'GB': {'name': 'United Kingdom'}, + 'GG': {'name': 'Guernsey'}, + 'GI': {'name': 'Gibraltar'}, + 'GR': {'name': 'Greece'}, + 'HR': {'name': 'Croatia'}, + 'HU': {'name': 'Hungary'}, + 'IE': {'name': 'Ireland'}, + 'IM': {'name': 'Isle of Man'}, + 'IS': {'name': 'Iceland'}, + 'IT': {'name': 'Italy'}, + 'JE': {'name': 'Jersey'}, + 'LI': {'name': 'Liechtenstein'}, + 'LT': {'name': 'Lithuania'}, + 'LU': {'name': 'Luxembourg'}, + 'LV': {'name': 'Latvia'}, + 'MC': {'name': 'Monaco'}, + 'MD': {'name': 'Moldova, Republic of'}, + 'ME': {'name': 'Montenegro'}, + 'MK': {'name': 'Macedonia, Republic of'}, + 'MT': {'name': 'Malta'}, + 'NL': {'name': 'Netherlands'}, + 'NO': {'name': 'Norway'}, + 'PL': {'name': 'Poland'}, + 'PT': {'name': 'Portugal'}, + 'RO': {'name': 'Romania'}, + 'RS': {'name': 'Serbia'}, + 'RU': {'name': 'Russian Federation'}, + 'SE': {'name': 'Sweden'}, + 'SI': {'name': 'Slovenia'}, + 'SJ': {'name': 'Svalbard and Jan Mayen'}, + 'SK': {'name': 'Slovakia'}, + 'SM': {'name': 'San Marino'}, + 'UA': {'name': 'Ukraine'}, + 'VA': {'name': 'Holy See (Vatican City State)'}}, + 'ID': {'TL': {'name': 'Timor-Leste'}}, + 'NA': {'AG': {'name': 'Antigua and Barbuda'}, + 'AI': {'name': 'Anguilla'}, + 'AW': {'name': 'Aruba'}, + 'BB': {'name': 'Barbados'}, + 'BL': {'name': 'Saint Barthélemy'}, + 'BM': {'name': 'Bermuda'}, + 'BQ': {'name': 'Bonaire, Sint Eustatius and Saba'}, + 'BS': {'name': 'Bahamas'}, + 'BZ': {'name': 'Belize'}, + 'CA': {'name': 'Canada'}, + 'CR': {'name': 'Costa Rica'}, + 'CU': {'name': 'Cuba'}, + 'CW': {'name': 'Curaçao'}, + 'DM': {'name': 'Dominica'}, + 'DO': {'name': 'Dominican Republic'}, + 'GD': {'name': 'Grenada'}, + 'GL': {'name': 'Greenland'}, + 'GP': {'name': 'Guadeloupe'}, + 'GT': {'name': 'Guatemala'}, + 'HN': {'name': 'Honduras'}, + 'HT': {'name': 'Haiti'}, + 'JM': {'name': 'Jamaica'}, + 'KN': {'name': 'Saint Kitts and Nevis'}, + 'KY': {'name': 'Cayman Islands'}, + 'LC': {'name': 'Saint Lucia'}, + 'MF': {'name': 'Saint Martin (French part)'}, + 'MQ': {'name': 'Martinique'}, + 'MS': {'name': 'Montserrat'}, + 'MX': {'name': 'Mexico'}, + 'NI': {'name': 'Nicaragua'}, + 'PA': {'name': 'Panama'}, + 'PM': {'name': 'Saint Pierre and Miquelon'}, + 'PR': {'name': 'Puerto Rico'}, + 'SV': {'name': 'El Salvador'}, + 'SX': {'name': 'Sint Maarten (Dutch part)'}, + 'TC': {'name': 'Turks and Caicos Islands'}, + 'TT': {'name': 'Trinidad and Tobago'}, + 'US': {'name': 'United States', + 'provinces': {'AK': {'name': 'Alaska'}, + 'AL': {'name': 'Alabama'}, + 'AR': {'name': 'Arkansas'}, + 'AS': {'name': 'American Samoa'}, + 'AZ': {'name': 'Arizona'}, + 'CA': {'name': 'California'}, + 'CO': {'name': 'Colorado'}, + 'CT': {'name': 'Connecticut'}, + 'DC': {'name': 'District of Columbia'}, + 'DE': {'name': 'Delaware'}, + 'FL': {'name': 'Florida'}, + 'GA': {'name': 'Georgia'}, + 'GU': {'name': 'Guam'}, + 'HI': {'name': 'Hawaii'}, + 'IA': {'name': 'Iowa'}, + 'ID': {'name': 'Idaho'}, + 'IL': {'name': 'Illinois'}, + 'IN': {'name': 'Indiana'}, + 'KS': {'name': 'Kansas'}, + 'KY': {'name': 'Kentucky'}, + 'LA': {'name': 'Louisiana'}, + 'MA': {'name': 'Massachusetts'}, + 'MD': {'name': 'Maryland'}, + 'ME': {'name': 'Maine'}, + 'MI': {'name': 'Michigan'}, + 'MN': {'name': 'Minnesota'}, + 'MO': {'name': 'Missouri'}, + 'MP': {'name': 'Northern Mariana Islands'}, + 'MS': {'name': 'Mississippi'}, + 'MT': {'name': 'Montana'}, + 'NC': {'name': 'North Carolina'}, + 'ND': {'name': 'North Dakota'}, + 'NE': {'name': 'Nebraska'}, + 'NH': {'name': 'New Hampshire'}, + 'NJ': {'name': 'New Jersey'}, + 'NM': {'name': 'New Mexico'}, + 'NV': {'name': 'Nevada'}, + 'NY': {'name': 'New York'}, + 'OH': {'name': 'Ohio'}, + 'OK': {'name': 'Oklahoma'}, + 'OR': {'name': 'Oregon'}, + 'PA': {'name': 'Pennsylvania'}, + 'PR': {'name': 'Puerto Rico'}, + 'RI': {'name': 'Rhode Island'}, + 'SC': {'name': 'South Carolina'}, + 'SD': {'name': 'South Dakota'}, + 'TN': {'name': 'Tennessee'}, + 'TX': {'name': 'Texas'}, + 'UM': {'name': 'United States Minor Outlying ' + 'Islands'}, + 'UT': {'name': 'Utah'}, + 'VA': {'name': 'Virginia'}, + 'VI': {'name': 'Virgin Islands'}, + 'VT': {'name': 'Vermont'}, + 'WA': {'name': 'Washington'}, + 'WI': {'name': 'Wisconsin'}, + 'WV': {'name': 'West Virginia'}, + 'WY': {'name': 'Wyoming'}}}, + 'VC': {'name': 'Saint Vincent and the Grenadines'}, + 'VG': {'name': 'Virgin Islands, British'}, + 'VI': {'name': 'Virgin Islands, U.S.'}}, + 'OC': {'AS': {'name': 'American Samoa'}, + 'AU': {'name': 'Australia'}, + 'CK': {'name': 'Cook Islands'}, + 'FJ': {'name': 'Fiji'}, + 'FM': {'name': 'Micronesia, Federated States of'}, + 'GU': {'name': 'Guam'}, + 'KI': {'name': 'Kiribati'}, + 'MH': {'name': 'Marshall Islands'}, + 'MP': {'name': 'Northern Mariana Islands'}, + 'NC': {'name': 'New Caledonia'}, + 'NF': {'name': 'Norfolk Island'}, + 'NR': {'name': 'Nauru'}, + 'NU': {'name': 'Niue'}, + 'NZ': {'name': 'New Zealand'}, + 'PF': {'name': 'French Polynesia'}, + 'PG': {'name': 'Papua New Guinea'}, + 'PN': {'name': 'Pitcairn'}, + 'PW': {'name': 'Palau'}, + 'SB': {'name': 'Solomon Islands'}, + 'TK': {'name': 'Tokelau'}, + 'TO': {'name': 'Tonga'}, + 'TV': {'name': 'Tuvalu'}, + 'UM': {'name': 'United States Minor Outlying Islands'}, + 'VU': {'name': 'Vanuatu'}, + 'WF': {'name': 'Wallis and Futuna'}, + 'WS': {'name': 'Samoa'}}, + 'SA': {'AR': {'name': 'Argentina'}, + 'BO': {'name': 'Bolivia, Plurinational State of'}, + 'BR': {'name': 'Brazil'}, + 'CL': {'name': 'Chile'}, + 'CO': {'name': 'Colombia'}, + 'EC': {'name': 'Ecuador'}, + 'FK': {'name': 'Falkland Islands (Malvinas)'}, + 'GF': {'name': 'French Guiana'}, + 'GS': {'name': 'South Georgia and the South Sandwich Islands'}, + 'GY': {'name': 'Guyana'}, + 'PE': {'name': 'Peru'}, + 'PY': {'name': 'Paraguay'}, + 'SR': {'name': 'Suriname'}, + 'UY': {'name': 'Uruguay'}, + 'VE': {'name': 'Venezuela, Bolivarian Republic of'}}} diff --git a/requirements-dev.txt b/requirements-dev.txt index 5b942dd..1afee06 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,6 +2,8 @@ coverage mock nose pycodestyle==2.4.0 +pycountry>=18.12.8 +pycountry_convert>=0.7.2 pyflakes==1.6.0 requests_mock twine==1.11.0 diff --git a/script/generate-geo-data b/script/generate-geo-data new file mode 100755 index 0000000..87a57b1 --- /dev/null +++ b/script/generate-geo-data @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +from collections import defaultdict +from pprint import pformat +from pycountry import countries, subdivisions +from pycountry_convert import country_alpha2_to_continent_code + + +subs = defaultdict(dict) +for subdivision in subdivisions: + # Route53 only supports US states, Dyn supports US states and CA provinces, but for now we'll just do US + if subdivision.country_code not in ('US'): + continue + subs[subdivision.country_code][subdivision.code[3:]] = { + 'name': subdivision.name + } +subs = dict(subs) +#pprint(subs) + +# These are best guesses at things pycountry_convert doesn't map +continent_backups = { + 'AQ': 'AN', + 'EH': 'AF', + 'PN': 'OC', + 'SX': 'NA', + 'TF': 'AN', + 'TL': 'ID', + 'UM': 'OC', + 'VA': 'EU', +} + +geos = defaultdict(dict) +for country in countries: + try: + continent_code = country_alpha2_to_continent_code(country.alpha_2) + except KeyError: + try: + continent_code = continent_backups[country.alpha_2] + except KeyError: + raise + print('{} {} {}'.format(country.alpha_2, country.name, getattr(country, 'official_name', ''))) + + geos[continent_code][country.alpha_2] = { + 'name': country.name + } + + try: + geos[continent_code][country.alpha_2]['provinces'] = subs[country.alpha_2] + except KeyError: + pass + +geos = dict(geos) +data = pformat(geos).replace('\n', '\n ') + +print('''# +# -*- coding: utf-8 -*- +# +# This file is generated using +# ./script/generate-geo-data > octodns/record/geo_data.py +# do not modify it directly +# + +geo_data = \\ + {}'''.format(data)) diff --git a/tests/test_octodns_record_geo.py b/tests/test_octodns_record_geo.py new file mode 100644 index 0000000..00e0bdc --- /dev/null +++ b/tests/test_octodns_record_geo.py @@ -0,0 +1,51 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from octodns.record.geo import GeoCodes + + +class TestRecordGeoCodes(TestCase): + + def test_validate(self): + # All valid + self.assertEquals([], GeoCodes.validate('NA')) + self.assertEquals([], GeoCodes.validate('NA-US')) + self.assertEquals([], GeoCodes.validate('NA-US-OR')) + + # Just plain bad + self.assertEquals(['Invalid geo code "XX-YY-ZZ-AA"'], + GeoCodes.validate('XX-YY-ZZ-AA')) + self.assertEquals(['Unknown continent code "X-Y-Z"'], + GeoCodes.validate('X-Y-Z')) + self.assertEquals(['Unknown continent code "XXX-Y-Z"'], + GeoCodes.validate('XXX-Y-Z')) + + # Bad continent + self.assertEquals(['Unknown continent code "XX"'], + GeoCodes.validate('XX')) + # Bad continent good country + self.assertEquals(['Unknown continent code "XX-US"'], + GeoCodes.validate('XX-US')) + # Bad continent good country and province + self.assertEquals(['Unknown continent code "XX-US-OR"'], + GeoCodes.validate('XX-US-OR')) + + # Bad country, good continent + self.assertEquals(['Unknown country code "NA-XX"'], + GeoCodes.validate('NA-XX')) + # Bad country, good continent and state + self.assertEquals(['Unknown country code "NA-XX-OR"'], + GeoCodes.validate('NA-XX-OR')) + # Good country, good continent, but bad match + self.assertEquals(['Unknown country code "NA-GB"'], + GeoCodes.validate('NA-GB')) + + # Bad province code, good continent and country + self.assertEquals(['Unknown province code "NA-US-XX"'], + GeoCodes.validate('NA-US-XX'))