Skip to content

Commit

Permalink
feat: fully support non-JSON responses
Browse files Browse the repository at this point in the history
  • Loading branch information
uniqueg committed Nov 11, 2021
1 parent 277a449 commit e8ed145
Show file tree
Hide file tree
Showing 3 changed files with 320 additions and 121 deletions.
21 changes: 11 additions & 10 deletions README.md
Expand Up @@ -13,6 +13,14 @@ including support for additional endpoints defined in [ELIXIR Cloud &
AAI's][res-elixir-cloud] generic [TRS-Filer][res-elixir-cloud-trs-filer] TRS
implementation.

The TRS API version underlying the client can be found
[here][res-ga4gh-trs-version].

TRS-cli has so far been succesfully tested with the
[TRS-Filer][res-elixir-cloud-trs-filer] and
[WorkflowHub][res-eosc-workflow-hub] TRS implementations. WorkflowHub's public
TRS API endpoint can be found here: <https://dev.workflowhub.eu/ga4gh/trs/v2>

## Table of Contents

* [Usage](#usage)
Expand Down Expand Up @@ -46,7 +54,7 @@ method. The following configuration parameters are available:
| Parameter | Type | Default | Description |
| --- | --- | ---- | --- |
| `debug` | `bool` | `False` | If set, the exception handler prints tracebacks for every exception encountered. |
| `no_validate` | `bool` | `False` | If set, responses are not validated. In that case, unserialized `response` objects are returned. |
| `no_validate` | `bool` | `False` | If set, responses JSON are not validated against the TRS API schemas. In that case, unserialized `response` objects are returned. Set this flag if the TRS implementation you are working with is not fully compliant with the TRS API specification. |

Example:

Expand Down Expand Up @@ -174,15 +182,6 @@ methods for additional endpoints implemented in
| [`.delete_version()`][docs-api-delete_version] | `DELETE ​/tools​/{id}​/versions​/{version_id}` | Delete a tool version |
| [`.post_service_info()`][docs-api-post_service_info] | `POST ​/service-info` | Register service info |

#### Convenience methods

Finally, TRS-cli tries to provide convenience methods for common operations
that involve multiple API calls. Currently there is one such method defined:

| Method | Description |
| --- | --- |
| [`.retrieve_files()`][docs-api-retrieve_files] | Retrieve all files associated with a given tool version and descriptor type. Useful for downloading workflows. |

### Authorization

Authorization [bearer tokens][res-bearer-token] can be provided either during
Expand Down Expand Up @@ -309,8 +308,10 @@ question etc.
[res-elixir-cloud-coc]: <https://github.com/elixir-cloud-aai/elixir-cloud-aai/blob/dev/CODE_OF_CONDUCT.md>
[res-elixir-cloud-contributing]: <https://github.com/elixir-cloud-aai/elixir-cloud-aai/blob/dev/CONTRIBUTING.md>
[res-elixir-cloud-trs-filer]: <https://github.com/elixir-cloud-aai/trs-filer>
[res-eosc-workflow-hub]: <https://workflowhub.eu/>
[res-ga4gh]: <https://www.ga4gh.org/>
[res-ga4gh-trs]: <https://github.com/ga4gh/tool-registry-service-schemas>
[res-ga4gh-trs-version]: <https://github.com/ga4gh/tool-registry-service-schemas/blob/91a57cd93caf380019d4952c0c74bb7e343e647b/openapi/openapi.yaml>
[res-ga4gh-trs-uri]: <https://ga4gh.github.io/tool-registry-service-schemas/DataModel/#trs_uris>
[res-pydantic]: <https://pydantic-docs.helpmanual.io/>
[res-pydantic-docs-export]: <https://pydantic-docs.helpmanual.io/usage/exporting_models/>
Expand Down
131 changes: 116 additions & 15 deletions tests/test_client.py
Expand Up @@ -32,6 +32,7 @@
MOCK_API = f"{MOCK_HOST}:{MOCK_PORT}/{MOCK_BASE_PATH}"
MOCK_ID = "123456"
MOCK_ID_INVALID = "N0T VAL!D"
MOCK_TEXT_PLAIN = "SOME TEXT"
MOCK_TRS_URI = f"trs://{MOCK_DOMAIN}/{MOCK_ID}"
MOCK_TRS_URI_VERSIONED = f"trs://{MOCK_DOMAIN}/{MOCK_ID}/versions/{MOCK_ID}"
MOCK_TOKEN = "MyT0k3n"
Expand Down Expand Up @@ -489,7 +490,7 @@ class TestDeleteVersion:
f"{cli.uri}/tools/{MOCK_ID}/versions/{MOCK_ID}"
)

def test_success(self, monkeypatch, requests_mock):
def test_success(self, requests_mock):
"""Returns 200 response."""
requests_mock.delete(self.endpoint, json=MOCK_ID)
r = self.cli.delete_version(
Expand Down Expand Up @@ -665,6 +666,10 @@ class TestGetDescriptorByPath:
f"{cli.uri}/tools/{MOCK_ID}/versions/{MOCK_ID}/{MOCK_DESCRIPTOR}"
f"/descriptor/{MOCK_ID}"
)
endpoint_plain = (
f"{cli.uri}/tools/{MOCK_ID}/versions/{MOCK_ID}/PLAIN_{MOCK_DESCRIPTOR}"
f"/descriptor/{MOCK_ID}"
)

def test_success(self, requests_mock):
"""Returns 200 response."""
Expand All @@ -677,6 +682,17 @@ def test_success(self, requests_mock):
)
assert r.dict() == MOCK_FILE_WRAPPER

def test_success_plain(self, requests_mock):
"""Returns 200 response."""
requests_mock.get(self.endpoint_plain, text=MOCK_TEXT_PLAIN)
r = self.cli.get_descriptor_by_path(
type=f"PLAIN_{MOCK_DESCRIPTOR}",
id=MOCK_ID,
version_id=MOCK_ID,
path=MOCK_ID,
)
assert r.text == MOCK_TEXT_PLAIN


class TestGetFiles:
"""Test getter for files of a given descriptor type."""
Expand Down Expand Up @@ -713,17 +729,85 @@ def test_ContentTypeUnavailable(self, requests_mock):
format=MOCK_ID,
)

def test_success_trs_uri_zip(self, requests_mock):
def test_success_trs_uri_zip(self, requests_mock, tmpdir):
"""Returns 200 ZIP response with TRS URI."""
requests_mock.get(self.endpoint, json=[MOCK_TOOL_FILE])
outfile = tmpdir / 'test.zip'
requests_mock.get(self.endpoint)
r = self.cli.get_files(
type=MOCK_DESCRIPTOR,
id=MOCK_TRS_URI_VERSIONED,
format='zip',
outfile=outfile,
)
if not isinstance(r, Error):
assert r[0].file_type.value == MOCK_TOOL_FILE['file_type']
assert r[0].path == MOCK_TOOL_FILE['path']
assert r == outfile

def test_success_trs_uri_zip_default_filename(
self,
requests_mock,
monkeypatch,
):
"""Returns 200 ZIP response with default filename."""
requests_mock.get(self.endpoint)
monkeypatch.setattr(
'builtins.open',
lambda *args, **kwargs: _raise(Exception)
)
with pytest.raises(Exception):
self.cli.get_files(
type=MOCK_DESCRIPTOR,
id=MOCK_TRS_URI_VERSIONED,
format='zip',
)

def test_zip_content_type(self, requests_mock, tmpdir):
"""Test wrong content type."""
outfile = tmpdir / 'test.zip'
requests_mock.get(
self.endpoint,
headers={'Content-Type': 'text/plain'},
)
r = self.cli.get_files(
type=MOCK_DESCRIPTOR,
id=MOCK_TRS_URI_VERSIONED,
format='zip',
outfile=outfile,
)
print(r)
print(type(r))
print(dir(r))
print(r.headers)
assert isinstance(r, requests.models.Response)

def test_zip_io_error(self, monkeypatch, requests_mock, tmpdir):
"""Test I/O error."""
outfile = tmpdir / 'test.zip'
requests_mock.get(self.endpoint)
monkeypatch.setattr(
'builtins.open',
lambda *args, **kwargs: _raise(IOError)
)
r = self.cli.get_files(
type=MOCK_DESCRIPTOR,
id=MOCK_TRS_URI_VERSIONED,
format='zip',
outfile=outfile,
)
assert isinstance(r, requests.models.Response)

def test_zip_connection_error(self, monkeypatch, tmpdir):
"""Test connection error."""
outfile = tmpdir / 'test.zip'
monkeypatch.setattr(
'requests.get',
lambda *args, **kwargs: _raise(requests.exceptions.ConnectionError)
)
with pytest.raises(requests.exceptions.ConnectionError):
self.cli.get_files(
type=MOCK_DESCRIPTOR,
id=MOCK_TRS_URI_VERSIONED,
format='zip',
outfile=outfile,
)


class TestGetTests:
Expand Down Expand Up @@ -1108,6 +1192,9 @@ class TestSendRequestAndValidateRespose:
"""Test request sending and response validation."""

cli = TRSClient(uri=MOCK_TRS_URI)
cli.headers = {
'Accept': 'application/json'
}
endpoint = MOCK_API
payload = {}

Expand Down Expand Up @@ -1136,7 +1223,7 @@ def test_get_str_validation(self, requests_mock):
requests_mock.get(self.endpoint, json=MOCK_ID)
response = self.cli._send_request_and_validate_response(
url=MOCK_API,
validation_class_ok=str,
json_validation_class=str,
)
assert response == MOCK_ID

Expand All @@ -1145,7 +1232,7 @@ def test_get_none_validation(self, requests_mock):
requests_mock.get(self.endpoint, json=MOCK_ID)
response = self.cli._send_request_and_validate_response(
url=MOCK_API,
validation_class_ok=None,
json_validation_class=None,
)
assert response is None

Expand All @@ -1154,7 +1241,7 @@ def test_get_model_validation(self, requests_mock):
requests_mock.get(self.endpoint, json=MOCK_TOOL)
response = self.cli._send_request_and_validate_response(
url=MOCK_API,
validation_class_ok=Tool,
json_validation_class=Tool,
)
assert response == MOCK_TOOL

Expand All @@ -1163,7 +1250,7 @@ def test_get_list_validation(self, requests_mock):
requests_mock.get(self.endpoint, json=[MOCK_TOOL])
response = self.cli._send_request_and_validate_response(
url=MOCK_API,
validation_class_ok=(Tool, ),
json_validation_class=(Tool, ),
)
assert response == [MOCK_TOOL]

Expand All @@ -1174,7 +1261,7 @@ def test_post_validation(self, requests_mock):
url=MOCK_API,
method='post',
payload=self.payload,
validation_class_ok=str,
json_validation_class=str,
)
assert response == MOCK_ID

Expand All @@ -1184,7 +1271,7 @@ def test_get_success_invalid(self, requests_mock):
with pytest.raises(InvalidResponseError):
self.cli._send_request_and_validate_response(
url=MOCK_API,
validation_class_ok=Error,
json_validation_class=Error,
)

def test_error_response(self, requests_mock):
Expand All @@ -1197,12 +1284,12 @@ def test_error_response(self, requests_mock):

def test_error_response_invalid(self, requests_mock):
"""Test for error response that fails validation."""
requests_mock.get(self.endpoint, json=MOCK_ERROR, status_code=400)
requests_mock.get(self.endpoint, json=MOCK_TOOL, status_code=400)
with pytest.raises(InvalidResponseError):
self.cli._send_request_and_validate_response(
bla = self.cli._send_request_and_validate_response(
url=MOCK_API,
validation_class_error=Tool,
)
print(bla)

def test_invalid_http_method(self, requests_mock):
"""Test for invalid HTTP method."""
Expand All @@ -1213,3 +1300,17 @@ def test_invalid_http_method(self, requests_mock):
method='non_existing',
payload=self.payload,
)

def test_text_plain_responses(self, requests_mock):
"""Test for invalid HTTP method."""
requests_mock.get(
self.endpoint,
text=MOCK_TEXT_PLAIN,
headers={'Content-Type': 'text/plain'},
)
self.cli.headers['Accept'] = 'text/plain'
r = self.cli._send_request_and_validate_response(
url=MOCK_API,
)
assert r == MOCK_TEXT_PLAIN
self.cli.headers['Accept'] = 'application/json'

0 comments on commit e8ed145

Please sign in to comment.