hdx-python-api 6.7.0__tar.gz → 6.7.1__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 (147) hide show
  1. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/PKG-INFO +3 -3
  2. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/documentation/index.md +65 -5
  3. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/pyproject.toml +2 -2
  4. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/_version.py +2 -2
  5. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/resource.py +180 -1
  6. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_resource.py +164 -0
  7. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/uv.lock +220 -8
  8. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/.github/workflows/publish.yaml +0 -0
  9. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/.github/workflows/run-python-tests.yaml +0 -0
  10. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/.gitignore +0 -0
  11. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/.pre-commit-config.yaml +0 -0
  12. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/CLAUDE.md +0 -0
  13. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/LICENSE +0 -0
  14. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/README.md +0 -0
  15. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/documentation/.readthedocs.yaml +0 -0
  16. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/documentation/mkdocs.yaml +0 -0
  17. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/__init__.py +0 -0
  18. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/configuration.py +0 -0
  19. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/hdx_base_configuration.yaml +0 -0
  20. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/locations.py +0 -0
  21. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/remotehdx.py +0 -0
  22. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/utilities/__init__.py +0 -0
  23. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/utilities/date_helper.py +0 -0
  24. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/utilities/filestore_helper.py +0 -0
  25. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/utilities/hdx_error_handler.py +0 -0
  26. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/utilities/hdx_state.py +0 -0
  27. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/api/utilities/url_utils.py +0 -0
  28. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/__init__.py +0 -0
  29. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/dataset.py +0 -0
  30. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/hdxobject.py +0 -0
  31. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/location.py +0 -0
  32. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/organization.py +0 -0
  33. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/resource_matcher.py +0 -0
  34. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/resource_view.py +0 -0
  35. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/showcase.py +0 -0
  36. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/user.py +0 -0
  37. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/data/vocabulary.py +0 -0
  38. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/facades/__init__.py +0 -0
  39. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/facades/infer_arguments.py +0 -0
  40. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/facades/keyword_arguments.py +0 -0
  41. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/src/hdx/facades/simple.py +0 -0
  42. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/__init__.py +0 -0
  43. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/Accepted_Tags.csv +0 -0
  44. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/CKAN/hdx_dataset_static.yaml +0 -0
  45. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/Tag_Mapping.csv +0 -0
  46. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/Tag_Mapping_ChainRuleError.csv +0 -0
  47. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/empty.yaml +0 -0
  48. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_base_config.json +0 -0
  49. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_base_config.yaml +0 -0
  50. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_config.json +0 -0
  51. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_config.yaml +0 -0
  52. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_dataset_static.json +0 -0
  53. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_dataset_static.yaml +0 -0
  54. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_datasource_topline.json +0 -0
  55. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_datasource_topline.yaml +0 -0
  56. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_email_configuration.json +0 -0
  57. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_email_configuration.yaml +0 -0
  58. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_missing_site_config.json +0 -0
  59. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_organization_static.json +0 -0
  60. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_organization_static.yaml +0 -0
  61. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_resource_static.json +0 -0
  62. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_resource_static.yaml +0 -0
  63. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_resource_view_static.json +0 -0
  64. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_resource_view_static.yaml +0 -0
  65. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_showcase_static.json +0 -0
  66. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_showcase_static.yaml +0 -0
  67. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_user_static.json +0 -0
  68. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_user_static.yaml +0 -0
  69. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_vocabulary_static.json +0 -0
  70. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_vocabulary_static.yaml +0 -0
  71. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/project_configuration.json +0 -0
  72. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/project_configuration.yaml +0 -0
  73. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/user_agent_config.yaml +0 -0
  74. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/user_agent_config2.yaml +0 -0
  75. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/user_agent_config3.yaml +0 -0
  76. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/config/user_agent_config_wrong.yaml +0 -0
  77. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/dataset_search_results.yaml +0 -0
  78. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/download_gen_resource/conflict_data_alg.csv +0 -0
  79. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/download_gen_resource/min_qc_conflict_data_alg.csv +0 -0
  80. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/download_gen_resource/qc_conflict_data_alg.csv +0 -0
  81. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/download_gen_resource/test_data_no_data.csv +0 -0
  82. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/download_gen_resource/test_data_no_years.csv +0 -0
  83. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/empty.csv +0 -0
  84. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/gen_resource/conflict_data_alg.csv +0 -0
  85. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/gen_resource/min_qc_conflict_data_alg.csv +0 -0
  86. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/gen_resource/qc_conflict_data_alg.csv +0 -0
  87. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/gen_resource/test_data_no_data.csv +0 -0
  88. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/gen_resource/test_data_no_years.csv +0 -0
  89. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/location/location_list_all_fields.yaml +0 -0
  90. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/location/location_show_results.yaml +0 -0
  91. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/organization/organization_list_all_fields.yaml +0 -0
  92. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/organization/organization_show_results.yaml +0 -0
  93. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/qc_from_rows/qc_conflict_data_alg.csv +0 -0
  94. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/qc_from_rows/qc_conflict_data_alg_one_col.csv +0 -0
  95. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/resource_formats.json +0 -0
  96. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/showcase_all_search_results.yaml +0 -0
  97. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/size_hash/ACLED-All-Africa-File_20170101-to-20170708.xlsx +0 -0
  98. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/state/analysis_dates.txt +0 -0
  99. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/state/last_build_date.txt +0 -0
  100. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/test_data.csv +0 -0
  101. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/test_data.zip +0 -0
  102. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/dem_data_zwe.csv +0 -0
  103. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/dem_indicatorlist_zwe.csv +0 -0
  104. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/expected_resources_to_update.json +0 -0
  105. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/opri_data_zwe.csv +0 -0
  106. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/opri_indicatorlist_zwe.csv +0 -0
  107. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/opri_metadata_zwe.csv +0 -0
  108. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/qc_sdg_data_zwe.csv +0 -0
  109. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/sdg_data_zwe.csv +0 -0
  110. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/sdg_indicatorlist_zwe.csv +0 -0
  111. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/sdg_metadata_zwe.csv +0 -0
  112. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/unesco_dataset.json +0 -0
  113. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/unesco_update_dataset.json +0 -0
  114. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_logic/update_logic_resources.yaml +0 -0
  115. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/fixtures/update_logic/update_logic_resources_new.yaml +0 -0
  116. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/__init__.py +0 -0
  117. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/api/__init__.py +0 -0
  118. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/api/test_ckan.py +0 -0
  119. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/api/test_configuration.py +0 -0
  120. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/api/test_locations.py +0 -0
  121. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/api/utilities/__init__.py +0 -0
  122. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/api/utilities/test_filestore_helper.py +0 -0
  123. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/api/utilities/test_hdx_error_handler.py +0 -0
  124. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/api/utilities/test_hdx_state.py +0 -0
  125. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/api/utilities/test_url_utils.py +0 -0
  126. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/conftest.py +0 -0
  127. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/__init__.py +0 -0
  128. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_dataset_add_hapi_error.py +0 -0
  129. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_dataset_core.py +0 -0
  130. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_dataset_noncore.py +0 -0
  131. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_dataset_resource_generation.py +0 -0
  132. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_location.py +0 -0
  133. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_organization.py +0 -0
  134. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_resource_view.py +0 -0
  135. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_showcase.py +0 -0
  136. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_update_dataset_resources.py +0 -0
  137. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_update_logic.py +0 -0
  138. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_user.py +0 -0
  139. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/data/test_vocabulary.py +0 -0
  140. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/facades/__init__.py +0 -0
  141. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/facades/test_infer_arguments.py +0 -0
  142. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/facades/test_keyword_arguments.py +0 -0
  143. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/tests/hdx/facades/test_simple.py +0 -0
  144. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/workingexample/my_code.py +0 -0
  145. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/workingexample/my_resource.csv +0 -0
  146. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/workingexample/my_resource.xlsx +0 -0
  147. {hdx_python_api-6.7.0 → hdx_python_api-6.7.1}/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.0
3
+ Version: 6.7.1
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.3
32
- Requires-Dist: hdx-python-utilities>=4.1.2
31
+ Requires-Dist: hdx-python-country>=4.2.3
32
+ Requires-Dist: hdx-python-utilities>=4.2.1
33
33
  Requires-Dist: makefun
34
34
  Requires-Dist: requests
35
35
  Provides-Extra: docs
@@ -832,9 +832,13 @@ dictionary. HEADERS is either a row number (rows start counting at 1), or the
832
832
  actual headers defined as a list of strings. If not set, all rows will be
833
833
  treated as containing values:
834
834
 
835
- dataset.generate_resource("FOLDER", "FILENAME", ROWS, RESOURCE DATA, HEADERS,
836
- COLUMNS, "FORMAT", "ENCODING", DATECOL or YEARCOL or
837
- DATE_FUNCTION)
835
+ dataset.generate_resource("FOLDER", "FILENAME", ROWS, RESOURCE_DATA,
836
+ headers=HEADERS, columns=COLUMNS, format="FORMAT",
837
+ encoding="ENCODING", datecol=DATECOL)
838
+
839
+ To use years or a custom date function instead, replace `datecol=DATECOL` with
840
+ `yearcol=YEARCOL` or `date_function=DATE_FUNCTION`. Supply only one of these
841
+ three arguments, using its keyword.
838
842
 
839
843
  The first 4 parameters are mandatory, the rest are optional. A resource can be generated
840
844
  from a given list or tuple or other iterable. The method returns a tuple with a bool
@@ -868,8 +872,12 @@ and outputs a modified row.
868
872
  The rest of the arguments are the same as for `generate_resource`.
869
873
 
870
874
  dataset.download_generate_resource(DOWNLOADER, "URL", "FOLDER", "FILENAME",
871
- RESOURCE_DATA, HEADER_INSERTIONS, ROW_FUNCTION,
872
- DATECOL or YEARCOL or DATE_FUNCTION, **KWARGS)
875
+ RESOURCE_DATA, header_insertions=HEADER_INSERTIONS,
876
+ row_function=ROW_FUNCTION, datecol=DATECOL,
877
+ **KWARGS)
878
+
879
+ As with `generate_resource`, use `yearcol=YEARCOL` or
880
+ `date_function=DATE_FUNCTION` instead of `datecol=DATECOL` when appropriate.
873
881
 
874
882
  ## Resource Specific Operations
875
883
 
@@ -965,6 +973,58 @@ using the getter:
965
973
 
966
974
  data_dictionary = resource.get_hdx_data_dictionary()
967
975
 
976
+ ### Datastore
977
+
978
+ A resource's data can be loaded into the HDX datastore and queried. HDX
979
+ requires an API key for all datastore actions and dumps, including reads of
980
+ public resources. To create, update or delete the datastore for a resource:
981
+
982
+ resource.create_datastore(
983
+ [{"id": "code", "type": "text"}, {"id": "value", "type": "numeric"}],
984
+ primary_key="code",
985
+ )
986
+ resource.update_datastore([{"code": "A", "value": 1.0}]) # upsert by default
987
+ has_datastore = resource.has_datastore()
988
+ resource.delete_datastore()
989
+
990
+ To download the whole datastore (rather than the resource's file) use
991
+ **download_datastore**, which returns the URL and path like **download**.
992
+ `file_format` can be `csv` (default), `tsv`, `json` or `xml`, and `filename`
993
+ defaults to the resource name with that extension. **get_datastore_dump_url**
994
+ returns the URL without downloading:
995
+
996
+ url, path = resource.download_datastore("FOLDER_TO_DOWNLOAD_TO")
997
+ url = resource.get_datastore_dump_url(file_format="json")
998
+
999
+ **search_datastore** wraps CKAN's `datastore_search`, returning the result
1000
+ dictionary (with keys including `records`, `fields` and `total`). It accepts
1001
+ `filters`, `q`, `fields`, `sort`, `distinct`, `limit` (default 100), `offset`
1002
+ and `include_total`, and passes any other keyword arguments to CKAN:
1003
+
1004
+ result = resource.search_datastore(
1005
+ filters={"code": ["A", "B"]}, fields=["code", "value"], sort="value desc"
1006
+ )
1007
+ records = result["records"]
1008
+
1009
+ **get_datastore_records** takes the same query parameters but pages through
1010
+ all matching rows, yielding each record (`page_size` defaults to 10000):
1011
+
1012
+ for record in resource.get_datastore_records(filters={"code": "A"}):
1013
+ ...
1014
+
1015
+ **get_datastore_fields** returns the datastore field definitions excluding the
1016
+ internal `_id` field:
1017
+
1018
+ fields = resource.get_datastore_fields()
1019
+
1020
+ **search_datastore_sql** wraps CKAN's `datastore_search_sql`, running a
1021
+ SELECT statement where tables are named by double quoted resource id and
1022
+ returning the list of records:
1023
+
1024
+ records = Resource.search_datastore_sql(
1025
+ 'SELECT code, SUM(value) AS total FROM "RESOURCE_ID" GROUP BY code'
1026
+ )
1027
+
968
1028
  ## Showcase Management
969
1029
 
970
1030
  The **Showcase** class enables you to manage showcases, creating, deleting and updating
@@ -39,8 +39,8 @@ dependencies = [
39
39
  "ckanapi>=4.11",
40
40
  "defopt>=7.0.0",
41
41
  "email_validator",
42
- "hdx-python-country>=4.1.3",
43
- "hdx-python-utilities>=4.1.2",
42
+ "hdx-python-country>=4.2.3",
43
+ "hdx-python-utilities>=4.2.1",
44
44
  "makefun",
45
45
  "requests",
46
46
  ]
@@ -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.0'
22
- __version_tuple__ = version_tuple = (6, 7, 0)
21
+ __version__ = version = '6.7.1'
22
+ __version_tuple__ = version_tuple = (6, 7, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -2,7 +2,7 @@
2
2
 
3
3
  import json
4
4
  import logging
5
- from collections.abc import Sequence
5
+ from collections.abc import Iterator, Sequence
6
6
  from datetime import datetime
7
7
  from pathlib import Path
8
8
  from typing import Any, Optional
@@ -34,6 +34,7 @@ class Resource(HDXObject):
34
34
  """
35
35
 
36
36
  _formats_dict = None
37
+ _valid_datastore_dump_formats = frozenset({"csv", "tsv", "json", "xml"})
37
38
  _valid_data_dictionary_types = frozenset(
38
39
  {"text", "numeric", "date", "timestamp without time zone"}
39
40
  )
@@ -70,6 +71,7 @@ class Resource(HDXObject):
70
71
  "datastore_insert": "datastore_insert",
71
72
  "datastore_upsert": "datastore_upsert",
72
73
  "datastore_search": "datastore_search",
74
+ "datastore_search_sql": "datastore_search_sql",
73
75
  }
74
76
 
75
77
  def update_from_yaml(
@@ -711,6 +713,62 @@ class Resource(HDXObject):
711
713
  path = downloader.download_file(url, folder=folder, filename=filename)
712
714
  return url, path
713
715
 
716
+ def get_datastore_dump_url(self, file_format: str = "csv") -> str:
717
+ """Get the URL from which the resource datastore can be downloaded in
718
+ full.
719
+
720
+ Args:
721
+ file_format: Dump format (csv, tsv, json or xml). Defaults to csv.
722
+
723
+ Returns:
724
+ Datastore dump URL
725
+ """
726
+ file_format = file_format.lower()
727
+ if file_format not in self._valid_datastore_dump_formats:
728
+ valid_formats = ", ".join(sorted(self._valid_datastore_dump_formats))
729
+ raise HDXError(
730
+ f"Invalid datastore dump format '{file_format}'! Must be one of: {valid_formats}"
731
+ )
732
+ url = (
733
+ f"{self.configuration.get_hdx_site_url()}/datastore/dump/{self.data['id']}"
734
+ )
735
+ if file_format != "csv":
736
+ url = f"{url}?format={file_format}"
737
+ return url
738
+
739
+ def download_datastore(
740
+ self,
741
+ folder: Path | str | None = None,
742
+ filename: str | None = None,
743
+ file_format: str = "csv",
744
+ retriever: Retrieve | None = None,
745
+ ) -> tuple[str, Path]:
746
+ """Download the resource datastore in full to provided folder or
747
+ temporary folder if no folder supplied
748
+
749
+ Args:
750
+ folder: Folder to download datastore to. Defaults to None.
751
+ filename: Filename to use. Defaults to None (resource name with file_format extension).
752
+ file_format: Dump format (csv, tsv, json or xml). Defaults to csv.
753
+ retriever: Retrieve object to use. Defaults to None.
754
+
755
+ Returns:
756
+ (URL downloaded, Path to downloaded file)
757
+ """
758
+ url = self.get_datastore_dump_url(file_format)
759
+ logger.debug(f"Downloading {url}")
760
+ if filename is None:
761
+ filename = self.data["name"]
762
+ extension = f".{file_format.lower()}"
763
+ if not filename.endswith(extension):
764
+ 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
771
+
714
772
  @staticmethod
715
773
  def get_all_resource_ids_in_datastore(
716
774
  configuration: Configuration | None = None,
@@ -805,6 +863,127 @@ class Resource(HDXObject):
805
863
  }
806
864
  self._write_to_hdx("datastore_upsert", data, "resource_id")
807
865
 
866
+ def search_datastore(
867
+ self,
868
+ filters: dict | None = None,
869
+ q: str | dict | None = None,
870
+ fields: Sequence[str] | None = None,
871
+ sort: str | None = None,
872
+ distinct: bool = False,
873
+ limit: int = 100,
874
+ offset: int = 0,
875
+ include_total: bool = True,
876
+ **kwargs: Any,
877
+ ) -> dict:
878
+ """Search the resource datastore using CKAN datastore_search.
879
+
880
+ Args:
881
+ filters: Dictionary of field name to value(s) to match. Defaults to None.
882
+ q: Full text query string or dictionary of field name to query. Defaults to None.
883
+ fields: Fields to return. Defaults to None (all fields).
884
+ sort: Comma separated field names with optional asc/desc eg. "code desc". Defaults to None.
885
+ distinct: Whether to return only distinct rows. Defaults to False.
886
+ limit: Maximum number of rows to return. Defaults to 100.
887
+ offset: Number of rows to skip. Defaults to 0.
888
+ include_total: Whether to include total number of matching rows. Defaults to True.
889
+ **kwargs: Other parameters to pass to datastore_search eg. plain, language.
890
+
891
+ Returns:
892
+ datastore_search result dictionary with keys including records, fields and total
893
+ """
894
+ data: dict = {
895
+ "resource_id": self.data["id"],
896
+ "limit": limit,
897
+ "offset": offset,
898
+ "include_total": include_total,
899
+ }
900
+ if filters is not None:
901
+ data["filters"] = filters
902
+ if q is not None:
903
+ data["q"] = q
904
+ if fields is not None:
905
+ data["fields"] = ",".join(fields)
906
+ if sort is not None:
907
+ data["sort"] = sort
908
+ if distinct:
909
+ data["distinct"] = True
910
+ data.update(kwargs)
911
+ return self._write_to_hdx("datastore_search", data, "resource_id")
912
+
913
+ def get_datastore_records(
914
+ self,
915
+ filters: dict | None = None,
916
+ q: str | dict | None = None,
917
+ fields: Sequence[str] | None = None,
918
+ sort: str | None = None,
919
+ distinct: bool = False,
920
+ page_size: int = 10000,
921
+ **kwargs: Any,
922
+ ) -> Iterator[dict]:
923
+ """Iterate over all matching records in the resource datastore, paging
924
+ through datastore_search results.
925
+
926
+ Args:
927
+ filters: Dictionary of field name to value(s) to match. Defaults to None.
928
+ q: Full text query string or dictionary of field name to query. Defaults to None.
929
+ fields: Fields to return. Defaults to None (all fields).
930
+ sort: Comma separated field names with optional asc/desc eg. "code desc". Defaults to None.
931
+ distinct: Whether to return only distinct rows. Defaults to False.
932
+ page_size: Number of rows to request per call. Defaults to 10000.
933
+ **kwargs: Other parameters to pass to datastore_search eg. plain, language.
934
+
935
+ Returns:
936
+ Iterator of record dictionaries
937
+ """
938
+ offset = 0
939
+ while True:
940
+ result = self.search_datastore(
941
+ filters=filters,
942
+ q=q,
943
+ fields=fields,
944
+ sort=sort,
945
+ distinct=distinct,
946
+ limit=page_size,
947
+ offset=offset,
948
+ include_total=False,
949
+ **kwargs,
950
+ )
951
+ records = result["records"]
952
+ yield from records
953
+ if len(records) < page_size:
954
+ return
955
+ offset += page_size
956
+
957
+ def get_datastore_fields(self) -> list[dict]:
958
+ """Get the field definitions of the resource datastore excluding the
959
+ internal _id field.
960
+
961
+ Returns:
962
+ List of field dictionaries each with id and type keys
963
+ """
964
+ result = self.search_datastore(limit=0, include_total=False)
965
+ return [field for field in result["fields"] if field["id"] != "_id"]
966
+
967
+ @staticmethod
968
+ def search_datastore_sql(
969
+ sql: str,
970
+ configuration: Configuration | None = None,
971
+ ) -> list[dict]:
972
+ """Run a read-only SQL query against the HDX datastore using CKAN
973
+ datastore_search_sql. Tables are named by resource id, which must be
974
+ double quoted eg. SELECT * FROM "RESOURCE_ID".
975
+
976
+ Args:
977
+ sql: SQL SELECT statement
978
+ configuration: HDX configuration. Defaults to global configuration.
979
+
980
+ Returns:
981
+ List of record dictionaries
982
+ """
983
+ resource = Resource(configuration=configuration)
984
+ result = resource._write_to_hdx("datastore_search_sql", {"sql": sql})
985
+ return result["records"]
986
+
808
987
  def delete_datastore(self) -> None:
809
988
  """Delete a resource from the HDX datastore
810
989
 
@@ -6,6 +6,7 @@ import os
6
6
  from datetime import datetime, timezone
7
7
  from os import remove
8
8
  from os.path import basename
9
+ from pathlib import Path
9
10
 
10
11
  import pytest
11
12
  from hdx.utilities.dateparse import parse_date
@@ -338,6 +339,7 @@ def mockresourceview(url, decodedata):
338
339
 
339
340
  class TestResource:
340
341
  datastore = None
342
+ datastore_request = None
341
343
 
342
344
  @pytest.fixture(scope="class")
343
345
  def static_yaml(self, configfolder):
@@ -600,6 +602,73 @@ class TestResource:
600
602
 
601
603
  Configuration.read().remoteckan().session = MockSession()
602
604
 
605
+ @pytest.fixture(scope="function")
606
+ def post_datastore_query(self):
607
+ all_records = [
608
+ {"_id": 1, "code": "A", "value": 1.0},
609
+ {"_id": 2, "code": "B", "value": 2.0},
610
+ {"_id": 3, "code": "C", "value": 3.0},
611
+ ]
612
+ fields = [
613
+ {"id": "_id", "type": "int"},
614
+ {"id": "code", "type": "text"},
615
+ {"id": "value", "type": "numeric"},
616
+ ]
617
+
618
+ class MockSession:
619
+ @staticmethod
620
+ def post(url, data, **kwargs):
621
+ datadict = json.loads(data.decode("utf-8"))
622
+ if url.endswith("datastore_search_sql"):
623
+ if "error" in datadict["sql"]:
624
+ return MockResponse(
625
+ 409,
626
+ '{"success": false, "error": {"message": "Bad SQL", "__type": "Validation Error"}, "help": "http://test-data.humdata.org/api/3/action/help_show?name=datastore_search_sql"}',
627
+ )
628
+ result = {"records": all_records[:2], "fields": fields}
629
+ return MockResponse(
630
+ 200,
631
+ json.dumps(
632
+ {
633
+ "success": True,
634
+ "result": result,
635
+ "help": "http://test-data.humdata.org/api/3/action/help_show?name=datastore_search_sql",
636
+ }
637
+ ),
638
+ )
639
+ if url.endswith("datastore_search"):
640
+ if datadict["resource_id"] != "datastore_query_resource":
641
+ return MockResponse(
642
+ 404,
643
+ '{"success": false, "error": {"message": "Not found", "__type": "Not Found Error"}, "help": "http://test-data.humdata.org/api/3/action/help_show?name=datastore_search"}',
644
+ )
645
+ TestResource.datastore_request = datadict
646
+ offset = datadict["offset"]
647
+ limit = datadict["limit"]
648
+ result = {
649
+ "resource_id": datadict["resource_id"],
650
+ "fields": fields,
651
+ "records": all_records[offset : offset + limit],
652
+ }
653
+ if datadict["include_total"]:
654
+ result["total"] = len(all_records)
655
+ return MockResponse(
656
+ 200,
657
+ json.dumps(
658
+ {
659
+ "success": True,
660
+ "result": result,
661
+ "help": "http://test-data.humdata.org/api/3/action/help_show?name=datastore_search",
662
+ }
663
+ ),
664
+ )
665
+ return MockResponse(
666
+ 404,
667
+ '{"success": false, "error": {"message": "TEST ERROR: Not datastore search", "__type": "TEST ERROR: Not Datastore Search Error"}, "help": "http://test-data.humdata.org/api/3/action/help_show?name=datastore_search"}',
668
+ )
669
+
670
+ Configuration.read().remoteckan().session = MockSession()
671
+
603
672
  @pytest.fixture(scope="function")
604
673
  def post_patch(self):
605
674
  class MockSession:
@@ -1031,6 +1100,39 @@ class TestResource:
1031
1100
  with pytest.raises(DownloadError):
1032
1101
  resource2.download()
1033
1102
 
1103
+ def test_download_datastore(self, configuration, read, monkeypatch):
1104
+ downloads = []
1105
+
1106
+ def download_file(self, url, folder=None, filename=None, path=None, **kwargs):
1107
+ if path is None:
1108
+ path = Path(folder or "tmp") / filename
1109
+ downloads.append(url)
1110
+ return path
1111
+
1112
+ monkeypatch.setattr(Download, "download_file", download_file)
1113
+ resource = Resource.read_from_hdx("74b74ae1-df0c-4716-829f-4f939a046811")
1114
+ site_url = resource.configuration.get_hdx_site_url()
1115
+ dump_url = f"{site_url}/datastore/dump/{resource['id']}"
1116
+ assert resource.get_datastore_dump_url() == dump_url
1117
+ assert resource.get_datastore_dump_url("JSON") == f"{dump_url}?format=json"
1118
+ with pytest.raises(HDXError):
1119
+ resource.get_datastore_dump_url("xlsx")
1120
+
1121
+ url, path = resource.download_datastore("folder")
1122
+ assert url == dump_url
1123
+ assert path == Path("folder", "MyResource1.csv")
1124
+ url, path = resource.download_datastore(
1125
+ filename="builds.tsv", file_format="tsv"
1126
+ )
1127
+ assert url == f"{dump_url}?format=tsv"
1128
+ assert path == Path("tmp", "builds.tsv")
1129
+ tempdir = get_temp_dir()
1130
+ retriever = Retrieve(Download(user_agent="test"), tempdir, tempdir, tempdir)
1131
+ url, path = resource.download_datastore(retriever=retriever)
1132
+ assert url == dump_url
1133
+ assert path.name == "MyResource1.csv"
1134
+ assert len(downloads) == 3
1135
+
1034
1136
  def test_datastore(self, configuration, post_datastore, topline_yaml, topline_json):
1035
1137
  resource_ids = Resource.get_all_resource_ids_in_datastore()
1036
1138
  assert resource_ids == [
@@ -1073,6 +1175,68 @@ class TestResource:
1073
1175
  resource.update_datastore(records, method="insert")
1074
1176
  assert TestResource.datastore == "upsert"
1075
1177
 
1178
+ def test_datastore_queries(self, configuration, post_datastore_query):
1179
+ resource = Resource({"id": "datastore_query_resource"})
1180
+ result = resource.search_datastore()
1181
+ assert result["total"] == 3
1182
+ assert len(result["records"]) == 3
1183
+ assert TestResource.datastore_request == {
1184
+ "resource_id": "datastore_query_resource",
1185
+ "limit": 100,
1186
+ "offset": 0,
1187
+ "include_total": True,
1188
+ }
1189
+
1190
+ resource.search_datastore(
1191
+ filters={"code": ["A", "B"]},
1192
+ q="A",
1193
+ fields=["code", "value"],
1194
+ sort="value desc",
1195
+ distinct=True,
1196
+ limit=2,
1197
+ offset=1,
1198
+ plain=False,
1199
+ )
1200
+ assert TestResource.datastore_request == {
1201
+ "resource_id": "datastore_query_resource",
1202
+ "limit": 2,
1203
+ "offset": 1,
1204
+ "include_total": True,
1205
+ "filters": {"code": ["A", "B"]},
1206
+ "q": "A",
1207
+ "fields": "code,value",
1208
+ "sort": "value desc",
1209
+ "distinct": True,
1210
+ "plain": False,
1211
+ }
1212
+
1213
+ records = list(resource.get_datastore_records(sort="code", page_size=2))
1214
+ assert [record["code"] for record in records] == ["A", "B", "C"]
1215
+ assert TestResource.datastore_request["offset"] == 2
1216
+ assert TestResource.datastore_request["sort"] == "code"
1217
+ assert TestResource.datastore_request["include_total"] is False
1218
+
1219
+ records = list(resource.get_datastore_records(page_size=3))
1220
+ assert len(records) == 3
1221
+ assert TestResource.datastore_request["offset"] == 3
1222
+
1223
+ assert resource.get_datastore_fields() == [
1224
+ {"id": "code", "type": "text"},
1225
+ {"id": "value", "type": "numeric"},
1226
+ ]
1227
+ assert TestResource.datastore_request["limit"] == 0
1228
+
1229
+ records = Resource.search_datastore_sql(
1230
+ 'SELECT * FROM "datastore_query_resource" LIMIT 2'
1231
+ )
1232
+ assert [record["code"] for record in records] == ["A", "B"]
1233
+ with pytest.raises(HDXError):
1234
+ Resource.search_datastore_sql("error")
1235
+
1236
+ resource = Resource({"id": "datastore_unknown_resource"})
1237
+ with pytest.raises(HDXError):
1238
+ resource.search_datastore()
1239
+
1076
1240
  def test_resource_views(self, configuration, post_resourceview):
1077
1241
  resource = Resource({"id": "25982d1c-f45a-45e1-b14e-87d367413045"})
1078
1242
  with pytest.raises(HDXError):