Skip to content
This repository has been archived by the owner on Jul 22, 2024. It is now read-only.

v0.7.0 #51

Merged
merged 5 commits into from
Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
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 @@ -41,20 +49,23 @@ from trs_cli import TRSClient
### Configure client class

It is possible to configure the `TRSClient` class with the `.config()` class
method. Currently there is only a single configuration parameter available,
the `debug` flag. Setting it to `True` (default: False) will have the exception
handler print tracebacks for every exception encountered.
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 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:

```py
from trs_cli import TRSClient

TRSClient.config(debug=True)
TRSClient.config(debug=True, no_validate=True)
```

> Note that as a _class method_, the `.config()` method will affect _all_
> client instances.
> client instances, including existing ones.

### Create client instance

Expand Down Expand Up @@ -171,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 @@ -306,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
142 changes: 126 additions & 16 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@
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_TRS_URI_VERSIONED = f"trs://{MOCK_DOMAIN}/{MOCK_ID}/{MOCK_ID}"
MOCK_TOKEN = "MyT0k3n"
MOCK_DESCRIPTOR = "CWL"
MOCK_RESPONSE_INVALID = {"not": "valid"}
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 All @@ -1122,12 +1209,21 @@ def test_connection_error(self, monkeypatch):
url=MOCK_API,
)

def test_no_validation(self, requests_mock):
TRSClient.config(no_validate=True)
requests_mock.get(self.endpoint, text=MOCK_ID)
response = self.cli._send_request_and_validate_response(
url=MOCK_API,
)
assert response.text == MOCK_ID
TRSClient.config(no_validate=False)

def test_get_str_validation(self, requests_mock):
"""Test for getter with string response."""
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 @@ -1136,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 @@ -1145,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 @@ -1154,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 @@ -1165,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 @@ -1175,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 @@ -1188,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 @@ -1204,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'
2 changes: 1 addition & 1 deletion trs_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '0.6.1'
__version__ = '0.7.0'

from trs_cli.client import TRSClient # noqa: F401
Loading