Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to search by revision_date #68

Merged
merged 1 commit into from
Jul 23, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- Support searching for cloud-hosted collections
([#54](https://github.com/nasa/python_cmr/issues/54))
- Option to search for collections and granules by `revision_date` ([#67](https://github.com/nasa/python_cmr/issues/67))

### Fixed

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ api.temporal("2016-10-10T01:02:00Z", "2016-10-12T00:00:30Z")
api.temporal("2016-10-10T01:02:00Z", None)
api.temporal(datetime(2016, 10, 10, 1, 2, 0), datetime.now())

# search for granules by revision_date
api.revision_date("2022-05-16", "2024-06-30")

# only include granules available for download
api.downloadable()

Expand Down
74 changes: 62 additions & 12 deletions cmr/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,24 +338,19 @@ def online_only(self, online_only: bool = True) -> Self:

return self

def temporal(
def _format_date(
self,
date_from: Optional[DateLike],
date_to: Optional[DateLike],
exclude_boundary: bool = False,
) -> Self:
date_to: Optional[DateLike]
) -> Tuple[str, str]:
"""
Filter by an open or closed date range.

Dates can be provided as native date objects or ISO 8601 formatted strings. Multiple
ranges can be provided by successive calls to this method before calling execute().

Format dates into expected format for date queries.

:param date_from: earliest date of temporal range
:param date_to: latest date of temporal range
:param exclude_boundary: whether or not to exclude the date_from/to in the matched range
:returns: GranueQuery instance
:returns: Tuple instance
"""

iso_8601 = "%Y-%m-%dT%H:%M:%SZ"

# process each date into a datetime object
Expand Down Expand Up @@ -395,6 +390,61 @@ def convert_to_string(date: Optional[DateLike], default: datetime) -> str:
# if we have both dates, make sure from isn't later than to
if date_from and date_to and date_from > date_to:
raise ValueError("date_from must be earlier than date_to.")

return date_from, date_to

def revision_date(
self,
date_from: Optional[DateLike],
date_to: Optional[DateLike],
exclude_boundary: bool = False,
) -> Self:
"""
Filter by an open or closed date range for a query that captures updated items.

Dates can be provided as native date objects or ISO 8601 formatted strings. Multiple
ranges can be provided by successive calls to this method before calling execute().

:param date_from: earliest date of temporal range
:param date_to: latest date of temporal range
:param exclude_boundary: whether or not to exclude the date_from/to in the matched range
:returns: GranueQuery instance
"""

date_from, date_to = self._format_date(date_from, date_to)

# good to go, make sure we have a param list
if "revision_date" not in self.params:
self.params["revision_date"] = []

self.params["revision_date"].append(f"{date_from},{date_to}")

if exclude_boundary:
self.options["revision_date"] = {
"exclude_boundary": True
}

return self

def temporal(
self,
date_from: Optional[DateLike],
date_to: Optional[DateLike],
exclude_boundary: bool = False,
) -> Self:
"""
Filter by an open or closed date range for a temporal query.

Dates can be provided as native date objects or ISO 8601 formatted strings. Multiple
ranges can be provided by successive calls to this method before calling execute().

:param date_from: earliest date of temporal range
:param date_to: latest date of temporal range
:param exclude_boundary: whether or not to exclude the date_from/to in the matched range
:returns: GranueQuery instance
"""

date_from, date_to = self._format_date(date_from, date_to)

# good to go, make sure we have a param list
if "temporal" not in self.params:
Expand Down
5 changes: 5 additions & 0 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,8 @@ def test_invalid_cloud_hosted(self):

with self.assertRaises(TypeError):
query.cloud_hosted("Test_string_for_cloud_hosted_param") # type: ignore[arg-type]

def test_revision_date(self):
query = CollectionQuery()
collections = query.short_name("SWOT_L2_HR_RiverSP_reach_2.0").revision_date("2022-05-16", "2024-06-30").get_all()
self.assertEqual(collections[0]["dataset_id"], "SWOT Level 2 River Single-Pass Vector Reach Data Product, Version 2.0")
17 changes: 16 additions & 1 deletion tests/test_granule.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest
from datetime import datetime, timezone, timedelta
import json
import unittest

from cmr.queries import GranuleQuery

Expand Down Expand Up @@ -64,6 +65,20 @@ def test_circle_set(self):

self.assertIn(self.circle, query.params)
self.assertEqual(query.params[self.circle], "10.0,15.1,1000")

def test_revision_date(self):
query = GranuleQuery()
granules = query.short_name("SWOT_L2_HR_RiverSP_reach_2.0").revision_date("2024-07-05", "2024-07-05").format("umm_json").get_all()
granule_dict = {}
for granule in granules:
granule_json = json.loads(granule)
for item in granule_json["items"]:
native_id = item["meta"]["native-id"]
granule_dict[native_id] = item

self.assertIn("SWOT_L2_HR_RiverSP_Reach_017_312_AS_20240630T042656_20240630T042706_PIC0_01_swot", granule_dict.keys())
self.assertIn("SWOT_L2_HR_RiverSP_Reach_017_310_SI_20240630T023426_20240630T023433_PIC0_01_swot", granule_dict.keys())
self.assertIn( "SWOT_L2_HR_RiverSP_Reach_017_333_EU_20240630T225156_20240630T225203_PIC0_01_swot", granule_dict.keys())

def test_temporal_invalid_strings(self):
query = GranuleQuery()
Expand Down