hdx-python-utilities 4.1.2__tar.gz → 4.2.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.
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/PKG-INFO +3 -2
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/documentation/index.md +39 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/pyproject.toml +1 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/_version.py +2 -2
- hdx_python_utilities-4.2.1/src/hdx/utilities/matching.py +598 -0
- hdx_python_utilities-4.2.1/tests/fixtures/matching/wfp_admin_matcher_calls.json +5489 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_downloader.py +1 -1
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_matching.py +276 -0
- hdx_python_utilities-4.2.1/tests/hdx/utilities/test_matching_wfp_admin.py +76 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/uv.lock +214 -1
- hdx_python_utilities-4.1.2/src/hdx/utilities/matching.py +0 -332
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/.github/workflows/publish-test.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/.github/workflows/publish.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/.github/workflows/run-python-tests.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/.gitignore +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/.pre-commit-config.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/CLAUDE.md +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/LICENSE +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/README.md +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/documentation/.readthedocs.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/documentation/mkdocs.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/__init__.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/base_downloader.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/compare.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/dateparse.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/dictandlist.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/downloader.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/easy_logging.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/email.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/encoding.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/error_handler.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/errors_onexit.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/file_hashing.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/frictionless_wrapper.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/html.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/loader.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/path.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/retriever.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/saver.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/session.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/state.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/text.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/url.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/useragent.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/uuid.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/src/hdx/utilities/zip_crc.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/compare/test_csv_processing.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/compare/test_csv_processing2.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/empty.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/hdx_config.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/hdx_config.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/hdx_email_configuration.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/hdx_email_configuration.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/logging_config.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/logging_config.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/project_configuration.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/project_configuration.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/smtp_config.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/smtp_config.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/user_agent_config.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/user_agent_config2.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/user_agent_config3.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/config/user_agent_config_wrong.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/basicauth.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/bearertoken.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/extra_params.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/extra_params.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/extra_params_tree.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/resume_test.dat +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/test_csv_processing.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/test_csv_processing_blanks.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/test_data.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/test_data.xlsx +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/test_data1.csv/empty.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/test_data2.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/test_json_processing.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/test_xls_processing.xls +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/downloader/test_xlsx_processing.xlsx +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/file_hashing/bad_header.zip +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/file_hashing/bad_index.xlsx +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/file_hashing/empty.zip +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/file_hashing/test.xlsx +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/file_hashing/test_shapefile.zip +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/file_hashing/valid_sig_invalid_body.zip +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/html/response.html +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/loader/empty.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/loader/empty.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/loader/empty_list.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/retriever/fallbacks/test.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/retriever/fallbacks/test.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/retriever/fallbacks/test.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/retriever/fallbacks/test.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/retriever/retriever-test.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/retriever/test.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/retriever/test.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/retriever/test.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/retriever/test.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/out.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/out.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/out2.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/out2.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/out5.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/out6.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/out7.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/out8.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/out8.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/pretty-false_sortkeys-false.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/pretty-false_sortkeys-false.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/pretty-false_sortkeys-true.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/pretty-false_sortkeys-true.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/pretty-true_sortkeys-false.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/pretty-true_sortkeys-false.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/pretty-true_sortkeys-true.json +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/saver/pretty-true_sortkeys-true.yaml +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/state/analysis_dates.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/state/last_build_date.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/test_data.csv +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/text/text1.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/fixtures/text/text2.txt +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/conftest.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_compare.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_dateparse.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_dictandlist.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_easy_logging.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_email.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_encoding.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_error_handler.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_file_hashing.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_html.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_loader.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_path.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_retriever.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_saver.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_state.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_text.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_url.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_useragent.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_uuid.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/tests/hdx/utilities/test_zip_crc.py +0 -0
- {hdx_python_utilities-4.1.2 → hdx_python_utilities-4.2.1}/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.1
|
|
3
|
+
Version: 4.2.1
|
|
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,25 @@ 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
|
+
# place_name_scorer also expands a leading initial to a word starting
|
|
898
|
+
# with it and requires a trailing single letter in the possible name
|
|
899
|
+
shewa = ["west shewa", "east shewa"]
|
|
900
|
+
assert matcher.match(shewa, "e shewa") == 1
|
|
901
|
+
assert matcher.match(["region iv a calabarzon"], "region iv b") is None
|
|
902
|
+
|
|
903
|
+
# Phonetics is an alternative matcher using Refined Soundex
|
|
881
904
|
phonetics = Phonetics()
|
|
882
905
|
assert phonetics.match(possible_names, "al dali") == 1
|
|
883
906
|
|
|
@@ -887,6 +910,22 @@ Examples:
|
|
|
887
910
|
}
|
|
888
911
|
assert get_code_from_name("NATIONAL_NGO", lookup, [], fuzzy_match=False) == "441"
|
|
889
912
|
|
|
913
|
+
# get_code_from_name fuzzy matches with RapidFuzzMatcher unless given
|
|
914
|
+
# another matcher. It resolves names made of parts and does not fuzzy
|
|
915
|
+
# match lists of names.
|
|
916
|
+
sector_lookup = {"food security": "FSC", "protection": "PRO", "gbv": "PRO-GBV", "wash": "WSH"}
|
|
917
|
+
assert get_code_from_name("Food Securty", sector_lookup, []) == "FSC"
|
|
918
|
+
assert get_code_from_name("Food Securty", sector_lookup, [], matcher=Phonetics()) == "FSC"
|
|
919
|
+
assert get_code_from_name("Protection - GBV", sector_lookup, []) == "PRO-GBV"
|
|
920
|
+
assert get_code_from_name("Wash & Protection", sector_lookup, []) is None
|
|
921
|
+
|
|
922
|
+
# Resolve a name made of parts, returning the code and whether it is a
|
|
923
|
+
# list of names. The third parameter is a part that qualifies the other,
|
|
924
|
+
# eg. a parent admin name.
|
|
925
|
+
admin_lookup = {"acosta": "VE1101", "falcon": "VE1109"}
|
|
926
|
+
assert resolve_name_parts("Falcón, Acosta", admin_lookup) == (None, True)
|
|
927
|
+
assert resolve_name_parts("Falcón, Acosta", admin_lookup, "falcon") == ("VE1101", False)
|
|
928
|
+
|
|
890
929
|
a = "The quick brown fox jumped over the lazy dog. It was so fast!"
|
|
891
930
|
|
|
892
931
|
# 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.1
|
|
22
|
-
__version_tuple__ = version_tuple = (4,
|
|
21
|
+
__version__ = version = '4.2.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (4, 2, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
import re
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
|
|
5
|
+
from pyphonetics import RefinedSoundex
|
|
6
|
+
from rapidfuzz import fuzz as rapidfuzz_fuzz
|
|
7
|
+
|
|
8
|
+
from hdx.utilities.text import normalise
|
|
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*")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def expand_initials(words: list[str], other_words: list[str]) -> list[str] | None:
|
|
17
|
+
"""Check single letters at the start or end of words against
|
|
18
|
+
other_words. A leading letter is an initial and is replaced by an unused
|
|
19
|
+
word in other_words that is not in words and starts with it ("e shewa"
|
|
20
|
+
-> "east shewa", "b gumuz" -> "benishangul gumuz"). A trailing letter
|
|
21
|
+
distinguishes places ("region iv b") so must be in other_words. Single
|
|
22
|
+
letters between other words are left alone as they are often conjunctions
|
|
23
|
+
(eg. Spanish "y").
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
words: Words that may contain single letters
|
|
27
|
+
other_words: Words to expand single letters to
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Expanded words or None if a single letter has no counterpart
|
|
31
|
+
"""
|
|
32
|
+
leading = []
|
|
33
|
+
for word in words:
|
|
34
|
+
if len(word) != 1:
|
|
35
|
+
break
|
|
36
|
+
leading.append(word)
|
|
37
|
+
trailing = []
|
|
38
|
+
for word in reversed(words[len(leading) :]):
|
|
39
|
+
if len(word) != 1:
|
|
40
|
+
break
|
|
41
|
+
trailing.append(word)
|
|
42
|
+
for letter in trailing:
|
|
43
|
+
if letter.isalpha() and letter not in other_words:
|
|
44
|
+
return None
|
|
45
|
+
unmatched = [word for word in other_words if word not in words and len(word) > 1]
|
|
46
|
+
expanded = list(words)
|
|
47
|
+
for i, letter in enumerate(leading):
|
|
48
|
+
if not letter.isalpha() or letter in other_words:
|
|
49
|
+
continue
|
|
50
|
+
for other in unmatched:
|
|
51
|
+
if other[0] == letter:
|
|
52
|
+
unmatched.remove(other)
|
|
53
|
+
expanded[i] = other
|
|
54
|
+
break
|
|
55
|
+
else:
|
|
56
|
+
return None
|
|
57
|
+
return expanded
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Phonetics(RefinedSoundex):
|
|
61
|
+
def match(
|
|
62
|
+
self,
|
|
63
|
+
possible_names: Sequence,
|
|
64
|
+
name: str,
|
|
65
|
+
alternative_name: str | None = None,
|
|
66
|
+
transform_possible_names: Sequence[Callable] = [],
|
|
67
|
+
threshold: int = 2,
|
|
68
|
+
) -> int | None:
|
|
69
|
+
"""
|
|
70
|
+
Match name to one of the given possible names. Returns None if no match
|
|
71
|
+
or the index of the matching name
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
possible_names: Possible names
|
|
75
|
+
name: Name to match
|
|
76
|
+
alternative_name: Alternative name to match. Defaults to None.
|
|
77
|
+
transform_possible_names: Functions to transform possible names.
|
|
78
|
+
threshold: Match threshold. Defaults to 2.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Index of matching name from possible names or None
|
|
82
|
+
"""
|
|
83
|
+
mindistance = None
|
|
84
|
+
matching_index = None
|
|
85
|
+
|
|
86
|
+
transform_possible_names.insert(0, lambda x: x)
|
|
87
|
+
|
|
88
|
+
def check_name(name, possible_name):
|
|
89
|
+
nonlocal mindistance, matching_index # noqa: E999
|
|
90
|
+
|
|
91
|
+
distance = self.distance(name, possible_name)
|
|
92
|
+
if mindistance is None or distance < mindistance:
|
|
93
|
+
mindistance = distance
|
|
94
|
+
matching_index = i
|
|
95
|
+
|
|
96
|
+
for i, possible_name in enumerate(possible_names):
|
|
97
|
+
for transform_possible_name in transform_possible_names:
|
|
98
|
+
transformed_possible_name = transform_possible_name(possible_name)
|
|
99
|
+
if not transformed_possible_name:
|
|
100
|
+
continue
|
|
101
|
+
check_name(name, transformed_possible_name)
|
|
102
|
+
if alternative_name:
|
|
103
|
+
check_name(alternative_name, transformed_possible_name)
|
|
104
|
+
if mindistance is None or mindistance > threshold:
|
|
105
|
+
return None
|
|
106
|
+
return matching_index
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class RapidFuzzMatcher:
|
|
110
|
+
"""
|
|
111
|
+
Default matcher for names, using rapidfuzz edit-distance scoring instead
|
|
112
|
+
of phonetic (Soundex-family) encoding as Phonetics does. Phonetic algorithms
|
|
113
|
+
encode English pronunciation rules, so they are a poor fit for matching
|
|
114
|
+
multilingual or transliterated names (eg. "Sana'a" vs "Sanaa") that are
|
|
115
|
+
spelling variants rather than being phonetically similar in English
|
|
116
|
+
terms. Edit distance on normalised strings tends to place such variants
|
|
117
|
+
closer together.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
scorer: Function taking two strings and returning a similarity score
|
|
121
|
+
from 0-100. Defaults to default_scorer. Use place_name_scorer for
|
|
122
|
+
place names. Ties are broken by rapidfuzz.fuzz.ratio over the
|
|
123
|
+
whole strings.
|
|
124
|
+
|
|
125
|
+
Caveat: "<name> city" scores ~100 against "<name>", as for any name whose
|
|
126
|
+
words are a subset of the other's. So withholding a text replacement that
|
|
127
|
+
strips " city" does not stop "Kenge city" matching "Kenge".
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, scorer: Callable[[str, str], float] | None = None) -> None:
|
|
131
|
+
self.scorer = scorer or self.default_scorer
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def default_scorer(name: str, possible_name: str) -> float:
|
|
135
|
+
"""
|
|
136
|
+
Score two strings with token_set_ratio, or with ratio on the strings
|
|
137
|
+
with spaces and punctuation removed if that is at least 90 and higher.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
name: Name to match
|
|
141
|
+
possible_name: Possible name
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Similarity score from 0-100
|
|
145
|
+
"""
|
|
146
|
+
score = rapidfuzz_fuzz.token_set_ratio(name, possible_name)
|
|
147
|
+
joined_score = rapidfuzz_fuzz.ratio(
|
|
148
|
+
NON_WORD_CHARACTERS.sub("", name),
|
|
149
|
+
NON_WORD_CHARACTERS.sub("", possible_name),
|
|
150
|
+
)
|
|
151
|
+
if joined_score >= 90:
|
|
152
|
+
return max(score, joined_score)
|
|
153
|
+
return score
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def place_name_scorer(name: str, possible_name: str) -> float:
|
|
157
|
+
"""
|
|
158
|
+
Score two strings with default_scorer, but score 0 if they share a
|
|
159
|
+
word, each has other words of 3+ characters, and one of the words on
|
|
160
|
+
the side with fewer has ratio < 60 to all those on the other side (eg.
|
|
161
|
+
"central kalimantan" vs "kalimantan utara", but not "central west" vs
|
|
162
|
+
"centre west"). Suits place names, where the differing word usually
|
|
163
|
+
distinguishes places, but not names like sectors, where extra words
|
|
164
|
+
are often descriptive (eg. "food security and livelihood").
|
|
165
|
+
|
|
166
|
+
Single letters at the start or end of name are checked first with
|
|
167
|
+
expand_initials ("e shewa" scores 0 against "west shewa" but matches
|
|
168
|
+
"east shewa"), unless the names differ only in spaces and punctuation
|
|
169
|
+
(eg. "sana a" vs "sanaa").
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
name: Name to match
|
|
173
|
+
possible_name: Possible name
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Similarity score from 0-100
|
|
177
|
+
"""
|
|
178
|
+
joined_score = rapidfuzz_fuzz.ratio(
|
|
179
|
+
NON_WORD_CHARACTERS.sub("", name),
|
|
180
|
+
NON_WORD_CHARACTERS.sub("", possible_name),
|
|
181
|
+
)
|
|
182
|
+
if joined_score < 90:
|
|
183
|
+
name_words = expand_initials(name.split(), possible_name.split())
|
|
184
|
+
if name_words is None:
|
|
185
|
+
return 0.0
|
|
186
|
+
name = " ".join(name_words)
|
|
187
|
+
words = set(name.split())
|
|
188
|
+
possible_words = set(possible_name.split())
|
|
189
|
+
|
|
190
|
+
def long_words(words: set[str]) -> set[str]:
|
|
191
|
+
return {word for word in words if len(word) > 2}
|
|
192
|
+
|
|
193
|
+
extra_words = long_words(words - possible_words)
|
|
194
|
+
possible_extra_words = long_words(possible_words - words)
|
|
195
|
+
if words & possible_words and extra_words and possible_extra_words:
|
|
196
|
+
fewer, more = sorted((extra_words, possible_extra_words), key=len)
|
|
197
|
+
for word in fewer:
|
|
198
|
+
if max(rapidfuzz_fuzz.ratio(word, other) for other in more) < 60:
|
|
199
|
+
return 0.0
|
|
200
|
+
return RapidFuzzMatcher.default_scorer(name, possible_name)
|
|
201
|
+
|
|
202
|
+
def match(
|
|
203
|
+
self,
|
|
204
|
+
possible_names: Sequence,
|
|
205
|
+
name: str,
|
|
206
|
+
alternative_name: str | None = None,
|
|
207
|
+
transform_possible_names: Sequence[Callable] = [],
|
|
208
|
+
threshold: float = 65.0,
|
|
209
|
+
) -> int | None:
|
|
210
|
+
"""
|
|
211
|
+
Match name to one of the given possible names. Returns None if no
|
|
212
|
+
match or the index of the matching name.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
possible_names: Possible names
|
|
216
|
+
name: Name to match
|
|
217
|
+
alternative_name: Alternative name to match. Defaults to None.
|
|
218
|
+
transform_possible_names: Functions to transform possible names.
|
|
219
|
+
threshold: Minimum similarity score, 0-100. Defaults to 65.0.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Index of matching name from possible names or None
|
|
223
|
+
"""
|
|
224
|
+
maxscore = None
|
|
225
|
+
matching_index = None
|
|
226
|
+
names = [name.lower()]
|
|
227
|
+
if alternative_name:
|
|
228
|
+
names.append(alternative_name.lower())
|
|
229
|
+
all_transforms = (lambda x: x, *transform_possible_names)
|
|
230
|
+
for i, possible_name in enumerate(possible_names):
|
|
231
|
+
for transform_possible_name in all_transforms:
|
|
232
|
+
transformed_possible_name = transform_possible_name(possible_name)
|
|
233
|
+
if not transformed_possible_name:
|
|
234
|
+
continue
|
|
235
|
+
transformed_possible_name = transformed_possible_name.lower()
|
|
236
|
+
for query in names:
|
|
237
|
+
score = (
|
|
238
|
+
self.scorer(query, transformed_possible_name),
|
|
239
|
+
rapidfuzz_fuzz.ratio(query, transformed_possible_name),
|
|
240
|
+
)
|
|
241
|
+
if maxscore is None or score > maxscore:
|
|
242
|
+
maxscore = score
|
|
243
|
+
matching_index = i
|
|
244
|
+
if maxscore is None or maxscore[0] < threshold:
|
|
245
|
+
return None
|
|
246
|
+
return matching_index
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def split_name(name: str, separators: re.Pattern) -> list[str]:
|
|
250
|
+
"""
|
|
251
|
+
Split name on separators, dropping parts that are empty once normalised.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
name: Name to split
|
|
255
|
+
separators: Compiled regular expression matching separators
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
List of parts
|
|
259
|
+
"""
|
|
260
|
+
parts = [part.strip() for part in separators.split(name)]
|
|
261
|
+
return [part for part in parts if normalise(part)]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def resolve_name_parts(
|
|
265
|
+
name: str, name_to_code: dict[str, str], ignore: str | None = None
|
|
266
|
+
) -> tuple[str | None, bool]:
|
|
267
|
+
"""
|
|
268
|
+
Resolve a name made of parts split by separators (",", ";", "/", ":", "&",
|
|
269
|
+
newline, "and", "et") or, if there are none, by a hyphen. Parts are looked
|
|
270
|
+
up in name_to_code. A name with two parts gives a code if:
|
|
271
|
+
- one part's normalised form equals ignore and the other part has a code
|
|
272
|
+
(eg. "Falcón, Acosta" with the parent admin name "falcon")
|
|
273
|
+
- one part's code followed by "-" starts the other's (eg. "Protection -
|
|
274
|
+
GBV" gives PRO-GBV)
|
|
275
|
+
- both parts have the same code.
|
|
276
|
+
It is a list of names (eg. "Wash & Protection") if it has more than two
|
|
277
|
+
parts, or two parts with different codes (eg. "Guba-Khachmaz").
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
name: Name to resolve
|
|
281
|
+
name_to_code: Mapping from names (raw or normalised) to codes
|
|
282
|
+
ignore: Normalised name of a part that qualifies the other part.
|
|
283
|
+
Defaults to None.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Tuple of code (or None) and whether name is a list of names
|
|
287
|
+
"""
|
|
288
|
+
parts = split_name(name, LIST_SEPARATORS)
|
|
289
|
+
if len(parts) > 2:
|
|
290
|
+
return None, True
|
|
291
|
+
if len(parts) < 2:
|
|
292
|
+
parts = split_name(name, HYPHEN)
|
|
293
|
+
if len(parts) != 2:
|
|
294
|
+
return None, False
|
|
295
|
+
normalised_parts = [normalise(part) for part in parts]
|
|
296
|
+
if ignore in normalised_parts:
|
|
297
|
+
index = 1 - normalised_parts.index(ignore)
|
|
298
|
+
parts = [parts[index]]
|
|
299
|
+
normalised_parts = [normalised_parts[index]]
|
|
300
|
+
codes = []
|
|
301
|
+
for part, normalised_part in zip(parts, normalised_parts):
|
|
302
|
+
code = name_to_code.get(part, name_to_code.get(normalised_part))
|
|
303
|
+
if not code:
|
|
304
|
+
return None, False
|
|
305
|
+
codes.append(code)
|
|
306
|
+
code = codes[0]
|
|
307
|
+
if len(codes) == 1:
|
|
308
|
+
return code, False
|
|
309
|
+
other_code = codes[1]
|
|
310
|
+
if code == other_code or other_code.startswith(f"{code}-"):
|
|
311
|
+
return other_code, False
|
|
312
|
+
if code.startswith(f"{other_code}-"):
|
|
313
|
+
return code, False
|
|
314
|
+
return None, True
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def get_code_from_name(
|
|
318
|
+
name: str,
|
|
319
|
+
code_lookup: dict[str, str],
|
|
320
|
+
unmatched: list[str],
|
|
321
|
+
fuzzy_match: bool = True,
|
|
322
|
+
match_threshold: int = 5,
|
|
323
|
+
matcher: Phonetics | RapidFuzzMatcher | None = None,
|
|
324
|
+
) -> str | None:
|
|
325
|
+
"""
|
|
326
|
+
Given a name (org type, sector, etc), return the corresponding code. A
|
|
327
|
+
name made of parts is resolved by resolve_name_parts, and is not fuzzy
|
|
328
|
+
matched if it is a list of names.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
name: Name to match
|
|
332
|
+
code_lookup: Dictionary of official names and codes
|
|
333
|
+
unmatched: List of unmatched names
|
|
334
|
+
fuzzy_match: Allow fuzzy matching or not
|
|
335
|
+
match_threshold: Match threshold
|
|
336
|
+
matcher: Fuzzy matcher to use. Defaults to None (RapidFuzzMatcher).
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Matching code
|
|
340
|
+
"""
|
|
341
|
+
code = code_lookup.get(name)
|
|
342
|
+
if code:
|
|
343
|
+
return code
|
|
344
|
+
if name in unmatched:
|
|
345
|
+
return None
|
|
346
|
+
name_clean = normalise(name)
|
|
347
|
+
code = code_lookup.get(name_clean)
|
|
348
|
+
if code:
|
|
349
|
+
code_lookup[name] = code
|
|
350
|
+
return code
|
|
351
|
+
if len(name) <= match_threshold:
|
|
352
|
+
unmatched.append(name)
|
|
353
|
+
return None
|
|
354
|
+
code, is_list = resolve_name_parts(name, code_lookup)
|
|
355
|
+
if code:
|
|
356
|
+
code_lookup[name] = code
|
|
357
|
+
return code
|
|
358
|
+
if not fuzzy_match or is_list:
|
|
359
|
+
unmatched.append(name)
|
|
360
|
+
return None
|
|
361
|
+
names = [x for x in code_lookup.keys() if len(x) > match_threshold]
|
|
362
|
+
if matcher is None:
|
|
363
|
+
matcher = RapidFuzzMatcher()
|
|
364
|
+
name_index = matcher.match(
|
|
365
|
+
possible_names=names,
|
|
366
|
+
name=name,
|
|
367
|
+
alternative_name=name_clean,
|
|
368
|
+
)
|
|
369
|
+
if name_index is None:
|
|
370
|
+
unmatched.append(name)
|
|
371
|
+
return None
|
|
372
|
+
code = code_lookup.get(names[name_index])
|
|
373
|
+
if code:
|
|
374
|
+
code_lookup[name] = code
|
|
375
|
+
code_lookup[name_clean] = code
|
|
376
|
+
return code
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def multiple_replace(string: str, replacements: dict[str, str]) -> str:
|
|
380
|
+
"""Simultaneously replace multiple strings in a string.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
string: Input string
|
|
384
|
+
replacements: Replacements dictionary
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
String with replacements
|
|
388
|
+
"""
|
|
389
|
+
if not replacements:
|
|
390
|
+
return string
|
|
391
|
+
pattern = re.compile(
|
|
392
|
+
"|".join([re.escape(k) for k in sorted(replacements, key=len, reverse=True)]),
|
|
393
|
+
flags=re.DOTALL,
|
|
394
|
+
)
|
|
395
|
+
return pattern.sub(lambda x: replacements[x.group(0)], string)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def match_template_variables(
|
|
399
|
+
string: str,
|
|
400
|
+
) -> tuple[str | None, str | None]:
|
|
401
|
+
"""Try to match {{XXX}} in input string.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
string: String in which to look for template
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
(Matched string with brackets, matched string without brackets)
|
|
408
|
+
"""
|
|
409
|
+
match = TEMPLATE_VARIABLES.search(string)
|
|
410
|
+
if match:
|
|
411
|
+
template_string = match.group()
|
|
412
|
+
return template_string, template_string[2:-2]
|
|
413
|
+
return None, None
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def earliest_index(string_to_search: str, strings_to_try: Sequence[str]) -> int | None:
|
|
417
|
+
"""Search a string for each of a list of strings and return the earliest
|
|
418
|
+
index.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
string_to_search: String to search
|
|
422
|
+
strings_to_try: Strings to try
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Earliest index of the strings to try in string to search or None
|
|
426
|
+
"""
|
|
427
|
+
after_string = len(string_to_search) + 1
|
|
428
|
+
indices = []
|
|
429
|
+
for string_to_try in strings_to_try:
|
|
430
|
+
try:
|
|
431
|
+
index = string_to_search.index(string_to_try)
|
|
432
|
+
indices.append(index)
|
|
433
|
+
except ValueError:
|
|
434
|
+
indices.append(after_string)
|
|
435
|
+
earliest_index = sorted(indices)[0]
|
|
436
|
+
if earliest_index == after_string:
|
|
437
|
+
return None
|
|
438
|
+
else:
|
|
439
|
+
return earliest_index
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def get_matching_text_in_strs(
|
|
443
|
+
a: str,
|
|
444
|
+
b: str,
|
|
445
|
+
match_min_size: int = 30,
|
|
446
|
+
ignore: str = "",
|
|
447
|
+
end_characters: str = "",
|
|
448
|
+
) -> list[str]:
|
|
449
|
+
"""Returns a list of matching blocks of text in a and b.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
a: First string to match
|
|
453
|
+
b: Second string to match
|
|
454
|
+
match_min_size: Minimum block size to match on. Defaults to 30.
|
|
455
|
+
ignore: Any characters to ignore in matching. Defaults to ''.
|
|
456
|
+
end_characters: End characters to look for. Defaults to ''.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
List of matching blocks of text
|
|
460
|
+
"""
|
|
461
|
+
compare = difflib.SequenceMatcher(lambda x: x in ignore)
|
|
462
|
+
compare.set_seqs(a=a, b=b)
|
|
463
|
+
matching_text = []
|
|
464
|
+
|
|
465
|
+
for match in compare.get_matching_blocks():
|
|
466
|
+
start = match.a
|
|
467
|
+
text = a[start : start + match.size]
|
|
468
|
+
if end_characters:
|
|
469
|
+
prev_text = text
|
|
470
|
+
while len(text) != 0 and text[0] in end_characters:
|
|
471
|
+
text = text[1:]
|
|
472
|
+
while len(text) != 0 and text[-1] not in end_characters:
|
|
473
|
+
text = text[:-1]
|
|
474
|
+
if len(text) == 0:
|
|
475
|
+
text = prev_text
|
|
476
|
+
if len(text) >= match_min_size:
|
|
477
|
+
matching_text.append(text)
|
|
478
|
+
return matching_text
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def get_matching_text(
|
|
482
|
+
string_list: list[str],
|
|
483
|
+
match_min_size: int = 30,
|
|
484
|
+
ignore: str = "",
|
|
485
|
+
end_characters: str = ".!\r\n",
|
|
486
|
+
) -> str:
|
|
487
|
+
"""Returns a string containing matching blocks of text in a list of strings
|
|
488
|
+
followed by non-matching.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
string_list: List of strings to match
|
|
492
|
+
match_min_size: Minimum block size to match on. Defaults to 30.
|
|
493
|
+
ignore: Any characters to ignore in matching. Defaults to ''.
|
|
494
|
+
end_characters: End characters to look for. Defaults to '.\r\n'.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
String containing matching blocks of text followed by non-matching
|
|
498
|
+
"""
|
|
499
|
+
a = string_list[0]
|
|
500
|
+
for i in range(1, len(string_list)):
|
|
501
|
+
b = string_list[i]
|
|
502
|
+
result = get_matching_text_in_strs(
|
|
503
|
+
a,
|
|
504
|
+
b,
|
|
505
|
+
match_min_size=match_min_size,
|
|
506
|
+
ignore=ignore,
|
|
507
|
+
end_characters=end_characters,
|
|
508
|
+
)
|
|
509
|
+
a = "".join(result)
|
|
510
|
+
return a
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def get_matching_then_nonmatching_text(
|
|
514
|
+
string_list: list[str],
|
|
515
|
+
separator: str = "",
|
|
516
|
+
match_min_size: int = 30,
|
|
517
|
+
ignore: str = "",
|
|
518
|
+
end_characters: str = ".!\r\n",
|
|
519
|
+
) -> str:
|
|
520
|
+
"""Returns a string containing matching blocks of text in a list of strings
|
|
521
|
+
followed by non-matching.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
string_list: List of strings to match
|
|
525
|
+
separator: Separator to add between blocks of text. Defaults to ''.
|
|
526
|
+
match_min_size: Minimum block size to match on. Defaults to 30.
|
|
527
|
+
ignore: Any characters to ignore in matching. Defaults to ''.
|
|
528
|
+
end_characters: End characters to look for. Defaults to '.\r\n'.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
String containing matching blocks of text followed by non-matching
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
def add_separator_if_needed(text_list):
|
|
535
|
+
if (
|
|
536
|
+
separator
|
|
537
|
+
and len(text_list) > 0
|
|
538
|
+
and text_list[-1][-len(separator) :] != separator
|
|
539
|
+
):
|
|
540
|
+
text_list.append(separator)
|
|
541
|
+
|
|
542
|
+
a = string_list[0]
|
|
543
|
+
for i in range(1, len(string_list)):
|
|
544
|
+
b = string_list[i]
|
|
545
|
+
combined_len = len(a) + len(b)
|
|
546
|
+
result = get_matching_text_in_strs(
|
|
547
|
+
a,
|
|
548
|
+
b,
|
|
549
|
+
match_min_size=match_min_size,
|
|
550
|
+
ignore=ignore,
|
|
551
|
+
end_characters=end_characters,
|
|
552
|
+
)
|
|
553
|
+
new_a = a
|
|
554
|
+
new_b = b
|
|
555
|
+
for text in result:
|
|
556
|
+
new_a = new_a.replace(text, "")
|
|
557
|
+
new_b = new_b.replace(text, "")
|
|
558
|
+
if new_a and new_a in a:
|
|
559
|
+
pos_a = a.index(new_a)
|
|
560
|
+
else:
|
|
561
|
+
pos_a = combined_len
|
|
562
|
+
if new_b and new_b in b:
|
|
563
|
+
pos_b = b.index(new_b)
|
|
564
|
+
else:
|
|
565
|
+
pos_b = combined_len
|
|
566
|
+
if pos_b > pos_a:
|
|
567
|
+
text_1 = new_b
|
|
568
|
+
pos_1 = pos_b
|
|
569
|
+
text_2 = new_a
|
|
570
|
+
pos_2 = pos_a
|
|
571
|
+
else:
|
|
572
|
+
text_1 = new_a
|
|
573
|
+
pos_1 = pos_a
|
|
574
|
+
text_2 = new_b
|
|
575
|
+
pos_2 = pos_b
|
|
576
|
+
output = []
|
|
577
|
+
pos = 0
|
|
578
|
+
for text in result:
|
|
579
|
+
output.append(text)
|
|
580
|
+
pos += len(text)
|
|
581
|
+
if text_1 and pos >= pos_1:
|
|
582
|
+
add_separator_if_needed(output)
|
|
583
|
+
output.append(text_1)
|
|
584
|
+
pos += len(text_1)
|
|
585
|
+
text_1 = None
|
|
586
|
+
if text_2 and pos >= pos_2:
|
|
587
|
+
add_separator_if_needed(output)
|
|
588
|
+
output.append(text_2)
|
|
589
|
+
pos += len(text_2)
|
|
590
|
+
text_2 = None
|
|
591
|
+
if text_1 and pos_1 == combined_len:
|
|
592
|
+
add_separator_if_needed(output)
|
|
593
|
+
output.append(text_1)
|
|
594
|
+
if text_2 and pos_2 == combined_len:
|
|
595
|
+
add_separator_if_needed(output)
|
|
596
|
+
output.append(text_2)
|
|
597
|
+
a = "".join(output)
|
|
598
|
+
return a
|