hdx-python-api 6.6.9__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.6.9 → hdx_python_api-6.7.1}/.gitignore +1 -0
  2. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/PKG-INFO +3 -3
  3. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/documentation/index.md +108 -5
  4. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/pyproject.toml +2 -2
  5. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/_version.py +2 -2
  6. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/hdx_base_configuration.yaml +4 -0
  7. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/locations.py +3 -4
  8. hdx_python_api-6.7.1/src/hdx/data/location.py +237 -0
  9. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/data/resource.py +180 -1
  10. hdx_python_api-6.7.1/tests/fixtures/location/location_list_all_fields.yaml +31 -0
  11. hdx_python_api-6.7.1/tests/fixtures/location/location_show_results.yaml +11 -0
  12. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/__init__.py +5 -0
  13. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/api/test_configuration.py +18 -0
  14. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/api/test_locations.py +4 -2
  15. hdx_python_api-6.7.1/tests/hdx/data/test_location.py +381 -0
  16. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_resource.py +164 -0
  17. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/uv.lock +220 -8
  18. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/.github/workflows/publish.yaml +0 -0
  19. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/.github/workflows/run-python-tests.yaml +0 -0
  20. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/.pre-commit-config.yaml +0 -0
  21. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/CLAUDE.md +0 -0
  22. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/LICENSE +0 -0
  23. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/README.md +0 -0
  24. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/documentation/.readthedocs.yaml +0 -0
  25. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/documentation/mkdocs.yaml +0 -0
  26. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/__init__.py +0 -0
  27. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/configuration.py +0 -0
  28. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/remotehdx.py +0 -0
  29. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/utilities/__init__.py +0 -0
  30. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/utilities/date_helper.py +0 -0
  31. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/utilities/filestore_helper.py +0 -0
  32. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/utilities/hdx_error_handler.py +0 -0
  33. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/utilities/hdx_state.py +0 -0
  34. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/api/utilities/url_utils.py +0 -0
  35. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/data/__init__.py +0 -0
  36. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/data/dataset.py +0 -0
  37. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/data/hdxobject.py +0 -0
  38. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/data/organization.py +0 -0
  39. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/data/resource_matcher.py +0 -0
  40. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/data/resource_view.py +0 -0
  41. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/data/showcase.py +0 -0
  42. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/data/user.py +0 -0
  43. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/data/vocabulary.py +0 -0
  44. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/facades/__init__.py +0 -0
  45. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/facades/infer_arguments.py +0 -0
  46. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/facades/keyword_arguments.py +0 -0
  47. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/src/hdx/facades/simple.py +0 -0
  48. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/__init__.py +0 -0
  49. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/Accepted_Tags.csv +0 -0
  50. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/CKAN/hdx_dataset_static.yaml +0 -0
  51. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/Tag_Mapping.csv +0 -0
  52. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/Tag_Mapping_ChainRuleError.csv +0 -0
  53. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/empty.yaml +0 -0
  54. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_base_config.json +0 -0
  55. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_base_config.yaml +0 -0
  56. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_config.json +0 -0
  57. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_config.yaml +0 -0
  58. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_dataset_static.json +0 -0
  59. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_dataset_static.yaml +0 -0
  60. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_datasource_topline.json +0 -0
  61. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_datasource_topline.yaml +0 -0
  62. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_email_configuration.json +0 -0
  63. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_email_configuration.yaml +0 -0
  64. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_missing_site_config.json +0 -0
  65. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_organization_static.json +0 -0
  66. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_organization_static.yaml +0 -0
  67. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_resource_static.json +0 -0
  68. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_resource_static.yaml +0 -0
  69. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_resource_view_static.json +0 -0
  70. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_resource_view_static.yaml +0 -0
  71. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_showcase_static.json +0 -0
  72. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_showcase_static.yaml +0 -0
  73. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_user_static.json +0 -0
  74. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_user_static.yaml +0 -0
  75. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_vocabulary_static.json +0 -0
  76. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/hdx_vocabulary_static.yaml +0 -0
  77. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/project_configuration.json +0 -0
  78. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/project_configuration.yaml +0 -0
  79. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/user_agent_config.yaml +0 -0
  80. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/user_agent_config2.yaml +0 -0
  81. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/user_agent_config3.yaml +0 -0
  82. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/config/user_agent_config_wrong.yaml +0 -0
  83. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/dataset_search_results.yaml +0 -0
  84. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/download_gen_resource/conflict_data_alg.csv +0 -0
  85. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/download_gen_resource/min_qc_conflict_data_alg.csv +0 -0
  86. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/download_gen_resource/qc_conflict_data_alg.csv +0 -0
  87. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/download_gen_resource/test_data_no_data.csv +0 -0
  88. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/download_gen_resource/test_data_no_years.csv +0 -0
  89. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/empty.csv +0 -0
  90. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/gen_resource/conflict_data_alg.csv +0 -0
  91. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/gen_resource/min_qc_conflict_data_alg.csv +0 -0
  92. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/gen_resource/qc_conflict_data_alg.csv +0 -0
  93. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/gen_resource/test_data_no_data.csv +0 -0
  94. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/gen_resource/test_data_no_years.csv +0 -0
  95. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/organization/organization_list_all_fields.yaml +0 -0
  96. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/organization/organization_show_results.yaml +0 -0
  97. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/qc_from_rows/qc_conflict_data_alg.csv +0 -0
  98. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/qc_from_rows/qc_conflict_data_alg_one_col.csv +0 -0
  99. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/resource_formats.json +0 -0
  100. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/showcase_all_search_results.yaml +0 -0
  101. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/size_hash/ACLED-All-Africa-File_20170101-to-20170708.xlsx +0 -0
  102. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/state/analysis_dates.txt +0 -0
  103. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/state/last_build_date.txt +0 -0
  104. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/test_data.csv +0 -0
  105. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/test_data.zip +0 -0
  106. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/dem_data_zwe.csv +0 -0
  107. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/dem_indicatorlist_zwe.csv +0 -0
  108. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/expected_resources_to_update.json +0 -0
  109. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/opri_data_zwe.csv +0 -0
  110. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/opri_indicatorlist_zwe.csv +0 -0
  111. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/opri_metadata_zwe.csv +0 -0
  112. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/qc_sdg_data_zwe.csv +0 -0
  113. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/sdg_data_zwe.csv +0 -0
  114. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/sdg_indicatorlist_zwe.csv +0 -0
  115. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/sdg_metadata_zwe.csv +0 -0
  116. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/unesco_dataset.json +0 -0
  117. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_dataset_resources/unesco_update_dataset.json +0 -0
  118. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_logic/update_logic_resources.yaml +0 -0
  119. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/fixtures/update_logic/update_logic_resources_new.yaml +0 -0
  120. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/api/__init__.py +0 -0
  121. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/api/test_ckan.py +0 -0
  122. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/api/utilities/__init__.py +0 -0
  123. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/api/utilities/test_filestore_helper.py +0 -0
  124. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/api/utilities/test_hdx_error_handler.py +0 -0
  125. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/api/utilities/test_hdx_state.py +0 -0
  126. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/api/utilities/test_url_utils.py +0 -0
  127. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/conftest.py +0 -0
  128. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/__init__.py +0 -0
  129. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_dataset_add_hapi_error.py +0 -0
  130. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_dataset_core.py +0 -0
  131. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_dataset_noncore.py +0 -0
  132. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_dataset_resource_generation.py +0 -0
  133. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_organization.py +0 -0
  134. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_resource_view.py +0 -0
  135. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_showcase.py +0 -0
  136. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_update_dataset_resources.py +0 -0
  137. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_update_logic.py +0 -0
  138. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_user.py +0 -0
  139. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/data/test_vocabulary.py +0 -0
  140. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/facades/__init__.py +0 -0
  141. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/facades/test_infer_arguments.py +0 -0
  142. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/facades/test_keyword_arguments.py +0 -0
  143. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/tests/hdx/facades/test_simple.py +0 -0
  144. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/workingexample/my_code.py +0 -0
  145. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/workingexample/my_resource.csv +0 -0
  146. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/workingexample/my_resource.xlsx +0 -0
  147. {hdx_python_api-6.6.9 → hdx_python_api-6.7.1}/workingexample/run.py +0 -0
@@ -100,3 +100,4 @@ _version.py
100
100
  tests/hdx/api/gsheet_auth.json
101
101
 
102
102
  .vscode/settings.json
103
+ .claude/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hdx-python-api
3
- Version: 6.6.9
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
@@ -33,6 +33,7 @@ upload your datasets to HDX.
33
33
  - [Showcase Management](#showcase-management)
34
34
  - [User Management](#user-management)
35
35
  - [Organization Management](#organization-management)
36
+ - [Location Management](#location-management)
36
37
  - [Vocabulary Management](#vocabulary-management)
37
38
  - [Pipeline State](#pipeline-state)
38
39
  - [Working Examples](#working-examples)
@@ -831,9 +832,13 @@ dictionary. HEADERS is either a row number (rows start counting at 1), or the
831
832
  actual headers defined as a list of strings. If not set, all rows will be
832
833
  treated as containing values:
833
834
 
834
- dataset.generate_resource("FOLDER", "FILENAME", ROWS, RESOURCE DATA, HEADERS,
835
- COLUMNS, "FORMAT", "ENCODING", DATECOL or YEARCOL or
836
- 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.
837
842
 
838
843
  The first 4 parameters are mandatory, the rest are optional. A resource can be generated
839
844
  from a given list or tuple or other iterable. The method returns a tuple with a bool
@@ -867,8 +872,12 @@ and outputs a modified row.
867
872
  The rest of the arguments are the same as for `generate_resource`.
868
873
 
869
874
  dataset.download_generate_resource(DOWNLOADER, "URL", "FOLDER", "FILENAME",
870
- RESOURCE_DATA, HEADER_INSERTIONS, ROW_FUNCTION,
871
- 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.
872
881
 
873
882
  ## Resource Specific Operations
874
883
 
@@ -964,6 +973,58 @@ using the getter:
964
973
 
965
974
  data_dictionary = resource.get_hdx_data_dictionary()
966
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
+
967
1028
  ## Showcase Management
968
1029
 
969
1030
  The **Showcase** class enables you to manage showcases, creating, deleting and updating
@@ -1063,6 +1124,48 @@ You can delete a user from an organization:
1063
1124
 
1064
1125
  organization.delete_user("USER ID")
1065
1126
 
1127
+ ## Location Management
1128
+
1129
+ The **Location** class wraps HDX locations (CKAN groups eg. countries, "world"). Reading
1130
+ locations does not require any special permissions, so these are the operations you will
1131
+ typically use. Creating, updating and deleting locations is restricted to HDX sysadmins
1132
+ and works the same way as for other HDX objects (see
1133
+ [Operations on HDX Objects](#operations-on-hdx-objects)).
1134
+
1135
+ You can read a single location:
1136
+
1137
+ location = Location.read_from_hdx("LOCATION_ID_OR_NAME")
1138
+
1139
+ You can get all location names in HDX:
1140
+
1141
+ location_names = Location.get_all_location_names(**kwargs)
1142
+
1143
+ Passing `all_fields=True` (plus optionally `include_extras=True` etc.) returns the full
1144
+ location dictionaries instead of just names. Various additional arguments (`**kwargs`)
1145
+ can be supplied and are detailed in the API documentation.
1146
+
1147
+ You can fetch the countries that are currently active on HDX's Data Grid (a live lookup,
1148
+ since Data Grid membership changes over time, rather than a fixed list):
1149
+
1150
+ country_codes = Location.get_data_grid_countries()
1151
+
1152
+ This returns a sorted list of 3-letter location codes.
1153
+
1154
+ You can get the datasets belonging to a location as follows:
1155
+
1156
+ datasets = location.get_datasets(**kwargs)
1157
+
1158
+ Various additional arguments (`**kwargs`) can be supplied. These are detailed in the API
1159
+ documentation.
1160
+
1161
+ You can autocomplete a location name:
1162
+
1163
+ matches = Location.autocomplete("NAME")
1164
+
1165
+ For simple name/code lookups against a cached list of valid locations (eg. converting
1166
+ between a country name and its HDX code) rather than the full **Location** object, see
1167
+ the **Locations** helper class instead.
1168
+
1066
1169
  ## Vocabulary Management
1067
1170
 
1068
1171
  The **Vocabulary** class enables you to manage CKAN vocabularies, creating, deleting and
@@ -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.6.9'
22
- __version_tuple__ = version_tuple = (6, 6, 9)
21
+ __version__ = version = '6.7.1'
22
+ __version_tuple__ = version_tuple = (6, 7, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -72,6 +72,10 @@ organization:
72
72
  - name
73
73
  - title
74
74
  - description
75
+ group:
76
+ required_fields:
77
+ - name
78
+ - title
75
79
  "resource view":
76
80
  required_fields:
77
81
  - resource_id
@@ -3,6 +3,7 @@
3
3
  from collections.abc import Sequence
4
4
 
5
5
  from hdx.api.configuration import Configuration
6
+ from hdx.data.location import Location
6
7
 
7
8
 
8
9
  class Locations:
@@ -22,10 +23,8 @@ class Locations:
22
23
  A list of valid locations
23
24
  """
24
25
  if cls._validlocations is None:
25
- if configuration is None:
26
- configuration = Configuration.read()
27
- cls._validlocations = configuration.call_remoteckan(
28
- "group_list", {"all_fields": True}
26
+ cls._validlocations = Location.get_all_location_names(
27
+ configuration=configuration, all_fields=True
29
28
  )
30
29
  return cls._validlocations
31
30
 
@@ -0,0 +1,237 @@
1
+ """Location class containing all logic for creating, checking, and updating locations."""
2
+
3
+ import logging
4
+ from collections.abc import Sequence
5
+ from typing import Any, Optional
6
+
7
+ from hdx.api.configuration import Configuration
8
+ from hdx.data.hdxobject import HDXObject
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Location(HDXObject):
14
+ """Location class containing all logic for creating, checking, and updating locations.
15
+ A location is a CKAN group.
16
+
17
+ Args:
18
+ initial_data: Initial location metadata dictionary. Defaults to None.
19
+ configuration: HDX configuration. Defaults to global configuration.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ initial_data: dict | None = None,
25
+ configuration: Configuration | None = None,
26
+ ) -> None:
27
+ if not initial_data:
28
+ initial_data = {}
29
+ super().__init__(initial_data, configuration=configuration)
30
+
31
+ @staticmethod
32
+ def actions() -> dict[str, str]:
33
+ """Dictionary of actions that can be performed on object
34
+
35
+ Returns:
36
+ Dictionary of actions that can be performed on object
37
+ """
38
+ return {
39
+ "show": "group_show",
40
+ "update": "group_update",
41
+ "create": "group_create",
42
+ "delete": "group_delete",
43
+ "list": "group_list",
44
+ "autocomplete": "group_autocomplete",
45
+ }
46
+
47
+ @classmethod
48
+ def read_from_hdx(
49
+ cls, identifier: str, configuration: Configuration | None = None
50
+ ) -> Optional["Location"]:
51
+ """Reads the location given by identifier from HDX and returns Location object
52
+
53
+ Args:
54
+ identifier: Identifier of location
55
+ configuration: HDX configuration. Defaults to global configuration.
56
+
57
+ Returns:
58
+ Location object if successful read, None if not
59
+ """
60
+ return cls._read_from_hdx_class("group", identifier, configuration)
61
+
62
+ def check_required_fields(self, ignore_fields: Sequence[str] = ()) -> None:
63
+ """Check that metadata for location is complete. The parameter ignore_fields should
64
+ be set if required to any fields that should be ignored for the particular operation.
65
+
66
+ Args:
67
+ ignore_fields: Fields to ignore. Default is ().
68
+
69
+ Returns:
70
+ None
71
+ """
72
+ self._check_required_fields("group", ignore_fields)
73
+
74
+ def update_in_hdx(self, **kwargs: Any) -> None:
75
+ """Check if location exists in HDX and if so, update location
76
+
77
+ Returns:
78
+ None
79
+ """
80
+ self._update_in_hdx("group", "id", **kwargs)
81
+
82
+ def create_in_hdx(self, **kwargs: Any) -> None:
83
+ """Check if location exists in HDX and if so, update it, otherwise create location
84
+
85
+ Returns:
86
+ None
87
+ """
88
+ self._create_in_hdx("group", "id", "name", **kwargs)
89
+
90
+ def delete_from_hdx(self) -> None:
91
+ """Deletes a location from HDX.
92
+
93
+ Returns:
94
+ None
95
+ """
96
+ self._delete_from_hdx("group", "id")
97
+
98
+ def get_datasets(self, query: str = "*:*", **kwargs: Any) -> list["Dataset"]: # noqa: F821
99
+ """Get list of datasets in location
100
+
101
+ Args:
102
+ query: Restrict datasets returned to this query (in Solr format). Defaults to '*:*'.
103
+ **kwargs: See below
104
+ sort (string): Sorting of the search results. Defaults to 'relevance asc, metadata_modified desc'.
105
+ rows (int): Number of matching rows to return. Defaults to all datasets (sys.maxsize).
106
+ start (int): Offset in the complete result for where the set of returned datasets should begin
107
+ facet (string): Whether to enable faceted results. Default to True.
108
+ facet.mincount (int): Minimum counts for facet fields should be included in the results
109
+ facet.limit (int): Maximum number of values the facet fields return (- = unlimited). Defaults to 50.
110
+ facet.field (list[str]): Fields to facet upon. Default is empty.
111
+ use_default_schema (bool): Use default package schema instead of custom schema. Defaults to False.
112
+
113
+ Returns:
114
+ List of datasets in location
115
+ """
116
+ import hdx.data.dataset # avoid circular import
117
+
118
+ return hdx.data.dataset.Dataset.search_in_hdx(
119
+ query=query,
120
+ configuration=self.configuration,
121
+ fq=f"groups:{self.data['name']}",
122
+ **kwargs,
123
+ )
124
+
125
+ @staticmethod
126
+ def get_all_location_names(
127
+ configuration: Configuration | None = None, **kwargs: Any
128
+ ) -> list[str]:
129
+ """Get all location names in HDX
130
+
131
+ Args:
132
+ configuration: HDX configuration. Defaults to global configuration.
133
+ **kwargs: See below
134
+ sort (str): Sort the search results according to field name and sort-order. Allowed fields are ‘name’, ‘package_count’ and ‘title’. Defaults to 'name asc'.
135
+ groups (list[str]): List of names of the groups to return.
136
+ all_fields (bool): Return group dictionaries instead of just names. Only core fields are returned - get some more using the include_* options. Defaults to False.
137
+ include_extras (bool): If all_fields, include the group extra fields. Defaults to False.
138
+ include_tags (bool): If all_fields, include the group tags. Defaults to False.
139
+ include_groups (bool): If all_fields, include the groups the groups are in. Defaults to False.
140
+
141
+ Returns:
142
+ List of all location names in HDX
143
+ """
144
+ location = Location(configuration=configuration)
145
+
146
+ # Early return for the standard, non-paginated case
147
+ if not kwargs.get("all_fields"):
148
+ return location._write_to_hdx("list", kwargs)
149
+
150
+ compiled_locations = {}
151
+ # Extract limit and offset to dictate our paging sizes, defaulting to 400 and 0
152
+ page_size = kwargs.pop("limit", 400)
153
+ current_offset = kwargs.pop("offset", 0)
154
+
155
+ # 1. Fetch in pages using the provided/default page size
156
+ while True:
157
+ page_kwargs = kwargs.copy()
158
+ page_kwargs["limit"] = page_size
159
+ page_kwargs["offset"] = current_offset
160
+
161
+ page_data = location._write_to_hdx("list", page_kwargs)
162
+
163
+ if not page_data:
164
+ break
165
+
166
+ # Store by name to automatically deduplicate offset shifts
167
+ for loc in page_data:
168
+ compiled_locations[loc["name"]] = loc
169
+
170
+ if len(page_data) < page_size:
171
+ break
172
+
173
+ current_offset += page_size
174
+
175
+ # 2. Establish Ground Truth LAST (without limit/offset constraints)
176
+ truth_kwargs = kwargs.copy()
177
+ truth_kwargs["all_fields"] = False
178
+
179
+ ground_truth_names = location._write_to_hdx("list", truth_kwargs)
180
+
181
+ # 3 & 4. Check for missing IDs, fetch individually, and implicitly prune deletes
182
+ final_locations = []
183
+ for name in ground_truth_names:
184
+ if name in compiled_locations:
185
+ final_locations.append(compiled_locations[name])
186
+ else:
187
+ try:
188
+ missing_location = location._write_to_hdx("show", {"id": name})
189
+ if missing_location:
190
+ final_locations.append(missing_location)
191
+ except Exception:
192
+ pass
193
+
194
+ return final_locations
195
+
196
+ @staticmethod
197
+ def get_data_grid_countries(
198
+ configuration: Configuration | None = None,
199
+ ) -> list[str]:
200
+ """Fetch HDX's active Data Grid countries (3-letter group names).
201
+
202
+ Treated as a live/short-TTL lookup rather than a frozen static list, since Data Grid
203
+ membership changes over time.
204
+
205
+ Args:
206
+ configuration: HDX configuration. Defaults to global configuration.
207
+
208
+ Returns:
209
+ Sorted list of active Data Grid country (group) names
210
+ """
211
+ groups = Location.get_all_location_names(
212
+ configuration=configuration, all_fields=True, include_extras=True
213
+ )
214
+ return sorted(
215
+ group["name"]
216
+ for group in groups
217
+ if len(group["name"]) == 3 and group.get("data_completeness") == "active"
218
+ )
219
+
220
+ @classmethod
221
+ def autocomplete(
222
+ cls,
223
+ name: str,
224
+ limit: int = 20,
225
+ configuration: Configuration | None = None,
226
+ ) -> list:
227
+ """Autocomplete a location name and return matches
228
+
229
+ Args:
230
+ name: Name to autocomplete
231
+ limit: Maximum number of matches to return
232
+ configuration: HDX configuration. Defaults to global configuration.
233
+
234
+ Returns:
235
+ Autocomplete matches
236
+ """
237
+ return cls._autocomplete(name, limit, configuration)
@@ -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