hdx-python-utilities 4.1.2__tar.gz → 4.2.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.
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/PKG-INFO +3 -2
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/documentation/index.md +33 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/pyproject.toml +1 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/_version.py +2 -2
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/matching.py +211 -3
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_downloader.py +1 -1
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_matching.py +250 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/uv.lock +214 -1
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/.github/workflows/publish-test.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/.github/workflows/publish.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/.github/workflows/run-python-tests.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/.gitignore +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/.pre-commit-config.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/CLAUDE.md +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/LICENSE +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/README.md +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/documentation/.readthedocs.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/documentation/mkdocs.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/__init__.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/base_downloader.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/compare.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/dateparse.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/dictandlist.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/downloader.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/easy_logging.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/email.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/encoding.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/error_handler.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/errors_onexit.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/file_hashing.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/frictionless_wrapper.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/html.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/loader.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/path.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/retriever.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/saver.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/session.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/state.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/text.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/url.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/useragent.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/uuid.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/src/hdx/utilities/zip_crc.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/compare/test_csv_processing.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/compare/test_csv_processing2.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/empty.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/hdx_config.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/hdx_config.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/hdx_email_configuration.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/hdx_email_configuration.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/logging_config.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/logging_config.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/project_configuration.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/project_configuration.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/smtp_config.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/smtp_config.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/user_agent_config.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/user_agent_config2.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/user_agent_config3.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/config/user_agent_config_wrong.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/basicauth.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/bearertoken.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/extra_params.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/extra_params.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/extra_params_tree.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/resume_test.dat +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_csv_processing.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_csv_processing_blanks.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_data.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_data.xlsx +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_data1.csv/empty.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_data2.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_json_processing.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_xls_processing.xls +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_xlsx_processing.xlsx +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/bad_header.zip +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/bad_index.xlsx +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/empty.zip +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/test.xlsx +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/test_shapefile.zip +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/valid_sig_invalid_body.zip +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/html/response.html +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/loader/empty.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/loader/empty.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/loader/empty_list.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/fallbacks/test.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/fallbacks/test.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/fallbacks/test.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/fallbacks/test.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/retriever-test.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/test.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/test.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/test.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/test.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out2.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out2.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out5.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out6.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out7.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out8.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out8.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-false_sortkeys-false.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-false_sortkeys-false.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-false_sortkeys-true.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-false_sortkeys-true.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-true_sortkeys-false.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-true_sortkeys-false.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-true_sortkeys-true.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-true_sortkeys-true.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/state/analysis_dates.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/state/last_build_date.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/test_data.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/text/text1.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/fixtures/text/text2.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/conftest.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_compare.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_dateparse.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_dictandlist.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_easy_logging.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_email.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_encoding.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_error_handler.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_file_hashing.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_html.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_loader.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_path.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_retriever.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_saver.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_state.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_text.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_url.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_useragent.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_uuid.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_zip_crc.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: hdx-python-utilities
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: HDX Python Utilities for streaming tabular data, date and time handling and other helpful functions
|
|
5
5
|
Project-URL: Homepage, https://github.com/OCHA-DAP/hdx-python-utilities
|
|
6
6
|
Author-email: Michael Rans <rans@email.com>
|
|
@@ -32,6 +32,7 @@ Requires-Dist: loguru
|
|
|
32
32
|
Requires-Dist: openpyxl>=3.1.2
|
|
33
33
|
Requires-Dist: pyphonetics
|
|
34
34
|
Requires-Dist: python-dateutil<2.9.1,>=2.9.0
|
|
35
|
+
Requires-Dist: rapidfuzz
|
|
35
36
|
Requires-Dist: ratelimit
|
|
36
37
|
Requires-Dist: requests-file
|
|
37
38
|
Requires-Dist: ruamel-yaml
|
|
@@ -33,6 +33,11 @@ The code for the library is [here](https://github.com/OCHA-DAP/hdx-python-utilit
|
|
|
33
33
|
The library has detailed API documentation which can be found in the menu at the top.
|
|
34
34
|
|
|
35
35
|
## Breaking Changes
|
|
36
|
+
From 4.2.0, get_code_from_name fuzzy matches using RapidFuzzMatcher rather than
|
|
37
|
+
Phonetics by default. Pass matcher=Phonetics() for the previous behaviour. A name
|
|
38
|
+
made of parts (eg. "Protection - GBV") is resolved from its parts, and a list of
|
|
39
|
+
names (eg. "Wash & Protection") is not fuzzy matched.
|
|
40
|
+
|
|
36
41
|
From 4.1.0, save_hxlated_output and Download.hxl_row removed. Parameter use_hxl removed
|
|
37
42
|
from all calls in Download and Retrieve.
|
|
38
43
|
|
|
@@ -877,7 +882,19 @@ Efficient Hashing of files that produces a stable hash even for zip and xlsx fil
|
|
|
877
882
|
|
|
878
883
|
Examples:
|
|
879
884
|
|
|
885
|
+
# Fuzzy match a name to one of a list of names using rapidfuzz scoring
|
|
880
886
|
possible_names = ["Al Maharah", "Ad Dali", "Dhamar"]
|
|
887
|
+
matcher = RapidFuzzMatcher()
|
|
888
|
+
assert matcher.match(possible_names, "al dali") == 1
|
|
889
|
+
|
|
890
|
+
# place_name_scorer does not match place names that share a word but differ
|
|
891
|
+
# in another that distinguishes places
|
|
892
|
+
kalimantan = ["kalimantan utara", "kalimantan tengah"]
|
|
893
|
+
assert matcher.match(kalimantan, "central kalimantan") == 0
|
|
894
|
+
matcher = RapidFuzzMatcher(RapidFuzzMatcher.place_name_scorer)
|
|
895
|
+
assert matcher.match(kalimantan, "central kalimantan") is None
|
|
896
|
+
|
|
897
|
+
# Phonetics is an alternative matcher using Refined Soundex
|
|
881
898
|
phonetics = Phonetics()
|
|
882
899
|
assert phonetics.match(possible_names, "al dali") == 1
|
|
883
900
|
|
|
@@ -887,6 +904,22 @@ Examples:
|
|
|
887
904
|
}
|
|
888
905
|
assert get_code_from_name("NATIONAL_NGO", lookup, [], fuzzy_match=False) == "441"
|
|
889
906
|
|
|
907
|
+
# get_code_from_name fuzzy matches with RapidFuzzMatcher unless given
|
|
908
|
+
# another matcher. It resolves names made of parts and does not fuzzy
|
|
909
|
+
# match lists of names.
|
|
910
|
+
sector_lookup = {"food security": "FSC", "protection": "PRO", "gbv": "PRO-GBV", "wash": "WSH"}
|
|
911
|
+
assert get_code_from_name("Food Securty", sector_lookup, []) == "FSC"
|
|
912
|
+
assert get_code_from_name("Food Securty", sector_lookup, [], matcher=Phonetics()) == "FSC"
|
|
913
|
+
assert get_code_from_name("Protection - GBV", sector_lookup, []) == "PRO-GBV"
|
|
914
|
+
assert get_code_from_name("Wash & Protection", sector_lookup, []) is None
|
|
915
|
+
|
|
916
|
+
# Resolve a name made of parts, returning the code and whether it is a
|
|
917
|
+
# list of names. The third parameter is a part that qualifies the other,
|
|
918
|
+
# eg. a parent admin name.
|
|
919
|
+
admin_lookup = {"acosta": "VE1101", "falcon": "VE1109"}
|
|
920
|
+
assert resolve_name_parts("Falcón, Acosta", admin_lookup) == (None, True)
|
|
921
|
+
assert resolve_name_parts("Falcón, Acosta", admin_lookup, "falcon") == ("VE1101", False)
|
|
922
|
+
|
|
890
923
|
a = "The quick brown fox jumped over the lazy dog. It was so fast!"
|
|
891
924
|
|
|
892
925
|
# Replace multiple strings in a string simultaneously
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '4.
|
|
22
|
-
__version_tuple__ = version_tuple = (4,
|
|
21
|
+
__version__ = version = '4.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (4, 2, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -3,10 +3,14 @@ import re
|
|
|
3
3
|
from collections.abc import Callable, Sequence
|
|
4
4
|
|
|
5
5
|
from pyphonetics import RefinedSoundex
|
|
6
|
+
from rapidfuzz import fuzz as rapidfuzz_fuzz
|
|
6
7
|
|
|
7
8
|
from hdx.utilities.text import normalise
|
|
8
9
|
|
|
9
10
|
TEMPLATE_VARIABLES = re.compile("{{.*?}}")
|
|
11
|
+
NON_WORD_CHARACTERS = re.compile(r"[\W_]+")
|
|
12
|
+
LIST_SEPARATORS = re.compile(r"\s*(?:[,;/:&\n]|\s(?:and|et)\s)\s*", re.IGNORECASE)
|
|
13
|
+
HYPHEN = re.compile(r"\s*-\s*")
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
class Phonetics(RefinedSoundex):
|
|
@@ -58,15 +62,212 @@ class Phonetics(RefinedSoundex):
|
|
|
58
62
|
return matching_index
|
|
59
63
|
|
|
60
64
|
|
|
65
|
+
class RapidFuzzMatcher:
|
|
66
|
+
"""
|
|
67
|
+
Default matcher for names, using rapidfuzz edit-distance scoring instead
|
|
68
|
+
of phonetic (Soundex-family) encoding as Phonetics does. Phonetic algorithms
|
|
69
|
+
encode English pronunciation rules, so they are a poor fit for matching
|
|
70
|
+
multilingual or transliterated names (eg. "Sana'a" vs "Sanaa") that are
|
|
71
|
+
spelling variants rather than being phonetically similar in English
|
|
72
|
+
terms. Edit distance on normalised strings tends to place such variants
|
|
73
|
+
closer together.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
scorer: Function taking two strings and returning a similarity score
|
|
77
|
+
from 0-100. Defaults to default_scorer. Use place_name_scorer for
|
|
78
|
+
place names. Ties are broken by rapidfuzz.fuzz.ratio over the
|
|
79
|
+
whole strings.
|
|
80
|
+
|
|
81
|
+
Caveat: "<name> city" scores ~100 against "<name>", as for any name whose
|
|
82
|
+
words are a subset of the other's. So withholding a text replacement that
|
|
83
|
+
strips " city" does not stop "Kenge city" matching "Kenge".
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, scorer: Callable[[str, str], float] | None = None) -> None:
|
|
87
|
+
self.scorer = scorer or self.default_scorer
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def default_scorer(name: str, possible_name: str) -> float:
|
|
91
|
+
"""
|
|
92
|
+
Score two strings with token_set_ratio, or with ratio on the strings
|
|
93
|
+
with spaces and punctuation removed if that is at least 90 and higher.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: Name to match
|
|
97
|
+
possible_name: Possible name
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Similarity score from 0-100
|
|
101
|
+
"""
|
|
102
|
+
score = rapidfuzz_fuzz.token_set_ratio(name, possible_name)
|
|
103
|
+
joined_score = rapidfuzz_fuzz.ratio(
|
|
104
|
+
NON_WORD_CHARACTERS.sub("", name),
|
|
105
|
+
NON_WORD_CHARACTERS.sub("", possible_name),
|
|
106
|
+
)
|
|
107
|
+
if joined_score >= 90:
|
|
108
|
+
return max(score, joined_score)
|
|
109
|
+
return score
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def place_name_scorer(name: str, possible_name: str) -> float:
|
|
113
|
+
"""
|
|
114
|
+
Score two strings with default_scorer, but score 0 if they share a
|
|
115
|
+
word, each has other words of 3+ characters, and one of the words on
|
|
116
|
+
the side with fewer has ratio < 60 to all those on the other side (eg.
|
|
117
|
+
"central kalimantan" vs "kalimantan utara", but not "central west" vs
|
|
118
|
+
"centre west"). Suits place names, where the differing word usually
|
|
119
|
+
distinguishes places, but not names like sectors, where extra words
|
|
120
|
+
are often descriptive (eg. "food security and livelihood").
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name: Name to match
|
|
124
|
+
possible_name: Possible name
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Similarity score from 0-100
|
|
128
|
+
"""
|
|
129
|
+
words = set(name.split())
|
|
130
|
+
possible_words = set(possible_name.split())
|
|
131
|
+
|
|
132
|
+
def long_words(words: set[str]) -> set[str]:
|
|
133
|
+
return {word for word in words if len(word) > 2}
|
|
134
|
+
|
|
135
|
+
extra_words = long_words(words - possible_words)
|
|
136
|
+
possible_extra_words = long_words(possible_words - words)
|
|
137
|
+
if words & possible_words and extra_words and possible_extra_words:
|
|
138
|
+
fewer, more = sorted((extra_words, possible_extra_words), key=len)
|
|
139
|
+
for word in fewer:
|
|
140
|
+
if max(rapidfuzz_fuzz.ratio(word, other) for other in more) < 60:
|
|
141
|
+
return 0.0
|
|
142
|
+
return RapidFuzzMatcher.default_scorer(name, possible_name)
|
|
143
|
+
|
|
144
|
+
def match(
|
|
145
|
+
self,
|
|
146
|
+
possible_names: Sequence,
|
|
147
|
+
name: str,
|
|
148
|
+
alternative_name: str | None = None,
|
|
149
|
+
transform_possible_names: Sequence[Callable] = [],
|
|
150
|
+
threshold: float = 65.0,
|
|
151
|
+
) -> int | None:
|
|
152
|
+
"""
|
|
153
|
+
Match name to one of the given possible names. Returns None if no
|
|
154
|
+
match or the index of the matching name.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
possible_names: Possible names
|
|
158
|
+
name: Name to match
|
|
159
|
+
alternative_name: Alternative name to match. Defaults to None.
|
|
160
|
+
transform_possible_names: Functions to transform possible names.
|
|
161
|
+
threshold: Minimum similarity score, 0-100. Defaults to 65.0.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Index of matching name from possible names or None
|
|
165
|
+
"""
|
|
166
|
+
maxscore = None
|
|
167
|
+
matching_index = None
|
|
168
|
+
names = [name.lower()]
|
|
169
|
+
if alternative_name:
|
|
170
|
+
names.append(alternative_name.lower())
|
|
171
|
+
all_transforms = (lambda x: x, *transform_possible_names)
|
|
172
|
+
for i, possible_name in enumerate(possible_names):
|
|
173
|
+
for transform_possible_name in all_transforms:
|
|
174
|
+
transformed_possible_name = transform_possible_name(possible_name)
|
|
175
|
+
if not transformed_possible_name:
|
|
176
|
+
continue
|
|
177
|
+
transformed_possible_name = transformed_possible_name.lower()
|
|
178
|
+
for query in names:
|
|
179
|
+
score = (
|
|
180
|
+
self.scorer(query, transformed_possible_name),
|
|
181
|
+
rapidfuzz_fuzz.ratio(query, transformed_possible_name),
|
|
182
|
+
)
|
|
183
|
+
if maxscore is None or score > maxscore:
|
|
184
|
+
maxscore = score
|
|
185
|
+
matching_index = i
|
|
186
|
+
if maxscore is None or maxscore[0] < threshold:
|
|
187
|
+
return None
|
|
188
|
+
return matching_index
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def split_name(name: str, separators: re.Pattern) -> list[str]:
|
|
192
|
+
"""
|
|
193
|
+
Split name on separators, dropping parts that are empty once normalised.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
name: Name to split
|
|
197
|
+
separators: Compiled regular expression matching separators
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of parts
|
|
201
|
+
"""
|
|
202
|
+
parts = [part.strip() for part in separators.split(name)]
|
|
203
|
+
return [part for part in parts if normalise(part)]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def resolve_name_parts(
|
|
207
|
+
name: str, name_to_code: dict[str, str], ignore: str | None = None
|
|
208
|
+
) -> tuple[str | None, bool]:
|
|
209
|
+
"""
|
|
210
|
+
Resolve a name made of parts split by separators (",", ";", "/", ":", "&",
|
|
211
|
+
newline, "and", "et") or, if there are none, by a hyphen. Parts are looked
|
|
212
|
+
up in name_to_code. A name with two parts gives a code if:
|
|
213
|
+
- one part's normalised form equals ignore and the other part has a code
|
|
214
|
+
(eg. "Falcón, Acosta" with the parent admin name "falcon")
|
|
215
|
+
- one part's code followed by "-" starts the other's (eg. "Protection -
|
|
216
|
+
GBV" gives PRO-GBV)
|
|
217
|
+
- both parts have the same code.
|
|
218
|
+
It is a list of names (eg. "Wash & Protection") if it has more than two
|
|
219
|
+
parts, or two parts with different codes (eg. "Guba-Khachmaz").
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
name: Name to resolve
|
|
223
|
+
name_to_code: Mapping from names (raw or normalised) to codes
|
|
224
|
+
ignore: Normalised name of a part that qualifies the other part.
|
|
225
|
+
Defaults to None.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Tuple of code (or None) and whether name is a list of names
|
|
229
|
+
"""
|
|
230
|
+
parts = split_name(name, LIST_SEPARATORS)
|
|
231
|
+
if len(parts) > 2:
|
|
232
|
+
return None, True
|
|
233
|
+
if len(parts) < 2:
|
|
234
|
+
parts = split_name(name, HYPHEN)
|
|
235
|
+
if len(parts) != 2:
|
|
236
|
+
return None, False
|
|
237
|
+
normalised_parts = [normalise(part) for part in parts]
|
|
238
|
+
if ignore in normalised_parts:
|
|
239
|
+
index = 1 - normalised_parts.index(ignore)
|
|
240
|
+
parts = [parts[index]]
|
|
241
|
+
normalised_parts = [normalised_parts[index]]
|
|
242
|
+
codes = []
|
|
243
|
+
for part, normalised_part in zip(parts, normalised_parts):
|
|
244
|
+
code = name_to_code.get(part, name_to_code.get(normalised_part))
|
|
245
|
+
if not code:
|
|
246
|
+
return None, False
|
|
247
|
+
codes.append(code)
|
|
248
|
+
code = codes[0]
|
|
249
|
+
if len(codes) == 1:
|
|
250
|
+
return code, False
|
|
251
|
+
other_code = codes[1]
|
|
252
|
+
if code == other_code or other_code.startswith(f"{code}-"):
|
|
253
|
+
return other_code, False
|
|
254
|
+
if code.startswith(f"{other_code}-"):
|
|
255
|
+
return code, False
|
|
256
|
+
return None, True
|
|
257
|
+
|
|
258
|
+
|
|
61
259
|
def get_code_from_name(
|
|
62
260
|
name: str,
|
|
63
261
|
code_lookup: dict[str, str],
|
|
64
262
|
unmatched: list[str],
|
|
65
263
|
fuzzy_match: bool = True,
|
|
66
264
|
match_threshold: int = 5,
|
|
265
|
+
matcher: Phonetics | RapidFuzzMatcher | None = None,
|
|
67
266
|
) -> str | None:
|
|
68
267
|
"""
|
|
69
|
-
Given a name (org type, sector, etc), return the corresponding code.
|
|
268
|
+
Given a name (org type, sector, etc), return the corresponding code. A
|
|
269
|
+
name made of parts is resolved by resolve_name_parts, and is not fuzzy
|
|
270
|
+
matched if it is a list of names.
|
|
70
271
|
|
|
71
272
|
Args:
|
|
72
273
|
name: Name to match
|
|
@@ -74,6 +275,7 @@ def get_code_from_name(
|
|
|
74
275
|
unmatched: List of unmatched names
|
|
75
276
|
fuzzy_match: Allow fuzzy matching or not
|
|
76
277
|
match_threshold: Match threshold
|
|
278
|
+
matcher: Fuzzy matcher to use. Defaults to None (RapidFuzzMatcher).
|
|
77
279
|
|
|
78
280
|
Returns:
|
|
79
281
|
Matching code
|
|
@@ -91,11 +293,17 @@ def get_code_from_name(
|
|
|
91
293
|
if len(name) <= match_threshold:
|
|
92
294
|
unmatched.append(name)
|
|
93
295
|
return None
|
|
94
|
-
|
|
296
|
+
code, is_list = resolve_name_parts(name, code_lookup)
|
|
297
|
+
if code:
|
|
298
|
+
code_lookup[name] = code
|
|
299
|
+
return code
|
|
300
|
+
if not fuzzy_match or is_list:
|
|
95
301
|
unmatched.append(name)
|
|
96
302
|
return None
|
|
97
303
|
names = [x for x in code_lookup.keys() if len(x) > match_threshold]
|
|
98
|
-
|
|
304
|
+
if matcher is None:
|
|
305
|
+
matcher = RapidFuzzMatcher()
|
|
306
|
+
name_index = matcher.match(
|
|
99
307
|
possible_names=names,
|
|
100
308
|
name=name,
|
|
101
309
|
alternative_name=name_clean,
|
{hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_downloader.py
RENAMED
|
@@ -462,7 +462,7 @@ class TestDownloader:
|
|
|
462
462
|
with Download() as downloader:
|
|
463
463
|
text = downloader.download_text(fixtureurl)
|
|
464
464
|
assert downloader.get_status() == 200
|
|
465
|
-
assert len(downloader.get_headers())
|
|
465
|
+
assert len(downloader.get_headers()) == 26
|
|
466
466
|
assert (
|
|
467
467
|
bool(re.match(r"7\d\d", downloader.get_header("Content-Length")))
|
|
468
468
|
is True
|
{hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_matching.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from hdx.utilities.matching import (
|
|
4
4
|
Phonetics,
|
|
5
|
+
RapidFuzzMatcher,
|
|
5
6
|
earliest_index,
|
|
6
7
|
get_code_from_name,
|
|
7
8
|
get_matching_text,
|
|
@@ -9,6 +10,7 @@ from hdx.utilities.matching import (
|
|
|
9
10
|
get_matching_then_nonmatching_text,
|
|
10
11
|
match_template_variables,
|
|
11
12
|
multiple_replace,
|
|
13
|
+
resolve_name_parts,
|
|
12
14
|
)
|
|
13
15
|
from hdx.utilities.text import normalise
|
|
14
16
|
|
|
@@ -29,6 +31,161 @@ class TestMatching:
|
|
|
29
31
|
is None
|
|
30
32
|
)
|
|
31
33
|
|
|
34
|
+
def test_rapidfuzz_matcher(self):
|
|
35
|
+
possible_names = ["Al Maharah", "Ad Dali", "Dhamar"]
|
|
36
|
+
matcher = RapidFuzzMatcher()
|
|
37
|
+
assert matcher.match(possible_names, "al dali") == 1
|
|
38
|
+
assert matcher.match(possible_names, "xxx", "Damar") == 2
|
|
39
|
+
transform_possible_names = [lambda x: None]
|
|
40
|
+
assert (
|
|
41
|
+
matcher.match(possible_names, "xxx", "yyy", transform_possible_names)
|
|
42
|
+
is None
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def test_rapidfuzz_matcher_transliteration(self):
|
|
46
|
+
# UKR admin_name_mappings entries that Phonetics cannot match
|
|
47
|
+
possible_names = [
|
|
48
|
+
"Avtonomna Respublika Krym",
|
|
49
|
+
"Vinnytska",
|
|
50
|
+
"Volynska",
|
|
51
|
+
"Dnipropetrovska",
|
|
52
|
+
"Donetska",
|
|
53
|
+
"Zhytomyrska",
|
|
54
|
+
"Zakarpatska",
|
|
55
|
+
"Zaporizka",
|
|
56
|
+
"Ivano Frankivska",
|
|
57
|
+
"Kyivska",
|
|
58
|
+
"Kirovohradska",
|
|
59
|
+
"Luhanska",
|
|
60
|
+
"Lvivska",
|
|
61
|
+
"Mykolaivska",
|
|
62
|
+
"Odeska",
|
|
63
|
+
"Poltavska",
|
|
64
|
+
"Rivnenska",
|
|
65
|
+
"Sumska",
|
|
66
|
+
"Ternopilska",
|
|
67
|
+
"Kharkivska",
|
|
68
|
+
"Khersonska",
|
|
69
|
+
"Khmelnytska",
|
|
70
|
+
"Cherkaska",
|
|
71
|
+
"Chernivetska",
|
|
72
|
+
"Chernihivska",
|
|
73
|
+
"Sevastopol",
|
|
74
|
+
]
|
|
75
|
+
replacements = {" oblast": ""}
|
|
76
|
+
|
|
77
|
+
def resolve(matcher, query):
|
|
78
|
+
normalised_query = normalise(query)
|
|
79
|
+
alt = multiple_replace(normalised_query, replacements)
|
|
80
|
+
index = matcher.match(
|
|
81
|
+
possible_names, normalised_query, alternative_name=alt
|
|
82
|
+
)
|
|
83
|
+
return possible_names[index] if index is not None else None
|
|
84
|
+
|
|
85
|
+
phonetics = Phonetics()
|
|
86
|
+
rapidfuzz_matcher = RapidFuzzMatcher()
|
|
87
|
+
for query, expected in (
|
|
88
|
+
("Chernіvеtskaja Oblast", "Chernivetska"),
|
|
89
|
+
("Luganskaja Oblast", "Luhanska"),
|
|
90
|
+
("Rovenskaja Oblast", "Rivnenska"),
|
|
91
|
+
("Sevastopol City", "Sevastopol"),
|
|
92
|
+
("Zapоrіzskaja Oblast", "Zaporizka"),
|
|
93
|
+
):
|
|
94
|
+
assert resolve(phonetics, query) is None
|
|
95
|
+
assert resolve(rapidfuzz_matcher, query) == expected
|
|
96
|
+
|
|
97
|
+
# A naming-scheme difference, not a spelling variant
|
|
98
|
+
assert resolve(phonetics, "Chernigovskaja Oblast") is None
|
|
99
|
+
assert resolve(rapidfuzz_matcher, "Chernigovskaja Oblast") != "Chernihivska"
|
|
100
|
+
|
|
101
|
+
def test_rapidfuzz_matcher_naming_scheme_mismatch(self):
|
|
102
|
+
# Unrelated names that only admin_name_mappings can resolve
|
|
103
|
+
phonetics = Phonetics()
|
|
104
|
+
rapidfuzz_matcher = RapidFuzzMatcher()
|
|
105
|
+
cases = (
|
|
106
|
+
(["Federal Capital Territory"], "Abuja"), # NGA: informal vs formal name
|
|
107
|
+
(["Nando"], "Centre-Ouest"), # BFA: region renamed
|
|
108
|
+
(["Avtonomna Respublika Krym"], "Crimea"), # UKR: colloquial vs formal
|
|
109
|
+
)
|
|
110
|
+
for possible_names, query in cases:
|
|
111
|
+
assert phonetics.match(possible_names, normalise(query)) is None
|
|
112
|
+
assert rapidfuzz_matcher.match(possible_names, normalise(query)) is None
|
|
113
|
+
|
|
114
|
+
def test_rapidfuzz_matcher_extra_word_caveat(self):
|
|
115
|
+
# Phonetics rejects both; RapidFuzzMatcher accepts both
|
|
116
|
+
phonetics = Phonetics()
|
|
117
|
+
rapidfuzz_matcher = RapidFuzzMatcher()
|
|
118
|
+
for query in ("sevastopol city", "kenge city"):
|
|
119
|
+
name = query.split(" ")[0].capitalize()
|
|
120
|
+
assert phonetics.match([name], query) is None
|
|
121
|
+
assert rapidfuzz_matcher.match([name], query) == 0
|
|
122
|
+
|
|
123
|
+
def test_rapidfuzz_matcher_word_split(self):
|
|
124
|
+
# Admin names from pipeline inputs that differ from the COD name only
|
|
125
|
+
# in spacing/punctuation. token_set_ratio alone scores these < 65.
|
|
126
|
+
rapidfuzz_matcher = RapidFuzzMatcher()
|
|
127
|
+
for possible_names, query, expected in (
|
|
128
|
+
(["dawlat abad", "kaldar", "sharak e hayratan"], "dawlatabad", 0),
|
|
129
|
+
(["waghaz", "qara bagh", "jaghuri"], "qarabagh", 1),
|
|
130
|
+
(["pemagatshel", "paro", "punakha"], "pema gatshel", 0),
|
|
131
|
+
(["north east", "grande'anse", "artibonite"], "grande anse", 1),
|
|
132
|
+
(["west", "north east", "north west"], "nord ouest", 2),
|
|
133
|
+
):
|
|
134
|
+
assert rapidfuzz_matcher.match(possible_names, query) == expected
|
|
135
|
+
|
|
136
|
+
def test_rapidfuzz_matcher_different_extra_words(self):
|
|
137
|
+
# Names sharing a word where each has another word the other lacks.
|
|
138
|
+
# default_scorer matches "central kalimantan" to "kalimantan utara"
|
|
139
|
+
# and "bas sassandra" to "haut sassandra".
|
|
140
|
+
rapidfuzz_matcher = RapidFuzzMatcher(RapidFuzzMatcher.place_name_scorer)
|
|
141
|
+
kalimantan = [
|
|
142
|
+
"kalimantan barat",
|
|
143
|
+
"kalimantan tengah",
|
|
144
|
+
"kalimantan selatan",
|
|
145
|
+
"kalimantan timur",
|
|
146
|
+
"kalimantan utara",
|
|
147
|
+
]
|
|
148
|
+
for possible_names, query, expected in (
|
|
149
|
+
(kalimantan, "central kalimantan", None),
|
|
150
|
+
(kalimantan, "north kalimantan", None),
|
|
151
|
+
(["haut sassandra", "bas uele"], "bas sassandra", None),
|
|
152
|
+
(["centre east", "centre west"], "central west", 1),
|
|
153
|
+
(["hodh gharbi", "hodh chargui"], "hosh ech chargui", 1),
|
|
154
|
+
(["ad dali", "al maharah"], "al dali", 0),
|
|
155
|
+
):
|
|
156
|
+
assert rapidfuzz_matcher.match(possible_names, query) == expected
|
|
157
|
+
|
|
158
|
+
def test_rapidfuzz_matcher_subset_tie_break(self):
|
|
159
|
+
# Haiti has both "West" and "North West" departments. For the
|
|
160
|
+
# translated alternative "north west", token_set_ratio scores "west",
|
|
161
|
+
# "north" and "north west" all at 100; fuzz.ratio breaks the tie.
|
|
162
|
+
possible_names = [
|
|
163
|
+
"west",
|
|
164
|
+
"south east",
|
|
165
|
+
"north",
|
|
166
|
+
"north east",
|
|
167
|
+
"artibonite",
|
|
168
|
+
"centre",
|
|
169
|
+
"south",
|
|
170
|
+
"grande'anse",
|
|
171
|
+
"north west",
|
|
172
|
+
"nippes",
|
|
173
|
+
]
|
|
174
|
+
replacements = {"sud": "south", "ouest": "west", "est": "east", "nord": "north"}
|
|
175
|
+
rapidfuzz_matcher = RapidFuzzMatcher()
|
|
176
|
+
|
|
177
|
+
normalised_query = normalise("Nord-Ouest")
|
|
178
|
+
translated_query = multiple_replace(normalised_query, replacements)
|
|
179
|
+
assert translated_query == "north west"
|
|
180
|
+
|
|
181
|
+
index = rapidfuzz_matcher.match(possible_names, normalised_query)
|
|
182
|
+
assert possible_names[index] == "north west"
|
|
183
|
+
|
|
184
|
+
index = rapidfuzz_matcher.match(
|
|
185
|
+
possible_names, normalised_query, alternative_name=translated_query
|
|
186
|
+
)
|
|
187
|
+
assert possible_names[index] == "north west"
|
|
188
|
+
|
|
32
189
|
def test_get_code_from_name_org_type(self):
|
|
33
190
|
org_type_lookup = {
|
|
34
191
|
"Academic / Research": "431",
|
|
@@ -172,6 +329,99 @@ class TestMatching:
|
|
|
172
329
|
is None
|
|
173
330
|
)
|
|
174
331
|
|
|
332
|
+
def test_resolve_name_parts(self):
|
|
333
|
+
sector_lookup = {
|
|
334
|
+
"food security": "FSC",
|
|
335
|
+
"nutrition": "NUT",
|
|
336
|
+
"protection": "PRO",
|
|
337
|
+
"gbv": "PRO-GBV",
|
|
338
|
+
"wash": "WSH",
|
|
339
|
+
"abris": "SHL",
|
|
340
|
+
"sante": "HEA",
|
|
341
|
+
}
|
|
342
|
+
for name, expected in (
|
|
343
|
+
("Wash & Protection", (None, True)),
|
|
344
|
+
("Santé/Nutrition", (None, True)),
|
|
345
|
+
("Santé\nProtection", (None, True)),
|
|
346
|
+
("developpement, paix, genre", (None, True)),
|
|
347
|
+
("Protection - GBV", ("PRO-GBV", False)),
|
|
348
|
+
("GBV/Protection", ("PRO-GBV", False)),
|
|
349
|
+
("Wash - Wash", ("WSH", False)),
|
|
350
|
+
("Food Security and Livelihood", (None, False)),
|
|
351
|
+
("Abris et Non vivres", (None, False)),
|
|
352
|
+
("Protection", (None, False)),
|
|
353
|
+
):
|
|
354
|
+
assert resolve_name_parts(name, sector_lookup) == expected
|
|
355
|
+
falcon_lookup = {"acosta": "VE1101", "falcon": "VE1109"}
|
|
356
|
+
assert resolve_name_parts("Falcón, Acosta", falcon_lookup) == (None, True)
|
|
357
|
+
for name, expected in (
|
|
358
|
+
("Falcón, Acosta", ("VE1101", False)),
|
|
359
|
+
("Acosta, Falcón", ("VE1101", False)),
|
|
360
|
+
("Falcón, Falcón", ("VE1109", False)),
|
|
361
|
+
("Falcón, Acostta", (None, False)),
|
|
362
|
+
):
|
|
363
|
+
assert resolve_name_parts(name, falcon_lookup, "falcon") == expected
|
|
364
|
+
aze_lookup = {"guba": "AZ30300001", "khachmaz": "AZ30200001"}
|
|
365
|
+
for name, expected in (
|
|
366
|
+
("Guba-Khachmaz", (None, True)),
|
|
367
|
+
("Guba - Khachmaz", (None, True)),
|
|
368
|
+
("Guba-Khizi", (None, False)),
|
|
369
|
+
):
|
|
370
|
+
assert resolve_name_parts(name, aze_lookup) == expected
|
|
371
|
+
assert resolve_name_parts("Mbanza-Ngungu", {"mbanza ngungu": "CD2013"}) == (
|
|
372
|
+
None,
|
|
373
|
+
False,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def test_get_code_from_name_list_of_names(self):
|
|
377
|
+
sector_lookup = {
|
|
378
|
+
"Food Security": "FSC",
|
|
379
|
+
"Nutrition": "NUT",
|
|
380
|
+
"Protection": "PRO",
|
|
381
|
+
"Emergency Shelter and NFI": "SHL",
|
|
382
|
+
"Health": "HEA",
|
|
383
|
+
"Gender Based Violence": "PRO-GBV",
|
|
384
|
+
"GBV": "PRO-GBV",
|
|
385
|
+
}
|
|
386
|
+
actual_sector_lookup = {normalise(k): v for k, v in sector_lookup.items()}
|
|
387
|
+
actual_sector_lookup.update(sector_lookup)
|
|
388
|
+
matcher = RapidFuzzMatcher()
|
|
389
|
+
for name, code in (
|
|
390
|
+
("Protection - GBV", "PRO-GBV"),
|
|
391
|
+
("GBV - Protection", "PRO-GBV"),
|
|
392
|
+
("Health-Nutrition", None),
|
|
393
|
+
("Nutrition, Protection", None),
|
|
394
|
+
("Food Security and Livelihood", "FSC"),
|
|
395
|
+
("Emergency Shelter and NFI", "SHL"),
|
|
396
|
+
):
|
|
397
|
+
assert (
|
|
398
|
+
get_code_from_name(name, actual_sector_lookup, [], matcher=matcher)
|
|
399
|
+
== code
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def test_get_code_from_name_rapidfuzz_matcher(self):
|
|
403
|
+
sector_lookup = {
|
|
404
|
+
"Emergency Shelter and NFI": "SHL",
|
|
405
|
+
"Camp Coordination / Management": "CCM",
|
|
406
|
+
"Education": "EDU",
|
|
407
|
+
"Health": "HEA",
|
|
408
|
+
}
|
|
409
|
+
actual_sector_lookup = {normalise(k): v for k, v in sector_lookup.items()}
|
|
410
|
+
org_type_lookup = {
|
|
411
|
+
"International NGO": "437",
|
|
412
|
+
"National NGO": "441",
|
|
413
|
+
"United Nations": "447",
|
|
414
|
+
}
|
|
415
|
+
actual_org_type_lookup = {normalise(k): v for k, v in org_type_lookup.items()}
|
|
416
|
+
matcher = RapidFuzzMatcher()
|
|
417
|
+
for name, lookup, expected in (
|
|
418
|
+
("Educaton", actual_sector_lookup, "EDU"),
|
|
419
|
+
("Healht", actual_sector_lookup, "HEA"),
|
|
420
|
+
# WRatio matches this to "International NGO"
|
|
421
|
+
("COOPÉRATION_INTERNATIONALE", actual_org_type_lookup, None),
|
|
422
|
+
):
|
|
423
|
+
assert get_code_from_name(name, lookup, [], matcher=matcher) == expected
|
|
424
|
+
|
|
175
425
|
def test_multiple_replace(self):
|
|
176
426
|
result = multiple_replace(
|
|
177
427
|
self.a, {"quick": "slow", "fast": "slow", "lazy": "busy"}
|