├── .coveragerc ├── .github └── workflows │ ├── publish.yaml │ └── run-python-tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── documentation ├── .readthedocs.yaml ├── index.md └── mkdocs.yaml ├── hatch.toml ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── ruff.toml ├── src └── hdx │ └── location │ ├── Countries & Territories Taxonomy MVP - C&T Taxonomy with HXL Tags.csv │ ├── __init__.py │ ├── adminlevel.py │ ├── country.py │ ├── currency.py │ ├── int_timestamp.py │ ├── wfp_api.py │ └── wfp_exchangerates.py └── tests ├── fixtures ├── adminlevel.yaml ├── adminlevelparent.yaml ├── download-global-pcode-lengths.csv ├── download-global-pcodes-adm-1-2.csv ├── secondary_historic_rates.csv ├── secondary_rates.json └── wfp │ ├── Currency_List_1.json │ ├── Currency_List_2.json │ ├── Currency_UsdIndirectQuotation_1.json │ ├── Currency_UsdIndirectQuotation_10.json │ ├── Currency_UsdIndirectQuotation_2.json │ ├── Currency_UsdIndirectQuotation_3.json │ ├── Currency_UsdIndirectQuotation_4.json │ ├── Currency_UsdIndirectQuotation_5.json │ ├── Currency_UsdIndirectQuotation_6.json │ ├── Currency_UsdIndirectQuotation_7.json │ ├── Currency_UsdIndirectQuotation_8.json │ └── Currency_UsdIndirectQuotation_9.json └── hdx └── location ├── Countries_UZB_Deleted.csv ├── __init__.py ├── conftest.py ├── test_adminlevel.py ├── test_country.py ├── test_currency.py └── test_wfp_exchangerates.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = src 3 | 4 | omit = */_version.py 5 | 6 | [report] 7 | exclude_also = 8 | from ._version 9 | def __repr__ 10 | if self.debug: 11 | if settings.DEBUG 12 | raise AssertionError 13 | raise NotImplementedError 14 | if 0: 15 | if __name__ == .__main__.: 16 | if TYPE_CHECKING: 17 | @(abc\.)?abstractmethod 18 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | environment: 12 | name: pypi 13 | url: https://pypi.org/p/hdx-python-country 14 | 15 | permissions: 16 | id-token: write # IMPORTANT: mandatory for trusted publishing 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Get history and tags for versioning to work 21 | run: | 22 | git fetch --prune --unshallow 23 | git fetch --depth=1 origin +refs/tags/*:refs/tags/* 24 | - name: Set up Python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: '3.x' 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | - name: Install Hatch 32 | uses: pypa/hatch@install 33 | - name: Build with hatch 34 | run: | 35 | hatch build 36 | - name: Publish distribution 📦 to PyPI 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | -------------------------------------------------------------------------------- /.github/workflows/run-python-tests.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, lint and run tests 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Run tests 5 | 6 | on: 7 | workflow_dispatch: # add run button in github 8 | push: 9 | branches-ignore: 10 | - gh-pages 11 | - 'dependabot/**' 12 | pull_request: 13 | branches-ignore: 14 | - gh-pages 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | permissions: 21 | checks: write 22 | pull-requests: write 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.x' 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | - name: Install Hatch 34 | uses: pypa/hatch@install 35 | - name: Test with hatch/pytest 36 | run: | 37 | hatch test 38 | - name: Check styling 39 | if: always() 40 | run: | 41 | hatch fmt --check 42 | - name: Publish Unit Test Results 43 | uses: EnricoMi/publish-unit-test-result-action@v2 44 | if: always() 45 | with: 46 | github_token: ${{ secrets.GITHUB_TOKEN }} 47 | junit_files: test-results.xml 48 | - name: Publish in Coveralls 49 | uses: coverallsapp/github-action@v2 50 | with: 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | flag-name: tests 53 | format: lcov 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | tests/*.log 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | documentation/_*/ 67 | documentation/source/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # IntelliJ 95 | .idea/ 96 | 97 | # hatch-vcs 98 | _version.py 99 | 100 | # Mac files 101 | .DS_Store 102 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.13 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-ast 10 | - repo: https://github.com/astral-sh/ruff-pre-commit 11 | rev: v0.12.0 12 | hooks: 13 | # Run the linter. 14 | - id: ruff-check 15 | args: [ --fix ] 16 | # Run the formatter. 17 | - id: ruff-format 18 | - repo: https://github.com/astral-sh/uv-pre-commit 19 | rev: 0.7.14 20 | hooks: 21 | # Run the pip compile 22 | - id: pip-compile 23 | name: pip-compile requirements.txt 24 | files: pyproject.toml 25 | args: [ pyproject.toml, --resolver=backtracking, --all-extras, --upgrade, -q, -o, requirements.txt ] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Michael Rans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | Regular expressions from countrycode 24 | (https://github.com/vincentarelbundock/countrycode) incorporated and relicensed 25 | under The MIT License (MIT) with the kind permission of Vincent Arel-Bundock, 26 | Nils Enevoldsen, and CJ Yetman. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/OCHA-DAP/hdx-python-country/actions/workflows/run-python-tests.yaml/badge.svg)](https://github.com/OCHA-DAP/hdx-python-country/actions/workflows/run-python-tests.yaml) 2 | [![Coverage Status](https://coveralls.io/repos/github/OCHA-DAP/hdx-python-country/badge.svg?branch=main&ts=1)](https://coveralls.io/github/OCHA-DAP/hdx-python-country?branch=main) 3 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 4 | [![Downloads](https://img.shields.io/pypi/dm/hdx-python-country.svg)](https://pypistats.org/packages/hdx-python-country) 5 | 6 | The HDX Python Country Library provides utilities to map between country and region 7 | codes and names and to match administrative level names from different sources. 8 | It also provides utilities for foreign exchange enabling obtaining current and historic 9 | FX rates for different currencies. 10 | 11 | It provides country mappings including ISO 2 and ISO 3 letter codes (ISO 3166) and regions 12 | using live official data from the [UN OCHA](https://vocabulary.unocha.org/) feed with 13 | fallbacks to an internal static file if there is any problem with retrieving data from 14 | the url. (Also it is possible to force the use of the internal static files.) 15 | 16 | It can exact match English, French, Spanish, Russian, Chinese and Arabic. There is a 17 | fuzzy matching for English look up that can handle abbreviations in country names like 18 | Dem. for Democratic and Rep. for Republic. 19 | 20 | Mapping administration level names from a source to a given base set is also handled 21 | including phonetic fuzzy name matching. 22 | 23 | It also provides foreign exchange rates and conversion from amounts in local 24 | currency to USD and vice-versa. The conversion relies on Yahoo Finance, falling 25 | back on [currency-api](https://github.com/fawazahmed0/currency-api) for current rates, and Yahoo Finance falling back 26 | on IMF data via IATI (with interpolation) for historic daily rates. 27 | 28 | For more information, please read the [documentation](https://hdx-python-country.readthedocs.io/en/latest/). 29 | 30 | This library is part of the [Humanitarian Data Exchange](https://data.humdata.org/) 31 | (HDX) project. If you have humanitarian related data, please upload your datasets to 32 | HDX. 33 | 34 | ## Development 35 | 36 | ### Environment 37 | 38 | Development is currently done using Python 3.12. We recommend using a virtual 39 | environment such as ``venv``: 40 | 41 | ```shell 42 | python -m venv venv 43 | source venv/bin/activate 44 | ``` 45 | 46 | In your virtual environment, install all packages for development by running: 47 | 48 | ```shell 49 | pip install -r requirements.txt 50 | ``` 51 | 52 | ### Pre-commit 53 | 54 | Be sure to install `pre-commit`, which is run every time you make a git commit: 55 | 56 | ```shell 57 | pip install pre-commit 58 | pre-commit install 59 | ``` 60 | 61 | With pre-commit, all code is formatted according to 62 | [ruff](https://docs.astral.sh/ruff/) guidelines. 63 | 64 | To check if your changes pass pre-commit without committing, run: 65 | 66 | ```shell 67 | pre-commit run --all-files 68 | ``` 69 | 70 | ### Testing 71 | 72 | Ensure you have the required packages to run the tests: 73 | 74 | ```shell 75 | pip install -r requirements.txt 76 | ``` 77 | 78 | To run the tests and view coverage, execute: 79 | 80 | ```shell 81 | pytest -c --cov hdx 82 | ``` 83 | 84 | ## Packages 85 | 86 | [uv](https://github.com/astral-sh/uv) is used for package management. If 87 | you’ve introduced a new package to the source code (i.e. anywhere in `src/`), 88 | please add it to the `project.dependencies` section of `pyproject.toml` with 89 | any known version constraints. 90 | 91 | To add packages required only for testing, add them to the `test` section under 92 | `[project.optional-dependencies]`. 93 | 94 | Any changes to the dependencies will be automatically reflected in 95 | `requirements.txt` with `pre-commit`, but you can re-generate the file without 96 | committing by executing: 97 | 98 | ```shell 99 | pre-commit run pip-compile --all-files 100 | ``` 101 | 102 | ## Project 103 | 104 | [Hatch](https://hatch.pypa.io/) is used for project management. The project can be built using: 105 | 106 | ```shell 107 | hatch build 108 | ``` 109 | 110 | Linting and syntax checking can be run with: 111 | 112 | ```shell 113 | hatch fmt --check 114 | ``` 115 | 116 | Tests can be executed using: 117 | 118 | ```shell 119 | hatch test 120 | ``` 121 | -------------------------------------------------------------------------------- /documentation/.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Required 2 | version: 2 3 | 4 | # Set the version of Python and other tools you might need 5 | build: 6 | os: ubuntu-24.04 7 | tools: 8 | python: "3.12" 9 | jobs: 10 | pre_build: 11 | - pip install .[docs] 12 | 13 | mkdocs: 14 | configuration: documentation/mkdocs.yaml 15 | -------------------------------------------------------------------------------- /documentation/index.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | The HDX Python Country Library provides utilities to map between country and region 4 | codes and names and to match administrative level names from different sources. 5 | It also provides utilities for foreign exchange enabling obtaining current and historic 6 | FX rates for different currencies. 7 | 8 | # Contents 9 | 10 | 1. [Information](#information) 11 | 2. [Countries](#countries) 12 | 3. [Administration Level](#administration-level) 13 | 4. [Currencies](#currencies) 14 | 15 | # Information 16 | 17 | The library provides country mappings including ISO 2 and ISO 3 letter codes (ISO 3166) 18 | and regions using live official data from the [UN OCHA](https://vocabulary.unocha.org/) 19 | feed with fallbacks to an internal static file if there is any problem with retrieving 20 | data from the url. (Also it is possible to force the use of the internal static files.) 21 | The UN OCHA feed has regex taken from 22 | [here](https://github.com/konstantinstadler/country_converter/blob/master/country_converter/country_data.tsv). 23 | with improvements contributed back. 24 | 25 | It can exact match English, French, Spanish, Russian, Chinese and Arabic. There is a 26 | fuzzy matching for English look up that can handle abbreviations in country names like 27 | Dem. for Democratic and Rep. for Republic. 28 | 29 | Mapping administration level names from a source to a given base set is also handled 30 | including phonetic fuzzy name matching. 31 | 32 | It also provides foreign exchange rates and conversion from amounts in local 33 | currency to USD and vice-versa. The conversion relies on Yahoo Finance, falling 34 | back on [currency-api](https://github.com/fawazahmed0/currency-api) for current rates, and Yahoo Finance falling back 35 | on IMF data via IATI (with interpolation) for historic daily rates. 36 | 37 | This library is part of the [Humanitarian Data Exchange](https://data.humdata.org/) 38 | (HDX) project. If you have humanitarian related data, please upload your datasets to 39 | HDX. 40 | 41 | The code for the library is [here](https://github.com/OCHA-DAP/hdx-python-country). 42 | The library has detailed API documentation which can be found in the menu at the top. 43 | 44 | ## Breaking Changes 45 | From 3.9.2, must call Currency.setup before using Currency methods. 46 | 47 | From 3.7.5, removed clean_name function. There is now a function normalise in 48 | HDX Python Utilities. 49 | 50 | From 3.5.5, after creating an Adminlevel, call either setup_from_admin_info, 51 | setup_from_libhxl_dataset or setup_from_url. 52 | 53 | From 3.4.6, Python 3.7 no longer supported 54 | 55 | From 3.3.2, major update to foreign exchange code and use of new Yahoo data source 56 | 57 | From 3.0.0, only supports Python >= 3.6 58 | 59 | Version 2.x.x of the library is a significant change from version 1.x.x which sourced 60 | its data from different feeds (UN Stats and the World Bank). Consequently, although 61 | most of the api calls work the same way in 2.x.x, the ones that return full country 62 | information do so in a different format to 1.x.x. The format they use is a dictionary 63 | using [Humanitarian Exchange Language](https://hxlstandard.org/) (HXL) hashtags as keys. 64 | 65 | # Description of Utilities 66 | 67 | ## Countries 68 | 69 | The usage of the country mappings functionality is best illustrated by some examples: 70 | 71 | from hdx.location.country import Country 72 | 73 | Country.countriesdata(include_unofficial=True, use_live=False, country_name_overrides={"PSE": "oPt"}) 74 | # Set up including unofficial alpha2 (eg. AN) and alpha3 codes (eg. XKX) 75 | # Use non live data from repo and override default country name 76 | # (Leaving out this step will exclude unofficial alpha codes, use live data and no overrides) 77 | Country.get_country_name_from_iso3("jpn", use_live=False) # returns "Japan" 78 | Country.get_country_name_from_iso3("vEn", formal=True) # returns "the Bolivarian Republic of Venezuela" 79 | # uselive=False forces the use of internal files instead of accessing the live feeds. 80 | # It only needs to be supplied to the first call as the data once loaded is held 81 | # in internal dictionaries for future use. 82 | Country.get_country_name_from_iso2("Pl") # returns "Poland" 83 | Country.get_iso3_country_code("UZBEKISTAN") # returns "UZB" 84 | Country.get_country_name_from_m49(4) # returns "Afghanistan" 85 | 86 | Country.get_iso3_country_code_fuzzy("Sierra") 87 | # performs fuzzy match and returns ("SLE", False). The False indicates a fuzzy rather than exact match. 88 | assert Country.get_iso3_country_code_fuzzy("Czech Rep.") 89 | # returns ("CZE", False) 90 | 91 | Country.get_country_info_from_iso2("jp") 92 | # Returns dictionary with HXL hashtags as keys. For more on HXL, see http://hxlstandard.org/ 93 | # {"#country+alt+i_ar+name+v_m49": "اليابان", "#country+alt+i_ar+name+v_unterm": "اليابان", 94 | # "#country+alt+i_en+name+v_m49": "Japan", "#country+alt+i_en+name+v_unterm": "Japan", 95 | # "#country+alt+i_es+name+v_m49": "Japón", "#country+alt+i_es+name+v_unterm": "Japón", 96 | # "#country+alt+i_fr+name+v_m49": "Japon", "#country+alt+i_fr+name+v_unterm": "Japon", 97 | # "#country+alt+i_ru+name+v_m49": "Япония", "#country+alt+i_ru+name+v_unterm": "Япония", 98 | # "#country+alt+i_zh+name+v_m49": "日本", "#country+alt+i_zh+name+v_unterm": "日本", 99 | # "#country+alt+name+v_dgacm": "", "#country+alt+name+v_hpctools": "", 100 | # "#country+alt+name+v_iso": "", "#country+alt+name+v_reliefweb": "", 101 | # "#country+code+num+v_m49": "392", "#country+code+v_hpctools": "112", 102 | # "#country+code+v_iso2": "JP", "#country+code+v_iso3": "JPN", 103 | # "#country+code+v_reliefweb": "128", "#country+formal+i_en+name+v_unterm": "Japan", 104 | # "#country+name+preferred": "Japan", "#country+name+short+v_reliefweb": "", 105 | # "#country+regex": "japan", "#currency+code": "JPY", "#date+start": "1974-01-01", 106 | # "#geo+admin_level": "0", "#geo+lat": "37.63209801", "#geo+lon": "138.0812256", 107 | # "#indicator+bool+hrp": "", "#indicator+bool+gho": "", "#indicator+incomelevel": "High", 108 | # "#meta+bool+deprecated": "N", "#meta+bool+independent": "Y", "#meta+id": "112", 109 | # "#region+code+intermediate": "", "#region+code+main": "142", "#region+code+sub": "30", 110 | # "#region+intermediate+name+preferred": "", "#region+main+name+preferred": "Asia", 111 | # "#region+name+preferred+sub": "Eastern Asia"} 112 | Country.get_countries_in_region("Channel Islands") 113 | # ["GGY", "JEY"] 114 | len(Country.get_countries_in_region("Africa")) 115 | # 60 116 | Country.get_countries_in_region(13) 117 | # ["BLZ", "CRI", "GTM", "HND", "MEX", "NIC", "PAN", "SLV"] 118 | 119 | ## Administration Level 120 | 121 | The administration level mappings takes input configuration dictionary, 122 | *admin_config*, which defaults to an empty dictionary. 123 | 124 | *admin_config* can have the following optional keys: 125 | 126 | *countries_fuzzy_try* are countries (iso3 codes) for which to try fuzzy 127 | matching. Default is all countries. 128 | *admin_name_mappings* is a dictionary of mappings from name to pcode. These can 129 | be global or they can be restricted by country or parent (if the AdminLevel 130 | object has been set up with parents). Keys take the form "MAPPING", 131 | "AFG|MAPPING" or "AF01|MAPPING". 132 | *admin_name_replacements* is a dictionary of textual replacements to try when 133 | fuzzy matching. It maps from string to string replacement. The replacements can 134 | be global or they can be restricted by country or parent (if the AdminLevel 135 | object has been set up with parents). Keys take the form "STRING_TO_REPLACE", 136 | "AFG|STRING_TO_REPLACE" or "AF01|STRING_TO_REPLACE". 137 | *admin_fuzzy_dont* is a list of names for which fuzzy matching should not be 138 | tried 139 | 140 | A Retrieve object can be passed in the *retriever* parameter that enables 141 | saving data downloaded to a file or loading previously saved data depending 142 | on how the Retrieve object is configured. 143 | 144 | Once an AdminLevel object is constructed, one of three setup methods must be 145 | called: *setup_from_admin_info*, *setup_from_libhxl_dataset* or 146 | *setup_from_url*. 147 | 148 | Method *setup_from_admin_info* takes key *admin_info* which is a list with 149 | values of the form: 150 | 151 | {"iso3": "AFG", "pcode": "AF01", "name": "Kabul"} 152 | {"iso3": "AFG", "pcode": "AF0101", "name": "Kabul", "parent": "AF01"} 153 | 154 | Dictionaries *pcode_to_name* and *pcode_to_iso3* are populated in the 155 | AdminLevel object. *parent* is optional, but if provided enables lookup of 156 | location names by both country and parent rather than just country which should 157 | help with any name clashes. It also results in the population of a dictionary 158 | in the AdminLevel object *pcode_to_parent*. 159 | 160 | Method *setup_from_libhxl_dataset* takes a libhxl Dataset object, while 161 | *setup_from_url* takes a URL which defaults to a resource in the global p-codes 162 | dataset on HDX. 163 | 164 | These methods also have optional parameter *countryiso3s* which is a tuple or 165 | list of country ISO3 codes to be read or None if all countries are desired. 166 | 167 | Examples of usage: 168 | 169 | AdminLevel.looks_like_pcode("YEM123") # returns True 170 | AdminLevel.looks_like_pcode("Yemen") # returns False 171 | AdminLevel.looks_like_pcode("YEME123") # returns False 172 | adminlevel = AdminLevel(config) 173 | adminlevel.setup_from_admin_info(admin_info, countryiso3s=("YEM",)) 174 | adminlevel.get_pcode("YEM", "YEM030", logname="test") # returns ("YE30", True) 175 | adminlevel.get_pcode("YEM", "Al Dhale"e / الضالع") # returns ("YE30", False) 176 | adminlevel.get_pcode("YEM", "Al Dhale"e / الضالع", fuzzy_match=False) # returns (None, True) 177 | assert admintwo.get_pcode("AFG", "Kabul", parent="AF01") == ("AF0101", True) 178 | 179 | There is basic admin 1 p-code length conversion by default. A more advanced 180 | p-code length conversion can be activated by calling *load_pcode_formats* 181 | which takes a URL that defaults to a resource in the global p-codes dataset on 182 | HDX: 183 | 184 | admintwo.load_pcode_formats() 185 | admintwo.get_pcode("YEM", "YEM30001") # returns ("YE3001", True) 186 | 187 | The length conversion can be further enhanced by supplying either parent 188 | AdminLevel objects in a list or lists of p-codes per parent admin level: 189 | 190 | admintwo.set_parent_admins_from_adminlevels([adminone]) 191 | admintwo.get_pcode("NER", "NE00409") # returns ("NER004009", True) 192 | admintwo.set_parent_admins([adminone.pcodes]) 193 | admintwo.get_pcode("NER", "NE00409") # returns ("NER004009", True) 194 | 195 | ## Currencies 196 | 197 | Various functions support the conversion of monetary amounts to USD. The setup 198 | method must be called once before using any other methods. Note that the 199 | returned values are cached to reduce network usage which means that the 200 | library is unsuited for use where rates are expected to update while the 201 | program is running: 202 | 203 | Currency.setup(fallback_historic_to_current=True, fallback_current_to_static=True, log_level=logging.INFO) 204 | currency = Country.get_currency_from_iso3("usa") # returns "USD" 205 | assert Currency.get_current_rate("usd") # returns 1 206 | Currency.get_current_value_in_usd(10, currency) # returns 10 207 | gbprate = Currency.get_current_value_in_usd(10, "gbp") 208 | assert gbprate != 10 209 | Currency.get_current_value_in_currency(gbprate, "GBP") # returns 10 210 | date = parse_date("2020-02-20") 211 | Currency.get_historic_rate("gbp", date) # returns 0.7735000252723694 212 | Currency.get_historic_rate("gbp", parse_date("2020-02-20 00:00:00 NZST"), 213 | ignore_timeinfo=False) # returns 0.76910001039505 214 | Currency.get_historic_value_in_usd(10, "USD", date) # returns 10 215 | Currency.get_historic_value_in_usd(10, "gbp", date) # returns 13.002210200027791 216 | Currency.get_historic_value_in_currency(10, "gbp", date) # returns 7.735000252723694 217 | Currency.get_historic_rate("gbp", parse_date("2020-02-20 00:00:00 NZST", 218 | timezone_handling=2), ignore_timeinfo=False) 219 | # == 0.76910001039505 220 | 221 | Historic daily rates can be made to fall back to current rates if desired (this 222 | is not the default). It is possible to pass in a Retrieve object to 223 | Currency.setup() to allow the downloaded files from the secondary sources to be 224 | saved or previously downloaded files to be reused and to allow fallbacks from 225 | current rates to a static file eg. 226 | 227 | Currency.setup(retriever, ..., fallback_historic_to_current=True, fallback_current_to_static=True) 228 | -------------------------------------------------------------------------------- /documentation/mkdocs.yaml: -------------------------------------------------------------------------------- 1 | site_name: HDX Python Country 2 | repo_url: https://github.com/OCHA-DAP/hdx-python-country/ 3 | repo_name: OCHA-DAP/hdx-python-country 4 | docs_dir: . 5 | site_dir: ../site 6 | theme: 7 | name: material 8 | highlightjs: true 9 | plugins: 10 | - search 11 | - mkapi 12 | nav: 13 | - Home: index.md 14 | - API Documentation: 15 | - Countries: $src/hdx.location.country.* 16 | - Administration Level: $src/hdx.location.adminlevel.* 17 | - Currencies: $src/hdx.location.currency.* 18 | -------------------------------------------------------------------------------- /hatch.toml: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | [build.targets.wheel] 4 | packages = ["src/hdx"] 5 | 6 | [build.hooks.vcs] 7 | version-file = "src/hdx/location/_version.py" 8 | 9 | [metadata] 10 | allow-direct-references = true 11 | 12 | # Versioning 13 | 14 | [version] 15 | source = "vcs" 16 | 17 | [version.raw-options] 18 | local_scheme = "no-local-version" 19 | version_scheme = "python-simplified-semver" 20 | 21 | # Tests 22 | 23 | [envs.hatch-test] 24 | features = ["test"] 25 | 26 | [[envs.hatch-test.matrix]] 27 | python = ["3.13"] 28 | 29 | [envs.hatch-test.scripts] 30 | run = """ 31 | pytest --rootdir=. --junitxml=test-results.xml --cov --no-cov-on-fail \ 32 | --cov-report=lcov --cov-report=term-missing 33 | """ 34 | 35 | [envs.hatch-static-analysis] 36 | config-path = "none" 37 | dependencies = ["ruff==0.9.10"] 38 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | ######################### 2 | # Project Configuration # 3 | ######################### 4 | 5 | [build-system] 6 | requires = ["hatchling", "hatch-vcs"] 7 | build-backend = "hatchling.build" 8 | 9 | [project] 10 | name = "hdx-python-country" 11 | description = "HDX Python country code and exchange rate (fx) utilities" 12 | authors = [{name = "Michael Rans"}] 13 | license = {text = "MIT"} 14 | keywords = ["HDX", "location", "country", "country code", "iso 3166", "iso2", "iso3", "region", "fx", "currency", "currencies", "exchange rate", "foreign exchange"] 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Topic :: Software Development :: Libraries :: Python Modules", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3 :: Only", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: MIT License", 28 | "Natural Language :: English", 29 | "Operating System :: POSIX :: Linux", 30 | "Operating System :: Unix", 31 | "Operating System :: MacOS", 32 | "Operating System :: Microsoft :: Windows", 33 | ] 34 | requires-python = ">=3.8" 35 | 36 | dependencies = [ 37 | "hdx-python-utilities>=3.9.2", 38 | "libhxl>=5.2.2", 39 | "tenacity", 40 | ] 41 | dynamic = ["version"] 42 | 43 | [project.readme] 44 | file = "README.md" 45 | content-type = "text/markdown" 46 | 47 | [project.urls] 48 | Homepage = "https://github.com/OCHA-DAP/hdx-python-country" 49 | 50 | [project.optional-dependencies] 51 | test = ["pytest", "pytest-cov"] 52 | dev = ["pre-commit"] 53 | docs = ["mkapi"] 54 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = src 3 | addopts = "--color=yes" 4 | log_cli = 1 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile pyproject.toml --resolver=backtracking --all-extras -o requirements.txt 3 | annotated-types==0.7.0 4 | # via pydantic 5 | astdoc==1.3.2 6 | # via mkapi 7 | attrs==25.3.0 8 | # via 9 | # frictionless 10 | # jsonlines 11 | # jsonschema 12 | # referencing 13 | babel==2.17.0 14 | # via mkdocs-material 15 | backrefs==5.9 16 | # via mkdocs-material 17 | certifi==2025.8.3 18 | # via requests 19 | cfgv==3.4.0 20 | # via pre-commit 21 | chardet==5.2.0 22 | # via frictionless 23 | charset-normalizer==3.4.3 24 | # via requests 25 | click==8.2.1 26 | # via 27 | # mkdocs 28 | # mkdocs-material 29 | # typer 30 | colorama==0.4.6 31 | # via mkdocs-material 32 | coverage==7.10.7 33 | # via pytest-cov 34 | distlib==0.4.0 35 | # via virtualenv 36 | et-xmlfile==2.0.0 37 | # via openpyxl 38 | filelock==3.19.1 39 | # via virtualenv 40 | frictionless==5.18.1 41 | # via hdx-python-utilities 42 | ghp-import==2.1.0 43 | # via mkdocs 44 | hdx-python-utilities==3.9.2 45 | # via hdx-python-country (pyproject.toml) 46 | humanize==4.13.0 47 | # via frictionless 48 | identify==2.6.14 49 | # via pre-commit 50 | idna==3.10 51 | # via requests 52 | ijson==3.4.0 53 | # via hdx-python-utilities 54 | iniconfig==2.1.0 55 | # via pytest 56 | isodate==0.7.2 57 | # via frictionless 58 | jinja2==3.1.6 59 | # via 60 | # frictionless 61 | # mkapi 62 | # mkdocs 63 | # mkdocs-material 64 | jsonlines==4.0.0 65 | # via hdx-python-utilities 66 | jsonpath-ng==1.7.0 67 | # via libhxl 68 | jsonschema==4.25.1 69 | # via 70 | # frictionless 71 | # tableschema-to-template 72 | jsonschema-specifications==2025.9.1 73 | # via jsonschema 74 | libhxl==5.2.2 75 | # via hdx-python-country (pyproject.toml) 76 | loguru==0.7.3 77 | # via hdx-python-utilities 78 | markdown==3.9 79 | # via 80 | # mkdocs 81 | # mkdocs-material 82 | # pymdown-extensions 83 | markdown-it-py==4.0.0 84 | # via rich 85 | marko==2.2.0 86 | # via frictionless 87 | markupsafe==3.0.2 88 | # via 89 | # jinja2 90 | # mkdocs 91 | mdurl==0.1.2 92 | # via markdown-it-py 93 | mergedeep==1.3.4 94 | # via 95 | # mkdocs 96 | # mkdocs-get-deps 97 | mkapi==4.4.5 98 | # via hdx-python-country (pyproject.toml) 99 | mkdocs==1.6.1 100 | # via 101 | # mkapi 102 | # mkdocs-material 103 | mkdocs-get-deps==0.2.0 104 | # via mkdocs 105 | mkdocs-material==9.6.20 106 | # via mkapi 107 | mkdocs-material-extensions==1.3.1 108 | # via mkdocs-material 109 | nodeenv==1.9.1 110 | # via pre-commit 111 | openpyxl==3.1.5 112 | # via hdx-python-utilities 113 | packaging==25.0 114 | # via 115 | # mkdocs 116 | # pytest 117 | paginate==0.5.7 118 | # via mkdocs-material 119 | pathspec==0.12.1 120 | # via mkdocs 121 | petl==1.7.17 122 | # via frictionless 123 | platformdirs==4.4.0 124 | # via 125 | # mkdocs-get-deps 126 | # virtualenv 127 | pluggy==1.6.0 128 | # via 129 | # pytest 130 | # pytest-cov 131 | ply==3.11 132 | # via 133 | # jsonpath-ng 134 | # libhxl 135 | pre-commit==4.3.0 136 | # via hdx-python-country (pyproject.toml) 137 | pydantic==2.11.9 138 | # via frictionless 139 | pydantic-core==2.33.2 140 | # via pydantic 141 | pygments==2.19.2 142 | # via 143 | # mkdocs-material 144 | # pytest 145 | # rich 146 | pymdown-extensions==10.16.1 147 | # via mkdocs-material 148 | pyphonetics==0.5.3 149 | # via hdx-python-utilities 150 | pytest==8.4.2 151 | # via 152 | # hdx-python-country (pyproject.toml) 153 | # pytest-cov 154 | pytest-cov==7.0.0 155 | # via hdx-python-country (pyproject.toml) 156 | python-dateutil==2.9.0.post0 157 | # via 158 | # frictionless 159 | # ghp-import 160 | # hdx-python-utilities 161 | # libhxl 162 | python-io-wrapper==0.3.1 163 | # via libhxl 164 | python-slugify==8.0.4 165 | # via frictionless 166 | pyyaml==6.0.2 167 | # via 168 | # frictionless 169 | # mkdocs 170 | # mkdocs-get-deps 171 | # pre-commit 172 | # pymdown-extensions 173 | # pyyaml-env-tag 174 | # tableschema-to-template 175 | pyyaml-env-tag==1.1 176 | # via mkdocs 177 | ratelimit==2.2.1 178 | # via hdx-python-utilities 179 | referencing==0.36.2 180 | # via 181 | # jsonschema 182 | # jsonschema-specifications 183 | requests==2.32.5 184 | # via 185 | # frictionless 186 | # libhxl 187 | # mkdocs-material 188 | # requests-file 189 | requests-file==2.1.0 190 | # via hdx-python-utilities 191 | rfc3986==2.0.0 192 | # via frictionless 193 | rich==14.1.0 194 | # via typer 195 | rpds-py==0.27.1 196 | # via 197 | # jsonschema 198 | # referencing 199 | ruamel-yaml==0.18.15 200 | # via hdx-python-utilities 201 | ruamel-yaml-clib==0.2.12 202 | # via ruamel-yaml 203 | shellingham==1.5.4 204 | # via typer 205 | simpleeval==1.0.3 206 | # via frictionless 207 | six==1.17.0 208 | # via python-dateutil 209 | structlog==25.4.0 210 | # via libhxl 211 | tableschema-to-template==0.0.13 212 | # via hdx-python-utilities 213 | tabulate==0.9.0 214 | # via frictionless 215 | tenacity==9.1.2 216 | # via hdx-python-country (pyproject.toml) 217 | text-unidecode==1.3 218 | # via python-slugify 219 | typer==0.19.1 220 | # via frictionless 221 | typing-extensions==4.15.0 222 | # via 223 | # frictionless 224 | # pydantic 225 | # pydantic-core 226 | # typer 227 | # typing-inspection 228 | typing-inspection==0.4.1 229 | # via pydantic 230 | unidecode==1.4.0 231 | # via 232 | # libhxl 233 | # pyphonetics 234 | urllib3==2.5.0 235 | # via 236 | # libhxl 237 | # requests 238 | validators==0.35.0 239 | # via frictionless 240 | virtualenv==20.34.0 241 | # via pre-commit 242 | watchdog==6.0.0 243 | # via mkdocs 244 | wheel==0.45.1 245 | # via libhxl 246 | xlrd==2.0.2 247 | # via hdx-python-utilities 248 | xlrd3==1.1.0 249 | # via libhxl 250 | xlsx2csv==0.8.4 251 | # via hdx-python-utilities 252 | xlsxwriter==3.2.9 253 | # via tableschema-to-template 254 | xlwt==1.3.0 255 | # via hdx-python-utilities 256 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | exclude = ["_version.py"] 2 | 3 | [lint] 4 | # List of rules: https://docs.astral.sh/ruff/rules/ 5 | select = [ 6 | "E", # pycodestyle - default 7 | "F", # pyflakes - default 8 | "I" # isort 9 | ] 10 | ignore = [ 11 | "E501" # Line too long 12 | ] 13 | 14 | [lint.isort] 15 | known-local-folder = ["hdx"] 16 | -------------------------------------------------------------------------------- /src/hdx/location/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import version as __version__ # noqa: F401 2 | -------------------------------------------------------------------------------- /src/hdx/location/adminlevel.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from typing import Any, Dict, List, Optional, Tuple 4 | 5 | import hxl 6 | from hxl import InputOptions 7 | from hxl.input import HXLIOException 8 | 9 | from hdx.location.country import Country 10 | from hdx.utilities.base_downloader import DownloadError 11 | from hdx.utilities.dictandlist import dict_of_sets_add 12 | from hdx.utilities.matching import Phonetics, multiple_replace 13 | from hdx.utilities.retriever import Retrieve 14 | from hdx.utilities.text import normalise 15 | from hdx.utilities.typehint import ListTuple 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class AdminLevel: 21 | """AdminLevel class which takes in p-codes and then maps names to those 22 | p-codes with fuzzy matching if necessary. 23 | 24 | The dictionary admin_config, which defaults to an empty dictionary, can 25 | have the following optional keys: 26 | countries_fuzzy_try are countries (iso3 codes) for which to try fuzzy 27 | matching. Default is all countries. 28 | admin_name_mappings is a dictionary of mappings from name to p-code (for 29 | where fuzzy matching fails) 30 | admin_name_replacements is a dictionary of textual replacements to try when 31 | fuzzy matching 32 | admin_fuzzy_dont is a list of names for which fuzzy matching should not be 33 | tried 34 | 35 | The admin_level_overrides parameter allows manually overriding the returned 36 | admin level for given countries. It is a dictionary with iso3s as keys and 37 | admin level numbers as values. 38 | 39 | The retriever parameter accepts an object of type Retrieve (or inherited 40 | classes). It is used to allow either that admin data from urls is saved 41 | to files or to enable already saved files to be used instead of downloading 42 | from urls. 43 | 44 | Args: 45 | admin_config (Dict): Configuration dictionary. Defaults to {}. 46 | admin_level (int): Admin level. Defaults to 1. 47 | admin_level_overrides (Dict): Countries at other admin levels. 48 | retriever (Optional[Retrieve]): Retriever object to use for loading/saving files. Defaults to None. 49 | """ 50 | 51 | pcode_regex = re.compile(r"^([a-zA-Z]{2,3})(\d+)$") 52 | _admin_url_default = "https://data.humdata.org/dataset/cb963915-d7d1-4ffa-90dc-31277e24406f/resource/f65bc260-4d8b-416f-ac07-f2433b4d5142/download/global_pcodes_adm_1_2.csv" 53 | admin_url = _admin_url_default 54 | admin_all_pcodes_url = "https://data.humdata.org/dataset/cb963915-d7d1-4ffa-90dc-31277e24406f/resource/793e66fe-4cdb-4076-b037-fb8c053239e2/download/global_pcodes.csv" 55 | _formats_url_default = "https://data.humdata.org/dataset/cb963915-d7d1-4ffa-90dc-31277e24406f/resource/f1161807-dab4-4331-b7b0-4e5dac56e0e4/download/global_pcode_lengths.csv" 56 | formats_url = _formats_url_default 57 | 58 | def __init__( 59 | self, 60 | admin_config: Dict = {}, 61 | admin_level: int = 1, 62 | admin_level_overrides: Dict = {}, 63 | retriever: Optional[Retrieve] = None, 64 | ) -> None: 65 | self.admin_level = admin_level 66 | self.admin_level_overrides = admin_level_overrides 67 | self.retriever: Optional[Retrieve] = retriever 68 | self.countries_fuzzy_try = admin_config.get("countries_fuzzy_try") 69 | self.admin_name_mappings = admin_config.get("admin_name_mappings", {}) 70 | self.admin_name_replacements = admin_config.get("admin_name_replacements", {}) 71 | self.admin_fuzzy_dont = admin_config.get("admin_fuzzy_dont", list()) 72 | self.pcodes = [] 73 | self.pcode_lengths = {} 74 | self.name_to_pcode = {} 75 | self.name_parent_to_pcode = {} 76 | self.pcode_to_name = {} 77 | self.pcode_to_iso3 = {} 78 | self.pcode_to_parent = {} 79 | self.pcode_formats = {} 80 | self.use_parent = False 81 | self.zeroes = {} 82 | self.parent_admins = [] 83 | 84 | self.init_matches_errors() 85 | self.phonetics = Phonetics() 86 | 87 | @classmethod 88 | def looks_like_pcode(cls, string: str) -> bool: 89 | """Check if a string looks like a p-code using regex matching of format. 90 | Checks for 2 or 3 letter country iso code at start and then numbers. 91 | 92 | Args: 93 | string (str): String to check 94 | 95 | Returns: 96 | bool: Whether string looks like a p-code 97 | """ 98 | if cls.pcode_regex.match(string): 99 | return True 100 | return False 101 | 102 | @classmethod 103 | def set_default_admin_url(cls, admin_url: Optional[str] = None) -> None: 104 | """ 105 | Set default admin URL from which to retrieve admin data 106 | 107 | Args: 108 | admin_url (Optional[str]): Admin URL from which to retrieve admin data. Defaults to internal value. 109 | 110 | Returns: 111 | None 112 | """ 113 | if admin_url is None: 114 | admin_url = cls._admin_url_default 115 | cls.admin_url = admin_url 116 | 117 | @staticmethod 118 | def get_libhxl_dataset( 119 | url: str = admin_url, retriever: Optional[Retrieve] = None 120 | ) -> hxl.Dataset: 121 | """ 122 | Get libhxl Dataset object given a URL which defaults to global p-codes 123 | dataset on HDX. 124 | 125 | Args: 126 | url (str): URL from which to load data. Defaults to internal admin url. 127 | retriever (Optional[Retrieve]): Retriever object to use for loading file. Defaults to None. 128 | 129 | Returns: 130 | hxl.Dataset: HXL Dataset object 131 | """ 132 | if retriever: 133 | try: 134 | url_to_use = retriever.download_file(url) 135 | except DownloadError: 136 | logger.exception(f"Setup of libhxl Dataset object with {url} failed!") 137 | raise 138 | else: 139 | url_to_use = url 140 | try: 141 | return hxl.data( 142 | url_to_use, 143 | InputOptions(InputOptions(allow_local=True, encoding="utf-8")), 144 | ) 145 | except (FileNotFoundError, HXLIOException): 146 | logger.exception(f"Setup of libhxl Dataset object with {url} failed!") 147 | raise 148 | 149 | def setup_row( 150 | self, 151 | countryiso3: str, 152 | pcode: str, 153 | adm_name: Optional[str], 154 | parent: Optional[str], 155 | ): 156 | """ 157 | Setup a single p-code 158 | 159 | Args: 160 | countryiso3 (str): Country 161 | pcode (str): P-code 162 | adm_name (Optional[str]): Administrative name (which can be None) 163 | parent (Optional[str]): Parent p-code 164 | 165 | Returns: 166 | None 167 | """ 168 | self.pcode_lengths[countryiso3] = len(pcode) 169 | self.pcodes.append(pcode) 170 | if adm_name is None: 171 | adm_name = "" 172 | self.pcode_to_name[pcode] = adm_name 173 | self.pcode_to_iso3[pcode] = countryiso3 174 | if not adm_name: 175 | logger.error(f"Admin name is blank for pcode {pcode} of {countryiso3}!") 176 | return 177 | 178 | adm_name = normalise(adm_name) 179 | name_to_pcode = self.name_to_pcode.get(countryiso3, {}) 180 | name_to_pcode[adm_name] = pcode 181 | self.name_to_pcode[countryiso3] = name_to_pcode 182 | 183 | if self.use_parent: 184 | name_parent_to_pcode = self.name_parent_to_pcode.get(countryiso3, {}) 185 | name_to_pcode = name_parent_to_pcode.get(parent, {}) 186 | name_to_pcode[adm_name] = pcode 187 | name_parent_to_pcode[parent] = name_to_pcode 188 | self.name_parent_to_pcode[countryiso3] = name_parent_to_pcode 189 | self.pcode_to_parent[pcode] = parent 190 | 191 | def setup_from_admin_info( 192 | self, 193 | admin_info: ListTuple[Dict], 194 | countryiso3s: Optional[ListTuple[str]] = None, 195 | ) -> None: 196 | """ 197 | Setup p-codes from admin_info which is a list with values of the form 198 | below with parent optional: 199 | :: 200 | {"iso3": "AFG", "pcode": "AF0101", "name": "Kabul", parent: "AF01"} 201 | Args: 202 | admin_info (ListTuple[Dict]): p-code dictionary 203 | countryiso3s (Optional[ListTuple[str]]): Countries to read. Defaults to None (all). 204 | 205 | Returns: 206 | None 207 | """ 208 | if countryiso3s: 209 | countryiso3s = [countryiso3.upper() for countryiso3 in countryiso3s] 210 | self.use_parent = "parent" in admin_info[0] 211 | for row in admin_info: 212 | countryiso3 = row["iso3"].upper() 213 | if countryiso3s and countryiso3 not in countryiso3s: 214 | continue 215 | pcode = row.get("pcode").upper() 216 | adm_name = row["name"] 217 | parent = row.get("parent") 218 | self.setup_row(countryiso3, pcode, adm_name, parent) 219 | 220 | def setup_from_libhxl_dataset( 221 | self, 222 | libhxl_dataset: hxl.Dataset, 223 | countryiso3s: Optional[ListTuple[str]] = None, 224 | ) -> None: 225 | """ 226 | Setup p-codes from a libhxl Dataset object. 227 | 228 | Args: 229 | libhxl_dataset (hxl.Dataset): Dataset object from libhxl library 230 | countryiso3s (Optional[ListTuple[str]]): Countries to read. Defaults to None (all). 231 | 232 | Returns: 233 | None 234 | """ 235 | admin_info = libhxl_dataset.with_rows(f"#geo+admin_level={self.admin_level}") 236 | if countryiso3s: 237 | countryiso3s = [countryiso3.upper() for countryiso3 in countryiso3s] 238 | self.use_parent = "#adm+code+parent" in admin_info.display_tags 239 | for row in admin_info: 240 | countryiso3 = row.get("#country+code").upper() 241 | if countryiso3s and countryiso3 not in countryiso3s: 242 | continue 243 | pcode = row.get("#adm+code").upper() 244 | adm_name = row.get("#adm+name") 245 | parent = row.get("#adm+code+parent") 246 | self.setup_row(countryiso3, pcode, adm_name, parent) 247 | 248 | def setup_from_url( 249 | self, 250 | admin_url: str = admin_url, 251 | countryiso3s: Optional[ListTuple[str]] = None, 252 | ) -> None: 253 | """ 254 | Setup p-codes from a URL. Defaults to global p-codes dataset on HDX. 255 | 256 | Args: 257 | admin_url (str): URL from which to load data. Defaults to global p-codes dataset. 258 | countryiso3s (Optional[ListTuple[str]]): Countries to read. Defaults to None (all). 259 | 260 | Returns: 261 | None 262 | """ 263 | admin_info = self.get_libhxl_dataset(admin_url, self.retriever) 264 | self.setup_from_libhxl_dataset(admin_info, countryiso3s) 265 | 266 | def load_pcode_formats_from_libhxl_dataset( 267 | self, libhxl_dataset: hxl.Dataset 268 | ) -> None: 269 | """ 270 | Load p-code formats from a libhxl Dataset object. 271 | 272 | Args: 273 | libhxl_dataset (hxl.Dataset): Dataset object from libhxl library 274 | 275 | Returns: 276 | None 277 | """ 278 | for row in libhxl_dataset: 279 | pcode_format = [int(row.get("#country+len"))] 280 | for admin_no in range(1, 4): 281 | length = row.get(f"#adm{admin_no}+len") 282 | if not length or "|" in length: 283 | break 284 | pcode_format.append(int(length)) 285 | self.pcode_formats[row.get("#country+code")] = pcode_format 286 | 287 | for pcode in self.pcodes: 288 | countryiso3 = self.pcode_to_iso3[pcode] 289 | for x in re.finditer("0", pcode): 290 | dict_of_sets_add(self.zeroes, countryiso3, x.start()) 291 | 292 | def load_pcode_formats(self, formats_url: str = formats_url) -> None: 293 | """ 294 | Load p-code formats from a URL. Defaults to global p-codes dataset on HDX. 295 | 296 | Args: 297 | formats_url (str): URL from which to load data. Defaults to global p-codes dataset. 298 | 299 | Returns: 300 | None 301 | """ 302 | formats_info = self.get_libhxl_dataset(formats_url, self.retriever) 303 | self.load_pcode_formats_from_libhxl_dataset(formats_info) 304 | 305 | def set_parent_admins(self, parent_admins: List[List]) -> None: 306 | """ 307 | Set parent admins 308 | 309 | Args: 310 | parent_admins (List[List]): List of P-codes per parent admin 311 | 312 | Returns: 313 | None 314 | """ 315 | self.parent_admins = parent_admins 316 | 317 | def set_parent_admins_from_adminlevels( 318 | self, adminlevels: List["AdminLevel"] 319 | ) -> None: 320 | """ 321 | Set parent admins from AdminLevel objects 322 | 323 | Args: 324 | parent_admins (List[AdminLevel]): List of parent AdminLevel objects 325 | 326 | Returns: 327 | None 328 | """ 329 | self.parent_admins = [adminlevel.pcodes for adminlevel in adminlevels] 330 | 331 | def get_pcode_list(self) -> List[str]: 332 | """Get list of all pcodes 333 | 334 | Returns: 335 | List[str]: List of pcodes 336 | """ 337 | return self.pcodes 338 | 339 | def get_admin_level(self, countryiso3: str) -> int: 340 | """Get admin level for country 341 | 342 | Args: 343 | countryiso3 (str): ISO3 country code 344 | 345 | Returns: 346 | int: Admin level 347 | """ 348 | admin_level = self.admin_level_overrides.get(countryiso3) 349 | if admin_level: 350 | return admin_level 351 | return self.admin_level 352 | 353 | def get_pcode_length(self, countryiso3: str) -> Optional[int]: 354 | """Get pcode length for country 355 | 356 | Args: 357 | countryiso3 (str): ISO3 country code 358 | 359 | Returns: 360 | Optional[int]: Country's pcode length or None 361 | """ 362 | return self.pcode_lengths.get(countryiso3) 363 | 364 | def init_matches_errors(self) -> None: 365 | """Initialise storage of fuzzy matches, ignored and errors for logging purposes 366 | 367 | Returns: 368 | None 369 | """ 370 | 371 | self.matches = set() 372 | self.ignored = set() 373 | self.errors = set() 374 | 375 | def convert_admin_pcode_length( 376 | self, countryiso3: str, pcode: str, **kwargs: Any 377 | ) -> Optional[str]: 378 | """Standardise pcode length by country and match to an internal pcode. 379 | Requires that p-code formats be loaded (eg. using load_pcode_formats) 380 | 381 | Args: 382 | countryiso3 (str): ISO3 country code 383 | pcode (str): P code to match 384 | **kwargs: 385 | parent (Optional[str]): Parent admin code 386 | logname (str): Log using this identifying name. Defaults to not logging. 387 | 388 | Returns: 389 | Optional[str]: Matched P code or None if no match 390 | """ 391 | logname = kwargs.get("logname") 392 | match = self.pcode_regex.match(pcode) 393 | if not match: 394 | return None 395 | pcode_format = self.pcode_formats.get(countryiso3) 396 | if not pcode_format: 397 | if self.get_admin_level(countryiso3) == 1: 398 | return self.convert_admin1_pcode_length(countryiso3, pcode, logname) 399 | return None 400 | countryiso, digits = match.groups() 401 | countryiso_length = len(countryiso) 402 | if countryiso_length > pcode_format[0]: 403 | countryiso2 = Country.get_iso2_from_iso3(countryiso3) 404 | pcode_parts = [countryiso2, digits] 405 | elif countryiso_length < pcode_format[0]: 406 | pcode_parts = [countryiso3, digits] 407 | else: 408 | pcode_parts = [countryiso, digits] 409 | new_pcode = "".join(pcode_parts) 410 | if new_pcode in self.pcodes: 411 | if logname: 412 | self.matches.add( 413 | ( 414 | logname, 415 | countryiso3, 416 | new_pcode, 417 | self.pcode_to_name[new_pcode], 418 | "pcode length conversion-country", 419 | ) 420 | ) 421 | return new_pcode 422 | total_length = sum(pcode_format[: self.admin_level + 1]) 423 | admin_changes = [] 424 | for admin_no in range(1, self.admin_level + 1): 425 | len_new_pcode = len(new_pcode) 426 | if len_new_pcode == total_length: 427 | break 428 | admin_length = pcode_format[admin_no] 429 | pcode_part = pcode_parts[admin_no] 430 | part_length = len(pcode_part) 431 | if part_length == admin_length: 432 | break 433 | pos = sum(pcode_format[:admin_no]) 434 | if part_length < admin_length: 435 | if pos in self.zeroes[countryiso3]: 436 | pcode_parts[admin_no] = f"0{pcode_part}" 437 | admin_changes.append(str(admin_no)) 438 | new_pcode = "".join(pcode_parts) 439 | break 440 | elif part_length > admin_length and admin_no == self.admin_level: 441 | if pcode_part[0] == "0": 442 | pcode_parts[admin_no] = pcode_part[1:] 443 | admin_changes.append(str(admin_no)) 444 | new_pcode = "".join(pcode_parts) 445 | break 446 | if len_new_pcode < total_length: 447 | if admin_length > 2 and pos in self.zeroes[countryiso3]: 448 | pcode_part = f"0{pcode_part}" 449 | if self.parent_admins and admin_no < self.admin_level: 450 | parent_pcode = [pcode_parts[i] for i in range(admin_no)] 451 | parent_pcode.append(pcode_part[:admin_length]) 452 | parent_pcode = "".join(parent_pcode) 453 | if parent_pcode not in self.parent_admins[admin_no - 1]: 454 | pcode_part = pcode_part[1:] 455 | else: 456 | admin_changes.append(str(admin_no)) 457 | else: 458 | admin_changes.append(str(admin_no)) 459 | elif len_new_pcode > total_length: 460 | if admin_length <= 2 and pcode_part[0] == "0": 461 | pcode_part = pcode_part[1:] 462 | if self.parent_admins and admin_no < self.admin_level: 463 | parent_pcode = [pcode_parts[i] for i in range(admin_no)] 464 | parent_pcode.append(pcode_part[:admin_length]) 465 | parent_pcode = "".join(parent_pcode) 466 | if parent_pcode not in self.parent_admins[admin_no - 1]: 467 | pcode_part = f"0{pcode_part}" 468 | else: 469 | admin_changes.append(str(admin_no)) 470 | else: 471 | admin_changes.append(str(admin_no)) 472 | pcode_parts[admin_no] = pcode_part[:admin_length] 473 | pcode_parts.append(pcode_part[admin_length:]) 474 | new_pcode = "".join(pcode_parts) 475 | if new_pcode in self.pcodes: 476 | if logname: 477 | admin_changes_str = ",".join(admin_changes) 478 | self.matches.add( 479 | ( 480 | logname, 481 | countryiso3, 482 | new_pcode, 483 | self.pcode_to_name[new_pcode], 484 | f"pcode length conversion-admins {admin_changes_str}", 485 | ) 486 | ) 487 | return new_pcode 488 | return None 489 | 490 | def convert_admin1_pcode_length( 491 | self, countryiso3: str, pcode: str, logname: Optional[str] = None 492 | ) -> Optional[str]: 493 | """Standardise pcode length by country and match to an internal pcode. 494 | Only works for admin1 pcodes. 495 | 496 | Args: 497 | countryiso3 (str): ISO3 country code 498 | pcode (str): P code for admin one to match 499 | logname (Optional[str]): Identifying name to use when logging. Defaults to None (don't log). 500 | 501 | Returns: 502 | Optional[str]: Matched P code or None if no match 503 | """ 504 | pcode_length = len(pcode) 505 | country_pcodelength = self.pcode_lengths.get(countryiso3) 506 | if not country_pcodelength: 507 | return None 508 | if pcode_length == country_pcodelength or pcode_length < 4 or pcode_length > 6: 509 | return None 510 | if country_pcodelength == 4: 511 | pcode = f"{Country.get_iso2_from_iso3(pcode[:3])}{pcode[-2:]}" 512 | elif country_pcodelength == 5: 513 | if pcode_length == 4: 514 | pcode = f"{pcode[:2]}0{pcode[-2:]}" 515 | else: 516 | pcode = f"{Country.get_iso2_from_iso3(pcode[:3])}{pcode[-3:]}" 517 | elif country_pcodelength == 6: 518 | if pcode_length == 4: 519 | pcode = f"{Country.get_iso3_from_iso2(pcode[:2])}0{pcode[-2:]}" 520 | else: 521 | pcode = f"{Country.get_iso3_from_iso2(pcode[:2])}{pcode[-3:]}" 522 | else: 523 | pcode = None 524 | if pcode in self.pcodes: 525 | if logname: 526 | self.matches.add( 527 | ( 528 | logname, 529 | countryiso3, 530 | pcode, 531 | self.pcode_to_name[pcode], 532 | "pcode length conversion", 533 | ) 534 | ) 535 | return pcode 536 | return None 537 | 538 | def get_admin_name_replacements( 539 | self, countryiso3: str, parent: Optional[str] 540 | ) -> Dict[str, str]: 541 | """Get relevant admin name replacements from admin name replacements 542 | which is a dictionary of mappings from string to string replacement. 543 | These can be global or they can be restricted by 544 | country or parent (if the AdminLevel object has been set up with 545 | parents). Keys take the form "STRING_TO_REPLACE", 546 | "AFG|STRING_TO_REPLACE" or "AF01|STRING_TO_REPLACE". 547 | 548 | Args: 549 | countryiso3 (str): ISO3 country code 550 | parent (Optional[str]): Parent admin code 551 | 552 | Returns: 553 | Dict[str, str]: Relevant admin name replacements 554 | """ 555 | relevant_name_replacements = {} 556 | for key, value in self.admin_name_replacements.items(): 557 | if "|" not in key: 558 | if key not in relevant_name_replacements: 559 | relevant_name_replacements[key] = value 560 | continue 561 | prefix, name = key.split("|") 562 | if parent: 563 | if prefix == parent: 564 | if name not in relevant_name_replacements: 565 | relevant_name_replacements[name] = value 566 | continue 567 | if prefix == countryiso3: 568 | if name not in relevant_name_replacements: 569 | relevant_name_replacements[name] = value 570 | continue 571 | return relevant_name_replacements 572 | 573 | def get_admin_fuzzy_dont( 574 | self, countryiso3: str, parent: Optional[str] 575 | ) -> List[str]: 576 | """Get relevant admin names that should not be fuzzy matched from 577 | admin fuzzy dont which is a list of strings. These can be global 578 | or they can be restricted by country or parent. Keys take the form 579 | "DONT_MATCH", "AFG|DONT_MATCH", or "AF01|DONT_MATCH". 580 | 581 | Args: 582 | countryiso3 (str): ISO3 country code 583 | parent (Optional[str]): Parent admin code 584 | 585 | Returns: 586 | List[str]: Relevant admin names that should not be fuzzy matched 587 | """ 588 | relevant_admin_fuzzy_dont = [] 589 | for value in self.admin_fuzzy_dont: 590 | if "|" not in value: 591 | if value not in relevant_admin_fuzzy_dont: 592 | relevant_admin_fuzzy_dont.append(value) 593 | continue 594 | prefix, name = value.split("|") 595 | if parent: 596 | if prefix == parent: 597 | if name not in relevant_admin_fuzzy_dont: 598 | relevant_admin_fuzzy_dont.append(name) 599 | if prefix == countryiso3: 600 | if name not in relevant_admin_fuzzy_dont: 601 | relevant_admin_fuzzy_dont.append(name) 602 | continue 603 | return relevant_admin_fuzzy_dont 604 | 605 | def fuzzy_pcode( 606 | self, 607 | countryiso3: str, 608 | name: str, 609 | normalised_name: str, 610 | **kwargs: Any, 611 | ) -> Optional[str]: 612 | """Fuzzy match name to pcode 613 | 614 | Args: 615 | countryiso3 (str): ISO3 country code 616 | name (str): Name to match 617 | normalised_name (str): Normalised name 618 | **kwargs: 619 | parent (Optional[str]): Parent admin code 620 | logname (str): Log using this identifying name. Defaults to not logging. 621 | 622 | Returns: 623 | Optional[str]: Matched P code or None if no match 624 | """ 625 | logname = kwargs.get("logname") 626 | if ( 627 | self.countries_fuzzy_try is not None 628 | and countryiso3 not in self.countries_fuzzy_try 629 | ): 630 | if logname: 631 | self.ignored.add((logname, countryiso3)) 632 | return None 633 | if self.use_parent: 634 | parent = kwargs.get("parent") 635 | else: 636 | parent = None 637 | if parent is None: 638 | name_to_pcode = self.name_to_pcode.get(countryiso3) 639 | if not name_to_pcode: 640 | if logname: 641 | self.errors.add((logname, countryiso3)) 642 | return None 643 | else: 644 | name_parent_to_pcode = self.name_parent_to_pcode.get(countryiso3) 645 | if not name_parent_to_pcode: 646 | if logname: 647 | self.errors.add((logname, countryiso3)) 648 | return None 649 | name_to_pcode = name_parent_to_pcode.get(parent) 650 | if not name_to_pcode: 651 | if logname: 652 | self.errors.add((logname, countryiso3, parent)) 653 | return None 654 | alt_normalised_name = multiple_replace( 655 | normalised_name, 656 | self.get_admin_name_replacements(countryiso3, parent), 657 | ) 658 | pcode = name_to_pcode.get( 659 | normalised_name, name_to_pcode.get(alt_normalised_name) 660 | ) 661 | if not pcode and name.lower() in self.get_admin_fuzzy_dont(countryiso3, parent): 662 | if logname: 663 | self.ignored.add((logname, countryiso3, name)) 664 | return None 665 | if not pcode: 666 | for map_name in name_to_pcode: 667 | if normalised_name in map_name: 668 | pcode = name_to_pcode[map_name] 669 | if logname: 670 | self.matches.add( 671 | ( 672 | logname, 673 | countryiso3, 674 | name, 675 | self.pcode_to_name[pcode], 676 | "substring", 677 | ) 678 | ) 679 | break 680 | for map_name in name_to_pcode: 681 | if alt_normalised_name in map_name: 682 | pcode = name_to_pcode[map_name] 683 | if logname: 684 | self.matches.add( 685 | ( 686 | logname, 687 | countryiso3, 688 | name, 689 | self.pcode_to_name[pcode], 690 | "substring", 691 | ) 692 | ) 693 | break 694 | if not pcode: 695 | map_names = list(name_to_pcode.keys()) 696 | 697 | def al_transform_1(name): 698 | prefix = name[:3] 699 | if prefix == "al ": 700 | return f"ad {name[3:]}" 701 | elif prefix == "ad ": 702 | return f"al {name[3:]}" 703 | else: 704 | return None 705 | 706 | def al_transform_2(name): 707 | prefix = name[:3] 708 | if prefix == "al " or prefix == "ad ": 709 | return name[3:] 710 | else: 711 | return None 712 | 713 | matching_index = self.phonetics.match( 714 | map_names, 715 | normalised_name, 716 | alternative_name=alt_normalised_name, 717 | transform_possible_names=[al_transform_1, al_transform_2], 718 | ) 719 | 720 | if matching_index is None: 721 | if logname: 722 | self.errors.add((logname, countryiso3, name)) 723 | return None 724 | 725 | map_name = map_names[matching_index] 726 | pcode = name_to_pcode[map_name] 727 | if logname: 728 | self.matches.add( 729 | ( 730 | logname, 731 | countryiso3, 732 | name, 733 | self.pcode_to_name[pcode], 734 | "fuzzy", 735 | ) 736 | ) 737 | return pcode 738 | 739 | def get_name_mapped_pcode( 740 | self, countryiso3: str, name: str, parent: Optional[str] 741 | ) -> Optional[str]: 742 | """Get pcode from admin name mappings which is a dictionary of mappings 743 | from name to pcode. These can be global or they can be restricted by 744 | country or parent (if the AdminLevel object has been set up with 745 | parents). Keys take the form "MAPPING", "AFG|MAPPING" or 746 | "AF01|MAPPING". 747 | 748 | Args: 749 | countryiso3 (str): ISO3 country code 750 | name (str): Name to match 751 | parent (Optional[str]): Parent admin code 752 | 753 | Returns: 754 | Optional[str]: P code match from admin name mappings or None if no match 755 | """ 756 | if parent: 757 | pcode = self.admin_name_mappings.get(f"{parent}|{name}") 758 | if pcode is None: 759 | pcode = self.admin_name_mappings.get(f"{countryiso3}|{name}") 760 | else: 761 | pcode = self.admin_name_mappings.get(f"{countryiso3}|{name}") 762 | if pcode is None: 763 | pcode = self.admin_name_mappings.get(name) 764 | return pcode 765 | 766 | def get_pcode( 767 | self, 768 | countryiso3: str, 769 | name: str, 770 | fuzzy_match: bool = True, 771 | fuzzy_length: int = 4, 772 | **kwargs: Any, 773 | ) -> Tuple[Optional[str], bool]: 774 | """Get pcode for a given name 775 | 776 | Args: 777 | countryiso3 (str): ISO3 country code 778 | name (str): Name to match 779 | fuzzy_match (bool): Whether to try fuzzy matching. Defaults to True. 780 | fuzzy_length (int): Minimum length for fuzzy matching. Defaults to 4. 781 | **kwargs: 782 | parent (Optional[str]): Parent admin code 783 | logname (str): Log using this identifying name. Defaults to not logging. 784 | 785 | Returns: 786 | Tuple[Optional[str], bool]: (Matched P code or None if no match, True if exact match or False if not) 787 | """ 788 | if self.use_parent: 789 | parent = kwargs.get("parent") 790 | else: 791 | parent = None 792 | pcode = self.get_name_mapped_pcode(countryiso3, name, parent) 793 | if pcode and self.pcode_to_iso3[pcode] == countryiso3: 794 | if parent: 795 | if self.pcode_to_parent[pcode] == parent: 796 | return pcode, True 797 | else: 798 | return pcode, True 799 | if self.looks_like_pcode(name): 800 | pcode = name.upper() 801 | if pcode in self.pcodes: # name is a p-code 802 | return name, True 803 | # name looks like a p-code, but doesn't match p-codes 804 | # so try adjusting p-code length 805 | pcode = self.convert_admin_pcode_length(countryiso3, pcode, **kwargs) 806 | return pcode, True 807 | else: 808 | normalised_name = normalise(name) 809 | if parent: 810 | name_parent_to_pcode = self.name_parent_to_pcode.get(countryiso3) 811 | if name_parent_to_pcode: 812 | name_to_pcode = name_parent_to_pcode.get(parent) 813 | if name_to_pcode is not None: 814 | pcode = name_to_pcode.get(normalised_name) 815 | if pcode: 816 | return pcode, True 817 | else: 818 | name_to_pcode = self.name_to_pcode.get(countryiso3) 819 | if name_to_pcode is not None: 820 | pcode = name_to_pcode.get(normalised_name) 821 | if pcode: 822 | return pcode, True 823 | if not fuzzy_match or len(normalised_name) < fuzzy_length: 824 | return None, True 825 | pcode = self.fuzzy_pcode(countryiso3, name, normalised_name, **kwargs) 826 | return pcode, False 827 | 828 | def output_matches(self) -> List[str]: 829 | """Output log of matches 830 | 831 | Returns: 832 | List[str]: List of matches 833 | """ 834 | output = [] 835 | for match in sorted(self.matches): 836 | line = f"{match[0]} - {match[1]}: Matching ({match[4]}) {match[2]} to {match[3]} on map" 837 | logger.info(line) 838 | output.append(line) 839 | return output 840 | 841 | def output_ignored(self) -> List[str]: 842 | """Output log of ignored 843 | 844 | Returns: 845 | List[str]: List of ignored 846 | """ 847 | output = [] 848 | for ignored in sorted(self.ignored): 849 | if len(ignored) == 2: 850 | line = f"{ignored[0]} - Ignored {ignored[1]}!" 851 | else: 852 | line = f"{ignored[0]} - {ignored[1]}: Ignored {ignored[2]}!" 853 | logger.info(line) 854 | output.append(line) 855 | return output 856 | 857 | def output_errors(self) -> List[str]: 858 | """Output log of errors 859 | 860 | Returns: 861 | List[str]: List of errors 862 | """ 863 | output = [] 864 | for error in sorted(self.errors): 865 | if len(error) == 2: 866 | line = f"{error[0]} - Could not find {error[1]} in map names!" 867 | else: 868 | line = ( 869 | f"{error[0]} - {error[1]}: Could not find {error[2]} in map names!" 870 | ) 871 | logger.error(line) 872 | output.append(line) 873 | return output 874 | 875 | def output_admin_name_mappings(self) -> List[str]: 876 | """Output log of name mappings 877 | 878 | Returns: 879 | List[str]: List of mappings 880 | """ 881 | output = [] 882 | for name, pcode in self.admin_name_mappings.items(): 883 | line = f"{name}: {self.pcode_to_name[pcode]} ({pcode})" 884 | logger.info(line) 885 | output.append(line) 886 | return output 887 | 888 | def output_admin_name_replacements(self) -> List[str]: 889 | """Output log of name replacements 890 | 891 | Returns: 892 | List[str]: List of name replacements 893 | """ 894 | output = [] 895 | for name, replacement in self.admin_name_replacements.items(): 896 | line = f"{name}: {replacement}" 897 | logger.info(line) 898 | output.append(line) 899 | return output 900 | -------------------------------------------------------------------------------- /src/hdx/location/currency.py: -------------------------------------------------------------------------------- 1 | """Currency conversion""" 2 | 3 | import logging 4 | from copy import deepcopy 5 | from datetime import datetime, timezone 6 | from typing import Dict, Optional, Union 7 | 8 | from .int_timestamp import get_int_timestamp 9 | from hdx.utilities.dateparse import ( 10 | now_utc, 11 | parse_date, 12 | ) 13 | from hdx.utilities.dictandlist import dict_of_dicts_add 14 | from hdx.utilities.downloader import Download, DownloadError 15 | from hdx.utilities.path import get_temp_dir 16 | from hdx.utilities.retriever import Retrieve 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class CurrencyError(Exception): 22 | pass 23 | 24 | 25 | class Currency: 26 | """Currency class for performing currency conversion. Uses Yahoo, falling back on 27 | https://github.com/fawazahmed0/currency-api for current rates and Yahoo falling back on IMF for historic 28 | rates. Note that rate calls are cached. 29 | """ 30 | 31 | _primary_rates_url = "https://query2.finance.yahoo.com/v8/finance/chart/{currency}=X?period1={date}&period2={date}&interval=1d&events=div%2Csplit&formatted=false&lang=en-US®ion=US&corsDomain=finance.yahoo.com" 32 | _secondary_rates_url = "https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.min.json" 33 | _secondary_historic_url = ( 34 | "https://codeforiati.org/imf-exchangerates/imf_exchangerates.csv" 35 | ) 36 | _cached_current_rates = {} 37 | _cached_historic_rates = {} 38 | _rates_api = "" 39 | _secondary_rates = {} 40 | _secondary_historic_rates = {} 41 | _fallback_to_current = False 42 | _no_historic = False 43 | _user_agent = "hdx-python-country-rates" 44 | _retriever = None 45 | _log_level = logging.DEBUG 46 | _fixed_now = None 47 | _threshold = 1.3 48 | 49 | @classmethod 50 | def setup( 51 | cls, 52 | retriever: Optional[Retrieve] = None, 53 | primary_rates_url: str = _primary_rates_url, 54 | secondary_rates_url: str = _secondary_rates_url, 55 | secondary_historic_url: Optional[str] = _secondary_historic_url, 56 | secondary_historic_rates: Optional[Dict] = None, 57 | fallback_historic_to_current: bool = False, 58 | fallback_current_to_static: bool = False, 59 | no_historic: bool = False, 60 | fixed_now: Optional[datetime] = None, 61 | log_level: int = logging.DEBUG, 62 | current_rates_cache: Dict = {"USD": 1}, 63 | historic_rates_cache: Dict = {}, 64 | use_secondary_historic: bool = False, 65 | ) -> None: 66 | """ 67 | Setup the sources. If you wish to use a static fallback file by setting 68 | fallback_current_to_static to True, it needs to be named "secondary_rates.json" 69 | and put in the fallback_dir of the passed in Retriever. 70 | 71 | Args: 72 | retriever (Optional[Retrieve]): Retrieve object to use for downloading. Defaults to None (generate a new one). 73 | primary_rates_url (str): Primary rates url to use. Defaults to Yahoo API. 74 | secondary_rates_url (str): Current rates url to use. Defaults to currency-api. 75 | secondary_historic_url (Optional[str]): Historic rates url to use. Defaults to IMF (via IATI). 76 | secondary_historic_rates (Optional[Dict]): Historic rates to use. Defaults to None. 77 | fallback_historic_to_current (bool): If historic unavailable, fallback to current. Defaults to False. 78 | fallback_current_to_static (bool): Use static file as final fallback. Defaults to False. 79 | no_historic (bool): Do not set up historic rates. Defaults to False. 80 | fixed_now (Optional[datetime]): Use a fixed datetime for now. Defaults to None (use datetime.now()). 81 | log_level (int): Level at which to log messages. Defaults to logging.DEBUG. 82 | current_rates_cache (Dict): Pre-populate current rates cache with given values. Defaults to {"USD": 1}. 83 | historic_rates_cache (Dict): Pre-populate historic rates cache with given values. Defaults to {}. 84 | use_secondary_historic (bool): Use secondary historic first. Defaults to False. 85 | 86 | Returns: 87 | None 88 | """ 89 | 90 | cls._cached_current_rates = deepcopy(current_rates_cache) 91 | cls._cached_historic_rates = deepcopy(historic_rates_cache) 92 | cls._rates_api = primary_rates_url 93 | cls._secondary_rates = {} 94 | if secondary_historic_rates is not None: 95 | cls._secondary_historic_rates = secondary_historic_rates 96 | else: 97 | cls._secondary_historic_rates = {} 98 | if retriever is None: 99 | downloader = Download(user_agent=cls._user_agent) 100 | temp_dir = get_temp_dir(cls._user_agent) 101 | retriever = Retrieve( 102 | downloader, 103 | None, 104 | temp_dir, 105 | temp_dir, 106 | save=False, 107 | use_saved=False, 108 | ) 109 | cls._retriever = retriever 110 | try: 111 | secondary_rates = retriever.download_json( 112 | secondary_rates_url, 113 | "secondary_rates.json", 114 | "secondary current exchange rates", 115 | fallback_current_to_static, 116 | ) 117 | cls._secondary_rates = secondary_rates["usd"] 118 | except (DownloadError, OSError): 119 | logger.exception("Error getting secondary current rates!") 120 | cls._fixed_now = fixed_now 121 | cls._log_level = log_level 122 | if no_historic: 123 | return 124 | cls._no_historic = no_historic 125 | if secondary_historic_url: 126 | try: 127 | _, iterator = retriever.get_tabular_rows( 128 | secondary_historic_url, 129 | dict_form=True, 130 | filename="historic_rates.csv", 131 | logstr="secondary historic exchange rates", 132 | ) 133 | for row in iterator: 134 | currency = row["Currency"] 135 | date = get_int_timestamp(parse_date(row["Date"])) 136 | rate = float(row["Rate"]) 137 | dict_of_dicts_add( 138 | cls._secondary_historic_rates, currency, date, rate 139 | ) 140 | except (DownloadError, OSError): 141 | logger.exception("Error getting secondary historic rates!") 142 | cls._fallback_to_current = fallback_historic_to_current 143 | if use_secondary_historic: 144 | cls._get_historic_rate = cls._get_historic_rate_secondary 145 | else: 146 | cls._get_historic_rate = cls._get_historic_rate_primary 147 | 148 | @classmethod 149 | def _get_primary_rates_data( 150 | cls, currency: str, timestamp: int, downloader=None 151 | ) -> Optional[Dict]: 152 | """ 153 | Get the primary fx rate data for currency 154 | 155 | Args: 156 | currency (str): Currency 157 | timestamp (int): Timestamp to use for fx conversion 158 | 159 | Returns: 160 | Optional[float]: fx rate or None 161 | """ 162 | if not cls._rates_api: 163 | return None 164 | url = cls._rates_api.format(currency=currency, date=str(timestamp)) 165 | if downloader is None: 166 | downloader = cls._retriever 167 | try: 168 | chart = downloader.download_json(url, log_level=cls._log_level)["chart"] 169 | if chart["error"] is not None: 170 | return None 171 | return chart["result"][0] 172 | except (DownloadError, KeyError): 173 | return None 174 | 175 | @classmethod 176 | def _get_adjclose( 177 | cls, indicators: Dict, currency: str, timestamp: int 178 | ) -> Optional[float]: 179 | """ 180 | Get the adjusted close fx rate from the indicators dictionary returned 181 | from the Yahoo API. 182 | 183 | Args: 184 | indicators (Dict): Indicators dictionary from Yahoo API 185 | currency (str): Currency 186 | timestamp (int): Timestamp to use for fx conversion 187 | 188 | Returns: 189 | Optional[float]: Adjusted close fx rate or None 190 | """ 191 | adjclose = indicators["adjclose"][0].get("adjclose") 192 | if adjclose is None: 193 | return None 194 | 195 | def beyond_threshold(x, y): 196 | if max(x, y) / min(x, y) > cls._threshold: 197 | return True 198 | return False 199 | 200 | def within_threshold(x, y): 201 | if max(x, y) / min(x, y) > cls._threshold: 202 | return False 203 | return True 204 | 205 | # Compare adjclose to other variables returned by Yahoo API 206 | adjclose = adjclose[0] 207 | quote = indicators["quote"][0] 208 | open = quote.get("open") 209 | fraction_ok = True 210 | if open: 211 | open = open[0] 212 | if beyond_threshold(adjclose, open): 213 | fraction_ok = False 214 | high = quote.get("high") 215 | if high: 216 | high = high[0] 217 | if beyond_threshold(adjclose, high): 218 | fraction_ok = False 219 | low = quote.get("low") 220 | if low: 221 | low = low[0] 222 | if beyond_threshold(adjclose, low): 223 | fraction_ok = False 224 | if fraction_ok: 225 | # if no discrepancies, adjclose is ok 226 | return adjclose 227 | 228 | if cls._no_historic: 229 | secondary_fx_rate = None 230 | else: 231 | secondary_fx_rate = cls._get_secondary_historic_rate(currency, timestamp) 232 | if not secondary_fx_rate: 233 | # compare with high and low to reveal errors from Yahoo feed 234 | if high and low: 235 | if within_threshold(low, high): 236 | return low + (high - low) / 2 237 | return None 238 | 239 | # compare with secondary historic rate 240 | if within_threshold(adjclose, secondary_fx_rate): 241 | return adjclose 242 | # if adjclose is wacky, find another value to return that is ok 243 | if high and low: 244 | if within_threshold(high, secondary_fx_rate) and within_threshold( 245 | low, secondary_fx_rate 246 | ): 247 | return low + (high - low) / 2 248 | if open: 249 | if within_threshold(open, secondary_fx_rate): 250 | return open 251 | if high: 252 | if within_threshold(high, secondary_fx_rate): 253 | return high 254 | if low: 255 | if within_threshold(low, secondary_fx_rate): 256 | return low 257 | return secondary_fx_rate 258 | 259 | @classmethod 260 | def _get_primary_rate( 261 | cls, currency: str, timestamp: Optional[int] = None 262 | ) -> Optional[float]: 263 | """ 264 | Get the primary current fx rate for currency ofr a given timestamp. If no timestamp is supplied, 265 | datetime.now() will be used unless fixed_now was passed in the constructor. 266 | 267 | Args: 268 | currency (str): Currency 269 | timestamp (Optional[int]): Timestamp to use for fx conversion. Defaults to None (datetime.now()) 270 | 271 | Returns: 272 | Optional[float]: fx rate or None 273 | """ 274 | if timestamp is None: 275 | if cls._fixed_now: 276 | now = cls._fixed_now 277 | get_close = True 278 | else: 279 | now = now_utc() 280 | get_close = False 281 | timestamp = get_int_timestamp(now) 282 | else: 283 | get_close = True 284 | data = cls._get_primary_rates_data(currency, timestamp) 285 | if not data: 286 | return None 287 | if get_close: 288 | return cls._get_adjclose(data["indicators"], currency, timestamp) 289 | return data["meta"]["regularMarketPrice"] 290 | 291 | @classmethod 292 | def _get_secondary_current_rate(cls, currency: str) -> Optional[float]: 293 | """ 294 | Get the secondary current fx rate for currency 295 | 296 | Args: 297 | currency (str): Currency 298 | 299 | Returns: 300 | Optional[float]: fx rate or None 301 | """ 302 | return cls._secondary_rates.get(currency.lower()) 303 | 304 | @classmethod 305 | def get_current_rate(cls, currency: str) -> float: 306 | """ 307 | Get the current fx rate for currency 308 | 309 | Args: 310 | currency (str): Currency 311 | 312 | Returns: 313 | float: fx rate 314 | """ 315 | currency = currency.upper() 316 | fx_rate = cls._cached_current_rates.get(currency) 317 | if fx_rate is not None: 318 | return fx_rate 319 | fx_rate = cls._get_primary_rate(currency) 320 | if fx_rate is not None: 321 | cls._cached_current_rates[currency] = fx_rate 322 | return fx_rate 323 | fx_rate = cls._get_secondary_current_rate(currency) 324 | if fx_rate is not None: 325 | logger.debug(f"Using secondary current rate for {currency}!") 326 | cls._cached_current_rates[currency] = fx_rate 327 | return fx_rate 328 | raise CurrencyError(f"Failed to get rate for currency {currency}!") 329 | 330 | @classmethod 331 | def get_current_value_in_usd(cls, value: Union[int, float], currency: str) -> float: 332 | """ 333 | Get the current USD value of the value in local currency 334 | 335 | Args: 336 | value (Union[int, float]): Value in local currency 337 | currency (str): Currency 338 | 339 | Returns: 340 | float: Value in USD 341 | """ 342 | currency = currency.upper() 343 | if currency == "USD": 344 | return value 345 | fx_rate = cls.get_current_rate(currency) 346 | return value / fx_rate 347 | 348 | @classmethod 349 | def get_current_value_in_currency( 350 | cls, usdvalue: Union[int, float], currency: str 351 | ) -> float: 352 | """ 353 | Get the current value in local currency of the value in USD 354 | 355 | Args: 356 | usdvalue (Union[int, float]): Value in USD 357 | currency (str): Currency 358 | 359 | Returns: 360 | float: Value in local currency 361 | """ 362 | currency = currency.upper() 363 | if currency == "USD": 364 | return usdvalue 365 | fx_rate = cls.get_current_rate(currency) 366 | return usdvalue * fx_rate 367 | 368 | @classmethod 369 | def _get_secondary_historic_rate( 370 | cls, currency: str, timestamp: int 371 | ) -> Optional[float]: 372 | """ 373 | Get the historic fx rate for currency on a particular date using 374 | interpolation if needed. 375 | 376 | Args: 377 | currency (str): Currency 378 | timestamp (int): Timestamp to use for fx conversion 379 | 380 | Returns: 381 | Optional[float]: fx rate or None 382 | """ 383 | currency_data = cls._secondary_historic_rates.get(currency) 384 | if currency_data is None: 385 | return None 386 | fx_rate = currency_data.get(timestamp) 387 | if fx_rate: 388 | return fx_rate 389 | timestamp1 = None 390 | timestamp2 = None 391 | for ts in currency_data.keys(): 392 | if timestamp > ts: 393 | timestamp1 = ts 394 | else: 395 | timestamp2 = ts 396 | break 397 | if timestamp1 is None: 398 | if timestamp2 is None: 399 | return None 400 | return currency_data[timestamp2] 401 | if timestamp2 is None: 402 | return currency_data[timestamp1] 403 | rate1 = currency_data[timestamp1] 404 | return rate1 + (timestamp - timestamp1) * ( 405 | (currency_data[timestamp2] - rate1) / (timestamp2 - timestamp1) 406 | ) 407 | 408 | @classmethod 409 | def _get_historic_rate_primary(cls, currency: str, timestamp: int) -> float: 410 | currency_data = cls._cached_historic_rates.get(currency) 411 | if currency_data is not None: 412 | fx_rate = currency_data.get(timestamp) 413 | if fx_rate is not None: 414 | return fx_rate 415 | fx_rate = cls._get_primary_rate(currency, timestamp) 416 | if fx_rate is not None: 417 | dict_of_dicts_add(cls._cached_historic_rates, currency, timestamp, fx_rate) 418 | return fx_rate 419 | fx_rate = cls._get_secondary_historic_rate(currency, timestamp) 420 | if fx_rate is not None: 421 | dict_of_dicts_add(cls._cached_historic_rates, currency, timestamp, fx_rate) 422 | return fx_rate 423 | if cls._fallback_to_current: 424 | fx_rate = cls.get_current_rate(currency) 425 | if fx_rate: 426 | logger.debug( 427 | f"Falling back to current rate for currency {currency} on timestamp {timestamp}!" 428 | ) 429 | return fx_rate 430 | raise CurrencyError( 431 | f"Failed to get rate for currency {currency} on timestamp {timestamp}!" 432 | ) 433 | 434 | @classmethod 435 | def _get_historic_rate_secondary(cls, currency: str, timestamp: int) -> float: 436 | currency_data = cls._cached_historic_rates.get(currency) 437 | if currency_data is not None: 438 | fx_rate = currency_data.get(timestamp) 439 | if fx_rate is not None: 440 | return fx_rate 441 | fx_rate = cls._get_secondary_historic_rate(currency, timestamp) 442 | if fx_rate is None: 443 | fx_rate = cls._get_primary_rate(currency, timestamp) 444 | if fx_rate is None: 445 | if cls._fallback_to_current: 446 | fx_rate = cls.get_current_rate(currency) 447 | if fx_rate: 448 | logger.debug( 449 | f"Falling back to current rate for currency {currency} on timestamp {timestamp}!" 450 | ) 451 | return fx_rate 452 | raise CurrencyError( 453 | f"Failed to get rate for currency {currency} on timestamp {timestamp}!" 454 | ) 455 | else: 456 | dict_of_dicts_add( 457 | cls._cached_historic_rates, currency, timestamp, fx_rate 458 | ) 459 | return fx_rate 460 | else: 461 | dict_of_dicts_add(cls._cached_historic_rates, currency, timestamp, fx_rate) 462 | return fx_rate 463 | 464 | @classmethod 465 | def get_historic_rate( 466 | cls, currency: str, date: datetime, ignore_timeinfo: bool = True 467 | ) -> float: 468 | """ 469 | Get the fx rate for currency on a particular date. Any time and time zone 470 | information will be ignored by default (meaning that the time is set to 00:00:00 471 | and the time zone set to UTC). To have the time and time zone accounted for, 472 | set ignore_timeinfo to False. This may affect which day's closing value is used. 473 | 474 | Args: 475 | currency (str): Currency 476 | date (datetime): Date to use for fx conversion 477 | ignore_timeinfo (bool): Ignore time and time zone of date. Defaults to True. 478 | 479 | Returns: 480 | float: fx rate 481 | """ 482 | currency = currency.upper() 483 | if currency == "USD": 484 | return 1 485 | if ignore_timeinfo: 486 | date = date.replace( 487 | hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc 488 | ) 489 | timestamp = get_int_timestamp(date) 490 | return cls._get_historic_rate(currency, timestamp) 491 | 492 | @classmethod 493 | def get_historic_value_in_usd( 494 | cls, 495 | value: Union[int, float], 496 | currency: str, 497 | date: datetime, 498 | ignore_timeinfo: bool = True, 499 | ) -> float: 500 | """ 501 | Get the USD value of the value in local currency on a particular date. Any time 502 | and time zone information will be ignored by default (meaning that the time is 503 | set to 00:00:00 and the time zone set to UTC). To have the time and time zone 504 | accounted for, set ignore_timeinfo to False. This may affect which day's closing 505 | value is used. 506 | 507 | Args: 508 | value (Union[int, float]): Value in local currency 509 | currency (str): Currency 510 | date (datetime): Date to use for fx conversion 511 | ignore_timeinfo (bool): Ignore time and time zone of date. Defaults to True. 512 | 513 | Returns: 514 | float: Value in USD 515 | """ 516 | currency = currency.upper() 517 | if currency == "USD": 518 | return value 519 | fx_rate = cls.get_historic_rate(currency, date, ignore_timeinfo=ignore_timeinfo) 520 | return value / fx_rate 521 | 522 | @classmethod 523 | def get_historic_value_in_currency( 524 | cls, 525 | usdvalue: Union[int, float], 526 | currency: str, 527 | date: datetime, 528 | ignore_timeinfo: bool = True, 529 | ) -> float: 530 | """ 531 | Get the current value in local currency of the value in USD on a particular 532 | date. Any time and time zone information will be ignored by default (meaning 533 | that the time is set to 00:00:00 and the time zone set to UTC). To have the time 534 | and time zone accounted for, set ignore_timeinfo to False. This may affect which 535 | day's closing value is used. 536 | 537 | Args: 538 | value (Union[int, float]): Value in USD 539 | currency (str): Currency 540 | date (datetime): Date to use for fx conversion 541 | ignore_timeinfo (bool): Ignore time and time zone of date. Defaults to True. 542 | 543 | Returns: 544 | float: Value in local currency 545 | """ 546 | currency = currency.upper() 547 | if currency == "USD": 548 | return usdvalue 549 | fx_rate = cls.get_historic_rate(currency, date, ignore_timeinfo=ignore_timeinfo) 550 | return usdvalue * fx_rate 551 | -------------------------------------------------------------------------------- /src/hdx/location/int_timestamp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from hdx.utilities.dateparse import get_timestamp_from_datetime 4 | 5 | _cache_timestamp_lookup = {} 6 | 7 | 8 | def get_int_timestamp(date: datetime) -> int: 9 | """ 10 | Get integer timestamp from datetime object with caching 11 | 12 | Args: 13 | date (datetime): datetime object 14 | 15 | Returns: 16 | int: Integer timestamp 17 | """ 18 | timestamp = _cache_timestamp_lookup.get(date) 19 | if timestamp is None: 20 | timestamp = int(round(get_timestamp_from_datetime(date))) 21 | _cache_timestamp_lookup[date] = timestamp 22 | return timestamp 23 | -------------------------------------------------------------------------------- /src/hdx/location/wfp_api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Dict, List, Optional 3 | 4 | from tenacity import ( 5 | Retrying, 6 | after_log, 7 | retry_if_exception_type, 8 | stop_after_attempt, 9 | wait_fixed, 10 | ) 11 | 12 | from hdx.utilities.base_downloader import DownloadError 13 | from hdx.utilities.downloader import Download 14 | from hdx.utilities.retriever import Retrieve 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class WFPAPI: 20 | """Light wrapper around WFP REST API. It needs a token_downloader that has 21 | been configured with WFP basic authentication credentials and a retriever 22 | that will configured by this class with the bearer token obtained from the 23 | token_downloader. 24 | 25 | Args: 26 | token_downloader (Download): Download object with WFP basic authentication 27 | retriever (Retrieve): Retrieve object for interacting with WFP API 28 | """ 29 | 30 | token_url = "https://api.wfp.org/token" 31 | base_url = "https://api.wfp.org/vam-data-bridges/6.0.0/" 32 | scope = "gefs_geoless-items-countries_get vamdatabridges_commodities-list_get vamdatabridges_commodityunits-list_get vamdatabridges_marketprices-alps_get vamdatabridges_commodities-categories-list_get vamdatabridges_commodityunits-conversion-list_get vamdatabridges_marketprices-priceweekly_get vamdatabridges_markets-geojsonlist_get vamdatabridges_marketprices-pricemonthly_get vamdatabridges_markets-list_get vamdatabridges_currency-list_get vamdatabridges_currency-usdindirectquotation_get" 33 | default_retry_params = { 34 | "retry": retry_if_exception_type(DownloadError), 35 | "after": after_log(logger, logging.INFO), 36 | } 37 | 38 | def __init__( 39 | self, 40 | token_downloader: Download, 41 | retriever: Retrieve, 42 | ): 43 | self.token_downloader = token_downloader 44 | self.retriever = retriever 45 | self.retry_params = {"attempts": 1, "wait": 1} 46 | 47 | def get_retry_params(self) -> Dict: 48 | return self.retry_params 49 | 50 | def update_retry_params(self, attempts: int, wait: int) -> Dict: 51 | self.retry_params["attempts"] = attempts 52 | self.retry_params["wait"] = wait 53 | return self.retry_params 54 | 55 | def refresh_token(self) -> None: 56 | self.token_downloader.download( 57 | self.token_url, 58 | post=True, 59 | parameters={ 60 | "grant_type": "client_credentials", 61 | "scope": self.scope, 62 | }, 63 | ) 64 | bearer_token = self.token_downloader.get_json()["access_token"] 65 | self.retriever.downloader.set_bearer_token(bearer_token) 66 | 67 | def retrieve( 68 | self, 69 | url: str, 70 | filename: str, 71 | log: str, 72 | parameters: Optional[Dict] = None, 73 | ) -> Any: 74 | """Retrieve JSON from WFP API. 75 | 76 | Args: 77 | url (str): URL to download 78 | filename (Optional[str]): Filename of saved file. Defaults to getting from url. 79 | log (Optional[str]): Text to use in log string to describe download. Defaults to filename. 80 | parameters (Dict): Parameters to pass to download_json call 81 | 82 | Returns: 83 | Any: The data from the JSON file 84 | """ 85 | retryer = Retrying( 86 | retry=self.default_retry_params["retry"], 87 | after=self.default_retry_params["after"], 88 | stop=stop_after_attempt(self.retry_params["attempts"]), 89 | wait=wait_fixed(self.retry_params["wait"]), 90 | ) 91 | for attempt in retryer: 92 | with attempt: 93 | try: 94 | results = self.retriever.download_json( 95 | url, filename, log, False, parameters=parameters 96 | ) 97 | except DownloadError: 98 | response = self.retriever.downloader.response 99 | if response and response.status_code not in ( 100 | 104, 101 | 401, 102 | 403, 103 | ): 104 | raise 105 | self.refresh_token() 106 | results = self.retriever.download_json( 107 | url, filename, log, False, parameters=parameters 108 | ) 109 | return results 110 | 111 | def get_items( 112 | self, 113 | endpoint: str, 114 | countryiso3: Optional[str] = None, 115 | parameters: Optional[Dict] = None, 116 | ) -> List: 117 | """Retrieve a list of items from the WFP API. 118 | 119 | Args: 120 | endpoint (str): End point to call 121 | countryiso3 (Optional[str]): Country for which to obtain data. Defaults to all countries. 122 | parameters (Optional[Dict]): Paramaters to pass to call. Defaults to None. 123 | 124 | Returns: 125 | List: List of items from the WFP endpoint 126 | """ 127 | if not parameters: 128 | parameters = {} 129 | all_data = [] 130 | url = f"{self.base_url}{endpoint}" 131 | url_parts = url.split("/") 132 | base_filename = f"{url_parts[-2]}_{url_parts[-1]}" 133 | if countryiso3 == "PSE": # hack as PSE is treated by WFP as 2 areas 134 | countryiso3s = ["PSW", "PSG"] 135 | else: 136 | countryiso3s = [countryiso3] 137 | for countryiso3 in countryiso3s: 138 | page = 1 139 | data = None 140 | while data is None or len(data) > 0: 141 | page_parameters = {"page": page} 142 | page_parameters.update(parameters) 143 | if countryiso3 is None: 144 | filename = f"{base_filename}_{page}.json" 145 | log = f"{base_filename} page {page}" 146 | else: 147 | filename = f"{base_filename}_{countryiso3}_{page}.json" 148 | log = f"{base_filename} for {countryiso3} page {page}" 149 | page_parameters["CountryCode"] = countryiso3 150 | try: 151 | json = self.retrieve(url, filename, log, page_parameters) 152 | except FileNotFoundError: 153 | json = {"items": []} 154 | data = json["items"] 155 | all_data.extend(data) 156 | page = page + 1 157 | return all_data 158 | -------------------------------------------------------------------------------- /src/hdx/location/wfp_exchangerates.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, List 3 | 4 | from .int_timestamp import get_int_timestamp 5 | from .wfp_api import WFPAPI 6 | from hdx.utilities.dateparse import parse_date 7 | from hdx.utilities.typehint import ListTuple 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class WFPExchangeRates: 13 | """Obtain WFP official exchange rates. It needs a WFP API object. 14 | 15 | Args: 16 | wfp_api (WFPAPI): WFPAPI object 17 | """ 18 | 19 | def __init__(self, wfp_api: WFPAPI): 20 | self.wfp_api = wfp_api 21 | 22 | def get_currencies_info(self) -> List[Dict]: 23 | """Get list of currency codes and names from WFP API 24 | 25 | Returns: 26 | List[Dict]: List of currency codes and names in WFP API 27 | """ 28 | currencies = [] 29 | for currency in self.wfp_api.get_items("Currency/List"): 30 | currency_name = currency["extendedName"] 31 | if currency_name: 32 | currency_name = currency_name.strip() 33 | currencies.append({"code": currency["name"], "name": currency_name}) 34 | return currencies 35 | 36 | def get_currencies(self) -> List[str]: 37 | """Get list of currency codes in WFP API 38 | 39 | Returns: 40 | List[str]: List of currency codes in WFP API 41 | """ 42 | currencies = [] 43 | for currency in self.wfp_api.get_items("Currency/List"): 44 | currencies.append(currency["name"]) 45 | return currencies 46 | 47 | def get_currency_historic_rates(self, currency: str) -> Dict[int, float]: 48 | """Get historic rates for currency from WFP API 49 | 50 | Args: 51 | currency (str): Currency 52 | 53 | Returns: 54 | Dict[int, float]: Mapping from timestamp to rate 55 | """ 56 | quotes = self.wfp_api.get_items( 57 | "Currency/UsdIndirectQuotation", 58 | parameters={"currencyName": currency}, 59 | ) 60 | historic_rates = {} 61 | for quote in reversed(quotes): 62 | if not quote["isOfficial"]: 63 | continue 64 | date = parse_date(quote["date"]) 65 | timestamp = get_int_timestamp(date) 66 | historic_rates[timestamp] = quote["value"] 67 | return historic_rates 68 | 69 | def get_historic_rates(self, currencies: ListTuple[str]) -> Dict[str, Dict]: 70 | """Get historic rates for a list of currencies from WFP API 71 | 72 | Args: 73 | currencies (List[str]): List of currencies 74 | 75 | Returns: 76 | Dict[str, Dict]: Mapping from currency to mapping from timestamp to rate 77 | """ 78 | historic_rates = {} 79 | for currency in currencies: 80 | logger.info(f"Getting WFP historic rates for {currency}") 81 | currency_historic_rates = self.get_currency_historic_rates(currency) 82 | historic_rates[currency.upper()] = currency_historic_rates 83 | return historic_rates 84 | -------------------------------------------------------------------------------- /tests/fixtures/adminlevel.yaml: -------------------------------------------------------------------------------- 1 | admin_info: 2 | - {pcode: AF01, name: Kabul, iso3: AFG} 3 | - {pcode: AF02, name: Kapisa, iso3: AFG} 4 | - {pcode: AF03, name: Parwan, iso3: AFG} 5 | - {pcode: AF04, name: Maidan Wardak, iso3: AFG} 6 | - {pcode: AF05, name: Logar, iso3: AFG} 7 | - {pcode: AF06, name: Nangarhar, iso3: AFG} 8 | - {pcode: AF07, name: Laghman, iso3: AFG} 9 | - {pcode: AF08, name: Panjsher, iso3: AFG} 10 | - {pcode: AF09, name: Baghlan, iso3: AFG} 11 | - {pcode: AF10, name: Bamyan, iso3: AFG} 12 | - {pcode: AF11, name: Ghazni, iso3: AFG} 13 | - {pcode: AF12, name: Paktika, iso3: AFG} 14 | - {pcode: AF13, name: Paktya, iso3: AFG} 15 | - {pcode: AF14, name: Khost, iso3: AFG} 16 | - {pcode: AF15, name: Kunar, iso3: AFG} 17 | - {pcode: AF16, name: Nuristan, iso3: AFG} 18 | - {pcode: AF17, name: Badakhshan, iso3: AFG} 19 | - {pcode: AF18, name: Takhar, iso3: AFG} 20 | - {pcode: AF19, name: Kunduz, iso3: AFG} 21 | - {pcode: AF20, name: Samangan, iso3: AFG} 22 | - {pcode: AF21, name: Balkh, iso3: AFG} 23 | - {pcode: AF22, name: Sar E Pul, iso3: AFG} 24 | - {pcode: AF23, name: Ghor, iso3: AFG} 25 | - {pcode: AF24, name: Daykundi, iso3: AFG} 26 | - {pcode: AF25, name: Uruzgan, iso3: AFG} 27 | - {pcode: AF26, name: Zabul, iso3: AFG} 28 | - {pcode: AF27, name: Kandahar, iso3: AFG} 29 | - {pcode: AF28, name: Jawzjan, iso3: AFG} 30 | - {pcode: AF29, name: Faryab, iso3: AFG} 31 | - {pcode: AF30, name: Hilmand, iso3: AFG} 32 | - {pcode: AF31, name: Badghis, iso3: AFG} 33 | - {pcode: AF32, name: Hirat, iso3: AFG} 34 | - {pcode: AF33, name: Farah, iso3: AFG} 35 | - {pcode: AF34, name: Nimroz, iso3: AFG} 36 | - {pcode: BDI001, name: Bubanza, iso3: BDI} 37 | - {pcode: BDI002, name: Bujumbura Rural, iso3: BDI} 38 | - {pcode: BDI003, name: Bururi, iso3: BDI} 39 | - {pcode: BDI004, name: Cankuzo, iso3: BDI} 40 | - {pcode: BDI005, name: Cibitoke, iso3: BDI} 41 | - {pcode: BDI006, name: Gitega, iso3: BDI} 42 | - {pcode: BDI007, name: Karuzi, iso3: BDI} 43 | - {pcode: BDI008, name: Kayanza, iso3: BDI} 44 | - {pcode: BDI009, name: Kirundo, iso3: BDI} 45 | - {pcode: BDI010, name: Makamba, iso3: BDI} 46 | - {pcode: BDI011, name: Muramvya, iso3: BDI} 47 | - {pcode: BDI012, name: Muyinga, iso3: BDI} 48 | - {pcode: BDI013, name: Mwaro, iso3: BDI} 49 | - {pcode: BDI014, name: Ngozi, iso3: BDI} 50 | - {pcode: BDI015, name: Rutana, iso3: BDI} 51 | - {pcode: BDI016, name: Ruyigi, iso3: BDI} 52 | - {pcode: BDI017, name: Bujumbura Mairie, iso3: BDI} 53 | - {pcode: BDI018, name: Rumonge, iso3: BDI} 54 | - {pcode: BF13, name: Centre, iso3: BFA} 55 | - {pcode: BF46, name: Boucle Du Mouhoun, iso3: BFA} 56 | - {pcode: BF47, name: Cascades, iso3: BFA} 57 | - {pcode: BF48, name: Centre Est, iso3: BFA} 58 | - {pcode: BF49, name: Centre Nord, iso3: BFA} 59 | - {pcode: BF50, name: Centre Ouest, iso3: BFA} 60 | - {pcode: BF51, name: Centre Sud, iso3: BFA} 61 | - {pcode: BF52, name: Est, iso3: BFA} 62 | - {pcode: BF53, name: Hauts Bassins, iso3: BFA} 63 | - {pcode: BF54, name: Nord, iso3: BFA} 64 | - {pcode: BF55, name: Plateau Central, iso3: BFA} 65 | - {pcode: BF56, name: Sahel, iso3: BFA} 66 | - {pcode: BF57, name: Sud Ouest, iso3: BFA} 67 | - {pcode: CF11, name: Ombella Mpoko, iso3: CAF} 68 | - {pcode: CF12, name: Lobaye, iso3: CAF} 69 | - {pcode: CF21, name: Mambere Kadei, iso3: CAF} 70 | - {pcode: CF22, name: Nana Mambere, iso3: CAF} 71 | - {pcode: CF23, name: Sangha Mbaere, iso3: CAF} 72 | - {pcode: CF31, name: Ouham Pende, iso3: CAF} 73 | - {pcode: CF32, name: Ouham, iso3: CAF} 74 | - {pcode: CF41, name: Kemo, iso3: CAF} 75 | - {pcode: CF42, name: Nana Gribizi, iso3: CAF} 76 | - {pcode: CF43, name: Ouaka, iso3: CAF} 77 | - {pcode: CF51, name: Bamingui Bangoran, iso3: CAF} 78 | - {pcode: CF52, name: Haute Kotto, iso3: CAF} 79 | - {pcode: CF53, name: Vakaga, iso3: CAF} 80 | - {pcode: CF61, name: Basse Kotto, iso3: CAF} 81 | - {pcode: CF62, name: Mbomou, iso3: CAF} 82 | - {pcode: CF63, name: Haut Mbomou, iso3: CAF} 83 | - {pcode: CF71, name: Bangui, iso3: CAF} 84 | - {pcode: CM001, name: Adamawa, iso3: CMR} 85 | - {pcode: CM002, name: Centre, iso3: CMR} 86 | - {pcode: CM003, name: East, iso3: CMR} 87 | - {pcode: CM004, name: Far North, iso3: CMR} 88 | - {pcode: CM005, name: Littoral, iso3: CMR} 89 | - {pcode: CM006, name: North, iso3: CMR} 90 | - {pcode: CM007, name: North West, iso3: CMR} 91 | - {pcode: CM008, name: West, iso3: CMR} 92 | - {pcode: CM009, name: South, iso3: CMR} 93 | - {pcode: CM010, name: South West, iso3: CMR} 94 | - {pcode: CD10, name: Kinshasa, iso3: COD} 95 | - {pcode: CD20, name: Kongo Central, iso3: COD} 96 | - {pcode: CD31, name: Kwango, iso3: COD} 97 | - {pcode: CD32, name: Kwilu, iso3: COD} 98 | - {pcode: CD33, name: Mai Ndombe, iso3: COD} 99 | - {pcode: CD41, name: Equateur, iso3: COD} 100 | - {pcode: CD42, name: Sud Ubangi, iso3: COD} 101 | - {pcode: CD43, name: Nord Ubangi, iso3: COD} 102 | - {pcode: CD44, name: Mongala, iso3: COD} 103 | - {pcode: CD45, name: Tshuapa, iso3: COD} 104 | - {pcode: CD51, name: Tshopo, iso3: COD} 105 | - {pcode: CD52, name: Bas Uele, iso3: COD} 106 | - {pcode: CD53, name: Haut Uele, iso3: COD} 107 | - {pcode: CD54, name: Ituri, iso3: COD} 108 | - {pcode: CD61, name: Nord Kivu, iso3: COD} 109 | - {pcode: CD62, name: Sud Kivu, iso3: COD} 110 | - {pcode: CD63, name: Maniema, iso3: COD} 111 | - {pcode: CD71, name: Haut Katanga, iso3: COD} 112 | - {pcode: CD72, name: Lualaba, iso3: COD} 113 | - {pcode: CD73, name: Haut Lomami, iso3: COD} 114 | - {pcode: CD74, name: Tanganyika, iso3: COD} 115 | - {pcode: CD81, name: Lomami, iso3: COD} 116 | - {pcode: CD82, name: Kasai Oriental, iso3: COD} 117 | - {pcode: CD83, name: Sankuru, iso3: COD} 118 | - {pcode: CD91, name: Kasai Central, iso3: COD} 119 | - {pcode: CD92, name: Kasai, iso3: COD} 120 | - {pcode: CO05, name: Antioquia, iso3: COL} 121 | - {pcode: CO08, name: Atlantico, iso3: COL} 122 | - {pcode: CO11, name: 'Bogota, D.C.', iso3: COL} 123 | - {pcode: CO13, name: Bolivar, iso3: COL} 124 | - {pcode: CO15, name: Boyaca, iso3: COL} 125 | - {pcode: CO17, name: Caldas, iso3: COL} 126 | - {pcode: CO18, name: Caqueta, iso3: COL} 127 | - {pcode: CO19, name: Cauca, iso3: COL} 128 | - {pcode: CO20, name: Cesar, iso3: COL} 129 | - {pcode: CO23, name: Cordoba, iso3: COL} 130 | - {pcode: CO25, name: Cundinamarca, iso3: COL} 131 | - {pcode: CO27, name: Choco, iso3: COL} 132 | - {pcode: CO41, name: Huila, iso3: COL} 133 | - {pcode: CO44, name: La Guajira, iso3: COL} 134 | - {pcode: CO47, name: Magdalena, iso3: COL} 135 | - {pcode: CO50, name: Meta, iso3: COL} 136 | - {pcode: CO52, name: Narino, iso3: COL} 137 | - {pcode: CO54, name: Norte de Santander, iso3: COL} 138 | - {pcode: CO63, name: Quindio, iso3: COL} 139 | - {pcode: CO66, name: Risaralda, iso3: COL} 140 | - {pcode: CO68, name: Santander, iso3: COL} 141 | - {pcode: CO70, name: Sucre, iso3: COL} 142 | - {pcode: CO73, name: Tolima, iso3: COL} 143 | - {pcode: CO76, name: Valle del Cauca, iso3: COL} 144 | - {pcode: CO81, name: Arauca, iso3: COL} 145 | - {pcode: CO85, name: Casanare, iso3: COL} 146 | - {pcode: CO86, name: Putumayo, iso3: COL} 147 | - {pcode: CO88, name: "Archipielago de San Andres, Providencia y Santa Catalina", iso3: COL} 148 | - {pcode: CO91, name: Amazonas, iso3: COL} 149 | - {pcode: CO94, name: Guainia, iso3: COL} 150 | - {pcode: CO95, name: Guaviare, iso3: COL} 151 | - {pcode: CO97, name: Vaupes, iso3: COL} 152 | - {pcode: CO99, name: Vichada, iso3: COL} 153 | - {pcode: ET01, name: Tigray, iso3: ETH} 154 | - {pcode: ET02, name: Afar, iso3: ETH} 155 | - {pcode: ET03, name: Amhara, iso3: ETH} 156 | - {pcode: ET04, name: Oromia, iso3: ETH} 157 | - {pcode: ET05, name: Somali, iso3: ETH} 158 | - {pcode: ET06, name: Benishangul Gumz, iso3: ETH} 159 | - {pcode: ET07, name: Snnp, iso3: ETH} 160 | - {pcode: ET12, name: Gambela, iso3: ETH} 161 | - {pcode: ET13, name: Harari, iso3: ETH} 162 | - {pcode: ET14, name: Addis Ababa, iso3: ETH} 163 | - {pcode: ET15, name: Dire Dawa, iso3: ETH} 164 | - {pcode: HT01, name: West, iso3: HTI} 165 | - {pcode: HT02, name: South East, iso3: HTI} 166 | - {pcode: HT03, name: North, iso3: HTI} 167 | - {pcode: HT04, name: North East, iso3: HTI} 168 | - {pcode: HT05, name: Artibonite, iso3: HTI} 169 | - {pcode: HT06, name: Centre, iso3: HTI} 170 | - {pcode: HT07, name: South, iso3: HTI} 171 | - {pcode: HT08, name: Grande'Anse, iso3: HTI} 172 | - {pcode: HT09, name: North West, iso3: HTI} 173 | - {pcode: HT10, name: Nippes, iso3: HTI} 174 | - {pcode: IQG01, name: Al Anbar, iso3: IRQ} 175 | - {pcode: IQG02, name: Al Basrah, iso3: IRQ} 176 | - {pcode: IQG03, name: Al Muthanna, iso3: IRQ} 177 | - {pcode: IQG04, name: Al Najaf, iso3: IRQ} 178 | - {pcode: IQG05, name: Al Qadissiya, iso3: IRQ} 179 | - {pcode: IQG06, name: Al Sulaymaniyah, iso3: IRQ} 180 | - {pcode: IQG07, name: Babil, iso3: IRQ} 181 | - {pcode: IQG08, name: Baghdad, iso3: IRQ} 182 | - {pcode: IQG09, name: Duhok, iso3: IRQ} 183 | - {pcode: IQG10, name: Diyala, iso3: IRQ} 184 | - {pcode: IQG11, name: Erbil, iso3: IRQ} 185 | - {pcode: IQG12, name: Kerbala, iso3: IRQ} 186 | - {pcode: IQG13, name: Kirkuk, iso3: IRQ} 187 | - {pcode: IQG14, name: Maysan, iso3: IRQ} 188 | - {pcode: IQG15, name: Ninewa, iso3: IRQ} 189 | - {pcode: IQG16, name: Salah Al Din, iso3: IRQ} 190 | - {pcode: IQG17, name: Thi Qar, iso3: IRQ} 191 | - {pcode: IQG18, name: Wassit, iso3: IRQ} 192 | - {pcode: LY01, name: East, iso3: LBY} 193 | - {pcode: LY02, name: West, iso3: LBY} 194 | - {pcode: LY03, name: South, iso3: LBY} 195 | - {pcode: ML01, name: Kayes, iso3: MLI} 196 | - {pcode: ML02, name: Koulikoro, iso3: MLI} 197 | - {pcode: ML03, name: Sikasso, iso3: MLI} 198 | - {pcode: ML04, name: Segou, iso3: MLI} 199 | - {pcode: ML05, name: Mopti, iso3: MLI} 200 | - {pcode: ML06, name: Tombouctou, iso3: MLI} 201 | - {pcode: ML07, name: Gao, iso3: MLI} 202 | - {pcode: ML08, name: Kidal, iso3: MLI} 203 | - {pcode: ML09, name: Bamako, iso3: MLI} 204 | - {pcode: MMR001, name: Kachin, iso3: MMR} 205 | - {pcode: MMR002, name: Kayah, iso3: MMR} 206 | - {pcode: MMR003, name: Kayin, iso3: MMR} 207 | - {pcode: MMR004, name: Chin, iso3: MMR} 208 | - {pcode: MMR005, name: Sagaing, iso3: MMR} 209 | - {pcode: MMR006, name: Tanintharyi, iso3: MMR} 210 | - {pcode: MMR009, name: Magway, iso3: MMR} 211 | - {pcode: MMR010, name: Mandalay, iso3: MMR} 212 | - {pcode: MMR011, name: Mon, iso3: MMR} 213 | - {pcode: MMR012, name: Rakhine, iso3: MMR} 214 | - {pcode: MMR013, name: Yangon, iso3: MMR} 215 | - {pcode: MMR017, name: Ayeyarwady, iso3: MMR} 216 | - {pcode: MMR018, name: Nay Pyi Taw, iso3: MMR} 217 | - {pcode: MMR111, name: Bago, iso3: MMR} 218 | - {pcode: MMR222, name: Shan, iso3: MMR} 219 | - {pcode: NER001, name: Agadez, iso3: NER} 220 | - {pcode: NER002, name: Diffa, iso3: NER} 221 | - {pcode: NER003, name: Dosso, iso3: NER} 222 | - {pcode: NER004, name: Maradi, iso3: NER} 223 | - {pcode: NER005, name: Tahoua, iso3: NER} 224 | - {pcode: NER006, name: Tillaberi, iso3: NER} 225 | - {pcode: NER007, name: Zinder, iso3: NER} 226 | - {pcode: NER008, name: Niamey, iso3: NER} 227 | - {pcode: NG001, name: Abia, iso3: NGA} 228 | - {pcode: NG002, name: Adamawa, iso3: NGA} 229 | - {pcode: NG003, name: Akwa Ibom, iso3: NGA} 230 | - {pcode: NG004, name: Anambra, iso3: NGA} 231 | - {pcode: NG005, name: Bauchi, iso3: NGA} 232 | - {pcode: NG006, name: Bayelsa, iso3: NGA} 233 | - {pcode: NG007, name: Benue, iso3: NGA} 234 | - {pcode: NG008, name: Borno, iso3: NGA} 235 | - {pcode: NG009, name: Cross River, iso3: NGA} 236 | - {pcode: NG010, name: Delta, iso3: NGA} 237 | - {pcode: NG011, name: Ebonyi, iso3: NGA} 238 | - {pcode: NG012, name: Edo, iso3: NGA} 239 | - {pcode: NG013, name: Ekiti, iso3: NGA} 240 | - {pcode: NG014, name: Enugu, iso3: NGA} 241 | - {pcode: NG015, name: Federal Capital Territory, iso3: NGA} 242 | - {pcode: NG016, name: Gombe, iso3: NGA} 243 | - {pcode: NG017, name: Imo, iso3: NGA} 244 | - {pcode: NG018, name: Jigawa, iso3: NGA} 245 | - {pcode: NG019, name: Kaduna, iso3: NGA} 246 | - {pcode: NG020, name: Kano, iso3: NGA} 247 | - {pcode: NG021, name: Katsina, iso3: NGA} 248 | - {pcode: NG022, name: Kebbi, iso3: NGA} 249 | - {pcode: NG023, name: Kogi, iso3: NGA} 250 | - {pcode: NG024, name: Kwara, iso3: NGA} 251 | - {pcode: NG025, name: Lagos, iso3: NGA} 252 | - {pcode: NG026, name: Nasarawa, iso3: NGA} 253 | - {pcode: NG027, name: Niger, iso3: NGA} 254 | - {pcode: NG028, name: Ogun, iso3: NGA} 255 | - {pcode: NG029, name: Ondo, iso3: NGA} 256 | - {pcode: NG030, name: Osun, iso3: NGA} 257 | - {pcode: NG031, name: Oyo, iso3: NGA} 258 | - {pcode: NG032, name: Plateau, iso3: NGA} 259 | - {pcode: NG033, name: Rivers, iso3: NGA} 260 | - {pcode: NG034, name: Sokoto, iso3: NGA} 261 | - {pcode: NG035, name: Taraba, iso3: NGA} 262 | - {pcode: NG036, name: Yobe, iso3: NGA} 263 | - {pcode: NG037, name: Zamfara, iso3: NGA} 264 | - {pcode: PS1, name: Gaza Strip, iso3: PSE} 265 | - {pcode: PS2, name: No Mans Land, iso3: PSE} 266 | - {pcode: PS3, name: West Bank, iso3: PSE} 267 | - {pcode: SD01, name: Khartoum, iso3: SDN} 268 | - {pcode: SD02, name: North Darfur, iso3: SDN} 269 | - {pcode: SD03, name: South Darfur, iso3: SDN} 270 | - {pcode: SD04, name: West Darfur, iso3: SDN} 271 | - {pcode: SD05, name: East Darfur, iso3: SDN} 272 | - {pcode: SD06, name: Central Darfur, iso3: SDN} 273 | - {pcode: SD07, name: South Kordofan, iso3: SDN} 274 | - {pcode: SD08, name: Blue Nile, iso3: SDN} 275 | - {pcode: SD09, name: White Nile, iso3: SDN} 276 | - {pcode: SD10, name: Red Sea, iso3: SDN} 277 | - {pcode: SD11, name: Kassala, iso3: SDN} 278 | - {pcode: SD12, name: Gedaref, iso3: SDN} 279 | - {pcode: SD13, name: North Kordofan, iso3: SDN} 280 | - {pcode: SD14, name: Sennar, iso3: SDN} 281 | - {pcode: SD15, name: Aj Jazirah, iso3: SDN} 282 | - {pcode: SD16, name: River Nile, iso3: SDN} 283 | - {pcode: SD17, name: Northern, iso3: SDN} 284 | - {pcode: SD18, name: West Kordofan, iso3: SDN} 285 | - {pcode: SD19, name: Abyei Pca, iso3: SDN} 286 | - {pcode: SO11, name: Awdal, iso3: SOM} 287 | - {pcode: SO12, name: Woqooyi Galbeed, iso3: SOM} 288 | - {pcode: SO13, name: Togdheer, iso3: SOM} 289 | - {pcode: SO14, name: Sool, iso3: SOM} 290 | - {pcode: SO15, name: Sanaag, iso3: SOM} 291 | - {pcode: SO16, name: Bari, iso3: SOM} 292 | - {pcode: SO17, name: Nugaal, iso3: SOM} 293 | - {pcode: SO18, name: Mudug, iso3: SOM} 294 | - {pcode: SO19, name: Galgaduud, iso3: SOM} 295 | - {pcode: SO20, name: Hiraan, iso3: SOM} 296 | - {pcode: SO21, name: Middle Shabelle, iso3: SOM} 297 | - {pcode: SO22, name: Banadir, iso3: SOM} 298 | - {pcode: SO23, name: Lower Shabelle, iso3: SOM} 299 | - {pcode: SO24, name: Bay, iso3: SOM} 300 | - {pcode: SO25, name: Bakool, iso3: SOM} 301 | - {pcode: SO26, name: Gedo, iso3: SOM} 302 | - {pcode: SO27, name: Middle Juba, iso3: SOM} 303 | - {pcode: SO28, name: Lower Juba, iso3: SOM} 304 | - {pcode: SS01, name: Central Equatoria, iso3: SSD} 305 | - {pcode: SS02, name: Eastern Equatoria, iso3: SSD} 306 | - {pcode: SS03, name: Jonglei, iso3: SSD} 307 | - {pcode: SS04, name: Lakes, iso3: SSD} 308 | - {pcode: SS05, name: Northern Bahr El Ghazal, iso3: SSD} 309 | - {pcode: SS06, name: Unity, iso3: SSD} 310 | - {pcode: SS07, name: Upper Nile, iso3: SSD} 311 | - {pcode: SS08, name: Warrap, iso3: SSD} 312 | - {pcode: SS09, name: Western Bahr El Ghazal, iso3: SSD} 313 | - {pcode: SS10, name: Western Equatoria, iso3: SSD} 314 | - {pcode: SY01, name: Damascus, iso3: SYR} 315 | - {pcode: SY02, name: Aleppo, iso3: SYR} 316 | - {pcode: SY03, name: Rural Damascus, iso3: SYR} 317 | - {pcode: SY04, name: Homs, iso3: SYR} 318 | - {pcode: SY05, name: Hama, iso3: SYR} 319 | - {pcode: SY06, name: Lattakia, iso3: SYR} 320 | - {pcode: SY07, name: Idleb, iso3: SYR} 321 | - {pcode: SY08, name: Al Hasakeh, iso3: SYR} 322 | - {pcode: SY09, name: Deir Ez Zor, iso3: SYR} 323 | - {pcode: SY10, name: Tartous, iso3: SYR} 324 | - {pcode: SY11, name: Ar Raqqa, iso3: SYR} 325 | - {pcode: SY12, name: Dara, iso3: SYR} 326 | - {pcode: SY13, name: As Sweida, iso3: SYR} 327 | - {pcode: SY14, name: Quneitra, iso3: SYR} 328 | - {pcode: TD01, name: Batha, iso3: TCD} 329 | - {pcode: TD02, name: Borkou, iso3: TCD} 330 | - {pcode: TD03, name: Chari Baguirmi, iso3: TCD} 331 | - {pcode: TD04, name: Guera, iso3: TCD} 332 | - {pcode: TD05, name: Hadjer Lamis, iso3: TCD} 333 | - {pcode: TD06, name: Kanem, iso3: TCD} 334 | - {pcode: TD07, name: Lac, iso3: TCD} 335 | - {pcode: TD08, name: Logone Occidental, iso3: TCD} 336 | - {pcode: TD09, name: Logone Oriental, iso3: TCD} 337 | - {pcode: TD10, name: Mandoul, iso3: TCD} 338 | - {pcode: TD11, name: Mayo Kebbi Est, iso3: TCD} 339 | - {pcode: TD12, name: Mayo Kebbi Ouest, iso3: TCD} 340 | - {pcode: TD13, name: Moyen Chari, iso3: TCD} 341 | - {pcode: TD14, name: Ouaddai, iso3: TCD} 342 | - {pcode: TD15, name: Salamat, iso3: TCD} 343 | - {pcode: TD16, name: Tandjile, iso3: TCD} 344 | - {pcode: TD17, name: Wadi Fira, iso3: TCD} 345 | - {pcode: TD18, name: Ndjamena, iso3: TCD} 346 | - {pcode: TD19, name: Barh El Gazel, iso3: TCD} 347 | - {pcode: TD20, name: Ennedi Est, iso3: TCD} 348 | - {pcode: TD21, name: Sila, iso3: TCD} 349 | - {pcode: TD22, name: Tibesti, iso3: TCD} 350 | - {pcode: TD23, name: Ennedi Ouest, iso3: TCD} 351 | - {pcode: UA01, name: Avtonomna Respublika Krym, iso3: UKR} 352 | - {pcode: UA05, name: Vinnytska, iso3: UKR} 353 | - {pcode: UA07, name: Volynska, iso3: UKR} 354 | - {pcode: UA12, name: Dnipropetrovska, iso3: UKR} 355 | - {pcode: UA14, name: Donetska, iso3: UKR} 356 | - {pcode: UA18, name: Zhytomyrska, iso3: UKR} 357 | - {pcode: UA21, name: Zakarpatska, iso3: UKR} 358 | - {pcode: UA23, name: Zaporizka, iso3: UKR} 359 | - {pcode: UA26, name: Ivano Frankivska, iso3: UKR} 360 | - {pcode: UA32, name: Kyivska, iso3: UKR} 361 | - {pcode: UA35, name: Kirovohradska, iso3: UKR} 362 | - {pcode: UA44, name: Luhanska, iso3: UKR} 363 | - {pcode: UA46, name: Lvivska, iso3: UKR} 364 | - {pcode: UA48, name: Mykolaivska, iso3: UKR} 365 | - {pcode: UA51, name: Odeska, iso3: UKR} 366 | - {pcode: UA53, name: Poltavska, iso3: UKR} 367 | - {pcode: UA56, name: Rivnenska, iso3: UKR} 368 | - {pcode: UA59, name: Sumska, iso3: UKR} 369 | - {pcode: UA61, name: Ternopilska, iso3: UKR} 370 | - {pcode: UA63, name: Kharkivska, iso3: UKR} 371 | - {pcode: UA65, name: Khersonska, iso3: UKR} 372 | - {pcode: UA68, name: Khmelnytska, iso3: UKR} 373 | - {pcode: UA71, name: Cherkaska, iso3: UKR} 374 | - {pcode: UA73, name: Chernivetska, iso3: UKR} 375 | - {pcode: UA74, name: Chernihivska, iso3: UKR} 376 | - {pcode: UA80, name: Kyivska, iso3: UKR} 377 | - {pcode: UA85, name: Sevastopol, iso3: UKR} 378 | - {pcode: VE01, name: Distrito Federal, iso3: VEN} 379 | - {pcode: VE02, name: Amazonas, iso3: VEN} 380 | - {pcode: VE03, name: Anzoategui, iso3: VEN} 381 | - {pcode: VE04, name: Apure, iso3: VEN} 382 | - {pcode: VE05, name: Aragua, iso3: VEN} 383 | - {pcode: VE06, name: Barinas, iso3: VEN} 384 | - {pcode: VE07, name: Bolivar, iso3: VEN} 385 | - {pcode: VE08, name: Carabobo, iso3: VEN} 386 | - {pcode: VE09, name: Cojedes, iso3: VEN} 387 | - {pcode: VE10, name: Delta Amacuro, iso3: VEN} 388 | - {pcode: VE11, name: Falcon, iso3: VEN} 389 | - {pcode: VE12, name: Guarico, iso3: VEN} 390 | - {pcode: VE13, name: Lara, iso3: VEN} 391 | - {pcode: VE14, name: Merida, iso3: VEN} 392 | - {pcode: VE15, name: Miranda, iso3: VEN} 393 | - {pcode: VE16, name: Monagas, iso3: VEN} 394 | - {pcode: VE17, name: Nueva Esparta, iso3: VEN} 395 | - {pcode: VE18, name: Portuguesa, iso3: VEN} 396 | - {pcode: VE19, name: Sucre, iso3: VEN} 397 | - {pcode: VE20, name: Tachira, iso3: VEN} 398 | - {pcode: VE21, name: Trujillo, iso3: VEN} 399 | - {pcode: VE22, name: Yaracuy, iso3: VEN} 400 | - {pcode: VE23, name: Zulia, iso3: VEN} 401 | - {pcode: VE24, name: Vargas, iso3: VEN} 402 | - {pcode: YE11, name: Ibb, iso3: YEM} 403 | - {pcode: YE12, name: Abyan, iso3: YEM} 404 | - {pcode: YE13, name: Sanaa City, iso3: YEM} 405 | - {pcode: YE14, name: Al Bayda, iso3: YEM} 406 | - {pcode: YE15, name: Taiz, iso3: YEM} 407 | - {pcode: YE16, name: Al Jawf, iso3: YEM} 408 | - {pcode: YE17, name: Hajjah, iso3: YEM} 409 | - {pcode: YE18, name: Al Hodeidah, iso3: YEM} 410 | - {pcode: YE19, name: Hadramawt, iso3: YEM} 411 | - {pcode: YE20, name: Dhamar, iso3: YEM} 412 | - {pcode: YE21, name: Shabwah, iso3: YEM} 413 | - {pcode: YE22, name: Sadah, iso3: YEM} 414 | - {pcode: YE23, name: Sanaa, iso3: YEM} 415 | - {pcode: YE24, name: Aden, iso3: YEM} 416 | - {pcode: YE25, name: Lahj, iso3: YEM} 417 | - {pcode: YE26, name: Marib, iso3: YEM} 418 | - {pcode: YE27, name: Al Mahwit, iso3: YEM} 419 | - {pcode: YE28, name: Al Maharah, iso3: YEM} 420 | - {pcode: YE29, name: Amran, iso3: YEM} 421 | - {pcode: YE30, name: Ad Dali, iso3: YEM} 422 | - {pcode: YE31, name: Raymah, iso3: YEM} 423 | - {pcode: YE32, name: Socotra, iso3: YEM} 424 | - {pcode: ZW10, name: Bulawayo, iso3: ZWE} 425 | - {pcode: ZW11, name: Manicaland, iso3: ZWE} 426 | - {pcode: ZW12, name: Mashonaland Central, iso3: ZWE} 427 | - {pcode: ZW13, name: Mashonaland East, iso3: ZWE} 428 | - {pcode: ZW14, name: Mashonaland West, iso3: ZWE} 429 | - {pcode: ZW15, name: Matabeleland North, iso3: ZWE} 430 | - {pcode: ZW16, name: Matabeleland South, iso3: ZWE} 431 | - {pcode: ZW17, name: Midlands, iso3: ZWE} 432 | - {pcode: ZW18, name: Masvingo, iso3: ZWE} 433 | - {pcode: ZW19, name: Harare, iso3: ZWE} 434 | - {pcode: XYZ123456, name: Random, iso3: XYZ} 435 | 436 | countries_fuzzy_try: 437 | - NER 438 | - NGA 439 | - UKR 440 | - YEM 441 | 442 | admin_name_mappings: 443 | "Nord-Ouest": "HT09" 444 | "nord-ouest": "HT09" 445 | "nord-quest": "HT09" 446 | "north-west": "HT09" 447 | "Abuja": "NG015" 448 | "FCT": "NG015" 449 | "FCT (Abuja)": "NG015" 450 | "FCT Abuja": "NG015" 451 | "Abuja FCT": "NG015" 452 | "C.EST": "BF48" 453 | "C.NORD": "BF49" 454 | "C.OUEST": "BF50" 455 | "C.SUD": "BF51" 456 | "centre-ouest": "BF50" 457 | "Centre-Ouest": "BF50" 458 | "central /south": "BF51" 459 | "central/south": "BF51" 460 | "S.OUEST": "BF57" 461 | "Bas congo": "CD20" 462 | "Bas-congo": "CD20" 463 | "bas-congo": "CD20" 464 | "MUTHANA": "IQG03" 465 | "Gezira": "SD15" 466 | "Gazira": "SD15" 467 | "Hasaka": "SY08" 468 | "Hassake": "SY08" 469 | "al-raka": "SY11" 470 | "al-amana": "YE23" 471 | "PL.Sool": "SO14" 472 | "Sc.Mudug": "SO18" 473 | "Amanat Al Asimah / أمانة العاصمة": "YE13" 474 | "Juba Dhexe": "SO27" 475 | "Juba Hoose": "SO28" 476 | "Shabelle Dhexe": "SO21" 477 | "Shabelle Hoose": "SO23" 478 | "ben-gumz": "ET06" 479 | "Rural-Dam": "SY03" 480 | "al-qunitara": "SY14" 481 | "harare /chitungwiza": "ZW19" 482 | "B MOUHOUN": "BF46" 483 | "H BASSINS": "BF53" 484 | "P CENTRAL": "BF55" 485 | "EXTREME NORD": "CM004" 486 | "Extreme Nord": "CM004" 487 | "Extr�me-Nord": "CM004" 488 | "extreme nord": "CM004" 489 | "extr�me-nord": "CM004" 490 | "SAN_ANDRES_ISLAS": "CO88" 491 | "FCT, Abuja": "NG015" 492 | "PL.Mudug": "SO18" 493 | "PL.Sanag": "SO15" 494 | "DAR SILA": "TD21" 495 | "Sana'a Govt.": "YE23" 496 | "DISTRITO CAPITAL": "VE01" 497 | "Chernigovskaja Oblast": "UA74" 498 | "Chernіvеtskaja Oblast": "UA73" 499 | "Crimea": "UA01" 500 | "Luganskaja Oblast": "UA44" 501 | "Rovenskaja Oblast": "UA56" 502 | "Sevastopol City": "UA85" 503 | "Zapоrіzskaja Oblast": "UA23" 504 | "CU Niamey": "NER008" 505 | 506 | admin_name_replacements: 507 | " urban": "" 508 | "sud": "south" 509 | "ouest": "west" 510 | "est": "east" 511 | "nord": "north" 512 | "'": "" 513 | "/": " " 514 | ".": " " 515 | " region": "" 516 | " oblast": "" 517 | 518 | admin_fuzzy_dont: 519 | - "YEM|nord" 520 | - "north" 521 | - "sud" 522 | - "south" 523 | - "est" 524 | - "east" 525 | - "ouest" 526 | - "west" 527 | - "comoe" 528 | - "mouhoun" 529 | - "nahouri" 530 | - "houet" 531 | - "nayala" 532 | - "seno" 533 | - "soum" 534 | - "sourou" 535 | - "yaound�" 536 | - "katanga" 537 | - "ta'amem" 538 | - "sabha" 539 | - "bago(east)" 540 | - "bago(west)" 541 | - "bago (east)" 542 | - "bago (west)" 543 | - "dakoro" 544 | - "mayahi" 545 | - "tessaoua" 546 | - "south south" 547 | - "BET" 548 | - "b. e. t." 549 | - "b.e.t." 550 | - "mayo-kebbi" 551 | - "mayo kebbi" 552 | - "desert" 553 | - "south & east" 554 | - "menaka" 555 | - "mordex" 556 | - "sahil" 557 | - "say'on" 558 | - "syria" 559 | 560 | admin_info_with_parent: 561 | - {pcode: AF0101, name: Kabul, iso3: AFG, parent: AF01} 562 | - {pcode: AF0102, name: Paghman, iso3: AFG, parent: AF01} 563 | - {pcode: AF0201, name: Kabul, iso3: AFG, parent: AF02} # testing purposes 564 | -------------------------------------------------------------------------------- /tests/fixtures/adminlevelparent.yaml: -------------------------------------------------------------------------------- 1 | admin_info_with_parent: 2 | - {pcode: AF0101, name: Kabul, iso3: AFG, parent: AF01} 3 | - {pcode: AF0102, name: Paghman, iso3: AFG, parent: AF01} 4 | - {pcode: AF0201, name: Kabul, iso3: AFG, parent: AF02} # testing purposes 5 | - {pcode: AF0301, name: Charikar, iso3: AFG, parent: AF03} 6 | - {pcode: AF0401, name: Maydan Shahr, iso3: AFG, parent: AF04} 7 | - {pcode: AF0501, name: Pul-e-Alam, iso3: AFG, parent: AF05} 8 | - {pcode: AF0501, name: Pul-e-Alam, iso3: AFG, parent: AF05} 9 | - {pcode: CD2013, name: Mbanza-Ngungu, iso3: COD, parent: CD20} 10 | - {pcode: CD3102, name: Kenge, iso3: COD, parent: CD31} 11 | - {pcode: MW305, name: Blantyre, iso3: MWI, parent: MW3} 12 | 13 | admin_name_mappings: 14 | "MyMapping": "AF0301" 15 | "AFG|MyMapping2": "AF0401" 16 | "AF05|MyMapping3": "AF0501" 17 | 18 | admin_name_replacements: 19 | " city": "" 20 | 21 | alt1_admin_name_replacements: 22 | "COD| city": "" 23 | 24 | alt2_admin_name_replacements: 25 | "CD20| city": "" 26 | -------------------------------------------------------------------------------- /tests/fixtures/download-global-pcode-lengths.csv: -------------------------------------------------------------------------------- 1 | Location,Country Length,Admin 1 Length,Admin 2 Length,Admin 3 Length,Admin 4 Length,Admin 5 Length 2 | #country+code,#country+len,#adm1+len,#adm2+len,#adm3+len,#adm4+len,#adm5+len 3 | AFG,2,2,2,,, 4 | AGO,2,2,3,3,, 5 | ALB,2,2,2,2,, 6 | ARE,2,2,,,, 7 | ARG,2,3,3,,, 8 | ARM,2,2,1,3,, 9 | ATG,2,2,,,, 10 | AZE,2,8,,,, 11 | BDI,3,3,3,,, 12 | BEN,2,2,2,,, 13 | BES,3,1,,,, 14 | BFA,2,2,2,2,, 15 | BGD,2,2,2,2,2, 16 | BGR,2,3,3,,, 17 | BLR,2,3,3,,, 18 | BLZ,2,2,,,, 19 | BMU,2,2,1,,, 20 | BOL,2,2,2,2,, 21 | BRA,2,2,5,,, 22 | BRB,2,2,,,, 23 | BTN,2,3,2,,, 24 | BWA,2,2,2,2,, 25 | CAF,2,2,1,1,2, 26 | CHL,2,2,1,2,, 27 | CHN,2,3,3,,, 28 | CIV,2,2,2,2,, 29 | CMR,2,3,3,3,, 30 | COD,2,2,2,,, 31 | COG,2,2,2,,, 32 | COL,2,2,3,,, 33 | COM,2,1,1,1,, 34 | CPV,2,2,2,,, 35 | CRI,2,1,2,2,, 36 | CUB,2,2,2,,, 37 | CUW,2,2,,,, 38 | CYM,2,2,,,, 39 | DJI,2,2,2,,, 40 | DMA,2,2,,,, 41 | DOM,2,2,2,2,2, 42 | DZA,2,3,3,,, 43 | ECU,2,2,2,2,, 44 | EGY,2,2,2,,, 45 | ERI,2,1,2,,, 46 | ESH,2,2,,,, 47 | ETH,2,2,2,2,, 48 | FJI,2,1,2,2,, 49 | FSM,2,1,2,,, 50 | GAB,3,3,3,,, 51 | GEO,2,2,2,,, 52 | GHA,2,2,2,,, 53 | GIN,2,3,3,2,, 54 | GLP,2,2,2,,, 55 | GMB,2,2,2,2,, 56 | GNB,2,2,2,,, 57 | GNQ,2,3,3,,, 58 | GRD,2,2,,,, 59 | GTM,2,2,2,,, 60 | GUF,2,1,3,,, 61 | GUY,2,2,2,,, 62 | HND,2,2,2,,, 63 | HTI,2,2,2,3,, 64 | HUN,2,3,3,,, 65 | IDN,2,2,2,3,, 66 | IRN,2,3,3,,, 67 | IRQ,2,3,3,3,, 68 | JAM,2,2,3,,, 69 | KAZ,3,3,3,,, 70 | KEN,2,3,3,,, 71 | KGZ,2,11,0,0,, 72 | KHM,2,2,2,2,, 73 | KIR,2,1,2,,, 74 | KNA,2,2,,,, 75 | KWT,2,2,,,, 76 | LAO,2,2,2,,, 77 | LBN,2,1,1,1,, 78 | LBR,2,2,2,,, 79 | LBY,2,2,2,,, 80 | LCA,2,2,9,,, 81 | LKA,2,1,1,2,3, 82 | LSO,2,1,2,,, 83 | MAR,2,3,3,7,1, 84 | MDA,2,3,,,, 85 | MDG,2,2,7|3,-1|3,3, 86 | MDV,2,3,3,3,, 87 | MEX,2,2,3,,, 88 | MHL,2,2,2,,, 89 | MLI,2,2,2,2,, 90 | MMR,3,3,4,-1,3,3 91 | MNG,2,2,2,,, 92 | MOZ,2,2,2,2,, 93 | MRT,2,2,1,2,, 94 | MSR,2,2,,,, 95 | MTQ,2,2,2,,, 96 | MUS,2,2,,,, 97 | MWI,2,1,2,2,, 98 | MYS,2,2,2,,, 99 | NAM,2,2,2,,, 100 | NER,2,3,3,3,, 101 | NGA,2,3,3,3,, 102 | NIC,2,2,2,,, 103 | NPL,2,2,2,3,, 104 | OMN,2,2,2,,, 105 | PAK,2,1,2,2,, 106 | PAN,2,2,2,2,, 107 | PER,2,2,2,2,, 108 | PHL,2,2,3,2,3, 109 | PNG,2,2,2,2,, 110 | POL,2,3,3,,, 111 | PRI,2,2,,,, 112 | PRK,2,2,2,,, 113 | PRY,2,2,2,,, 114 | PSE,2,2,2,,, 115 | QAT,3,3,3,,, 116 | ROU,2,3,3,,, 117 | RUS,2,3,3,,, 118 | RWA,2,1,1,2,2, 119 | SAU,2,2,,,, 120 | SDN,2,2,3,,, 121 | SEN,2,2,2,2,, 122 | SLB,2,2,4,4,, 123 | SLE,2,2,2,2,2, 124 | SLV,2,2,2,,, 125 | SOM,2,2,2,,, 126 | SSD,2,2,2,2,, 127 | STP,2,2,2,,, 128 | SUR,2,2,2,,, 129 | SVK,2,3,3,,, 130 | SWZ,2,1,2,,, 131 | SXM,2,1,,,, 132 | SYC,2,1,1,4,, 133 | SYR,2,2,2,2,, 134 | TCA,2,1,2,,, 135 | TCD,2,2,2,2,, 136 | TGO,2,2,2,2,, 137 | THA,2,2,2,2,, 138 | TJK,0,7,0,,, 139 | TLS,2,2,2,2,, 140 | TON,2,1,1,2,, 141 | TTO,2,2,,,, 142 | TUN,2,1,1,2,2, 143 | TUR,3,3,3,3,3, 144 | TZA,2,2,2,3|4,, 145 | UGA,2,1,3,2,2, 146 | UKR,2,2,2,3,3, 147 | URY,2,2,3,,, 148 | UZB,2,2,3,,, 149 | VCT,2,1,2,,, 150 | VEN,2,2,2,2,, 151 | VGB,2,2,,,, 152 | VIR,2,3,5,,, 153 | VNM,2,3,2,,, 154 | VUT,2,2,3,,, 155 | YEM,2,2,2,2,, 156 | ZAF,2,1,2,1,3, 157 | ZMB,2,3,3,3,3, 158 | ZWE,2,2,2,2,, 159 | -------------------------------------------------------------------------------- /tests/fixtures/secondary_rates.json: -------------------------------------------------------------------------------- 1 | {"date":"2023-10-03","usd":{"00":12.95552661,"1inch":3.72771145,"aave":0.014324392,"abt":13.57078229,"ach":67.36716765,"acs":544.87178826,"ada":3.83538375,"aed":3.6725,"aergo":9.69623514,"afn":77.84602222,"agld":1.70081907,"aioz":81.31436216,"akt":1.1304956,"alcx":0.080626091,"aleph":14.42201376,"algo":9.79438332,"alice":1.35523937,"all":101.42693607,"amd":397.35403772,"amp":612.19122172,"ang":1.79035415,"ankr":50.13366689,"ant":0.20144343,"aoa":833.81097254,"ape":0.84105187,"api3":0.89141413,"apt":0.18339017,"ar":0.23316845,"arb":1.06288152,"arpa":22.65383136,"ars":350.00775858,"asm":46.93414615,"ast":11.17504642,"ata":11.99240593,"atom":0.13979267,"ats":13.14259146,"auction":0.1805355,"aud":1.57603186,"audio":6.74193903,"aurora":19.42071174,"avax":0.1073441,"avt":1.32472405,"awg":1.79,"axl":2.96609328,"axs":0.2170831,"azm":8499.76821427,"azn":1.69995364,"badger":0.4566971,"bake":7.4725826,"bal":0.3017094,"bam":1.86803156,"band":0.87580267,"bat":5.61868828,"bbd":2,"bch":0.004033272,"bdt":110.4995277,"bef":38.52901648,"bgn":1.86803156,"bhd":0.376,"bico":4.23082341,"bif":2843.63803136,"bit":2.22481738,"blur":5.48036523,"blz":6.58654366,"bmd":1,"bnb":0.0046470595,"bnd":1.37447226,"bnt":2.49947933,"bob":6.93854279,"boba":8.80041051,"bond":0.44276825,"brl":5.06288066,"bsd":1,"bsv":0.023795821,"bsw":15.02412861,"btc":0.0000363307,"btcb":0.00003630713,"btg":0.070004723,"btn":83.24804509,"btrst":3.15509986,"btt":2584182.48697375,"busd":1.00009772,"bwp":13.82427878,"byn":3.30298616,"byr":33029.86163378,"bzd":2.02219455,"c98":6.7305966,"cad":1.36967221,"cake":0.80620309,"cbeth":0.00057466806,"cdf":2491.54671122,"celo":2.22752974,"celr":80.78890586,"cfx":7.44120839,"cgld":2.23500928,"chf":0.91889329,"chz":16.24604765,"clp":908.5563717,"clv":29.99000463,"cnh":7.32545633,"cny":7.25611758,"comp":0.021689327,"cop":4161.31748417,"coti":24.27588967,"coval":143.59894537,"crc":538.44601025,"cro":19.76909958,"crpt":14.89451459,"crv":1.99466719,"cspr":31.26466571,"ctsi":7.56269682,"ctx":1.15626203,"cuc":1,"cup":24.0897165,"cvc":13.22971765,"cve":105.31991023,"cvx":0.33980158,"cyp":0.55900068,"czk":23.32978072,"dai":1.00029646,"dar":10.58377412,"dash":0.036329172,"dcr":0.073279873,"ddx":13.72782651,"dem":1.86803156,"deso":0.11320505,"dext":1.95097368,"dfi":3.43447005,"dia":3.9916455,"dimo":11.50410231,"djf":177.83020569,"dkk":7.12351151,"dnt":43.29725777,"doge":16.03066266,"dop":56.96714617,"dot":0.24123665,"drep":4.06737151,"dydx":0.48281508,"dyp":7.90926934,"dzd":137.8371642,"eek":14.94425247,"egld":0.040040852,"egp":30.92513661,"ela":0.72240278,"enj":4.47787713,"ens":0.1277352,"eos":1.6741299,"ern":15,"esp":158.91682764,"etb":55.9434994,"etc":0.060383493,"eth":0.0006002192,"eth2":0.00060158229,"eur":0.95510937,"euroc":0.95435749,"farm":0.043627771,"fei":1.01524185,"fet":4.50728551,"fida":6.05611053,"fil":0.29798002,"fim":5.67882243,"fis":3.52979232,"fjd":2.28104441,"fkp":0.82804563,"flow":2.20615426,"flr":91.64669988,"fort":8.71225309,"forth":0.35279247,"fox":46.19707411,"frax":1.00222144,"frf":6.26510677,"ftm":4.98423213,"ftt":0.86707398,"fx":8.28638488,"fxs":0.18360913,"gal":0.81646258,"gala":66.77218883,"gbp":0.82804563,"gel":2.67989246,"gfi":2.25085328,"ggp":0.82804563,"ghc":116099.16732335,"ghs":11.60991673,"ghst":1.25412747,"gip":0.82804563,"glm":5.55185487,"gmd":64.83100117,"gmt":6.3847217,"gmx":0.025230508,"gnf":8602.57126358,"gno":0.0098976363,"gnt":5.56853445,"gods":6.37190873,"grd":325.45351783,"grt":11.25600568,"gst":106.00600472,"gt":0.26620172,"gtc":1.01539762,"gtq":7.88395327,"gusd":1.000422,"gyd":209.87610154,"gyen":148.39267871,"hbar":19.59194805,"hft":3.00080004,"high":0.76788227,"hkd":7.83160585,"hnl":24.77704374,"hnt":0.67797046,"hopr":24.04246766,"hot":968.64176843,"hrk":7.19627155,"ht":0.4192932,"htg":135.73067844,"huf":371.65466266,"icp":0.32078575,"idex":20.22581708,"idr":15590.86069601,"iep":0.75220976,"ils":3.84278789,"ilv":0.024867396,"imp":0.82804563,"imx":1.73411529,"index":1.06969696,"inj":0.13158094,"inr":83.24804509,"inv":0.036899711,"iotx":57.28331355,"iqd":1309.63349047,"irr":42047.69732039,"isk":140.11041004,"itl":1849.34961986,"jasmy":298.11226663,"jep":0.82804563,"jmd":155.82087975,"jod":0.709,"jpy":149.85719252,"jup":1111.29094894,"kas":20.02373228,"kava":1.57939035,"kcs":0.21615111,"kda":2.09489329,"keep":11.25933417,"kes":148.33624855,"kgs":88.70858205,"khr":4143.63394175,"klay":8.74155887,"kmf":469.88300777,"knc":1.51771152,"kpw":900.00629549,"krl":4.68791495,"krw":1361.11423235,"ksm":0.051905024,"kwd":0.30936089,"kyd":0.8200525,"kzt":477.48090982,"lak":20468.19275037,"lbp":15030.96331779,"lcx":24.3349551,"ldo":0.63497699,"leo":0.26902848,"link":0.1322708,"lit":1.39454358,"lkr":324.68314579,"loka":5.01965699,"loom":8.0310777,"lpt":0.1508547,"lqty":0.9306906,"lrc":5.39445086,"lrd":187.25511243,"lseth":0.00059005664,"lsl":19.26172239,"ltc":0.01507327,"ltl":3.29780163,"luf":38.52901648,"luna":1.88090728,"lunc":16203.67156001,"lvl":0.67125087,"lyd":4.91198367,"mad":10.34437892,"magic":1.78649041,"mana":3.19761157,"mask":0.35279247,"math":14.01775269,"matic":1.80375201,"mco2":0.54063062,"mdl":18.2983752,"mdt":21.37336584,"media":0.16289359,"metis":0.075398919,"mga":4546.05813424,"mgf":22730.29067122,"mina":2.60585918,"miota":6.58407245,"mir":71.26271617,"mkd":58.65369659,"mkr":0.00068049005,"mln":0.06496698,"mmk":2105.19136666,"mnde":23.70344009,"mnt":3468.3643834,"mona":0.003307867,"mop":8.06655402,"mpl":0.17733451,"mro":382.19870289,"mru":38.21987029,"msol":0.03767106,"mtl":0.41002845,"multi":0.40835245,"mur":44.44315135,"musd":1.00353745,"muse":0.16887575,"mvr":15.40633905,"mwk":1086.87219943,"mxc":128.63879801,"mxn":17.71906258,"mxv":2.22275993,"myr":4.72324655,"mzm":63851.00139919,"mzn":63.8510014,"nad":19.26172239,"nct":117.11553332,"near":0.89219712,"neo":0.1337661,"nest":220.28911403,"nexo":1.79383287,"nft":3070013.63045702,"ngn":767.46703405,"nio":36.76414304,"nkn":11.45666271,"nlg":2.10478407,"nmr":0.076290363,"nok":10.88233137,"npr":133.25930819,"nu":16.21095102,"nzd":1.6888783,"ocean":3.21390313,"ogn":7.09967457,"okb":0.023127597,"omg":2.03936678,"omr":0.38476524,"one":101.99623845,"ooki":561.72878479,"op":0.71555421,"orca":1.11582156,"orn":1.92896173,"osmo":3.23416865,"oxt":14.34959332,"pab":1,"pax":1.00111772,"paxg":0.00054319819,"pen":3.79451608,"pepe":1342425.95431703,"perp":1.59249527,"pgk":3.66298674,"php":56.88682862,"pkr":286.84504866,"pla":6.6126721,"pln":4.41140864,"plu":0.17485431,"png":52.21439074,"pols":3.64957728,"poly":7.96945541,"pond":117.59749024,"powr":6.50726516,"prime":0.28169741,"pro":4.35138853,"prq":18.88888866,"pte":191.48223672,"pundix":2.70315312,"pyg":7334.67586363,"pyr":0.32948992,"pyusd":1.00021166,"qar":3.64,"qi":189.30001978,"qnt":0.01115365,"qsp":99.76724733,"qtum":0.42963189,"quick":0.0227621,"rad":0.74918851,"rai":0.36569165,"rare":16.28935919,"rari":1.11750464,"rbn":6.19835557,"ren":21.39393913,"rep":1.69738294,"repv2":1.69738294,"req":15.32822459,"rgt":1.26939328,"rlc":0.9639696,"rly":160.25686912,"rndr":0.58798808,"rol":47491.57663338,"ron":4.74915766,"rose":23.95321888,"rpl":0.045375652,"rsd":111.92747997,"rub":98.96611838,"rune":0.49304347,"rvn":65.24898746,"rwf":1223.0006346,"sand":3.25639595,"sar":3.75,"sbd":8.43910727,"scr":13.29710831,"sdd":60072.65885558,"sdg":600.72658856,"sei":8.37660515,"sek":11.06817939,"sgd":1.37447226,"shib":136379.32549316,"shp":0.82804563,"shping":214.30611839,"sit":228.88240943,"skk":28.77362488,"skl":45.15425077,"sle":22.5451997,"sll":22545.19969942,"snt":43.07349933,"snx":0.48016562,"sol":0.042279567,"sos":569.25353421,"spa":265.22584315,"spell":2036.5845134,"spl":0.16666667,"srd":38.24944226,"srg":38249.44226012,"ssp":600.10000131,"std":23484.89104553,"stg":2.14280187,"stn":23.48489105,"storj":2.05436306,"stx":1.91857458,"sui":2.1378131,"suku":19.40187497,"super":12.48491642,"sushi":1.6867639,"svc":8.75,"swftc":930.38758561,"sylo":833.12507666,"syn":3.13531867,"syp":13001.77068133,"szl":19.26172239,"t":53.15794071,"thb":37.11562737,"theta":1.58421834,"time":0.064714763,"tjs":10.98191075,"tmm":17471.13646899,"tmt":3.49422729,"tnd":3.17912661,"ton":0.49491134,"tone":93.60474071,"top":2.38744642,"trac":4.17431826,"trb":0.019437696,"tribe":3.77782361,"trl":27518356.81650728,"tru":24.07139963,"trx":11.39264529,"try":27.51835682,"ttd":6.8067835,"ttt":301.34445152,"tusd":1.00145908,"tvd":1.57603186,"tvk":49.53772435,"twd":32.32058725,"twt":1.26257326,"tzs":2509.36698404,"uah":36.87098004,"ugx":3772.36511877,"uma":0.71799473,"unfi":0.14247388,"uni":0.22262429,"upi":1106.94101198,"usd":1,"usdc":1.00020285,"usdd":1.00238208,"usdp":1.00756095,"usdt":1.00012951,"ust":85.30206009,"uyu":38.68548598,"uzs":12232.23817783,"val":1849.34961986,"vara":8.09196322,"veb":3438224057.9852734,"ved":34.39270752,"vef":3439270.75241739,"ves":34.39270752,"vet":58.59130492,"vgx":8.30940447,"vnd":24352.95798061,"voxel":7.25284013,"vtho":832.0854031,"vuv":122.19903698,"wampl":0.32315562,"waves":0.63799441,"waxl":2.96521392,"wbtc":0.000036334132,"wcfg":3.15509986,"wemix":0.97852653,"wluna":16400.13333533,"woo":5.58005508,"wst":2.78230059,"xaf":626.51067702,"xag":0.048054655,"xau":0.0005488203,"xaut":0.00054327396,"xbt":0.0000363307,"xcd":2.70211298,"xch":0.0395784,"xcn":1324.72404573,"xdc":19.66712156,"xdr":0.76479065,"xec":35875.92764194,"xem":38.06872149,"xlm":8.93466498,"xmon":0.00098914821,"xmr":0.0068214181,"xof":626.51067702,"xpd":0.000825595,"xpf":113.97486516,"xpt":0.0011390386,"xrp":1.94692745,"xtz":1.46262705,"xyo":340.19273964,"yer":250.31417789,"yfi":0.00019099123,"yfii":0.0018151593,"zar":19.26172239,"zec":0.036291919,"zen":0.12317323,"zil":58.05750895,"zmk":21160.73224334,"zmw":21.16073224,"zrx":5.24720255,"zwd":361.9,"zwl":5531.21758552}} 2 | -------------------------------------------------------------------------------- /tests/fixtures/wfp/Currency_List_1.json: -------------------------------------------------------------------------------- 1 | {"items": [{"createDate": "2021-08-02T18:24:29.657", "extendedName": "United Arab Emirates Dirham", "id": 131, "name": "AED", "updateDate": "2023-10-05T16:05:48.223"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Afghan Afghani", "id": 87, "name": "AFN", "updateDate": "2023-10-05T16:06:06.557"}, {"createDate": "2021-08-02T18:22:53.87", "extendedName": "Albanian Lek", "id": 130, "name": "ALL", "updateDate": "2023-10-05T16:06:11.963"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Armenian Dram", "id": 37, "name": "AMD", "updateDate": "2023-10-05T16:06:16.883"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Angolan Kwanza", "id": 96, "name": "AOA", "updateDate": "2023-10-05T15:07:06.5"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Argentine Peso", "id": 106, "name": "ARS", "updateDate": "2023-10-05T16:06:22.83"}, {"createDate": "2021-08-02T18:28:54.6", "extendedName": "Australian Dollar", "id": 133, "name": "AUD", "updateDate": "2023-10-05T16:06:28.503"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Azerbaijani Manat", "id": 36, "name": "AZN", "updateDate": "2023-10-05T16:06:33.617"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Bangladeshi Taka", "id": 25, "name": "BDT", "updateDate": "2023-10-05T16:06:39.94"}, {"createDate": "2021-08-02T18:27:55.72", "extendedName": "Bulgarian Lev", "id": 132, "name": "BGN", "updateDate": "2023-10-05T16:06:45.027"}, {"createDate": "2021-10-04T18:28:31.65", "extendedName": "Bahraini Dinar", "id": 138, "name": "BHD", "updateDate": "2023-10-05T16:06:52.357"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Burundian Franc", "id": 32, "name": "BIF", "updateDate": "2023-10-05T16:05:30.807"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Boliviano", "id": 46, "name": "BOB", "updateDate": "2023-10-05T16:07:16.897"}, {"createDate": "2024-01-19T10:08:31.157", "extendedName": "Brazilian Real", "id": 159, "name": "BRL", "updateDate": null}, {"createDate": "2021-11-16T13:56:25.917", "extendedName": "Bahamian Dollar", "id": 144, "name": "BSD", "updateDate": "2023-10-05T16:09:05.437"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Bhutanese Ngultrum", "id": 76, "name": "BTN", "updateDate": "2023-10-05T16:07:41.03"}, {"createDate": "2022-09-09T08:56:59.297", "extendedName": "Botswana Pula", "id": 148, "name": "BWP", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Belarusian Ruble", "id": 107, "name": "BYR", "updateDate": "2023-10-05T16:08:22.667"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Congolese Franc", "id": 27, "name": "CDF", "updateDate": "2023-10-05T16:08:43.547"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Chilean Peso", "id": 108, "name": "CLP", "updateDate": "2023-10-05T16:08:58.83"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Renminbi", "id": 109, "name": "CNY", "updateDate": "2023-10-05T16:09:23.17"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Colombian Peso", "id": 67, "name": "COP", "updateDate": "2023-10-05T16:09:51.277"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Costa Rican Colon", "id": 92, "name": "CRC", "updateDate": "2023-10-05T16:10:04.74"}, {"createDate": "2021-08-02T19:18:01.077", "extendedName": "Cuban Convertible Pesos", "id": 134, "name": "CUC", "updateDate": "2023-10-05T16:10:15.51"}, {"createDate": "2021-07-23T10:23:48.907", "extendedName": "Cuban Peso", "id": 127, "name": "CUP", "updateDate": "2021-07-23T10:24:03.74"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Cape Verdean Escudo", "id": 57, "name": "CVE", "updateDate": "2023-10-05T16:10:33.13"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Djiboutian Franc", "id": 59, "name": "DJF", "updateDate": "2023-10-05T16:10:45.763"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Dominican Peso", "id": 85, "name": "DOP", "updateDate": "2023-10-05T16:11:12.62"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Algerian Dinar", "id": 91, "name": "DZD", "updateDate": "2023-10-05T16:11:26.253"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Egyptian Pound", "id": 38, "name": "EGP", "updateDate": "2023-10-05T16:11:39.353"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Eritrean Nakfa", "id": 110, "name": "ERN", "updateDate": "2023-10-05T16:11:53.783"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Ethiopian Birr", "id": 47, "name": "ETB", "updateDate": "2023-10-05T16:12:07.26"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Euro", "id": 111, "name": "EUR", "updateDate": "2023-10-05T16:12:17.437"}, {"createDate": "2021-11-16T13:59:56.407", "extendedName": "Fijian dollar", "id": 146, "name": "FJD", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Georgian Lari", "id": 50, "name": "GEL", "updateDate": "2023-10-05T16:12:32.133"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Ghanaian Cedi", "id": 35, "name": "GHS", "updateDate": "2023-10-05T16:12:51.58"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Gambian Dalasi", "id": 42, "name": "GMD", "updateDate": "2023-10-05T16:13:02.697"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Guinean Franc", "id": 54, "name": "GNF", "updateDate": "2023-10-05T16:13:27.12"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Guatemalan Quetzal", "id": 82, "name": "GTQ", "updateDate": "2023-10-05T16:13:42.487"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Honduran Lempira", "id": 112, "name": "HNL", "updateDate": "2023-10-05T16:14:02.63"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Haitian Gourde", "id": 24, "name": "HTG", "updateDate": "2023-10-05T16:16:54.453"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Indonesian Rupiah", "id": 34, "name": "IDR", "updateDate": "2023-10-05T16:51:09.01"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "New Israeli Shekel", "id": 73, "name": "ILS", "updateDate": "2021-09-20T17:55:14.71"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Indian Rupee", "id": 68, "name": "INR", "updateDate": "2023-10-05T16:51:24.84"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Iraqi Dinar", "id": 84, "name": "IQD", "updateDate": "2023-10-05T16:51:42.25"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Iranian Rial", "id": 89, "name": "IRR", "updateDate": "2023-10-05T16:52:01.813"}, {"createDate": "2021-11-16T13:50:01.28", "extendedName": "Jamaican Dollar", "id": 139, "name": "JMD", "updateDate": "2021-11-16T13:51:47.597"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Jordanian Dinar", "id": 58, "name": "JOD", "updateDate": "2023-10-05T16:52:13.29"}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Japanese Yen", "id": 113, "name": "JPY", "updateDate": "2023-10-05T16:52:25.02"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Kenyan Shilling", "id": 30, "name": "KES", "updateDate": "2023-10-05T17:20:23.877"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Kyrgyzstani Som", "id": 61, "name": "KGS", "updateDate": "2023-10-05T17:20:37.52"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Cambodian Riel", "id": 60, "name": "KHR", "updateDate": "2023-10-05T17:20:54.753"}, {"createDate": "2022-09-09T08:58:07.763", "extendedName": "Comorian Franc", "id": 149, "name": "KMF", "updateDate": null}, {"createDate": "2021-11-16T13:53:56.17", "extendedName": "North Korean Won", "id": 141, "name": "KPW", "updateDate": null}, {"createDate": "2021-11-16T13:57:58.577", "extendedName": "Cayman Islands Dollar", "id": 145, "name": "KYD", "updateDate": "2023-10-05T17:21:02.627"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Kazakhstani Tenge", "id": 99, "name": "KZT", "updateDate": "2023-10-05T17:21:22.537"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Lao Kip", "id": 63, "name": "LAK", "updateDate": "2023-10-05T17:21:33.213"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Lebanese Pound", "id": 90, "name": "LBP", "updateDate": "2023-10-05T17:21:44.72"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Sri Lankan Rupee", "id": 55, "name": "LKR", "updateDate": "2023-10-05T17:21:55.583"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Liberian Dollar", "id": 40, "name": "LRD", "updateDate": "2023-10-05T17:22:08.767"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Lesotho Loti", "id": 56, "name": "LSL", "updateDate": "2023-10-05T17:22:20.387"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Libyan Dinar", "id": 97, "name": "LYD", "updateDate": "2023-10-05T17:22:34.527"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Moroccan Dirham", "id": 101, "name": "MAD", "updateDate": "2023-10-05T17:22:45.337"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Moldovan Leu", "id": 100, "name": "MDL", "updateDate": "2023-10-05T17:22:56.143"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Malagasy Ariary", "id": 65, "name": "MGA", "updateDate": "2023-10-05T17:23:11.417"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Myanmar Kyat", "id": 74, "name": "MMK", "updateDate": "2023-10-05T17:23:21.75"}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Mongolian T\u00f6gr\u00f6g", "id": 114, "name": "MNT", "updateDate": "2023-10-05T17:23:37.12"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Mauritanian Ouguiya", "id": 64, "name": "MRO", "updateDate": "2023-10-05T17:23:52.63"}, {"createDate": "2022-05-18T14:54:28.977", "extendedName": "Ouguiya", "id": 147, "name": "MRU", "updateDate": "2022-05-20T14:29:42.43"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Malawian Kwacha", "id": 66, "name": "MWK", "updateDate": "2023-10-05T17:24:11.41"}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Mexican Peso", "id": 115, "name": "MXN", "updateDate": "2023-10-05T17:24:27.67"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Mozambican Metical", "id": 51, "name": "MZN", "updateDate": "2023-10-06T13:09:29"}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Namibian Dollar", "id": 116, "name": "NAD", "updateDate": "2023-10-06T13:09:36.943"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Nigerian Naira", "id": 95, "name": "NGN", "updateDate": "2023-10-06T13:09:47.943"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Nicaraguan C\u00f3rdoba", "id": 86, "name": "NIO", "updateDate": "2023-10-06T13:10:00.62"}, {"createDate": "2023-10-06T13:10:24.21", "extendedName": "Norwegian krone", "id": 152, "name": "NOK", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Nepalese Rupee", "id": 62, "name": "NPR", "updateDate": "2023-10-06T13:10:37.26"}, {"createDate": "2023-10-06T13:11:01.767", "extendedName": "New Zealand Dollar", "id": 153, "name": "NZD", "updateDate": null}, {"createDate": "2023-10-06T13:11:20.807", "extendedName": "Omani Rial", "id": 154, "name": "OMR", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Panamanian Balboa", "id": 117, "name": "PAB", "updateDate": "2023-10-06T13:11:45.267"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Peruvian Sol", "id": 33, "name": "PEN", "updateDate": "2023-10-06T13:11:54.71"}, {"createDate": "2021-11-16T13:51:13.607", "extendedName": "Papua New Guinean Kina", "id": 140, "name": "PGK", "updateDate": "2021-11-16T13:51:20.977"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Philippine Peso", "id": 29, "name": "PHP", "updateDate": "2023-10-06T13:12:31.657"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Pakistani Rupee", "id": 45, "name": "PKR", "updateDate": "2023-10-06T13:12:45.58"}, {"createDate": "2023-10-06T13:12:59.437", "extendedName": "Polish z\u0142oty", "id": 155, "name": "PLN", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Paraguayan Guaran\u00ed", "id": 118, "name": "PYG", "updateDate": "2023-10-06T13:13:08.133"}, {"createDate": "2023-10-06T13:14:01.817", "extendedName": "Romanian Leu", "id": 156, "name": "RON", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Russian Ruble", "id": 102, "name": "RUB", "updateDate": "2023-10-06T13:13:29.22"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Rwandan Franc", "id": 77, "name": "RWF", "updateDate": "2023-10-06T13:14:15.373"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Saudi Riyal", "id": 103, "name": "SAR", "updateDate": "2023-10-06T13:14:24.197"}, {"createDate": "2022-09-09T10:23:48.027", "extendedName": "Solomon Islands Dollar", "id": 150, "name": "SBD", "updateDate": "2023-10-06T13:14:30.883"}, {"createDate": "2023-10-06T13:14:55.84", "extendedName": "Seychelles Rupee", "id": 157, "name": "SCR", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Sudanese Pound", "id": 75, "name": "SDG", "updateDate": "2023-10-06T13:15:03.55"}, {"createDate": "2023-10-06T13:15:21.86", "extendedName": "Swedish Krona ", "id": 158, "name": "SEK", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Sierra Leonean Leone ", "id": 53, "name": "SLL", "updateDate": "2023-10-06T13:15:44.363"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Somaliland Shilling", "id": 81, "name": "SLS", "updateDate": "2020-05-13T19:08:07.153"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Somali Shilling", "id": 79, "name": "SOS", "updateDate": "2023-10-06T13:15:54.223"}, {"createDate": "2021-09-03T16:47:19.25", "extendedName": "Surinamese dollar", "id": 137, "name": "SRD", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "South Sudanese Pound", "id": 83, "name": "SSP", "updateDate": "2023-10-06T13:16:09.307"}, {"createDate": "2021-11-16T13:54:49.843", "extendedName": "S\u00e3o Tom\u00e9 and Pr\u00edncipe Dobra", "id": 142, "name": "STN", "updateDate": "2022-02-04T18:49:22.997"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Syrian Pound", "id": 78, "name": "SYP", "updateDate": "2023-10-06T13:16:20.727"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Swazi Lilangeni", "id": 23, "name": "SZL", "updateDate": "2023-10-06T13:16:32.907"}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Thai Baht", "id": 119, "name": "THB", "updateDate": "2023-10-06T13:16:43.66"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Tajikistani Somoni", "id": 49, "name": "TJS", "updateDate": "2023-10-06T13:16:53.313"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Turkmenistan Manat", "id": 105, "name": "TMT", "updateDate": "2023-10-06T13:17:03.19"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Tunisian Dinar", "id": 104, "name": "TND", "updateDate": "2023-10-06T13:17:16.303"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Turkish Lira", "id": 88, "name": "TRY", "updateDate": "2023-10-06T13:17:27.8"}, {"createDate": "2021-11-16T13:55:49.043", "extendedName": "Trinidad and Tobago dollar", "id": 143, "name": "TTD", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Tanzanian Shilling", "id": 22, "name": "TZS", "updateDate": "2023-10-06T13:17:41.38"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Ukrainian Hryvnia", "id": 93, "name": "UAH", "updateDate": "2023-10-06T13:17:49.587"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Ugandan Shilling", "id": 39, "name": "UGX", "updateDate": "2023-10-06T13:18:00.927"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "United States Dollar", "id": 28, "name": "USD", "updateDate": "2023-10-06T13:18:14.463"}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Uruguayan Peso", "id": 120, "name": "UYU", "updateDate": "2022-02-04T19:02:54.16"}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Uzbekistan Sum", "id": 121, "name": "UZS", "updateDate": "2023-10-06T13:18:39.537"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": null, "id": 98, "name": "VEF", "updateDate": null}, {"createDate": "2021-08-27T15:20:05.343", "extendedName": "Venezuelan Bolivar", "id": 135, "name": "VES", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Vietnamese Dong", "id": 122, "name": "VND", "updateDate": "2023-10-06T13:19:04.453"}, {"createDate": "2022-09-09T10:27:13.38", "extendedName": "Vanuatu Vatu", "id": 151, "name": "VUV", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Samoan Tala", "id": 123, "name": "WST", "updateDate": "2023-10-06T13:19:15.853"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "CFA franc BEAC", "id": 48, "name": "XAF", "updateDate": "2023-10-06T13:19:28.203"}, {"createDate": "2021-08-02T18:17:10.363", "extendedName": "East Caribbean dollar", "id": 129, "name": "XCD", "updateDate": null}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "CFA franc BCEAO", "id": 26, "name": "XOF", "updateDate": "2023-10-06T13:19:46.72"}, {"createDate": "2020-04-22T22:02:26.937", "extendedName": "Yemeni Rial", "id": 31, "name": "YER", "updateDate": "2023-10-06T13:19:55.923"}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "South African Rand", "id": 124, "name": "ZAR", "updateDate": "2023-10-06T13:20:04.47"}, {"createDate": "2024-05-10T16:34:21.023", "extendedName": "Zimbabwe Gold", "id": 160, "name": "ZiG", "updateDate": "2024-05-16T20:03:21.057"}, {"createDate": "2020-04-22T22:02:26.94", "extendedName": "Zambian Kwacha", "id": 94, "name": "ZMW", "updateDate": "2022-09-09T10:19:57.007"}, {"createDate": "2020-04-22T22:02:26.943", "extendedName": "Zimbabwean Dollar", "id": 125, "name": "ZWL", "updateDate": "2024-05-10T16:34:31.017"}], "page": 1, "totalItems": 127} 2 | -------------------------------------------------------------------------------- /tests/fixtures/wfp/Currency_List_2.json: -------------------------------------------------------------------------------- 1 | {"items": [], "page": 2, "totalItems": 127} 2 | -------------------------------------------------------------------------------- /tests/fixtures/wfp/Currency_UsdIndirectQuotation_10.json: -------------------------------------------------------------------------------- 1 | {"items": [], "page": 10, "totalItems": 8082} 2 | -------------------------------------------------------------------------------- /tests/fixtures/wfp/Currency_UsdIndirectQuotation_9.json: -------------------------------------------------------------------------------- 1 | {"items": [{"adm0Code": 1, "countryISO3": "AFG", "date": "1999-06-10T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-06-09T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-06-08T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-06-07T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-06-04T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-06-03T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-06-02T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-06-01T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-31T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-28T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-27T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-26T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-25T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-24T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-21T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-20T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-19T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-18T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-17T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-14T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-13T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-12T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-11T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-10T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-07T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-06T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-05T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-04T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-05-03T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-30T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-29T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-28T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-27T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-26T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-23T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-22T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-21T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-20T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-19T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-16T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-15T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-14T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-13T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-12T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-09T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-08T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-07T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-06T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-05T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-02T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-04-01T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-31T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-30T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-29T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-26T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-25T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-24T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-23T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-22T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.03}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-19T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-18T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-17T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-16T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-15T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-12T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-11T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-10T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-09T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-08T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-05T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-04T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-03T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-02T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-03-01T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.26}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-02-26T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.5}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-02-25T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.5}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-02-24T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.5}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-02-23T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.5}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-02-22T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.5}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-02-19T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.5}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-02-18T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.5}, {"adm0Code": 1, "countryISO3": "AFG", "date": "1999-02-17T00:00:00", "frequency": "Daily", "id": 87, "isOfficial": true, "name": "AFN", "value": 47.5}], "page": 9, "totalItems": 8082} 2 | -------------------------------------------------------------------------------- /tests/hdx/location/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OCHA-DAP/hdx-python-country/2f1b0c0e40d32238ac877203e1ff008f56ffd6f1/tests/hdx/location/__init__.py -------------------------------------------------------------------------------- /tests/hdx/location/conftest.py: -------------------------------------------------------------------------------- 1 | from os.path import join 2 | 3 | import pytest 4 | 5 | from hdx.location.currency import Currency 6 | 7 | 8 | @pytest.fixture(scope="session") 9 | def fixtures_dir(): 10 | return join("tests", "fixtures") 11 | 12 | 13 | @pytest.fixture(scope="session") 14 | def input_dir(fixtures_dir): 15 | return join(fixtures_dir, "wfp") 16 | 17 | 18 | @pytest.fixture(scope="function") 19 | def reset_currency(): 20 | Currency._cached_current_rates = {} 21 | Currency._cached_historic_rates = {} 22 | Currency._rates_api = "" 23 | Currency._secondary_rates = {} 24 | Currency._secondary_historic_rates = {} 25 | Currency._fallback_to_current = False 26 | Currency._no_historic = False 27 | -------------------------------------------------------------------------------- /tests/hdx/location/test_adminlevel.py: -------------------------------------------------------------------------------- 1 | """location Tests""" 2 | 3 | from os.path import join 4 | 5 | import pytest 6 | 7 | from hdx.location.adminlevel import AdminLevel 8 | from hdx.utilities.base_downloader import DownloadError 9 | from hdx.utilities.downloader import Download 10 | from hdx.utilities.loader import load_yaml 11 | from hdx.utilities.path import temp_dir 12 | from hdx.utilities.retriever import Retrieve 13 | 14 | 15 | class TestAdminLevel: 16 | @pytest.fixture(scope="function") 17 | def config(self, fixtures_dir): 18 | return load_yaml(join(fixtures_dir, "adminlevel.yaml")) 19 | 20 | @pytest.fixture(scope="function") 21 | def config_parent(self, fixtures_dir): 22 | return load_yaml(join(fixtures_dir, "adminlevelparent.yaml")) 23 | 24 | @pytest.fixture(scope="function") 25 | def url(self, fixtures_dir): 26 | return join(fixtures_dir, "download-global-pcodes-adm-1-2.csv") 27 | 28 | @pytest.fixture(scope="function") 29 | def formats_url(self, fixtures_dir): 30 | return join(fixtures_dir, "download-global-pcode-lengths.csv") 31 | 32 | def test_adminlevel(self, config): 33 | adminone = AdminLevel(config) 34 | adminone.setup_from_admin_info(config["admin_info"], countryiso3s=("yem",)) 35 | assert len(adminone.get_pcode_list()) == 22 36 | adminone = AdminLevel(config) 37 | adminone.setup_from_admin_info(config["admin_info"]) 38 | assert adminone.get_admin_level("YEM") == 1 39 | assert len(adminone.get_pcode_list()) == 433 40 | assert adminone.get_pcode_length("YEM") == 4 41 | assert adminone.use_parent is False 42 | assert adminone.pcode_to_iso3["YE30"] == "YEM" 43 | assert adminone.get_pcode("YEM", "YE30", logname="test") == ( 44 | "YE30", 45 | True, 46 | ) 47 | assert adminone.get_pcode("YEM", "YEM30", logname="test") == ( 48 | "YE30", 49 | True, 50 | ) 51 | assert adminone.get_pcode("YEM", "YEM3000", logname="test") == ( 52 | None, 53 | True, 54 | ) 55 | assert adminone.get_pcode("YEM", "YEM030", logname="test") == ( 56 | "YE30", 57 | True, 58 | ) 59 | assert adminone.get_pcode("NGA", "NG015", logname="test") == ( 60 | "NG015", 61 | True, 62 | ) 63 | assert adminone.get_pcode("NGA", "NG15", logname="test") == ( 64 | "NG015", 65 | True, 66 | ) 67 | assert adminone.get_pcode("NGA", "NGA015", logname="test") == ( 68 | "NG015", 69 | True, 70 | ) 71 | assert adminone.get_pcode("NER", "NER004", logname="test") == ( 72 | "NER004", 73 | True, 74 | ) 75 | assert adminone.get_pcode("NER", "NE04", logname="test") == ( 76 | "NER004", 77 | True, 78 | ) 79 | assert adminone.get_pcode("NER", "NE004", logname="test") == ( 80 | "NER004", 81 | True, 82 | ) 83 | assert adminone.get_pcode("ABC", "NE004", logname="test") == ( 84 | None, 85 | True, 86 | ) 87 | assert adminone.get_pcode("ABC", "BLAH", logname="test") == ( 88 | None, 89 | False, 90 | ) 91 | config["countries_fuzzy_try"].append("ABC") 92 | assert adminone.get_pcode("ABC", "NE004", logname="test") == ( 93 | None, 94 | True, 95 | ) 96 | assert adminone.get_pcode("ABC", "BLAH", logname="test") == ( 97 | None, 98 | False, 99 | ) 100 | assert adminone.get_pcode("XYZ", "XYZ123", logname="test") == ( 101 | None, 102 | True, 103 | ) 104 | assert adminone.get_pcode("XYZ", "BLAH", logname="test") == ( 105 | None, 106 | False, 107 | ) 108 | assert adminone.get_pcode("NER", "ABCDEFGH", logname="test") == ( 109 | None, 110 | False, 111 | ) 112 | assert adminone.get_pcode("YEM", "Ad Dali", logname="test") == ( 113 | "YE30", 114 | True, 115 | ) 116 | assert adminone.get_pcode("YEM", "Ad Dal", logname="test") == ( 117 | "YE30", 118 | False, 119 | ) 120 | assert adminone.get_pcode("YEM", "nord", logname="test") == ( 121 | None, 122 | False, 123 | ) 124 | assert adminone.get_pcode("NGA", "FCT (Abuja)", logname="test") == ( 125 | "NG015", 126 | True, 127 | ) 128 | assert adminone.get_pcode("UKR", "Chernihiv Oblast", logname="test") == ( 129 | "UA74", 130 | False, 131 | ) 132 | assert adminone.get_pcode( 133 | "UKR", 134 | "Chernihiv Oblast", 135 | fuzzy_match=False, 136 | logname="test", 137 | ) == ( 138 | None, 139 | True, 140 | ) 141 | assert adminone.get_pcode("ZWE", "ABCDEFGH", logname="test") == ( 142 | None, 143 | False, 144 | ) 145 | output = adminone.output_matches() 146 | assert output == [ 147 | "test - NER: Matching (pcode length conversion) NER004 to Maradi on map", 148 | "test - NGA: Matching (pcode length conversion) NG015 to Federal Capital Territory on map", 149 | "test - UKR: Matching (substring) Chernihiv Oblast to Chernihivska on map", 150 | "test - YEM: Matching (substring) Ad Dal to Ad Dali on map", 151 | "test - YEM: Matching (pcode length conversion) YE30 to Ad Dali on map", 152 | ] 153 | output = adminone.output_ignored() 154 | assert output == [ 155 | "test - Ignored ABC!", 156 | "test - Ignored XYZ!", 157 | "test - YEM: Ignored nord!", 158 | "test - Ignored ZWE!", 159 | ] 160 | output = adminone.output_errors() 161 | assert output == [ 162 | "test - Could not find ABC in map names!", 163 | "test - NER: Could not find ABCDEFGH in map names!", 164 | ] 165 | output = adminone.output_admin_name_mappings() 166 | assert len(output) == 62 167 | assert output[0] == "Nord-Ouest: North West (HT09)" 168 | assert output[31] == "Juba Dhexe: Middle Juba (SO27)" 169 | assert output[61] == "CU Niamey: Niamey (NER008)" 170 | 171 | output = adminone.output_admin_name_replacements() 172 | assert output == [ 173 | " urban: ", 174 | "sud: south", 175 | "ouest: west", 176 | "est: east", 177 | "nord: north", 178 | "': ", 179 | "/: ", 180 | ".: ", 181 | " region: ", 182 | " oblast: ", 183 | ] 184 | 185 | def test_adminlevel_fuzzy(self, config): 186 | adminone = AdminLevel(config) 187 | adminone.setup_from_admin_info(config["admin_info"]) 188 | assert adminone.get_pcode("YEM", "Al_Dhale'a", logname="test") == ( 189 | "YE30", 190 | False, 191 | ) 192 | assert adminone.get_pcode("YEM", "Al Dali", logname="test") == ( 193 | "YE30", 194 | False, 195 | ) 196 | assert adminone.get_pcode("YEM", "Al Dhale'e / الضالع", logname="test") == ( 197 | "YE30", 198 | False, 199 | ) 200 | assert adminone.get_pcode("SOM", "Bay", logname="test") == ( 201 | "SO24", 202 | True, 203 | ) 204 | output = adminone.output_matches() 205 | assert output == [ 206 | "test - YEM: Matching (fuzzy) Al Dali to Ad Dali on map", 207 | "test - YEM: Matching (fuzzy) Al Dhale'e / الضالع to Ad Dali on map", 208 | "test - YEM: Matching (fuzzy) Al_Dhale'a to Ad Dali on map", 209 | ] 210 | 211 | def test_adminlevel_parent(self, config_parent): 212 | admintwo = AdminLevel(config_parent) 213 | admintwo.countries_fuzzy_try = None 214 | admintwo.setup_from_admin_info(config_parent["admin_info_with_parent"]) 215 | assert admintwo.use_parent is True 216 | assert admintwo.pcode_to_parent["AF0101"] == "AF01" 217 | assert admintwo.get_pcode("AFG", "AF0101", logname="test") == ( 218 | "AF0101", 219 | True, 220 | ) 221 | assert admintwo.get_pcode("AFG", "AF0101", parent="blah", logname="test") == ( 222 | "AF0101", 223 | True, 224 | ) 225 | assert admintwo.get_pcode("AFG", "Kabul", logname="test") == ( 226 | "AF0201", 227 | True, 228 | ) 229 | assert admintwo.get_pcode("AFG", "Kabul", parent="AF01", logname="test") == ( 230 | "AF0101", 231 | True, 232 | ) 233 | assert admintwo.get_pcode("AFG", "Kabul", parent="blah", logname="test") == ( 234 | None, 235 | False, 236 | ) 237 | assert admintwo.get_pcode("AFG", "Kabul", parent="AF02", logname="test") == ( 238 | "AF0201", 239 | True, 240 | ) 241 | assert admintwo.get_pcode("AFG", "Kabull", parent="AF01", logname="test") == ( 242 | "AF0101", 243 | False, 244 | ) 245 | assert admintwo.get_pcode("AFG", "Kabull", parent="blah", logname="test") == ( 246 | None, 247 | False, 248 | ) 249 | assert admintwo.get_pcode("AFG", "Kabull", parent="AF02", logname="test") == ( 250 | "AF0201", 251 | False, 252 | ) 253 | assert admintwo.get_pcode("ABC", "Kabull", parent="AF02", logname="test") == ( 254 | None, 255 | False, 256 | ) 257 | 258 | output = admintwo.output_admin_name_mappings() 259 | assert output == [ 260 | "MyMapping: Charikar (AF0301)", 261 | "AFG|MyMapping2: Maydan Shahr (AF0401)", 262 | "AF05|MyMapping3: Pul-e-Alam (AF0501)", 263 | ] 264 | assert admintwo.get_pcode("AFG", "MyMapping", logname="test") == ( 265 | "AF0301", 266 | True, 267 | ) 268 | assert admintwo.get_pcode( 269 | "AFG", "MyMapping", parent="AF03", logname="test" 270 | ) == ("AF0301", True) 271 | assert admintwo.get_pcode( 272 | "AFG", "MyMapping", parent="AF04", logname="test" 273 | ) == (None, False) 274 | 275 | assert admintwo.get_pcode("AFG", "MyMapping2", logname="test") == ( 276 | "AF0401", 277 | True, 278 | ) 279 | assert admintwo.get_pcode( 280 | "AFG", "MyMapping2", parent="AF04", logname="test" 281 | ) == ("AF0401", True) 282 | assert admintwo.get_pcode( 283 | "AFG", "MyMapping2", parent="AF05", logname="test" 284 | ) == (None, False) 285 | 286 | assert admintwo.get_pcode("AFG", "MyMapping3", logname="test") == ( 287 | None, 288 | False, 289 | ) 290 | assert admintwo.get_pcode( 291 | "AFG", "MyMapping3", parent="AF05", logname="test" 292 | ) == ("AF0501", True) 293 | assert admintwo.get_pcode( 294 | "AFG", "MyMapping3", parent="AF04", logname="test" 295 | ) == (None, False) 296 | 297 | output = admintwo.output_admin_name_replacements() 298 | assert output == [" city: "] 299 | assert admintwo.get_pcode("COD", "Mbanza-Ngungu city", logname="test") == ( 300 | "CD2013", 301 | False, 302 | ) 303 | assert admintwo.get_pcode( 304 | "COD", "Mbanza-Ngungu city", parent="CD20", logname="test" 305 | ) == ("CD2013", False) 306 | assert admintwo.get_pcode( 307 | "COD", "Mbanza-Ngungu city", parent="CD19", logname="test" 308 | ) == (None, False) 309 | assert admintwo.get_pcode("COD", "Kenge city", logname="test") == ( 310 | "CD3102", 311 | False, 312 | ) 313 | assert admintwo.get_pcode("MWI", "Blantyre city", logname="test") == ( 314 | "MW305", 315 | False, 316 | ) 317 | 318 | admintwo.admin_name_replacements = config_parent["alt1_admin_name_replacements"] 319 | output = admintwo.output_admin_name_replacements() 320 | assert output == ["COD| city: "] 321 | assert admintwo.get_pcode("COD", "Mbanza-Ngungu city", logname="test") == ( 322 | "CD2013", 323 | False, 324 | ) 325 | assert admintwo.get_pcode( 326 | "COD", "Mbanza-Ngungu city", parent="CD20", logname="test" 327 | ) == ("CD2013", False) 328 | assert admintwo.get_pcode( 329 | "COD", "Mbanza-Ngungu city", parent="CD19", logname="test" 330 | ) == (None, False) 331 | assert admintwo.get_pcode("COD", "Kenge city", logname="test") == ( 332 | "CD3102", 333 | False, 334 | ) 335 | assert admintwo.get_pcode( 336 | "COD", "Kenge city", parent="CD31", logname="test" 337 | ) == ("CD3102", False) 338 | assert admintwo.get_pcode("MWI", "Blantyre city", logname="test") == ( 339 | None, 340 | False, 341 | ) 342 | assert admintwo.get_pcode( 343 | "MWI", "Blantyre city", parent="MW3", logname="test" 344 | ) == (None, False) 345 | 346 | admintwo.admin_name_replacements = config_parent["alt2_admin_name_replacements"] 347 | output = admintwo.output_admin_name_replacements() 348 | assert output == ["CD20| city: "] 349 | assert admintwo.get_pcode("COD", "Mbanza-Ngungu city", logname="test") == ( 350 | None, 351 | False, 352 | ) 353 | assert admintwo.get_pcode( 354 | "COD", "Mbanza-Ngungu city", parent="CD20", logname="test" 355 | ) == ("CD2013", False) 356 | assert admintwo.get_pcode( 357 | "COD", "Mbanza-Ngungu city", parent="CD19", logname="test" 358 | ) == (None, False) 359 | assert admintwo.get_pcode("COD", "Kenge city", logname="test") == ( 360 | None, 361 | False, 362 | ) 363 | assert admintwo.get_pcode( 364 | "COD", "Kenge city", parent="CD31", logname="test" 365 | ) == (None, False) 366 | assert admintwo.get_pcode("MWI", "Blantyre city", logname="test") == ( 367 | None, 368 | False, 369 | ) 370 | assert admintwo.get_pcode( 371 | "MWI", "Blantyre city", parent="MW3", logname="test" 372 | ) == (None, False) 373 | 374 | def test_adminlevel_with_url(self, config, url, fixtures_dir): 375 | adminone = AdminLevel(config, admin_level_overrides={"YEM": 5}) 376 | assert adminone.get_admin_level("YEM") == 5 377 | with pytest.raises(FileNotFoundError): 378 | adminone.setup_from_url("fake_url") 379 | adminone = AdminLevel(config) 380 | AdminLevel.set_default_admin_url() 381 | assert AdminLevel.admin_url == AdminLevel._admin_url_default 382 | AdminLevel.set_default_admin_url(url) 383 | assert AdminLevel.admin_url == url 384 | adminone.setup_from_url(countryiso3s=("YEM",)) 385 | assert len(adminone.get_pcode_list()) == 22 386 | 387 | with temp_dir( 388 | "TestAdminLevelRetriever", 389 | delete_on_success=True, 390 | delete_on_failure=False, 391 | ) as tempdir: 392 | with Download(user_agent="test") as downloader: 393 | retriever = Retrieve( 394 | downloader, 395 | tempdir, 396 | fixtures_dir, 397 | tempdir, 398 | save=False, 399 | use_saved=False, 400 | ) 401 | adminone = AdminLevel(config, retriever=retriever) 402 | with pytest.raises(DownloadError): 403 | adminone.setup_from_url("fake_url") 404 | retriever = Retrieve( 405 | downloader, 406 | tempdir, 407 | fixtures_dir, 408 | tempdir, 409 | save=False, 410 | use_saved=True, 411 | ) 412 | adminone = AdminLevel(config, retriever=retriever) 413 | with pytest.raises(FileNotFoundError): 414 | adminone.setup_from_url("fake_url") 415 | adminone.setup_from_url(countryiso3s=("YEM",)) 416 | assert len(adminone.get_pcode_list()) == 22 417 | 418 | adminone = AdminLevel(config) 419 | adminone.setup_from_url() 420 | assert adminone.get_admin_level("YEM") == 1 421 | assert len(adminone.get_pcode_list()) == 2455 422 | assert adminone.get_pcode_length("YEM") == 4 423 | assert adminone.get_pcode("YEM", "YE30", logname="test") == ( 424 | "YE30", 425 | True, 426 | ) 427 | assert adminone.get_pcode("YEM", "YE30", parent="YEM", logname="test") == ( 428 | "YE30", 429 | True, 430 | ) 431 | # Exact match of p-code so doesn't need parent 432 | assert adminone.get_pcode("YEM", "YE30", parent="Blah1", logname="test") == ( 433 | "YE30", 434 | True, 435 | ) 436 | assert adminone.get_pcode("YEM", "YEM30", logname="test") == ( 437 | "YE30", 438 | True, 439 | ) 440 | assert adminone.get_pcode("YEM", "YEM030", logname="test") == ( 441 | "YE30", 442 | True, 443 | ) 444 | assert adminone.get_pcode("NGA", "NG015", logname="test") == ( 445 | "NG015", 446 | True, 447 | ) 448 | assert adminone.get_pcode("NGA", "NG15", logname="test") == ( 449 | "NG015", 450 | True, 451 | ) 452 | assert adminone.get_pcode("NGA", "NGA015", logname="test") == ( 453 | "NG015", 454 | True, 455 | ) 456 | assert adminone.get_pcode("NER", "NER004", logname="test") == ( 457 | "NE004", 458 | True, 459 | ) 460 | assert adminone.get_pcode("NER", "NE04", logname="test") == ( 461 | "NE004", 462 | True, 463 | ) 464 | assert adminone.get_pcode("NER", "NE004", logname="test") == ( 465 | "NE004", 466 | True, 467 | ) 468 | assert adminone.get_pcode("ABC", "NE004", logname="test") == ( 469 | "NE004", 470 | True, 471 | ) 472 | assert adminone.get_pcode("ABC", "NER004", logname="test") == ( 473 | None, 474 | True, 475 | ) 476 | assert adminone.get_pcode("ABC", "BLAH", logname="test") == ( 477 | None, 478 | False, 479 | ) 480 | config["countries_fuzzy_try"].append("ABC") 481 | assert adminone.get_pcode("ABC", "NER004", logname="test") == ( 482 | None, 483 | True, 484 | ) 485 | assert adminone.get_pcode("ABC", "BLAH", logname="test") == ( 486 | None, 487 | False, 488 | ) 489 | assert adminone.get_pcode("XYZ", "XYZ123", logname="test") == ( 490 | None, 491 | True, 492 | ) 493 | assert adminone.get_pcode("XYZ", "BLAH", logname="test") == ( 494 | None, 495 | False, 496 | ) 497 | assert adminone.get_pcode("NER", "ABCDEFGH", logname="test") == ( 498 | None, 499 | False, 500 | ) 501 | assert adminone.get_pcode("YEM", "Ad Dali", logname="test") == ( 502 | "YE30", 503 | True, 504 | ) 505 | assert adminone.get_pcode("YEM", "Ad Dal", logname="test") == ( 506 | "YE30", 507 | False, 508 | ) 509 | assert adminone.get_pcode("YEM", "Ad Dal", parent="YEM", logname="test") == ( 510 | "YE30", 511 | False, 512 | ) 513 | # Invalid parent means fuzzy matching won't match 514 | assert adminone.get_pcode("YEM", "Ad Dal", parent="Blah2", logname="test") == ( 515 | None, 516 | False, 517 | ) 518 | assert adminone.get_pcode("YEM", "nord", logname="test") == ( 519 | None, 520 | False, 521 | ) 522 | assert adminone.get_pcode("NGA", "FCT (Abuja)", logname="test") == ( 523 | "NG015", 524 | True, 525 | ) 526 | assert adminone.get_pcode("UKR", "Chernihiv Oblast", logname="test") == ( 527 | "UA74", 528 | False, 529 | ) 530 | assert adminone.get_pcode("ZWE", "ABCDEFGH", logname="test") == ( 531 | None, 532 | False, 533 | ) 534 | output = adminone.output_matches() 535 | assert output == [ 536 | "test - NER: Matching (pcode length conversion) NE004 to Maradi on map", 537 | "test - NGA: Matching (pcode length conversion) NG015 to Federal Capital Territory on map", 538 | "test - UKR: Matching (substring) Chernihiv Oblast to Chernihivska on map", 539 | "test - YEM: Matching (substring) Ad Dal to Ad Dali' on map", 540 | "test - YEM: Matching (pcode length conversion) YE30 to Ad Dali' on map", 541 | ] 542 | output = adminone.output_ignored() 543 | assert output == [ 544 | "test - Ignored ABC!", 545 | "test - Ignored XYZ!", 546 | "test - YEM: Ignored nord!", 547 | "test - Ignored ZWE!", 548 | ] 549 | output = adminone.output_errors() 550 | assert output == [ 551 | "test - Could not find ABC in map names!", 552 | "test - NER: Could not find ABCDEFGH in map names!", 553 | "test - YEM: Could not find Blah2 in map names!", 554 | ] 555 | 556 | def test_adminlevel_pcode_formats(self, config, url, formats_url): 557 | adminone = AdminLevel(config) 558 | adminone.setup_from_url(admin_url=url) 559 | adminone.load_pcode_formats(formats_url=formats_url) 560 | assert adminone.convert_admin_pcode_length("YEM", "YEME123") is None 561 | assert adminone.get_pcode("YEM", "YE30", logname="test") == ( 562 | "YE30", 563 | True, 564 | ) 565 | assert adminone.get_pcode("YEM", "YEM30", logname="test") == ( 566 | "YE30", 567 | True, 568 | ) 569 | assert adminone.get_pcode("YEM", "YEM030", logname="test") == ( 570 | "YE30", 571 | True, 572 | ) 573 | assert adminone.get_pcode("NGA", "NG015", logname="test") == ( 574 | "NG015", 575 | True, 576 | ) 577 | assert adminone.get_pcode("NGA", "NG15", logname="test") == ( 578 | "NG015", 579 | True, 580 | ) 581 | assert adminone.get_pcode("NGA", "NGA015", logname="test") == ( 582 | "NG015", 583 | True, 584 | ) 585 | assert adminone.get_pcode("NER", "NE004", logname="test") == ( 586 | "NE004", 587 | True, 588 | ) 589 | assert adminone.get_pcode("NER", "NE04", logname="test") == ( 590 | "NE004", 591 | True, 592 | ) 593 | assert adminone.get_pcode("NER", "NER004", logname="test") == ( 594 | "NE004", 595 | True, 596 | ) 597 | assert adminone.get_pcode("ABC", "NER004", logname="test") == ( 598 | None, 599 | True, 600 | ) 601 | 602 | admintwo = AdminLevel(config, admin_level=2) 603 | admintwo.setup_from_url(admin_url=url) 604 | assert admintwo.pcode_to_parent["YE3001"] == "YE30" 605 | assert admintwo.get_pcode("YEM", "YE03001", logname="test") == ( 606 | None, 607 | True, 608 | ) 609 | 610 | admintwo.load_pcode_formats(formats_url=formats_url) 611 | assert admintwo.get_pcode("YEM", "YE3001", logname="test") == ( 612 | "YE3001", 613 | True, 614 | ) 615 | assert admintwo.get_pcode("YEM", "YEM3001", logname="test") == ( 616 | "YE3001", 617 | True, 618 | ) 619 | assert admintwo.get_pcode("YEM", "YEM3001", parent="Blah", logname="test") == ( 620 | "YE3001", 621 | True, 622 | ) 623 | assert admintwo.get_pcode("YEM", "YEM03001", logname="test") == ( 624 | "YE3001", 625 | True, 626 | ) 627 | assert admintwo.get_pcode("YEM", "YE301", logname="test") == ( 628 | "YE3001", 629 | True, 630 | ) 631 | assert admintwo.get_pcode("YEM", "YEM30001", logname="test") == ( 632 | "YE3001", 633 | True, 634 | ) 635 | assert admintwo.get_pcode("YEM", "YEM030001", logname="test") == ( 636 | "YE3001", 637 | True, 638 | ) 639 | assert admintwo.get_pcode("NGA", "NG015001", logname="test") == ( 640 | "NG015001", 641 | True, 642 | ) 643 | assert admintwo.get_pcode("NGA", "NG15001", logname="test") == ( 644 | "NG015001", 645 | True, 646 | ) 647 | assert admintwo.get_pcode("NGA", "NGA015001", logname="test") == ( 648 | "NG015001", 649 | True, 650 | ) 651 | assert admintwo.get_pcode("NGA", "NG1501", logname="test") == ( 652 | "NG015001", 653 | True, 654 | ) 655 | assert admintwo.get_pcode("NGA", "NG3614", logname="test") == ( 656 | "NG036014", 657 | True, 658 | ) 659 | # Algorithm inserts 0 to make NG001501 and hence fails (NG001 is in any 660 | # case a valid admin 1) 661 | assert admintwo.get_pcode("NGA", "NG01501", logname="test") == ( 662 | None, 663 | True, 664 | ) 665 | # Algorithm can only insert one zero per admin level right now 666 | assert admintwo.get_pcode("NGA", "NG0151", logname="test") == ( 667 | None, 668 | True, 669 | ) 670 | assert admintwo.get_pcode("NGA", "NG151", logname="test") == ( 671 | None, 672 | True, 673 | ) 674 | assert admintwo.get_pcode("NER", "NER004009", logname="test") == ( 675 | "NE004009", 676 | True, 677 | ) 678 | assert admintwo.get_pcode("NER", "NE04009", logname="test") == ( 679 | "NE004009", 680 | True, 681 | ) 682 | # Algorithm inserts 0 to make NER000409 and hence fails (it has no 683 | # knowledge that NER000 is an invalid admin 1) 684 | assert admintwo.get_pcode("NER", "NE00409", logname="test") == ( 685 | None, 686 | True, 687 | ) 688 | 689 | assert admintwo.get_pcode("DZA", "DZ009009", logname="test") == ( 690 | "DZ009009", 691 | True, 692 | ) 693 | assert admintwo.get_pcode("DZA", "DZ0090009", logname="test") == ( 694 | "DZ009009", 695 | True, 696 | ) 697 | 698 | assert admintwo.get_pcode("COL", "CO08849", logname="test") == ( 699 | "CO08849", 700 | True, 701 | ) 702 | # Algorithm removes 0 to make CO80849 and hence fails (it has no 703 | # knowledge that CO80 is an invalid admin 1) 704 | assert admintwo.get_pcode("COL", "CO080849", logname="test") == ( 705 | None, 706 | True, 707 | ) 708 | assert admintwo.get_pcode("NER", "NE00409", parent="blah", logname="test") == ( 709 | None, 710 | True, 711 | ) 712 | 713 | admintwo.set_parent_admins_from_adminlevels([adminone]) 714 | # The lookup in admin1 reveals that adding a 0 prefix to the admin1 715 | # is not a valid admin1 (NER000) so the algorithm tries adding 716 | # the 0 prefix at the admin2 level instead and hence succeeds 717 | assert admintwo.get_pcode("NER", "NE00409", logname="test") == ( 718 | "NE004009", 719 | True, 720 | ) 721 | # we don't use the parent because it could have a pcode length issue 722 | # itself 723 | assert admintwo.get_pcode("NER", "NE00409", parent="blah", logname="test") == ( 724 | "NE004009", 725 | True, 726 | ) 727 | # The lookup in admin1 reveals that removing the 0 prefix from the 728 | # admin1 is not a valid admin1 (CO80849) so the algorithm tries 729 | # removing the 0 prefix at the admin2 level instead and hence succeeds 730 | assert admintwo.get_pcode("COL", "CO080849", logname="test") == ( 731 | "CO08849", 732 | True, 733 | ) 734 | 735 | admintwo.set_parent_admins([adminone.pcodes]) 736 | assert admintwo.get_pcode("YEM", "YEM03001", logname="test") == ( 737 | "YE3001", 738 | True, 739 | ) 740 | assert admintwo.get_pcode("NGA", "NG1501", logname="test") == ( 741 | "NG015001", 742 | True, 743 | ) 744 | assert admintwo.get_pcode("JAM", "JM10001", logname="test") == ( 745 | "JM10001", 746 | True, 747 | ) 748 | assert admintwo.get_pcode("JAM", "JAM10001", logname="test") == ( 749 | "JM10001", 750 | True, 751 | ) 752 | -------------------------------------------------------------------------------- /tests/hdx/location/test_currency.py: -------------------------------------------------------------------------------- 1 | """Currency Tests""" 2 | 3 | from os.path import join 4 | 5 | import pytest 6 | 7 | from hdx.location.currency import Currency, CurrencyError 8 | from hdx.location.int_timestamp import get_int_timestamp 9 | from hdx.utilities.dateparse import parse_date 10 | from hdx.utilities.downloader import Download 11 | from hdx.utilities.path import get_temp_dir 12 | from hdx.utilities.retriever import Retrieve 13 | from hdx.utilities.useragent import UserAgent 14 | 15 | 16 | class TestCurrency: 17 | @pytest.fixture(scope="class") 18 | def secondary_rates_url(self, fixtures_dir): 19 | return join(fixtures_dir, "secondary_rates.json") 20 | 21 | @pytest.fixture(scope="class") 22 | def secondary_historic_url(self, fixtures_dir): 23 | return join(fixtures_dir, "secondary_historic_rates.csv") 24 | 25 | @pytest.fixture(scope="class", autouse=True) 26 | def retrievers(self, fixtures_dir): 27 | name = "hdx-python-country-rates" 28 | UserAgent.set_global(name) 29 | downloader = Download() 30 | fallback_dir = fixtures_dir 31 | temp_dir = get_temp_dir(name) 32 | retriever = Retrieve( 33 | downloader, 34 | fallback_dir, 35 | temp_dir, 36 | temp_dir, 37 | save=False, 38 | use_saved=False, 39 | ) 40 | retriever_broken = Retrieve( 41 | downloader, 42 | "tests", 43 | temp_dir, 44 | temp_dir, 45 | save=False, 46 | use_saved=False, 47 | ) 48 | yield retriever, retriever_broken 49 | UserAgent.clear_global() 50 | 51 | def test_get_current_value_in_usd( 52 | self, reset_currency, retrievers, secondary_rates_url 53 | ): 54 | Currency.setup(no_historic=True) 55 | assert Currency.get_current_value_in_usd(10, "usd") == 10 56 | assert Currency.get_current_value_in_currency(10, "usd") == 10 57 | gbprate = Currency.get_current_value_in_usd(10, "gbp") 58 | assert gbprate != 10 59 | assert Currency.get_current_value_in_currency(gbprate, "GBP") == 10 60 | xdrrate = Currency.get_current_value_in_usd(10, "xdr") 61 | assert xdrrate != 10 62 | assert Currency.get_current_value_in_currency(xdrrate, "xdr") == 10 63 | with pytest.raises(CurrencyError): 64 | Currency.get_current_value_in_usd(10, "XYZ") 65 | with pytest.raises(CurrencyError): 66 | Currency.get_current_value_in_currency(10, "XYZ") 67 | retriever = retrievers[0] 68 | Currency.setup( 69 | retriever=retriever, 70 | primary_rates_url="fail", 71 | secondary_rates_url="fail", 72 | fallback_current_to_static=True, 73 | no_historic=True, 74 | ) 75 | assert Currency.get_current_value_in_usd(10, "gbp") == 12.076629158709528 76 | assert Currency.get_current_value_in_currency(10, "gbp") == 8.280456299999999 77 | with pytest.raises(CurrencyError): 78 | Currency.get_current_value_in_usd(10, "XYZ") 79 | with pytest.raises(CurrencyError): 80 | Currency.get_current_value_in_currency(10, "XYZ") 81 | Currency.setup( 82 | no_historic=True, 83 | primary_rates_url="fail", 84 | secondary_rates_url=secondary_rates_url, 85 | ) 86 | xdrrate = Currency.get_current_value_in_usd(10, "xdr") 87 | assert xdrrate == 13.075473660667791 88 | with pytest.raises(CurrencyError): 89 | Currency.setup( 90 | retriever=retriever, 91 | primary_rates_url="fail", 92 | secondary_rates_url="fail", 93 | fallback_current_to_static=False, 94 | no_historic=True, 95 | ) 96 | Currency.get_current_value_in_currency(10, "gbp") 97 | with pytest.raises(CurrencyError): 98 | Currency.setup( 99 | retriever=retrievers[1], 100 | primary_rates_url="fail", 101 | secondary_rates_url="fail", 102 | fallback_current_to_static=True, 103 | no_historic=True, 104 | ) 105 | Currency.get_current_value_in_currency(10, "gbp") 106 | Currency._rates_api = None 107 | with pytest.raises(CurrencyError): 108 | Currency.get_current_rate("gbp") 109 | assert Currency.get_current_rate("usd") == 1 110 | 111 | Currency._cached_current_rates = {} 112 | Currency._cached_historic_rates = {} 113 | Currency._rates_api = "" 114 | Currency._secondary_rates = {} 115 | Currency._secondary_historic_rates = {} 116 | Currency._fallback_to_current = False 117 | Currency._no_historic = False 118 | Currency.setup(no_historic=True) 119 | rate1gbp = Currency.get_current_rate("gbp") 120 | assert rate1gbp != 1 121 | rate1xdr = Currency.get_current_rate("xdr") 122 | assert rate1xdr != 1 123 | Currency.setup( 124 | retriever=retrievers[1], 125 | primary_rates_url="fail", 126 | fallback_current_to_static=False, 127 | no_historic=True, 128 | ) 129 | rate2gbp = Currency.get_current_rate("gbp") 130 | assert rate2gbp != 1 131 | assert abs(rate1gbp - rate2gbp) / rate1gbp < 0.008 132 | Currency.setup( 133 | retriever=retrievers[1], 134 | primary_rates_url="fail", 135 | fallback_current_to_static=False, 136 | no_historic=True, 137 | ) 138 | rate2xdr = Currency.get_current_rate("xdr") 139 | assert rate2xdr != 1 140 | assert abs(rate1xdr - rate2xdr) / rate1xdr < 0.1 141 | 142 | def test_get_current_value_in_usd_fixednnow( 143 | self, reset_currency, retrievers, secondary_rates_url 144 | ): 145 | date = parse_date("2020-02-20") 146 | Currency.setup( 147 | no_historic=True, 148 | fixed_now=date, 149 | secondary_rates_url=secondary_rates_url, 150 | ) 151 | assert Currency.get_current_rate("usd") == 1 152 | assert Currency.get_current_value_in_usd(10, "USD") == 10 153 | assert Currency.get_current_value_in_currency(10, "usd") == 10 154 | assert Currency.get_current_rate("gbp") == 0.7735000252723694 155 | # falls back to secondary current rates 156 | assert Currency.get_current_rate("xdr") == 0.76479065 157 | 158 | def test_get_historic_value_in_usd( 159 | self, reset_currency, retrievers, secondary_historic_url 160 | ): 161 | Currency._no_historic = False 162 | Currency.setup(secondary_historic_url=secondary_historic_url) 163 | date = parse_date("2020-02-20") 164 | assert Currency.get_historic_rate("usd", date) == 1 165 | assert Currency.get_historic_value_in_usd(10, "USD", date) == 10 166 | assert Currency.get_historic_value_in_currency(10, "usd", date) == 10 167 | assert Currency.get_historic_rate("gbp", date) == 0.7735000252723694 168 | # falls back to secondary historic rates 169 | assert Currency.get_historic_rate("xdr", date) == 0.7275806206896552 170 | assert ( 171 | Currency.get_historic_rate( 172 | "gbp", 173 | parse_date("2020-02-20 00:00:00 NZST", timezone_handling=2), 174 | ignore_timeinfo=False, 175 | ) 176 | == 0.76910001039505 177 | ) 178 | assert ( 179 | Currency.get_historic_rate( 180 | "gbp", 181 | parse_date("2020-02-19"), 182 | ) 183 | == 0.76910001039505 184 | ) 185 | assert Currency.get_historic_value_in_usd(10, "gbp", date) == 12.928247799964508 186 | assert ( 187 | Currency.get_historic_value_in_currency(10, "gbp", date) 188 | == 7.735000252723694 189 | ) 190 | with pytest.raises(CurrencyError): 191 | Currency.get_historic_value_in_usd(10, "XYZ", date) 192 | with pytest.raises(CurrencyError): 193 | Currency.get_historic_value_in_currency(10, "XYZ", date) 194 | Currency.setup( 195 | primary_rates_url="fail", 196 | secondary_historic_url="fail", 197 | fallback_historic_to_current=True, 198 | ) 199 | gbprate = Currency.get_historic_value_in_usd(1, "gbp", date) 200 | assert gbprate == Currency.get_current_value_in_usd(1, "gbp") 201 | gbprate = Currency.get_historic_value_in_currency(1, "gbp", date) 202 | assert gbprate == Currency.get_current_value_in_currency(1, "gbp") 203 | retriever = retrievers[0] 204 | Currency.setup( 205 | retriever=retriever, 206 | primary_rates_url="fail", 207 | secondary_rates_url="fail", 208 | secondary_historic_url="fail", 209 | fallback_historic_to_current=True, 210 | fallback_current_to_static=True, 211 | ) 212 | assert Currency.get_historic_value_in_usd(10, "gbp", date) == 12.076629158709528 213 | Currency.setup( 214 | retriever=retriever, 215 | secondary_historic_url=secondary_historic_url, 216 | primary_rates_url="fail", 217 | secondary_rates_url="fail", 218 | fallback_historic_to_current=False, 219 | fallback_current_to_static=False, 220 | ) 221 | assert ( 222 | Currency.get_historic_rate("gbp", parse_date("2010-02-20")) 223 | == 0.663042036865137 224 | ) 225 | assert ( 226 | Currency.get_historic_rate("gbp", parse_date("2030-02-20")) 227 | == 0.745156482861401 228 | ) 229 | assert ( 230 | Currency.get_historic_rate("gbp", parse_date("2020-01-31")) 231 | == 0.761817697025102 232 | ) 233 | with pytest.raises(CurrencyError): 234 | Currency.setup( 235 | retriever=retriever, 236 | primary_rates_url="fail", 237 | secondary_historic_url="fail", 238 | fallback_historic_to_current=False, 239 | ) 240 | Currency.get_historic_value_in_usd(10, "gbp", date) 241 | Currency.setup( 242 | retriever=retriever, 243 | primary_rates_url="fail", 244 | fallback_historic_to_current=False, 245 | ) 246 | # Interpolation 247 | # 0.761817697025102 + (0.776276975624903 - 0.761817697025102) * 20 / 29 248 | # 0.761817697025102 + (0.776276975624903 - 0.761817697025102) * (1582156800-1580428800) / (1582934400 - 1580428800) 249 | assert Currency.get_historic_rate("gbp", date) == 0.7717896133008268 250 | 251 | def test_broken_rates_no_secondary(self, reset_currency, retrievers): 252 | Currency._no_historic = False 253 | Currency.setup(secondary_historic_url="fail") 254 | # Without the checking against high and low returned by Yahoo API, this 255 | # returned 3.140000104904175 256 | assert Currency.get_historic_rate("NGN", parse_date("2017-02-15")) == 314.5 257 | # Without the checking against high and low returned by Yahoo API, this 258 | # returned 0.10000000149011612 259 | assert ( 260 | Currency.get_historic_rate("YER", parse_date("2016-09-15")) 261 | == 249.7249984741211 262 | ) 263 | 264 | # This adjclose value which is the same as the low is wrong! 265 | # The high and open are very different so exception is raised 266 | with pytest.raises(CurrencyError): 267 | Currency.get_historic_rate("COP", parse_date("2015-12-15")) 268 | # Since the adjclose is not too different from the low and high, 269 | # despite being outside their range, we use adjclose 270 | assert ( 271 | Currency.get_historic_rate("XAF", parse_date("2022-04-14")) 272 | == 605.5509643554688 273 | ) 274 | # Since the adjclose is not too different from the low and high, 275 | # despite being outside their range, we use adjclose 276 | assert ( 277 | Currency.get_historic_rate("XAF", parse_date("2022-04-15")) 278 | == 601.632568359375 279 | ) 280 | 281 | def test_broken_rates_with_secondary( 282 | self, reset_currency, retrievers, secondary_historic_url 283 | ): 284 | Currency._no_historic = False 285 | Currency.setup(secondary_historic_url=secondary_historic_url) 286 | # Without the checking against secondary historic rate, this 287 | # returned 3.140000104904175 288 | assert Currency.get_historic_rate("NGN", parse_date("2017-02-15")) == 314.5 289 | # Without the checking against secondary historic rate, this 290 | # returned 0.10000000149011612 291 | assert ( 292 | Currency.get_historic_rate("YER", parse_date("2016-09-15")) 293 | == 249.7249984741211 294 | ) 295 | # Without the checking against secondary historic rate, this 296 | # returned 33.13999938964844 297 | assert ( 298 | Currency.get_historic_rate("COP", parse_date("2015-12-15")) 299 | == 3269.199951171875 300 | ) 301 | 302 | # Since the adjclose is not too different from the secondary historic 303 | # rate, we use adjclose 304 | assert ( 305 | Currency.get_historic_rate("XAF", parse_date("2022-04-14")) 306 | == 605.5509643554688 307 | ) 308 | # Since the adjclose is not too different from the secondary historic 309 | # rate, we use adjclose 310 | assert ( 311 | Currency.get_historic_rate("XAF", parse_date("2022-04-15")) 312 | == 601.632568359375 313 | ) 314 | 315 | def test_get_adjclose(self, retrievers, secondary_historic_url): 316 | Currency._no_historic = False 317 | Currency.setup(secondary_historic_url="fail") 318 | indicators = { 319 | "adjclose": [{"adjclose": [3.140000104904175]}], 320 | "quote": [ 321 | { 322 | "close": [3.140000104904175], 323 | "high": [315.0], 324 | "low": [314.0], 325 | "open": [315.0], 326 | "volume": [0], 327 | } 328 | ], 329 | } 330 | timestamp = get_int_timestamp(parse_date("2017-02-15")) 331 | assert Currency._get_adjclose(indicators, "NGN", timestamp) == 314.5 332 | indicators = { 333 | "adjclose": [{"adjclose": [33.13999938964844]}], 334 | "quote": [ 335 | { 336 | "close": [33.13999938964844], 337 | "high": [3320.0], 338 | "low": [33.13999938964844], 339 | "open": [3269.199951171875], 340 | "volume": [0], 341 | } 342 | ], 343 | } 344 | timestamp = get_int_timestamp(parse_date("2015-12-15")) 345 | assert Currency._get_adjclose(indicators, "COP", timestamp) is None 346 | indicators = { 347 | "adjclose": [{"adjclose": [605.5509643554688]}], 348 | "quote": [ 349 | { 350 | "close": [605.5509643554688], 351 | "high": [602.6080932617188], 352 | "low": [601.632568359375], 353 | "open": [602.6080932617188], 354 | "volume": [0], 355 | } 356 | ], 357 | } 358 | timestamp = get_int_timestamp(parse_date("2022-04-14")) 359 | assert Currency._get_adjclose(indicators, "XAF", timestamp) == 605.5509643554688 360 | indicators = { 361 | "adjclose": [{"adjclose": [601.632568359375]}], 362 | "quote": [ 363 | { 364 | "close": [601.632568359375], 365 | "high": [606.8197631835938], 366 | "low": [606.8197631835938], 367 | "open": [606.8197631835938], 368 | "volume": [0], 369 | } 370 | ], 371 | } 372 | timestamp = get_int_timestamp(parse_date("2022-04-15")) 373 | assert Currency._get_adjclose(indicators, "XAF", timestamp) == 601.632568359375 374 | indicators = { 375 | "adjclose": [{"adjclose": [314.0000104904175]}], 376 | "quote": [ 377 | { 378 | "close": [314.0000104904175], 379 | "high": [3.150], 380 | "low": [3.140], 381 | "open": [3.150], 382 | "volume": [0], 383 | } 384 | ], 385 | } 386 | timestamp = get_int_timestamp(parse_date("2017-02-15")) 387 | assert Currency._get_adjclose(indicators, "XXX", timestamp) == 3.145 388 | 389 | Currency.setup(secondary_historic_url=secondary_historic_url) 390 | indicators = { 391 | "adjclose": [{"adjclose": [3.140000104904175]}], 392 | "quote": [ 393 | { 394 | "close": [3.140000104904175], 395 | "high": [315.0], 396 | "low": [314.0], 397 | "open": [315.0], 398 | "volume": [0], 399 | } 400 | ], 401 | } 402 | timestamp = get_int_timestamp(parse_date("2017-02-15")) 403 | assert Currency._get_adjclose(indicators, "NGN", timestamp) == 314.5 404 | indicators = { 405 | "adjclose": [{"adjclose": [33.13999938964844]}], 406 | "quote": [ 407 | { 408 | "close": [33.13999938964844], 409 | "high": [3320.0], 410 | "low": [33.13999938964844], 411 | "open": [3269.199951171875], 412 | "volume": [0], 413 | } 414 | ], 415 | } 416 | timestamp = get_int_timestamp(parse_date("2015-12-15")) 417 | assert Currency._get_adjclose(indicators, "COP", timestamp) == 3269.199951171875 418 | indicators = { 419 | "adjclose": [{"adjclose": [605.5509643554688]}], 420 | "quote": [ 421 | { 422 | "close": [605.5509643554688], 423 | "high": [602.6080932617188], 424 | "low": [601.632568359375], 425 | "open": [602.6080932617188], 426 | "volume": [0], 427 | } 428 | ], 429 | } 430 | timestamp = get_int_timestamp(parse_date("2022-04-14")) 431 | assert Currency._get_adjclose(indicators, "XAF", timestamp) == 605.5509643554688 432 | indicators = { 433 | "adjclose": [{"adjclose": [601.632568359375]}], 434 | "quote": [ 435 | { 436 | "close": [601.632568359375], 437 | "high": [606.8197631835938], 438 | "low": [606.8197631835938], 439 | "open": [606.8197631835938], 440 | "volume": [0], 441 | } 442 | ], 443 | } 444 | timestamp = get_int_timestamp(parse_date("2022-04-15")) 445 | assert Currency._get_adjclose(indicators, "XAF", timestamp) == 601.632568359375 446 | indicators = { 447 | "adjclose": [{"adjclose": [314.0000104904175]}], 448 | "quote": [ 449 | { 450 | "close": [314.0000104904175], 451 | "high": [3.150], 452 | "low": [3.140], 453 | "open": [3.150], 454 | "volume": [0], 455 | } 456 | ], 457 | } 458 | timestamp = get_int_timestamp(parse_date("2017-02-15")) 459 | assert Currency._get_adjclose(indicators, "XXX", timestamp) == 3.145 460 | indicators = { 461 | "adjclose": [{"adjclose": [33.13999938964844]}], 462 | "quote": [ 463 | { 464 | "close": [33.13999938964844], 465 | "high": [3320.0], 466 | "low": [33.13999938964844], 467 | "open": [33.199951171875], 468 | "volume": [0], 469 | } 470 | ], 471 | } 472 | timestamp = get_int_timestamp(parse_date("2015-12-15")) 473 | assert Currency._get_adjclose(indicators, "COP", timestamp) == 3320.0 474 | indicators = { 475 | "adjclose": [{"adjclose": [33.13999938964844]}], 476 | "quote": [ 477 | { 478 | "close": [33.13999938964844], 479 | "high": [33.200], 480 | "low": [3313.999938964844], 481 | "open": [33.199951171875], 482 | "volume": [0], 483 | } 484 | ], 485 | } 486 | timestamp = get_int_timestamp(parse_date("2015-12-15")) 487 | assert Currency._get_adjclose(indicators, "COP", timestamp) == 3313.999938964844 488 | # Everything is wacky but the values are in the same order of magnitude 489 | # as each other so adjclose is assumed to be ok 490 | indicators = { 491 | "adjclose": [{"adjclose": [33.13999938964844]}], 492 | "quote": [ 493 | { 494 | "close": [33.13999938964844], 495 | "high": [33.200], 496 | "low": [33.999938964844], 497 | "open": [33.199951171875], 498 | "volume": [0], 499 | } 500 | ], 501 | } 502 | timestamp = get_int_timestamp(parse_date("2015-12-15")) 503 | assert Currency._get_adjclose(indicators, "COP", timestamp) == 33.13999938964844 504 | # Everything is wacky and the values are not in the same order of 505 | # magnitude as each other so secondary historic rate is returned 506 | indicators = { 507 | "adjclose": [{"adjclose": [333.13999938964844]}], 508 | "quote": [ 509 | { 510 | "close": [333.13999938964844], 511 | "high": [33333.200], 512 | "low": [3.999938964844], 513 | "open": [33.199951171875], 514 | "volume": [0], 515 | } 516 | ], 517 | } 518 | timestamp = get_int_timestamp(parse_date("2015-12-15")) 519 | assert Currency._get_adjclose(indicators, "COP", timestamp) == 3124.504838709677 520 | # Everything is wacky but adjclose is in the same order of 521 | # magnitude as the secondary historic rate so return adjclose 522 | indicators = { 523 | "adjclose": [{"adjclose": [3270]}], 524 | "quote": [ 525 | { 526 | "close": [3270], 527 | "high": [33333.200], 528 | "low": [3.999938964844], 529 | "open": [33.199951171875], 530 | "volume": [0], 531 | } 532 | ], 533 | } 534 | timestamp = get_int_timestamp(parse_date("2015-12-15")) 535 | assert Currency._get_adjclose(indicators, "COP", timestamp) == 3270 536 | 537 | Currency._no_historic = True 538 | assert Currency._get_adjclose(indicators, "COP", timestamp) is None 539 | -------------------------------------------------------------------------------- /tests/hdx/location/test_wfp_exchangerates.py: -------------------------------------------------------------------------------- 1 | from hdx.location.currency import Currency 2 | from hdx.location.int_timestamp import get_int_timestamp 3 | from hdx.location.wfp_api import WFPAPI 4 | from hdx.location.wfp_exchangerates import WFPExchangeRates 5 | from hdx.utilities.dateparse import parse_date 6 | from hdx.utilities.downloader import Download 7 | from hdx.utilities.path import temp_dir 8 | from hdx.utilities.retriever import Retrieve 9 | 10 | 11 | class TestWFPExchangeRates: 12 | def test_wfp_exchangerates(self, reset_currency, input_dir): 13 | with temp_dir( 14 | "TestWFPExchangeRates", 15 | delete_on_success=True, 16 | delete_on_failure=False, 17 | ) as tempdir: 18 | with Download(user_agent="test") as downloader: 19 | retriever = Retrieve( 20 | downloader, 21 | tempdir, 22 | input_dir, 23 | tempdir, 24 | save=False, 25 | use_saved=True, 26 | ) 27 | currency = "afn" 28 | date = parse_date("2020-02-20") 29 | wfp_api = WFPAPI(downloader, retriever) 30 | wfp_api.update_retry_params(attempts=5, wait=5) 31 | wfp_fx = WFPExchangeRates(wfp_api) 32 | retry_params = wfp_fx.wfp_api.get_retry_params() 33 | assert retry_params["attempts"] == 5 34 | assert retry_params["wait"] == 5 35 | currenciesinfo = wfp_fx.get_currencies_info() 36 | assert len(currenciesinfo) == 127 37 | currencies = wfp_fx.get_currencies() 38 | assert len(currencies) == 127 39 | 40 | Currency.setup() 41 | assert Currency.get_historic_rate(currency, date) == 76.80000305175781 42 | timestamp = get_int_timestamp(date) 43 | historic_rates = wfp_fx.get_currency_historic_rates(currency) 44 | keys = list(historic_rates.keys()) 45 | sorted_keys = sorted(keys) 46 | assert keys == sorted_keys 47 | assert historic_rates[timestamp] == 77.01 48 | 49 | all_historic_rates = wfp_fx.get_historic_rates([currency]) 50 | Currency.setup( 51 | historic_rates_cache=all_historic_rates, 52 | secondary_historic_rates=all_historic_rates, 53 | use_secondary_historic=True, 54 | ) 55 | assert Currency.get_historic_rate(currency, date) == 77.01 56 | date = parse_date("2020-02-21") 57 | assert Currency.get_historic_rate(currency, date) == 77.01 58 | date = parse_date("2020-02-20 12:00:00") 59 | assert Currency.get_historic_rate(currency, date) == 77.01 60 | assert ( 61 | Currency.get_historic_rate(currency, date, ignore_timeinfo=False) 62 | == 77.01 63 | ) 64 | --------------------------------------------------------------------------------