diff --git a/.github/workflows/pytest-windows.yml b/.github/workflows/pytest-windows.yml index 6884749..34a557e 100644 --- a/.github/workflows/pytest-windows.yml +++ b/.github/workflows/pytest-windows.yml @@ -11,7 +11,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.10"] + python-version: ["3.11"] os: [windows-latest] steps: diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 3a32dfc..334600e 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -11,7 +11,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.8", "3.11"] + python-version: ["3.8", "3.12"] os: [ubuntu-20.04] steps: diff --git a/Makefile b/Makefile index 5033ebb..40d1dfe 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ run-coverage: coverage run -m pytest html-report: - coverage html + coverage html --omit="*/test*" open-coverage: cd htmlcov && google-chrome index.html diff --git a/pephubclient/constants.py b/pephubclient/constants.py index 27f22cb..26e8ed7 100644 --- a/pephubclient/constants.py +++ b/pephubclient/constants.py @@ -4,10 +4,10 @@ from pydantic import BaseModel, field_validator -# PEPHUB_BASE_URL = os.getenv( -# "PEPHUB_BASE_URL", default="https://pephub-api.databio.org/" -# ) -PEPHUB_BASE_URL = "http://0.0.0.0:8000/" +PEPHUB_BASE_URL = os.getenv( + "PEPHUB_BASE_URL", default="https://pephub-api.databio.org/" +) +# PEPHUB_BASE_URL = "http://0.0.0.0:8000/" PEPHUB_PEP_API_BASE_URL = f"{PEPHUB_BASE_URL}api/v1/projects/" PEPHUB_PEP_SEARCH_URL = f"{PEPHUB_BASE_URL}api/v1/namespaces/{{namespace}}/projects" PEPHUB_PUSH_URL = f"{PEPHUB_BASE_URL}api/v1/namespaces/{{namespace}}/projects/json" diff --git a/pephubclient/files_manager.py b/pephubclient/files_manager.py index 6331ed8..a3d9b56 100644 --- a/pephubclient/files_manager.py +++ b/pephubclient/files_manager.py @@ -6,7 +6,6 @@ import yaml import zipfile -from pephubclient.constants import RegistryPath from pephubclient.exceptions import PEPExistsError diff --git a/pephubclient/models.py b/pephubclient/models.py index b4a0172..2df7681 100644 --- a/pephubclient/models.py +++ b/pephubclient/models.py @@ -1,5 +1,5 @@ import datetime -from typing import Optional, List +from typing import Optional, List, Union from pydantic import BaseModel, Field, field_validator, ConfigDict from peppy.const import CONFIG_KEY, SUBSAMPLE_RAW_LIST_KEY, SAMPLE_RAW_DICT_KEY @@ -43,6 +43,9 @@ class ProjectAnnotationModel(BaseModel): submission_date: datetime.datetime digest: str pep_schema: str + pop: bool = False + stars_number: Optional[int] = 0 + forked_from: Optional[Union[str, None]] = None class SearchReturnModel(BaseModel): diff --git a/pephubclient/modules/sample.py b/pephubclient/modules/sample.py index 881ed6e..d66a8f1 100644 --- a/pephubclient/modules/sample.py +++ b/pephubclient/modules/sample.py @@ -42,12 +42,16 @@ def get( response = self.send_request( method="GET", url=url, headers=self.parse_header(self.__jwt_data) ) - if response.status_code != ResponseStatusCodes.OK: + if response.status_code == ResponseStatusCodes.OK: + return self.decode_response(response, output_json=True) + if response.status_code == ResponseStatusCodes.NOT_EXIST: + raise ResponseError("Sample does not exist.") + elif response.status_code == ResponseStatusCodes.INTERNAL_ERROR: + raise ResponseError("Internal server error. Unexpected return value.") + else: raise ResponseError( - f"Sample does not exist, or Internal server error occurred." + f"Unexpected return value. Error: {response.status_code}" ) - else: - return self.decode_response(response, output_json=True) def create( self, @@ -93,9 +97,7 @@ def create( return None elif response.status_code == ResponseStatusCodes.NOT_EXIST: - raise ResponseError( - f"Project '{namespace}/{name}:{tag}' does not exist. Error: {response.status_code}" - ) + raise ResponseError(f"Project '{namespace}/{name}:{tag}' does not exist.") elif response.status_code == ResponseStatusCodes.CONFLICT: raise ResponseError( f"Sample '{sample_name}' already exists. Set overwrite to True to overwrite sample." diff --git a/pephubclient/modules/view.py b/pephubclient/modules/view.py index ef14d55..5633e28 100644 --- a/pephubclient/modules/view.py +++ b/pephubclient/modules/view.py @@ -8,6 +8,7 @@ ResponseStatusCodes, ) from pephubclient.exceptions import ResponseError +from pephubclient.models import ProjectDict class PEPHubView(RequestManager): @@ -52,6 +53,7 @@ def get( output = self.decode_response(response, output_json=True) if raw: return output + output = ProjectDict(**output).model_dump(by_alias=True) return peppy.Project.from_dict(output) elif response.status_code == ResponseStatusCodes.NOT_EXIST: raise ResponseError("View does not exist, or you are unauthorized.") @@ -213,7 +215,7 @@ def remove_sample( ) elif response.status_code == ResponseStatusCodes.UNAUTHORIZED: raise ResponseError( - f"You are unauthorized to remove this sample from the view." + "You are unauthorized to remove this sample from the view." ) else: raise ResponseError( diff --git a/pephubclient/pephubclient.py b/pephubclient/pephubclient.py index 1cd62b5..6aa54ed 100644 --- a/pephubclient/pephubclient.py +++ b/pephubclient/pephubclient.py @@ -1,10 +1,8 @@ -import json from typing import NoReturn, Optional, Literal from typing_extensions import deprecated import peppy from peppy.const import NAME_KEY -import requests import urllib3 from pydantic import ValidationError from ubiquerg import parse_registry_path @@ -107,7 +105,6 @@ def load_project( :param query_param: query parameters used in get request :return Project: peppy project. """ - jwt = FilesManager.load_jwt_data_from_file(PATH_TO_FILE_WITH_JWT) raw_pep = self.load_raw_pep(project_registry_path, query_param) peppy_project = peppy.Project().from_dict(raw_pep) return peppy_project @@ -250,11 +247,11 @@ def find_project( cookies=None, ) if pephub_response.status_code == ResponseStatusCodes.OK: - decoded_response = self._handle_pephub_response(pephub_response) + decoded_response = self.decode_response(pephub_response, output_json=True) project_list = [] - for project_found in json.loads(decoded_response)["items"]: + for project_found in decoded_response["items"]: project_list.append(ProjectAnnotationModel(**project_found)) - return SearchReturnModel(**json.loads(decoded_response)) + return SearchReturnModel(**decoded_response) @deprecated("This method is deprecated. Use load_raw_pep instead.") def _load_raw_pep( @@ -297,8 +294,8 @@ def load_raw_pep( cookies=None, ) if pephub_response.status_code == ResponseStatusCodes.OK: - decoded_response = self._handle_pephub_response(pephub_response) - correct_proj_dict = ProjectDict(**json.loads(decoded_response)) + decoded_response = self.decode_response(pephub_response, output_json=True) + correct_proj_dict = ProjectDict(**decoded_response) # This step is necessary because of this issue: https://github.com/pepkit/pephub/issues/124 return correct_proj_dict.model_dump(by_alias=True) @@ -362,15 +359,3 @@ def _build_push_request_url(namespace: str) -> str: :return: url string """ return PEPHUB_PUSH_URL.format(namespace=namespace) - - @staticmethod - def _handle_pephub_response(pephub_response: requests.Response): - """ - Check pephub response - """ - decoded_response = PEPHubClient.decode_response(pephub_response) - - if pephub_response.status_code != ResponseStatusCodes.OK: - raise ResponseError(message=json.loads(decoded_response).get("detail")) - - return decoded_response diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt deleted file mode 100644 index e69de29..0000000 diff --git a/setup.py b/setup.py index aaf8893..9943860 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ def read_reqs(reqs_name): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering :: Bio-Informatics", ], keywords="project, bioinformatics, metadata", diff --git a/tests/conftest.py b/tests/conftest.py index 48b4483..e0a5469 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,3 @@ -import json - import pytest from pephubclient.pephub_oauth.models import InitializeDeviceCodeResponse @@ -29,7 +27,7 @@ def test_raw_pep_return(): {"time": "0", "file_path": "source1", "sample_name": "frog_0h"}, ], } - return json.dumps(sample_prj) + return sample_prj @pytest.fixture diff --git a/tests/test_manual.py b/tests/test_manual.py new file mode 100644 index 0000000..1848210 --- /dev/null +++ b/tests/test_manual.py @@ -0,0 +1,101 @@ +from pephubclient.pephubclient import PEPHubClient +import pytest + + +@pytest.mark.skip(reason="Manual test") +class TestViewsManual: + + def test_get(self): + ff = PEPHubClient().view.get( + "databio", + "bedset1", + "default", + "test_view", + ) + print(ff) + + def test_create(self): + PEPHubClient().view.create( + "databio", + "bedset1", + "default", + "test_view", + sample_list=["orange", "grape1", "apple1"], + ) + + def test_delete(self): + PEPHubClient().view.delete( + "databio", + "bedset1", + "default", + "test_view", + ) + + def test_add_sample(self): + PEPHubClient().view.add_sample( + "databio", + "bedset1", + "default", + "test_view", + "name", + ) + + def test_delete_sample(self): + PEPHubClient().view.remove_sample( + "databio", + "bedset1", + "default", + "test_view", + "name", + ) + + +@pytest.mark.skip(reason="Manual test") +class TestSamplesManual: + def test_manual(self): + ff = PEPHubClient().sample.get( + "databio", + "bedset1", + "default", + "grape1", + ) + ff + + def test_update(self): + ff = PEPHubClient().sample.get( + "databio", + "bedset1", + "default", + "newf", + ) + ff.update({"shefflab": "test1"}) + ff["sample_type"] = "new_type" + PEPHubClient().sample.update( + "databio", + "bedset1", + "default", + "newf", + sample_dict=ff, + ) + + def test_add(self): + ff = { + "genome": "phc_test1", + "sample_type": "phc_test", + } + PEPHubClient().sample.create( + "databio", + "bedset1", + "default", + "new_2222", + overwrite=False, + sample_dict=ff, + ) + + def test_delete(self): + PEPHubClient().sample.remove( + "databio", + "bedset1", + "default", + "new_2222", + ) diff --git a/tests/test_pephubclient.py b/tests/test_pephubclient.py index f6caa53..6a9aec9 100644 --- a/tests/test_pephubclient.py +++ b/tests/test_pephubclient.py @@ -61,7 +61,7 @@ def test_pull(self, mocker, test_jwt, test_raw_pep_return): return_value=Mock(content="some return", status_code=200), ) mocker.patch( - "pephubclient.pephubclient.PEPHubClient._handle_pephub_response", + "pephubclient.helpers.RequestManager.decode_response", return_value=test_raw_pep_return, ) save_yaml_mock = mocker.patch( @@ -150,11 +150,38 @@ def test_push_with_pephub_error_response( ) def test_search_prj(self, mocker): - return_value = b'{"count":1,"limit":100,"offset":0,"items":[{"namespace":"namespace1","name":"basic","tag":"default","is_private":false,"number_of_samples":2,"description":"None","last_update_date":"2023-08-27 19:07:31.552861+00:00","submission_date":"2023-08-27 19:07:31.552858+00:00","digest":"08cbcdbf4974fc84bee824c562b324b5","pep_schema":"random_schema_name"}],"session_info":null,"can_edit":false}' + return_value = { + "count": 1, + "limit": 100, + "offset": 0, + "items": [ + { + "namespace": "namespace1", + "name": "basic", + "tag": "default", + "is_private": False, + "number_of_samples": 2, + "description": "None", + "last_update_date": "2023-08-27 19:07:31.552861+00:00", + "submission_date": "2023-08-27 19:07:31.552858+00:00", + "digest": "08cbcdbf4974fc84bee824c562b324b5", + "pep_schema": "random_schema_name", + "pop": False, + "stars_number": 0, + "forked_from": None, + } + ], + "session_info": None, + "can_edit": False, + } mocker.patch( "requests.request", return_value=Mock(content=return_value, status_code=200), ) + mocker.patch( + "pephubclient.helpers.RequestManager.decode_response", + return_value=return_value, + ) return_value = PEPHubClient().find_project(namespace="namespace1") assert return_value.count == 1 @@ -203,157 +230,383 @@ def test_is_registry_path(self, input_str, expected_output): assert is_registry_path(input_str) is expected_output -# @pytest.mark.skipif(True, reason="not implemented yet") -# def test_save_zip_pep(self): -# ... -# -# @pytest.mark.skipif(True, reason="not implemented yet") -# def test_save_unzip_pep(self): -# ... -# -# -# @pytest.mark.skipif(True, reason="not implemented yet") -# class TestSamplesModification: -# def test_get_sumple(self): -# ... -# -# def test_add_sample(self): -# ... -# -# def test_remove_sample(self): -# ... -# -# def test_update_sample(self): -# ... -# -# -# @pytest.mark.skipif(True, reason="not implemented yet") -# class TestProjectVeiw: -# def test_get_view(self): -# ... -# -# def test_create_view(self): -# ... -# -# def test_delete_view(self): -# ... -# -# -class TestManual: - def test_manual(self): - ff = PEPHubClient().sample.get( - "khoroshevskyi", - "bedset1", - "default", - "grape1", - ) - ff +class TestSamples: - def test_update(self): - ff = PEPHubClient().sample.get( - "khoroshevskyi", - "bedset1", - "default", - "newf", + def test_get(self, mocker): + return_value = { + "genome": "phc_test1", + "sample_type": "phc_test", + "sample_name": "gg1", + } + mocker.patch( + "requests.request", + return_value=Mock(content=return_value, status_code=200), ) - ff.update({"fff": "test1"}) - ff["sample_type"] = "new_type" - PEPHubClient().sample.update( - "khoroshevskyi", - "bedset1", + mocker.patch( + "pephubclient.helpers.RequestManager.decode_response", + return_value=return_value, + ) + return_value = PEPHubClient().sample.get( + "test_namespace", + "taest_name", "default", - "newf", - sample_dict=ff, + "gg1", + ) + assert return_value == return_value + + @pytest.mark.parametrize( + "status_code, expected_error_message", + [ + ( + 404, + "Sample does not exist.", + ), + ( + 500, + "Internal server error. Unexpected return value.", + ), + ( + 403, + "Unexpected return value. Error: 403", + ), + ], + ) + def test_sample_get_with_pephub_error_response( + self, mocker, status_code, expected_error_message + ): + mocker.patch("requests.request", return_value=Mock(status_code=status_code)) + with pytest.raises(ResponseError, match=expected_error_message): + PEPHubClient().sample.get( + "test_namespace", + "taest_name", + "default", + "gg1", + ) + + @pytest.mark.parametrize( + "prj_dict", + [ + {"genome": "phc_test1", "sample_type": "phc_test", "sample_name": "gg1"}, + {"genome": "phc_test1", "sample_type": "phc_test"}, + ], + ) + def test_create(self, mocker, prj_dict): + return_value = prj_dict + mocker_obj = mocker.patch( + "requests.request", + return_value=Mock(content=return_value, status_code=202), ) - def test_add(self): - ff = { - "genome": "phc_test1", - "sample_type": "phc_test", - } PEPHubClient().sample.create( - "khoroshevskyi", - "bedset1", + "test_namespace", + "taest_name", "default", - "new_f", - overwrite=False, - sample_dict=ff, + "gg1", + sample_dict=return_value, + ) + assert mocker_obj.called + + @pytest.mark.parametrize( + "status_code, expected_error_message", + [ + ( + 404, + "does not exist.", + ), + ( + 409, + "already exists. Set overwrite to True to overwrite sample.", + ), + ( + 500, + "Unexpected return value.", + ), + ], + ) + def test_sample_create_with_pephub_error_response( + self, mocker, status_code, expected_error_message + ): + mocker.patch("requests.request", return_value=Mock(status_code=status_code)) + with pytest.raises(ResponseError, match=expected_error_message): + PEPHubClient().sample.create( + "test_namespace", + "taest_name", + "default", + "gg1", + sample_dict={ + "genome": "phc_test1", + "sample_type": "phc_test", + "sample_name": "gg1", + }, + ) + + def test_delete(self, mocker): + mocker_obj = mocker.patch( + "requests.request", + return_value=Mock(status_code=202), ) - def test_delete(self): PEPHubClient().sample.remove( - "khoroshevskyi", - "bedset1", + "test_namespace", + "taest_name", "default", - "new_f", + "gg1", ) + assert mocker_obj.called - # test add sample: - # 1. add correct 202 - # 2. add existing 409 - # 3. add with sample_name - # 4. add without sample_name - # 5. add with overwrite - # 6. add to unexisting project 404 + @pytest.mark.parametrize( + "status_code, expected_error_message", + [ + ( + 404, + "does not exist.", + ), + ( + 500, + "Unexpected return value.", + ), + ], + ) + def test_sample_delete_with_pephub_error_response( + self, mocker, status_code, expected_error_message + ): + mocker.patch("requests.request", return_value=Mock(status_code=status_code)) + with pytest.raises(ResponseError, match=expected_error_message): + PEPHubClient().sample.remove( + "test_namespace", + "taest_name", + "default", + "gg1", + ) - # delete sample: - # 1. delete existing 202 - # 2. delete unexisting 404 + def test_update(self, mocker): + mocker_obj = mocker.patch( + "requests.request", + return_value=Mock(status_code=202), + ) - # get sample: - # 1. get existing 200 - # 2. get unexisting 404 - # 3. get with raw 200 - # 4. get from unexisting project 404 + PEPHubClient().sample.update( + "test_namespace", + "taest_name", + "default", + "gg1", + sample_dict={ + "genome": "phc_test1", + "sample_type": "phc_test", + "new_col": "column", + }, + ) + assert mocker_obj.called - # update sample: - # 1. update existing 202 - # 2. update unexisting sample 404 - # 3. update unexisting project 404 + @pytest.mark.parametrize( + "status_code, expected_error_message", + [ + ( + 404, + "does not exist.", + ), + ( + 500, + "Unexpected return value.", + ), + ], + ) + def test_sample_update_with_pephub_error_response( + self, mocker, status_code, expected_error_message + ): + mocker.patch("requests.request", return_value=Mock(status_code=status_code)) + with pytest.raises(ResponseError, match=expected_error_message): + PEPHubClient().sample.update( + "test_namespace", + "taest_name", + "default", + "gg1", + sample_dict={ + "genome": "phc_test1", + "sample_type": "phc_test", + "new_col": "column", + }, + ) class TestViews: + def test_get(self, mocker, test_raw_pep_return): + return_value = test_raw_pep_return + mocker.patch( + "requests.request", + return_value=Mock(content=return_value, status_code=200), + ) + mocker.patch( + "pephubclient.helpers.RequestManager.decode_response", + return_value=return_value, + ) - def test_get(self): - ff = PEPHubClient().view.get( - "khoroshevskyi", - "bedset1", + return_value = PEPHubClient().view.get( + "test_namespace", + "taest_name", "default", - "test_view", + "gg1", + ) + assert return_value == return_value + + @pytest.mark.parametrize( + "status_code, expected_error_message", + [ + ( + 404, + "does not exist.", + ), + ( + 500, + "Internal server error.", + ), + ], + ) + def test_view_get_with_pephub_error_response( + self, mocker, status_code, expected_error_message + ): + mocker.patch("requests.request", return_value=Mock(status_code=status_code)) + with pytest.raises(ResponseError, match=expected_error_message): + PEPHubClient().view.get( + "test_namespace", + "taest_name", + "default", + "gg1", + ) + + def test_create(self, mocker): + mocker_obj = mocker.patch( + "requests.request", + return_value=Mock(status_code=202), ) - print(ff) - def test_create(self): PEPHubClient().view.create( - "khoroshevskyi", - "bedset1", + "test_namespace", + "taest_name", "default", - "test_view", - sample_list=["orange", "grape1", "apple1"], + "gg1", + sample_list=["sample1", "sample2"], + ) + assert mocker_obj.called + + @pytest.mark.parametrize( + "status_code, expected_error_message", + [ + ( + 404, + "does not exist.", + ), + ( + 409, + "already exists in the project.", + ), + ], + ) + def test_view_create_with_pephub_error_response( + self, mocker, status_code, expected_error_message + ): + mocker.patch("requests.request", return_value=Mock(status_code=status_code)) + with pytest.raises(ResponseError, match=expected_error_message): + PEPHubClient().view.create( + "test_namespace", + "taest_name", + "default", + "gg1", + sample_list=["sample1", "sample2"], + ) + + def test_delete(self, mocker): + mocker_obj = mocker.patch( + "requests.request", + return_value=Mock(status_code=202), ) - def test_delete(self): PEPHubClient().view.delete( - "khoroshevskyi", - "bedset1", + "test_namespace", + "taest_name", "default", - "test_view", + "gg1", + ) + assert mocker_obj.called + + @pytest.mark.parametrize( + "status_code, expected_error_message", + [ + ( + 404, + "does not exist.", + ), + ( + 401, + "You are unauthorized to delete this view.", + ), + ], + ) + def test_view_delete_with_pephub_error_response( + self, mocker, status_code, expected_error_message + ): + mocker.patch("requests.request", return_value=Mock(status_code=status_code)) + with pytest.raises(ResponseError, match=expected_error_message): + PEPHubClient().view.delete( + "test_namespace", + "taest_name", + "default", + "gg1", + ) + + def test_add_sample(self, mocker): + mocker_obj = mocker.patch( + "requests.request", + return_value=Mock(status_code=202), ) - def test_add_sample(self): PEPHubClient().view.add_sample( - "khoroshevskyi", - "bedset1", + "test_namespace", + "taest_name", "default", - "test_view", - "apple", + "gg1", + "sample1", + ) + assert mocker_obj.called + + def test_delete_sample(self, mocker): + mocker_obj = mocker.patch( + "requests.request", + return_value=Mock(status_code=202), ) - def test_delete_sample(self): PEPHubClient().view.remove_sample( - "khoroshevskyi", - "bedset1", + "test_namespace", + "taest_name", "default", - "test_view", - "apple", + "gg1", + "sample1", ) + assert mocker_obj.called + + +### + + +# test add sample: +# 1. add correct 202 +# 2. add existing 409 +# 3. add with sample_name +# 4. add without sample_name +# 5. add with overwrite +# 6. add to unexisting project 404 + +# delete sample: +# 1. delete existing 202 +# 2. delete unexisting 404 + +# get sample: +# 1. get existing 200 +# 2. get unexisting 404 +# 3. get with raw 200 +# 4. get from unexisting project 404 + +# update sample: +# 1. update existing 202 +# 2. update unexisting sample 404 +# 3. update unexisting project 404