hdx-python-country 3.6.8__tar.gz → 3.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/.config/pre-commit-config.yaml +2 -2
  2. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/.github/workflows/publish.yaml +2 -2
  3. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/.github/workflows/run-python-tests.yaml +2 -2
  4. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/CONTRIBUTING.md +3 -4
  5. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/PKG-INFO +6 -7
  6. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/README.md +1 -2
  7. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/documentation/main.md +8 -0
  8. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/pyproject.toml +3 -3
  9. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/requirements.txt +12 -12
  10. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/src/hdx/location/_version.py +2 -2
  11. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/src/hdx/location/adminlevel.py +106 -35
  12. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/src/hdx/location/wfp_exchangerates.py +1 -1
  13. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/fixtures/adminlevel.yaml +5 -0
  14. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/hdx/location/test_adminlevel.py +90 -1
  15. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/.config/coveragerc +0 -0
  16. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/.config/pytest.ini +0 -0
  17. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/.config/ruff.toml +0 -0
  18. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/.gitignore +0 -0
  19. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/LICENSE +0 -0
  20. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/documentation/.readthedocs.yaml +0 -0
  21. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/documentation/pydoc-markdown.yaml +0 -0
  22. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/src/hdx/location/Countries & Territories Taxonomy MVP - C&T Taxonomy with HXL Tags.csv +0 -0
  23. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/src/hdx/location/__init__.py +0 -0
  24. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/src/hdx/location/country.py +0 -0
  25. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/src/hdx/location/currency.py +0 -0
  26. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/src/hdx/location/names.py +0 -0
  27. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/src/hdx/location/phonetics.py +0 -0
  28. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/fixtures/global_pcode_lengths.csv +0 -0
  29. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/fixtures/global_pcodes_adm_1_2.csv +0 -0
  30. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/fixtures/secondary_historic_rates.csv +0 -0
  31. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/fixtures/secondary_rates.json +0 -0
  32. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/hdx/location/Countries_UZB_Deleted.csv +0 -0
  33. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/hdx/location/__init__.py +0 -0
  34. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/hdx/location/test_country.py +0 -0
  35. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/hdx/location/test_currency.py +0 -0
  36. {hdx_python_country-3.6.8 → hdx_python_country-3.7.0}/tests/hdx/location/test_wfp_exchangerates.py +0 -0
@@ -8,7 +8,7 @@ repos:
8
8
  - id: end-of-file-fixer
9
9
  - id: check-ast
10
10
  - repo: https://github.com/astral-sh/ruff-pre-commit
11
- rev: v0.3.0
11
+ rev: v0.3.2
12
12
  hooks:
13
13
  # Run the linter.
14
14
  - id: ruff
@@ -17,7 +17,7 @@ repos:
17
17
  - id: ruff-format
18
18
  args: [--config, .config/ruff.toml]
19
19
  - repo: https://github.com/jazzband/pip-tools
20
- rev: 7.4.0
20
+ rev: 7.4.1
21
21
  hooks:
22
22
  - id: pip-compile
23
23
  name: pip-compile requirements.txt
@@ -9,13 +9,13 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
 
11
11
  steps:
12
- - uses: actions/checkout@v3
12
+ - uses: actions/checkout@v4
13
13
  - name: Get history and tags for versioning to work
14
14
  run: |
15
15
  git fetch --prune --unshallow
16
16
  git fetch --depth=1 origin +refs/tags/*:refs/tags/*
17
17
  - name: Set up Python
18
- uses: actions/setup-python@v4
18
+ uses: actions/setup-python@v5
19
19
  with:
20
20
  python-version: '3.x'
21
21
  - name: Install dependencies
@@ -18,9 +18,9 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
 
20
20
  steps:
21
- - uses: actions/checkout@v3
21
+ - uses: actions/checkout@v4
22
22
  - name: Set up Python
23
- uses: actions/setup-python@v4
23
+ uses: actions/setup-python@v5
24
24
  with:
25
25
  python-version: '3.x'
26
26
  - name: Install dependencies
@@ -21,15 +21,14 @@ you make a git commit:
21
21
  pre-commit install
22
22
 
23
23
  The configuration file for this project is in a
24
- non-start location. Thus, you will need to edit your
24
+ non-standard location. Thus, you will need to edit your
25
25
  `.git/hooks/pre-commit` file to reflect this. Change
26
26
  the line that begins with `ARGS` to:
27
27
 
28
28
  ARGS=(hook-impl --config=.config/pre-commit-config.yaml --hook-type=pre-commit)
29
29
 
30
30
  With pre-commit, all code is formatted according to
31
- [black]("https://github.com/psf/black") and
32
- [ruff]("https://github.com/charliermarsh/ruff") guidelines.
31
+ [ruff](https://github.com/astral-sh/ruff) guidelines.
33
32
 
34
33
  To check if your changes pass pre-commit without committing, run:
35
34
 
@@ -41,7 +40,7 @@ To run the tests and view coverage, execute:
41
40
 
42
41
  pytest -c .config/pytest.ini --cov hdx --cov-config .config/coveragerc
43
42
 
44
- Follow the example set out already in ``api.rst`` as you write the documentation.
43
+ Follow the example set out already in ``documentation/main.md`` as you write the documentation.
45
44
 
46
45
  ## Packages
47
46
 
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: hdx-python-country
3
- Version: 3.6.8
3
+ Version: 3.7.0
4
4
  Summary: HDX Python country code and exchange rate (fx) utilities
5
5
  Project-URL: Homepage, https://github.com/OCHA-DAP/hdx-python-country
6
- Author-email: Michael Rans <rans@email.com>
6
+ Author: Michael Rans
7
7
  License: MIT
8
8
  License-File: LICENSE
9
9
  Keywords: HDX,country,country code,currencies,currency,exchange rate,foreign exchange,fx,iso 3166,iso2,iso3,location,region
@@ -25,8 +25,8 @@ Classifier: Programming Language :: Python :: 3.11
25
25
  Classifier: Programming Language :: Python :: 3.12
26
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
27
  Requires-Python: >=3.8
28
- Requires-Dist: hdx-python-utilities>=3.6.5
29
- Requires-Dist: libhxl>=5.2
28
+ Requires-Dist: hdx-python-utilities>=3.6.7
29
+ Requires-Dist: libhxl>=5.2.1
30
30
  Requires-Dist: pyphonetics
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: pre-commit; extra == 'dev'
@@ -37,8 +37,7 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  [![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)
39
39
  [![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)
40
- [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
41
- [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
40
+ [![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)
42
41
  [![Downloads](https://img.shields.io/pypi/dm/hdx-python-country.svg)](https://pypistats.org/packages/hdx-python-country)
43
42
 
44
43
  The HDX Python Country Library provides utilities to map between country and region
@@ -1,7 +1,6 @@
1
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
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
- [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
4
- [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
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)
5
4
  [![Downloads](https://img.shields.io/pypi/dm/hdx-python-country.svg)](https://pypistats.org/packages/hdx-python-country)
6
5
 
7
6
  The HDX Python Country Library provides utilities to map between country and region
@@ -126,6 +126,13 @@ Method *setup_from_admin_info* takes key *admin_info* which is a list with
126
126
  values of the form:
127
127
 
128
128
  {"iso3": "AFG", "pcode": "AF01", "name": "Kabul"}
129
+ {"iso3": "AFG", "pcode": "AF0101", "name": "Kabul", "parent": "AF01"}
130
+
131
+ Dictionaries *pcode_to_name* and *pcode_to_iso3* are populated in the
132
+ AdminLevel object. *parent* is optional, but if provided enables lookup of
133
+ location names by both country and parent rather than just country which should
134
+ help with any name clashes. It also results in the population of a dictionary
135
+ in the AdminLevel object *pcode_to_parent*.
129
136
 
130
137
  Method *setup_from_libhxl_dataset* takes a libhxl Dataset object, while
131
138
  *setup_from_url* takes a URL which defaults to a resource in the global p-codes
@@ -144,6 +151,7 @@ Examples of usage:
144
151
  adminlevel.get_pcode("YEM", "YEM030", logname="test") # returns ("YE30", True)
145
152
  adminlevel.get_pcode("YEM", "Al Dhale"e / الضالع") # returns ("YE30", False)
146
153
  adminlevel.get_pcode("YEM", "Al Dhale"e / الضالع", fuzzy_match=False) # returns (None, True)
154
+ assert admintwo.get_pcode("AFG", "Kabul", parent="AF01") == ("AF0101", True)
147
155
 
148
156
  There is basic admin 1 p-code length conversion by default. A more advanced
149
157
  p-code length conversion can be activated by calling *load_pcode_formats*
@@ -9,7 +9,7 @@ build-backend = "hatchling.build"
9
9
  [project]
10
10
  name = "hdx-python-country"
11
11
  description = "HDX Python country code and exchange rate (fx) utilities"
12
- authors = [{name = "Michael Rans", email = "rans@email.com"}]
12
+ authors = [{name = "Michael Rans"}]
13
13
  license = {text = "MIT"}
14
14
  keywords = ["HDX", "location", "country", "country code", "iso 3166", "iso2", "iso3", "region", "fx", "currency", "currencies", "exchange rate", "foreign exchange"]
15
15
  classifiers = [
@@ -34,8 +34,8 @@ classifiers = [
34
34
  requires-python = ">=3.8"
35
35
 
36
36
  dependencies = [
37
- "hdx-python-utilities>=3.6.5",
38
- "libhxl>=5.2",
37
+ "hdx-python-utilities>=3.6.7",
38
+ "libhxl>=5.2.1",
39
39
  "pyphonetics",
40
40
  ]
41
41
  dynamic = ["version"]
@@ -23,17 +23,17 @@ click==8.1.7
23
23
  # via typer
24
24
  colorama==0.4.6
25
25
  # via typer
26
- coverage[toml]==7.4.3
26
+ coverage[toml]==7.4.4
27
27
  # via pytest-cov
28
28
  distlib==0.3.8
29
29
  # via virtualenv
30
30
  et-xmlfile==1.1.0
31
31
  # via openpyxl
32
- filelock==3.13.1
32
+ filelock==3.13.3
33
33
  # via virtualenv
34
34
  frictionless==5.16.1
35
35
  # via hdx-python-utilities
36
- hdx-python-utilities==3.6.5
36
+ hdx-python-utilities==3.6.7
37
37
  # via hdx-python-country (pyproject.toml)
38
38
  humanize==4.9.0
39
39
  # via frictionless
@@ -73,9 +73,9 @@ nodeenv==1.8.0
73
73
  # via pre-commit
74
74
  openpyxl==3.1.2
75
75
  # via hdx-python-utilities
76
- packaging==23.2
76
+ packaging==24.0
77
77
  # via pytest
78
- petl==1.7.14
78
+ petl==1.7.15
79
79
  # via frictionless
80
80
  platformdirs==4.2.0
81
81
  # via virtualenv
@@ -85,9 +85,9 @@ ply==3.11
85
85
  # via
86
86
  # jsonpath-ng
87
87
  # libhxl
88
- pre-commit==3.6.2
88
+ pre-commit==3.7.0
89
89
  # via hdx-python-country (pyproject.toml)
90
- pydantic==2.6.3
90
+ pydantic==2.6.4
91
91
  # via frictionless
92
92
  pydantic-core==2.16.3
93
93
  # via pydantic
@@ -97,11 +97,11 @@ pyphonetics==0.5.3
97
97
  # via hdx-python-country (pyproject.toml)
98
98
  pyrsistent==0.20.0
99
99
  # via jsonschema
100
- pytest==8.1.0
100
+ pytest==8.1.1
101
101
  # via
102
102
  # hdx-python-country (pyproject.toml)
103
103
  # pytest-cov
104
- pytest-cov==4.1.0
104
+ pytest-cov==5.0.0
105
105
  # via hdx-python-country (pyproject.toml)
106
106
  python-dateutil==2.8.2
107
107
  # via
@@ -152,7 +152,7 @@ tabulate==0.9.0
152
152
  # via frictionless
153
153
  text-unidecode==1.3
154
154
  # via python-slugify
155
- typer[all]==0.9.0
155
+ typer[all]==0.10.0
156
156
  # via frictionless
157
157
  typing-extensions==4.10.0
158
158
  # via
@@ -168,11 +168,11 @@ urllib3==2.2.1
168
168
  # via
169
169
  # libhxl
170
170
  # requests
171
- validators==0.22.0
171
+ validators==0.24.0
172
172
  # via frictionless
173
173
  virtualenv==20.25.1
174
174
  # via pre-commit
175
- wheel==0.42.0
175
+ wheel==0.43.0
176
176
  # via libhxl
177
177
  xlrd==2.0.1
178
178
  # via hdx-python-utilities
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '3.6.8'
16
- __version_tuple__ = version_tuple = (3, 6, 8)
15
+ __version__ = version = '3.7.0'
16
+ __version_tuple__ = version_tuple = (3, 7, 0)
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  import re
3
- from typing import Dict, List, Optional, Tuple
3
+ from typing import Any, Dict, List, Optional, Tuple
4
4
 
5
5
  import hxl
6
6
  from hxl import InputOptions
@@ -61,9 +61,12 @@ class AdminLevel:
61
61
  self.pcodes = []
62
62
  self.pcode_lengths = {}
63
63
  self.name_to_pcode = {}
64
+ self.name_parent_to_pcode = {}
64
65
  self.pcode_to_name = {}
65
66
  self.pcode_to_iso3 = {}
67
+ self.pcode_to_parent = {}
66
68
  self.pcode_formats = {}
69
+ self.use_parent = False
67
70
  self.zeroes = {}
68
71
  self.parent_admins = []
69
72
 
@@ -120,15 +123,55 @@ class AdminLevel:
120
123
  )
121
124
  raise
122
125
 
126
+ def setup_row(
127
+ self,
128
+ countryiso3: str,
129
+ pcode: str,
130
+ adm_name: str,
131
+ parent: Optional[str],
132
+ ):
133
+ """
134
+ Setup a single p-code
135
+
136
+ Args:
137
+ countryiso3 (str): Country
138
+ pcode (str): P-code
139
+ adm_name (str): Administrative name
140
+ parent (Optional[str]): Parent p-code
141
+
142
+ Returns:
143
+ None
144
+ """
145
+ self.pcode_lengths[countryiso3] = len(pcode)
146
+ self.pcodes.append(pcode)
147
+ self.pcode_to_name[pcode] = adm_name
148
+
149
+ name_to_pcode = self.name_to_pcode.get(countryiso3, {})
150
+ name_to_pcode[unidecode(adm_name).lower()] = pcode
151
+ self.name_to_pcode[countryiso3] = name_to_pcode
152
+ self.pcode_to_iso3[pcode] = countryiso3
153
+ self.pcode_to_iso3[pcode] = countryiso3
154
+
155
+ if self.use_parent:
156
+ name_parent_to_pcode = self.name_parent_to_pcode.get(
157
+ countryiso3, {}
158
+ )
159
+ name_to_pcode = name_parent_to_pcode.get(parent, {})
160
+ name_to_pcode[unidecode(adm_name).lower()] = pcode
161
+ name_parent_to_pcode[parent] = name_to_pcode
162
+ self.name_parent_to_pcode[countryiso3] = name_parent_to_pcode
163
+ self.pcode_to_parent[pcode] = parent
164
+
123
165
  def setup_from_admin_info(
124
166
  self,
125
167
  admin_info: ListTuple[Dict],
126
168
  countryiso3s: Optional[ListTuple[str]] = None,
127
169
  ) -> None:
128
170
  """
129
- Setup p-codes from admin_info which is a list with values of the form:
171
+ Setup p-codes from admin_info which is a list with values of the form
172
+ below with parent optional:
130
173
  ::
131
- {"iso3": "AFG", "pcode": "AF01", "name": "Kabul"}
174
+ {"iso3": "AFG", "pcode": "AF0101", "name": "Kabul", parent: "AF01"}
132
175
  Args:
133
176
  admin_info (ListTuple[Dict]): p-code dictionary
134
177
  countryiso3s (Optional[ListTuple[str]]): Countries to read. Defaults to None (all).
@@ -140,19 +183,15 @@ class AdminLevel:
140
183
  countryiso3s = [
141
184
  countryiso3.upper() for countryiso3 in countryiso3s
142
185
  ]
186
+ self.use_parent = "parent" in admin_info[0]
143
187
  for row in admin_info:
144
188
  countryiso3 = row["iso3"].upper()
145
189
  if countryiso3s and countryiso3 not in countryiso3s:
146
190
  continue
147
191
  pcode = row.get("pcode").upper()
148
- self.pcodes.append(pcode)
149
- self.pcode_lengths[countryiso3] = len(pcode)
150
192
  adm_name = row["name"]
151
- self.pcode_to_name[pcode] = adm_name
152
- name_to_pcode = self.name_to_pcode.get(countryiso3, {})
153
- name_to_pcode[unidecode(adm_name).lower()] = pcode
154
- self.name_to_pcode[countryiso3] = name_to_pcode
155
- self.pcode_to_iso3[pcode] = countryiso3
193
+ parent = row.get("parent")
194
+ self.setup_row(countryiso3, pcode, adm_name, parent)
156
195
 
157
196
  def setup_from_libhxl_dataset(
158
197
  self,
@@ -176,19 +215,15 @@ class AdminLevel:
176
215
  countryiso3s = [
177
216
  countryiso3.upper() for countryiso3 in countryiso3s
178
217
  ]
218
+ self.use_parent = "#adm+code+parent" in admin_info.display_tags
179
219
  for row in admin_info:
180
220
  countryiso3 = row.get("#country+code").upper()
181
221
  if countryiso3s and countryiso3 not in countryiso3s:
182
222
  continue
183
223
  pcode = row.get("#adm+code").upper()
184
- self.pcodes.append(pcode)
185
- self.pcode_lengths[countryiso3] = len(pcode)
186
224
  adm_name = row.get("#adm+name")
187
- self.pcode_to_name[pcode] = adm_name
188
- name_to_pcode = self.name_to_pcode.get(countryiso3, {})
189
- name_to_pcode[unidecode(adm_name).lower()] = pcode
190
- self.name_to_pcode[countryiso3] = name_to_pcode
191
- self.pcode_to_iso3[pcode] = countryiso3
225
+ parent = row.get("#adm+code+parent")
226
+ self.setup_row(countryiso3, pcode, adm_name, parent)
192
227
 
193
228
  def setup_from_url(
194
229
  self,
@@ -304,7 +339,7 @@ class AdminLevel:
304
339
  self.errors = set()
305
340
 
306
341
  def convert_admin_pcode_length(
307
- self, countryiso3: str, pcode: str, logname: Optional[str] = None
342
+ self, countryiso3: str, pcode: str, **kwargs: Any
308
343
  ) -> Optional[str]:
309
344
  """Standardise pcode length by country and match to an internal pcode.
310
345
  Requires that p-code formats be loaded (eg. using load_pcode_formats)
@@ -312,11 +347,14 @@ class AdminLevel:
312
347
  Args:
313
348
  countryiso3 (str): ISO3 country code
314
349
  pcode (str): P code to match
315
- logname (Optional[str]): Identifying name to use when logging. Defaults to None (don't log).
350
+ **kwargs:
351
+ parent (Optional[str]): Parent admin code
352
+ logname (str): Log using this identifying name. Defaults to not logging.
316
353
 
317
354
  Returns:
318
355
  Optional[str]: Matched P code or None if no match
319
356
  """
357
+ logname = kwargs.get("logname")
320
358
  match = self.pcode_regex.match(pcode)
321
359
  if not match:
322
360
  return None
@@ -480,18 +518,24 @@ class AdminLevel:
480
518
  return None
481
519
 
482
520
  def fuzzy_pcode(
483
- self, countryiso3: str, name: str, logname: Optional[str] = None
521
+ self,
522
+ countryiso3: str,
523
+ name: str,
524
+ **kwargs: Any,
484
525
  ) -> Optional[str]:
485
526
  """Fuzzy match name to pcode
486
527
 
487
528
  Args:
488
529
  countryiso3 (str): Iso3 country code
489
530
  name (str): Name to match
490
- logname (Optional[str]): Identifying name to use when logging. Defaults to None (don't log).
531
+ **kwargs:
532
+ parent (Optional[str]): Parent admin code
533
+ logname (str): Log using this identifying name. Defaults to not logging.
491
534
 
492
535
  Returns:
493
536
  Optional[str]: Matched P code or None if no match
494
537
  """
538
+ logname = kwargs.get("logname")
495
539
  if (
496
540
  self.countries_fuzzy_try is not None
497
541
  and countryiso3 not in self.countries_fuzzy_try
@@ -499,11 +543,24 @@ class AdminLevel:
499
543
  if logname:
500
544
  self.ignored.add((logname, countryiso3))
501
545
  return None
502
- name_to_pcode = self.name_to_pcode.get(countryiso3)
503
- if not name_to_pcode:
504
- if logname:
505
- self.errors.add((logname, countryiso3))
506
- return None
546
+ if self.use_parent and "parent" in kwargs:
547
+ parent = kwargs["parent"]
548
+ name_parent_to_pcode = self.name_parent_to_pcode.get(countryiso3)
549
+ if not name_parent_to_pcode:
550
+ if logname:
551
+ self.errors.add((logname, countryiso3))
552
+ return None
553
+ name_to_pcode = name_parent_to_pcode.get(parent)
554
+ if not name_to_pcode:
555
+ if logname:
556
+ self.errors.add((logname, countryiso3, parent))
557
+ return None
558
+ else:
559
+ name_to_pcode = self.name_to_pcode.get(countryiso3)
560
+ if not name_to_pcode:
561
+ if logname:
562
+ self.errors.add((logname, countryiso3))
563
+ return None
507
564
  adm_name_lookup = clean_name(name)
508
565
  adm_name_lookup2 = multiple_replace(
509
566
  adm_name_lookup, self.admin_name_replacements
@@ -591,7 +648,7 @@ class AdminLevel:
591
648
  countryiso3: str,
592
649
  name: str,
593
650
  fuzzy_match: bool = True,
594
- logname: Optional[str] = None,
651
+ **kwargs: Any,
595
652
  ) -> Tuple[Optional[str], bool]:
596
653
  """Get pcode for a given name
597
654
 
@@ -599,7 +656,9 @@ class AdminLevel:
599
656
  countryiso3 (str): Iso3 country code
600
657
  name (str): Name to match
601
658
  fuzzy_match (bool): Whether to try fuzzy matching. Defaults to True.
602
- logname (Optional[str]): Identifying name to use when logging. Defaults to None (don't log).
659
+ **kwargs:
660
+ parent (Optional[str]): Parent admin code
661
+ logname (str): Log using this identifying name. Defaults to not logging.
603
662
 
604
663
  Returns:
605
664
  Tuple[Optional[str], bool]: (Matched P code or None if no match, True if exact match or False if not)
@@ -614,18 +673,30 @@ class AdminLevel:
614
673
  # name looks like a p-code, but doesn't match p-codes
615
674
  # so try adjusting p-code length
616
675
  pcode = self.convert_admin_pcode_length(
617
- countryiso3, pcode, logname
676
+ countryiso3, pcode, **kwargs
618
677
  )
619
678
  return pcode, True
620
679
  else:
621
- name_to_pcode = self.name_to_pcode.get(countryiso3)
622
- if name_to_pcode is not None:
623
- pcode = name_to_pcode.get(name.lower())
624
- if pcode:
625
- return pcode, True
680
+ if self.use_parent and "parent" in kwargs:
681
+ parent = kwargs["parent"]
682
+ name_parent_to_pcode = self.name_parent_to_pcode.get(
683
+ countryiso3
684
+ )
685
+ if name_parent_to_pcode:
686
+ name_to_pcode = name_parent_to_pcode.get(parent)
687
+ if name_to_pcode is not None:
688
+ pcode = name_to_pcode.get(name.lower())
689
+ if pcode:
690
+ return pcode, True
691
+ else:
692
+ name_to_pcode = self.name_to_pcode.get(countryiso3)
693
+ if name_to_pcode is not None:
694
+ pcode = name_to_pcode.get(name.lower())
695
+ if pcode:
696
+ return pcode, True
626
697
  if not fuzzy_match:
627
698
  return None, True
628
- pcode = self.fuzzy_pcode(countryiso3, name, logname)
699
+ pcode = self.fuzzy_pcode(countryiso3, name, **kwargs)
629
700
  return pcode, False
630
701
 
631
702
  def output_matches(self) -> List[str]:
@@ -6,7 +6,7 @@ from . import get_int_timestamp
6
6
  from hdx.utilities.typehint import ListTuple
7
7
 
8
8
  try:
9
- from data_bridges_client import ApiClient, Configuration, CurrencyApi
9
+ from data_bridges_client import ApiClient, CurrencyApi
10
10
  from data_bridges_client.exceptions import ApiException
11
11
  from data_bridges_client.token import WfpApiToken
12
12
  except ImportError:
@@ -556,3 +556,8 @@ admin_fuzzy_dont:
556
556
  - "sahil"
557
557
  - "say'on"
558
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
@@ -33,6 +33,8 @@ class TestAdminLevel:
33
33
  assert adminone.get_admin_level("YEM") == 1
34
34
  assert len(adminone.get_pcode_list()) == 433
35
35
  assert adminone.get_pcode_length("YEM") == 4
36
+ assert adminone.use_parent is False
37
+ assert adminone.pcode_to_iso3["YE30"] == "YEM"
36
38
  assert adminone.get_pcode("YEM", "YE30", logname="test") == (
37
39
  "YE30",
38
40
  True,
@@ -177,6 +179,45 @@ class TestAdminLevel:
177
179
  "test - YEM: Matching (fuzzy) Al Dhale'e / الضالع to Ad Dali on map",
178
180
  ]
179
181
 
182
+ def test_adminlevel_parent(self, config):
183
+ admintwo = AdminLevel(config)
184
+ admintwo.countries_fuzzy_try = None
185
+ admintwo.setup_from_admin_info(config["admin_info_with_parent"])
186
+ assert admintwo.use_parent is True
187
+ assert admintwo.pcode_to_parent["AF0101"] == "AF01"
188
+ assert admintwo.get_pcode("AFG", "AF0101", logname="test") == (
189
+ "AF0101",
190
+ True,
191
+ )
192
+ assert admintwo.get_pcode(
193
+ "AFG", "AF0101", parent="blah", logname="test"
194
+ ) == ("AF0101", True)
195
+ assert admintwo.get_pcode("AFG", "Kabul", logname="test") == (
196
+ "AF0201",
197
+ True,
198
+ )
199
+ assert admintwo.get_pcode(
200
+ "AFG", "Kabul", parent="AF01", logname="test"
201
+ ) == ("AF0101", True)
202
+ assert admintwo.get_pcode(
203
+ "AFG", "Kabul", parent="blah", logname="test"
204
+ ) == (None, False)
205
+ assert admintwo.get_pcode(
206
+ "AFG", "Kabul", parent="AF02", logname="test"
207
+ ) == ("AF0201", True)
208
+ assert admintwo.get_pcode(
209
+ "AFG", "Kabull", parent="AF01", logname="test"
210
+ ) == ("AF0101", False)
211
+ assert admintwo.get_pcode(
212
+ "AFG", "Kabull", parent="blah", logname="test"
213
+ ) == (None, False)
214
+ assert admintwo.get_pcode(
215
+ "AFG", "Kabull", parent="AF02", logname="test"
216
+ ) == ("AF0201", False)
217
+ assert admintwo.get_pcode(
218
+ "ABC", "Kabull", parent="AF02", logname="test"
219
+ ) == (None, False)
220
+
180
221
  def test_adminlevel_with_url(self, config, url):
181
222
  adminone = AdminLevel(config)
182
223
  with pytest.raises(HXLIOException):
@@ -190,12 +231,25 @@ class TestAdminLevel:
190
231
  adminone = AdminLevel(config)
191
232
  adminone.setup_from_url()
192
233
  assert adminone.get_admin_level("YEM") == 1
193
- assert len(adminone.get_pcode_list()) == 2553
234
+ assert len(adminone.get_pcode_list()) == 2506
194
235
  assert adminone.get_pcode_length("YEM") == 4
195
236
  assert adminone.get_pcode("YEM", "YE30", logname="test") == (
196
237
  "YE30",
197
238
  True,
198
239
  )
240
+ assert adminone.get_pcode(
241
+ "YEM", "YE30", parent="YEM", logname="test"
242
+ ) == (
243
+ "YE30",
244
+ True,
245
+ )
246
+ # Exact match of p-code so doesn't need parent
247
+ assert adminone.get_pcode(
248
+ "YEM", "YE30", parent="Blah1", logname="test"
249
+ ) == (
250
+ "YE30",
251
+ True,
252
+ )
199
253
  assert adminone.get_pcode("YEM", "YEM30", logname="test") == (
200
254
  "YE30",
201
255
  True,
@@ -269,6 +323,19 @@ class TestAdminLevel:
269
323
  "YE30",
270
324
  False,
271
325
  )
326
+ assert adminone.get_pcode(
327
+ "YEM", "Ad Dal", parent="YEM", logname="test"
328
+ ) == (
329
+ "YE30",
330
+ False,
331
+ )
332
+ # Invalid parent means fuzzy matching won't match
333
+ assert adminone.get_pcode(
334
+ "YEM", "Ad Dal", parent="Blah2", logname="test"
335
+ ) == (
336
+ None,
337
+ False,
338
+ )
272
339
  assert adminone.get_pcode("YEM", "nord", logname="test") == (
273
340
  None,
274
341
  False,
@@ -307,6 +374,7 @@ class TestAdminLevel:
307
374
  assert output == [
308
375
  "test - Could not find ABC in map names!",
309
376
  "test - NER: Could not find ABCDEFGH in map names!",
377
+ "test - YEM: Could not find Blah2 in map names!",
310
378
  ]
311
379
 
312
380
  def test_adminlevel_pcode_formats(self, config, url, formats_url):
@@ -357,6 +425,7 @@ class TestAdminLevel:
357
425
 
358
426
  admintwo = AdminLevel(config, admin_level=2)
359
427
  admintwo.setup_from_url(admin_url=url)
428
+ assert admintwo.pcode_to_parent["YE3001"] == "YE30"
360
429
  assert admintwo.get_pcode("YEM", "YE03001", logname="test") == (
361
430
  None,
362
431
  True,
@@ -371,6 +440,12 @@ class TestAdminLevel:
371
440
  "YE3001",
372
441
  True,
373
442
  )
443
+ assert admintwo.get_pcode(
444
+ "YEM", "YEM3001", parent="Blah", logname="test"
445
+ ) == (
446
+ "YE3001",
447
+ True,
448
+ )
374
449
  assert admintwo.get_pcode("YEM", "YEM03001", logname="test") == (
375
450
  "YE3001",
376
451
  True,
@@ -456,6 +531,12 @@ class TestAdminLevel:
456
531
  None,
457
532
  True,
458
533
  )
534
+ assert admintwo.get_pcode(
535
+ "NER", "NE00409", parent="blah", logname="test"
536
+ ) == (
537
+ None,
538
+ True,
539
+ )
459
540
 
460
541
  admintwo.set_parent_admins_from_adminlevels([adminone])
461
542
  # The lookup in admin1 reveals that adding a 0 prefix to the admin1
@@ -465,6 +546,14 @@ class TestAdminLevel:
465
546
  "NER004009",
466
547
  True,
467
548
  )
549
+ # we don't use the parent because it could have a pcode length issue
550
+ # itself
551
+ assert admintwo.get_pcode(
552
+ "NER", "NE00409", parent="blah", logname="test"
553
+ ) == (
554
+ "NER004009",
555
+ True,
556
+ )
468
557
  # The lookup in admin1 reveals that removing the 0 prefix from the
469
558
  # admin1 is not a valid admin1 (CO80849) so the algorithm tries
470
559
  # removing the 0 prefix at the admin2 level instead and hence succeeds