hdx-python-utilities 4.1.1__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.1 → hdx_python_utilities-4.2.0}/PKG-INFO +3 -2
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/documentation/index.md +44 -1
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/pyproject.toml +2 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/_version.py +2 -2
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/downloader.py +81 -17
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/matching.py +211 -3
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/retriever.py +7 -1
- hdx_python_utilities-4.2.0/tests/fixtures/downloader/resume_test.dat +9 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_downloader.py +157 -1
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_matching.py +250 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_retriever.py +21 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/uv.lock +228 -1
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/.github/workflows/publish-test.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/.github/workflows/publish.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/.github/workflows/run-python-tests.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/.gitignore +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/.pre-commit-config.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/CLAUDE.md +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/LICENSE +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/README.md +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/documentation/.readthedocs.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/documentation/mkdocs.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/__init__.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/base_downloader.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/compare.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/dateparse.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/dictandlist.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/easy_logging.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/email.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/encoding.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/error_handler.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/errors_onexit.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/file_hashing.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/frictionless_wrapper.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/html.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/loader.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/path.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/saver.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/session.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/state.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/text.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/url.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/useragent.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/uuid.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/src/hdx/utilities/zip_crc.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/compare/test_csv_processing.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/compare/test_csv_processing2.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/empty.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/hdx_config.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/hdx_config.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/hdx_email_configuration.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/hdx_email_configuration.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/logging_config.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/logging_config.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/project_configuration.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/project_configuration.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/smtp_config.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/smtp_config.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/user_agent_config.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/user_agent_config2.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/user_agent_config3.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/config/user_agent_config_wrong.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/basicauth.txt +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/bearertoken.txt +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/extra_params.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/extra_params.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/extra_params_tree.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_csv_processing.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_csv_processing_blanks.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_data.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_data.xlsx +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_data1.csv/empty.txt +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_data2.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_json_processing.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_xls_processing.xls +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/downloader/test_xlsx_processing.xlsx +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/bad_header.zip +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/bad_index.xlsx +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/empty.zip +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/test.xlsx +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/test_shapefile.zip +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/file_hashing/valid_sig_invalid_body.zip +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/html/response.html +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/loader/empty.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/loader/empty.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/loader/empty_list.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/fallbacks/test.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/fallbacks/test.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/fallbacks/test.txt +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/fallbacks/test.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/retriever-test.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/test.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/test.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/test.txt +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/retriever/test.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out2.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out2.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out5.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out6.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out7.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out8.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/out8.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-false_sortkeys-false.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-false_sortkeys-false.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-false_sortkeys-true.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-false_sortkeys-true.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-true_sortkeys-false.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-true_sortkeys-false.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-true_sortkeys-true.json +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/saver/pretty-true_sortkeys-true.yaml +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/state/analysis_dates.txt +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/state/last_build_date.txt +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/test_data.csv +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/text/text1.txt +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/fixtures/text/text2.txt +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/conftest.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_compare.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_dateparse.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_dictandlist.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_easy_logging.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_email.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_encoding.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_error_handler.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_file_hashing.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_html.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_loader.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_path.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_saver.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_state.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_text.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_url.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_useragent.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_uuid.py +0 -0
- {hdx_python_utilities-4.1.1 → hdx_python_utilities-4.2.0}/tests/hdx/utilities/test_zip_crc.py +0 -0
- {hdx_python_utilities-4.1.1 → 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
|
|
|
@@ -205,7 +210,15 @@ Other useful functions:
|
|
|
205
210
|
# if no folder supplied
|
|
206
211
|
# path = Download.get_path_for_url(url, folder)
|
|
207
212
|
|
|
208
|
-
|
|
213
|
+
### Resumable downloads
|
|
214
|
+
|
|
215
|
+
Pass `resume=True` and a fixed `path` to continue an interrupted download using
|
|
216
|
+
HTTP Range requests. If the server returns 200 instead of 206, the file is
|
|
217
|
+
downloaded from scratch. Add `retries` to automatically retry mid-stream
|
|
218
|
+
failures; each retry continues from the current end of the partial file.
|
|
219
|
+
`retries` has no effect without `resume=True`.
|
|
220
|
+
|
|
221
|
+
path = downloader.download_file(url, path=mypath, resume=True, retries=3)
|
|
209
222
|
|
|
210
223
|
## Retrieving files
|
|
211
224
|
|
|
@@ -261,6 +274,8 @@ Examples:
|
|
|
261
274
|
retriever = Retrieve(downloader, fallback_dir, saved_dir, temp_dir, save=False, use_saved=True)
|
|
262
275
|
data = retriever.download_json(url, filename, logstr="test json", fallback=False, log_level=logging.DEBUG)
|
|
263
276
|
|
|
277
|
+
`Retrieve.download_file` also accepts `resume=True` and `retries=N` — see [Resumable downloads](#resumable-downloads).
|
|
278
|
+
|
|
264
279
|
## Date utilities
|
|
265
280
|
|
|
266
281
|
There are utilities to parse dates. By default, *no timezone information will be parsed
|
|
@@ -867,7 +882,19 @@ Efficient Hashing of files that produces a stable hash even for zip and xlsx fil
|
|
|
867
882
|
|
|
868
883
|
Examples:
|
|
869
884
|
|
|
885
|
+
# Fuzzy match a name to one of a list of names using rapidfuzz scoring
|
|
870
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
|
|
871
898
|
phonetics = Phonetics()
|
|
872
899
|
assert phonetics.match(possible_names, "al dali") == 1
|
|
873
900
|
|
|
@@ -877,6 +904,22 @@ Examples:
|
|
|
877
904
|
}
|
|
878
905
|
assert get_code_from_name("NATIONAL_NGO", lookup, [], fuzzy_match=False) == "441"
|
|
879
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
|
+
|
|
880
923
|
a = "The quick brown fox jumped over the lazy dog. It was so fast!"
|
|
881
924
|
|
|
882
925
|
# Replace multiple strings in a string simultaneously
|
|
@@ -52,6 +52,7 @@ dependencies = [
|
|
|
52
52
|
"typing_extensions",
|
|
53
53
|
"loguru",
|
|
54
54
|
"pyphonetics",
|
|
55
|
+
"rapidfuzz",
|
|
55
56
|
"python-dateutil>=2.9.0, <2.9.1",
|
|
56
57
|
"ratelimit",
|
|
57
58
|
"requests-file",
|
|
@@ -70,6 +71,7 @@ dev = [
|
|
|
70
71
|
"pytest",
|
|
71
72
|
"pytest-cov",
|
|
72
73
|
"pytest-loguru",
|
|
74
|
+
"pytest-mock",
|
|
73
75
|
"pre-commit",
|
|
74
76
|
"ruff==0.14.13",
|
|
75
77
|
]
|
|
@@ -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
|
|
@@ -250,13 +250,16 @@ class Download(BaseDownload):
|
|
|
250
250
|
f"Download of {url} failed in retrieval of stream!" % url
|
|
251
251
|
)
|
|
252
252
|
|
|
253
|
-
def stream_path(
|
|
253
|
+
def stream_path(
|
|
254
|
+
self, path: Path | str, errormsg: str, append: bool = False
|
|
255
|
+
) -> Path:
|
|
254
256
|
"""Stream file from url and store in provided path. Must call setup
|
|
255
257
|
method first.
|
|
256
258
|
|
|
257
259
|
Args:
|
|
258
260
|
path: Path for downloaded file
|
|
259
261
|
errormsg: Error message to display if there is a problem
|
|
262
|
+
append: Whether to append to an existing file. Defaults to False.
|
|
260
263
|
|
|
261
264
|
Returns:
|
|
262
265
|
Path of downloaded file
|
|
@@ -264,7 +267,7 @@ class Download(BaseDownload):
|
|
|
264
267
|
f = None
|
|
265
268
|
try:
|
|
266
269
|
path = Path(path)
|
|
267
|
-
f = path.open("wb")
|
|
270
|
+
f = path.open("ab" if append else "wb")
|
|
268
271
|
for chunk in self.response.iter_content(chunk_size=10240):
|
|
269
272
|
if chunk: # filter out keep-alive new chunks
|
|
270
273
|
f.write(chunk)
|
|
@@ -325,6 +328,8 @@ class Download(BaseDownload):
|
|
|
325
328
|
path (str): Full path to use for downloaded file instead of folder and filename.
|
|
326
329
|
overwrite (bool): Whether to overwrite existing file. Defaults to False.
|
|
327
330
|
keep (bool): Whether to keep already downloaded file. Defaults to False.
|
|
331
|
+
resume (bool): Whether to resume a partial download using Range requests where the server supports it. Defaults to False.
|
|
332
|
+
retries (int): Number of times to retry a mid-stream failure. Only effective when resume=True so that each retry can continue from the partial file. Defaults to 0.
|
|
328
333
|
post (bool): Whether to use POST instead of GET. Defaults to False.
|
|
329
334
|
parameters (dict): Parameters to pass. Defaults to None.
|
|
330
335
|
timeout (float): Timeout for connecting to URL. Defaults to None (no timeout).
|
|
@@ -340,25 +345,84 @@ class Download(BaseDownload):
|
|
|
340
345
|
path = kwargs.get("path")
|
|
341
346
|
overwrite = kwargs.get("overwrite", False)
|
|
342
347
|
keep = kwargs.get("keep", False)
|
|
348
|
+
resume = kwargs.get("resume", False)
|
|
349
|
+
retries = kwargs.get("retries", 0)
|
|
343
350
|
try:
|
|
344
|
-
|
|
351
|
+
# When resuming, skip uniqueness renaming so we get back the exact target path
|
|
352
|
+
path = get_path_for_url(
|
|
353
|
+
url, folder, filename, path, overwrite, keep or resume
|
|
354
|
+
)
|
|
345
355
|
except ValueError as ex:
|
|
346
356
|
raise DownloadError(ex) from ex
|
|
347
|
-
if keep and exists(path):
|
|
357
|
+
if keep and not resume and exists(path):
|
|
348
358
|
return path
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
359
|
+
errormsg = f"Download of {url} failed in retrieval of stream!"
|
|
360
|
+
setup_headers = dict(kwargs.get("headers") or {})
|
|
361
|
+
append = False
|
|
362
|
+
if resume and path.exists() and path.stat().st_size > 0:
|
|
363
|
+
setup_headers["Range"] = f"bytes={path.stat().st_size}-"
|
|
364
|
+
# Range bytes must come from the raw stream: partial gzip streams
|
|
365
|
+
# lack the header and cannot be decoded, so request uncompressed.
|
|
366
|
+
setup_headers.setdefault("Accept-Encoding", "identity")
|
|
367
|
+
try:
|
|
368
|
+
self.setup(
|
|
369
|
+
url,
|
|
370
|
+
stream=True,
|
|
371
|
+
post=kwargs.get("post", False),
|
|
372
|
+
parameters=kwargs.get("parameters"),
|
|
373
|
+
timeout=kwargs.get("timeout"),
|
|
374
|
+
headers=setup_headers,
|
|
375
|
+
encoding=kwargs.get("encoding"),
|
|
376
|
+
json_string=kwargs.get("json_string", False),
|
|
377
|
+
)
|
|
378
|
+
except DownloadError:
|
|
379
|
+
if self.response is not None and self.response.status_code == 416:
|
|
380
|
+
return path
|
|
381
|
+
raise
|
|
382
|
+
append = self.response.status_code == 206
|
|
383
|
+
else:
|
|
384
|
+
self.setup(
|
|
385
|
+
url,
|
|
386
|
+
stream=True,
|
|
387
|
+
post=kwargs.get("post", False),
|
|
388
|
+
parameters=kwargs.get("parameters"),
|
|
389
|
+
timeout=kwargs.get("timeout"),
|
|
390
|
+
headers=setup_headers or None,
|
|
391
|
+
encoding=kwargs.get("encoding"),
|
|
392
|
+
json_string=kwargs.get("json_string", False),
|
|
393
|
+
)
|
|
394
|
+
attempts_remaining = retries
|
|
395
|
+
while True:
|
|
396
|
+
try:
|
|
397
|
+
return self.stream_path(path, errormsg, append=append)
|
|
398
|
+
except DownloadError:
|
|
399
|
+
if not resume or attempts_remaining == 0:
|
|
400
|
+
raise
|
|
401
|
+
attempts_remaining -= 1
|
|
402
|
+
logger.warning(
|
|
403
|
+
"Download of %s interrupted, retrying (attempt %d of %d)...",
|
|
404
|
+
url,
|
|
405
|
+
retries - attempts_remaining,
|
|
406
|
+
retries,
|
|
407
|
+
)
|
|
408
|
+
setup_headers["Range"] = f"bytes={path.stat().st_size}-"
|
|
409
|
+
setup_headers.setdefault("Accept-Encoding", "identity")
|
|
410
|
+
try:
|
|
411
|
+
self.setup(
|
|
412
|
+
url,
|
|
413
|
+
stream=True,
|
|
414
|
+
post=kwargs.get("post", False),
|
|
415
|
+
parameters=kwargs.get("parameters"),
|
|
416
|
+
timeout=kwargs.get("timeout"),
|
|
417
|
+
headers=setup_headers,
|
|
418
|
+
encoding=kwargs.get("encoding"),
|
|
419
|
+
json_string=kwargs.get("json_string", False),
|
|
420
|
+
)
|
|
421
|
+
except DownloadError:
|
|
422
|
+
if self.response is not None and self.response.status_code == 416:
|
|
423
|
+
return path
|
|
424
|
+
raise
|
|
425
|
+
append = self.response.status_code == 206
|
|
362
426
|
|
|
363
427
|
def download(self, url: Path | str, **kwargs: Any) -> requests.Response:
|
|
364
428
|
"""Download url.
|
|
@@ -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,
|
|
@@ -184,6 +184,8 @@ class Retrieve(BaseDownload):
|
|
|
184
184
|
filename: str | None = None,
|
|
185
185
|
logstr: str | None = None,
|
|
186
186
|
fallback: bool = False,
|
|
187
|
+
resume: bool = False,
|
|
188
|
+
retries: int = 0,
|
|
187
189
|
log_level: int = None,
|
|
188
190
|
**kwargs: Any,
|
|
189
191
|
) -> Path:
|
|
@@ -194,6 +196,8 @@ class Retrieve(BaseDownload):
|
|
|
194
196
|
filename: Filename of saved file. Defaults to getting from url.
|
|
195
197
|
logstr: Text to use in log string to describe download. Defaults to filename.
|
|
196
198
|
fallback: Whether to use static fallback if download fails. Defaults to False.
|
|
199
|
+
resume: Whether to resume a partial download using Range requests where the server supports it. Defaults to False.
|
|
200
|
+
retries: Number of times to retry a mid-stream failure. Only effective when resume=True. Defaults to 0.
|
|
197
201
|
log_level: Level at which to log messages. Overrides level from constructor.
|
|
198
202
|
**kwargs: Parameters to pass to download_file call
|
|
199
203
|
|
|
@@ -219,7 +223,9 @@ class Retrieve(BaseDownload):
|
|
|
219
223
|
log_level,
|
|
220
224
|
f"Downloading {logstr} from {self.get_url_logstr(url)} into {output_path}",
|
|
221
225
|
)
|
|
222
|
-
return self.downloader.download_file(
|
|
226
|
+
return self.downloader.download_file(
|
|
227
|
+
url, path=output_path, resume=resume, retries=retries, **kwargs
|
|
228
|
+
)
|
|
223
229
|
except DownloadError:
|
|
224
230
|
if not fallback:
|
|
225
231
|
raise
|