From fb62f22fc70a5e02482d824ffd9ef38d470d9e52 Mon Sep 17 00:00:00 2001 From: Mikhail Andrenkov Date: Thu, 7 Apr 2022 11:10:59 -0400 Subject: [PATCH 1/7] Add support for parsing query parameter validation errors --- tests/test_connection.py | 32 +++++++++++++++++++++++++++++--- xcc/connection.py | 15 +++++++++++---- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 501c41c..0d75699 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -136,16 +136,42 @@ def test_request_failure_due_to_invalid_json(self, connection): with pytest.raises(HTTPError, match=r"400 Client Error: Bad Request"): connection.request("GET", "/healthz") + @pytest.mark.parametrize( + "meta, match", + [ + ( + { + "_schema": ["Foo", "Bar"], + "size": ["Baz"], + }, + r"Foo; Bar; Baz", + ), + ( + { + "head": {"0": ["Foo", "Bar"]}, + "tail": {"0": ["Baz"], "1": ["Bud"]}, + }, + r"Foo; Bar; Baz; Bud", + ), + ( + { + "List": ["Foo"], + "Dict": {"0": ["Bar"]}, + }, + r"Foo; Bar", + ), + ], + ) @responses.activate - def test_request_failure_due_to_validation_error(self, connection): + def test_request_failure_due_to_validation_error(self, connection, meta, match): """Tests that an HTTPError is raised when the HTTP response of a connection request indicates that a validation error occurred and the body of the response contains a non-empty "meta" field. """ - body = {"code": "validation-error", "meta": {"_schema": ["Foo", "Bar"], "size": ["Baz"]}} + body = {"code": "validation-error", "meta": meta} responses.add(responses.GET, connection.url("healthz"), status=400, body=json.dumps(body)) - with pytest.raises(HTTPError, match=r"Foo; Bar; Baz"): + with pytest.raises(HTTPError, match=match): connection.request("GET", "/healthz") @responses.activate diff --git a/xcc/connection.py b/xcc/connection.py index e183a90..180639d 100644 --- a/xcc/connection.py +++ b/xcc/connection.py @@ -2,7 +2,7 @@ This module contains the :class:`~xcc.Connection` class. """ from itertools import chain -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union import requests @@ -225,9 +225,16 @@ def request(self, method: str, path: str, **kwargs) -> requests.Response: else: # The details of a validation error are encoded in the "meta" field. if response.status_code == 400 and body.get("code", "") == "validation-error": - errors: Dict[str, List[str]] = body.get("meta", {}) - if errors: - message = "; ".join(chain.from_iterable(errors.values())) + meta: Dict[str, Union[List[str], Dict[str, List[str]]]] = body.get("meta", {}) + if meta: + errors = [] + for entry in meta.values(): + if isinstance(entry, list): + errors.extend(entry) + else: + errors.extend(chain.from_iterable(entry.values())) + + message = "; ".join(errors) raise requests.exceptions.HTTPError(message, response=response) # Otherwise, the details of the error may be encoded in the "detail" field. From a93aa5634029c02e8081d16d48966d0ad701949d Mon Sep 17 00:00:00 2001 From: Mikhail Andrenkov Date: Thu, 7 Apr 2022 11:13:25 -0400 Subject: [PATCH 2/7] Add `ids` parameter to Job.list() --- tests/test_job.py | 48 ++++++++++++++++++++++++++++++++++++++++------- xcc/job.py | 7 +++++-- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/tests/test_job.py b/tests/test_job.py index f4a0598..8e0a3d3 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -54,15 +54,50 @@ class TestJob: """Tests the :class:`xcc.Job` class.""" @pytest.mark.parametrize( - "limit, want_names", + "limit, ids, want_params, want_names", [ - (1, ["foo"]), - (2, ["foo", "bar"]), + ( + 1, + None, + {"size": "1"}, + ["foo"], + ), + ( + 2, + None, + {"size": "2"}, + ["foo", "bar"], + ), + ( + 1, + ["00000000-0000-0000-0000-000000000001"], + {"size": "1", "id": "00000000-0000-0000-0000-000000000001"}, + ["foo"], + ), + ( + 2, + [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003", + ], + { + "size": "2", + "id": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003", + ], + }, + ["foo", "bar"], + ), ], ) @responses.activate - def test_list(self, connection, add_response, limit, want_names): - """Tests that the correct jobs are listed.""" + def test_list(self, connection, add_response, limit, ids, want_params, want_names): + """Tests that the correct jobs are listed and that the correct query + parameters are encoded in the HTTP request to the Xanadu Cloud platform. + """ data = [ { "id": "00000000-0000-0000-0000-000000000001", @@ -82,11 +117,10 @@ def test_list(self, connection, add_response, limit, want_names): ][:limit] add_response(body={"data": data}, path="/jobs") - have_names = [job.name for job in xcc.Job.list(connection, limit=limit)] + have_names = [job.name for job in xcc.Job.list(connection, limit=limit, ids=ids)] assert have_names == want_names have_params = responses.calls[0].request.params # pyright: reportGeneralTypeIssues=false - want_params = {"size": str(limit)} assert have_params == want_params @pytest.mark.parametrize("name", [None, "foo"]) diff --git a/xcc/job.py b/xcc/job.py index e0f4bff..6e87065 100644 --- a/xcc/job.py +++ b/xcc/job.py @@ -92,18 +92,21 @@ class Job: """ @staticmethod - def list(connection: Connection, limit: int = 5) -> Sequence[Job]: + def list( + connection: Connection, limit: int = 5, ids: Optional[Sequence[str]] = None + ) -> Sequence[Job]: """Returns jobs submitted to the Xanadu Cloud. Args: connection (Connection): connection to the Xanadu Cloud limit (int): maximum number of jobs to retrieve + ids (Sequence[str], optional): IDs of the jobs to retrieve Returns: Sequence[Job]: jobs which were submitted on the Xanadu Cloud by the user associated with the Xanadu Cloud connection """ - response = connection.request("GET", "/jobs", params={"size": limit}) + response = connection.request("GET", "/jobs", params={"size": limit, "id": ids}) jobs = [] From 5172b47645ce46ba964894d5a3bb4af6f26add7a Mon Sep 17 00:00:00 2001 From: Mikhail Andrenkov Date: Thu, 7 Apr 2022 11:15:03 -0400 Subject: [PATCH 3/7] Add `ids` parameter to `xcc job list` command --- tests/test_commands.py | 2 +- xcc/commands.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 5f80f01..38d0eb3 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -47,7 +47,7 @@ class MockJob(xcc.Job): """ @staticmethod - def list(connection, limit=10): + def list(connection, limit=10, ids=None): connection = xcc.commands.load_connection() return [MockJob(id_, connection) for id_ in ("foo", "bar", "baz")][:limit] diff --git a/xcc/commands.py b/xcc/commands.py index 2a2224e..4ce8f9f 100644 --- a/xcc/commands.py +++ b/xcc/commands.py @@ -5,7 +5,7 @@ import functools import json import sys -from typing import Any, Callable, Mapping, Sequence, Tuple, Union +from typing import Any, Callable, Mapping, Optional, Sequence, Tuple, Union import fire from fire.core import FireError @@ -259,16 +259,17 @@ def get_job( @beautify -def list_jobs(limit: int = 10) -> Sequence[Mapping]: +def list_jobs(limit: int = 5, ids: Optional[Sequence[str]] = None) -> Sequence[Mapping]: """Lists jobs submitted to the Xanadu Cloud. Args: limit (int): Maximum number of jobs to display. + ids (Sequence[str], optional): IDs of the jobs to display. Returns: Sequence[Mapping]: Overview of each job submitted to the Xanadu Cloud. """ - jobs = Job.list(connection=load_connection(), limit=limit) + jobs = Job.list(connection=load_connection(), limit=limit, ids=ids) return [job.overview for job in jobs] From 56118d9b01b7942a9ebaa3628a193edd7d2352bc Mon Sep 17 00:00:00 2001 From: Mikhail Andrenkov Date: Thu, 7 Apr 2022 11:16:05 -0400 Subject: [PATCH 4/7] Upgrade Black to address psf/black#2964 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index cbeb0af..45088be 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -black==21.9b0 +black==22.3.0 build==0.7.0 isort[colors]==5.9.3 pylint==2.11.1 From c91f0fc63c0cbdd6d6f2cd6016784cf58bcfa477 Mon Sep 17 00:00:00 2001 From: Mikhail Andrenkov Date: Thu, 7 Apr 2022 11:27:21 -0400 Subject: [PATCH 5/7] Update changelog --- .github/CHANGELOG.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index f622f93..b29b51a 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,12 +2,28 @@ ### New features since last release +* Following an update to the Xanadu Cloud 0.4.0 API, job lists can now be filtered by ID. + [(#21)](https://github.com/XanaduAI/xanadu-cloud-client/pull/20) + + Using the CLI: + + ```bash + xcc job list --ids '["", "", ...]' + ``` + + Using the Python API: + + ```python + xcc.Job.list(connection, ids=["", "", ...]) + ``` + + ### Breaking Changes ### Bug fixes * The license file is included in the source distribution, even when using `setuptools <56.0.0`. - [(#20)](https://github.com/XanaduAI/xanadu-cloud-client/pull/20) + [(#20)](https://github.com/XanaduAI/xanadu-cloud-client/pull/20) ### Documentation From e5d16712a41e7649302d1f3de4dd05b792dd9d59 Mon Sep 17 00:00:00 2001 From: Mikhail Andrenkov Date: Thu, 7 Apr 2022 15:08:24 -0400 Subject: [PATCH 6/7] Update PR link in changelog --- .github/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b29b51a..7f2469c 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -3,7 +3,7 @@ ### New features since last release * Following an update to the Xanadu Cloud 0.4.0 API, job lists can now be filtered by ID. - [(#21)](https://github.com/XanaduAI/xanadu-cloud-client/pull/20) + [(#21)](https://github.com/XanaduAI/xanadu-cloud-client/pull/21) Using the CLI: From ef4f0ab94c2db2155955f0113579d4e1f5e814d4 Mon Sep 17 00:00:00 2001 From: Mikhail Andrenkov Date: Fri, 8 Apr 2022 16:09:46 -0400 Subject: [PATCH 7/7] Override limit with the number of job IDs --- tests/test_job.py | 2 +- xcc/commands.py | 7 ++++--- xcc/job.py | 11 +++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/test_job.py b/tests/test_job.py index 8e0a3d3..254cc79 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -82,7 +82,7 @@ class TestJob: "00000000-0000-0000-0000-000000000003", ], { - "size": "2", + "size": "3", "id": [ "00000000-0000-0000-0000-000000000001", "00000000-0000-0000-0000-000000000002", diff --git a/xcc/commands.py b/xcc/commands.py index 4ce8f9f..6248d9f 100644 --- a/xcc/commands.py +++ b/xcc/commands.py @@ -5,7 +5,7 @@ import functools import json import sys -from typing import Any, Callable, Mapping, Optional, Sequence, Tuple, Union +from typing import Any, Callable, List, Mapping, Sequence, Tuple, Union import fire from fire.core import FireError @@ -259,12 +259,13 @@ def get_job( @beautify -def list_jobs(limit: int = 5, ids: Optional[Sequence[str]] = None) -> Sequence[Mapping]: +def list_jobs(limit: int = 5, ids: List[str] = None) -> Sequence[Mapping]: """Lists jobs submitted to the Xanadu Cloud. Args: limit (int): Maximum number of jobs to display. - ids (Sequence[str], optional): IDs of the jobs to display. + ids (List[str], optional): IDs of the jobs to display. If at least one + ID is specified, the limit flag will be set to the number of IDs. Returns: Sequence[Mapping]: Overview of each job submitted to the Xanadu Cloud. diff --git a/xcc/job.py b/xcc/job.py index 6e87065..275d6d5 100644 --- a/xcc/job.py +++ b/xcc/job.py @@ -8,7 +8,7 @@ import time from datetime import datetime, timedelta from itertools import count, takewhile -from typing import Any, List, Mapping, Optional, Sequence, Union +from typing import Any, Collection, List, Mapping, Optional, Sequence, Union import dateutil.parser import numpy as np @@ -93,20 +93,23 @@ class Job: @staticmethod def list( - connection: Connection, limit: int = 5, ids: Optional[Sequence[str]] = None + connection: Connection, limit: int = 5, ids: Optional[Collection[str]] = None ) -> Sequence[Job]: """Returns jobs submitted to the Xanadu Cloud. Args: connection (Connection): connection to the Xanadu Cloud limit (int): maximum number of jobs to retrieve - ids (Sequence[str], optional): IDs of the jobs to retrieve + ids (Collection[str], optional): IDs of the jobs to retrieve; if at + least one ID is specified, ``limit`` will be set to the length + of the ID collection Returns: Sequence[Job]: jobs which were submitted on the Xanadu Cloud by the user associated with the Xanadu Cloud connection """ - response = connection.request("GET", "/jobs", params={"size": limit, "id": ids}) + size = len(ids) if ids else limit + response = connection.request("GET", "/jobs", params={"size": size, "id": ids}) jobs = []