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.
@@ -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,8 @@
1
+ from .lookup import Lookup
2
+
3
+
4
+ class OrgType(Lookup):
5
+ """Populate the org type mapping."""
6
+
7
+ def __init__(self):
8
+ super().__init__("org_type_configuration.yaml", OrgType)
@@ -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,8 @@
1
+ from .lookup import Lookup
2
+
3
+
4
+ class Sector(Lookup):
5
+ """Populate the sector mapping."""
6
+
7
+ def __init__(self):
8
+ super().__init__("sector_configuration.yaml", Sector)
@@ -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
+ [![Build Status](https://github.com/OCHA-DAP/hdx-python-pipelineutils/actions/workflows/run-python-tests.yaml/badge.svg)](https://github.com/OCHA-DAP/hdx-python-pipelineutils/actions/workflows/run-python-tests.yaml)
41
+ [![Coverage Status](https://coveralls.io/repos/github/OCHA-DAP/hdx-python-pipelineutils/badge.svg?branch=main&ts=1)](https://coveralls.io/github/OCHA-DAP/hdx-python-pipelineutils?branch=main)
42
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
43
+ [![Downloads](https://img.shields.io/pypi/dm/hdx-python-pipelineutils.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.