Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- Add `attribute` method on granule and collection queries for CMR `attribute[]` search ([#104](https://github.com/nasa/python_cmr/issues/104))
- Add method `Query.results` for returning results as an iterator instead of sequence ([#37](https://github.com/nasa/python_cmr/issues/37))

### Changed
Expand Down
69 changes: 69 additions & 0 deletions cmr/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,75 @@ def platform(self, platform: str) -> Self:
self.params['platform'] = platform
return self

def attribute(self, *components: Union[str, FloatLike, Sequence[str]]) -> Self:
"""
Filter by additional attribute using CMR ``attribute[]``.

Pass a single preformatted CMR attribute string, separate components
(type, name, value and/or range bounds) that are joined with commas, or
a sequence of complete attribute strings. Call more than once to add
further ``attribute[]`` constraints. By default all must match; use
``option("attribute", "or", True)`` for any match. For range searches,
``option("attribute", "exclude_boundary", True)`` excludes range
endpoints. For granules, ``option("attribute", "exclude_collection", True)``
skips collection level attributes.

Examples:

.. code:: python

>>> query = GranuleQuery()
>>> query.attribute("PERCENTAGE") # doctest: +ELLIPSIS
<cmr.queries.GranuleQuery ...>
>>> query.attribute("float", "PERCENTAGE", 25.5) # doctest: +ELLIPSIS
<cmr.queries.GranuleQuery ...>
>>> query.attribute("string", "ID", "cosmic1c1-G25-200703252358") # doctest: +ELLIPSIS
<cmr.queries.GranuleQuery ...>
>>> query.attribute("float", "PERCENTAGE", 25.5, 30) # doctest: +ELLIPSIS
<cmr.queries.GranuleQuery ...>

When more than one component is given, commas inside each component are
escaped as ``\\,`` per the CMR Search API.

See `CMR collection additional attribute`_ and
`CMR granule additional attribute`_.

.. _CMR collection additional attribute:
https://cmr.earthdata.nasa.gov/search/site/docs/search/api.html#c-additional-attribute
.. _CMR granule additional attribute:
https://cmr.earthdata.nasa.gov/search/site/docs/search/api.html#g-additional-attribute

:param components: preformatted attribute string, type/name/value parts,
or a sequence of complete attribute strings
:returns: self
"""

if not components:
raise ValueError("Please provide an attribute name or CMR attribute[] value")

first = components[0]
if (
len(components) == 1
and isinstance(first, (list, tuple))
and not isinstance(first, (str, bytes))
):
values = [str(item) for item in first]
elif len(components) == 1:
values = [str(first)]
else:
escaped = [str(part).replace(",", r"\,") for part in components]
values = [",".join(escaped)]

if not values or any(not value for value in values):
raise ValueError("Please provide an attribute name or CMR attribute[] value")

if "attribute" not in self.params:
self.params["attribute"] = []

self.params["attribute"].extend(values)

return self


class GranuleQuery(GranuleCollectionBaseQuery):
"""
Expand Down
7 changes: 7 additions & 0 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ def _get_cassette_library_dir(self):
testdir = os.path.dirname(inspect.getfile(self.__class__))
return os.path.join(testdir, "fixtures", "vcr_cassettes")

def test_attribute(self):
query = CollectionQuery()
query.attribute("float", "PERCENTAGE", 25.5)

self.assertEqual(query.params["attribute"], ["float,PERCENTAGE,25.5"])
self.assertIn("attribute[]=float,PERCENTAGE,25.5", query._build_url())

def test_archive_center(self):
query = CollectionQuery()
query.archive_center("LP DAAC")
Expand Down
76 changes: 76 additions & 0 deletions tests/test_granule.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,82 @@ def test_invalid_mode_constructor(self):
with self.assertRaises(ValueError):
GranuleQuery(None) # type: ignore[arg-type]

def test_attribute_name_only(self):
query = GranuleQuery()
query.attribute("PERCENTAGE")

self.assertEqual(query.params["attribute"], ["PERCENTAGE"])

def test_attribute_typed_value(self):
query = GranuleQuery()
query.attribute("string", "ID", "cosmic1c1-G25-200703252358")

self.assertEqual(
query.params["attribute"],
["string,ID,cosmic1c1-G25-200703252358"],
)

def test_attribute_range_and_bounds(self):
query = GranuleQuery()
query.attribute("float", "PERCENTAGE", 25.5, 30)
query.attribute("float", "PERCENTAGE", 25.5, "")
query.attribute("float", "PERCENTAGE", "", 30)

self.assertEqual(
query.params["attribute"],
[
"float,PERCENTAGE,25.5,30",
"float,PERCENTAGE,25.5,",
"float,PERCENTAGE,,30",
],
)

def test_attribute_preformatted_and_list(self):
query = GranuleQuery()
query.attribute("float,PERCENTAGE,25.5")
query.attribute(["string,MISSION_NAME,Big Island\\, HI", "PERCENTAGE"])

self.assertEqual(
query.params["attribute"],
[
"float,PERCENTAGE,25.5",
"string,MISSION_NAME,Big Island\\, HI",
"PERCENTAGE",
],
)

def test_attribute_escapes_commas_in_components(self):
query = GranuleQuery()
query.attribute("string", "MISSION_NAME", "Big Island, HI")

self.assertEqual(
query.params["attribute"],
["string,MISSION_NAME,Big Island\\, HI"],
)

def test_attribute_in_url(self):
query = GranuleQuery()
query.short_name("gnssro_cosmic1_jpl_l1b")
query.attribute("string", "ID", "cosmic1c1-G25-200703252358")

url = query._build_url()
self.assertIn("attribute[]=string,ID,cosmic1c1-G25-200703252358", url)

def test_attribute_via_parameters(self):
query = GranuleQuery()
query.parameters(attribute=("string", "ID", "abc"))

self.assertEqual(query.params["attribute"], ["string,ID,abc"])

def test_attribute_empty_rejected(self):
query = GranuleQuery()

with self.assertRaises(ValueError):
query.attribute()

with self.assertRaises(ValueError):
query.attribute("")

def test_valid_parameters(self):
query = GranuleQuery()

Expand Down