hdx-python-api 6.7.2__tar.gz → 6.7.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/PKG-INFO +1 -1
  2. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/documentation/index.md +80 -5
  3. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/_version.py +2 -2
  4. hdx_python_api-6.7.3/src/hdx/api/utilities/hdx_stats.py +47 -0
  5. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/activity.py +3 -1
  6. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/dataset.py +21 -7
  7. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/hdxobject.py +28 -1
  8. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/location.py +1 -7
  9. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/organization.py +1 -7
  10. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/resource.py +163 -18
  11. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/user.py +21 -8
  12. hdx_python_api-6.7.3/tests/hdx/api/utilities/test_hdx_stats.py +59 -0
  13. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_activity.py +6 -1
  14. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_dataset_noncore.py +38 -0
  15. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_resource.py +210 -1
  16. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_user.py +28 -0
  17. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/.github/workflows/publish.yaml +0 -0
  18. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/.github/workflows/run-python-tests.yaml +0 -0
  19. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/.gitignore +0 -0
  20. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/.pre-commit-config.yaml +0 -0
  21. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/CLAUDE.md +0 -0
  22. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/LICENSE +0 -0
  23. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/README.md +0 -0
  24. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/documentation/.readthedocs.yaml +0 -0
  25. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/documentation/mkdocs.yaml +0 -0
  26. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/pyproject.toml +0 -0
  27. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/__init__.py +0 -0
  28. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/configuration.py +0 -0
  29. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/hdx_base_configuration.yaml +0 -0
  30. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/locations.py +0 -0
  31. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/remotehdx.py +0 -0
  32. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/utilities/__init__.py +0 -0
  33. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/utilities/date_helper.py +0 -0
  34. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/utilities/filestore_helper.py +0 -0
  35. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/utilities/hdx_error_handler.py +0 -0
  36. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/utilities/hdx_state.py +0 -0
  37. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/api/utilities/url_utils.py +0 -0
  38. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/__init__.py +0 -0
  39. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/resource_matcher.py +0 -0
  40. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/resource_view.py +0 -0
  41. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/showcase.py +0 -0
  42. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/data/vocabulary.py +0 -0
  43. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/facades/__init__.py +0 -0
  44. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/facades/infer_arguments.py +0 -0
  45. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/facades/keyword_arguments.py +0 -0
  46. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/src/hdx/facades/simple.py +0 -0
  47. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/__init__.py +0 -0
  48. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/Accepted_Tags.csv +0 -0
  49. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/CKAN/hdx_dataset_static.yaml +0 -0
  50. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/Tag_Mapping.csv +0 -0
  51. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/Tag_Mapping_ChainRuleError.csv +0 -0
  52. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/empty.yaml +0 -0
  53. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_base_config.json +0 -0
  54. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_base_config.yaml +0 -0
  55. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_config.json +0 -0
  56. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_config.yaml +0 -0
  57. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_dataset_static.json +0 -0
  58. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_dataset_static.yaml +0 -0
  59. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_datasource_topline.json +0 -0
  60. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_datasource_topline.yaml +0 -0
  61. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_email_configuration.json +0 -0
  62. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_email_configuration.yaml +0 -0
  63. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_missing_site_config.json +0 -0
  64. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_organization_static.json +0 -0
  65. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_organization_static.yaml +0 -0
  66. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_resource_static.json +0 -0
  67. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_resource_static.yaml +0 -0
  68. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_resource_view_static.json +0 -0
  69. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_resource_view_static.yaml +0 -0
  70. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_showcase_static.json +0 -0
  71. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_showcase_static.yaml +0 -0
  72. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_user_static.json +0 -0
  73. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_user_static.yaml +0 -0
  74. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_vocabulary_static.json +0 -0
  75. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/hdx_vocabulary_static.yaml +0 -0
  76. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/project_configuration.json +0 -0
  77. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/project_configuration.yaml +0 -0
  78. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/user_agent_config.yaml +0 -0
  79. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/user_agent_config2.yaml +0 -0
  80. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/user_agent_config3.yaml +0 -0
  81. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/config/user_agent_config_wrong.yaml +0 -0
  82. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/dataset_search_results.yaml +0 -0
  83. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/download_gen_resource/conflict_data_alg.csv +0 -0
  84. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/download_gen_resource/min_qc_conflict_data_alg.csv +0 -0
  85. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/download_gen_resource/qc_conflict_data_alg.csv +0 -0
  86. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/download_gen_resource/test_data_no_data.csv +0 -0
  87. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/download_gen_resource/test_data_no_years.csv +0 -0
  88. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/empty.csv +0 -0
  89. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/gen_resource/conflict_data_alg.csv +0 -0
  90. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/gen_resource/min_qc_conflict_data_alg.csv +0 -0
  91. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/gen_resource/qc_conflict_data_alg.csv +0 -0
  92. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/gen_resource/test_data_no_data.csv +0 -0
  93. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/gen_resource/test_data_no_years.csv +0 -0
  94. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/location/location_list_all_fields.yaml +0 -0
  95. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/location/location_show_results.yaml +0 -0
  96. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/organization/organization_list_all_fields.yaml +0 -0
  97. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/organization/organization_show_results.yaml +0 -0
  98. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/qc_from_rows/qc_conflict_data_alg.csv +0 -0
  99. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/qc_from_rows/qc_conflict_data_alg_one_col.csv +0 -0
  100. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/resource_formats.json +0 -0
  101. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/showcase_all_search_results.yaml +0 -0
  102. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/size_hash/ACLED-All-Africa-File_20170101-to-20170708.xlsx +0 -0
  103. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/state/analysis_dates.txt +0 -0
  104. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/state/last_build_date.txt +0 -0
  105. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/test_data.csv +0 -0
  106. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/test_data.zip +0 -0
  107. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/dem_data_zwe.csv +0 -0
  108. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/dem_indicatorlist_zwe.csv +0 -0
  109. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/expected_resources_to_update.json +0 -0
  110. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/opri_data_zwe.csv +0 -0
  111. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/opri_indicatorlist_zwe.csv +0 -0
  112. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/opri_metadata_zwe.csv +0 -0
  113. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/qc_sdg_data_zwe.csv +0 -0
  114. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/sdg_data_zwe.csv +0 -0
  115. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/sdg_indicatorlist_zwe.csv +0 -0
  116. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/sdg_metadata_zwe.csv +0 -0
  117. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/unesco_dataset.json +0 -0
  118. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_dataset_resources/unesco_update_dataset.json +0 -0
  119. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_logic/update_logic_resources.yaml +0 -0
  120. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/fixtures/update_logic/update_logic_resources_new.yaml +0 -0
  121. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/__init__.py +0 -0
  122. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/api/__init__.py +0 -0
  123. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/api/test_ckan.py +0 -0
  124. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/api/test_configuration.py +0 -0
  125. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/api/test_locations.py +0 -0
  126. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/api/utilities/__init__.py +0 -0
  127. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/api/utilities/test_filestore_helper.py +0 -0
  128. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/api/utilities/test_hdx_error_handler.py +0 -0
  129. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/api/utilities/test_hdx_state.py +0 -0
  130. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/api/utilities/test_url_utils.py +0 -0
  131. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/conftest.py +0 -0
  132. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/__init__.py +0 -0
  133. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_dataset_add_hapi_error.py +0 -0
  134. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_dataset_core.py +0 -0
  135. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_dataset_resource_generation.py +0 -0
  136. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_location.py +0 -0
  137. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_organization.py +0 -0
  138. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_resource_view.py +0 -0
  139. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_showcase.py +0 -0
  140. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_update_dataset_resources.py +0 -0
  141. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_update_logic.py +0 -0
  142. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/data/test_vocabulary.py +0 -0
  143. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/facades/__init__.py +0 -0
  144. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/facades/test_infer_arguments.py +0 -0
  145. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/facades/test_keyword_arguments.py +0 -0
  146. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/tests/hdx/facades/test_simple.py +0 -0
  147. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/uv.lock +0 -0
  148. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/workingexample/my_code.py +0 -0
  149. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/workingexample/my_resource.csv +0 -0
  150. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/workingexample/my_resource.xlsx +0 -0
  151. {hdx_python_api-6.7.2 → hdx_python_api-6.7.3}/workingexample/run.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hdx-python-api
3
- Version: 6.7.2
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
@@ -36,6 +36,7 @@ upload your datasets to HDX.
36
36
  - [Location Management](#location-management)
37
37
  - [Vocabulary Management](#vocabulary-management)
38
38
  - [Activity Streams](#activity-streams)
39
+ - [HDX Statistics](#hdx-statistics)
39
40
  - [Pipeline State](#pipeline-state)
40
41
  - [Working Examples](#working-examples)
41
42
  - [Project Framework](#project-framework)
@@ -636,6 +637,16 @@ function, for example:
636
637
 
637
638
  dataset.remove_showcase(showcase)
638
639
 
640
+ HDX runs file structure checks on resources, recording schema changes (eg.
641
+ column names, order or types) and whether sheets have merged cells. You can get
642
+ these checks for all of a dataset's resources from HDX:
643
+
644
+ fs_check_info = dataset.get_fs_check_info()
645
+
646
+ This returns a list of dictionaries, each from a resource name to a list of
647
+ check events with keys such as `state`, `timestamp`, `sheet_changes` and
648
+ `hxl_proxy_response`.
649
+
639
650
  ### Time Period
640
651
 
641
652
  Time Period is a mandatory field in HDX. It is the earliest start date and
@@ -947,6 +958,12 @@ If the method **set_file_to_upload** is used to supply a file, the resource
947
958
  `last_modified` field is set to now automatically regardless of the value of
948
959
  `data_updated` or whether **mark_data_updated** has been called.
949
960
 
961
+ The file structure check events of a resource (see Dataset Specific
962
+ Operations) can be obtained from its `fs_check_info` field without a call to
963
+ HDX:
964
+
965
+ events = resource.get_fs_check_info()
966
+
950
967
  ### Data Dictionary
951
968
 
952
969
  A resource can have a data dictionary describing the columns in the data it
@@ -974,11 +991,36 @@ using the getter:
974
991
 
975
992
  data_dictionary = resource.get_hdx_data_dictionary()
976
993
 
994
+ To change the data dictionary of a resource already in HDX without sending
995
+ dataset update notifications, use **update_hdx_data_dictionary**. It patches
996
+ only the `hdx_data_dictionary` field (so `last_modified` is unchanged), then,
997
+ if the resource has a datastore, updates the datastore field labels and
998
+ descriptions without reloading the data. The given columns are merged by
999
+ `field` into the existing data dictionary in the resource metadata, so read
1000
+ the resource from HDX first. Pass `replace=True` to replace the data
1001
+ dictionary instead and `update_datastore=False` to leave the datastore alone:
1002
+
1003
+ resource = Resource.read_from_hdx(RESOURCE_ID)
1004
+ resource.update_hdx_data_dictionary([
1005
+ {
1006
+ "field": "col_name",
1007
+ "label": "Column Label",
1008
+ "description": "New description.",
1009
+ },
1010
+ ])
1011
+
1012
+ Columns are matched to datastore fields by `field`. A warning is logged for
1013
+ any column with no matching datastore field.
1014
+
977
1015
  ### Datastore
978
1016
 
979
- A resource's data can be loaded into the HDX datastore and queried. HDX
980
- requires an API key for all datastore actions and dumps, including reads of
981
- public resources. To create, update or delete the datastore for a resource:
1017
+ A resource's data can be loaded into the HDX datastore and queried. HDX calls this
1018
+ [Tabular Data Endpoints (TDE)](https://docs.humdata.org/build/hdx-apis/tabular-data-endpoints),
1019
+ and it is available only for resources with TDE enabled (their `datastore_active`
1020
+ field is true). Contact hdx@un.org to request TDE for your datasets. HDX requires an
1021
+ API key for all datastore actions and dumps, including reads of public resources,
1022
+ and limits each IP address to 60 requests per minute. To create, update or delete
1023
+ the datastore for a resource:
982
1024
 
983
1025
  resource.create_datastore(
984
1026
  [{"id": "code", "type": "text"}, {"id": "value", "type": "numeric"}],
@@ -988,6 +1030,17 @@ public resources. To create, update or delete the datastore for a resource:
988
1030
  has_datastore = resource.has_datastore()
989
1031
  resource.delete_datastore()
990
1032
 
1033
+ HDX loads uploaded files into the datastore using DataPusher+. To reload a
1034
+ resource's file into the datastore without uploading it again (so
1035
+ `last_modified` is unchanged), use **submit_to_datapusher**. It returns False
1036
+ if the job was not submitted, eg. because one is already pending. HDX's
1037
+ datastore allowlist and format checks, which apply to automatic submissions
1038
+ on upload, are not applied. **get_datapusher_status** returns the status of
1039
+ the latest job or None if there is none:
1040
+
1041
+ submitted = resource.submit_to_datapusher(ignore_hash=True)
1042
+ status = resource.get_datapusher_status() # eg. status["status"] == "complete"
1043
+
991
1044
  To download the whole datastore (rather than the resource's file) use
992
1045
  **download_datastore**, which returns the URL and path like **download**.
993
1046
  `file_format` can be `csv` (default), `tsv`, `json` or `xml`, and `filename`
@@ -1008,7 +1061,8 @@ and `include_total`, and passes any other keyword arguments to CKAN:
1008
1061
  records = result["records"]
1009
1062
 
1010
1063
  **get_datastore_records** takes the same query parameters but pages through
1011
- all matching rows, yielding each record (`page_size` defaults to 10000):
1064
+ all matching rows, yielding each record (`page_size` defaults to 10000).
1065
+ `records_format` csv and tsv are not supported; use `search_datastore` for those:
1012
1066
 
1013
1067
  for record in resource.get_datastore_records(filters={"code": "A"}):
1014
1068
  ...
@@ -1020,7 +1074,9 @@ internal `_id` field:
1020
1074
 
1021
1075
  **search_datastore_sql** wraps CKAN's `datastore_search_sql`, running a
1022
1076
  SELECT statement where tables are named by double quoted resource id and
1023
- returning the list of records:
1077
+ returning the list of records. HDX returns at most 32,000 rows per query and
1078
+ cuts off larger results, in which case a warning is logged (see
1079
+ [paging in the TDE docs](https://docs.humdata.org/build/hdx-apis/tabular-data-endpoints/troubleshooting-and-error-handling#pagination)):
1024
1080
 
1025
1081
  records = Resource.search_datastore_sql(
1026
1082
  'SELECT code, SUM(value) AS total FROM "RESOURCE_ID" GROUP BY code'
@@ -1067,6 +1123,11 @@ configuration):
1067
1123
 
1068
1124
  user = User.get_current_user()
1069
1125
 
1126
+ You can get the name of the API token in the configuration and the md5 hash
1127
+ of its owner's email:
1128
+
1129
+ token_info = User.get_current_token_info() # {"email_hash": ..., "token_name": ...}
1130
+
1070
1131
  You can check that the current user has a particular permission to a specific
1071
1132
  organization:
1072
1133
 
@@ -1223,6 +1284,20 @@ permission to edit the object the activity relates to for these:
1223
1284
  data = activity.get_data()
1224
1285
  diff = activity.get_diff(diff_type="unified") # or "context" or "html"
1225
1286
 
1287
+ ## HDX Statistics
1288
+
1289
+ General statistics about datasets, organizations and users on HDX can be
1290
+ obtained as follows:
1291
+
1292
+ from hdx.api.utilities.hdx_stats import get_general_statistics, push_general_stats
1293
+
1294
+ stats = get_general_statistics()
1295
+
1296
+ Users with permission to manage basic scheduled tasks can compute the statistics
1297
+ and push them to analytics:
1298
+
1299
+ pushed_stats = push_general_stats()
1300
+
1226
1301
  ## Pipeline State
1227
1302
 
1228
1303
  The HDXState class allows the reading and writing of state to a given dataset. Input
@@ -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.2'
22
- __version_tuple__ = version_tuple = (6, 7, 2)
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)
@@ -129,7 +129,9 @@ class Activity(HDXObject):
129
129
  result = activity._write_to_hdx(action, data)
130
130
  return [cls(x, configuration=configuration) for x in result]
131
131
  data = dict(data)
132
- limit = data.setdefault("limit", 100)
132
+ # HDX silently caps limit at ckan.activity_list_limit_max
133
+ limit = min(data.get("limit", 100), 100)
134
+ data["limit"] = limit
133
135
  offset = data.get("offset", 0)
134
136
  activities = []
135
137
  while True:
@@ -147,6 +147,7 @@ class Dataset(HDXObject):
147
147
  "list": "package_list",
148
148
  "autocomplete": "package_autocomplete",
149
149
  "create_default_views": "package_create_default_resource_views",
150
+ "fs_check_info": "fs_check_info_show",
150
151
  }
151
152
 
152
153
  def __setitem__(self, key: Any, value: Any) -> None:
@@ -1447,13 +1448,7 @@ class Dataset(HDXObject):
1447
1448
  Returns:
1448
1449
  List of Activity objects
1449
1450
  """
1450
- return Activity.get_activities(
1451
- "package",
1452
- self.data.get("id") or self.data.get("name"),
1453
- self.configuration,
1454
- all_pages,
1455
- **kwargs,
1456
- )
1451
+ return self._get_activities("package", all_pages, **kwargs)
1457
1452
 
1458
1453
  @classmethod
1459
1454
  def autocomplete(
@@ -2333,6 +2328,25 @@ class Dataset(HDXObject):
2333
2328
  return None
2334
2329
  return f"{self.configuration.get_hdx_site_url()}/api/3/action/package_show?id={name_or_id}"
2335
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
+
2336
2350
  def generate_resource(
2337
2351
  self,
2338
2352
  folder: Path | str,
@@ -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,
@@ -232,13 +232,7 @@ class Location(HDXObject):
232
232
  Returns:
233
233
  List of Activity objects
234
234
  """
235
- return Activity.get_activities(
236
- "group",
237
- self.data.get("id") or self.data.get("name"),
238
- self.configuration,
239
- all_pages,
240
- **kwargs,
241
- )
235
+ return self._get_activities("group", all_pages, **kwargs)
242
236
 
243
237
  @classmethod
244
238
  def autocomplete(
@@ -326,13 +326,7 @@ class Organization(HDXObject):
326
326
  Returns:
327
327
  List of Activity objects
328
328
  """
329
- return Activity.get_activities(
330
- "organization",
331
- self.data.get("id") or self.data.get("name"),
332
- self.configuration,
333
- all_pages,
334
- **kwargs,
335
- )
329
+ return self._get_activities("organization", all_pages, **kwargs)
336
330
 
337
331
  @classmethod
338
332
  def autocomplete(
@@ -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
 
@@ -45,6 +45,7 @@ class User(HDXObject):
45
45
  "list": "user_list",
46
46
  "listorgs": "organization_list_for_user",
47
47
  "token_list": "api_token_list",
48
+ "token_info": "hdx_token_info",
48
49
  "autocomplete": "user_autocomplete",
49
50
  }
50
51
 
@@ -334,7 +335,7 @@ class User(HDXObject):
334
335
  Returns:
335
336
  List of organization dicts in HDX that logged in user is a member of
336
337
  """
337
- user = User(configuration=configuration)
338
+ user = cls(configuration=configuration)
338
339
  try:
339
340
  return user.configuration.call_remoteckan(
340
341
  cls.actions()["listorgs"], {"permission": permission}
@@ -470,13 +471,25 @@ class User(HDXObject):
470
471
  Returns:
471
472
  List of Activity objects
472
473
  """
473
- return Activity.get_activities(
474
- "user",
475
- self.data.get("id") or self.data.get("name"),
476
- self.configuration,
477
- all_pages,
478
- **kwargs,
479
- )
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
480
493
 
481
494
  @classmethod
482
495
  def autocomplete(