feat(platform): standardize country inputs with ISO 3166 Country type (#7333)

* feat(platform): standardize country inputs using pydantic-extra-types

Add CountryParam type that accepts:
- ISO 3166-1 alpha-2 codes (e.g., 'US', 'us')
- Full country names (e.g., 'United States')
- lower_snake_case names (e.g., 'united_states')

Uses pydantic-extra-types for programmatic ISO validation instead of
hardcoded mappings. Serializes to lowercase alpha-2 code by default.

Example usage in FMP equity screener:
- country: CountryParam | None

Partially addresses #6969

* refactor: use Country class inheriting from str for type checker compatibility

- Replace Annotated CountryParam with Country(str) class per maintainer feedback
- Store full pycountry object for access to alpha_2, alpha_3, name, numeric
- Use pycountry directly instead of pydantic-extra-types
- Add support for alpha-3 codes (e.g., 'USA')
- Update tests to cover new functionality

* Replace pycountry with static JSON country data

- Add country_data.json with ISO 3166-1 data (20KB vs 6MB pycountry)
- Update Country class to use static JSON lookup
- Remove pycountry dependency from pyproject.toml
- No decompression needed, data loaded once at import

* fix(lint): D402 docstring should start with verb

* fix(lint): work around pydocstyle D402 false positive

When a method is named `name` and `.pydocstyle.ini` uses `match = .*\.py`,
pydocstyle incorrectly triggers D402 for any docstring containing the word
"name" anywhere - not just at the start as the rule intends.

Changed docstring from "Return the full country name..." to
"The country's official designation..." to avoid the false positive.

See: https://github.com/PyCQA/pydocstyle/issues/375

* fix(lint): resolve pylint warnings in country_utils

* fix(lint): use ternary for ruff SIM108 in country_utils

* fix(lint): format ternary for black line length

---------

Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>
This commit is contained in:
John Seong
2026-02-09 13:11:11 -08:00
committed by GitHub
parent 4684db3f8b
commit f0c3742729
4 changed files with 555 additions and 2 deletions

View File

@@ -0,0 +1,253 @@
{
"countries": [
{"alpha_2": "AF", "alpha_3": "AFG", "name": "Afghanistan", "numeric": "004"},
{"alpha_2": "AL", "alpha_3": "ALB", "name": "Albania", "numeric": "008"},
{"alpha_2": "DZ", "alpha_3": "DZA", "name": "Algeria", "numeric": "012"},
{"alpha_2": "AS", "alpha_3": "ASM", "name": "American Samoa", "numeric": "016"},
{"alpha_2": "AD", "alpha_3": "AND", "name": "Andorra", "numeric": "020"},
{"alpha_2": "AO", "alpha_3": "AGO", "name": "Angola", "numeric": "024"},
{"alpha_2": "AI", "alpha_3": "AIA", "name": "Anguilla", "numeric": "660"},
{"alpha_2": "AQ", "alpha_3": "ATA", "name": "Antarctica", "numeric": "010"},
{"alpha_2": "AG", "alpha_3": "ATG", "name": "Antigua and Barbuda", "numeric": "028"},
{"alpha_2": "AR", "alpha_3": "ARG", "name": "Argentina", "numeric": "032"},
{"alpha_2": "AM", "alpha_3": "ARM", "name": "Armenia", "numeric": "051"},
{"alpha_2": "AW", "alpha_3": "ABW", "name": "Aruba", "numeric": "533"},
{"alpha_2": "AU", "alpha_3": "AUS", "name": "Australia", "numeric": "036"},
{"alpha_2": "AT", "alpha_3": "AUT", "name": "Austria", "numeric": "040"},
{"alpha_2": "AZ", "alpha_3": "AZE", "name": "Azerbaijan", "numeric": "031"},
{"alpha_2": "BS", "alpha_3": "BHS", "name": "Bahamas", "numeric": "044"},
{"alpha_2": "BH", "alpha_3": "BHR", "name": "Bahrain", "numeric": "048"},
{"alpha_2": "BD", "alpha_3": "BGD", "name": "Bangladesh", "numeric": "050"},
{"alpha_2": "BB", "alpha_3": "BRB", "name": "Barbados", "numeric": "052"},
{"alpha_2": "BY", "alpha_3": "BLR", "name": "Belarus", "numeric": "112"},
{"alpha_2": "BE", "alpha_3": "BEL", "name": "Belgium", "numeric": "056"},
{"alpha_2": "BZ", "alpha_3": "BLZ", "name": "Belize", "numeric": "084"},
{"alpha_2": "BJ", "alpha_3": "BEN", "name": "Benin", "numeric": "204"},
{"alpha_2": "BM", "alpha_3": "BMU", "name": "Bermuda", "numeric": "060"},
{"alpha_2": "BT", "alpha_3": "BTN", "name": "Bhutan", "numeric": "064"},
{"alpha_2": "BO", "alpha_3": "BOL", "name": "Bolivia, Plurinational State of", "numeric": "068"},
{"alpha_2": "BQ", "alpha_3": "BES", "name": "Bonaire, Sint Eustatius and Saba", "numeric": "535"},
{"alpha_2": "BA", "alpha_3": "BIH", "name": "Bosnia and Herzegovina", "numeric": "070"},
{"alpha_2": "BW", "alpha_3": "BWA", "name": "Botswana", "numeric": "072"},
{"alpha_2": "BV", "alpha_3": "BVT", "name": "Bouvet Island", "numeric": "074"},
{"alpha_2": "BR", "alpha_3": "BRA", "name": "Brazil", "numeric": "076"},
{"alpha_2": "IO", "alpha_3": "IOT", "name": "British Indian Ocean Territory", "numeric": "086"},
{"alpha_2": "BN", "alpha_3": "BRN", "name": "Brunei Darussalam", "numeric": "096"},
{"alpha_2": "BG", "alpha_3": "BGR", "name": "Bulgaria", "numeric": "100"},
{"alpha_2": "BF", "alpha_3": "BFA", "name": "Burkina Faso", "numeric": "854"},
{"alpha_2": "BI", "alpha_3": "BDI", "name": "Burundi", "numeric": "108"},
{"alpha_2": "CV", "alpha_3": "CPV", "name": "Cabo Verde", "numeric": "132"},
{"alpha_2": "KH", "alpha_3": "KHM", "name": "Cambodia", "numeric": "116"},
{"alpha_2": "CM", "alpha_3": "CMR", "name": "Cameroon", "numeric": "120"},
{"alpha_2": "CA", "alpha_3": "CAN", "name": "Canada", "numeric": "124"},
{"alpha_2": "KY", "alpha_3": "CYM", "name": "Cayman Islands", "numeric": "136"},
{"alpha_2": "CF", "alpha_3": "CAF", "name": "Central African Republic", "numeric": "140"},
{"alpha_2": "TD", "alpha_3": "TCD", "name": "Chad", "numeric": "148"},
{"alpha_2": "CL", "alpha_3": "CHL", "name": "Chile", "numeric": "152"},
{"alpha_2": "CN", "alpha_3": "CHN", "name": "China", "numeric": "156"},
{"alpha_2": "CX", "alpha_3": "CXR", "name": "Christmas Island", "numeric": "162"},
{"alpha_2": "CC", "alpha_3": "CCK", "name": "Cocos (Keeling) Islands", "numeric": "166"},
{"alpha_2": "CO", "alpha_3": "COL", "name": "Colombia", "numeric": "170"},
{"alpha_2": "KM", "alpha_3": "COM", "name": "Comoros", "numeric": "174"},
{"alpha_2": "CG", "alpha_3": "COG", "name": "Congo", "numeric": "178"},
{"alpha_2": "CD", "alpha_3": "COD", "name": "Congo, Democratic Republic of the", "numeric": "180"},
{"alpha_2": "CK", "alpha_3": "COK", "name": "Cook Islands", "numeric": "184"},
{"alpha_2": "CR", "alpha_3": "CRI", "name": "Costa Rica", "numeric": "188"},
{"alpha_2": "HR", "alpha_3": "HRV", "name": "Croatia", "numeric": "191"},
{"alpha_2": "CU", "alpha_3": "CUB", "name": "Cuba", "numeric": "192"},
{"alpha_2": "CW", "alpha_3": "CUW", "name": "Curaçao", "numeric": "531"},
{"alpha_2": "CY", "alpha_3": "CYP", "name": "Cyprus", "numeric": "196"},
{"alpha_2": "CZ", "alpha_3": "CZE", "name": "Czechia", "numeric": "203"},
{"alpha_2": "CI", "alpha_3": "CIV", "name": "Côte d'Ivoire", "numeric": "384"},
{"alpha_2": "DK", "alpha_3": "DNK", "name": "Denmark", "numeric": "208"},
{"alpha_2": "DJ", "alpha_3": "DJI", "name": "Djibouti", "numeric": "262"},
{"alpha_2": "DM", "alpha_3": "DMA", "name": "Dominica", "numeric": "212"},
{"alpha_2": "DO", "alpha_3": "DOM", "name": "Dominican Republic", "numeric": "214"},
{"alpha_2": "EC", "alpha_3": "ECU", "name": "Ecuador", "numeric": "218"},
{"alpha_2": "EG", "alpha_3": "EGY", "name": "Egypt", "numeric": "818"},
{"alpha_2": "SV", "alpha_3": "SLV", "name": "El Salvador", "numeric": "222"},
{"alpha_2": "GQ", "alpha_3": "GNQ", "name": "Equatorial Guinea", "numeric": "226"},
{"alpha_2": "ER", "alpha_3": "ERI", "name": "Eritrea", "numeric": "232"},
{"alpha_2": "EE", "alpha_3": "EST", "name": "Estonia", "numeric": "233"},
{"alpha_2": "SZ", "alpha_3": "SWZ", "name": "Eswatini", "numeric": "748"},
{"alpha_2": "ET", "alpha_3": "ETH", "name": "Ethiopia", "numeric": "231"},
{"alpha_2": "FK", "alpha_3": "FLK", "name": "Falkland Islands (Malvinas)", "numeric": "238"},
{"alpha_2": "FO", "alpha_3": "FRO", "name": "Faroe Islands", "numeric": "234"},
{"alpha_2": "FJ", "alpha_3": "FJI", "name": "Fiji", "numeric": "242"},
{"alpha_2": "FI", "alpha_3": "FIN", "name": "Finland", "numeric": "246"},
{"alpha_2": "FR", "alpha_3": "FRA", "name": "France", "numeric": "250"},
{"alpha_2": "GF", "alpha_3": "GUF", "name": "French Guiana", "numeric": "254"},
{"alpha_2": "PF", "alpha_3": "PYF", "name": "French Polynesia", "numeric": "258"},
{"alpha_2": "TF", "alpha_3": "ATF", "name": "French Southern Territories", "numeric": "260"},
{"alpha_2": "GA", "alpha_3": "GAB", "name": "Gabon", "numeric": "266"},
{"alpha_2": "GM", "alpha_3": "GMB", "name": "Gambia", "numeric": "270"},
{"alpha_2": "GE", "alpha_3": "GEO", "name": "Georgia", "numeric": "268"},
{"alpha_2": "DE", "alpha_3": "DEU", "name": "Germany", "numeric": "276"},
{"alpha_2": "GH", "alpha_3": "GHA", "name": "Ghana", "numeric": "288"},
{"alpha_2": "GI", "alpha_3": "GIB", "name": "Gibraltar", "numeric": "292"},
{"alpha_2": "GR", "alpha_3": "GRC", "name": "Greece", "numeric": "300"},
{"alpha_2": "GL", "alpha_3": "GRL", "name": "Greenland", "numeric": "304"},
{"alpha_2": "GD", "alpha_3": "GRD", "name": "Grenada", "numeric": "308"},
{"alpha_2": "GP", "alpha_3": "GLP", "name": "Guadeloupe", "numeric": "312"},
{"alpha_2": "GU", "alpha_3": "GUM", "name": "Guam", "numeric": "316"},
{"alpha_2": "GT", "alpha_3": "GTM", "name": "Guatemala", "numeric": "320"},
{"alpha_2": "GG", "alpha_3": "GGY", "name": "Guernsey", "numeric": "831"},
{"alpha_2": "GN", "alpha_3": "GIN", "name": "Guinea", "numeric": "324"},
{"alpha_2": "GW", "alpha_3": "GNB", "name": "Guinea-Bissau", "numeric": "624"},
{"alpha_2": "GY", "alpha_3": "GUY", "name": "Guyana", "numeric": "328"},
{"alpha_2": "HT", "alpha_3": "HTI", "name": "Haiti", "numeric": "332"},
{"alpha_2": "HM", "alpha_3": "HMD", "name": "Heard Island and McDonald Islands", "numeric": "334"},
{"alpha_2": "VA", "alpha_3": "VAT", "name": "Holy See", "numeric": "336"},
{"alpha_2": "HN", "alpha_3": "HND", "name": "Honduras", "numeric": "340"},
{"alpha_2": "HK", "alpha_3": "HKG", "name": "Hong Kong", "numeric": "344"},
{"alpha_2": "HU", "alpha_3": "HUN", "name": "Hungary", "numeric": "348"},
{"alpha_2": "IS", "alpha_3": "ISL", "name": "Iceland", "numeric": "352"},
{"alpha_2": "IN", "alpha_3": "IND", "name": "India", "numeric": "356"},
{"alpha_2": "ID", "alpha_3": "IDN", "name": "Indonesia", "numeric": "360"},
{"alpha_2": "IR", "alpha_3": "IRN", "name": "Iran, Islamic Republic of", "numeric": "364"},
{"alpha_2": "IQ", "alpha_3": "IRQ", "name": "Iraq", "numeric": "368"},
{"alpha_2": "IE", "alpha_3": "IRL", "name": "Ireland", "numeric": "372"},
{"alpha_2": "IM", "alpha_3": "IMN", "name": "Isle of Man", "numeric": "833"},
{"alpha_2": "IL", "alpha_3": "ISR", "name": "Israel", "numeric": "376"},
{"alpha_2": "IT", "alpha_3": "ITA", "name": "Italy", "numeric": "380"},
{"alpha_2": "JM", "alpha_3": "JAM", "name": "Jamaica", "numeric": "388"},
{"alpha_2": "JP", "alpha_3": "JPN", "name": "Japan", "numeric": "392"},
{"alpha_2": "JE", "alpha_3": "JEY", "name": "Jersey", "numeric": "832"},
{"alpha_2": "JO", "alpha_3": "JOR", "name": "Jordan", "numeric": "400"},
{"alpha_2": "KZ", "alpha_3": "KAZ", "name": "Kazakhstan", "numeric": "398"},
{"alpha_2": "KE", "alpha_3": "KEN", "name": "Kenya", "numeric": "404"},
{"alpha_2": "KI", "alpha_3": "KIR", "name": "Kiribati", "numeric": "296"},
{"alpha_2": "KP", "alpha_3": "PRK", "name": "Korea, Democratic People's Republic of", "numeric": "408"},
{"alpha_2": "KR", "alpha_3": "KOR", "name": "Korea, Republic of", "numeric": "410"},
{"alpha_2": "KW", "alpha_3": "KWT", "name": "Kuwait", "numeric": "414"},
{"alpha_2": "KG", "alpha_3": "KGZ", "name": "Kyrgyzstan", "numeric": "417"},
{"alpha_2": "LA", "alpha_3": "LAO", "name": "Lao People's Democratic Republic", "numeric": "418"},
{"alpha_2": "LV", "alpha_3": "LVA", "name": "Latvia", "numeric": "428"},
{"alpha_2": "LB", "alpha_3": "LBN", "name": "Lebanon", "numeric": "422"},
{"alpha_2": "LS", "alpha_3": "LSO", "name": "Lesotho", "numeric": "426"},
{"alpha_2": "LR", "alpha_3": "LBR", "name": "Liberia", "numeric": "430"},
{"alpha_2": "LY", "alpha_3": "LBY", "name": "Libya", "numeric": "434"},
{"alpha_2": "LI", "alpha_3": "LIE", "name": "Liechtenstein", "numeric": "438"},
{"alpha_2": "LT", "alpha_3": "LTU", "name": "Lithuania", "numeric": "440"},
{"alpha_2": "LU", "alpha_3": "LUX", "name": "Luxembourg", "numeric": "442"},
{"alpha_2": "MO", "alpha_3": "MAC", "name": "Macao", "numeric": "446"},
{"alpha_2": "MG", "alpha_3": "MDG", "name": "Madagascar", "numeric": "450"},
{"alpha_2": "MW", "alpha_3": "MWI", "name": "Malawi", "numeric": "454"},
{"alpha_2": "MY", "alpha_3": "MYS", "name": "Malaysia", "numeric": "458"},
{"alpha_2": "MV", "alpha_3": "MDV", "name": "Maldives", "numeric": "462"},
{"alpha_2": "ML", "alpha_3": "MLI", "name": "Mali", "numeric": "466"},
{"alpha_2": "MT", "alpha_3": "MLT", "name": "Malta", "numeric": "470"},
{"alpha_2": "MH", "alpha_3": "MHL", "name": "Marshall Islands", "numeric": "584"},
{"alpha_2": "MQ", "alpha_3": "MTQ", "name": "Martinique", "numeric": "474"},
{"alpha_2": "MR", "alpha_3": "MRT", "name": "Mauritania", "numeric": "478"},
{"alpha_2": "MU", "alpha_3": "MUS", "name": "Mauritius", "numeric": "480"},
{"alpha_2": "YT", "alpha_3": "MYT", "name": "Mayotte", "numeric": "175"},
{"alpha_2": "MX", "alpha_3": "MEX", "name": "Mexico", "numeric": "484"},
{"alpha_2": "FM", "alpha_3": "FSM", "name": "Micronesia, Federated States of", "numeric": "583"},
{"alpha_2": "MD", "alpha_3": "MDA", "name": "Moldova, Republic of", "numeric": "498"},
{"alpha_2": "MC", "alpha_3": "MCO", "name": "Monaco", "numeric": "492"},
{"alpha_2": "MN", "alpha_3": "MNG", "name": "Mongolia", "numeric": "496"},
{"alpha_2": "ME", "alpha_3": "MNE", "name": "Montenegro", "numeric": "499"},
{"alpha_2": "MS", "alpha_3": "MSR", "name": "Montserrat", "numeric": "500"},
{"alpha_2": "MA", "alpha_3": "MAR", "name": "Morocco", "numeric": "504"},
{"alpha_2": "MZ", "alpha_3": "MOZ", "name": "Mozambique", "numeric": "508"},
{"alpha_2": "MM", "alpha_3": "MMR", "name": "Myanmar", "numeric": "104"},
{"alpha_2": "NA", "alpha_3": "NAM", "name": "Namibia", "numeric": "516"},
{"alpha_2": "NR", "alpha_3": "NRU", "name": "Nauru", "numeric": "520"},
{"alpha_2": "NP", "alpha_3": "NPL", "name": "Nepal", "numeric": "524"},
{"alpha_2": "NL", "alpha_3": "NLD", "name": "Netherlands", "numeric": "528"},
{"alpha_2": "NC", "alpha_3": "NCL", "name": "New Caledonia", "numeric": "540"},
{"alpha_2": "NZ", "alpha_3": "NZL", "name": "New Zealand", "numeric": "554"},
{"alpha_2": "NI", "alpha_3": "NIC", "name": "Nicaragua", "numeric": "558"},
{"alpha_2": "NE", "alpha_3": "NER", "name": "Niger", "numeric": "562"},
{"alpha_2": "NG", "alpha_3": "NGA", "name": "Nigeria", "numeric": "566"},
{"alpha_2": "NU", "alpha_3": "NIU", "name": "Niue", "numeric": "570"},
{"alpha_2": "NF", "alpha_3": "NFK", "name": "Norfolk Island", "numeric": "574"},
{"alpha_2": "MK", "alpha_3": "MKD", "name": "North Macedonia", "numeric": "807"},
{"alpha_2": "MP", "alpha_3": "MNP", "name": "Northern Mariana Islands", "numeric": "580"},
{"alpha_2": "NO", "alpha_3": "NOR", "name": "Norway", "numeric": "578"},
{"alpha_2": "OM", "alpha_3": "OMN", "name": "Oman", "numeric": "512"},
{"alpha_2": "PK", "alpha_3": "PAK", "name": "Pakistan", "numeric": "586"},
{"alpha_2": "PW", "alpha_3": "PLW", "name": "Palau", "numeric": "585"},
{"alpha_2": "PS", "alpha_3": "PSE", "name": "Palestine, State of", "numeric": "275"},
{"alpha_2": "PA", "alpha_3": "PAN", "name": "Panama", "numeric": "591"},
{"alpha_2": "PG", "alpha_3": "PNG", "name": "Papua New Guinea", "numeric": "598"},
{"alpha_2": "PY", "alpha_3": "PRY", "name": "Paraguay", "numeric": "600"},
{"alpha_2": "PE", "alpha_3": "PER", "name": "Peru", "numeric": "604"},
{"alpha_2": "PH", "alpha_3": "PHL", "name": "Philippines", "numeric": "608"},
{"alpha_2": "PN", "alpha_3": "PCN", "name": "Pitcairn", "numeric": "612"},
{"alpha_2": "PL", "alpha_3": "POL", "name": "Poland", "numeric": "616"},
{"alpha_2": "PT", "alpha_3": "PRT", "name": "Portugal", "numeric": "620"},
{"alpha_2": "PR", "alpha_3": "PRI", "name": "Puerto Rico", "numeric": "630"},
{"alpha_2": "QA", "alpha_3": "QAT", "name": "Qatar", "numeric": "634"},
{"alpha_2": "RO", "alpha_3": "ROU", "name": "Romania", "numeric": "642"},
{"alpha_2": "RU", "alpha_3": "RUS", "name": "Russian Federation", "numeric": "643"},
{"alpha_2": "RW", "alpha_3": "RWA", "name": "Rwanda", "numeric": "646"},
{"alpha_2": "RE", "alpha_3": "REU", "name": "Réunion", "numeric": "638"},
{"alpha_2": "BL", "alpha_3": "BLM", "name": "Saint Barthélemy", "numeric": "652"},
{"alpha_2": "SH", "alpha_3": "SHN", "name": "Saint Helena, Ascension and Tristan da Cunha", "numeric": "654"},
{"alpha_2": "KN", "alpha_3": "KNA", "name": "Saint Kitts and Nevis", "numeric": "659"},
{"alpha_2": "LC", "alpha_3": "LCA", "name": "Saint Lucia", "numeric": "662"},
{"alpha_2": "MF", "alpha_3": "MAF", "name": "Saint Martin (French part)", "numeric": "663"},
{"alpha_2": "PM", "alpha_3": "SPM", "name": "Saint Pierre and Miquelon", "numeric": "666"},
{"alpha_2": "VC", "alpha_3": "VCT", "name": "Saint Vincent and the Grenadines", "numeric": "670"},
{"alpha_2": "WS", "alpha_3": "WSM", "name": "Samoa", "numeric": "882"},
{"alpha_2": "SM", "alpha_3": "SMR", "name": "San Marino", "numeric": "674"},
{"alpha_2": "ST", "alpha_3": "STP", "name": "Sao Tome and Principe", "numeric": "678"},
{"alpha_2": "SA", "alpha_3": "SAU", "name": "Saudi Arabia", "numeric": "682"},
{"alpha_2": "SN", "alpha_3": "SEN", "name": "Senegal", "numeric": "686"},
{"alpha_2": "RS", "alpha_3": "SRB", "name": "Serbia", "numeric": "688"},
{"alpha_2": "SC", "alpha_3": "SYC", "name": "Seychelles", "numeric": "690"},
{"alpha_2": "SL", "alpha_3": "SLE", "name": "Sierra Leone", "numeric": "694"},
{"alpha_2": "SG", "alpha_3": "SGP", "name": "Singapore", "numeric": "702"},
{"alpha_2": "SX", "alpha_3": "SXM", "name": "Sint Maarten (Dutch part)", "numeric": "534"},
{"alpha_2": "SK", "alpha_3": "SVK", "name": "Slovakia", "numeric": "703"},
{"alpha_2": "SI", "alpha_3": "SVN", "name": "Slovenia", "numeric": "705"},
{"alpha_2": "SB", "alpha_3": "SLB", "name": "Solomon Islands", "numeric": "090"},
{"alpha_2": "SO", "alpha_3": "SOM", "name": "Somalia", "numeric": "706"},
{"alpha_2": "ZA", "alpha_3": "ZAF", "name": "South Africa", "numeric": "710"},
{"alpha_2": "GS", "alpha_3": "SGS", "name": "South Georgia and the South Sandwich Islands", "numeric": "239"},
{"alpha_2": "SS", "alpha_3": "SSD", "name": "South Sudan", "numeric": "728"},
{"alpha_2": "ES", "alpha_3": "ESP", "name": "Spain", "numeric": "724"},
{"alpha_2": "LK", "alpha_3": "LKA", "name": "Sri Lanka", "numeric": "144"},
{"alpha_2": "SD", "alpha_3": "SDN", "name": "Sudan", "numeric": "729"},
{"alpha_2": "SR", "alpha_3": "SUR", "name": "Suriname", "numeric": "740"},
{"alpha_2": "SJ", "alpha_3": "SJM", "name": "Svalbard and Jan Mayen", "numeric": "744"},
{"alpha_2": "SE", "alpha_3": "SWE", "name": "Sweden", "numeric": "752"},
{"alpha_2": "CH", "alpha_3": "CHE", "name": "Switzerland", "numeric": "756"},
{"alpha_2": "SY", "alpha_3": "SYR", "name": "Syrian Arab Republic", "numeric": "760"},
{"alpha_2": "TW", "alpha_3": "TWN", "name": "Taiwan, Province of China", "numeric": "158"},
{"alpha_2": "TJ", "alpha_3": "TJK", "name": "Tajikistan", "numeric": "762"},
{"alpha_2": "TZ", "alpha_3": "TZA", "name": "Tanzania, United Republic of", "numeric": "834"},
{"alpha_2": "TH", "alpha_3": "THA", "name": "Thailand", "numeric": "764"},
{"alpha_2": "TL", "alpha_3": "TLS", "name": "Timor-Leste", "numeric": "626"},
{"alpha_2": "TG", "alpha_3": "TGO", "name": "Togo", "numeric": "768"},
{"alpha_2": "TK", "alpha_3": "TKL", "name": "Tokelau", "numeric": "772"},
{"alpha_2": "TO", "alpha_3": "TON", "name": "Tonga", "numeric": "776"},
{"alpha_2": "TT", "alpha_3": "TTO", "name": "Trinidad and Tobago", "numeric": "780"},
{"alpha_2": "TN", "alpha_3": "TUN", "name": "Tunisia", "numeric": "788"},
{"alpha_2": "TR", "alpha_3": "TUR", "name": "Turkey", "numeric": "792"},
{"alpha_2": "TM", "alpha_3": "TKM", "name": "Turkmenistan", "numeric": "795"},
{"alpha_2": "TC", "alpha_3": "TCA", "name": "Turks and Caicos Islands", "numeric": "796"},
{"alpha_2": "TV", "alpha_3": "TUV", "name": "Tuvalu", "numeric": "798"},
{"alpha_2": "UG", "alpha_3": "UGA", "name": "Uganda", "numeric": "800"},
{"alpha_2": "UA", "alpha_3": "UKR", "name": "Ukraine", "numeric": "804"},
{"alpha_2": "AE", "alpha_3": "ARE", "name": "United Arab Emirates", "numeric": "784"},
{"alpha_2": "GB", "alpha_3": "GBR", "name": "United Kingdom", "numeric": "826"},
{"alpha_2": "US", "alpha_3": "USA", "name": "United States", "numeric": "840"},
{"alpha_2": "UM", "alpha_3": "UMI", "name": "United States Minor Outlying Islands", "numeric": "581"},
{"alpha_2": "UY", "alpha_3": "URY", "name": "Uruguay", "numeric": "858"},
{"alpha_2": "UZ", "alpha_3": "UZB", "name": "Uzbekistan", "numeric": "860"},
{"alpha_2": "VU", "alpha_3": "VUT", "name": "Vanuatu", "numeric": "548"},
{"alpha_2": "VE", "alpha_3": "VEN", "name": "Venezuela, Bolivarian Republic of", "numeric": "862"},
{"alpha_2": "VN", "alpha_3": "VNM", "name": "Viet Nam", "numeric": "704"},
{"alpha_2": "VG", "alpha_3": "VGB", "name": "Virgin Islands, British", "numeric": "092"},
{"alpha_2": "VI", "alpha_3": "VIR", "name": "Virgin Islands, U.S.", "numeric": "850"},
{"alpha_2": "WF", "alpha_3": "WLF", "name": "Wallis and Futuna", "numeric": "876"},
{"alpha_2": "EH", "alpha_3": "ESH", "name": "Western Sahara", "numeric": "732"},
{"alpha_2": "YE", "alpha_3": "YEM", "name": "Yemen", "numeric": "887"},
{"alpha_2": "ZM", "alpha_3": "ZMB", "name": "Zambia", "numeric": "894"},
{"alpha_2": "ZW", "alpha_3": "ZWE", "name": "Zimbabwe", "numeric": "716"},
{"alpha_2": "AX", "alpha_3": "ALA", "name": "Åland Islands", "numeric": "248"}
]
}

View File

@@ -0,0 +1,182 @@
"""Utilities for standardizing country inputs across providers using ISO 3166 standards.
This module provides a Country type that inherits from str for type checker compatibility
while providing full access to ISO 3166 country data via a static dataset.
Providers can access alpha_2, alpha_3, name, and numeric properties as needed.
References:
- ISO 3166-1: https://en.wikipedia.org/wiki/ISO_3166-1
"""
import json
from pathlib import Path
from typing import Any
from pydantic_core import core_schema
def _load_country_data() -> dict[str, dict[str, str]]:
"""Load country data from JSON and build lookup indices.
Returns a dict with lookup keys (alpha_2, alpha_3, name variants) mapping to country data.
"""
data_path = Path(__file__).parent / "country_data.json"
with open(data_path, encoding="utf-8") as f:
data = json.load(f)
lookup: dict[str, dict[str, str]] = {}
for country in data["countries"]:
# Index by alpha_2 (case-insensitive)
lookup[country["alpha_2"].upper()] = country
lookup[country["alpha_2"].lower()] = country
# Index by alpha_3 (case-insensitive)
lookup[country["alpha_3"].upper()] = country
lookup[country["alpha_3"].lower()] = country
# Index by name (case-insensitive)
name_lower = country["name"].lower()
lookup[name_lower] = country
lookup[country["name"]] = country
# Index by snake_case name
snake_name = name_lower.replace(" ", "_").replace(",", "").replace("'", "")
lookup[snake_name] = country
return lookup
# Load once at module import - no decompression needed, just simple JSON parse
_COUNTRY_LOOKUP = _load_country_data()
class Country(str):
"""Country string type with full ISO 3166 data access.
Inherits from str (storing alpha_2 code) for type checker compatibility
while providing access to alpha_2, alpha_3, name, and numeric properties.
Accepts:
- ISO 3166-1 alpha-2 codes (e.g., "US", "us")
- ISO 3166-1 alpha-3 codes (e.g., "USA", "usa")
- Full country names (e.g., "United States")
- lower_snake_case names (e.g., "united_states")
Examples
--------
>>> c = Country("united_states")
>>> str(c)
'US'
>>> c.alpha_2
'US'
>>> c.alpha_3
'USA'
>>> c.name
'United States'
"""
_country_data: dict[str, str]
def __new__(cls, value: Any) -> "Country":
"""Create a new Country instance.
Parameters
----------
value : Any
Country input (alpha-2, alpha-3, name, or lower_snake_case).
Returns
-------
Country
A Country instance storing the alpha_2 code as its string value.
Raises
------
ValueError
If the country cannot be resolved.
"""
country_data = (
value._country_data
if isinstance(value, Country)
else cls._lookup_country(value)
)
# Create str instance with the alpha_2 code
instance = super().__new__(cls, country_data["alpha_2"])
# Store the country data dict
instance._country_data = country_data
return instance
@staticmethod
def _lookup_country(value: Any) -> dict[str, str]:
"""Look up a country from various input formats.
Parameters
----------
value : Any
Country input to look up.
Returns
-------
dict[str, str]
The country data dictionary with alpha_2, alpha_3, name, numeric.
Raises
------
ValueError
If the country cannot be found.
"""
val = str(value).strip()
# Convert lower_snake_case to lookup key
if "_" in val:
val = val.replace("_", " ")
# Try direct lookup
lookup_key = val.lower()
if lookup_key in _COUNTRY_LOOKUP:
return _COUNTRY_LOOKUP[lookup_key]
# Try original case
if val in _COUNTRY_LOOKUP:
return _COUNTRY_LOOKUP[val]
raise ValueError(
f"Invalid country: '{value}'. "
"Accepts ISO 3166-1 alpha-2 codes (e.g., 'US'), "
"alpha-3 codes (e.g., 'USA'), "
"or country names (e.g., 'United States', 'united_states')."
)
@property
def alpha_2(self) -> str:
"""ISO 3166-1 alpha-2 code (e.g., 'US')."""
return self._country_data["alpha_2"]
@property
def alpha_3(self) -> str:
"""ISO 3166-1 alpha-3 code (e.g., 'USA')."""
return self._country_data["alpha_3"]
@property
def name(self) -> str:
"""The country's official designation (e.g., 'United States')."""
return self._country_data["name"]
@property
def numeric(self) -> str | None:
"""ISO 3166-1 numeric code (e.g., '840'), if available."""
return self._country_data.get("numeric")
@classmethod
def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any:
"""Return the Pydantic core schema for validation."""
return core_schema.no_info_after_validator_function(
cls,
core_schema.str_schema(),
)
# Backwards-compatible alias
CountryParam = Country

View File

@@ -0,0 +1,99 @@
"""Tests for country_utils module."""
import pytest
from openbb_core.provider.utils.country_utils import Country, CountryParam
from pydantic import BaseModel, ValidationError
class CountryTestModel(BaseModel):
"""Test model for Country validation."""
country: Country | None = None
class TestCountry:
"""Tests for Country type."""
def test_iso_alpha2_code(self):
"""ISO alpha-2 codes should be accepted."""
model = CountryTestModel(country="US")
assert str(model.country) == "US"
assert model.country.alpha_2 == "US"
assert model.country.alpha_3 == "USA"
model = CountryTestModel(country="us")
assert str(model.country) == "US"
model = CountryTestModel(country="DE")
assert str(model.country) == "DE"
assert model.country.alpha_3 == "DEU"
def test_iso_alpha3_code(self):
"""ISO alpha-3 codes should be accepted."""
model = CountryTestModel(country="USA")
assert str(model.country) == "US"
assert model.country.alpha_2 == "US"
assert model.country.alpha_3 == "USA"
model = CountryTestModel(country="deu")
assert str(model.country) == "DE"
def test_country_name(self):
"""Full country names should resolve to alpha-2 codes."""
model = CountryTestModel(country="United States")
assert str(model.country) == "US"
assert model.country.name == "United States"
model = CountryTestModel(country="Germany")
assert str(model.country) == "DE"
assert model.country.name == "Germany"
model = CountryTestModel(country="United Kingdom")
assert str(model.country) == "GB"
def test_lower_snake_case(self):
"""lower_snake_case names should be accepted."""
model = CountryTestModel(country="united_states")
assert str(model.country) == "US"
model = CountryTestModel(country="united_kingdom")
assert str(model.country) == "GB"
def test_none_value(self):
"""None values should pass through."""
model = CountryTestModel(country=None)
assert model.country is None
def test_invalid_country(self):
"""Invalid country names should raise ValidationError."""
with pytest.raises(ValidationError):
CountryTestModel(country="InvalidCountry")
with pytest.raises(ValidationError):
CountryTestModel(country="XX") # Invalid ISO code
def test_country_properties(self):
"""Country should expose all ISO properties."""
c = Country("US")
assert c.alpha_2 == "US"
assert c.alpha_3 == "USA"
assert c.name == "United States"
assert c.numeric == "840"
def test_str_inheritance(self):
"""Country should behave like a string."""
c = Country("US")
assert isinstance(c, str)
assert c == "US"
assert c.lower() == "us"
assert f"Country: {c}" == "Country: US"
def test_model_dump(self):
"""model_dump should serialize to string."""
model = CountryTestModel(country="united_states")
dumped = model.model_dump()
assert dumped["country"] == "US"
def test_country_param_alias(self):
"""CountryParam should be an alias for Country."""
assert CountryParam is Country

View File

@@ -9,6 +9,7 @@ from openbb_core.provider.standard_models.equity_screener import (
EquityScreenerData,
EquityScreenerQueryParams,
)
from openbb_core.provider.utils.country_utils import Country
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_fmp.utils.definitions import (
Countries,
@@ -121,8 +122,10 @@ class FMPEquityScreenerQueryParams(EquityScreenerQueryParams):
default=None,
description="Filter by industry.",
)
country: Countries | None = Field(
default=None, description="Filter by country, as a two-letter country code."
country: Country | None = Field(
default=None,
description="Filter by country. Accepts ISO 3166-1 alpha-2 codes (e.g., 'US', 'DE'), "
"alpha-3 codes (e.g., 'USA'), or country names (e.g., 'United States', 'united_states').",
)
exchange: Exchanges | None = Field(default=None, description="Filter by exchange.")
is_etf: bool | None = Field(
@@ -154,6 +157,22 @@ class FMPEquityScreenerQueryParams(EquityScreenerQueryParams):
raise ValueError(f"Industry must be one of {', '.join(industries)}")
return v
@field_validator("country", mode="after")
@classmethod
def _validate_country(cls, v):
"""Validate country is supported by FMP."""
if v is None:
return v
# Country stores alpha_2 in uppercase, FMP expects lowercase
country_code = v.alpha_2.lower()
valid_countries = list(Countries.__args__)
if country_code not in valid_countries:
raise ValueError(
f"Country '{v.name}' ({v.alpha_2}) is not supported by FMP. "
f"Valid options: {', '.join(sorted(valid_countries)[:20])}..."
)
return v
class FMPEquityScreenerData(EquityScreenerData):
"""FMP Equity Screener Data."""