diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1e60aa..47fa276 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.13 + python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9f9e095 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,57 @@ +# CLAUDE.md + +## Project Overview + +**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. + +## Source Layout + +- `src/hdx/api/` — Configuration, session management, locations, utilities + - `configuration.py` — `Configuration` class (credentials, site URL, user agent) + - `remotehdx.py` — Low-level CKAN API client +- `src/hdx/data/` — Data model objects, all inheriting from `HDXObject` + - `dataset.py`, `resource.py`, `organization.py`, `user.py`, `vocabulary.py`, `showcase.py`, `resource_view.py` + - `hdxobject.py` — Base class providing `_read_from_hdx` / `_write_to_hdx` and CRUD scaffolding +- `src/hdx/facades/` — High-level entry-point helpers (`simple`, `keyword_arguments`, `infer_arguments`) +- `tests/hdx/` — Test suite mirroring src layout; fixtures in `tests/fixtures/` + +## Running Tests + +```bash +uv run pytest +``` + +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. + +Coverage is written to `coverage.lcov` and JUnit XML to `test-results.xml`. + +## Code Style + +Formatted and linted with `ruff` (rules: E, F, I, UP; line-length not enforced). Run before committing: + +```bash +pre-commit run --all-files +``` + +- Python ≥ 3.10 +- Type hints throughout; use `X | Y` union syntax (PEP 604), not `Optional`/`Union` +- Google-style docstrings with `Args:` and `Returns:` sections +- No inline comments unless the *why* is non-obvious + +## Key Patterns + +**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. + +**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()`. + +**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`). + +## Collaboration Style + +- Be objective, not agreeable. Act as a partner, not a sycophant. Push back when you disagree, flag tradeoffs honestly, and don't sugarcoat problems. +- Keep explanations brief and to the point. +- 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. + +## Scope of Changes + +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. diff --git a/pyproject.toml b/pyproject.toml index 6b68553..464d207 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dynamic = ["version"] requires-python = ">=3.10" dependencies = [ - "ckanapi>=4.8", + "ckanapi>=4.11", "defopt>=7.0.0", "email_validator", "hdx-python-country>=4.1.1", diff --git a/src/hdx/data/resource.py b/src/hdx/data/resource.py index 437dc77..9277de3 100755 --- a/src/hdx/data/resource.py +++ b/src/hdx/data/resource.py @@ -62,6 +62,9 @@ def actions() -> dict[str, str]: "search": "resource_search", "broken": "hdx_mark_broken_link_in_resource", "datastore_delete": "datastore_delete", + "datastore_create": "datastore_create", + "datastore_insert": "datastore_insert", + "datastore_upsert": "datastore_upsert", "datastore_search": "datastore_search", } @@ -701,6 +704,53 @@ def has_datastore(self) -> bool: return True return False + def create_datastore( + self, + schema: Sequence[dict], + primary_key: str | Sequence[str] | None = None, + ) -> None: + """Create a datastore for the resource with the given schema. + + Args: + schema: Sequence of field definitions, each a dict with 'id' and 'type' keys. + primary_key: Primary key field name(s). Defaults to None. + + Returns: + None + """ + data: dict = { + "resource_id": self.data["id"], + "force": True, + "fields": schema, + } + if primary_key is not None: + if not isinstance(primary_key, str): + primary_key = ",".join(primary_key) + data["primary_key"] = primary_key + self._write_to_hdx("datastore_create", data, "resource_id") + + def update_datastore( + self, + records: Sequence[dict], + method: str = "upsert", + ) -> None: + """Update (upsert) records into the resource datastore. + + Args: + records: Sequence of record dicts to insert or update. + method: Datastore update method ('upsert', 'insert', or 'update'). Defaults to 'upsert'. + + Returns: + None + """ + data = { + "resource_id": self.data["id"], + "force": True, + "method": method, + "records": records, + } + self._write_to_hdx("datastore_upsert", data, "resource_id") + def delete_datastore(self) -> None: """Delete a resource from the HDX datastore diff --git a/tests/hdx/api/utilities/test_hdx_state.py b/tests/hdx/api/utilities/test_hdx_state.py index ae90c93..e57f42c 100644 --- a/tests/hdx/api/utilities/test_hdx_state.py +++ b/tests/hdx/api/utilities/test_hdx_state.py @@ -47,7 +47,7 @@ def date2(self): def do_state(self, tempfolder, statefile): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): if "resource" in url: result = json.dumps(resultdict) return MockResponse( @@ -71,7 +71,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def do_state_multi(self, tempfolder, multidatestatefile): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): if "resource" in url: result = json.dumps(resultdict) return MockResponse( diff --git a/tests/hdx/data/test_dataset_add_hapi_error.py b/tests/hdx/data/test_dataset_add_hapi_error.py index 48e99a4..bc27aea 100644 --- a/tests/hdx/data/test_dataset_add_hapi_error.py +++ b/tests/hdx/data/test_dataset_add_hapi_error.py @@ -13,7 +13,7 @@ class TestDatasetAddHAPIError: def hapi_resource_update(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "show" in url: resource_id = datadict["id"] diff --git a/tests/hdx/data/test_dataset_core.py b/tests/hdx/data/test_dataset_core.py index c26d163..c814865 100755 --- a/tests/hdx/data/test_dataset_core.py +++ b/tests/hdx/data/test_dataset_core.py @@ -182,7 +182,7 @@ def static_json(self, configfolder): def read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return dataset_mockshow(url, datadict) @@ -192,7 +192,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_revise(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): if isinstance(data, dict): datadict = { k.decode("utf8"): v.decode("utf8") for k, v in data.items() @@ -219,7 +219,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_create(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): if isinstance(data, dict): datadict = { k.decode("utf8"): v.decode("utf8") for k, v in data.items() @@ -286,7 +286,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_update(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): if isinstance(data, dict): datadict = { k.decode("utf8"): v.decode("utf8") for k, v in data.items() @@ -363,7 +363,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_reorder(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "show" in url: @@ -390,7 +390,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_delete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "show" in url: @@ -423,7 +423,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def search(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return mocksearch(url, datadict) @@ -433,7 +433,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_list(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return mocklist(url, datadict) @@ -443,7 +443,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def all(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return mockall(url, datadict) @@ -453,7 +453,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_autocomplete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "autocomplete" not in url or "acled" not in datadict["q"]: diff --git a/tests/hdx/data/test_dataset_noncore.py b/tests/hdx/data/test_dataset_noncore.py index 6b1138d..726d9e8 100755 --- a/tests/hdx/data/test_dataset_noncore.py +++ b/tests/hdx/data/test_dataset_noncore.py @@ -52,7 +52,7 @@ def vocabulary_read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return vocabulary_mockshow(url, datadict) @@ -62,7 +62,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def user_read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return user_mockshow(url, datadict) @@ -72,7 +72,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def organization_read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return organization_mockshow(url, datadict) @@ -82,7 +82,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def showcase_read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "showcase_list" in url: result = json.dumps([showcase_resultdict]) @@ -111,7 +111,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def vocabulary_update(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): if isinstance(data, dict): datadict = { k.decode("utf8"): v.decode("utf8") for k, v in data.items() diff --git a/tests/hdx/data/test_organization.py b/tests/hdx/data/test_organization.py index 8e66c4e..2087ddb 100755 --- a/tests/hdx/data/test_organization.py +++ b/tests/hdx/data/test_organization.py @@ -115,7 +115,7 @@ def static_json(self, configfolder): def read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return organization_mockshow(url, datadict) @@ -125,7 +125,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_create(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "show" in url: return organization_mockshow(url, datadict) @@ -165,7 +165,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_update(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "show" in url: return organization_mockshow(url, datadict) @@ -205,7 +205,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_delete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "show" in url: @@ -232,7 +232,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_list(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): json.loads(data.decode("utf-8")) return mocklist(url) @@ -242,7 +242,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_all_fields(self, fixturesfolder): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): kwargs = json.loads(data.decode("utf-8")) if "show" in url: return organization_mockshow(url, kwargs) @@ -264,7 +264,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def user_read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return user_mockshow(url, datadict) @@ -274,7 +274,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def datasets_get(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return mockgetdatasets(url, datadict) @@ -284,7 +284,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_autocomplete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "autocomplete" not in url or "innago" not in datadict["q"]: diff --git a/tests/hdx/data/test_resource.py b/tests/hdx/data/test_resource.py index 1c4f488..dcc4281 100755 --- a/tests/hdx/data/test_resource.py +++ b/tests/hdx/data/test_resource.py @@ -359,7 +359,7 @@ def topline_json(self, configfolder): def read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return mockshow(url, datadict) @@ -369,7 +369,8 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_create(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): + files = kwargs.get("files") if isinstance(data, dict): datadict = { k.decode("utf8"): v.decode("utf8") for k, v in data.items() @@ -431,7 +432,8 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_update(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): + files = kwargs.get("files") if isinstance(data, dict): datadict = { k.decode("utf8"): v.decode("utf8") for k, v in data.items() @@ -491,7 +493,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_delete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "show" in url: @@ -518,7 +520,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_datastore(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "show" in url: @@ -574,11 +576,18 @@ def post(url, data, headers, files, allow_redirects, auth=None): '{"success": true, "result": {"include_total": true, "resource_id": "_table_metadata", "fields": [{"type": "int", "id": "_id"}, {"type": "name", "id": "name"}, {"type": "oid", "id": "oid"}, {"type": "name", "id": "alias_of"}], "records_format": "objects", "records": [{"_id":"f9cd60f3d7f2f6d0","name":"f9228459-d808-4b51-948f-68a5850abfde","oid":"919290","alias_of":null},{"_id":"7ae63490de9b7d7b","name":"af618a0b-09b8-42c8-836f-2be597e1ea34","oid":"135294","alias_of":null},{"_id":"1dc37f4e89988644","name":"748b40dd-7bd3-40a3-941b-e76f0bfbe0eb","oid":"117144","alias_of":null},{"_id":"2a554a61bd366206","name":"91c78d24-eab3-40b5-ba91-6b29bcda7178","oid":"116963","alias_of":null},{"_id":"fd787575143afe90","name":"9320cfce-4620-489a-bcbe-25c73867d4fc","oid":"107430","alias_of":null},{"_id":"a70093abd230f647","name":"b9d2eb36-e65c-417a-bc28-f4dadb149302","oid":"107409","alias_of":null},{"_id":"95fbdd2d06c07aea","name":"ca6a0891-8395-4d58-9168-6c44e17e0193","oid":"107385","alias_of":null}], "limit": 10000, "_links": {"start": "/api/action/datastore_search?limit=10000&resource_id=_table_metadata", "next": "/api/action/datastore_search?offset=10000&limit=10000&resource_id=_table_metadata"}, "total": 7}}', ) if ( - "create" in url - or "insert" in url - or "upsert" in url - or "search" in url - ) and datadict["resource_id"] == "de6549d8-268b-4dfe-adaf-a4ae5c8510d5": + "upsert" in url + and datadict["resource_id"] + == "de6549d8-268b-4dfe-adaf-a4ae5c8510d5" + ): + TestResource.datastore = "upsert" + return MockResponse( + 200, + '{"success": true, "result": {"method": "upsert", "resource_id": "de6549d8-268b-4dfe-adaf-a4ae5c8510d5"}, "help": "http://test-data.humdata.org/api/3/action/help_show?name=datastore_upsert"}', + ) + if ("create" in url or "insert" in url or "search" in url) and datadict[ + "resource_id" + ] == "de6549d8-268b-4dfe-adaf-a4ae5c8510d5": TestResource.datastore = "create" return MockResponse( 200, @@ -595,7 +604,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_patch(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "show" in url: return mockshow(url, datadict) @@ -607,7 +616,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_dataset(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return mockdataset(url, datadict) @@ -617,7 +626,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def search(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return mocksearch(url, datadict) @@ -627,7 +636,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_resourceview(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") return mockresourceview(url, decodedata) @@ -637,7 +646,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_broken(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): if isinstance(data, dict): datadict = { k.decode("utf8"): v.decode("utf8") for k, v in data.items() @@ -977,6 +986,29 @@ def test_datastore(self, configuration, post_datastore, topline_yaml, topline_js TestResource.datastore = None assert resource2.has_datastore() is False + schema = [{"id": "code", "type": "text"}, {"id": "value", "type": "float"}] + records = [{"code": "A", "value": 1.0}, {"code": "B", "value": 2.0}] + + TestResource.datastore = None + resource.create_datastore(schema, primary_key="code") + assert TestResource.datastore == "create" + + TestResource.datastore = None + resource.create_datastore(schema, primary_key=["code", "value"]) + assert TestResource.datastore == "create" + + TestResource.datastore = None + resource.create_datastore(schema) + assert TestResource.datastore == "create" + + TestResource.datastore = None + resource.update_datastore(records) + assert TestResource.datastore == "upsert" + + TestResource.datastore = None + resource.update_datastore(records, method="insert") + assert TestResource.datastore == "upsert" + def test_resource_views(self, configuration, post_resourceview): resource = Resource({"id": "25982d1c-f45a-45e1-b14e-87d367413045"}) with pytest.raises(HDXError): diff --git a/tests/hdx/data/test_resource_view.py b/tests/hdx/data/test_resource_view.py index 5e9a110..a473c95 100755 --- a/tests/hdx/data/test_resource_view.py +++ b/tests/hdx/data/test_resource_view.py @@ -136,7 +136,7 @@ def static_json(self, configfolder): def read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return resource_view_mockshow(url, datadict) @@ -146,7 +146,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_create(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "show" in url: return resource_view_mockshow(url, datadict) @@ -165,7 +165,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_update(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "show" in url: return resource_view_mockshow(url, datadict) @@ -207,7 +207,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_delete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "show" in url: @@ -234,7 +234,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_list(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return resource_view_mocklist(url, datadict) diff --git a/tests/hdx/data/test_showcase.py b/tests/hdx/data/test_showcase.py index 01ec043..b675bc8 100755 --- a/tests/hdx/data/test_showcase.py +++ b/tests/hdx/data/test_showcase.py @@ -185,7 +185,7 @@ def static_json(self, configfolder): def read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "association_delete" in url: TestShowcase.association = "delete" @@ -208,7 +208,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_create(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "vocabulary" in url: return vocabulary_mockshow(url, datadict) @@ -253,7 +253,7 @@ def post_update(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "vocabulary" in url: return vocabulary_mockshow(url, datadict) @@ -295,7 +295,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_delete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if url.endswith("show") or "list" in url: @@ -322,7 +322,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def allsearch(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return mockallsearch(url, datadict) diff --git a/tests/hdx/data/test_user.py b/tests/hdx/data/test_user.py index 3169749..3b09a8a 100755 --- a/tests/hdx/data/test_user.py +++ b/tests/hdx/data/test_user.py @@ -147,7 +147,7 @@ def static_json(self, configfolder): def read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return user_mockshow(url, datadict) @@ -157,7 +157,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_create(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "show" in url: return user_mockshow(url, datadict) @@ -197,7 +197,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_update(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "show" in url: return user_mockshow(url, datadict) @@ -237,7 +237,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_delete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "show" in url: @@ -264,7 +264,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def show_current_user(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): if "show" not in url: return MockResponse( 404, @@ -282,7 +282,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_list(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): json.loads(data.decode("utf-8")) return mocklist(url) @@ -292,7 +292,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_listorgs(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "user" in url: @@ -321,7 +321,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_check_current_user_write_access(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "user" in url: @@ -361,7 +361,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_listorgs_invalid(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "user" in url: @@ -379,7 +379,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_tokenlist(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "user" in url: @@ -402,7 +402,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_autocomplete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "autocomplete" not in url or "fake" not in datadict["q"]: diff --git a/tests/hdx/data/test_vocabulary.py b/tests/hdx/data/test_vocabulary.py index 1f0eca4..48ac0e4 100755 --- a/tests/hdx/data/test_vocabulary.py +++ b/tests/hdx/data/test_vocabulary.py @@ -1137,7 +1137,7 @@ def static_json(self, configfolder): def read(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return vocabulary_mockshow(url, datadict) @@ -1147,7 +1147,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_create(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "show" in url: return vocabulary_mockshow(url, datadict) @@ -1193,7 +1193,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_update(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) if "show" in url: return vocabulary_mockshow(url, datadict) @@ -1245,7 +1245,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_delete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return vocabulary_delete(url, datadict) @@ -1255,7 +1255,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_list(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): datadict = json.loads(data.decode("utf-8")) return vocabulary_mocklist(url, datadict) @@ -1265,7 +1265,7 @@ def post(url, data, headers, files, allow_redirects, auth=None): def post_autocomplete(self): class MockSession: @staticmethod - def post(url, data, headers, files, allow_redirects, auth=None): + def post(url, data, **kwargs): decodedata = data.decode("utf-8") datadict = json.loads(decodedata) if "autocomplete" not in url or "health" not in datadict["q"]: diff --git a/uv.lock b/uv.lock index 8b767c6..3a81534 100644 --- a/uv.lock +++ b/uv.lock @@ -170,7 +170,7 @@ wheels = [ [[package]] name = "ckanapi" -version = "4.9" +version = "4.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docopt" }, @@ -178,11 +178,10 @@ dependencies = [ { name = "requests" }, { name = "setuptools" }, { name = "simplejson" }, - { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/4b/c8e6ad9c549918c9503b59c7ce95fd49c211d862c82229df2034ec661c41/ckanapi-4.9.tar.gz", hash = "sha256:4d5d59da939586e9f34324c016f5cbea1f6ab4753f792d45d24379aa79ccbc06", size = 37717, upload-time = "2025-11-10T19:50:49.35Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/fd/53ad4c91cba8eb829262f723d38f63da07778e056f8dbe17547a90bf8591/ckanapi-4.11.tar.gz", hash = "sha256:913dc04e23e16dac9357d013eefda797f2ae5b7a5b584077c10b70d7336b467e", size = 43151, upload-time = "2026-03-20T14:51:19.806Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/83/ee5d03e4394a1bcb69215321ac6f116fb02554cee40b0390b9190cf052d5/ckanapi-4.9-py3-none-any.whl", hash = "sha256:845c066dd7bdfc768644aee50336058aafddc70b1db23649a48e05782032bf45", size = 46527, upload-time = "2025-11-10T19:50:47.123Z" }, + { url = "https://files.pythonhosted.org/packages/de/c9/c8e1c0e0188a60fc2dc77b7d5dec784ea7152edc0f3a79e162a08786ce5c/ckanapi-4.11-py3-none-any.whl", hash = "sha256:6625efd482ef969487dfcd3cc5d6c81d296cf240c7371b7daf5e7a5e430a9f2a", size = 50959, upload-time = "2026-03-20T14:51:18.208Z" }, ] [[package]] @@ -512,7 +511,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "ckanapi", specifier = ">=4.8" }, + { name = "ckanapi", specifier = ">=4.11" }, { name = "defopt", specifier = ">=7.0.0" }, { name = "email-validator" }, { name = "hdx-python-country", specifier = ">=4.1.1" },