hdx-python-api 6.7.1__py3-none-any.whl → 6.7.3__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.7.1'
22
- __version_tuple__ = version_tuple = (6, 7, 1)
21
+ __version__ = version = '6.7.3'
22
+ __version_tuple__ = version_tuple = (6, 7, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,47 @@
1
+ """HDX site-wide statistics"""
2
+
3
+ from hdx.api.configuration import Configuration
4
+ from hdx.data.hdxobject import HDXError
5
+
6
+
7
+ def _call_stats_action(action: str, configuration: Configuration | None) -> dict:
8
+ """Call an HDX statistics action
9
+
10
+ Args:
11
+ action: CKAN action name
12
+ configuration: HDX configuration. Defaults to global configuration.
13
+
14
+ Returns:
15
+ Statistics dictionary
16
+ """
17
+ if configuration is None:
18
+ configuration = Configuration.read()
19
+ try:
20
+ return configuration.call_remoteckan(action, {})
21
+ except Exception as e:
22
+ raise HDXError(f"Failed when trying to call {action}!") from e
23
+
24
+
25
+ def get_general_statistics(configuration: Configuration | None = None) -> dict:
26
+ """Get general statistics about datasets, organizations and users on HDX
27
+
28
+ Args:
29
+ configuration: HDX configuration. Defaults to global configuration.
30
+
31
+ Returns:
32
+ Dictionary with keys datasets, organizations and users
33
+ """
34
+ return _call_stats_action("hdx_general_statistics", configuration)
35
+
36
+
37
+ def push_general_stats(configuration: Configuration | None = None) -> dict:
38
+ """Compute general statistics on HDX and push them to analytics. Requires
39
+ permission to manage basic scheduled tasks.
40
+
41
+ Args:
42
+ configuration: HDX configuration. Defaults to global configuration.
43
+
44
+ Returns:
45
+ Statistics that were pushed
46
+ """
47
+ return _call_stats_action("hdx_push_general_stats", configuration)
hdx/data/activity.py ADDED
@@ -0,0 +1,247 @@
1
+ """Activity class containing all logic for reading activity streams."""
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 HDXError, HDXObject
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Activity(HDXObject):
14
+ """Activity class containing all logic for reading activity streams. Activities
15
+ are read only.
16
+
17
+ Args:
18
+ initial_data: Initial activity 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": "activity_show",
40
+ "data_show": "activity_data_show",
41
+ "diff": "activity_diff",
42
+ "package_list": "package_activity_list",
43
+ "organization_list": "organization_activity_list",
44
+ "group_list": "group_activity_list",
45
+ "user_list": "user_activity_list",
46
+ "recently_changed_packages": "recently_changed_packages_activity_list",
47
+ }
48
+
49
+ @classmethod
50
+ def read_from_hdx(
51
+ cls, identifier: str, configuration: Configuration | None = None
52
+ ) -> Optional["Activity"]:
53
+ """Reads the activity given by identifier from HDX and returns Activity object.
54
+ HDX requires permission to edit the object the activity relates to.
55
+
56
+ Args:
57
+ identifier: Identifier of activity
58
+ configuration: HDX configuration. Defaults to global configuration.
59
+
60
+ Returns:
61
+ Activity object if successful read, None if not
62
+ """
63
+ return cls._read_from_hdx_class("activity", identifier, configuration)
64
+
65
+ def get_object_type(self) -> str:
66
+ """Get the CKAN object type the activity relates to eg. package,
67
+ organization, group or user. This is the last word of the activity type
68
+ (eg. "changed package") with "dataset" (from "follow dataset") mapped to
69
+ "package".
70
+
71
+ Returns:
72
+ Object type
73
+ """
74
+ object_type = self.data["activity_type"].split(" ")[-1]
75
+ if object_type == "dataset":
76
+ return "package"
77
+ return object_type
78
+
79
+ def get_data(self) -> dict:
80
+ """Get the data of the activity which includes a snapshot of the object
81
+ at the time of the activity. HDX requires permission to edit the object
82
+ the activity relates to.
83
+
84
+ Returns:
85
+ Activity data
86
+ """
87
+ return self._write_to_hdx("data_show", {"id": self.data["id"]}, "id")
88
+
89
+ def get_diff(self, diff_type: str = "unified") -> dict:
90
+ """Get a diff of the object compared to its previous version. HDX requires
91
+ permission to edit the object the activity relates to.
92
+
93
+ Args:
94
+ diff_type: One of unified, context or html. Defaults to unified.
95
+
96
+ Returns:
97
+ Activity diff
98
+ """
99
+ data = {
100
+ "id": self.data["id"],
101
+ "object_type": self.get_object_type(),
102
+ "diff_type": diff_type,
103
+ }
104
+ return self._write_to_hdx("diff", data, "id")
105
+
106
+ @classmethod
107
+ def _get_list(
108
+ cls,
109
+ action: str,
110
+ data: dict,
111
+ configuration: Configuration | None = None,
112
+ all_pages: bool = False,
113
+ max_activities: int | None = None,
114
+ ) -> list["Activity"]:
115
+ """Helper method to get a list of activities from HDX
116
+
117
+ Args:
118
+ action: Action to perform
119
+ data: Data to pass to CKAN including any offset and limit
120
+ configuration: HDX configuration. Defaults to global configuration.
121
+ all_pages: Whether to get all pages of results. Defaults to False.
122
+ max_activities: Maximum number of activities if all_pages is True. Defaults to None (no maximum).
123
+
124
+ Returns:
125
+ List of Activity objects
126
+ """
127
+ activity = cls(configuration=configuration)
128
+ if not all_pages:
129
+ result = activity._write_to_hdx(action, data)
130
+ return [cls(x, configuration=configuration) for x in result]
131
+ data = dict(data)
132
+ # HDX silently caps limit at ckan.activity_list_limit_max
133
+ limit = min(data.get("limit", 100), 100)
134
+ data["limit"] = limit
135
+ offset = data.get("offset", 0)
136
+ activities = []
137
+ while True:
138
+ data["offset"] = offset
139
+ result = activity._write_to_hdx(action, data)
140
+ activities.extend(cls(x, configuration=configuration) for x in result)
141
+ if max_activities is not None and len(activities) >= max_activities:
142
+ return activities[:max_activities]
143
+ if len(result) < limit:
144
+ return activities
145
+ offset += limit
146
+
147
+ @classmethod
148
+ def get_activities(
149
+ cls,
150
+ object_type: str,
151
+ identifier: str,
152
+ configuration: Configuration | None = None,
153
+ all_pages: bool = False,
154
+ **kwargs: Any,
155
+ ) -> list["Activity"]:
156
+ """Get the activity stream of a CKAN object. One page of results (whose
157
+ size is given by limit) is returned unless all_pages is True.
158
+
159
+ Args:
160
+ object_type: One of package, organization, group or user
161
+ identifier: Id or name of object
162
+ configuration: HDX configuration. Defaults to global configuration.
163
+ all_pages: Whether to get all pages of results. Defaults to False.
164
+ **kwargs: See below
165
+ offset (int): Where to start getting activity items from. Defaults to 0.
166
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
167
+ after (int | str): Only activities after this timestamp (package and user only)
168
+ before (int | str): Only activities before this timestamp (package and user only)
169
+ include_hidden_activity (bool): Include hidden activity, sysadmins only (package, organization and group only)
170
+ activity_types (list): Activity types to include (package only)
171
+ exclude_activity_types (list): Activity types to exclude (package only)
172
+
173
+ Returns:
174
+ List of Activity objects
175
+ """
176
+ if not identifier:
177
+ raise HDXError(f"No id or name for {object_type} to get activities!")
178
+ data = {"id": identifier}
179
+ data.update(kwargs)
180
+ return cls._get_list(f"{object_type}_list", data, configuration, all_pages)
181
+
182
+ @classmethod
183
+ def get_recently_changed_packages(
184
+ cls,
185
+ configuration: Configuration | None = None,
186
+ all_pages: bool = False,
187
+ max_activities: int | None = 1000,
188
+ **kwargs: Any,
189
+ ) -> list["Activity"]:
190
+ """Get the activity stream of all recently added or changed datasets.
191
+ One page of results (whose size is given by limit) is returned unless
192
+ all_pages is True. all_pages is capped by max_activities because the
193
+ stream covers the whole history of HDX.
194
+
195
+ Args:
196
+ configuration: HDX configuration. Defaults to global configuration.
197
+ all_pages: Whether to get all pages of results. Defaults to False.
198
+ max_activities: Maximum number of activities if all_pages is True. Defaults to 1000. None means no maximum (very slow).
199
+ **kwargs: See below
200
+ offset (int): Where to start getting activity items from. Defaults to 0.
201
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
202
+
203
+ Returns:
204
+ List of Activity objects
205
+ """
206
+ return cls._get_list(
207
+ "recently_changed_packages",
208
+ kwargs,
209
+ configuration,
210
+ all_pages,
211
+ max_activities,
212
+ )
213
+
214
+ def check_required_fields(self, ignore_fields: Sequence[str] = ()) -> None:
215
+ """Activities are read only so this raises an exception
216
+
217
+ Args:
218
+ ignore_fields: Fields to ignore. Default is ().
219
+
220
+ Returns:
221
+ None
222
+ """
223
+ raise HDXError("Activities are read only!")
224
+
225
+ def update_in_hdx(self, **kwargs: Any) -> None:
226
+ """Activities are read only so this raises an exception
227
+
228
+ Returns:
229
+ None
230
+ """
231
+ raise HDXError("Activities are read only!")
232
+
233
+ def create_in_hdx(self, **kwargs: Any) -> None:
234
+ """Activities are read only so this raises an exception
235
+
236
+ Returns:
237
+ None
238
+ """
239
+ raise HDXError("Activities are read only!")
240
+
241
+ def delete_from_hdx(self) -> None:
242
+ """Activities are read only so this raises an exception
243
+
244
+ Returns:
245
+ None
246
+ """
247
+ raise HDXError("Activities are read only!")
hdx/data/dataset.py CHANGED
@@ -40,6 +40,7 @@ from hdx.api.configuration import Configuration
40
40
  from hdx.api.locations import Locations
41
41
  from hdx.api.utilities.date_helper import DateHelper
42
42
  from hdx.api.utilities.filestore_helper import FilestoreHelper
43
+ from hdx.data.activity import Activity
43
44
  from hdx.data.hdxobject import HDXError, HDXObject
44
45
  from hdx.data.resource_matcher import ResourceMatcher
45
46
 
@@ -146,6 +147,7 @@ class Dataset(HDXObject):
146
147
  "list": "package_list",
147
148
  "autocomplete": "package_autocomplete",
148
149
  "create_default_views": "package_create_default_resource_views",
150
+ "fs_check_info": "fs_check_info_show",
149
151
  }
150
152
 
151
153
  def __setitem__(self, key: Any, value: Any) -> None:
@@ -1428,6 +1430,26 @@ class Dataset(HDXObject):
1428
1430
  resources.append(resource)
1429
1431
  return resources
1430
1432
 
1433
+ def get_activities(self, all_pages: bool = False, **kwargs: Any) -> list[Activity]:
1434
+ """Get the activity stream of this dataset. One page of results (whose size
1435
+ is given by limit) is returned unless all_pages is True.
1436
+
1437
+ Args:
1438
+ all_pages: Whether to get all pages of results. Defaults to False.
1439
+ **kwargs: See below
1440
+ offset (int): Where to start getting activity items from. Defaults to 0.
1441
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
1442
+ after (int | str): Only activities after this timestamp
1443
+ before (int | str): Only activities before this timestamp
1444
+ include_hidden_activity (bool): Include hidden activity (sysadmins only)
1445
+ activity_types (list): Activity types to include
1446
+ exclude_activity_types (list): Activity types to exclude
1447
+
1448
+ Returns:
1449
+ List of Activity objects
1450
+ """
1451
+ return self._get_activities("package", all_pages, **kwargs)
1452
+
1431
1453
  @classmethod
1432
1454
  def autocomplete(
1433
1455
  cls,
@@ -2306,6 +2328,25 @@ class Dataset(HDXObject):
2306
2328
  return None
2307
2329
  return f"{self.configuration.get_hdx_site_url()}/api/3/action/package_show?id={name_or_id}"
2308
2330
 
2331
+ def get_fs_check_info(self) -> list[dict]:
2332
+ """Get file structure check information for the dataset's resources
2333
+ from HDX. Each list item is a dictionary from resource name to a list
2334
+ of check events, each of which can have keys such as state, timestamp,
2335
+ sheet_changes and hxl_proxy_response.
2336
+
2337
+ Returns:
2338
+ List of dictionaries from resource name to file structure checks
2339
+ """
2340
+ success, result = self._read_from_hdx(
2341
+ "dataset",
2342
+ self.get_name_or_id(prefer_name=False),
2343
+ "id",
2344
+ self.actions()["fs_check_info"],
2345
+ )
2346
+ if success:
2347
+ return result
2348
+ return []
2349
+
2309
2350
  def generate_resource(
2310
2351
  self,
2311
2352
  folder: Path | str,
hdx/data/hdxobject.py CHANGED
@@ -9,7 +9,7 @@ from collections import UserDict
9
9
  from collections.abc import Sequence
10
10
  from os.path import isfile
11
11
  from pathlib import Path
12
- from typing import Any, Optional, Union
12
+ from typing import TYPE_CHECKING, Any, Optional, Union
13
13
 
14
14
  from ckanapi.errors import NotFound
15
15
  from hdx.utilities.dictandlist import merge_two_dictionaries
@@ -20,6 +20,9 @@ from hdx.utilities.loader import (
20
20
 
21
21
  from hdx.api.configuration import Configuration
22
22
 
23
+ if TYPE_CHECKING:
24
+ from hdx.data.activity import Activity
25
+
23
26
  logger = logging.getLogger(__name__)
24
27
 
25
28
 
@@ -376,6 +379,30 @@ class HDXObject(UserDict, ABC):
376
379
  **kwargs,
377
380
  )
378
381
 
382
+ def _get_activities(
383
+ self, object_type: str, all_pages: bool = False, **kwargs: Any
384
+ ) -> list["Activity"]:
385
+ """Helper method to get the activity stream of this object
386
+
387
+ Args:
388
+ object_type: One of package, organization, group or user
389
+ all_pages: Whether to get all pages of results. Defaults to False.
390
+ **kwargs: Parameters to pass to Activity.get_activities
391
+
392
+ Returns:
393
+ List of Activity objects
394
+ """
395
+ # Imported here as hdx.data.activity imports this module
396
+ from hdx.data.activity import Activity
397
+
398
+ return Activity.get_activities(
399
+ object_type,
400
+ self.data.get("id") or self.data.get("name"),
401
+ self.configuration,
402
+ all_pages,
403
+ **kwargs,
404
+ )
405
+
379
406
  def _write_to_hdx(
380
407
  self,
381
408
  action: str,
hdx/data/location.py CHANGED
@@ -5,6 +5,7 @@ from collections.abc import Sequence
5
5
  from typing import Any, Optional
6
6
 
7
7
  from hdx.api.configuration import Configuration
8
+ from hdx.data.activity import Activity
8
9
  from hdx.data.hdxobject import HDXObject
9
10
 
10
11
  logger = logging.getLogger(__name__)
@@ -217,6 +218,22 @@ class Location(HDXObject):
217
218
  if len(group["name"]) == 3 and group.get("data_completeness") == "active"
218
219
  )
219
220
 
221
+ def get_activities(self, all_pages: bool = False, **kwargs: Any) -> list[Activity]:
222
+ """Get the activity stream of this location. One page of results (whose size
223
+ is given by limit) is returned unless all_pages is True.
224
+
225
+ Args:
226
+ all_pages: Whether to get all pages of results. Defaults to False.
227
+ **kwargs: See below
228
+ offset (int): Where to start getting activity items from. Defaults to 0.
229
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
230
+ include_hidden_activity (bool): Include hidden activity (sysadmins only)
231
+
232
+ Returns:
233
+ List of Activity objects
234
+ """
235
+ return self._get_activities("group", all_pages, **kwargs)
236
+
220
237
  @classmethod
221
238
  def autocomplete(
222
239
  cls,
hdx/data/organization.py CHANGED
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union
8
8
  import hdx.data.dataset
9
9
  import hdx.data.user as user_module
10
10
  from hdx.api.configuration import Configuration
11
+ from hdx.data.activity import Activity
11
12
  from hdx.data.hdxobject import HDXError, HDXObject
12
13
 
13
14
  if TYPE_CHECKING:
@@ -311,6 +312,22 @@ class Organization(HDXObject):
311
312
 
312
313
  return final_orgs
313
314
 
315
+ def get_activities(self, all_pages: bool = False, **kwargs: Any) -> list[Activity]:
316
+ """Get the activity stream of this organization. One page of results (whose size
317
+ is given by limit) is returned unless all_pages is True.
318
+
319
+ Args:
320
+ all_pages: Whether to get all pages of results. Defaults to False.
321
+ **kwargs: See below
322
+ offset (int): Where to start getting activity items from. Defaults to 0.
323
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
324
+ include_hidden_activity (bool): Include hidden activity (sysadmins only)
325
+
326
+ Returns:
327
+ List of Activity objects
328
+ """
329
+ return self._get_activities("organization", all_pages, **kwargs)
330
+
314
331
  @classmethod
315
332
  def autocomplete(
316
333
  cls,
hdx/data/resource.py CHANGED
@@ -72,6 +72,8 @@ class Resource(HDXObject):
72
72
  "datastore_upsert": "datastore_upsert",
73
73
  "datastore_search": "datastore_search",
74
74
  "datastore_search_sql": "datastore_search_sql",
75
+ "datapusher_submit": "datapusher_submit",
76
+ "datapusher_status": "datapusher_status",
75
77
  }
76
78
 
77
79
  def update_from_yaml(
@@ -319,6 +321,69 @@ class Resource(HDXObject):
319
321
  data_dictionary, separators=(",", ":")
320
322
  )
321
323
 
324
+ def update_hdx_data_dictionary(
325
+ self,
326
+ data_dictionary: Sequence[dict],
327
+ replace: bool = False,
328
+ update_datastore: bool = True,
329
+ ) -> None:
330
+ """Update the resource's data dictionary (column definitions) in HDX
331
+ without changing last_modified, so no dataset update notifications are
332
+ sent. Column definitions are as for set_hdx_data_dictionary. Unless
333
+ replace is True, they are merged by field into the existing data
334
+ dictionary in the resource metadata (so call read_from_hdx first).
335
+ The resource's hdx_data_dictionary field is patched, then if
336
+ update_datastore is True and the resource has a datastore, the
337
+ datastore field labels and descriptions are updated using
338
+ datastore_create without reloading the data.
339
+
340
+ Args:
341
+ data_dictionary: Sequence of column definition dictionaries
342
+ replace: Replace rather than merge into existing data dictionary. Defaults to False.
343
+ update_datastore: Update datastore field labels and descriptions. Defaults to True.
344
+
345
+ Returns:
346
+ None
347
+ """
348
+ if replace:
349
+ merged = [dict(column) for column in data_dictionary]
350
+ else:
351
+ new_columns = {column["field"]: column for column in data_dictionary}
352
+ merged = []
353
+ for column in self.get_hdx_data_dictionary() or []:
354
+ new_column = new_columns.pop(column.get("field"), None)
355
+ merged.append({**column, **new_column} if new_column else column)
356
+ merged.extend(new_columns.values())
357
+ self.set_hdx_data_dictionary(merged)
358
+ self._write_to_hdx(
359
+ "patch",
360
+ {
361
+ "id": self.data["id"],
362
+ "hdx_data_dictionary": self.data["hdx_data_dictionary"],
363
+ },
364
+ "id",
365
+ )
366
+ if not update_datastore or not self.has_datastore():
367
+ return
368
+ columns = {column["field"]: column for column in merged}
369
+ fields = []
370
+ for field in self.get_datastore_fields():
371
+ info = dict(field.get("info") or {})
372
+ column = columns.pop(field["id"], None)
373
+ if column:
374
+ info["label"] = column["label"]
375
+ info["notes"] = column["description"]
376
+ fields.append({"id": field["id"], "type": field["type"], "info": info})
377
+ if columns:
378
+ logger.warning(
379
+ f"Data dictionary fields {', '.join(columns)} not in datastore for resource {self.data['id']}!"
380
+ )
381
+ self._write_to_hdx(
382
+ "datastore_create",
383
+ {"resource_id": self.data["id"], "force": True, "fields": fields},
384
+ "resource_id",
385
+ )
386
+
322
387
  def clean_format(self) -> str:
323
388
  """Clean the resource's format, setting it to None if it is invalid and
324
389
  cannot be mapped
@@ -683,6 +748,30 @@ class Resource(HDXObject):
683
748
  logger.debug(result)
684
749
  return resources
685
750
 
751
+ def _download_file(
752
+ self,
753
+ url: str,
754
+ folder: Path | str | None,
755
+ filename: str,
756
+ retriever: Retrieve | None,
757
+ ) -> Path:
758
+ """Helper method to download a file from HDX
759
+
760
+ Args:
761
+ url: URL to download
762
+ folder: Folder to download file to. None means temporary folder.
763
+ filename: Filename to use
764
+ retriever: Retrieve object to use. None means do not use one.
765
+
766
+ Returns:
767
+ Path to downloaded file
768
+ """
769
+ session = hdx.api.utilities.url_utils.get_ckan_ready_session(self.configuration)
770
+ with Download(session=session) as downloader:
771
+ if retriever:
772
+ downloader = retriever.clone(downloader)
773
+ return downloader.download_file(url, folder=folder, filename=filename)
774
+
686
775
  def download(
687
776
  self, folder: Path | str | None = None, retriever: Retrieve | None = None
688
777
  ) -> tuple[str, Path]:
@@ -706,12 +795,7 @@ class Resource(HDXObject):
706
795
  file_format = f".{self.get_format()}"
707
796
  if not filename.endswith(file_format):
708
797
  filename = f"{filename}{file_format}"
709
- session = hdx.api.utilities.url_utils.get_ckan_ready_session(self.configuration)
710
- with Download(session=session) as downloader:
711
- if retriever:
712
- downloader = retriever.clone(downloader)
713
- path = downloader.download_file(url, folder=folder, filename=filename)
714
- return url, path
798
+ return url, self._download_file(url, folder, filename, retriever)
715
799
 
716
800
  def get_datastore_dump_url(self, file_format: str = "csv") -> str:
717
801
  """Get the URL from which the resource datastore can be downloaded in
@@ -755,19 +839,15 @@ class Resource(HDXObject):
755
839
  Returns:
756
840
  (URL downloaded, Path to downloaded file)
757
841
  """
842
+ file_format = file_format.lower()
758
843
  url = self.get_datastore_dump_url(file_format)
759
844
  logger.debug(f"Downloading {url}")
760
845
  if filename is None:
761
846
  filename = self.data["name"]
762
- extension = f".{file_format.lower()}"
847
+ extension = f".{file_format}"
763
848
  if not filename.endswith(extension):
764
849
  filename = f"{filename}{extension}"
765
- session = hdx.api.utilities.url_utils.get_ckan_ready_session(self.configuration)
766
- with Download(session=session) as downloader:
767
- if retriever:
768
- downloader = retriever.clone(downloader)
769
- path = downloader.download_file(url, folder=folder, filename=filename)
770
- return url, path
850
+ return url, self._download_file(url, folder, filename, retriever)
771
851
 
772
852
  @staticmethod
773
853
  def get_all_resource_ids_in_datastore(
@@ -930,11 +1010,18 @@ class Resource(HDXObject):
930
1010
  sort: Comma separated field names with optional asc/desc eg. "code desc". Defaults to None.
931
1011
  distinct: Whether to return only distinct rows. Defaults to False.
932
1012
  page_size: Number of rows to request per call. Defaults to 10000.
933
- **kwargs: Other parameters to pass to datastore_search eg. plain, language.
1013
+ **kwargs: Other parameters to pass to datastore_search eg. plain, language. records_format csv and tsv are not supported.
934
1014
 
935
1015
  Returns:
936
1016
  Iterator of record dictionaries
937
1017
  """
1018
+ if page_size < 1:
1019
+ raise HDXError("page_size must be at least 1!")
1020
+ records_format = kwargs.get("records_format")
1021
+ if records_format in ("csv", "tsv"):
1022
+ raise HDXError(
1023
+ f"records_format {records_format} is not supported! Use search_datastore instead."
1024
+ )
938
1025
  offset = 0
939
1026
  while True:
940
1027
  result = self.search_datastore(
@@ -950,9 +1037,11 @@ class Resource(HDXObject):
950
1037
  )
951
1038
  records = result["records"]
952
1039
  yield from records
953
- if len(records) < page_size:
1040
+ # HDX caps limit at its maximum rows per call and returns the limit applied
1041
+ limit = result["limit"]
1042
+ if len(records) < limit:
954
1043
  return
955
- offset += page_size
1044
+ offset += limit
956
1045
 
957
1046
  def get_datastore_fields(self) -> list[dict]:
958
1047
  """Get the field definitions of the resource datastore excluding the
@@ -971,7 +1060,8 @@ class Resource(HDXObject):
971
1060
  ) -> list[dict]:
972
1061
  """Run a read-only SQL query against the HDX datastore using CKAN
973
1062
  datastore_search_sql. Tables are named by resource id, which must be
974
- double quoted eg. SELECT * FROM "RESOURCE_ID".
1063
+ double quoted eg. SELECT * FROM "RESOURCE_ID". Logs a warning if HDX
1064
+ truncates the results.
975
1065
 
976
1066
  Args:
977
1067
  sql: SQL SELECT statement
@@ -982,7 +1072,12 @@ class Resource(HDXObject):
982
1072
  """
983
1073
  resource = Resource(configuration=configuration)
984
1074
  result = resource._write_to_hdx("datastore_search_sql", {"sql": sql})
985
- return result["records"]
1075
+ records = result["records"]
1076
+ if result.get("records_truncated"):
1077
+ logger.warning(
1078
+ f"SQL query results truncated by HDX to {len(records)} rows!"
1079
+ )
1080
+ return records
986
1081
 
987
1082
  def delete_datastore(self) -> None:
988
1083
  """Delete a resource from the HDX datastore
@@ -1000,6 +1095,41 @@ class Resource(HDXObject):
1000
1095
  if not success:
1001
1096
  logger.debug(result)
1002
1097
 
1098
+ def submit_to_datapusher(self, ignore_hash: bool = False) -> bool:
1099
+ """Submit the resource to DataPusher+ to load its file into the
1100
+ datastore. This does not change last_modified. Unlike the automatic
1101
+ submission on file upload, HDX's datastore allowlist and format checks
1102
+ are not applied.
1103
+
1104
+ Args:
1105
+ ignore_hash: Reload even if the file hash is unchanged. Defaults to False.
1106
+
1107
+ Returns:
1108
+ True if submitted, False if not (eg. a job is already pending)
1109
+ """
1110
+ return self._write_to_hdx(
1111
+ "datapusher_submit",
1112
+ {"resource_id": self.data["id"], "ignore_hash": ignore_hash},
1113
+ "resource_id",
1114
+ )
1115
+
1116
+ def get_datapusher_status(self) -> dict | None:
1117
+ """Get the status of the resource's latest DataPusher+ job
1118
+
1119
+ Returns:
1120
+ Status dictionary with keys including status and error or None if no job
1121
+ """
1122
+ success, result = self._read_from_hdx(
1123
+ "datapusher",
1124
+ self.data["id"],
1125
+ "resource_id",
1126
+ self.actions()["datapusher_status"],
1127
+ )
1128
+ if success:
1129
+ return result
1130
+ logger.debug(result)
1131
+ return None
1132
+
1003
1133
  def get_resource_views(self) -> list[ResourceView]:
1004
1134
  """Get any resource views in the resource
1005
1135
 
@@ -1159,6 +1289,21 @@ class Resource(HDXObject):
1159
1289
  else:
1160
1290
  logger.debug(result)
1161
1291
 
1292
+ def get_fs_check_info(self) -> list[dict]:
1293
+ """Get file structure check information for the resource from its
1294
+ fs_check_info field. Each check event can have keys such as state,
1295
+ timestamp, sheet_changes and hxl_proxy_response.
1296
+
1297
+ Returns:
1298
+ List of file structure check events
1299
+ """
1300
+ fs_check_info = self.data.get("fs_check_info")
1301
+ if not fs_check_info:
1302
+ return []
1303
+ if isinstance(fs_check_info, str):
1304
+ return json.loads(fs_check_info)
1305
+ return fs_check_info
1306
+
1162
1307
  def is_marked_data_updated(self) -> bool:
1163
1308
  """Return if the resource's data is marked to be updated
1164
1309
 
hdx/data/user.py CHANGED
@@ -7,6 +7,7 @@ from typing import Any, Optional
7
7
 
8
8
  import hdx.data.organization
9
9
  from hdx.api.configuration import Configuration
10
+ from hdx.data.activity import Activity
10
11
  from hdx.data.hdxobject import HDXError, HDXObject
11
12
 
12
13
  logger = logging.getLogger(__name__)
@@ -44,6 +45,7 @@ class User(HDXObject):
44
45
  "list": "user_list",
45
46
  "listorgs": "organization_list_for_user",
46
47
  "token_list": "api_token_list",
48
+ "token_info": "hdx_token_info",
47
49
  "autocomplete": "user_autocomplete",
48
50
  }
49
51
 
@@ -333,7 +335,7 @@ class User(HDXObject):
333
335
  Returns:
334
336
  List of organization dicts in HDX that logged in user is a member of
335
337
  """
336
- user = User(configuration=configuration)
338
+ user = cls(configuration=configuration)
337
339
  try:
338
340
  return user.configuration.call_remoteckan(
339
341
  cls.actions()["listorgs"], {"permission": permission}
@@ -453,6 +455,42 @@ class User(HDXObject):
453
455
  return result
454
456
  return []
455
457
 
458
+ def get_activities(self, all_pages: bool = False, **kwargs: Any) -> list[Activity]:
459
+ """Get the activity stream of this user. One page of results (whose size
460
+ is given by limit) is returned unless all_pages is True. HDX requires
461
+ authorisation to view the user's profile.
462
+
463
+ Args:
464
+ all_pages: Whether to get all pages of results. Defaults to False.
465
+ **kwargs: See below
466
+ offset (int): Where to start getting activity items from. Defaults to 0.
467
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
468
+ after (int | str): Only activities after this timestamp
469
+ before (int | str): Only activities before this timestamp
470
+
471
+ Returns:
472
+ List of Activity objects
473
+ """
474
+ return self._get_activities("user", all_pages, **kwargs)
475
+
476
+ @classmethod
477
+ def get_current_token_info(cls, configuration: Configuration | None = None) -> dict:
478
+ """Get information about the API token in the configuration: the md5
479
+ hash of the token owner's email (email_hash) and the token name
480
+ (token_name).
481
+
482
+ Args:
483
+ configuration: HDX configuration. Defaults to global configuration.
484
+
485
+ Returns:
486
+ Dictionary with keys email_hash and token_name
487
+ """
488
+ user = cls(configuration=configuration)
489
+ try:
490
+ return user.configuration.call_remoteckan(cls.actions()["token_info"], {})
491
+ except Exception as e:
492
+ raise HDXError("Failed when trying to get API token info!") from e
493
+
456
494
  @classmethod
457
495
  def autocomplete(
458
496
  cls,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hdx-python-api
3
- Version: 6.7.1
3
+ Version: 6.7.3
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
@@ -1,5 +1,5 @@
1
1
  hdx/api/__init__.py,sha256=11ozJKiUsqDCZ3_mcAHhGYUyGK_Unl54djVSBBExFB4,59
2
- hdx/api/_version.py,sha256=7uXJAam46P0GVJ-CiBxd6tVnUUkt-epJdnvQ6m2fpb4,520
2
+ hdx/api/_version.py,sha256=O7pvZmvSWCf7hJuRAdEJClOMPVWnCFeffbn1fVR7KII,520
3
3
  hdx/api/configuration.py,sha256=9WvuVI-3th2Dv70_t_4VoyrlJwB_9qUSN5_zUKbp4sQ,23805
4
4
  hdx/api/hdx_base_configuration.yaml,sha256=jj2t5b7NnI499rHVwOjj3Q5BAjE8i7NDGVKnWZ5P5-4,2222
5
5
  hdx/api/locations.py,sha256=bFl_cKz_AEQzkE_xWOkrRlUQ0GT2LMPSk-G_LsbV-Ns,4099
@@ -9,23 +9,25 @@ hdx/api/utilities/date_helper.py,sha256=sXxjRtXz-2rpBVZBbJAk80gfjpYKl_AtHplWr-rn
9
9
  hdx/api/utilities/filestore_helper.py,sha256=0FcUpGHpYlAzlNDpDxdXbSqV6VHFfGCLakCmK37hieU,6917
10
10
  hdx/api/utilities/hdx_error_handler.py,sha256=x1rb2qBO0C6CJjqToSFj_XB1F3pf3nHpA8otxXfTFzA,8711
11
11
  hdx/api/utilities/hdx_state.py,sha256=KzrK0A-H5R6cRPqwF4d23_S_zfIa1ow4K5u46FesGmY,2442
12
+ hdx/api/utilities/hdx_stats.py,sha256=Vc3CesIbROeumOmSyVItmUaGjFv0MF4r4mlKbpI0rxo,1474
12
13
  hdx/api/utilities/url_utils.py,sha256=Pb4r-BAqIHYmeoB_i7QjutDp6nOAUh_OHvCtiNLda3Y,4534
13
14
  hdx/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- hdx/data/dataset.py,sha256=0gS7tQSswTgG0g8YeXg0FL8pFVkocBQSRPFpQAurN00,99455
15
- hdx/data/hdxobject.py,sha256=_jlwYjb_CcjaXsTXObknMfq-OiNOcpJfLJ9b1vKV1Zo,27494
16
- hdx/data/location.py,sha256=WIQRdY1boy6xPweIWRczWNiJLFrp4w0DVUwppHeNccU,8765
17
- hdx/data/organization.py,sha256=HGopFsCYOWrR7sUtZzZSXq6wLFw4djus2gzg6yNXOqc,12296
18
- hdx/data/resource.py,sha256=iVARfA3_8gGsGjXgNxomm7umAadvxzFsGGqxTpBB3WQ,45196
15
+ hdx/data/activity.py,sha256=YNa7hAgsMP96EIUX7UMInxAXnJ8TynE1KpecRrv8Vso,9200
16
+ hdx/data/dataset.py,sha256=BdNPybJklyhI_IeyY0QGrohau6yTEv-jeFVFLjs5CeQ,101301
17
+ hdx/data/hdxobject.py,sha256=EG5KkVpi-yF6m6HVF79EqDneq1W7mo5M-bPJHG79T0I,28383
18
+ hdx/data/location.py,sha256=CvDGfzQnPIB0aT7WOc60zhnAfvDFMJFcrbfacOsYR-U,9598
19
+ hdx/data/organization.py,sha256=IRen2F2NsXLC8r-PVTS9eQp7LTn_44OcDUa1S7SCKiQ,13140
20
+ hdx/data/resource.py,sha256=KuO9UB6OP7G2rcKcwTCjdfseQpcub-aK6LscXtI8IM8,50909
19
21
  hdx/data/resource_matcher.py,sha256=Yzk9pWa4zk1p09m6zcZMj0XZf5Y16Pa3KToOdUt7rP8,4247
20
22
  hdx/data/resource_view.py,sha256=GcYkh0iDAe6_m2ErY_A6i3Z7ZFX949eGt1TLnLZv_8s,7346
21
23
  hdx/data/showcase.py,sha256=vnRZ45xd9Ct65eFQkWZtMIJPFifZfKOrH4obO5D5Pvc,14062
22
- hdx/data/user.py,sha256=STWtZtFHMyXPjLyEioK4rV5Cj2eH88uZJG8gIH8NdqM,15613
24
+ hdx/data/user.py,sha256=NXBuvUu-pNVSEelL59wdNQgeWmDgeWke_6JxViww3Cc,17320
23
25
  hdx/data/vocabulary.py,sha256=sDDVh9ivCbZlPKXIdOoj0ltTdRufL0ypfGZUirIb2IA,20776
24
26
  hdx/facades/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
27
  hdx/facades/infer_arguments.py,sha256=DvyDuozfOPmoaV7HjhQySojGIrMWW9nos31auQBVVl8,4032
26
28
  hdx/facades/keyword_arguments.py,sha256=aVapHCfqBO6H7xxBgTdK9ISdgrBFsNypPDWihfQ3edU,1125
27
29
  hdx/facades/simple.py,sha256=QWsBkaHG_UM6X3Zu2u3LWVOQzeR24q-EUClrfsL_mkM,1064
28
- hdx_python_api-6.7.1.dist-info/METADATA,sha256=7SaqX2zZKJ828e94hbPjvjJ_3q5obaL0NU9Ghut_86A,4946
29
- hdx_python_api-6.7.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
30
- hdx_python_api-6.7.1.dist-info/licenses/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
31
- hdx_python_api-6.7.1.dist-info/RECORD,,
30
+ hdx_python_api-6.7.3.dist-info/METADATA,sha256=UI9iCTmytO8zgeetqK5dfGaRHW3d4qzu0ex1r8WE8c0,4946
31
+ hdx_python_api-6.7.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
32
+ hdx_python_api-6.7.3.dist-info/licenses/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
33
+ hdx_python_api-6.7.3.dist-info/RECORD,,