diff --git a/pyDataverse/api.py b/pyDataverse/api.py index efd3186..23b0493 100644 --- a/pyDataverse/api.py +++ b/pyDataverse/api.py @@ -167,24 +167,28 @@ def post_request(self, url, data=None, auth=False, params=None, files=None): if self.api_token: params["key"] = self.api_token + if isinstance(data, str): data = json.loads(data) + # Decide whether to use 'data' or 'json' args + request_params = self._check_json_data_form(data) + if self.client is None: return self._sync_request( method=httpx.post, url=url, - json=data, params=params, files=files, + **request_params ) else: return self._async_request( method=self.client.post, url=url, - json=data, params=params, files=files, + **request_params, ) def put_request(self, url, data=None, auth=False, params=None): @@ -216,19 +220,22 @@ def put_request(self, url, data=None, auth=False, params=None): if isinstance(data, str): data = json.loads(data) + # Decide whether to use 'data' or 'json' args + request_params = self._check_json_data_form(data) + if self.client is None: return self._sync_request( method=httpx.put, url=url, - json=data, params=params, + **request_params, ) else: return self._async_request( method=self.client.put, url=url, - json=data, params=params, + **request_params, ) def delete_request(self, url, auth=False, params=None): @@ -268,6 +275,33 @@ def delete_request(self, url, auth=False, params=None): params=params, ) + @staticmethod + def _check_json_data_form(data: Optional[Dict]): + """This method checks and distributes given payload to match Dataverse expectations. + + In the case of the form-data keyed by "jsonData", Dataverse expects + the payload as a string in a form of a dictionary. This is not possible + using HTTPXs json parameter, so we need to handle this case separately. + """ + + if not data: + return {} + elif not isinstance(data, dict): + raise ValueError("Data must be a dictionary.") + elif "jsonData" not in data: + return {"json": data} + + assert list(data.keys()) == ["jsonData"], ( + "jsonData must be the only key in the dictionary." + ) + + # Content of JSON data should ideally be a string + content = data["jsonData"] + if not isinstance(content, str): + data["jsonData"] = json.dumps(content) + + return {"data": data} + def _sync_request( self, method, diff --git a/tests/api/test_upload.py b/tests/api/test_upload.py index 34af3a4..4b8f5f2 100644 --- a/tests/api/test_upload.py +++ b/tests/api/test_upload.py @@ -141,6 +141,7 @@ def test_file_replacement(self): "description": "My description.", "categories": ["Data"], "forceReplace": False, + "directoryLabel": "some/other", } response = api.replace_datafile( @@ -152,12 +153,17 @@ def test_file_replacement(self): # Assert replaced_id = self._get_file_id(BASE_URL, API_TOKEN, pid) + file_metadata = self._get_file_metadata(BASE_URL, API_TOKEN, replaced_id) + data_file = file_metadata["dataFile"] replaced_content = self._fetch_datafile_content( BASE_URL, API_TOKEN, replaced_id, ) + assert data_file["description"] == "My description.", "Description does not match." + assert data_file["categories"] == ["Data"], "Categories do not match." + assert file_metadata["directoryLabel"] == "some/other", "Directory label does not match." assert response.status_code == 200, "File replacement failed." assert ( replaced_content == mutated @@ -246,3 +252,30 @@ def _fetch_datafile_content( response.raise_for_status() return response.content.decode("utf-8") + + + @staticmethod + def _get_file_metadata( + BASE_URL: str, + API_TOKEN: str, + id: str, + ): + """ + Retrieves the metadata for a file in Dataverse. + + Args: + BASE_URL (str): The base URL of the Dataverse instance. + API_TOKEN (str): The API token for authentication. + id (str): The ID of the file. + + Returns: + dict: The metadata for the file. + """ + response = httpx.get( + url=f"{BASE_URL}/api/files/{id}", + headers={"X-Dataverse-key": API_TOKEN}, + ) + + response.raise_for_status() + + return response.json()["data"]