hdx-python-pipelineutils 0.0.1__py3-none-any.whl
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/pipelineutils/__init__.py +55 -0
- hdx/python/pipelineutils/_version.py +24 -0
- hdx/python/pipelineutils/hapi_admins.py +117 -0
- hdx/python/pipelineutils/lookup.py +113 -0
- hdx/python/pipelineutils/org_type.py +8 -0
- hdx/python/pipelineutils/org_type_configuration.yaml +76 -0
- hdx/python/pipelineutils/reader.py +595 -0
- hdx/python/pipelineutils/sector.py +8 -0
- hdx/python/pipelineutils/sector_configuration.yaml +179 -0
- hdx_python_pipelineutils-0.0.1.dist-info/METADATA +50 -0
- hdx_python_pipelineutils-0.0.1.dist-info/RECORD +13 -0
- hdx_python_pipelineutils-0.0.1.dist-info/WHEEL +4 -0
- hdx_python_pipelineutils-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from hdx.data.dataset import Dataset
|
|
5
|
+
|
|
6
|
+
template = re.compile("{{.*?}}")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def string_params_to_dict(string: str) -> dict[str, str]:
|
|
10
|
+
params = {}
|
|
11
|
+
if not string:
|
|
12
|
+
return params
|
|
13
|
+
for name_par in string.split(","):
|
|
14
|
+
name, par = name_par.strip().split(":")
|
|
15
|
+
params[name] = par.strip()
|
|
16
|
+
return params
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def match_template(input: str) -> tuple[str | None, str | None]:
|
|
20
|
+
"""Try to match {{XXX}} in input string
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
input: String in which to look for template
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
(Matched string with brackets, matched string without brackets)
|
|
27
|
+
"""
|
|
28
|
+
match = template.search(input)
|
|
29
|
+
if match:
|
|
30
|
+
template_string = match.group()
|
|
31
|
+
return template_string, template_string[2:-2]
|
|
32
|
+
return None, None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_startend_dates_from_time_period(
|
|
36
|
+
dataset: Dataset, today: datetime | None = None
|
|
37
|
+
) -> dict | None:
|
|
38
|
+
"""Return the time period in form required for source_date
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
dataset: Dataset object
|
|
42
|
+
today: Date to use for today. Default is None (datetime.utcnow)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Time period in form required for source_date
|
|
46
|
+
"""
|
|
47
|
+
if today is None:
|
|
48
|
+
date_info = dataset.get_time_period()
|
|
49
|
+
else:
|
|
50
|
+
date_info = dataset.get_time_period(today=today)
|
|
51
|
+
startdate = date_info.get("startdate")
|
|
52
|
+
enddate = date_info.get("enddate")
|
|
53
|
+
if enddate is None:
|
|
54
|
+
return None
|
|
55
|
+
return {"start": startdate, "end": enddate}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.0.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from hdx.location.adminlevel import AdminLevel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def complete_admins(
|
|
5
|
+
admins: list[AdminLevel],
|
|
6
|
+
countryiso3: str,
|
|
7
|
+
provider_adm_names: list,
|
|
8
|
+
adm_codes: list,
|
|
9
|
+
adm_names: list,
|
|
10
|
+
fuzzy_match: bool = True,
|
|
11
|
+
) -> tuple[int, list[str]]:
|
|
12
|
+
"""Use information from adm_codes to populate adm_names and from
|
|
13
|
+
provider_adm_names to populate adm_codes with outptu of the admin level
|
|
14
|
+
and arnings for unknown and mismatched p-codes. All provided lists
|
|
15
|
+
should be of the same length.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
admins: List of AdminLevel objects
|
|
19
|
+
countryiso3: Country ISO3 code
|
|
20
|
+
provider_adm_names: List of provider adm names
|
|
21
|
+
adm_codes: List of adm codes
|
|
22
|
+
adm_names: List of adm names
|
|
23
|
+
fuzzy_match: Whether to use fuzzy matching. Default is True.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Admin level and warnings
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
warnings = []
|
|
30
|
+
child = None
|
|
31
|
+
adm_level = len(provider_adm_names)
|
|
32
|
+
|
|
33
|
+
def check_unknown_pcode(adm_code: str, pcode: str) -> str:
|
|
34
|
+
if pcode:
|
|
35
|
+
warnings.append(f"PCode unknown {adm_code}->{pcode} ({warntxt})")
|
|
36
|
+
return pcode
|
|
37
|
+
else:
|
|
38
|
+
warnings.append(f"PCode unknown {adm_code}->''")
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
for i, provider_adm_name in reversed(list(enumerate(provider_adm_names))):
|
|
42
|
+
adm_code = adm_codes[i]
|
|
43
|
+
parent = admins[i].pcode_to_parent.get(adm_code)
|
|
44
|
+
if not parent and i > 0:
|
|
45
|
+
parent = adm_codes[i - 1]
|
|
46
|
+
if not provider_adm_name:
|
|
47
|
+
provider_adm_name = ""
|
|
48
|
+
provider_adm_names[i] = ""
|
|
49
|
+
if child:
|
|
50
|
+
pcode = admins[i + 1].pcode_to_parent.get(child)
|
|
51
|
+
warntxt = "parent"
|
|
52
|
+
elif provider_adm_name:
|
|
53
|
+
pcode, _ = admins[i].get_pcode(
|
|
54
|
+
countryiso3,
|
|
55
|
+
provider_adm_name,
|
|
56
|
+
parent=parent,
|
|
57
|
+
fuzzy_match=fuzzy_match,
|
|
58
|
+
)
|
|
59
|
+
warntxt = f"provider_adm{i + 1}_name"
|
|
60
|
+
else:
|
|
61
|
+
pcode = None
|
|
62
|
+
if adm_code:
|
|
63
|
+
if adm_code not in admins[i].pcodes:
|
|
64
|
+
if admins[i].looks_like_pcode(adm_code):
|
|
65
|
+
adj_adm_code = admins[i].convert_admin_pcode_length(
|
|
66
|
+
countryiso3, adm_code, parent=parent
|
|
67
|
+
)
|
|
68
|
+
if adj_adm_code:
|
|
69
|
+
warnings.append(f"PCode length {adm_code}->{adj_adm_code}")
|
|
70
|
+
adm_code = adj_adm_code
|
|
71
|
+
else:
|
|
72
|
+
adm_code = check_unknown_pcode(adm_code, pcode)
|
|
73
|
+
else:
|
|
74
|
+
adm_code = check_unknown_pcode(adm_code, pcode)
|
|
75
|
+
elif pcode and adm_code != pcode:
|
|
76
|
+
if child:
|
|
77
|
+
warnings.append(f"PCode mismatch {adm_code}->{pcode} ({warntxt})")
|
|
78
|
+
adm_code = pcode
|
|
79
|
+
else:
|
|
80
|
+
warnings.append(f"PCode mismatch {adm_code} != {provider_adm_name}")
|
|
81
|
+
elif pcode:
|
|
82
|
+
adm_code = pcode
|
|
83
|
+
else:
|
|
84
|
+
adm_code = ""
|
|
85
|
+
adm_codes[i] = adm_code
|
|
86
|
+
if adm_code:
|
|
87
|
+
adm_names[i] = admins[i].pcode_to_name.get(adm_code, "")
|
|
88
|
+
child = adm_code
|
|
89
|
+
else:
|
|
90
|
+
adm_names[i] = ""
|
|
91
|
+
if provider_adm_name == "":
|
|
92
|
+
adm_level -= 1
|
|
93
|
+
return adm_level, warnings
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def pad_admins(
|
|
97
|
+
provider_adm_names: list[str],
|
|
98
|
+
adm_codes: list[str],
|
|
99
|
+
adm_names: list[str],
|
|
100
|
+
adm_level: int = 2,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Pad lists to size given in adm_level adding as many "" as needed.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
provider_adm_names: List of provider adm names
|
|
106
|
+
adm_codes: List of adm codes
|
|
107
|
+
adm_names: List of adm names
|
|
108
|
+
adm_level: Admin level to which to pad. Default is 2.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Admin level and warnings
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
for i in range(len(provider_adm_names), adm_level):
|
|
115
|
+
provider_adm_names.append("")
|
|
116
|
+
adm_codes.append("")
|
|
117
|
+
adm_names.append("")
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from copy import copy
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from hdx.utilities.loader import load_yaml
|
|
6
|
+
from hdx.utilities.matching import get_code_from_name
|
|
7
|
+
from hdx.utilities.path import script_dir_plus_file
|
|
8
|
+
from hdx.utilities.text import normalise
|
|
9
|
+
|
|
10
|
+
from hdx.python.pipelineutils.reader import Read
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Lookup:
|
|
16
|
+
"""Lookup class. YAML input is in this form:
|
|
17
|
+
https://github.com/OCHA-DAP/hdx-python-scraper/blob/main/src/hdx/scraper/framework/utilities/sector_configuration.yaml
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
yaml_config_path: YAML configuration file
|
|
21
|
+
classobject: Child class
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, yaml_config_path: Path | str, classobject: type):
|
|
25
|
+
configuration = load_yaml(script_dir_plus_file(yaml_config_path, classobject))
|
|
26
|
+
self._configuration = configuration
|
|
27
|
+
initial_lookup = configuration.get("initial_lookup", {})
|
|
28
|
+
self._code_lookup = copy(initial_lookup)
|
|
29
|
+
self._code_to_name = {}
|
|
30
|
+
self._unmatched = []
|
|
31
|
+
self.setup()
|
|
32
|
+
|
|
33
|
+
def add_to_lookup(self, code: str, name: str) -> None:
|
|
34
|
+
"""Add code and name to lookup
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
code: Code to add to lookup
|
|
38
|
+
name: Name to add to lookup
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
None
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
self._code_lookup[name] = code
|
|
45
|
+
self._code_lookup[code] = code
|
|
46
|
+
self._code_lookup[normalise(name)] = code
|
|
47
|
+
self._code_lookup[normalise(code)] = code
|
|
48
|
+
self._code_to_name[code] = name
|
|
49
|
+
|
|
50
|
+
def setup(self) -> None:
|
|
51
|
+
"""Setup lookup from YAML configuration
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
None
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
log_message = self._configuration["log_message"]
|
|
58
|
+
logger.info(f"Populating {log_message}")
|
|
59
|
+
|
|
60
|
+
reader = Read.get_reader()
|
|
61
|
+
datasetinfo = self._configuration["datasetinfo"]
|
|
62
|
+
headers, iterator = reader.read(
|
|
63
|
+
datasetinfo, file_prefix=self._configuration["file_prefix"]
|
|
64
|
+
)
|
|
65
|
+
code_key = self._configuration["code_key"]
|
|
66
|
+
name_key = self._configuration["name_key"]
|
|
67
|
+
for row in iterator:
|
|
68
|
+
self.add_to_lookup(
|
|
69
|
+
code=row[code_key],
|
|
70
|
+
name=row[name_key],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
extra_entries = self._configuration.get("extra_entries", {})
|
|
74
|
+
for code, name in extra_entries.items():
|
|
75
|
+
self.add_to_lookup(code=code, name=name)
|
|
76
|
+
|
|
77
|
+
def get_code(self, code: str) -> str | None:
|
|
78
|
+
"""Get code from lookup using fuzzy matching if needed
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
code: Code to get from lookup
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Code obtained from lookup or None if no code is found
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
return get_code_from_name(
|
|
88
|
+
name=code,
|
|
89
|
+
code_lookup=self._code_lookup,
|
|
90
|
+
unmatched=self._unmatched,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def get_name(self, code: str, default: str | None = None) -> str | None:
|
|
94
|
+
"""Get name from code
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
code: Code to lookup
|
|
98
|
+
default: Default name to return if code is not found
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Name obtained from lookup or default if no name is found
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
return self._code_to_name.get(code, default)
|
|
105
|
+
|
|
106
|
+
def get_code_to_name(self) -> dict[str, str]:
|
|
107
|
+
"""Get the code to name lookup dictionary
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Code to name lookup dictionary
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
return self._code_to_name
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
log_message: "Org type mapping"
|
|
2
|
+
file_prefix: "org_type"
|
|
3
|
+
code_key: "HR.info ID"
|
|
4
|
+
name_key: "Preferred Term"
|
|
5
|
+
|
|
6
|
+
datasetinfo:
|
|
7
|
+
dataset: "organization-types-beta"
|
|
8
|
+
resource: "Organization Types (Beta) - CSV no HXL"
|
|
9
|
+
format: "csv"
|
|
10
|
+
headers: 1
|
|
11
|
+
|
|
12
|
+
extra_entries:
|
|
13
|
+
"501": "Civil Society"
|
|
14
|
+
"502": "Observer"
|
|
15
|
+
"503": "Development Programme"
|
|
16
|
+
"504": "Local NGO"
|
|
17
|
+
|
|
18
|
+
initial_lookup:
|
|
19
|
+
academy: "431"
|
|
20
|
+
agence un: "447"
|
|
21
|
+
agence des nations unies: "447"
|
|
22
|
+
agence du systeme des nations unies: "447"
|
|
23
|
+
autre type: "443"
|
|
24
|
+
civile society: "501"
|
|
25
|
+
cooperation internationale: "501"
|
|
26
|
+
croix rouge: "445"
|
|
27
|
+
donantes: "433"
|
|
28
|
+
entite gouvernementale: "435"
|
|
29
|
+
gouv: "435"
|
|
30
|
+
gouvernement: "435"
|
|
31
|
+
gov: "435"
|
|
32
|
+
government agency: "435"
|
|
33
|
+
gobierno: "435"
|
|
34
|
+
govt: "435"
|
|
35
|
+
iglesia grupo religioso: "446"
|
|
36
|
+
ingo: "437"
|
|
37
|
+
institution etatique: "435"
|
|
38
|
+
institución publica: "435"
|
|
39
|
+
international government: "435"
|
|
40
|
+
international institution: "438"
|
|
41
|
+
international ngos: "437"
|
|
42
|
+
local ngo: "504"
|
|
43
|
+
mouv cr: "445"
|
|
44
|
+
mouvement croix rouge: "445"
|
|
45
|
+
mouvement de la croix rouge: "445"
|
|
46
|
+
movimiento de la cruz roja media luna roja: "445"
|
|
47
|
+
national local ngo: "441"
|
|
48
|
+
nations unies: "447"
|
|
49
|
+
naciones unidas: "447"
|
|
50
|
+
nngo: "441"
|
|
51
|
+
observer: "502"
|
|
52
|
+
ong int: "437"
|
|
53
|
+
ong internacional: "437"
|
|
54
|
+
ong internationale: "437"
|
|
55
|
+
ong local: "504"
|
|
56
|
+
ong nat: "441"
|
|
57
|
+
ong national: "441"
|
|
58
|
+
ong nacional: "441"
|
|
59
|
+
ong nationale: "441"
|
|
60
|
+
organisation des nations unies: "447"
|
|
61
|
+
organismo internacional: "438"
|
|
62
|
+
others: "443"
|
|
63
|
+
otro: "443"
|
|
64
|
+
programme de developpement: "503"
|
|
65
|
+
red cross: "445"
|
|
66
|
+
red cross movement: "445"
|
|
67
|
+
red cross mvt: "445"
|
|
68
|
+
red cross and red crescent movement: "445"
|
|
69
|
+
religious group: "446"
|
|
70
|
+
religious organization: "446"
|
|
71
|
+
service etatique: "435"
|
|
72
|
+
sociedad civil: "501"
|
|
73
|
+
societe civile: "501"
|
|
74
|
+
un: "447"
|
|
75
|
+
un agency: "447"
|
|
76
|
+
un agencies: "447"
|
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import logging
|
|
3
|
+
from collections.abc import Iterator, Sequence
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import parse_qsl
|
|
8
|
+
|
|
9
|
+
from hdx.api.configuration import Configuration
|
|
10
|
+
from hdx.api.utilities.url_utils import follow_url
|
|
11
|
+
from hdx.data.dataset import Dataset
|
|
12
|
+
from hdx.data.resource import Resource
|
|
13
|
+
from hdx.utilities.dateparse import parse_date
|
|
14
|
+
from hdx.utilities.downloader import Download
|
|
15
|
+
from hdx.utilities.retriever import Retrieve
|
|
16
|
+
from hdx.utilities.saver import save_json
|
|
17
|
+
from slugify import slugify
|
|
18
|
+
|
|
19
|
+
from . import get_startend_dates_from_time_period, match_template
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Read(Retrieve):
|
|
25
|
+
"""Read data from tabular source eg. csv, xls, xlsx
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
downloader: Download object
|
|
29
|
+
fallback_dir: Directory containing static fallback data
|
|
30
|
+
saved_dir: Directory to save or load downloaded data
|
|
31
|
+
temp_dir: Temporary directory for when data is not needed after downloading
|
|
32
|
+
save: Whether to save downloaded data. Default is False.
|
|
33
|
+
use_saved: Whether to use saved data. Default is False.
|
|
34
|
+
prefix: Prefix to add to filenames. Default is "".
|
|
35
|
+
delete: Whether to delete saved_dir if save is True. Default is True.
|
|
36
|
+
today: Value to use for today. Default is None (datetime.utcnow).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
downloader: Download,
|
|
42
|
+
fallback_dir: Path | str,
|
|
43
|
+
saved_dir: Path | str,
|
|
44
|
+
temp_dir: Path | str,
|
|
45
|
+
save: bool = False,
|
|
46
|
+
use_saved: bool = False,
|
|
47
|
+
prefix: str = "",
|
|
48
|
+
delete: bool = True,
|
|
49
|
+
today: datetime | None = None,
|
|
50
|
+
):
|
|
51
|
+
super().__init__(
|
|
52
|
+
downloader,
|
|
53
|
+
fallback_dir,
|
|
54
|
+
saved_dir,
|
|
55
|
+
temp_dir,
|
|
56
|
+
save,
|
|
57
|
+
use_saved,
|
|
58
|
+
prefix,
|
|
59
|
+
delete,
|
|
60
|
+
)
|
|
61
|
+
self.today: datetime | None = today
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def create_readers(
|
|
65
|
+
cls,
|
|
66
|
+
fallback_dir: Path | str,
|
|
67
|
+
saved_dir: Path | str,
|
|
68
|
+
temp_dir: Path | str,
|
|
69
|
+
save: bool = False,
|
|
70
|
+
use_saved: bool = False,
|
|
71
|
+
ignore: Sequence[str] = tuple(),
|
|
72
|
+
rate_limit: dict | None = {"calls": 1, "period": 0.1},
|
|
73
|
+
today: datetime | None = None,
|
|
74
|
+
**kwargs: Any,
|
|
75
|
+
):
|
|
76
|
+
"""Generate a default reader and an HDX reader. Additional readers are generated
|
|
77
|
+
if any of header_auths, basic_auths or extra_params are populated. header_auths
|
|
78
|
+
and basic_auths are dictionaries of form {"scraper name": "auth", ...}.
|
|
79
|
+
extra_params is of form {"scraper name": {"key": "auth", ...}, ...}.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
fallback_dir: Directory containing static fallback data
|
|
83
|
+
saved_dir: Directory to save or load downloaded data
|
|
84
|
+
temp_dir: Temporary directory for when data is not needed after downloading
|
|
85
|
+
save: Whether to save downloaded data. Default is False.
|
|
86
|
+
use_saved: Whether to use saved data. Default is False.
|
|
87
|
+
ignore: Don't generate retrievers for these downloaders
|
|
88
|
+
rate_limit: Rate limiting per host. Default is {"calls": 1, "period": 0.1}
|
|
89
|
+
today: Value to use for today. Default is None (datetime.utcnow).
|
|
90
|
+
**kwargs: See below and parameters of Download class in HDX Python Utilities
|
|
91
|
+
hdx_auth (str): HDX API key
|
|
92
|
+
header_auths (Mapping[str, str]): Header authorisations
|
|
93
|
+
basic_auths (Mapping[str, str]): Basic authorisations
|
|
94
|
+
param_auths (Mapping[str, str]): Extra parameter authorisations
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
None
|
|
98
|
+
"""
|
|
99
|
+
if rate_limit:
|
|
100
|
+
kwargs["rate_limit"] = rate_limit
|
|
101
|
+
custom_configs = {}
|
|
102
|
+
hdx_auth = kwargs.get("hdx_auth")
|
|
103
|
+
if hdx_auth:
|
|
104
|
+
custom_configs["hdx"] = {"headers": {"Authorization": hdx_auth}}
|
|
105
|
+
del kwargs["hdx_auth"]
|
|
106
|
+
header_auths = kwargs.get("header_auths")
|
|
107
|
+
if header_auths is not None:
|
|
108
|
+
for name in header_auths:
|
|
109
|
+
custom_configs[name] = {
|
|
110
|
+
"headers": {"Authorization": header_auths[name]}
|
|
111
|
+
}
|
|
112
|
+
del kwargs["header_auths"]
|
|
113
|
+
basic_auths = kwargs.get("basic_auths")
|
|
114
|
+
if basic_auths is not None:
|
|
115
|
+
for name in basic_auths:
|
|
116
|
+
custom_configs[name] = {"basic_auth": basic_auths[name]}
|
|
117
|
+
del kwargs["basic_auths"]
|
|
118
|
+
bearer_tokens = kwargs.get("bearer_tokens")
|
|
119
|
+
if bearer_tokens is not None:
|
|
120
|
+
for name in bearer_tokens:
|
|
121
|
+
custom_configs[name] = {"bearer_token": bearer_tokens[name]}
|
|
122
|
+
del kwargs["bearer_tokens"]
|
|
123
|
+
param_auths = kwargs.get("param_auths")
|
|
124
|
+
if param_auths is not None:
|
|
125
|
+
for name in param_auths:
|
|
126
|
+
custom_configs[name] = {
|
|
127
|
+
"extra_params_dict": dict(parse_qsl(param_auths[name]))
|
|
128
|
+
}
|
|
129
|
+
del kwargs["param_auths"]
|
|
130
|
+
Download.generate_downloaders(custom_configs, **kwargs)
|
|
131
|
+
cls.generate_retrievers(
|
|
132
|
+
fallback_dir,
|
|
133
|
+
saved_dir,
|
|
134
|
+
temp_dir,
|
|
135
|
+
save,
|
|
136
|
+
use_saved,
|
|
137
|
+
ignore,
|
|
138
|
+
today=today,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def get_reader(cls, name: str | None = None) -> "Read":
|
|
143
|
+
"""Get a generated reader given a name. If name is not supplied, the default
|
|
144
|
+
one will be returned.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
name: Name of reader. Default is None (get default).
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Reader object
|
|
151
|
+
"""
|
|
152
|
+
return cls.get_retriever(name)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def get_url(url: str, **kwargs: Any) -> str:
|
|
156
|
+
"""Get url from a string replacing any template arguments
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
url: Url to read
|
|
160
|
+
**kwargs: Variables to use when evaluating template arguments
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Url with any template arguments replaced
|
|
164
|
+
"""
|
|
165
|
+
for kwarg in kwargs:
|
|
166
|
+
globals()[kwarg] = kwargs[kwarg]
|
|
167
|
+
template_string, match_string = match_template(url)
|
|
168
|
+
if template_string:
|
|
169
|
+
replace_string = eval(match_string)
|
|
170
|
+
url = url.replace(template_string, replace_string)
|
|
171
|
+
return url
|
|
172
|
+
|
|
173
|
+
def clone(self, downloader: Download) -> "Read":
|
|
174
|
+
"""Clone a given reader but use the given downloader
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
downloader: Downloader to use
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Cloned reader
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
return Read(
|
|
184
|
+
downloader,
|
|
185
|
+
fallback_dir=self.fallback_dir,
|
|
186
|
+
saved_dir=self.saved_dir,
|
|
187
|
+
temp_dir=self.temp_dir,
|
|
188
|
+
save=self.save,
|
|
189
|
+
use_saved=self.use_saved,
|
|
190
|
+
prefix=self.prefix,
|
|
191
|
+
delete=False,
|
|
192
|
+
today=self.today,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def setup_tabular(self, datasetinfo: dict, kwargs: dict) -> str | list:
|
|
196
|
+
"""Setup kwargs for tabular source eg. csv, xls, xlsx from
|
|
197
|
+
datasetinfo and return url.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
datasetinfo: Dictionary of information about dataset
|
|
201
|
+
kwargs: Parameters to pass to download_file call
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
url or list of urls
|
|
205
|
+
"""
|
|
206
|
+
sheet = datasetinfo.get("sheet")
|
|
207
|
+
headers = datasetinfo.get("headers")
|
|
208
|
+
if headers is None:
|
|
209
|
+
headers = 1
|
|
210
|
+
datasetinfo["headers"] = 1
|
|
211
|
+
format = datasetinfo["format"]
|
|
212
|
+
kwargs["format"] = format
|
|
213
|
+
if format in ("xls", "xlsx"):
|
|
214
|
+
if not sheet:
|
|
215
|
+
sheet = 1
|
|
216
|
+
if isinstance(headers, list):
|
|
217
|
+
kwargs["fill_merged_cells"] = True
|
|
218
|
+
elif "fill_merged_cells" not in kwargs:
|
|
219
|
+
kwargs["fill_merged_cells"] = False
|
|
220
|
+
kwargs["xlsx2csv"] = datasetinfo.get("xlsx2csv", False)
|
|
221
|
+
if sheet:
|
|
222
|
+
kwargs["sheet"] = sheet
|
|
223
|
+
kwargs["headers"] = headers
|
|
224
|
+
compression = datasetinfo.get("compression")
|
|
225
|
+
if compression:
|
|
226
|
+
kwargs["compression"] = compression
|
|
227
|
+
url = datasetinfo["url"]
|
|
228
|
+
if isinstance(url, list):
|
|
229
|
+
url = [self.get_url(x, **kwargs) for x in url]
|
|
230
|
+
filename = kwargs.get("filename")
|
|
231
|
+
if not filename:
|
|
232
|
+
filename = datasetinfo.get("filename")
|
|
233
|
+
if filename:
|
|
234
|
+
kwargs["filename"] = filename
|
|
235
|
+
if filename:
|
|
236
|
+
# remove file_prefix if filename provided
|
|
237
|
+
kwargs.pop("file_prefix", None)
|
|
238
|
+
elif "file_prefix" not in kwargs:
|
|
239
|
+
file_prefix = datasetinfo.get("file_prefix")
|
|
240
|
+
if file_prefix:
|
|
241
|
+
kwargs["file_prefix"] = file_prefix
|
|
242
|
+
return url
|
|
243
|
+
|
|
244
|
+
def read_tabular(
|
|
245
|
+
self, datasetinfo: dict, **kwargs: Any
|
|
246
|
+
) -> tuple[list[str], Iterator[dict]]:
|
|
247
|
+
"""Read data from tabular source eg. csv, xls, xlsx
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
datasetinfo: Dictionary of information about dataset
|
|
251
|
+
**kwargs: Parameters to pass to download_file call
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Tuple (headers, iterator where each row is a dictionary)
|
|
255
|
+
"""
|
|
256
|
+
url = self.setup_tabular(datasetinfo, kwargs)
|
|
257
|
+
return self.get_tabular_rows(
|
|
258
|
+
url,
|
|
259
|
+
dict_form=True,
|
|
260
|
+
**kwargs,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def read_dataset(
|
|
264
|
+
self, dataset_name: str, configuration: Configuration | None = None
|
|
265
|
+
) -> Dataset | None:
|
|
266
|
+
"""Read HDX dataset
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
dataset_name: Dataset name
|
|
270
|
+
configuration: HDX configuration. Default is global configuration.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
The dataset that was read or None
|
|
274
|
+
"""
|
|
275
|
+
saved_path = self.saved_dir / f"{dataset_name}.json"
|
|
276
|
+
if self.use_saved:
|
|
277
|
+
logger.info(f"Using saved dataset {dataset_name} in {saved_path}")
|
|
278
|
+
dataset = Dataset.load_from_json(saved_path)
|
|
279
|
+
else:
|
|
280
|
+
dataset = Dataset.read_from_hdx(dataset_name, configuration)
|
|
281
|
+
if self.save:
|
|
282
|
+
logger.info(f"Saving dataset {dataset_name} in {saved_path}")
|
|
283
|
+
if dataset is None:
|
|
284
|
+
save_json(None, saved_path)
|
|
285
|
+
else:
|
|
286
|
+
dataset.save_to_json(saved_path, follow_urls=True)
|
|
287
|
+
return dataset
|
|
288
|
+
|
|
289
|
+
def search_datasets(
|
|
290
|
+
self,
|
|
291
|
+
filename: str,
|
|
292
|
+
query: str | None = "*:*",
|
|
293
|
+
configuration: Configuration | None = None,
|
|
294
|
+
page_size: int = 1000,
|
|
295
|
+
**kwargs: Any,
|
|
296
|
+
) -> list[Dataset]:
|
|
297
|
+
"""Read HDX dataset
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
filename: Filename for saved files. Will be prefixed by underscore and a number.
|
|
301
|
+
query: Query (in Solr format). Default is '*:*'.
|
|
302
|
+
configuration: HDX configuration. Default is global configuration.
|
|
303
|
+
page_size: Size of page to return. Default is 1000.
|
|
304
|
+
**kwargs: See below
|
|
305
|
+
fq (string): Any filter queries to apply
|
|
306
|
+
rows (int): Number of matching rows to return. Default is all datasets (sys.maxsize).
|
|
307
|
+
start (int): Offset in the complete result for where the set of returned datasets should begin
|
|
308
|
+
sort (string): Sorting of results. Default is 'relevance asc, metadata_modified desc' if rows<=page_size or 'metadata_modified asc' if rows>page_size.
|
|
309
|
+
facet (string): Whether to enable faceted results. Default to True.
|
|
310
|
+
facet.mincount (int): Minimum counts for facet fields should be included in the results
|
|
311
|
+
facet.limit (int): Maximum number of values the facet fields return (- = unlimited). Default is 50.
|
|
312
|
+
facet.field (list[str]): Fields to facet upon. Default is empty.
|
|
313
|
+
use_default_schema (bool): Use default package schema instead of custom schema. Default is False.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
list of datasets resulting from query
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
saved_path = self.saved_dir / filename
|
|
320
|
+
if self.use_saved:
|
|
321
|
+
logger.info(
|
|
322
|
+
f"Using saved datasets in {filename}_n.json in {self.saved_dir}"
|
|
323
|
+
)
|
|
324
|
+
datasets = []
|
|
325
|
+
for file_path in sorted(glob.glob(f"{saved_path}_*.json")):
|
|
326
|
+
datasets.append(Dataset.load_from_json(file_path))
|
|
327
|
+
else:
|
|
328
|
+
datasets = Dataset.search_in_hdx(query, configuration, page_size, **kwargs)
|
|
329
|
+
if self.save:
|
|
330
|
+
for i, dataset in enumerate(datasets):
|
|
331
|
+
file_path = f"{saved_path}_{i}.json"
|
|
332
|
+
name = dataset["name"]
|
|
333
|
+
logger.info(f"Saving dataset {name} in {file_path}")
|
|
334
|
+
dataset.save_to_json(file_path, follow_urls=True)
|
|
335
|
+
return datasets
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def construct_filename(name: str, format: str):
|
|
339
|
+
"""Construct filename from name and format. The filename of the file
|
|
340
|
+
comes from the name and format.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
name: Name for the download
|
|
344
|
+
format: Format of download
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Filename of file
|
|
348
|
+
"""
|
|
349
|
+
filename = name.lower()
|
|
350
|
+
file_type = f".{format}"
|
|
351
|
+
if filename.endswith(file_type):
|
|
352
|
+
filename = filename[: -len(file_type)]
|
|
353
|
+
return f"{slugify(filename, separator='_')}{file_type}"
|
|
354
|
+
|
|
355
|
+
def construct_filename_and_download(
|
|
356
|
+
self, name: str, format: str, url: str, **kwargs: Any
|
|
357
|
+
) -> tuple[str, str]:
|
|
358
|
+
"""Construct filename, download file and return the url downloaded and
|
|
359
|
+
the path of the file. The filename of the file comes from the name and
|
|
360
|
+
format.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
name: Name for the download
|
|
364
|
+
format: Format of download
|
|
365
|
+
url: URL of download
|
|
366
|
+
**kwargs: Parameters to pass to download_file call
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
(URL that was downloaded, path to downloaded file)
|
|
370
|
+
"""
|
|
371
|
+
filename = kwargs.get("filename")
|
|
372
|
+
if not filename:
|
|
373
|
+
kwargs["filename"] = self.construct_filename(name, format)
|
|
374
|
+
url = follow_url(url)
|
|
375
|
+
path = self.download_file(url, **kwargs)
|
|
376
|
+
return url, path
|
|
377
|
+
|
|
378
|
+
def download_resource(self, resource: Resource, **kwargs: Any) -> tuple[str, str]:
|
|
379
|
+
"""Download HDX resource os a file and return the url downloaded and
|
|
380
|
+
the path of the file. The filename of the file comes from the name and
|
|
381
|
+
format.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
resource: HDX resource
|
|
385
|
+
**kwargs: Parameters to pass to download_file call
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
(URL that was downloaded, path to downloaded file)
|
|
389
|
+
"""
|
|
390
|
+
return self.construct_filename_and_download(
|
|
391
|
+
resource["name"],
|
|
392
|
+
resource.get_format(),
|
|
393
|
+
resource["url"],
|
|
394
|
+
**kwargs,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def get_hapi_dataset_metadata(dataset: Dataset, datasetinfo: dict) -> dict:
|
|
399
|
+
"""Get HAPI dataset metadata from HDX dataset
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
dataset: HDX dataset
|
|
403
|
+
datasetinfo: Dictionary of information about dataset
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
HAPI dataset metadata
|
|
407
|
+
"""
|
|
408
|
+
license_id = dataset["license_id"]
|
|
409
|
+
if license_id == "hdx-other":
|
|
410
|
+
license = dataset["license_other"]
|
|
411
|
+
else:
|
|
412
|
+
license_title = dataset["license_title"]
|
|
413
|
+
license_url = dataset.get("license_url")
|
|
414
|
+
if license_url:
|
|
415
|
+
license = f"[{license_title}]({license_url})"
|
|
416
|
+
else:
|
|
417
|
+
license = license_title
|
|
418
|
+
return {
|
|
419
|
+
"hdx_id": dataset["id"],
|
|
420
|
+
"hdx_stub": dataset["name"],
|
|
421
|
+
"title": dataset["title"],
|
|
422
|
+
"hdx_provider_stub": dataset["organization"]["name"],
|
|
423
|
+
"hdx_provider_name": dataset["organization"]["title"],
|
|
424
|
+
"license": license,
|
|
425
|
+
"time_period": datasetinfo["time_period"],
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def get_hapi_resource_metadata(resource: Resource) -> dict:
|
|
430
|
+
"""Get HAPI resource metadata from HDX resource
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
resource: HDX dataset
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
HAPI resource metadata
|
|
437
|
+
"""
|
|
438
|
+
return {
|
|
439
|
+
"hdx_id": resource["id"],
|
|
440
|
+
"name": resource["name"],
|
|
441
|
+
"format": resource.get_format(),
|
|
442
|
+
"update_date": parse_date(resource["last_modified"]),
|
|
443
|
+
"download_url": resource["url"],
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
def read_hdx_metadata(
|
|
447
|
+
self,
|
|
448
|
+
datasetinfo: dict,
|
|
449
|
+
do_resource_check: bool = True,
|
|
450
|
+
configuration: Configuration | None = None,
|
|
451
|
+
) -> Resource | None:
|
|
452
|
+
"""Read metadata from HDX dataset and add to input dictionary. If url
|
|
453
|
+
is not supplied, will look through resources for one that matches
|
|
454
|
+
specified format and use its url unless do_resource_check is False.
|
|
455
|
+
The dataset key of the parameter datasetinfo will usually point to a
|
|
456
|
+
string (single dataset) but where sources vary across HXL tags can be
|
|
457
|
+
a dictionary that maps from HXL tags to datasets with the key
|
|
458
|
+
default_dataset setting a default for HXL tags. For a single dataset,
|
|
459
|
+
the keys hapi_dataset_metadata and hapi_resource_metadata will be
|
|
460
|
+
populated with more detailed dataset and resource information required
|
|
461
|
+
by HAPI.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
datasetinfo: Dictionary of information about dataset
|
|
465
|
+
do_resource_check: Whether to check resources. Default is False.
|
|
466
|
+
configuration: HDX configuration. Default is global configuration.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
The resource if a url was not given
|
|
470
|
+
"""
|
|
471
|
+
dataset_nameinfo = datasetinfo["dataset"]
|
|
472
|
+
dataset = self.read_dataset(dataset_nameinfo, configuration)
|
|
473
|
+
resource = None
|
|
474
|
+
url = datasetinfo.get("url")
|
|
475
|
+
resource_name = datasetinfo.get("resource")
|
|
476
|
+
# Only loop through resources if do_resource_check is True and
|
|
477
|
+
# either there is no url in the datasetinfo dictionary or a
|
|
478
|
+
# resource name has been specified in there
|
|
479
|
+
if do_resource_check and (not url or resource_name):
|
|
480
|
+
format = datasetinfo["format"].lower()
|
|
481
|
+
found = False
|
|
482
|
+
for resource in dataset.get_resources():
|
|
483
|
+
if resource["format"].lower() == format:
|
|
484
|
+
if resource_name: # if resource is specified,
|
|
485
|
+
if resource["name"] == resource_name: # match it
|
|
486
|
+
found = True
|
|
487
|
+
break
|
|
488
|
+
continue
|
|
489
|
+
else: # if resource is not specified, use first one
|
|
490
|
+
found = True
|
|
491
|
+
break
|
|
492
|
+
if not found:
|
|
493
|
+
error = [f"Cannot find {format} resource"]
|
|
494
|
+
if resource_name:
|
|
495
|
+
error.append(f"with name {resource_name}")
|
|
496
|
+
error.append(f"in {dataset_nameinfo}!")
|
|
497
|
+
raise ValueError(" ".join(error))
|
|
498
|
+
if url: # if there is a url in the datasetinfo dictionary,
|
|
499
|
+
resource["url"] = url # set the resource url to it
|
|
500
|
+
else:
|
|
501
|
+
url = resource["url"] # otherwise set the url key in
|
|
502
|
+
# datasetinfo to the resource url (by setting url here)
|
|
503
|
+
datasetinfo["hapi_resource_metadata"] = self.get_hapi_resource_metadata(
|
|
504
|
+
resource
|
|
505
|
+
)
|
|
506
|
+
datasetinfo["url"] = url
|
|
507
|
+
if "source_date" not in datasetinfo:
|
|
508
|
+
datasetinfo["source_date"] = get_startend_dates_from_time_period(
|
|
509
|
+
dataset, today=self.today
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def set_date(date, startend):
|
|
513
|
+
if isinstance(date, str):
|
|
514
|
+
date = parse_date(date)
|
|
515
|
+
if startend == "end":
|
|
516
|
+
date = date.replace(
|
|
517
|
+
hour=23,
|
|
518
|
+
minute=59,
|
|
519
|
+
second=59,
|
|
520
|
+
microsecond=999999,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
datasetinfo["time_period"][startend] = date
|
|
524
|
+
|
|
525
|
+
start_date = datasetinfo["source_date"]["start"]
|
|
526
|
+
end_date = datasetinfo["source_date"]["end"]
|
|
527
|
+
datasetinfo["time_period"] = {}
|
|
528
|
+
set_date(start_date, "start")
|
|
529
|
+
set_date(end_date, "end")
|
|
530
|
+
if "source" not in datasetinfo:
|
|
531
|
+
datasetinfo["source"] = dataset["dataset_source"]
|
|
532
|
+
if "source_url" not in datasetinfo:
|
|
533
|
+
datasetinfo["source_url"] = dataset.get_hdx_url()
|
|
534
|
+
datasetinfo["hapi_dataset_metadata"] = self.get_hapi_dataset_metadata(
|
|
535
|
+
dataset, datasetinfo
|
|
536
|
+
)
|
|
537
|
+
return resource
|
|
538
|
+
|
|
539
|
+
def read_hdx(
|
|
540
|
+
self,
|
|
541
|
+
datasetinfo: dict,
|
|
542
|
+
configuration: Configuration | None = None,
|
|
543
|
+
**kwargs: Any,
|
|
544
|
+
) -> tuple[list[str], Iterator[dict]]:
|
|
545
|
+
"""Read data and metadata from HDX dataset
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
datasetinfo: Dictionary of information about dataset
|
|
549
|
+
configuration: HDX configuration. Default is global configuration.
|
|
550
|
+
**kwargs: Parameters to pass to download_file call
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Tuple (headers, iterator where each row is a dictionary)
|
|
554
|
+
"""
|
|
555
|
+
resource = self.read_hdx_metadata(datasetinfo, configuration=configuration)
|
|
556
|
+
filename = kwargs.get("filename")
|
|
557
|
+
if filename:
|
|
558
|
+
del kwargs["filename"]
|
|
559
|
+
datasetinfo["filename"] = filename
|
|
560
|
+
filename = datasetinfo.get("filename")
|
|
561
|
+
if resource and not filename:
|
|
562
|
+
filename = self.construct_filename(resource["name"], resource.get_format())
|
|
563
|
+
file_prefix = kwargs.get("file_prefix")
|
|
564
|
+
if not file_prefix:
|
|
565
|
+
file_prefix = datasetinfo.get("file_prefix")
|
|
566
|
+
if file_prefix:
|
|
567
|
+
filename = f"{file_prefix}_{filename}"
|
|
568
|
+
datasetinfo["filename"] = filename
|
|
569
|
+
return self.read_tabular(datasetinfo, **kwargs)
|
|
570
|
+
|
|
571
|
+
def read(
|
|
572
|
+
self,
|
|
573
|
+
datasetinfo: dict,
|
|
574
|
+
configuration: Configuration | None = None,
|
|
575
|
+
**kwargs: Any,
|
|
576
|
+
) -> tuple[list[str], Iterator[dict]]:
|
|
577
|
+
"""Read data and metadata from HDX dataset
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
datasetinfo: Dictionary of information about dataset
|
|
581
|
+
configuration: HDX configuration. Default is global configuration.
|
|
582
|
+
**kwargs: Parameters to pass to download_file call
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
Tuple (headers, iterator where each row is a dictionary)
|
|
586
|
+
"""
|
|
587
|
+
format = datasetinfo["format"]
|
|
588
|
+
if format in ["json", "csv", "xls", "xlsx"]:
|
|
589
|
+
if "dataset" in datasetinfo:
|
|
590
|
+
headers, iterator = self.read_hdx(datasetinfo, configuration, **kwargs)
|
|
591
|
+
else:
|
|
592
|
+
headers, iterator = self.read_tabular(datasetinfo, **kwargs)
|
|
593
|
+
else:
|
|
594
|
+
raise ValueError(f"Invalid format {format} for {datasetinfo['name']}!")
|
|
595
|
+
return headers, iterator
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
log_message: "Sector mapping"
|
|
2
|
+
file_prefix: "sector"
|
|
3
|
+
code_key: "ACRONYM"
|
|
4
|
+
name_key: "Preferred Term"
|
|
5
|
+
|
|
6
|
+
datasetinfo:
|
|
7
|
+
dataset: "global-coordination-groups-beta"
|
|
8
|
+
resource: "Global Coordination Groups (Beta) CSV no HXL"
|
|
9
|
+
format: "csv"
|
|
10
|
+
headers: 1
|
|
11
|
+
|
|
12
|
+
extra_entries:
|
|
13
|
+
"Cash": "Cash programming"
|
|
14
|
+
"Hum": "Humanitarian assistance (unspecified)"
|
|
15
|
+
"Multi": "Multi-sector (unspecified)"
|
|
16
|
+
"Intersectoral": "Intersectoral"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
initial_lookup:
|
|
20
|
+
abna: "SHL"
|
|
21
|
+
abri: "SHL"
|
|
22
|
+
abri bna: "SHL"
|
|
23
|
+
abris: "SHL"
|
|
24
|
+
abris ame: "SHL"
|
|
25
|
+
abris bna: "SHL"
|
|
26
|
+
abris bna cccm: "SHL"
|
|
27
|
+
abris durgence et nfi: "SHL"
|
|
28
|
+
abris nfi: "SHL"
|
|
29
|
+
action contre les mines: "PRO-MIN"
|
|
30
|
+
aee: "SHL"
|
|
31
|
+
agr: "FSC"
|
|
32
|
+
agriculture: "FSC"
|
|
33
|
+
agua saneamiento e higiene: "WSH"
|
|
34
|
+
all: "Intersectoral"
|
|
35
|
+
alojamiento de emergencia: "SHL"
|
|
36
|
+
alojamiento de emergencia shelter: "SHL"
|
|
37
|
+
alojamiento energía y enseres: "SHL"
|
|
38
|
+
alojamientos y asentamientos: "SHL"
|
|
39
|
+
ame: "SHL"
|
|
40
|
+
ash: "WSH"
|
|
41
|
+
assainissement: "WSH"
|
|
42
|
+
basic assistance: "Cash"
|
|
43
|
+
camp coordination and camp management: "CCM"
|
|
44
|
+
camp coordination camp management: "CCM"
|
|
45
|
+
cash: "Cash"
|
|
46
|
+
cccm: "CCM"
|
|
47
|
+
ccs: "CCM"
|
|
48
|
+
cluster coordination: "CCM"
|
|
49
|
+
coord services support: "CCM"
|
|
50
|
+
coordinacion informacion: "CCM"
|
|
51
|
+
coord support services: "CCM"
|
|
52
|
+
coordination: "CCM"
|
|
53
|
+
coordination and common services: "CCM"
|
|
54
|
+
coordination et gestion des camps: "CCM"
|
|
55
|
+
cp: "PRO-CPN"
|
|
56
|
+
css: "CCM"
|
|
57
|
+
eah: "WSH"
|
|
58
|
+
eau: "WSH"
|
|
59
|
+
eau assainissement et hygiene: "WSH"
|
|
60
|
+
eau hygiene: "WSH"
|
|
61
|
+
eau hygiene assainissement: "WSH"
|
|
62
|
+
eau hygiene et assainissement: "WSH"
|
|
63
|
+
educacion: "EDU"
|
|
64
|
+
educacion en emergencias: "EDU"
|
|
65
|
+
education: "EDU"
|
|
66
|
+
efectivo multiproposito: "Cash"
|
|
67
|
+
eha: "WSH"
|
|
68
|
+
emergency shelter and non food items: "SHL"
|
|
69
|
+
epah: "WSH"
|
|
70
|
+
erl: "ERY"
|
|
71
|
+
esnfi: "SHL"
|
|
72
|
+
ets: "TEL"
|
|
73
|
+
explosive hazards: "PRO-MIN"
|
|
74
|
+
fss: "FSC"
|
|
75
|
+
food: "FSC"
|
|
76
|
+
food safety: "FSC"
|
|
77
|
+
food security and agriculture: "FSC"
|
|
78
|
+
food security and livelihoods: "FSC"
|
|
79
|
+
food security and nutrition: "FSC"
|
|
80
|
+
food security livelihood: "FSC"
|
|
81
|
+
formation professionnelle: "EDU"
|
|
82
|
+
fsl: "FSC"
|
|
83
|
+
gestion des sites daccueil temporaires: "SHL"
|
|
84
|
+
gbv: "PRO-GBV"
|
|
85
|
+
general protection: "PRO"
|
|
86
|
+
global protection: "PRO"
|
|
87
|
+
hlp: "PRO-HLP"
|
|
88
|
+
housing land property: "PRO-HLP"
|
|
89
|
+
humanitaire: "Hum"
|
|
90
|
+
hygiene: "WSH"
|
|
91
|
+
hygiene assainissement: "WSH"
|
|
92
|
+
intercluster: "Multi" # From Somalia 3W, hopefully not to be confused with intersectoral
|
|
93
|
+
logement terre et biens: "PRO-HLP"
|
|
94
|
+
logistica: "LOG"
|
|
95
|
+
logistique: "LOG"
|
|
96
|
+
lutte contre la traite: "PRO-GBV"
|
|
97
|
+
ma: "PRO-MIN"
|
|
98
|
+
manejo y gestion de campamentos: "CCM"
|
|
99
|
+
migrant protection: "PRO"
|
|
100
|
+
mpc: "Cash"
|
|
101
|
+
mpca: "Cash"
|
|
102
|
+
ms: "Multi"
|
|
103
|
+
multi purpose cash: "Cash"
|
|
104
|
+
multi secteur: "Multi"
|
|
105
|
+
multisectoriel: "Multi"
|
|
106
|
+
nutricion: "NUT"
|
|
107
|
+
nutrition: "NUT"
|
|
108
|
+
operatioanl presence water sanitation hygiene: "WSH"
|
|
109
|
+
operational presence education in emergencies: "EDU"
|
|
110
|
+
operational presence emergency shelter non food items: "SHL"
|
|
111
|
+
operational presence food security agriculture: "FSC"
|
|
112
|
+
operational presence health: "HEA"
|
|
113
|
+
operational presence nutrition: "NUT"
|
|
114
|
+
operational presence protection: "PRO"
|
|
115
|
+
pro cpm: "PRO-CPN"
|
|
116
|
+
pronna: "PRO-CPN"
|
|
117
|
+
propg: "PRO"
|
|
118
|
+
proteccion infantil: "PRO-CPN"
|
|
119
|
+
proteccion ninos ninas adolescentes: "PRO-CPN"
|
|
120
|
+
proteccion violencia de genero: "PRO-GBV"
|
|
121
|
+
protection: "PRO"
|
|
122
|
+
protection de lenfance: "PRO-CPN"
|
|
123
|
+
protection de lenfant: "PRO-CPN"
|
|
124
|
+
protection generale: "PRO"
|
|
125
|
+
protection logement terre et propriete: "PRO-HLP"
|
|
126
|
+
protection ltb: "PRO-HLP"
|
|
127
|
+
protection lutte anti mines: "PRO-MIN"
|
|
128
|
+
protection pe: "PRO-CPN"
|
|
129
|
+
protection protection de lenfant: "PRO-CPN"
|
|
130
|
+
protection violences basees sur le genre: "PRO-GBV"
|
|
131
|
+
protection vgb: "PRO-GBV"
|
|
132
|
+
proteccion: "PRO"
|
|
133
|
+
provbg: "PRO-GBV"
|
|
134
|
+
psea: "PRO-GBV"
|
|
135
|
+
rapid response mechanism: "ERY"
|
|
136
|
+
rcf: "CCM"
|
|
137
|
+
rcf education: "EDU"
|
|
138
|
+
rcf food security and livelihoods: "FSC"
|
|
139
|
+
rcf health and nutrtion: "HEA"
|
|
140
|
+
rcf protection: "PRO"
|
|
141
|
+
recuperacion temprana: "ERY"
|
|
142
|
+
relevement precoce: "ERY"
|
|
143
|
+
relevement rapide: "ERY"
|
|
144
|
+
refugees: "CCM"
|
|
145
|
+
refugee response: "CCM"
|
|
146
|
+
refugees migrants multi sector: "CCM"
|
|
147
|
+
reponse aux refugies: "CCM"
|
|
148
|
+
sa: "FSC"
|
|
149
|
+
sal: "HEA"
|
|
150
|
+
salud: "HEA"
|
|
151
|
+
same: "FSC"
|
|
152
|
+
samv: "FSC"
|
|
153
|
+
sante: "HEA"
|
|
154
|
+
securite alimentaire: "FSC"
|
|
155
|
+
securite alimentaire et moyen dexistence: "FSC"
|
|
156
|
+
seguridad alimentaria: "FSC"
|
|
157
|
+
seguridad alimentaria y nutricion: "FSC"
|
|
158
|
+
services humanitaires communs: "Hum"
|
|
159
|
+
sexual and reproductive health: "HEA"
|
|
160
|
+
shelter: "SHL"
|
|
161
|
+
shelter nfi: "SHL"
|
|
162
|
+
shelter nfis: "SHL"
|
|
163
|
+
shelter and nfi: "SHL"
|
|
164
|
+
shelter and nfis: "SHL"
|
|
165
|
+
shelter and non food items: "SHL"
|
|
166
|
+
shelter site coordination: "SHL"
|
|
167
|
+
site management: "CCM"
|
|
168
|
+
snfi: "SHL"
|
|
169
|
+
sspe: "PRO-CPN"
|
|
170
|
+
telecommunications: "TEL"
|
|
171
|
+
telecommunications durgence: "TEL"
|
|
172
|
+
telecomunicaciones de emergencia: "TEL"
|
|
173
|
+
transversal: "Multi"
|
|
174
|
+
vbg: "PRO-GBV"
|
|
175
|
+
violences basees sur le genre: "PRO-GBV"
|
|
176
|
+
violence basee sur le genre: "PRO-GBV"
|
|
177
|
+
violencia basada en genero: "PRO-GBV"
|
|
178
|
+
wash: "WSH"
|
|
179
|
+
water sanitation and hygiene: "WSH"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hdx-python-pipelineutils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: HDX Python Pipeline Utilities
|
|
5
|
+
Project-URL: Homepage, https://github.com/OCHA-DAP/hdx-python-pipelineutils
|
|
6
|
+
Author-email: Michael Rans <rans@email.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: HDX,pipelines,scrapers,utilities
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Operating System :: Unix
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: gspread
|
|
29
|
+
Requires-Dist: hdx-python-api>=6.6.7
|
|
30
|
+
Requires-Dist: hdx-python-country>=4.1.1
|
|
31
|
+
Requires-Dist: hdx-python-utilities>=4.0.8
|
|
32
|
+
Requires-Dist: libhxl
|
|
33
|
+
Requires-Dist: regex
|
|
34
|
+
Provides-Extra: docs
|
|
35
|
+
Requires-Dist: mkapi; extra == 'docs'
|
|
36
|
+
Provides-Extra: pandas
|
|
37
|
+
Requires-Dist: pandas>=2.3.3; extra == 'pandas'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
[](https://github.com/OCHA-DAP/hdx-python-pipelineutils/actions/workflows/run-python-tests.yaml)
|
|
41
|
+
[](https://coveralls.io/github/OCHA-DAP/hdx-python-pipelineutils?branch=main)
|
|
42
|
+
[](https://github.com/astral-sh/ruff)
|
|
43
|
+
[](https://pypistats.org/packages/hdx-python-pipelineutils)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
The HDX Python PipelineUtils Library contains various utilities for use by pipelines.
|
|
47
|
+
|
|
48
|
+
This library is part of the
|
|
49
|
+
[Humanitarian Data Exchange](https://data.humdata.org/) (HDX) project. If you have
|
|
50
|
+
humanitarian related data, please upload your datasets to HDX.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
hdx/python/pipelineutils/__init__.py,sha256=KYANd5ijl4h95gKIWtzk8Nw2q2-etHJmXZq-bp-UtZQ,1477
|
|
2
|
+
hdx/python/pipelineutils/_version.py,sha256=8OsTLsIVB9D0HdPTmt5rVwyVUBe9xTVGkRslXicxzkM,520
|
|
3
|
+
hdx/python/pipelineutils/hapi_admins.py,sha256=3dDK_MggacaWoH5ZNV7y5_q3el7E9dmax8s2A3kkS5k,3935
|
|
4
|
+
hdx/python/pipelineutils/lookup.py,sha256=-6_ILD04gqcUIUIO7er8lx4w0qEDJmmhaciqDV4TcJI,3361
|
|
5
|
+
hdx/python/pipelineutils/org_type.py,sha256=euQyRV01yA8kJ3nMFvZxnTRLnvCuxgV1ZZQx8gEOB8Y,183
|
|
6
|
+
hdx/python/pipelineutils/org_type_configuration.yaml,sha256=IwDfnHmIDM8PzLtEotArzAR37fc4FSviiLi4NvP8iew,1825
|
|
7
|
+
hdx/python/pipelineutils/reader.py,sha256=dE22z2g88IiquKrw4CbvSgXflr8A9OqODqzHLT5BHhw,22685
|
|
8
|
+
hdx/python/pipelineutils/sector.py,sha256=XGysivvPhTqQfK6z1y96sDJATk3zx7sS_qGqCa4PbaI,177
|
|
9
|
+
hdx/python/pipelineutils/sector_configuration.yaml,sha256=9Z3qEI-9FLKUV8xXIixpxaqGoCaTmzelEGUXMVIrjkY,4958
|
|
10
|
+
hdx_python_pipelineutils-0.0.1.dist-info/METADATA,sha256=SN4YZl1IMjuHsDgYWhHMx_rdx5Xe8RF21UKUuOjW2rw,2487
|
|
11
|
+
hdx_python_pipelineutils-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
+
hdx_python_pipelineutils-0.0.1.dist-info/licenses/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
|
|
13
|
+
hdx_python_pipelineutils-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 Michael Rans
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|