hdx-python-api 6.7.1__tar.gz → 6.7.2__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 (149) hide show
  1. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/PKG-INFO +1 -1
  2. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/documentation/index.md +35 -0
  3. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/_version.py +2 -2
  4. hdx_python_api-6.7.2/src/hdx/data/activity.py +245 -0
  5. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/dataset.py +27 -0
  6. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/location.py +23 -0
  7. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/organization.py +23 -0
  8. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/user.py +25 -0
  9. hdx_python_api-6.7.2/tests/hdx/data/test_activity.py +196 -0
  10. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/.github/workflows/publish.yaml +0 -0
  11. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/.github/workflows/run-python-tests.yaml +0 -0
  12. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/.gitignore +0 -0
  13. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/.pre-commit-config.yaml +0 -0
  14. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/CLAUDE.md +0 -0
  15. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/LICENSE +0 -0
  16. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/README.md +0 -0
  17. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/documentation/.readthedocs.yaml +0 -0
  18. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/documentation/mkdocs.yaml +0 -0
  19. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/pyproject.toml +0 -0
  20. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/__init__.py +0 -0
  21. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/configuration.py +0 -0
  22. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/hdx_base_configuration.yaml +0 -0
  23. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/locations.py +0 -0
  24. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/remotehdx.py +0 -0
  25. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/utilities/__init__.py +0 -0
  26. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/utilities/date_helper.py +0 -0
  27. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/utilities/filestore_helper.py +0 -0
  28. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/utilities/hdx_error_handler.py +0 -0
  29. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/utilities/hdx_state.py +0 -0
  30. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/api/utilities/url_utils.py +0 -0
  31. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/__init__.py +0 -0
  32. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/hdxobject.py +0 -0
  33. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/resource.py +0 -0
  34. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/resource_matcher.py +0 -0
  35. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/resource_view.py +0 -0
  36. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/showcase.py +0 -0
  37. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/data/vocabulary.py +0 -0
  38. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/facades/__init__.py +0 -0
  39. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/facades/infer_arguments.py +0 -0
  40. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/facades/keyword_arguments.py +0 -0
  41. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/src/hdx/facades/simple.py +0 -0
  42. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/__init__.py +0 -0
  43. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/Accepted_Tags.csv +0 -0
  44. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/CKAN/hdx_dataset_static.yaml +0 -0
  45. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/Tag_Mapping.csv +0 -0
  46. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/Tag_Mapping_ChainRuleError.csv +0 -0
  47. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/empty.yaml +0 -0
  48. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_base_config.json +0 -0
  49. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_base_config.yaml +0 -0
  50. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_config.json +0 -0
  51. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_config.yaml +0 -0
  52. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_dataset_static.json +0 -0
  53. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_dataset_static.yaml +0 -0
  54. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_datasource_topline.json +0 -0
  55. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_datasource_topline.yaml +0 -0
  56. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_email_configuration.json +0 -0
  57. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_email_configuration.yaml +0 -0
  58. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_missing_site_config.json +0 -0
  59. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_organization_static.json +0 -0
  60. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_organization_static.yaml +0 -0
  61. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_resource_static.json +0 -0
  62. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_resource_static.yaml +0 -0
  63. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_resource_view_static.json +0 -0
  64. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_resource_view_static.yaml +0 -0
  65. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_showcase_static.json +0 -0
  66. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_showcase_static.yaml +0 -0
  67. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_user_static.json +0 -0
  68. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_user_static.yaml +0 -0
  69. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_vocabulary_static.json +0 -0
  70. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/hdx_vocabulary_static.yaml +0 -0
  71. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/project_configuration.json +0 -0
  72. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/project_configuration.yaml +0 -0
  73. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/user_agent_config.yaml +0 -0
  74. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/user_agent_config2.yaml +0 -0
  75. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/user_agent_config3.yaml +0 -0
  76. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/config/user_agent_config_wrong.yaml +0 -0
  77. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/dataset_search_results.yaml +0 -0
  78. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/download_gen_resource/conflict_data_alg.csv +0 -0
  79. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/download_gen_resource/min_qc_conflict_data_alg.csv +0 -0
  80. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/download_gen_resource/qc_conflict_data_alg.csv +0 -0
  81. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/download_gen_resource/test_data_no_data.csv +0 -0
  82. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/download_gen_resource/test_data_no_years.csv +0 -0
  83. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/empty.csv +0 -0
  84. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/gen_resource/conflict_data_alg.csv +0 -0
  85. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/gen_resource/min_qc_conflict_data_alg.csv +0 -0
  86. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/gen_resource/qc_conflict_data_alg.csv +0 -0
  87. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/gen_resource/test_data_no_data.csv +0 -0
  88. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/gen_resource/test_data_no_years.csv +0 -0
  89. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/location/location_list_all_fields.yaml +0 -0
  90. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/location/location_show_results.yaml +0 -0
  91. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/organization/organization_list_all_fields.yaml +0 -0
  92. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/organization/organization_show_results.yaml +0 -0
  93. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/qc_from_rows/qc_conflict_data_alg.csv +0 -0
  94. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/qc_from_rows/qc_conflict_data_alg_one_col.csv +0 -0
  95. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/resource_formats.json +0 -0
  96. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/showcase_all_search_results.yaml +0 -0
  97. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/size_hash/ACLED-All-Africa-File_20170101-to-20170708.xlsx +0 -0
  98. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/state/analysis_dates.txt +0 -0
  99. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/state/last_build_date.txt +0 -0
  100. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/test_data.csv +0 -0
  101. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/test_data.zip +0 -0
  102. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/dem_data_zwe.csv +0 -0
  103. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/dem_indicatorlist_zwe.csv +0 -0
  104. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/expected_resources_to_update.json +0 -0
  105. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/opri_data_zwe.csv +0 -0
  106. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/opri_indicatorlist_zwe.csv +0 -0
  107. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/opri_metadata_zwe.csv +0 -0
  108. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/qc_sdg_data_zwe.csv +0 -0
  109. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/sdg_data_zwe.csv +0 -0
  110. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/sdg_indicatorlist_zwe.csv +0 -0
  111. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/sdg_metadata_zwe.csv +0 -0
  112. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/unesco_dataset.json +0 -0
  113. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_dataset_resources/unesco_update_dataset.json +0 -0
  114. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_logic/update_logic_resources.yaml +0 -0
  115. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/fixtures/update_logic/update_logic_resources_new.yaml +0 -0
  116. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/__init__.py +0 -0
  117. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/api/__init__.py +0 -0
  118. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/api/test_ckan.py +0 -0
  119. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/api/test_configuration.py +0 -0
  120. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/api/test_locations.py +0 -0
  121. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/api/utilities/__init__.py +0 -0
  122. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/api/utilities/test_filestore_helper.py +0 -0
  123. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/api/utilities/test_hdx_error_handler.py +0 -0
  124. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/api/utilities/test_hdx_state.py +0 -0
  125. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/api/utilities/test_url_utils.py +0 -0
  126. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/conftest.py +0 -0
  127. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/__init__.py +0 -0
  128. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_dataset_add_hapi_error.py +0 -0
  129. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_dataset_core.py +0 -0
  130. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_dataset_noncore.py +0 -0
  131. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_dataset_resource_generation.py +0 -0
  132. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_location.py +0 -0
  133. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_organization.py +0 -0
  134. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_resource.py +0 -0
  135. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_resource_view.py +0 -0
  136. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_showcase.py +0 -0
  137. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_update_dataset_resources.py +0 -0
  138. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_update_logic.py +0 -0
  139. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_user.py +0 -0
  140. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/data/test_vocabulary.py +0 -0
  141. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/facades/__init__.py +0 -0
  142. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/facades/test_infer_arguments.py +0 -0
  143. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/facades/test_keyword_arguments.py +0 -0
  144. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/tests/hdx/facades/test_simple.py +0 -0
  145. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/uv.lock +0 -0
  146. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/workingexample/my_code.py +0 -0
  147. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/workingexample/my_resource.csv +0 -0
  148. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/workingexample/my_resource.xlsx +0 -0
  149. {hdx_python_api-6.7.1 → hdx_python_api-6.7.2}/workingexample/run.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hdx-python-api
3
- Version: 6.7.1
3
+ Version: 6.7.2
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
@@ -35,6 +35,7 @@ upload your datasets to HDX.
35
35
  - [Organization Management](#organization-management)
36
36
  - [Location Management](#location-management)
37
37
  - [Vocabulary Management](#vocabulary-management)
38
+ - [Activity Streams](#activity-streams)
38
39
  - [Pipeline State](#pipeline-state)
39
40
  - [Working Examples](#working-examples)
40
41
  - [Project Framework](#project-framework)
@@ -1188,6 +1189,40 @@ If you want to add a list of tags, you do it as follows:
1188
1189
 
1189
1190
  vocabulary.add_tags(["TAG","TAG","TAG"...])
1190
1191
 
1192
+ ## Activity Streams
1193
+
1194
+ The **Activity** class wraps CKAN activities, which record who changed what and when.
1195
+ Activities are read only. You can get the activity stream of a dataset, organization,
1196
+ location or user:
1197
+
1198
+ activities = dataset.get_activities(**kwargs)
1199
+ activities = organization.get_activities(**kwargs)
1200
+ activities = location.get_activities(**kwargs)
1201
+ activities = user.get_activities(**kwargs)
1202
+
1203
+ These return a list of **Activity** objects, each with fields like `timestamp`,
1204
+ `user_id`, `object_id` and `activity_type` (eg. "changed package"). One page of results
1205
+ is returned. Its size is set by `limit` (default 31, HDX maximum 100) and where it starts
1206
+ by `offset`. Passing `all_pages=True` gets every page. Other arguments (`**kwargs`) like
1207
+ `after`, `before` and `activity_types` depend on the object type and are detailed in the
1208
+ API documentation. Getting a user's activity stream requires authorisation to view the
1209
+ user's profile.
1210
+
1211
+ You can get the activity stream of all recently added or changed datasets:
1212
+
1213
+ activities = Activity.get_recently_changed_packages(limit=10)
1214
+
1215
+ With `all_pages=True`, this stream covers the whole history of HDX so it is capped by
1216
+ `max_activities` (default 1000). Pass `max_activities=None` to remove the cap.
1217
+
1218
+ You can read a single activity, get its data (including a snapshot of the object at the
1219
+ time of the activity) or get a diff against the object's previous version. HDX requires
1220
+ permission to edit the object the activity relates to for these:
1221
+
1222
+ activity = Activity.read_from_hdx("ACTIVITY_ID")
1223
+ data = activity.get_data()
1224
+ diff = activity.get_diff(diff_type="unified") # or "context" or "html"
1225
+
1191
1226
  ## Pipeline State
1192
1227
 
1193
1228
  The HDXState class allows the reading and writing of state to a given dataset. Input
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '6.7.1'
22
- __version_tuple__ = version_tuple = (6, 7, 1)
21
+ __version__ = version = '6.7.2'
22
+ __version_tuple__ = version_tuple = (6, 7, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,245 @@
1
+ """Activity class containing all logic for reading activity streams."""
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 HDXError, HDXObject
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Activity(HDXObject):
14
+ """Activity class containing all logic for reading activity streams. Activities
15
+ are read only.
16
+
17
+ Args:
18
+ initial_data: Initial activity 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": "activity_show",
40
+ "data_show": "activity_data_show",
41
+ "diff": "activity_diff",
42
+ "package_list": "package_activity_list",
43
+ "organization_list": "organization_activity_list",
44
+ "group_list": "group_activity_list",
45
+ "user_list": "user_activity_list",
46
+ "recently_changed_packages": "recently_changed_packages_activity_list",
47
+ }
48
+
49
+ @classmethod
50
+ def read_from_hdx(
51
+ cls, identifier: str, configuration: Configuration | None = None
52
+ ) -> Optional["Activity"]:
53
+ """Reads the activity given by identifier from HDX and returns Activity object.
54
+ HDX requires permission to edit the object the activity relates to.
55
+
56
+ Args:
57
+ identifier: Identifier of activity
58
+ configuration: HDX configuration. Defaults to global configuration.
59
+
60
+ Returns:
61
+ Activity object if successful read, None if not
62
+ """
63
+ return cls._read_from_hdx_class("activity", identifier, configuration)
64
+
65
+ def get_object_type(self) -> str:
66
+ """Get the CKAN object type the activity relates to eg. package,
67
+ organization, group or user. This is the last word of the activity type
68
+ (eg. "changed package") with "dataset" (from "follow dataset") mapped to
69
+ "package".
70
+
71
+ Returns:
72
+ Object type
73
+ """
74
+ object_type = self.data["activity_type"].split(" ")[-1]
75
+ if object_type == "dataset":
76
+ return "package"
77
+ return object_type
78
+
79
+ def get_data(self) -> dict:
80
+ """Get the data of the activity which includes a snapshot of the object
81
+ at the time of the activity. HDX requires permission to edit the object
82
+ the activity relates to.
83
+
84
+ Returns:
85
+ Activity data
86
+ """
87
+ return self._write_to_hdx("data_show", {"id": self.data["id"]}, "id")
88
+
89
+ def get_diff(self, diff_type: str = "unified") -> dict:
90
+ """Get a diff of the object compared to its previous version. HDX requires
91
+ permission to edit the object the activity relates to.
92
+
93
+ Args:
94
+ diff_type: One of unified, context or html. Defaults to unified.
95
+
96
+ Returns:
97
+ Activity diff
98
+ """
99
+ data = {
100
+ "id": self.data["id"],
101
+ "object_type": self.get_object_type(),
102
+ "diff_type": diff_type,
103
+ }
104
+ return self._write_to_hdx("diff", data, "id")
105
+
106
+ @classmethod
107
+ def _get_list(
108
+ cls,
109
+ action: str,
110
+ data: dict,
111
+ configuration: Configuration | None = None,
112
+ all_pages: bool = False,
113
+ max_activities: int | None = None,
114
+ ) -> list["Activity"]:
115
+ """Helper method to get a list of activities from HDX
116
+
117
+ Args:
118
+ action: Action to perform
119
+ data: Data to pass to CKAN including any offset and limit
120
+ configuration: HDX configuration. Defaults to global configuration.
121
+ all_pages: Whether to get all pages of results. Defaults to False.
122
+ max_activities: Maximum number of activities if all_pages is True. Defaults to None (no maximum).
123
+
124
+ Returns:
125
+ List of Activity objects
126
+ """
127
+ activity = cls(configuration=configuration)
128
+ if not all_pages:
129
+ result = activity._write_to_hdx(action, data)
130
+ return [cls(x, configuration=configuration) for x in result]
131
+ data = dict(data)
132
+ limit = data.setdefault("limit", 100)
133
+ offset = data.get("offset", 0)
134
+ activities = []
135
+ while True:
136
+ data["offset"] = offset
137
+ result = activity._write_to_hdx(action, data)
138
+ activities.extend(cls(x, configuration=configuration) for x in result)
139
+ if max_activities is not None and len(activities) >= max_activities:
140
+ return activities[:max_activities]
141
+ if len(result) < limit:
142
+ return activities
143
+ offset += limit
144
+
145
+ @classmethod
146
+ def get_activities(
147
+ cls,
148
+ object_type: str,
149
+ identifier: str,
150
+ configuration: Configuration | None = None,
151
+ all_pages: bool = False,
152
+ **kwargs: Any,
153
+ ) -> list["Activity"]:
154
+ """Get the activity stream of a CKAN object. One page of results (whose
155
+ size is given by limit) is returned unless all_pages is True.
156
+
157
+ Args:
158
+ object_type: One of package, organization, group or user
159
+ identifier: Id or name of object
160
+ configuration: HDX configuration. Defaults to global configuration.
161
+ all_pages: Whether to get all pages of results. Defaults to False.
162
+ **kwargs: See below
163
+ offset (int): Where to start getting activity items from. Defaults to 0.
164
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
165
+ after (int | str): Only activities after this timestamp (package and user only)
166
+ before (int | str): Only activities before this timestamp (package and user only)
167
+ include_hidden_activity (bool): Include hidden activity, sysadmins only (package, organization and group only)
168
+ activity_types (list): Activity types to include (package only)
169
+ exclude_activity_types (list): Activity types to exclude (package only)
170
+
171
+ Returns:
172
+ List of Activity objects
173
+ """
174
+ if not identifier:
175
+ raise HDXError(f"No id or name for {object_type} to get activities!")
176
+ data = {"id": identifier}
177
+ data.update(kwargs)
178
+ return cls._get_list(f"{object_type}_list", data, configuration, all_pages)
179
+
180
+ @classmethod
181
+ def get_recently_changed_packages(
182
+ cls,
183
+ configuration: Configuration | None = None,
184
+ all_pages: bool = False,
185
+ max_activities: int | None = 1000,
186
+ **kwargs: Any,
187
+ ) -> list["Activity"]:
188
+ """Get the activity stream of all recently added or changed datasets.
189
+ One page of results (whose size is given by limit) is returned unless
190
+ all_pages is True. all_pages is capped by max_activities because the
191
+ stream covers the whole history of HDX.
192
+
193
+ Args:
194
+ configuration: HDX configuration. Defaults to global configuration.
195
+ all_pages: Whether to get all pages of results. Defaults to False.
196
+ max_activities: Maximum number of activities if all_pages is True. Defaults to 1000. None means no maximum (very slow).
197
+ **kwargs: See below
198
+ offset (int): Where to start getting activity items from. Defaults to 0.
199
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
200
+
201
+ Returns:
202
+ List of Activity objects
203
+ """
204
+ return cls._get_list(
205
+ "recently_changed_packages",
206
+ kwargs,
207
+ configuration,
208
+ all_pages,
209
+ max_activities,
210
+ )
211
+
212
+ def check_required_fields(self, ignore_fields: Sequence[str] = ()) -> None:
213
+ """Activities are read only so this raises an exception
214
+
215
+ Args:
216
+ ignore_fields: Fields to ignore. Default is ().
217
+
218
+ Returns:
219
+ None
220
+ """
221
+ raise HDXError("Activities are read only!")
222
+
223
+ def update_in_hdx(self, **kwargs: Any) -> None:
224
+ """Activities are read only so this raises an exception
225
+
226
+ Returns:
227
+ None
228
+ """
229
+ raise HDXError("Activities are read only!")
230
+
231
+ def create_in_hdx(self, **kwargs: Any) -> None:
232
+ """Activities are read only so this raises an exception
233
+
234
+ Returns:
235
+ None
236
+ """
237
+ raise HDXError("Activities are read only!")
238
+
239
+ def delete_from_hdx(self) -> None:
240
+ """Activities are read only so this raises an exception
241
+
242
+ Returns:
243
+ None
244
+ """
245
+ raise HDXError("Activities are read only!")
@@ -40,6 +40,7 @@ from hdx.api.configuration import Configuration
40
40
  from hdx.api.locations import Locations
41
41
  from hdx.api.utilities.date_helper import DateHelper
42
42
  from hdx.api.utilities.filestore_helper import FilestoreHelper
43
+ from hdx.data.activity import Activity
43
44
  from hdx.data.hdxobject import HDXError, HDXObject
44
45
  from hdx.data.resource_matcher import ResourceMatcher
45
46
 
@@ -1428,6 +1429,32 @@ class Dataset(HDXObject):
1428
1429
  resources.append(resource)
1429
1430
  return resources
1430
1431
 
1432
+ def get_activities(self, all_pages: bool = False, **kwargs: Any) -> list[Activity]:
1433
+ """Get the activity stream of this dataset. One page of results (whose size
1434
+ is given by limit) is returned unless all_pages is True.
1435
+
1436
+ Args:
1437
+ all_pages: Whether to get all pages of results. Defaults to False.
1438
+ **kwargs: See below
1439
+ offset (int): Where to start getting activity items from. Defaults to 0.
1440
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
1441
+ after (int | str): Only activities after this timestamp
1442
+ before (int | str): Only activities before this timestamp
1443
+ include_hidden_activity (bool): Include hidden activity (sysadmins only)
1444
+ activity_types (list): Activity types to include
1445
+ exclude_activity_types (list): Activity types to exclude
1446
+
1447
+ Returns:
1448
+ List of Activity objects
1449
+ """
1450
+ return Activity.get_activities(
1451
+ "package",
1452
+ self.data.get("id") or self.data.get("name"),
1453
+ self.configuration,
1454
+ all_pages,
1455
+ **kwargs,
1456
+ )
1457
+
1431
1458
  @classmethod
1432
1459
  def autocomplete(
1433
1460
  cls,
@@ -5,6 +5,7 @@ from collections.abc import Sequence
5
5
  from typing import Any, Optional
6
6
 
7
7
  from hdx.api.configuration import Configuration
8
+ from hdx.data.activity import Activity
8
9
  from hdx.data.hdxobject import HDXObject
9
10
 
10
11
  logger = logging.getLogger(__name__)
@@ -217,6 +218,28 @@ class Location(HDXObject):
217
218
  if len(group["name"]) == 3 and group.get("data_completeness") == "active"
218
219
  )
219
220
 
221
+ def get_activities(self, all_pages: bool = False, **kwargs: Any) -> list[Activity]:
222
+ """Get the activity stream of this location. One page of results (whose size
223
+ is given by limit) is returned unless all_pages is True.
224
+
225
+ Args:
226
+ all_pages: Whether to get all pages of results. Defaults to False.
227
+ **kwargs: See below
228
+ offset (int): Where to start getting activity items from. Defaults to 0.
229
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
230
+ include_hidden_activity (bool): Include hidden activity (sysadmins only)
231
+
232
+ Returns:
233
+ List of Activity objects
234
+ """
235
+ return Activity.get_activities(
236
+ "group",
237
+ self.data.get("id") or self.data.get("name"),
238
+ self.configuration,
239
+ all_pages,
240
+ **kwargs,
241
+ )
242
+
220
243
  @classmethod
221
244
  def autocomplete(
222
245
  cls,
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union
8
8
  import hdx.data.dataset
9
9
  import hdx.data.user as user_module
10
10
  from hdx.api.configuration import Configuration
11
+ from hdx.data.activity import Activity
11
12
  from hdx.data.hdxobject import HDXError, HDXObject
12
13
 
13
14
  if TYPE_CHECKING:
@@ -311,6 +312,28 @@ class Organization(HDXObject):
311
312
 
312
313
  return final_orgs
313
314
 
315
+ def get_activities(self, all_pages: bool = False, **kwargs: Any) -> list[Activity]:
316
+ """Get the activity stream of this organization. One page of results (whose size
317
+ is given by limit) is returned unless all_pages is True.
318
+
319
+ Args:
320
+ all_pages: Whether to get all pages of results. Defaults to False.
321
+ **kwargs: See below
322
+ offset (int): Where to start getting activity items from. Defaults to 0.
323
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
324
+ include_hidden_activity (bool): Include hidden activity (sysadmins only)
325
+
326
+ Returns:
327
+ List of Activity objects
328
+ """
329
+ return Activity.get_activities(
330
+ "organization",
331
+ self.data.get("id") or self.data.get("name"),
332
+ self.configuration,
333
+ all_pages,
334
+ **kwargs,
335
+ )
336
+
314
337
  @classmethod
315
338
  def autocomplete(
316
339
  cls,
@@ -7,6 +7,7 @@ from typing import Any, Optional
7
7
 
8
8
  import hdx.data.organization
9
9
  from hdx.api.configuration import Configuration
10
+ from hdx.data.activity import Activity
10
11
  from hdx.data.hdxobject import HDXError, HDXObject
11
12
 
12
13
  logger = logging.getLogger(__name__)
@@ -453,6 +454,30 @@ class User(HDXObject):
453
454
  return result
454
455
  return []
455
456
 
457
+ def get_activities(self, all_pages: bool = False, **kwargs: Any) -> list[Activity]:
458
+ """Get the activity stream of this user. One page of results (whose size
459
+ is given by limit) is returned unless all_pages is True. HDX requires
460
+ authorisation to view the user's profile.
461
+
462
+ Args:
463
+ all_pages: Whether to get all pages of results. Defaults to False.
464
+ **kwargs: See below
465
+ offset (int): Where to start getting activity items from. Defaults to 0.
466
+ limit (int): Maximum number of activities per page. Defaults to 31 (HDX maximum 100) or 100 if all_pages is True.
467
+ after (int | str): Only activities after this timestamp
468
+ before (int | str): Only activities before this timestamp
469
+
470
+ Returns:
471
+ List of Activity objects
472
+ """
473
+ return Activity.get_activities(
474
+ "user",
475
+ self.data.get("id") or self.data.get("name"),
476
+ self.configuration,
477
+ all_pages,
478
+ **kwargs,
479
+ )
480
+
456
481
  @classmethod
457
482
  def autocomplete(
458
483
  cls,
@@ -0,0 +1,196 @@
1
+ """Activity Tests"""
2
+
3
+ import json
4
+
5
+ import pytest
6
+
7
+ from .. import MockResponse
8
+ from hdx.api.configuration import Configuration
9
+ from hdx.data.activity import Activity
10
+ from hdx.data.dataset import Dataset
11
+ from hdx.data.hdxobject import HDXError
12
+ from hdx.data.location import Location
13
+ from hdx.data.organization import Organization
14
+ from hdx.data.user import User
15
+
16
+ activity_id = "7d18e25e-c004-4e9f-8f6b-a4b4fe8a0159"
17
+ activity = {
18
+ "id": activity_id,
19
+ "timestamp": "2026-10-14T01:49:59.740998",
20
+ "user_id": "52c8d2f3-a0d6-4cd9-8765-cf5cb4b67427",
21
+ "object_id": "38772da7-c0b0-477e-a60e-a385a9dd4d64",
22
+ "activity_type": "changed package",
23
+ "permission_labels": ["public"],
24
+ }
25
+ activity_data = {"package": {"name": "test-dataset"}, "actor": "testuser"}
26
+ activity_diff = {"diff": "--- a\n+++ b\n", "activities": []}
27
+
28
+ total_activities = 250
29
+ activities = [
30
+ activity | {"id": f"activity{i}", "timestamp": str(i)}
31
+ for i in range(total_activities)
32
+ ]
33
+
34
+
35
+ def mockresponse(result, action):
36
+ return MockResponse(
37
+ 200,
38
+ f'{{"success": true, "result": {json.dumps(result)}, "help": "http://test-data.humdata.org/api/3/action/help_show?name={action}"}}',
39
+ )
40
+
41
+
42
+ def mocknotfound(action):
43
+ return MockResponse(
44
+ 404,
45
+ f'{{"success": false, "error": {{"message": "Not found", "__type": "Not Found Error"}}, "help": "http://test-data.humdata.org/api/3/action/help_show?name={action}"}}',
46
+ )
47
+
48
+
49
+ class TestActivity:
50
+ calls = []
51
+
52
+ @pytest.fixture(scope="function")
53
+ def post_activity(self):
54
+ TestActivity.calls = []
55
+
56
+ class MockSession:
57
+ @staticmethod
58
+ def post(url, data, **kwargs):
59
+ datadict = json.loads(data.decode("utf-8"))
60
+ action = url.split("/")[-1]
61
+ TestActivity.calls.append((action, datadict))
62
+ if action.endswith("_activity_list"):
63
+ if (
64
+ action != "recently_changed_packages_activity_list"
65
+ and datadict["id"] != "TEST1"
66
+ ):
67
+ return mocknotfound(action)
68
+ offset = datadict.get("offset", 0)
69
+ limit = datadict.get("limit", 31)
70
+ return mockresponse(activities[offset : offset + limit], action)
71
+ if datadict["id"] != activity_id:
72
+ return mocknotfound(action)
73
+ if action == "activity_show":
74
+ return mockresponse(activity, action)
75
+ if action == "activity_data_show":
76
+ return mockresponse(activity_data, action)
77
+ if action == "activity_diff":
78
+ return mockresponse(activity_diff, action)
79
+ return mocknotfound(action)
80
+
81
+ Configuration.read().remoteckan().session = MockSession()
82
+
83
+ def test_read_from_hdx(self, configuration, post_activity):
84
+ result = Activity.read_from_hdx(activity_id)
85
+ assert result.data == activity
86
+ assert Activity.read_from_hdx("TEST2") is None
87
+
88
+ def test_get_object_type(self, configuration):
89
+ assert Activity(activity).get_object_type() == "package"
90
+ result = Activity({"activity_type": "new organization"}).get_object_type()
91
+ assert result == "organization"
92
+ result = Activity({"activity_type": "follow dataset"}).get_object_type()
93
+ assert result == "package"
94
+
95
+ def test_get_data_and_diff(self, configuration, post_activity):
96
+ result = Activity(activity)
97
+ assert result.get_data() == activity_data
98
+ assert result.get_diff() == activity_diff
99
+ assert TestActivity.calls[-1] == (
100
+ "activity_diff",
101
+ {"id": activity_id, "object_type": "package", "diff_type": "unified"},
102
+ )
103
+ result.get_diff("html")
104
+ assert TestActivity.calls[-1][1]["diff_type"] == "html"
105
+ result = Activity({"id": "TEST2", "activity_type": "changed package"})
106
+ with pytest.raises(HDXError):
107
+ result.get_data()
108
+ with pytest.raises(HDXError):
109
+ result.get_diff()
110
+
111
+ def test_get_activities(self, configuration, post_activity):
112
+ result = Activity.get_activities("package", "TEST1")
113
+ assert len(result) == 31
114
+ assert isinstance(result[0], Activity)
115
+ assert result[0]["id"] == "activity0"
116
+ result = Activity.get_activities("package", "TEST1", offset=10, limit=5)
117
+ assert [x["id"] for x in result] == [f"activity{i}" for i in range(10, 15)]
118
+ assert TestActivity.calls[-1] == (
119
+ "package_activity_list",
120
+ {"id": "TEST1", "offset": 10, "limit": 5},
121
+ )
122
+ with pytest.raises(HDXError):
123
+ Activity.get_activities("package", "TEST2")
124
+ with pytest.raises(HDXError):
125
+ Activity.get_activities("package", None)
126
+
127
+ def test_get_activities_all_pages(self, configuration, post_activity):
128
+ result = Activity.get_activities("package", "TEST1", all_pages=True)
129
+ assert [x["id"] for x in result] == [x["id"] for x in activities]
130
+ assert [x[1]["offset"] for x in TestActivity.calls] == [0, 100, 200]
131
+ TestActivity.calls = []
132
+ result = Activity.get_activities(
133
+ "package", "TEST1", all_pages=True, offset=150, limit=50
134
+ )
135
+ assert [x["id"] for x in result] == [x["id"] for x in activities[150:]]
136
+ assert [x[1]["offset"] for x in TestActivity.calls] == [150, 200, 250]
137
+
138
+ def test_get_recently_changed_packages(self, configuration, post_activity):
139
+ result = Activity.get_recently_changed_packages(limit=3)
140
+ assert [x["id"] for x in result] == ["activity0", "activity1", "activity2"]
141
+ assert TestActivity.calls[-1] == (
142
+ "recently_changed_packages_activity_list",
143
+ {"limit": 3},
144
+ )
145
+ TestActivity.calls = []
146
+ result = Activity.get_recently_changed_packages(all_pages=True)
147
+ assert len(result) == total_activities
148
+ assert [x[1]["offset"] for x in TestActivity.calls] == [0, 100, 200]
149
+ TestActivity.calls = []
150
+ result = Activity.get_recently_changed_packages(
151
+ all_pages=True, max_activities=120
152
+ )
153
+ assert [x["id"] for x in result] == [x["id"] for x in activities[:120]]
154
+ assert [x[1]["offset"] for x in TestActivity.calls] == [0, 100]
155
+ result = Activity.get_recently_changed_packages(
156
+ all_pages=True, max_activities=100
157
+ )
158
+ assert len(result) == 100
159
+ result = Activity.get_recently_changed_packages(
160
+ all_pages=True, max_activities=None, limit=50
161
+ )
162
+ assert len(result) == total_activities
163
+
164
+ @pytest.mark.parametrize(
165
+ "hdxobjectclass,action",
166
+ [
167
+ (Dataset, "package_activity_list"),
168
+ (Organization, "organization_activity_list"),
169
+ (Location, "group_activity_list"),
170
+ (User, "user_activity_list"),
171
+ ],
172
+ )
173
+ def test_hdxobject_get_activities(
174
+ self, configuration, post_activity, hdxobjectclass, action
175
+ ):
176
+ hdxobject = hdxobjectclass({"name": "TEST1"})
177
+ result = hdxobject.get_activities(limit=2)
178
+ assert [x["id"] for x in result] == ["activity0", "activity1"]
179
+ assert isinstance(result[0], Activity)
180
+ assert TestActivity.calls[-1] == (action, {"id": "TEST1", "limit": 2})
181
+ hdxobject = hdxobjectclass({"id": "TEST1", "name": "TEST2"})
182
+ hdxobject.get_activities()
183
+ assert TestActivity.calls[-1] == (action, {"id": "TEST1"})
184
+ with pytest.raises(HDXError):
185
+ hdxobjectclass().get_activities()
186
+
187
+ def test_read_only(self, configuration):
188
+ result = Activity(activity)
189
+ with pytest.raises(HDXError):
190
+ result.check_required_fields()
191
+ with pytest.raises(HDXError):
192
+ result.create_in_hdx()
193
+ with pytest.raises(HDXError):
194
+ result.update_in_hdx()
195
+ with pytest.raises(HDXError):
196
+ result.delete_from_hdx()
File without changes
File without changes
File without changes
File without changes