hdx-python-country 3.6.9__tar.gz → 3.7.1__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.9 → hdx_python_country-3.7.1}/.config/pre-commit-config.yaml +5 -4
  2. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/.config/ruff.toml +0 -1
  3. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/.github/workflows/publish.yaml +2 -2
  4. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/.github/workflows/run-python-tests.yaml +2 -2
  5. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/CONTRIBUTING.md +3 -4
  6. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/PKG-INFO +4 -4
  7. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/documentation/.readthedocs.yaml +1 -0
  8. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/documentation/main.md +8 -0
  9. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/documentation/pydoc-markdown.yaml +3 -3
  10. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/pyproject.toml +2 -2
  11. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/requirements.txt +38 -36
  12. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/src/hdx/location/_version.py +2 -2
  13. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/src/hdx/location/adminlevel.py +119 -35
  14. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/tests/fixtures/adminlevel.yaml +5 -0
  15. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/tests/hdx/location/test_adminlevel.py +95 -1
  16. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/.config/coveragerc +0 -0
  17. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/.config/pytest.ini +0 -0
  18. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/.gitignore +0 -0
  19. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/LICENSE +0 -0
  20. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/README.md +0 -0
  21. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/src/hdx/location/Countries & Territories Taxonomy MVP - C&T Taxonomy with HXL Tags.csv +0 -0
  22. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/src/hdx/location/__init__.py +0 -0
  23. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/src/hdx/location/country.py +0 -0
  24. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/src/hdx/location/currency.py +0 -0
  25. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/src/hdx/location/names.py +0 -0
  26. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/src/hdx/location/phonetics.py +0 -0
  27. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/src/hdx/location/wfp_exchangerates.py +0 -0
  28. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/tests/fixtures/global_pcode_lengths.csv +0 -0
  29. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/tests/fixtures/global_pcodes_adm_1_2.csv +0 -0
  30. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/tests/fixtures/secondary_historic_rates.csv +0 -0
  31. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/tests/fixtures/secondary_rates.json +0 -0
  32. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/tests/hdx/location/Countries_UZB_Deleted.csv +0 -0
  33. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/tests/hdx/location/__init__.py +0 -0
  34. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/tests/hdx/location/test_country.py +0 -0
  35. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/tests/hdx/location/test_currency.py +0 -0
  36. {hdx_python_country-3.6.9 → hdx_python_country-3.7.1}/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.2
11
+ rev: v0.4.4
12
12
  hooks:
13
13
  # Run the linter.
14
14
  - id: ruff
@@ -16,10 +16,11 @@ repos:
16
16
  # Run the formatter.
17
17
  - id: ruff-format
18
18
  args: [--config, .config/ruff.toml]
19
- - repo: https://github.com/jazzband/pip-tools
20
- rev: 7.4.1
19
+ - repo: https://github.com/astral-sh/uv-pre-commit
20
+ rev: 0.1.45
21
21
  hooks:
22
+ # Run the pip compile
22
23
  - id: pip-compile
23
24
  name: pip-compile requirements.txt
24
25
  files: pyproject.toml
25
- args: [pyproject.toml, --resolver=backtracking, --all-extras, --upgrade, -q, -o, requirements.txt]
26
+ args: [ pyproject.toml, --resolver=backtracking, --all-extras, --upgrade, -q, -o, requirements.txt ]
@@ -12,6 +12,5 @@ ignore = [
12
12
  "E501" # Line too long
13
13
  ]
14
14
 
15
-
16
15
  [lint.isort]
17
16
  known-local-folder = ["hdx"]
@@ -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.9
3
+ Version: 3.7.1
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,7 +25,7 @@ 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.6
28
+ Requires-Dist: hdx-python-utilities>=3.6.8
29
29
  Requires-Dist: libhxl>=5.2.1
30
30
  Requires-Dist: pyphonetics
31
31
  Provides-Extra: dev
@@ -11,5 +11,6 @@ build:
11
11
  jobs:
12
12
  pre_build:
13
13
  - pip install --upgrade mkdocs
14
+ - pip install mkdocs-material
14
15
  - pip install pydoc-markdown
15
16
  - pydoc-markdown documentation/pydoc-markdown.yaml
@@ -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 @@ renderer:
9
9
  output_directory: docs
10
10
  mkdocs_config:
11
11
  site_name: HDX Python Country
12
- theme: mkdocs
12
+ theme: material
13
13
  repo_url: "https://github.com/OCHA-DAP/hdx-python-country"
14
14
  markdown:
15
15
  source_linker:
@@ -24,9 +24,9 @@ renderer:
24
24
  - title: Countries
25
25
  contents:
26
26
  - hdx.location.country.*
27
- - title: Administration level one
27
+ - title: Administration Level
28
28
  contents:
29
- - hdx.location.adminone.*
29
+ - hdx.location.adminlevel.*
30
30
  - title: Currencies
31
31
  contents:
32
32
  - hdx.location.currency.*
@@ -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,7 +34,7 @@ classifiers = [
34
34
  requires-python = ">=3.8"
35
35
 
36
36
  dependencies = [
37
- "hdx-python-utilities>=3.6.6",
37
+ "hdx-python-utilities>=3.6.8",
38
38
  "libhxl>=5.2.1",
39
39
  "pyphonetics",
40
40
  ]
@@ -1,16 +1,13 @@
1
- #
2
- # This file is autogenerated by pip-compile with Python 3.11
3
- # by the following command:
4
- #
5
- # pip-compile --all-extras --output-file=requirements.txt pyproject.toml
6
- #
7
- annotated-types==0.6.0
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
8
4
  # via pydantic
9
5
  attrs==23.2.0
10
6
  # via
11
7
  # frictionless
12
8
  # jsonlines
13
9
  # jsonschema
10
+ # referencing
14
11
  certifi==2024.2.2
15
12
  # via requests
16
13
  cfgv==3.4.0
@@ -21,25 +18,23 @@ charset-normalizer==3.3.2
21
18
  # via requests
22
19
  click==8.1.7
23
20
  # via typer
24
- colorama==0.4.6
25
- # via typer
26
- coverage[toml]==7.4.3
21
+ coverage==7.5.1
27
22
  # via pytest-cov
28
23
  distlib==0.3.8
29
24
  # via virtualenv
30
25
  et-xmlfile==1.1.0
31
26
  # via openpyxl
32
- filelock==3.13.1
27
+ filelock==3.14.0
33
28
  # via virtualenv
34
- frictionless==5.16.1
29
+ frictionless==5.17.0
35
30
  # via hdx-python-utilities
36
- hdx-python-utilities==3.6.6
31
+ hdx-python-utilities==3.6.8
37
32
  # via hdx-python-country (pyproject.toml)
38
33
  humanize==4.9.0
39
34
  # via frictionless
40
- identify==2.5.35
35
+ identify==2.5.36
41
36
  # via pre-commit
42
- idna==3.6
37
+ idna==3.7
43
38
  # via requests
44
39
  ijson==3.2.3
45
40
  # via hdx-python-utilities
@@ -47,16 +42,18 @@ iniconfig==2.0.0
47
42
  # via pytest
48
43
  isodate==0.6.1
49
44
  # via frictionless
50
- jinja2==3.1.3
45
+ jinja2==3.1.4
51
46
  # via frictionless
52
47
  jsonlines==4.0.0
53
48
  # via hdx-python-utilities
54
49
  jsonpath-ng==1.6.1
55
50
  # via libhxl
56
- jsonschema==4.17.3
51
+ jsonschema==4.22.0
57
52
  # via
58
53
  # frictionless
59
54
  # tableschema-to-template
55
+ jsonschema-specifications==2023.12.1
56
+ # via jsonschema
60
57
  libhxl==5.2.1
61
58
  # via hdx-python-country (pyproject.toml)
62
59
  loguru==0.7.2
@@ -77,31 +74,29 @@ packaging==24.0
77
74
  # via pytest
78
75
  petl==1.7.15
79
76
  # via frictionless
80
- platformdirs==4.2.0
77
+ platformdirs==4.2.2
81
78
  # via virtualenv
82
- pluggy==1.4.0
79
+ pluggy==1.5.0
83
80
  # via pytest
84
81
  ply==3.11
85
82
  # via
86
83
  # jsonpath-ng
87
84
  # libhxl
88
- pre-commit==3.6.2
85
+ pre-commit==3.7.1
89
86
  # via hdx-python-country (pyproject.toml)
90
- pydantic==2.6.4
87
+ pydantic==2.7.1
91
88
  # via frictionless
92
- pydantic-core==2.16.3
89
+ pydantic-core==2.18.2
93
90
  # via pydantic
94
- pygments==2.17.2
91
+ pygments==2.18.0
95
92
  # via rich
96
93
  pyphonetics==0.5.3
97
94
  # via hdx-python-country (pyproject.toml)
98
- pyrsistent==0.20.0
99
- # via jsonschema
100
- pytest==8.1.1
95
+ pytest==8.2.1
101
96
  # via
102
97
  # hdx-python-country (pyproject.toml)
103
98
  # pytest-cov
104
- pytest-cov==4.1.0
99
+ pytest-cov==5.0.0
105
100
  # via hdx-python-country (pyproject.toml)
106
101
  python-dateutil==2.8.2
107
102
  # via
@@ -119,21 +114,31 @@ pyyaml==6.0.1
119
114
  # tableschema-to-template
120
115
  ratelimit==2.2.1
121
116
  # via hdx-python-utilities
122
- requests==2.31.0
117
+ referencing==0.35.1
118
+ # via
119
+ # jsonschema
120
+ # jsonschema-specifications
121
+ requests==2.32.2
123
122
  # via
124
123
  # frictionless
125
124
  # libhxl
126
125
  # requests-file
127
- requests-file==2.0.0
126
+ requests-file==2.1.0
128
127
  # via hdx-python-utilities
129
128
  rfc3986==2.0.0
130
129
  # via frictionless
131
130
  rich==13.7.1
132
131
  # via typer
132
+ rpds-py==0.18.1
133
+ # via
134
+ # jsonschema
135
+ # referencing
133
136
  ruamel-yaml==0.18.6
134
137
  # via hdx-python-utilities
135
138
  ruamel-yaml-clib==0.2.8
136
139
  # via ruamel-yaml
140
+ setuptools==70.0.0
141
+ # via nodeenv
137
142
  shellingham==1.5.4
138
143
  # via typer
139
144
  simpleeval==0.9.13
@@ -152,9 +157,9 @@ tabulate==0.9.0
152
157
  # via frictionless
153
158
  text-unidecode==1.3
154
159
  # via python-slugify
155
- typer[all]==0.9.0
160
+ typer==0.12.3
156
161
  # via frictionless
157
- typing-extensions==4.10.0
162
+ typing-extensions==4.11.0
158
163
  # via
159
164
  # frictionless
160
165
  # pydantic
@@ -168,9 +173,9 @@ urllib3==2.2.1
168
173
  # via
169
174
  # libhxl
170
175
  # requests
171
- validators==0.22.0
176
+ validators==0.28.1
172
177
  # via frictionless
173
- virtualenv==20.25.1
178
+ virtualenv==20.26.2
174
179
  # via pre-commit
175
180
  wheel==0.43.0
176
181
  # via libhxl
@@ -182,6 +187,3 @@ xlsxwriter==3.2.0
182
187
  # via tableschema-to-template
183
188
  xlwt==1.3.0
184
189
  # via hdx-python-utilities
185
-
186
- # The following packages are considered to be unsafe in a requirements file:
187
- # setuptools
@@ -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.9'
16
- __version_tuple__ = version_tuple = (3, 6, 9)
15
+ __version__ = version = '3.7.1'
16
+ __version_tuple__ = version_tuple = (3, 7, 1)
@@ -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]:
@@ -672,3 +743,16 @@ class AdminLevel:
672
743
  logger.error(line)
673
744
  output.append(line)
674
745
  return output
746
+
747
+ def output_admin_name_mappings(self) -> List[str]:
748
+ """Output log of name mappings
749
+
750
+ Returns:
751
+ List[str]: List of mappings
752
+ """
753
+ output = []
754
+ for name, pcode in self.admin_name_mappings.items():
755
+ line = f"{name}: {self.pcode_to_name[pcode]} ({pcode})"
756
+ logger.info(line)
757
+ output.append(line)
758
+ return output
@@ -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,
@@ -153,6 +155,11 @@ class TestAdminLevel:
153
155
  "test - Could not find ABC in map names!",
154
156
  "test - NER: Could not find ABCDEFGH in map names!",
155
157
  ]
158
+ output = adminone.output_admin_name_mappings()
159
+ assert len(output) == 62
160
+ assert output[0] == "Nord-Ouest: North West (HT09)"
161
+ assert output[31] == "Juba Dhexe: Middle Juba (SO27)"
162
+ assert output[61] == "CU Niamey: Niamey (NER008)"
156
163
 
157
164
  def test_adminlevel_fuzzy(self, config):
158
165
  adminone = AdminLevel(config)
@@ -177,6 +184,45 @@ class TestAdminLevel:
177
184
  "test - YEM: Matching (fuzzy) Al Dhale'e / الضالع to Ad Dali on map",
178
185
  ]
179
186
 
187
+ def test_adminlevel_parent(self, config):
188
+ admintwo = AdminLevel(config)
189
+ admintwo.countries_fuzzy_try = None
190
+ admintwo.setup_from_admin_info(config["admin_info_with_parent"])
191
+ assert admintwo.use_parent is True
192
+ assert admintwo.pcode_to_parent["AF0101"] == "AF01"
193
+ assert admintwo.get_pcode("AFG", "AF0101", logname="test") == (
194
+ "AF0101",
195
+ True,
196
+ )
197
+ assert admintwo.get_pcode(
198
+ "AFG", "AF0101", parent="blah", logname="test"
199
+ ) == ("AF0101", True)
200
+ assert admintwo.get_pcode("AFG", "Kabul", logname="test") == (
201
+ "AF0201",
202
+ True,
203
+ )
204
+ assert admintwo.get_pcode(
205
+ "AFG", "Kabul", parent="AF01", logname="test"
206
+ ) == ("AF0101", True)
207
+ assert admintwo.get_pcode(
208
+ "AFG", "Kabul", parent="blah", logname="test"
209
+ ) == (None, False)
210
+ assert admintwo.get_pcode(
211
+ "AFG", "Kabul", parent="AF02", logname="test"
212
+ ) == ("AF0201", True)
213
+ assert admintwo.get_pcode(
214
+ "AFG", "Kabull", parent="AF01", logname="test"
215
+ ) == ("AF0101", False)
216
+ assert admintwo.get_pcode(
217
+ "AFG", "Kabull", parent="blah", logname="test"
218
+ ) == (None, False)
219
+ assert admintwo.get_pcode(
220
+ "AFG", "Kabull", parent="AF02", logname="test"
221
+ ) == ("AF0201", False)
222
+ assert admintwo.get_pcode(
223
+ "ABC", "Kabull", parent="AF02", logname="test"
224
+ ) == (None, False)
225
+
180
226
  def test_adminlevel_with_url(self, config, url):
181
227
  adminone = AdminLevel(config)
182
228
  with pytest.raises(HXLIOException):
@@ -190,12 +236,25 @@ class TestAdminLevel:
190
236
  adminone = AdminLevel(config)
191
237
  adminone.setup_from_url()
192
238
  assert adminone.get_admin_level("YEM") == 1
193
- assert len(adminone.get_pcode_list()) == 2553
239
+ assert len(adminone.get_pcode_list()) == 2509
194
240
  assert adminone.get_pcode_length("YEM") == 4
195
241
  assert adminone.get_pcode("YEM", "YE30", logname="test") == (
196
242
  "YE30",
197
243
  True,
198
244
  )
245
+ assert adminone.get_pcode(
246
+ "YEM", "YE30", parent="YEM", logname="test"
247
+ ) == (
248
+ "YE30",
249
+ True,
250
+ )
251
+ # Exact match of p-code so doesn't need parent
252
+ assert adminone.get_pcode(
253
+ "YEM", "YE30", parent="Blah1", logname="test"
254
+ ) == (
255
+ "YE30",
256
+ True,
257
+ )
199
258
  assert adminone.get_pcode("YEM", "YEM30", logname="test") == (
200
259
  "YE30",
201
260
  True,
@@ -269,6 +328,19 @@ class TestAdminLevel:
269
328
  "YE30",
270
329
  False,
271
330
  )
331
+ assert adminone.get_pcode(
332
+ "YEM", "Ad Dal", parent="YEM", logname="test"
333
+ ) == (
334
+ "YE30",
335
+ False,
336
+ )
337
+ # Invalid parent means fuzzy matching won't match
338
+ assert adminone.get_pcode(
339
+ "YEM", "Ad Dal", parent="Blah2", logname="test"
340
+ ) == (
341
+ None,
342
+ False,
343
+ )
272
344
  assert adminone.get_pcode("YEM", "nord", logname="test") == (
273
345
  None,
274
346
  False,
@@ -307,6 +379,7 @@ class TestAdminLevel:
307
379
  assert output == [
308
380
  "test - Could not find ABC in map names!",
309
381
  "test - NER: Could not find ABCDEFGH in map names!",
382
+ "test - YEM: Could not find Blah2 in map names!",
310
383
  ]
311
384
 
312
385
  def test_adminlevel_pcode_formats(self, config, url, formats_url):
@@ -357,6 +430,7 @@ class TestAdminLevel:
357
430
 
358
431
  admintwo = AdminLevel(config, admin_level=2)
359
432
  admintwo.setup_from_url(admin_url=url)
433
+ assert admintwo.pcode_to_parent["YE3001"] == "YE30"
360
434
  assert admintwo.get_pcode("YEM", "YE03001", logname="test") == (
361
435
  None,
362
436
  True,
@@ -371,6 +445,12 @@ class TestAdminLevel:
371
445
  "YE3001",
372
446
  True,
373
447
  )
448
+ assert admintwo.get_pcode(
449
+ "YEM", "YEM3001", parent="Blah", logname="test"
450
+ ) == (
451
+ "YE3001",
452
+ True,
453
+ )
374
454
  assert admintwo.get_pcode("YEM", "YEM03001", logname="test") == (
375
455
  "YE3001",
376
456
  True,
@@ -456,6 +536,12 @@ class TestAdminLevel:
456
536
  None,
457
537
  True,
458
538
  )
539
+ assert admintwo.get_pcode(
540
+ "NER", "NE00409", parent="blah", logname="test"
541
+ ) == (
542
+ None,
543
+ True,
544
+ )
459
545
 
460
546
  admintwo.set_parent_admins_from_adminlevels([adminone])
461
547
  # The lookup in admin1 reveals that adding a 0 prefix to the admin1
@@ -465,6 +551,14 @@ class TestAdminLevel:
465
551
  "NER004009",
466
552
  True,
467
553
  )
554
+ # we don't use the parent because it could have a pcode length issue
555
+ # itself
556
+ assert admintwo.get_pcode(
557
+ "NER", "NE00409", parent="blah", logname="test"
558
+ ) == (
559
+ "NER004009",
560
+ True,
561
+ )
468
562
  # The lookup in admin1 reveals that removing the 0 prefix from the
469
563
  # admin1 is not a valid admin1 (CO80849) so the algorithm tries
470
564
  # removing the 0 prefix at the admin2 level instead and hence succeeds