Skip to content

Commit 1439889

Browse files
authored
Merge pull request #31 from nasa/release/0.10.0
Release/0.10.0
2 parents d2e9e0c + 8946428 commit 1439889

File tree

16 files changed

+53515
-145
lines changed

16 files changed

+53515
-145
lines changed

.github/workflows/python-app.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ jobs:
1515
runs-on: ubuntu-latest
1616

1717
steps:
18-
- uses: actions/checkout@v3
18+
- uses: actions/checkout@v4
1919
- name: Set up Python 3.10
20-
uses: actions/setup-python@v4
20+
uses: actions/setup-python@v5
2121
with:
2222
python-version: "3.10"
2323
- name: Install Poetry
24-
uses: abatilo/actions-poetry@v2
24+
uses: abatilo/actions-poetry@v3
2525
with:
26-
poetry-version: 1.5.1
26+
poetry-version: 1.7.1
2727
- name: Install dependencies
2828
run: |
2929
poetry run python -m pip install --upgrade pip

.github/workflows/python-publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ jobs:
2121
id-token: write
2222

2323
steps:
24-
- uses: actions/checkout@v3
24+
- uses: actions/checkout@v4
2525
- name: Set up Python
26-
uses: actions/setup-python@v4
26+
uses: actions/setup-python@v5
2727
with:
2828
python-version: '3.x'
2929
- name: Install Poetry
30-
uses: abatilo/actions-poetry@v2
30+
uses: abatilo/actions-poetry@v3
3131
with:
3232
poetry-version: 1.5.1
3333
- name: Build package

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ venv/
44
tags
55
.venv
66
*.egg-info
7-
dist
7+
dist
8+
.vscode/*
9+
.DS_Store

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
99

10+
## [0.10.0]
11+
### Changed
12+
- [issues/29](https://github.com/nasa/python_cmr/issues/29) - Date parsing has been improved to accept more ISO-8601 string formats as well as timezone-aware datetime objects
13+
### Added
14+
- [pull/27](https://github.com/nasa/python_cmr/pull/27) New feature to search by `readable_granlue_name` https://cmr.earthdata.nasa.gov/search/site/docs/search/api.html#g-granule-ur-or-producer-granule-id
15+
### Fixed
16+
- [pull/27](https://github.com/nasa/python_cmr/pull/27) Fixed bug with constructing the `options` sent to CMR which was causing filters to not get applied correctly.
17+
- [pull/28](https://github.com/nasa/python_cmr/pull/28) Fixed bug where `KeyError` was thrown if search result contained 0 hits
18+
1019
## [0.9.0]
1120
### Added
1221
- [pull/17](https://github.com/nasa/python_cmr/pull/17) New feature that allows sort_keys to be passed into this Api up to the CMR. Used the valid sort_keys as of July 2023
@@ -45,7 +54,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4554
## [Older]
4655
- Prior releases of this software originated from https://github.com/jddeal/python-cmr/releases
4756

48-
[Unreleased]: https://github.com/nasa/python_cmr/compare/v0.8.0...HEAD
57+
[Unreleased]: https://github.com/nasa/python_cmr/compare/v0.10.0...HEAD
58+
[0.10.0]: https://github.com/nasa/python_cmr/compare/v0.9.0...v0.10.0
59+
[0.9.0]: https://github.com/nasa/python_cmr/compare/v0.8.0...v0.9.0
4960
[0.8.0]: https://github.com/nasa/python_cmr/compare/v0.7.0...v0.8.0
5061
[0.7.0]: https://github.com/nasa/python_cmr/compare/v0.6.0...v0.7.0
5162
[0.6.0]: https://github.com/nasa/python_cmr/compare/v0.5.0...v0.6.0

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ Granule searches support these methods (in addition to the shared methods above)
110110
>>> api.granule_ur("SC:AST_L1T.003:2150315169")
111111
# search for granules from a specific orbit
112112
>>> api.orbit_number(5000)
113+
# search for a granule by name
114+
>>> api.short_name("MOD09GA").readable_granule_name(["*h32v08*","*h30v13*"])
113115

114116
# filter by the day/night flag
115117
>>> api.day_night_flag("day")

cmr/queries.py

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
except ImportError:
88
from urllib import pathname2url as quote
99

10-
from datetime import datetime
10+
from datetime import datetime, timezone
1111
from inspect import getmembers, ismethod
1212
from re import search
1313

1414
from requests import get, exceptions
15+
from dateutil.parser import parse as dateutil_parse
1516

1617
CMR_OPS = "https://cmr.earthdata.nasa.gov/search/"
1718
CMR_UAT = "https://cmr.uat.earthdata.nasa.gov/search/"
@@ -51,10 +52,15 @@ def get(self, limit=2000):
5152
url = self._build_url()
5253

5354
results = []
54-
page = 1
55-
while len(results) < limit:
55+
more_results = True
56+
while more_results == True:
5657

57-
response = get(url, headers=self.headers, params={'page_size': page_size, 'page_num': page})
58+
# Only get what we need
59+
page_size = min(limit - len(results), page_size)
60+
response = get(url, headers=self.headers, params={'page_size': page_size})
61+
if self.headers == None:
62+
self.headers = {}
63+
self.headers['cmr-search-after'] = response.headers.get('cmr-search-after')
5864

5965
try:
6066
response.raise_for_status()
@@ -65,13 +71,16 @@ def get(self, limit=2000):
6571
latest = response.json()['feed']['entry']
6672
else:
6773
latest = [response.text]
68-
69-
if len(latest) == 0:
70-
break
71-
74+
7275
results.extend(latest)
73-
page += 1
74-
76+
77+
if page_size > len(response.json()['feed']['entry']) or len(results) >= limit:
78+
more_results = False
79+
80+
# This header is transient. We need to get rid of it before we do another different query
81+
if self.headers['cmr-search-after']:
82+
del self.headers['cmr-search-after']
83+
7584
return results
7685

7786
def hits(self):
@@ -195,7 +204,7 @@ def _build_url(self):
195204
formatted_options.append("options[{}][{}]={}".format(
196205
param_key,
197206
option_key,
198-
val
207+
str(val).lower()
199208
))
200209

201210
options_as_string = "&".join(formatted_options)
@@ -332,7 +341,7 @@ def temporal(self, date_from, date_to, exclude_boundary=False):
332341
"""
333342
Filter by an open or closed date range.
334343
335-
Dates can be provided as a datetime objects or ISO 8601 formatted strings. Multiple
344+
Dates can be provided as native date objects or ISO 8601 formatted strings. Multiple
336345
ranges can be provided by successive calls to this method before calling execute().
337346
338347
:param date_from: earliest date of temporal range
@@ -344,29 +353,37 @@ def temporal(self, date_from, date_to, exclude_boundary=False):
344353
iso_8601 = "%Y-%m-%dT%H:%M:%SZ"
345354

346355
# process each date into a datetime object
347-
def convert_to_string(date):
356+
def convert_to_string(date, default):
348357
"""
349358
Returns the argument as an ISO 8601 or empty string.
350359
"""
351360

352361
if not date:
353362
return ""
354363

355-
try:
356-
# see if it's datetime-like
357-
return date.strftime(iso_8601)
358-
except AttributeError:
364+
# handle str, date-like objects without time, and datetime objects
365+
if isinstance(date, str):
366+
# handle string by parsing with default
367+
date = dateutil_parse(date, default=default)
368+
elif not isinstance(date, datetime):
369+
# handle (naive by definition) date by converting to utc datetime
359370
try:
360-
# maybe it already is an ISO 8601 string
361-
datetime.strptime(date, iso_8601)
362-
return date
371+
date = datetime.combine(date, default.time())
363372
except TypeError:
364-
raise ValueError(
365-
"Please provide None, datetime objects, or ISO 8601 formatted strings."
366-
)
373+
msg = f"Date must be a date object or ISO 8601 string, not {date.__class__.__name__}."
374+
raise TypeError(msg)
375+
date = date.replace(tzinfo=timezone.utc)
376+
else:
377+
# pass aware datetime and handle naive datetime by assuming utc
378+
date = date if date.tzinfo else date.replace(tzinfo=timezone.utc)
379+
380+
# convert aware datetime to utc datetime
381+
date = date.astimezone(timezone.utc)
367382

368-
date_from = convert_to_string(date_from)
369-
date_to = convert_to_string(date_to)
383+
return date.strftime(iso_8601)
384+
385+
date_from = convert_to_string(date_from, datetime(1, 1, 1, 0, 0, 0, tzinfo=timezone.utc))
386+
date_to = convert_to_string(date_to, datetime(1, 12, 31, 23, 59, 59, tzinfo=timezone.utc))
370387

371388
# if we have both dates, make sure from isn't later than to
372389
if date_from and date_to:
@@ -733,6 +750,27 @@ def granule_ur(self, granule_ur=""):
733750

734751
self.params['granule_ur'] = granule_ur
735752
return self
753+
754+
def readable_granule_name(self, readable_granule_name=""):
755+
"""
756+
Filter by the readable granule name (producer_granule_id if present, otherwise producer_granule_id).
757+
758+
Can use wildcards for substring matching:
759+
760+
asterisk (*) will match any number of characters.
761+
question mark (?) will match exactly one character.
762+
763+
:param readable_granule_name: granule name or substring
764+
:returns: Query instance
765+
"""
766+
767+
if isinstance(readable_granule_name, str):
768+
readable_granule_name = [readable_granule_name]
769+
770+
self.params["readable_granule_name"] = readable_granule_name
771+
self.options["readable_granule_name"] = {"pattern": True}
772+
773+
return self
736774

737775
def _valid_state(self):
738776

0 commit comments

Comments
 (0)