From bde3f978dccc5f66ce48631bca7f5194c3af260e Mon Sep 17 00:00:00 2001 From: Chuck Daniels Date: Thu, 16 Apr 2026 16:13:17 -0400 Subject: [PATCH] Support "unchecked" query parameters. Fixes #106 --- CHANGELOG.md | 17 ++++++++++++++++- cmr/queries.py | 21 ++++++++++++++------- tests/test_queries.py | 30 +++++++++++++++++++++++------- tests/test_service.py | 2 +- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e94a9..127b621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Changed -- Deprecate methods `Query.get` and `Query.get_all` in favor of the new `Query.results` method. These deprecated methods will likely be removed for the 1.0.0 release. ([#37](https://github.com/nasa/python_cmr/issues/37)) +- Deprecate methods `Query.get` and `Query.get_all` in favor of the new + `Query.results` method. These deprecated methods will likely be removed for + the 1.0.0 release. ([#37](https://github.com/nasa/python_cmr/issues/37)) +- `Query.parameters` accepts "unchecked" keywords, meaning that it accepts + keywords that do not have a corresponding method by the same name in the + `Query` class (or specific subclass being used). + + This allows the caller to supply a parameter that does not have a + corresponding method without raising a `ValueError`. Instead, such a parameter + is passed directly through to the CMR, where it will be checked. If the + parameter is not supported or its value is invalid, the CMR response will + indicate as such. + + This avoids the need to wait for the corresponding method to be added, or + having to write cumbersome code to get around the limitation. + ([#106](https://github.com/nasa/python_cmr/issues/106)) ## [0.13.0] diff --git a/cmr/queries.py b/cmr/queries.py index cdf9f2e..1cf0f69 100644 --- a/cmr/queries.py +++ b/cmr/queries.py @@ -7,7 +7,7 @@ from datetime import date, datetime, timezone from inspect import getmembers, ismethod from re import search -from typing import Iterator +from typing import Iterable, Iterator from typing_extensions import ( Any, @@ -127,7 +127,7 @@ def get_all(self) -> Sequence[Any]: :returns: query results as a list """ - + return list(self.get(self.hits())) def results(self, page_size: int = 2000) -> Iterator[Any]: @@ -196,12 +196,19 @@ def parameters(self, **kwargs: Any) -> Self: methods = dict(getmembers(self, predicate=ismethod)) for key, val in kwargs.items(): - # verify the key matches one of our methods + # If the key does not match one of the methods defined in the Query + # class or subclass, simply set the parameter "unchecked" (i.e., + # set the parameter, but without a method that can do some value + # checking. If the value is invalid, the CMR response will indicate + # the problem). if key not in methods: - raise ValueError(f"Unknown key {key}") - - # call the method - if isinstance(val, tuple): + if isinstance(val, str) or not isinstance(val, Iterable): + # Set single-valued parameter + self.params[key] = val + else: + # Set multi-valued parameter adding `[]` suffix to key + self.params[f"{key}[]"] = tuple(val) + elif isinstance(val, tuple): methods[key](*val) else: methods[key](val) diff --git a/tests/test_queries.py b/tests/test_queries.py index 142a026..98125ea 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -2,17 +2,21 @@ class MockQuery(Query): + + def __init__(self) -> None: + super().__init__("/foo") + def _valid_state(self) -> bool: return True def test_query_headers_initially_empty(): - query = MockQuery("/foo") + query = MockQuery() assert query.headers == {} def test_bearer_token_adds_header(): - query = MockQuery("/foo") + query = MockQuery() query.headers["foo"] = "bar" query.bearer_token("bearertoken") @@ -20,14 +24,14 @@ def test_bearer_token_adds_header(): def test_bearer_token_does_not_clobber_other_headers(): - query = MockQuery("/foo") + query = MockQuery() query.bearer_token("bearertoken") assert query.headers["Authorization"] == "Bearer bearertoken" def test_bearer_token_replaces_existing_auth_header(): - query = MockQuery("/foo") + query = MockQuery() query.token("token") query.bearer_token("bearertoken") @@ -35,14 +39,14 @@ def test_bearer_token_replaces_existing_auth_header(): def test_token_adds_header(): - query = MockQuery("/foo") + query = MockQuery() query.token("token") assert query.headers["Authorization"] == "token" def test_token_does_not_clobber_other_headers(): - query = MockQuery("/foo") + query = MockQuery() query.headers["foo"] = "bar" query.token("token") @@ -50,8 +54,20 @@ def test_token_does_not_clobber_other_headers(): def test_token_replaces_existing_auth_header(): - query = MockQuery("/foo") + query = MockQuery() query.bearer_token("bearertoken") query.token("token") assert query.headers["Authorization"] == "token" + + +def test_singular_unknown_parameter(): + query = MockQuery().parameters(unknown_parameter="foo") + + assert query.params["unknown_parameter"] == "foo" + + +def test_plural_unknown_parameter(): + query = MockQuery().parameters(unknown_parameter=["foo", "bar"]) + + assert query.params["unknown_parameter[]"] == ("foo", "bar") diff --git a/tests/test_service.py b/tests/test_service.py index 2b9d412..47e1d34 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -78,7 +78,7 @@ def test_token(self): self.assertIn("Authorization", query.headers) self.assertEqual(query.headers["Authorization"], "123TOKEN") - def bearer_test_token(self): + def test_bearer_token(self): query = ServiceQuery() query.bearer_token("123TOKEN")