hdx-python-api 6.6.8__py3-none-any.whl → 6.7.0__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/api/_version.py CHANGED
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '6.6.8'
22
- __version_tuple__ = version_tuple = (6, 6, 8)
21
+ __version__ = version = '6.7.0'
22
+ __version_tuple__ = version_tuple = (6, 7, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -72,6 +72,10 @@ organization:
72
72
  - name
73
73
  - title
74
74
  - description
75
+ group:
76
+ required_fields:
77
+ - name
78
+ - title
75
79
  "resource view":
76
80
  required_fields:
77
81
  - resource_id
hdx/api/locations.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from collections.abc import Sequence
4
4
 
5
5
  from hdx.api.configuration import Configuration
6
+ from hdx.data.location import Location
6
7
 
7
8
 
8
9
  class Locations:
@@ -22,10 +23,8 @@ class Locations:
22
23
  A list of valid locations
23
24
  """
24
25
  if cls._validlocations is None:
25
- if configuration is None:
26
- configuration = Configuration.read()
27
- cls._validlocations = configuration.call_remoteckan(
28
- "group_list", {"all_fields": True}
26
+ cls._validlocations = Location.get_all_location_names(
27
+ configuration=configuration, all_fields=True
29
28
  )
30
29
  return cls._validlocations
31
30
 
hdx/data/location.py ADDED
@@ -0,0 +1,237 @@
1
+ """Location class containing all logic for creating, checking, and updating locations."""
2
+
3
+ import logging
4
+ from collections.abc import Sequence
5
+ from typing import Any, Optional
6
+
7
+ from hdx.api.configuration import Configuration
8
+ from hdx.data.hdxobject import HDXObject
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Location(HDXObject):
14
+ """Location class containing all logic for creating, checking, and updating locations.
15
+ A location is a CKAN group.
16
+
17
+ Args:
18
+ initial_data: Initial location metadata dictionary. Defaults to None.
19
+ configuration: HDX configuration. Defaults to global configuration.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ initial_data: dict | None = None,
25
+ configuration: Configuration | None = None,
26
+ ) -> None:
27
+ if not initial_data:
28
+ initial_data = {}
29
+ super().__init__(initial_data, configuration=configuration)
30
+
31
+ @staticmethod
32
+ def actions() -> dict[str, str]:
33
+ """Dictionary of actions that can be performed on object
34
+
35
+ Returns:
36
+ Dictionary of actions that can be performed on object
37
+ """
38
+ return {
39
+ "show": "group_show",
40
+ "update": "group_update",
41
+ "create": "group_create",
42
+ "delete": "group_delete",
43
+ "list": "group_list",
44
+ "autocomplete": "group_autocomplete",
45
+ }
46
+
47
+ @classmethod
48
+ def read_from_hdx(
49
+ cls, identifier: str, configuration: Configuration | None = None
50
+ ) -> Optional["Location"]:
51
+ """Reads the location given by identifier from HDX and returns Location object
52
+
53
+ Args:
54
+ identifier: Identifier of location
55
+ configuration: HDX configuration. Defaults to global configuration.
56
+
57
+ Returns:
58
+ Location object if successful read, None if not
59
+ """
60
+ return cls._read_from_hdx_class("group", identifier, configuration)
61
+
62
+ def check_required_fields(self, ignore_fields: Sequence[str] = ()) -> None:
63
+ """Check that metadata for location is complete. The parameter ignore_fields should
64
+ be set if required to any fields that should be ignored for the particular operation.
65
+
66
+ Args:
67
+ ignore_fields: Fields to ignore. Default is ().
68
+
69
+ Returns:
70
+ None
71
+ """
72
+ self._check_required_fields("group", ignore_fields)
73
+
74
+ def update_in_hdx(self, **kwargs: Any) -> None:
75
+ """Check if location exists in HDX and if so, update location
76
+
77
+ Returns:
78
+ None
79
+ """
80
+ self._update_in_hdx("group", "id", **kwargs)
81
+
82
+ def create_in_hdx(self, **kwargs: Any) -> None:
83
+ """Check if location exists in HDX and if so, update it, otherwise create location
84
+
85
+ Returns:
86
+ None
87
+ """
88
+ self._create_in_hdx("group", "id", "name", **kwargs)
89
+
90
+ def delete_from_hdx(self) -> None:
91
+ """Deletes a location from HDX.
92
+
93
+ Returns:
94
+ None
95
+ """
96
+ self._delete_from_hdx("group", "id")
97
+
98
+ def get_datasets(self, query: str = "*:*", **kwargs: Any) -> list["Dataset"]: # noqa: F821
99
+ """Get list of datasets in location
100
+
101
+ Args:
102
+ query: Restrict datasets returned to this query (in Solr format). Defaults to '*:*'.
103
+ **kwargs: See below
104
+ sort (string): Sorting of the search results. Defaults to 'relevance asc, metadata_modified desc'.
105
+ rows (int): Number of matching rows to return. Defaults to all datasets (sys.maxsize).
106
+ start (int): Offset in the complete result for where the set of returned datasets should begin
107
+ facet (string): Whether to enable faceted results. Default to True.
108
+ facet.mincount (int): Minimum counts for facet fields should be included in the results
109
+ facet.limit (int): Maximum number of values the facet fields return (- = unlimited). Defaults to 50.
110
+ facet.field (list[str]): Fields to facet upon. Default is empty.
111
+ use_default_schema (bool): Use default package schema instead of custom schema. Defaults to False.
112
+
113
+ Returns:
114
+ List of datasets in location
115
+ """
116
+ import hdx.data.dataset # avoid circular import
117
+
118
+ return hdx.data.dataset.Dataset.search_in_hdx(
119
+ query=query,
120
+ configuration=self.configuration,
121
+ fq=f"groups:{self.data['name']}",
122
+ **kwargs,
123
+ )
124
+
125
+ @staticmethod
126
+ def get_all_location_names(
127
+ configuration: Configuration | None = None, **kwargs: Any
128
+ ) -> list[str]:
129
+ """Get all location names in HDX
130
+
131
+ Args:
132
+ configuration: HDX configuration. Defaults to global configuration.
133
+ **kwargs: See below
134
+ sort (str): Sort the search results according to field name and sort-order. Allowed fields are ‘name’, ‘package_count’ and ‘title’. Defaults to 'name asc'.
135
+ groups (list[str]): List of names of the groups to return.
136
+ all_fields (bool): Return group dictionaries instead of just names. Only core fields are returned - get some more using the include_* options. Defaults to False.
137
+ include_extras (bool): If all_fields, include the group extra fields. Defaults to False.
138
+ include_tags (bool): If all_fields, include the group tags. Defaults to False.
139
+ include_groups (bool): If all_fields, include the groups the groups are in. Defaults to False.
140
+
141
+ Returns:
142
+ List of all location names in HDX
143
+ """
144
+ location = Location(configuration=configuration)
145
+
146
+ # Early return for the standard, non-paginated case
147
+ if not kwargs.get("all_fields"):
148
+ return location._write_to_hdx("list", kwargs)
149
+
150
+ compiled_locations = {}
151
+ # Extract limit and offset to dictate our paging sizes, defaulting to 400 and 0
152
+ page_size = kwargs.pop("limit", 400)
153
+ current_offset = kwargs.pop("offset", 0)
154
+
155
+ # 1. Fetch in pages using the provided/default page size
156
+ while True:
157
+ page_kwargs = kwargs.copy()
158
+ page_kwargs["limit"] = page_size
159
+ page_kwargs["offset"] = current_offset
160
+
161
+ page_data = location._write_to_hdx("list", page_kwargs)
162
+
163
+ if not page_data:
164
+ break
165
+
166
+ # Store by name to automatically deduplicate offset shifts
167
+ for loc in page_data:
168
+ compiled_locations[loc["name"]] = loc
169
+
170
+ if len(page_data) < page_size:
171
+ break
172
+
173
+ current_offset += page_size
174
+
175
+ # 2. Establish Ground Truth LAST (without limit/offset constraints)
176
+ truth_kwargs = kwargs.copy()
177
+ truth_kwargs["all_fields"] = False
178
+
179
+ ground_truth_names = location._write_to_hdx("list", truth_kwargs)
180
+
181
+ # 3 & 4. Check for missing IDs, fetch individually, and implicitly prune deletes
182
+ final_locations = []
183
+ for name in ground_truth_names:
184
+ if name in compiled_locations:
185
+ final_locations.append(compiled_locations[name])
186
+ else:
187
+ try:
188
+ missing_location = location._write_to_hdx("show", {"id": name})
189
+ if missing_location:
190
+ final_locations.append(missing_location)
191
+ except Exception:
192
+ pass
193
+
194
+ return final_locations
195
+
196
+ @staticmethod
197
+ def get_data_grid_countries(
198
+ configuration: Configuration | None = None,
199
+ ) -> list[str]:
200
+ """Fetch HDX's active Data Grid countries (3-letter group names).
201
+
202
+ Treated as a live/short-TTL lookup rather than a frozen static list, since Data Grid
203
+ membership changes over time.
204
+
205
+ Args:
206
+ configuration: HDX configuration. Defaults to global configuration.
207
+
208
+ Returns:
209
+ Sorted list of active Data Grid country (group) names
210
+ """
211
+ groups = Location.get_all_location_names(
212
+ configuration=configuration, all_fields=True, include_extras=True
213
+ )
214
+ return sorted(
215
+ group["name"]
216
+ for group in groups
217
+ if len(group["name"]) == 3 and group.get("data_completeness") == "active"
218
+ )
219
+
220
+ @classmethod
221
+ def autocomplete(
222
+ cls,
223
+ name: str,
224
+ limit: int = 20,
225
+ configuration: Configuration | None = None,
226
+ ) -> list:
227
+ """Autocomplete a location name and return matches
228
+
229
+ Args:
230
+ name: Name to autocomplete
231
+ limit: Maximum number of matches to return
232
+ configuration: HDX configuration. Defaults to global configuration.
233
+
234
+ Returns:
235
+ Autocomplete matches
236
+ """
237
+ return cls._autocomplete(name, limit, configuration)
hdx/data/resource.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Resource class containing all logic for creating, checking, and updating resources."""
2
2
 
3
+ import json
3
4
  import logging
4
5
  from collections.abc import Sequence
5
6
  from datetime import datetime
@@ -33,6 +34,9 @@ class Resource(HDXObject):
33
34
  """
34
35
 
35
36
  _formats_dict = None
37
+ _valid_data_dictionary_types = frozenset(
38
+ {"text", "numeric", "date", "timestamp without time zone"}
39
+ )
36
40
 
37
41
  def __init__(
38
42
  self,
@@ -263,6 +267,56 @@ class Resource(HDXObject):
263
267
  self.data["format"] = file_format
264
268
  return file_format
265
269
 
270
+ def get_hdx_data_dictionary(self) -> list[dict] | None:
271
+ """Get the resource's data dictionary (column definitions)
272
+
273
+ Returns:
274
+ List of column definition dictionaries or None if not set
275
+ """
276
+ data_dictionary = self.data.get("hdx_data_dictionary")
277
+ if data_dictionary is None:
278
+ return None
279
+ if isinstance(data_dictionary, str):
280
+ return json.loads(data_dictionary)
281
+ return data_dictionary
282
+
283
+ def set_hdx_data_dictionary(self, data_dictionary: Sequence[dict]) -> None:
284
+ """Set the resource's data dictionary (column definitions). Each column
285
+ definition must be a dict with non-empty string values for field
286
+ (column name in the CSV file), label (human-readable column label) and
287
+ description (human-readable description of the column). If present,
288
+ data_type must be one of the PostgreSQL types produced by DataPusher+
289
+ (text, numeric, date, timestamp without time zone).
290
+
291
+ Args:
292
+ data_dictionary: Sequence of column definition dictionaries
293
+
294
+ Returns:
295
+ None
296
+ """
297
+ if not data_dictionary:
298
+ raise HDXError("hdx_data_dictionary must be a non-empty list!")
299
+ for i, column in enumerate(data_dictionary):
300
+ if not isinstance(column, dict):
301
+ raise HDXError(f"hdx_data_dictionary[{i}] must be a dict!")
302
+ for required_key in ("field", "label", "description"):
303
+ value = column.get(required_key)
304
+ if not isinstance(value, str) or not value.strip():
305
+ raise HDXError(
306
+ f"hdx_data_dictionary[{i}] is missing a non-empty '{required_key}'!"
307
+ )
308
+ if "data_type" in column:
309
+ data_type = column["data_type"]
310
+ if data_type not in self._valid_data_dictionary_types:
311
+ valid_types = ", ".join(sorted(self._valid_data_dictionary_types))
312
+ raise HDXError(
313
+ f"hdx_data_dictionary[{i}] has invalid 'data_type' "
314
+ f"'{data_type}'! Must be one of: {valid_types}"
315
+ )
316
+ self.data["hdx_data_dictionary"] = json.dumps(
317
+ data_dictionary, separators=(",", ":")
318
+ )
319
+
266
320
  def clean_format(self) -> str:
267
321
  """Clean the resource's format, setting it to None if it is invalid and
268
322
  cannot be mapped
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: hdx-python-api
3
- Version: 6.6.8
3
+ Version: 6.7.0
4
4
  Summary: HDX Python API for interacting with the Humanitarian Data Exchange
5
5
  Project-URL: Homepage, https://github.com/OCHA-DAP/hdx-python-api
6
6
  Author: Michael Rans
@@ -28,8 +28,8 @@ Requires-Python: >=3.10
28
28
  Requires-Dist: ckanapi>=4.11
29
29
  Requires-Dist: defopt>=7.0.0
30
30
  Requires-Dist: email-validator
31
- Requires-Dist: hdx-python-country>=4.1.1
32
- Requires-Dist: hdx-python-utilities>=4.0.8
31
+ Requires-Dist: hdx-python-country>=4.1.3
32
+ Requires-Dist: hdx-python-utilities>=4.1.2
33
33
  Requires-Dist: makefun
34
34
  Requires-Dist: requests
35
35
  Provides-Extra: docs
@@ -1,8 +1,8 @@
1
1
  hdx/api/__init__.py,sha256=11ozJKiUsqDCZ3_mcAHhGYUyGK_Unl54djVSBBExFB4,59
2
- hdx/api/_version.py,sha256=DuBvygFq7Ca-heE19aRqV-dGslR55qXsxEXRXjW5FNM,520
2
+ hdx/api/_version.py,sha256=ySRWjy-IYGDZdNB2wR2G-LZ8vwxqmQqgeGjL_1f7wkc,520
3
3
  hdx/api/configuration.py,sha256=9WvuVI-3th2Dv70_t_4VoyrlJwB_9qUSN5_zUKbp4sQ,23805
4
- hdx/api/hdx_base_configuration.yaml,sha256=9yJYroZwoyMQo6tcxGfWHWvYahnTma7Szx0J5Z378z4,2173
5
- hdx/api/locations.py,sha256=blnqOjEASvh0mWgmo2iEE7ES_xOuRwQo3fOMqTPV_P0,4139
4
+ hdx/api/hdx_base_configuration.yaml,sha256=jj2t5b7NnI499rHVwOjj3Q5BAjE8i7NDGVKnWZ5P5-4,2222
5
+ hdx/api/locations.py,sha256=bFl_cKz_AEQzkE_xWOkrRlUQ0GT2LMPSk-G_LsbV-Ns,4099
6
6
  hdx/api/remotehdx.py,sha256=IjKwP3jutlkrjvWCJPP7YM-xzg3pP_QVmiJfblllBY0,2079
7
7
  hdx/api/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  hdx/api/utilities/date_helper.py,sha256=sXxjRtXz-2rpBVZBbJAk80gfjpYKl_AtHplWr-rniIk,6396
@@ -13,8 +13,9 @@ hdx/api/utilities/url_utils.py,sha256=Pb4r-BAqIHYmeoB_i7QjutDp6nOAUh_OHvCtiNLda3
13
13
  hdx/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  hdx/data/dataset.py,sha256=0gS7tQSswTgG0g8YeXg0FL8pFVkocBQSRPFpQAurN00,99455
15
15
  hdx/data/hdxobject.py,sha256=_jlwYjb_CcjaXsTXObknMfq-OiNOcpJfLJ9b1vKV1Zo,27494
16
+ hdx/data/location.py,sha256=WIQRdY1boy6xPweIWRczWNiJLFrp4w0DVUwppHeNccU,8765
16
17
  hdx/data/organization.py,sha256=HGopFsCYOWrR7sUtZzZSXq6wLFw4djus2gzg6yNXOqc,12296
17
- hdx/data/resource.py,sha256=YcxGzKuFduLZIOnCvo-zcwJfnTGOmnvtDeFr2xKeOM4,35647
18
+ hdx/data/resource.py,sha256=SBAEou9Fj5sgpfoDmQ6j33W0srMMmhlsj00hiTuW86Y,38097
18
19
  hdx/data/resource_matcher.py,sha256=Yzk9pWa4zk1p09m6zcZMj0XZf5Y16Pa3KToOdUt7rP8,4247
19
20
  hdx/data/resource_view.py,sha256=GcYkh0iDAe6_m2ErY_A6i3Z7ZFX949eGt1TLnLZv_8s,7346
20
21
  hdx/data/showcase.py,sha256=vnRZ45xd9Ct65eFQkWZtMIJPFifZfKOrH4obO5D5Pvc,14062
@@ -24,7 +25,7 @@ hdx/facades/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
25
  hdx/facades/infer_arguments.py,sha256=DvyDuozfOPmoaV7HjhQySojGIrMWW9nos31auQBVVl8,4032
25
26
  hdx/facades/keyword_arguments.py,sha256=aVapHCfqBO6H7xxBgTdK9ISdgrBFsNypPDWihfQ3edU,1125
26
27
  hdx/facades/simple.py,sha256=QWsBkaHG_UM6X3Zu2u3LWVOQzeR24q-EUClrfsL_mkM,1064
27
- hdx_python_api-6.6.8.dist-info/METADATA,sha256=tRdOttf0O-eRc7iASB0HkCahGiC4dzf5SIVCCv8VHxw,4946
28
- hdx_python_api-6.6.8.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
29
- hdx_python_api-6.6.8.dist-info/licenses/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
30
- hdx_python_api-6.6.8.dist-info/RECORD,,
28
+ hdx_python_api-6.7.0.dist-info/METADATA,sha256=RYuk0RvwKnxNcRzipF3cYH0_Vk-16d0XRt7lbt8x6yo,4946
29
+ hdx_python_api-6.7.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
30
+ hdx_python_api-6.7.0.dist-info/licenses/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
31
+ hdx_python_api-6.7.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.32.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any