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.
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/.pre-commit-config.yaml +1 -1
- hdx_python_api-6.6.9/CLAUDE.md +57 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/PKG-INFO +5 -5
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/documentation/index.md +27 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/pyproject.toml +3 -3
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/_version.py +2 -2
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/resource.py +104 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/utilities/test_hdx_state.py +2 -2
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_dataset_add_hapi_error.py +1 -1
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_dataset_core.py +10 -10
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_dataset_noncore.py +5 -5
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_organization.py +9 -9
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_resource.py +111 -15
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_resource_view.py +5 -5
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_showcase.py +5 -5
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_user.py +11 -11
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_vocabulary.py +6 -6
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/uv.lock +136 -108
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/.github/workflows/publish.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/.github/workflows/run-python-tests.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/.gitignore +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/LICENSE +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/README.md +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/documentation/.readthedocs.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/documentation/mkdocs.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/__init__.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/configuration.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/hdx_base_configuration.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/locations.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/remotehdx.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/__init__.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/date_helper.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/filestore_helper.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/hdx_error_handler.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/hdx_state.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/api/utilities/url_utils.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/__init__.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/dataset.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/hdxobject.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/organization.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/resource_matcher.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/resource_view.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/showcase.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/user.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/data/vocabulary.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/facades/__init__.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/facades/infer_arguments.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/facades/keyword_arguments.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/src/hdx/facades/simple.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/__init__.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/Accepted_Tags.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/CKAN/hdx_dataset_static.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/Tag_Mapping.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/Tag_Mapping_ChainRuleError.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/empty.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_base_config.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_base_config.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_config.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_config.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_dataset_static.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_dataset_static.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_datasource_topline.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_datasource_topline.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_email_configuration.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_email_configuration.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_missing_site_config.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_organization_static.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_organization_static.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_resource_static.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_resource_static.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_resource_view_static.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_resource_view_static.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_showcase_static.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_showcase_static.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_user_static.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_user_static.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_vocabulary_static.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/hdx_vocabulary_static.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/project_configuration.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/project_configuration.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/user_agent_config.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/user_agent_config2.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/user_agent_config3.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/config/user_agent_config_wrong.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/dataset_search_results.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/download_gen_resource/conflict_data_alg.csv +0 -0
- {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
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/download_gen_resource/qc_conflict_data_alg.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/download_gen_resource/test_data_no_data.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/download_gen_resource/test_data_no_years.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/empty.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/gen_resource/conflict_data_alg.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/gen_resource/min_qc_conflict_data_alg.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/gen_resource/qc_conflict_data_alg.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/gen_resource/test_data_no_data.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/gen_resource/test_data_no_years.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/organization/organization_list_all_fields.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/organization/organization_show_results.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/qc_from_rows/qc_conflict_data_alg.csv +0 -0
- {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
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/resource_formats.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/showcase_all_search_results.yaml +0 -0
- {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
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/state/analysis_dates.txt +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/state/last_build_date.txt +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/test_data.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/test_data.zip +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/dem_data_zwe.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/dem_indicatorlist_zwe.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/expected_resources_to_update.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/opri_data_zwe.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/opri_indicatorlist_zwe.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/opri_metadata_zwe.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/qc_sdg_data_zwe.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/sdg_data_zwe.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/sdg_indicatorlist_zwe.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/sdg_metadata_zwe.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/unesco_dataset.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_dataset_resources/unesco_update_dataset.json +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_logic/update_logic_resources.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/fixtures/update_logic/update_logic_resources_new.yaml +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/__init__.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/__init__.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/test_ckan.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/test_configuration.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/test_locations.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/utilities/__init__.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/utilities/test_filestore_helper.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/utilities/test_hdx_error_handler.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/api/utilities/test_url_utils.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/conftest.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/__init__.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_dataset_resource_generation.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_update_dataset_resources.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/data/test_update_logic.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/facades/__init__.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/facades/test_infer_arguments.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/facades/test_keyword_arguments.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/tests/hdx/facades/test_simple.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/workingexample/my_code.py +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/workingexample/my_resource.csv +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/workingexample/my_resource.xlsx +0 -0
- {hdx_python_api-6.6.7 → hdx_python_api-6.6.9}/workingexample/run.py +0 -0
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: hdx-python-api
|
|
3
|
-
Version: 6.6.
|
|
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.
|
|
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.
|
|
32
|
-
Requires-Dist: hdx-python-utilities>=4.
|
|
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.
|
|
39
|
+
"ckanapi>=4.11",
|
|
40
40
|
"defopt>=7.0.0",
|
|
41
41
|
"email_validator",
|
|
42
|
-
"hdx-python-country>=4.1.
|
|
43
|
-
"hdx-python-utilities>=4.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (6, 6,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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"]:
|