hdx-python-api 6.6.7__tar.gz → 6.6.9__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 (143) hide show
  1. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/.pre-commit-config.yaml +1 -1
  2. hdx_python_api-6.6.9/CLAUDE.md +57 -0
  3. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/PKG-INFO +5 -5
  4. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/documentation/index.md +27 -0
  5. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/pyproject.toml +3 -3
  6. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/_version.py +2 -2
  7. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/resource.py +104 -0
  8. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/utilities/test_hdx_state.py +2 -2
  9. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_dataset_add_hapi_error.py +1 -1
  10. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_dataset_core.py +10 -10
  11. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_dataset_noncore.py +5 -5
  12. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_organization.py +9 -9
  13. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_resource.py +111 -15
  14. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_resource_view.py +5 -5
  15. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_showcase.py +5 -5
  16. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_user.py +11 -11
  17. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_vocabulary.py +6 -6
  18. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/uv.lock +136 -108
  19. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/.github/workflows/publish.yaml +0 -0
  20. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/.github/workflows/run-python-tests.yaml +0 -0
  21. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/.gitignore +0 -0
  22. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/LICENSE +0 -0
  23. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/README.md +0 -0
  24. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/documentation/.readthedocs.yaml +0 -0
  25. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/documentation/mkdocs.yaml +0 -0
  26. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/__init__.py +0 -0
  27. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/configuration.py +0 -0
  28. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/hdx_base_configuration.yaml +0 -0
  29. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/locations.py +0 -0
  30. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/remotehdx.py +0 -0
  31. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/__init__.py +0 -0
  32. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/date_helper.py +0 -0
  33. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/filestore_helper.py +0 -0
  34. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/hdx_error_handler.py +0 -0
  35. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/hdx_state.py +0 -0
  36. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/url_utils.py +0 -0
  37. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/__init__.py +0 -0
  38. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/dataset.py +0 -0
  39. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/hdxobject.py +0 -0
  40. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/organization.py +0 -0
  41. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/resource_matcher.py +0 -0
  42. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/resource_view.py +0 -0
  43. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/showcase.py +0 -0
  44. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/user.py +0 -0
  45. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/vocabulary.py +0 -0
  46. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/facades/__init__.py +0 -0
  47. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/facades/infer_arguments.py +0 -0
  48. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/facades/keyword_arguments.py +0 -0
  49. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/facades/simple.py +0 -0
  50. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/__init__.py +0 -0
  51. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/Accepted_Tags.csv +0 -0
  52. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/CKAN/hdx_dataset_static.yaml +0 -0
  53. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/Tag_Mapping.csv +0 -0
  54. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/Tag_Mapping_ChainRuleError.csv +0 -0
  55. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/empty.yaml +0 -0
  56. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_base_config.json +0 -0
  57. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_base_config.yaml +0 -0
  58. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_config.json +0 -0
  59. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_config.yaml +0 -0
  60. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_dataset_static.json +0 -0
  61. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_dataset_static.yaml +0 -0
  62. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_datasource_topline.json +0 -0
  63. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_datasource_topline.yaml +0 -0
  64. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_email_configuration.json +0 -0
  65. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_email_configuration.yaml +0 -0
  66. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_missing_site_config.json +0 -0
  67. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_organization_static.json +0 -0
  68. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_organization_static.yaml +0 -0
  69. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_resource_static.json +0 -0
  70. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_resource_static.yaml +0 -0
  71. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_resource_view_static.json +0 -0
  72. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_resource_view_static.yaml +0 -0
  73. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_showcase_static.json +0 -0
  74. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_showcase_static.yaml +0 -0
  75. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_user_static.json +0 -0
  76. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_user_static.yaml +0 -0
  77. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_vocabulary_static.json +0 -0
  78. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_vocabulary_static.yaml +0 -0
  79. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/project_configuration.json +0 -0
  80. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/project_configuration.yaml +0 -0
  81. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/user_agent_config.yaml +0 -0
  82. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/user_agent_config2.yaml +0 -0
  83. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/user_agent_config3.yaml +0 -0
  84. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/user_agent_config_wrong.yaml +0 -0
  85. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/dataset_search_results.yaml +0 -0
  86. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/download_gen_resource/conflict_data_alg.csv +0 -0
  87. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/download_gen_resource/min_qc_conflict_data_alg.csv +0 -0
  88. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/download_gen_resource/qc_conflict_data_alg.csv +0 -0
  89. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/download_gen_resource/test_data_no_data.csv +0 -0
  90. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/download_gen_resource/test_data_no_years.csv +0 -0
  91. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/empty.csv +0 -0
  92. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/gen_resource/conflict_data_alg.csv +0 -0
  93. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/gen_resource/min_qc_conflict_data_alg.csv +0 -0
  94. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/gen_resource/qc_conflict_data_alg.csv +0 -0
  95. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/gen_resource/test_data_no_data.csv +0 -0
  96. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/gen_resource/test_data_no_years.csv +0 -0
  97. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/organization/organization_list_all_fields.yaml +0 -0
  98. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/organization/organization_show_results.yaml +0 -0
  99. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/qc_from_rows/qc_conflict_data_alg.csv +0 -0
  100. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/qc_from_rows/qc_conflict_data_alg_one_col.csv +0 -0
  101. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/resource_formats.json +0 -0
  102. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/showcase_all_search_results.yaml +0 -0
  103. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/size_hash/ACLED-All-Africa-File_20170101-to-20170708.xlsx +0 -0
  104. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/state/analysis_dates.txt +0 -0
  105. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/state/last_build_date.txt +0 -0
  106. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/test_data.csv +0 -0
  107. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/test_data.zip +0 -0
  108. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/dem_data_zwe.csv +0 -0
  109. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/dem_indicatorlist_zwe.csv +0 -0
  110. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/expected_resources_to_update.json +0 -0
  111. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/opri_data_zwe.csv +0 -0
  112. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/opri_indicatorlist_zwe.csv +0 -0
  113. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/opri_metadata_zwe.csv +0 -0
  114. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/qc_sdg_data_zwe.csv +0 -0
  115. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/sdg_data_zwe.csv +0 -0
  116. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/sdg_indicatorlist_zwe.csv +0 -0
  117. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/sdg_metadata_zwe.csv +0 -0
  118. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/unesco_dataset.json +0 -0
  119. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/unesco_update_dataset.json +0 -0
  120. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_logic/update_logic_resources.yaml +0 -0
  121. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_logic/update_logic_resources_new.yaml +0 -0
  122. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/__init__.py +0 -0
  123. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/__init__.py +0 -0
  124. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/test_ckan.py +0 -0
  125. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/test_configuration.py +0 -0
  126. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/test_locations.py +0 -0
  127. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/utilities/__init__.py +0 -0
  128. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/utilities/test_filestore_helper.py +0 -0
  129. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/utilities/test_hdx_error_handler.py +0 -0
  130. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/utilities/test_url_utils.py +0 -0
  131. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/conftest.py +0 -0
  132. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/__init__.py +0 -0
  133. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_dataset_resource_generation.py +0 -0
  134. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_update_dataset_resources.py +0 -0
  135. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_update_logic.py +0 -0
  136. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/facades/__init__.py +0 -0
  137. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/facades/test_infer_arguments.py +0 -0
  138. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/facades/test_keyword_arguments.py +0 -0
  139. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/facades/test_simple.py +0 -0
  140. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/workingexample/my_code.py +0 -0
  141. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/workingexample/my_resource.csv +0 -0
  142. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/workingexample/my_resource.xlsx +0 -0
  143. {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/workingexample/run.py +0 -0
@@ -1,5 +1,5 @@
1
1
  default_language_version:
2
- python: python3.13
2
+ python: python3
3
3
 
4
4
  repos:
5
5
  - repo: https://github.com/pre-commit/pre-commit-hooks
@@ -0,0 +1,57 @@
1
+ # CLAUDE.md
2
+
3
+ ## Project Overview
4
+
5
+ **hdx-python-api** is the official Python library for interacting with the [Humanitarian Data Exchange (HDX)](https://data.humdata.org/) platform. It provides ORM-like classes wrapping the CKAN API to read, create, update, and delete datasets, resources, organizations, users, vocabularies, showcases, and datastores.
6
+
7
+ ## Source Layout
8
+
9
+ - `src/hdx/api/` — Configuration, session management, locations, utilities
10
+ - `configuration.py` — `Configuration` class (credentials, site URL, user agent)
11
+ - `remotehdx.py` — Low-level CKAN API client
12
+ - `src/hdx/data/` — Data model objects, all inheriting from `HDXObject`
13
+ - `dataset.py`, `resource.py`, `organization.py`, `user.py`, `vocabulary.py`, `showcase.py`, `resource_view.py`
14
+ - `hdxobject.py` — Base class providing `_read_from_hdx` / `_write_to_hdx` and CRUD scaffolding
15
+ - `src/hdx/facades/` — High-level entry-point helpers (`simple`, `keyword_arguments`, `infer_arguments`)
16
+ - `tests/hdx/` — Test suite mirroring src layout; fixtures in `tests/fixtures/`
17
+
18
+ ## Running Tests
19
+
20
+ ```bash
21
+ uv run pytest
22
+ ```
23
+
24
+ Tests mock the CKAN HTTP session via inner `MockSession` classes in each test file — no live HDX connection needed for the standard suite. Integration tests require `HDX_KEY_TEST` and `HDX_PIPELINE_GSHEET_AUTH` environment variables.
25
+
26
+ Coverage is written to `coverage.lcov` and JUnit XML to `test-results.xml`.
27
+
28
+ ## Code Style
29
+
30
+ Formatted and linted with `ruff` (rules: E, F, I, UP; line-length not enforced). Run before committing:
31
+
32
+ ```bash
33
+ pre-commit run --all-files
34
+ ```
35
+
36
+ - Python ≥ 3.10
37
+ - Type hints throughout; use `X | Y` union syntax (PEP 604), not `Optional`/`Union`
38
+ - Google-style docstrings with `Args:` and `Returns:` sections
39
+ - No inline comments unless the *why* is non-obvious
40
+
41
+ ## Key Patterns
42
+
43
+ **HDXObject subclasses** expose a standard interface: `create_in_hdx()`, `update_in_hdx()`, `delete_from_hdx()`, `read_from_hdx()`. Each class defines an `actions()` static method mapping action names to CKAN API action strings.
44
+
45
+ **Writing to HDX** uses `self._write_to_hdx(action_key, data_dict, id_field_name)` inherited from `HDXObject`. The `action_key` must be in `actions()`.
46
+
47
+ **Test mocking** — each test fixture defines a `MockSession` with a `post()` method that inspects the URL and decoded JSON body to return `MockResponse` objects. State is tracked on class variables (e.g. `TestResource.datastore`).
48
+
49
+ ## Collaboration Style
50
+
51
+ - Be objective, not agreeable. Act as a partner, not a sycophant. Push back when you disagree, flag tradeoffs honestly, and don't sugarcoat problems.
52
+ - Keep explanations brief and to the point.
53
+ - Don't rely on recalled knowledge for facts that could be stale (API behaviour, library versions, external systems). Search or read the actual source first. If you lack verified information, say so rather than speculate.
54
+
55
+ ## Scope of Changes
56
+
57
+ When fixing a bug or addressing PR feedback, change only what is necessary to resolve the specific issue. Do not refactor surrounding code, rename variables, adjust formatting, or make improvements in the same commit unless they are directly required by the fix.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: hdx-python-api
3
- Version: 6.6.7
3
+ Version: 6.6.9
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
@@ -25,11 +25,11 @@ Classifier: Programming Language :: Python :: 3.13
25
25
  Classifier: Programming Language :: Python :: 3.14
26
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
27
  Requires-Python: >=3.10
28
- Requires-Dist: ckanapi>=4.8
28
+ Requires-Dist: ckanapi>=4.11
29
29
  Requires-Dist: defopt>=7.0.0
30
30
  Requires-Dist: email-validator
31
- Requires-Dist: hdx-python-country>=4.1.1
32
- Requires-Dist: hdx-python-utilities>=4.0.8
31
+ Requires-Dist: hdx-python-country>=4.1.3
32
+ Requires-Dist: hdx-python-utilities>=4.1.2
33
33
  Requires-Dist: makefun
34
34
  Requires-Dist: requests
35
35
  Provides-Extra: docs
@@ -937,6 +937,33 @@ If the method **set_file_to_upload** is used to supply a file, the resource
937
937
  `last_modified` field is set to now automatically regardless of the value of
938
938
  `data_updated` or whether **mark_data_updated** has been called.
939
939
 
940
+ ### Data Dictionary
941
+
942
+ A resource can have a data dictionary describing the columns in the data it
943
+ contains. It is a list of dictionaries, each with the keys `field` (the
944
+ column name as it appears in the file), `label` (a human-readable label) and
945
+ `description` (a human-readable description of the column). Set it using
946
+ **set_hdx_data_dictionary**:
947
+
948
+ resource.set_hdx_data_dictionary([
949
+ {
950
+ "field": "col_name",
951
+ "label": "Column Label",
952
+ "description": "Human-readable description.",
953
+ },
954
+ {
955
+ "field": "another_col",
956
+ "label": "Another Label",
957
+ "description": "Second column description.",
958
+ },
959
+ ])
960
+
961
+ `field`, `label` and `description` must all be non-empty strings for every
962
+ column, otherwise an `HDXError` is raised. Read the data dictionary back
963
+ using the getter:
964
+
965
+ data_dictionary = resource.get_hdx_data_dictionary()
966
+
940
967
  ## Showcase Management
941
968
 
942
969
  The **Showcase** class enables you to manage showcases, creating, deleting and updating
@@ -36,11 +36,11 @@ dynamic = ["version"]
36
36
  requires-python = ">=3.10"
37
37
 
38
38
  dependencies = [
39
- "ckanapi>=4.8",
39
+ "ckanapi>=4.11",
40
40
  "defopt>=7.0.0",
41
41
  "email_validator",
42
- "hdx-python-country>=4.1.1",
43
- "hdx-python-utilities>=4.0.8",
42
+ "hdx-python-country>=4.1.3",
43
+ "hdx-python-utilities>=4.1.2",
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.7'
22
- __version_tuple__ = version_tuple = (6, 6, 7)
21
+ __version__ = version = '6.6.9'
22
+ __version_tuple__ = version_tuple = (6, 6, 9)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1,5 +1,6 @@
1
1
  """Resource class containing all logic for creating, checking, and updating resources."""
2
2
 
3
+ import json
3
4
  import logging
4
5
  from collections.abc import Sequence
5
6
  from datetime import datetime
@@ -33,6 +34,9 @@ class Resource(HDXObject):
33
34
  """
34
35
 
35
36
  _formats_dict = None
37
+ _valid_data_dictionary_types = frozenset(
38
+ {"text", "numeric", "date", "timestamp without time zone"}
39
+ )
36
40
 
37
41
  def __init__(
38
42
  self,
@@ -62,6 +66,9 @@ class Resource(HDXObject):
62
66
  "search": "resource_search",
63
67
  "broken": "hdx_mark_broken_link_in_resource",
64
68
  "datastore_delete": "datastore_delete",
69
+ "datastore_create": "datastore_create",
70
+ "datastore_insert": "datastore_insert",
71
+ "datastore_upsert": "datastore_upsert",
65
72
  "datastore_search": "datastore_search",
66
73
  }
67
74
 
@@ -260,6 +267,56 @@ class Resource(HDXObject):
260
267
  self.data["format"] = file_format
261
268
  return file_format
262
269
 
270
+ def get_hdx_data_dictionary(self) -> list[dict] | None:
271
+ """Get the resource's data dictionary (column definitions)
272
+
273
+ Returns:
274
+ List of column definition dictionaries or None if not set
275
+ """
276
+ data_dictionary = self.data.get("hdx_data_dictionary")
277
+ if data_dictionary is None:
278
+ return None
279
+ if isinstance(data_dictionary, str):
280
+ return json.loads(data_dictionary)
281
+ return data_dictionary
282
+
283
+ def set_hdx_data_dictionary(self, data_dictionary: Sequence[dict]) -> None:
284
+ """Set the resource's data dictionary (column definitions). Each column
285
+ definition must be a dict with non-empty string values for field
286
+ (column name in the CSV file), label (human-readable column label) and
287
+ description (human-readable description of the column). If present,
288
+ data_type must be one of the PostgreSQL types produced by DataPusher+
289
+ (text, numeric, date, timestamp without time zone).
290
+
291
+ Args:
292
+ data_dictionary: Sequence of column definition dictionaries
293
+
294
+ Returns:
295
+ None
296
+ """
297
+ if not data_dictionary:
298
+ raise HDXError("hdx_data_dictionary must be a non-empty list!")
299
+ for i, column in enumerate(data_dictionary):
300
+ if not isinstance(column, dict):
301
+ raise HDXError(f"hdx_data_dictionary[{i}] must be a dict!")
302
+ for required_key in ("field", "label", "description"):
303
+ value = column.get(required_key)
304
+ if not isinstance(value, str) or not value.strip():
305
+ raise HDXError(
306
+ f"hdx_data_dictionary[{i}] is missing a non-empty '{required_key}'!"
307
+ )
308
+ if "data_type" in column:
309
+ data_type = column["data_type"]
310
+ if data_type not in self._valid_data_dictionary_types:
311
+ valid_types = ", ".join(sorted(self._valid_data_dictionary_types))
312
+ raise HDXError(
313
+ f"hdx_data_dictionary[{i}] has invalid 'data_type' "
314
+ f"'{data_type}'! Must be one of: {valid_types}"
315
+ )
316
+ self.data["hdx_data_dictionary"] = json.dumps(
317
+ data_dictionary, separators=(",", ":")
318
+ )
319
+
263
320
  def clean_format(self) -> str:
264
321
  """Clean the resource's format, setting it to None if it is invalid and
265
322
  cannot be mapped
@@ -701,6 +758,53 @@ class Resource(HDXObject):
701
758
  return True
702
759
  return False
703
760
 
761
+ def create_datastore(
762
+ self,
763
+ schema: Sequence[dict],
764
+ primary_key: str | Sequence[str] | None = None,
765
+ ) -> None:
766
+ """Create a datastore for the resource with the given schema.
767
+
768
+ Args:
769
+ schema: Sequence of field definitions, each a dict with 'id' and 'type' keys.
770
+ primary_key: Primary key field name(s). Defaults to None.
771
+
772
+ Returns:
773
+ None
774
+ """
775
+ data: dict = {
776
+ "resource_id": self.data["id"],
777
+ "force": True,
778
+ "fields": schema,
779
+ }
780
+ if primary_key is not None:
781
+ if not isinstance(primary_key, str):
782
+ primary_key = ",".join(primary_key)
783
+ data["primary_key"] = primary_key
784
+ self._write_to_hdx("datastore_create", data, "resource_id")
785
+
786
+ def update_datastore(
787
+ self,
788
+ records: Sequence[dict],
789
+ method: str = "upsert",
790
+ ) -> None:
791
+ """Update (upsert) records into the resource datastore.
792
+
793
+ Args:
794
+ records: Sequence of record dicts to insert or update.
795
+ method: Datastore update method ('upsert', 'insert', or 'update'). Defaults to 'upsert'.
796
+
797
+ Returns:
798
+ None
799
+ """
800
+ data = {
801
+ "resource_id": self.data["id"],
802
+ "force": True,
803
+ "method": method,
804
+ "records": records,
805
+ }
806
+ self._write_to_hdx("datastore_upsert", data, "resource_id")
807
+
704
808
  def delete_datastore(self) -> None:
705
809
  """Delete a resource from the HDX datastore
706
810
 
@@ -47,7 +47,7 @@ class TestState:
47
47
  def do_state(self, tempfolder, statefile):
48
48
  class MockSession:
49
49
  @staticmethod
50
- def post(url, data, headers, files, allow_redirects, auth=None):
50
+ def post(url, data, **kwargs):
51
51
  if "resource" in url:
52
52
  result = json.dumps(resultdict)
53
53
  return MockResponse(
@@ -71,7 +71,7 @@ class TestState:
71
71
  def do_state_multi(self, tempfolder, multidatestatefile):
72
72
  class MockSession:
73
73
  @staticmethod
74
- def post(url, data, headers, files, allow_redirects, auth=None):
74
+ def post(url, data, **kwargs):
75
75
  if "resource" in url:
76
76
  result = json.dumps(resultdict)
77
77
  return MockResponse(
@@ -13,7 +13,7 @@ class TestDatasetAddHAPIError:
13
13
  def hapi_resource_update(self):
14
14
  class MockSession:
15
15
  @staticmethod
16
- def post(url, data, headers, files, allow_redirects, auth=None):
16
+ def post(url, data, **kwargs):
17
17
  datadict = json.loads(data.decode("utf-8"))
18
18
  if "show" in url:
19
19
  resource_id = datadict["id"]
@@ -182,7 +182,7 @@ class TestDatasetCore:
182
182
  def read(self):
183
183
  class MockSession:
184
184
  @staticmethod
185
- def post(url, data, headers, files, allow_redirects, auth=None):
185
+ def post(url, data, **kwargs):
186
186
  datadict = json.loads(data.decode("utf-8"))
187
187
  return dataset_mockshow(url, datadict)
188
188
 
@@ -192,7 +192,7 @@ class TestDatasetCore:
192
192
  def post_revise(self):
193
193
  class MockSession:
194
194
  @staticmethod
195
- def post(url, data, headers, files, allow_redirects, auth=None):
195
+ def post(url, data, **kwargs):
196
196
  if isinstance(data, dict):
197
197
  datadict = {
198
198
  k.decode("utf8"): v.decode("utf8") for k, v in data.items()
@@ -219,7 +219,7 @@ class TestDatasetCore:
219
219
  def post_create(self):
220
220
  class MockSession:
221
221
  @staticmethod
222
- def post(url, data, headers, files, allow_redirects, auth=None):
222
+ def post(url, data, **kwargs):
223
223
  if isinstance(data, dict):
224
224
  datadict = {
225
225
  k.decode("utf8"): v.decode("utf8") for k, v in data.items()
@@ -286,7 +286,7 @@ class TestDatasetCore:
286
286
  def post_update(self):
287
287
  class MockSession:
288
288
  @staticmethod
289
- def post(url, data, headers, files, allow_redirects, auth=None):
289
+ def post(url, data, **kwargs):
290
290
  if isinstance(data, dict):
291
291
  datadict = {
292
292
  k.decode("utf8"): v.decode("utf8") for k, v in data.items()
@@ -363,7 +363,7 @@ class TestDatasetCore:
363
363
  def post_reorder(self):
364
364
  class MockSession:
365
365
  @staticmethod
366
- def post(url, data, headers, files, allow_redirects, auth=None):
366
+ def post(url, data, **kwargs):
367
367
  decodedata = data.decode("utf-8")
368
368
  datadict = json.loads(decodedata)
369
369
  if "show" in url:
@@ -390,7 +390,7 @@ class TestDatasetCore:
390
390
  def post_delete(self):
391
391
  class MockSession:
392
392
  @staticmethod
393
- def post(url, data, headers, files, allow_redirects, auth=None):
393
+ def post(url, data, **kwargs):
394
394
  decodedata = data.decode("utf-8")
395
395
  datadict = json.loads(decodedata)
396
396
  if "show" in url:
@@ -423,7 +423,7 @@ class TestDatasetCore:
423
423
  def search(self):
424
424
  class MockSession:
425
425
  @staticmethod
426
- def post(url, data, headers, files, allow_redirects, auth=None):
426
+ def post(url, data, **kwargs):
427
427
  datadict = json.loads(data.decode("utf-8"))
428
428
  return mocksearch(url, datadict)
429
429
 
@@ -433,7 +433,7 @@ class TestDatasetCore:
433
433
  def post_list(self):
434
434
  class MockSession:
435
435
  @staticmethod
436
- def post(url, data, headers, files, allow_redirects, auth=None):
436
+ def post(url, data, **kwargs):
437
437
  datadict = json.loads(data.decode("utf-8"))
438
438
  return mocklist(url, datadict)
439
439
 
@@ -443,7 +443,7 @@ class TestDatasetCore:
443
443
  def all(self):
444
444
  class MockSession:
445
445
  @staticmethod
446
- def post(url, data, headers, files, allow_redirects, auth=None):
446
+ def post(url, data, **kwargs):
447
447
  datadict = json.loads(data.decode("utf-8"))
448
448
  return mockall(url, datadict)
449
449
 
@@ -453,7 +453,7 @@ class TestDatasetCore:
453
453
  def post_autocomplete(self):
454
454
  class MockSession:
455
455
  @staticmethod
456
- def post(url, data, headers, files, allow_redirects, auth=None):
456
+ def post(url, data, **kwargs):
457
457
  decodedata = data.decode("utf-8")
458
458
  datadict = json.loads(decodedata)
459
459
  if "autocomplete" not in url or "acled" not in datadict["q"]:
@@ -52,7 +52,7 @@ class TestDatasetNoncore:
52
52
 
53
53
  class MockSession:
54
54
  @staticmethod
55
- def post(url, data, headers, files, allow_redirects, auth=None):
55
+ def post(url, data, **kwargs):
56
56
  datadict = json.loads(data.decode("utf-8"))
57
57
  return vocabulary_mockshow(url, datadict)
58
58
 
@@ -62,7 +62,7 @@ class TestDatasetNoncore:
62
62
  def user_read(self):
63
63
  class MockSession:
64
64
  @staticmethod
65
- def post(url, data, headers, files, allow_redirects, auth=None):
65
+ def post(url, data, **kwargs):
66
66
  datadict = json.loads(data.decode("utf-8"))
67
67
  return user_mockshow(url, datadict)
68
68
 
@@ -72,7 +72,7 @@ class TestDatasetNoncore:
72
72
  def organization_read(self):
73
73
  class MockSession:
74
74
  @staticmethod
75
- def post(url, data, headers, files, allow_redirects, auth=None):
75
+ def post(url, data, **kwargs):
76
76
  datadict = json.loads(data.decode("utf-8"))
77
77
  return organization_mockshow(url, datadict)
78
78
 
@@ -82,7 +82,7 @@ class TestDatasetNoncore:
82
82
  def showcase_read(self):
83
83
  class MockSession:
84
84
  @staticmethod
85
- def post(url, data, headers, files, allow_redirects, auth=None):
85
+ def post(url, data, **kwargs):
86
86
  datadict = json.loads(data.decode("utf-8"))
87
87
  if "showcase_list" in url:
88
88
  result = json.dumps([showcase_resultdict])
@@ -111,7 +111,7 @@ class TestDatasetNoncore:
111
111
  def vocabulary_update(self):
112
112
  class MockSession:
113
113
  @staticmethod
114
- def post(url, data, headers, files, allow_redirects, auth=None):
114
+ def post(url, data, **kwargs):
115
115
  if isinstance(data, dict):
116
116
  datadict = {
117
117
  k.decode("utf8"): v.decode("utf8") for k, v in data.items()
@@ -115,7 +115,7 @@ class TestOrganization:
115
115
  def read(self):
116
116
  class MockSession:
117
117
  @staticmethod
118
- def post(url, data, headers, files, allow_redirects, auth=None):
118
+ def post(url, data, **kwargs):
119
119
  datadict = json.loads(data.decode("utf-8"))
120
120
  return organization_mockshow(url, datadict)
121
121
 
@@ -125,7 +125,7 @@ class TestOrganization:
125
125
  def post_create(self):
126
126
  class MockSession:
127
127
  @staticmethod
128
- def post(url, data, headers, files, allow_redirects, auth=None):
128
+ def post(url, data, **kwargs):
129
129
  datadict = json.loads(data.decode("utf-8"))
130
130
  if "show" in url:
131
131
  return organization_mockshow(url, datadict)
@@ -165,7 +165,7 @@ class TestOrganization:
165
165
  def post_update(self):
166
166
  class MockSession:
167
167
  @staticmethod
168
- def post(url, data, headers, files, allow_redirects, auth=None):
168
+ def post(url, data, **kwargs):
169
169
  datadict = json.loads(data.decode("utf-8"))
170
170
  if "show" in url:
171
171
  return organization_mockshow(url, datadict)
@@ -205,7 +205,7 @@ class TestOrganization:
205
205
  def post_delete(self):
206
206
  class MockSession:
207
207
  @staticmethod
208
- def post(url, data, headers, files, allow_redirects, auth=None):
208
+ def post(url, data, **kwargs):
209
209
  decodedata = data.decode("utf-8")
210
210
  datadict = json.loads(decodedata)
211
211
  if "show" in url:
@@ -232,7 +232,7 @@ class TestOrganization:
232
232
  def post_list(self):
233
233
  class MockSession:
234
234
  @staticmethod
235
- def post(url, data, headers, files, allow_redirects, auth=None):
235
+ def post(url, data, **kwargs):
236
236
  json.loads(data.decode("utf-8"))
237
237
  return mocklist(url)
238
238
 
@@ -242,7 +242,7 @@ class TestOrganization:
242
242
  def post_all_fields(self, fixturesfolder):
243
243
  class MockSession:
244
244
  @staticmethod
245
- def post(url, data, headers, files, allow_redirects, auth=None):
245
+ def post(url, data, **kwargs):
246
246
  kwargs = json.loads(data.decode("utf-8"))
247
247
  if "show" in url:
248
248
  return organization_mockshow(url, kwargs)
@@ -264,7 +264,7 @@ class TestOrganization:
264
264
  def user_read(self):
265
265
  class MockSession:
266
266
  @staticmethod
267
- def post(url, data, headers, files, allow_redirects, auth=None):
267
+ def post(url, data, **kwargs):
268
268
  datadict = json.loads(data.decode("utf-8"))
269
269
  return user_mockshow(url, datadict)
270
270
 
@@ -274,7 +274,7 @@ class TestOrganization:
274
274
  def datasets_get(self):
275
275
  class MockSession:
276
276
  @staticmethod
277
- def post(url, data, headers, files, allow_redirects, auth=None):
277
+ def post(url, data, **kwargs):
278
278
  datadict = json.loads(data.decode("utf-8"))
279
279
  return mockgetdatasets(url, datadict)
280
280
 
@@ -284,7 +284,7 @@ class TestOrganization:
284
284
  def post_autocomplete(self):
285
285
  class MockSession:
286
286
  @staticmethod
287
- def post(url, data, headers, files, allow_redirects, auth=None):
287
+ def post(url, data, **kwargs):
288
288
  decodedata = data.decode("utf-8")
289
289
  datadict = json.loads(decodedata)
290
290
  if "autocomplete" not in url or "innago" not in datadict["q"]: