New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

python-cmr

Package Overview
Dependencies
Maintainers
2
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

python-cmr - pypi Package Compare versions

Comparing version
0.10.0
to
0.11.0
cmr/py.typed
+275
-236

@@ -5,12 +5,24 @@ """

try:
from urllib.parse import quote
except ImportError:
from urllib import pathname2url as quote
from datetime import datetime, timezone
from abc import abstractmethod
from datetime import date, datetime, timezone
from inspect import getmembers, ismethod
from re import search
from typing_extensions import (
Any,
List,
Literal,
MutableMapping,
Optional,
Sequence,
Self,
Set,
SupportsFloat,
Tuple,
TypeAlias,
Union,
override,
)
from urllib.parse import quote
from requests import get, exceptions
import requests
from dateutil.parser import parse as dateutil_parse

@@ -22,4 +34,10 @@

DateLike: TypeAlias = Union[str, date, datetime]
DayNightFlag: TypeAlias = Union[
Literal["day"], Literal["night"], Literal["unspecified"]
]
FloatLike: TypeAlias = Union[str, SupportsFloat]
PointLike: TypeAlias = Tuple[FloatLike, FloatLike]
class Query(object):
class Query:
"""

@@ -37,11 +55,11 @@ Base class for all CMR queries.

def __init__(self, route, mode=CMR_OPS):
self.params = {}
self.options = {}
def __init__(self, route: str, mode: str = CMR_OPS):
self.params: MutableMapping[str, Any] = {}
self.options: MutableMapping[str, Any] = {}
self._route = route
self.mode(mode)
self.concept_id_chars = []
self.headers = None
self.concept_id_chars: Set[str] = set()
self.headers: MutableMapping[str, str] = {}
def get(self, limit=2000):
def get(self, limit: int = 2000) -> Sequence[Any]:
"""

@@ -54,38 +72,37 @@ Get all results up to some limit, even if spanning multiple pages.

page_size = min(limit, 2000)
url = self._build_url()
results = []
headers = dict(self.headers or {})
more_results = True
while more_results == True:
n_results = 0
# Only get what we need
page_size = min(limit - len(results), page_size)
response = get(url, headers=self.headers, params={'page_size': page_size})
if self.headers == None:
self.headers = {}
self.headers['cmr-search-after'] = response.headers.get('cmr-search-after')
while more_results:
# Only get what we need on the last page.
page_size = min(limit - n_results, 2000)
response = requests.get(
url, headers=headers, params={"page_size": page_size}
)
response.raise_for_status()
try:
response.raise_for_status()
except exceptions.HTTPError as ex:
raise RuntimeError(ex.response.text)
# Explicitly track the number of results we have because the length
# of the results list will only match the number of entries fetched
# when the format is JSON. Otherwise, the length of the results
# list is the number of *pages* fetched, not the number of *items*.
n_results += page_size
if self._format == "json":
latest = response.json()['feed']['entry']
else:
latest = [response.text]
results.extend(latest)
if page_size > len(response.json()['feed']['entry']) or len(results) >= limit:
more_results = False
# This header is transient. We need to get rid of it before we do another different query
if self.headers['cmr-search-after']:
del self.headers['cmr-search-after']
results.extend(
response.json()["feed"]["entry"]
if self._format == "json"
else [response.text]
)
if cmr_search_after := response.headers.get("cmr-search-after"):
headers["cmr-search-after"] = cmr_search_after
more_results = n_results < limit and cmr_search_after is not None
return results
def hits(self):
def hits(self) -> int:
"""

@@ -100,12 +117,8 @@ Returns the number of hits the current query will return. This is done by

response = get(url, headers=self.headers, params={'page_size': 0})
response = requests.get(url, headers=self.headers, params={"page_size": 0})
response.raise_for_status()
try:
response.raise_for_status()
except exceptions.HTTPError as ex:
raise RuntimeError(ex.response.text)
return int(response.headers["CMR-Hits"])
def get_all(self):
def get_all(self) -> Sequence[Any]:
"""

@@ -121,3 +134,3 @@ Returns all of the results for the query. This will call hits() first to determine how many

def parameters(self, **kwargs):
def parameters(self, **kwargs: Any) -> Self:
"""

@@ -129,15 +142,11 @@ Provide query parameters as keyword arguments. The keyword needs to match the name

:returns: Query instance
:returns: self
"""
# build a dictionary of method names and their reference
methods = {}
for name, func in getmembers(self, predicate=ismethod):
methods[name] = func
methods = dict(getmembers(self, predicate=ismethod))
for key, val in kwargs.items():
# verify the key matches one of our methods
if key not in methods:
raise ValueError("Unknown key {}".format(key))
raise ValueError(f"Unknown key {key}")

@@ -152,3 +161,3 @@ # call the method

def format(self, output_format="json"):
def format(self, output_format: str = "json") -> Self:
"""

@@ -158,3 +167,3 @@ Sets the format for the returned results.

:param output_format: Preferred output format
:returns: Query instance
:returns: self
"""

@@ -172,5 +181,5 @@

# if we got here, we didn't find a matching format
raise ValueError("Unsupported format '{}'".format(output_format))
raise ValueError(f"Unsupported format: '{output_format}'")
def _build_url(self):
def _build_url(self) -> str:
"""

@@ -194,7 +203,7 @@ Builds the URL that will be used to query CMR.

for list_val in val:
formatted_params.append("{}[]={}".format(key, list_val))
formatted_params.append(f"{key}[]={list_val}")
elif isinstance(val, bool):
formatted_params.append("{}={}".format(key, str(val).lower()))
formatted_params.append(f"{key}={str(val).lower()}")
else:
formatted_params.append("{}={}".format(key, val))
formatted_params.append(f"{key}={val}")

@@ -204,3 +213,3 @@ params_as_string = "&".join(formatted_params)

# encode options
formatted_options = []
formatted_options: List[str] = []
for param_key in self.options:

@@ -211,24 +220,13 @@ for option_key, val in self.options[param_key].items():

if not isinstance(val, bool):
raise ValueError("parameter '{}' with option '{}' must be a boolean".format(
param_key,
option_key
))
raise TypeError(
f"parameter '{param_key}' with option '{option_key}' must be a boolean"
)
formatted_options.append("options[{}][{}]={}".format(
param_key,
option_key,
str(val).lower()
))
formatted_options.append(f"options[{param_key}][{option_key}]={str(val).lower()}")
options_as_string = "&".join(formatted_options)
res = "{}.{}?{}&{}".format(
self._base_url,
self._format,
params_as_string,
options_as_string
)
res = res.rstrip('&')
return res
res = f"{self._base_url}.{self._format}?{params_as_string}&{options_as_string}"
return res.rstrip('&')
def concept_id(self, IDs):
def concept_id(self, IDs: Union[str, Sequence[str]]) -> Self:
"""

@@ -244,3 +242,3 @@ Filter by concept ID (ex: C1299783579-LPDAAC_ECS or G1327299284-LPDAAC_ECS, T12345678-LPDAAC_ECS, S12345678-LPDAAC_ECS)

:param IDs: concept ID(s) to search by. Can be provided as a string or list of strings.
:returns: Query instance
:returns: self
"""

@@ -255,3 +253,4 @@

raise ValueError(
"Only concept ids that begin with '{}' can be provided: {}".format(self.concept_id_chars, ID))
f"Only concept IDs that begin with '{self.concept_id_chars}' can be provided: {ID}"
)

@@ -262,3 +261,3 @@ self.params["concept_id"] = IDs

def provider(self, provider):
def provider(self, provider: str) -> Self:
"""

@@ -268,3 +267,3 @@ Filter by provider.

:param provider: provider of tool.
:returns: Query instance
:returns: self
"""

@@ -278,3 +277,4 @@

def _valid_state(self):
@abstractmethod
def _valid_state(self) -> bool:
"""

@@ -289,3 +289,3 @@ Determines if the Query is in a valid state based on the parameters and options

def mode(self, mode=CMR_OPS):
def mode(self, mode: str = CMR_OPS) -> Self:
"""

@@ -296,3 +296,4 @@ Sets the mode of the api target to the given URL

:param mode: Mode to set the query to target
:throws: Will throw if provided None
:returns: self
:raises: Will raise if provided None
"""

@@ -302,5 +303,6 @@ if mode is None:

self._base_url = str(mode) + self._route
self._base_url = mode + self._route
return self
def token(self, token):
def token(self, token: str) -> Self:
"""

@@ -310,3 +312,3 @@ Add token into authorization headers.

:param token: Token from EDL Echo-Token or NASA Launchpad token.
:returns: Query instance
:returns: self
"""

@@ -321,3 +323,3 @@

def bearer_token(self, bearer_token):
def bearer_token(self, bearer_token: str) -> Self:
"""

@@ -327,3 +329,3 @@ Add token into authorization headers.

:param token: Token from EDL token.
:returns: Query instance
:returns: self
"""

@@ -334,3 +336,3 @@

self.headers = {'Authorization': 'Bearer ' + bearer_token}
self.headers = {'Authorization': f'Bearer {bearer_token}'}

@@ -345,3 +347,3 @@ return self

def online_only(self, online_only=True):
def online_only(self, online_only: bool = True) -> Self:
"""

@@ -352,3 +354,3 @@ Only match granules that are listed online and not available for download.

:param online_only: True to require granules only be online
:returns: Query instance
:returns: self
"""

@@ -367,3 +369,8 @@

def temporal(self, date_from, date_to, exclude_boundary=False):
def temporal(
self,
date_from: Optional[DateLike],
date_to: Optional[DateLike],
exclude_boundary: bool = False,
) -> Self:
"""

@@ -384,3 +391,3 @@ Filter by an open or closed date range.

# process each date into a datetime object
def convert_to_string(date, default):
def convert_to_string(date: Optional[DateLike], default: datetime) -> str:
"""

@@ -402,4 +409,5 @@ Returns the argument as an ISO 8601 or empty string.

except TypeError:
msg = f"Date must be a date object or ISO 8601 string, not {date.__class__.__name__}."
raise TypeError(msg)
raise TypeError(
f"Date must be a date object or ISO 8601 string, not {date.__class__.__name__}."
) from None
date = date.replace(tzinfo=timezone.utc)

@@ -409,5 +417,5 @@ else:

date = date if date.tzinfo else date.replace(tzinfo=timezone.utc)
# convert aware datetime to utc datetime
date = date.astimezone(timezone.utc)
date = date.astimezone(timezone.utc)

@@ -420,5 +428,4 @@ return date.strftime(iso_8601)

# if we have both dates, make sure from isn't later than to
if date_from and date_to:
if date_from > date_to:
raise ValueError("date_from must be earlier than date_to.")
if date_from and date_to and date_from > date_to:
raise ValueError("date_from must be earlier than date_to.")

@@ -429,3 +436,3 @@ # good to go, make sure we have a param list

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

@@ -439,3 +446,3 @@ if exclude_boundary:

def short_name(self, short_name):
def short_name(self, short_name: str) -> Self:
"""

@@ -445,3 +452,3 @@ Filter by short name (aka product or collection name).

:param short_name: name of collection
:returns: Query instance
:returns: self
"""

@@ -455,3 +462,3 @@

def version(self, version):
def version(self, version: str) -> Self:
"""

@@ -462,3 +469,3 @@ Filter by version. Note that CMR defines this as a string. For example,

:param version: version string
:returns: Query instance
:returns: self
"""

@@ -472,3 +479,3 @@

def point(self, lon, lat):
def point(self, lon: FloatLike, lat: FloatLike) -> Self:
"""

@@ -479,8 +486,5 @@ Filter by granules that include a geographic point.

:param lat: latitude of geographic point
:returns: Query instance
:returns: self
"""
if not lat or not lon:
return self
# coordinates must be a float

@@ -490,7 +494,7 @@ lon = float(lon)

self.params['point'] = "{},{}".format(lon, lat)
self.params['point'] = f"{lon},{lat}"
return self
def circle(self, lon: float, lat: float, dist: int):
def circle(self, lon: FloatLike, lat: FloatLike, dist: FloatLike) -> Self:
"""Filter by granules within the circle around lat/lon

@@ -501,3 +505,3 @@

:param dist: distance in meters around waypoint (lat,lon)
:returns: Query instance
:returns: self
"""

@@ -508,3 +512,3 @@ self.params['circle'] = f"{lon},{lat},{dist}"

def polygon(self, coordinates):
def polygon(self, coordinates: Sequence[PointLike]) -> Self:
"""

@@ -515,3 +519,3 @@ Filter by granules that overlap a polygonal area. Must be used in combination with a

:param coordinates: list of (lon, lat) tuples
:returns: Query instance
:returns: self
"""

@@ -526,7 +530,11 @@

except TypeError:
raise ValueError("A line must be an iterable of coordinate tuples. Ex: [(90,90), (91, 90), ...]")
raise TypeError(
f"A line must be an iterable of coordinate tuples. Ex: [(90,90), (91, 90), ...]; got {type(coordinates)}."
) from None
# polygon requires at least 4 pairs of coordinates
if len(coordinates) < 4:
raise ValueError("A polygon requires at least 4 pairs of coordinates.")
raise ValueError(
f"A polygon requires at least 4 pairs of coordinates; got {len(coordinates)}."
)

@@ -540,3 +548,5 @@ # convert to floats

if as_floats[0] != as_floats[-2] or as_floats[1] != as_floats[-1]:
raise ValueError("Coordinates of the last pair must match the first pair.")
raise ValueError(
f"Coordinates of the last pair must match the first pair: {coordinates[0]} != {coordinates[-1]}"
)

@@ -550,3 +560,9 @@ # convert to strings

def bounding_box(self, lower_left_lon, lower_left_lat, upper_right_lon, upper_right_lat):
def bounding_box(
self,
lower_left_lon: FloatLike,
lower_left_lat: FloatLike,
upper_right_lon: FloatLike,
upper_right_lat: FloatLike,
) -> Self:
"""

@@ -560,10 +576,7 @@ Filter by granules that overlap a bounding box. Must be used in combination with

:param upper_right_lat: upper right latitude of the box
:returns: Query instance
:returns: self
"""
self.params["bounding_box"] = "{},{},{},{}".format(
float(lower_left_lon),
float(lower_left_lat),
float(upper_right_lon),
float(upper_right_lat)
self.params["bounding_box"] = (
f"{float(lower_left_lon)},{float(lower_left_lat)},{float(upper_right_lon)},{float(upper_right_lat)}"
)

@@ -573,3 +586,3 @@

def line(self, coordinates):
def line(self, coordinates: Sequence[PointLike]) -> Self:
"""

@@ -580,3 +593,3 @@ Filter by granules that overlap a series of connected points. Must be used in combination

:param coordinates: a list of (lon, lat) tuples
:returns: Query instance
:returns: self
"""

@@ -591,7 +604,11 @@

except TypeError:
raise ValueError("A line must be an iterable of coordinate tuples. Ex: [(90,90), (91, 90), ...]")
raise TypeError(
f"A line must be an iterable of coordinate tuples. Ex: [(90,90), (91, 90), ...]; got {type(coordinates)}."
) from None
# need at least 2 pairs of coordinates
if len(coordinates) < 2:
raise ValueError("A line requires at least 2 pairs of coordinates.")
raise ValueError(
f"A line requires at least 2 pairs of coordinates; got {len(coordinates)}."
)

@@ -610,3 +627,3 @@ # make sure they're all floats

def downloadable(self, downloadable=True):
def downloadable(self, downloadable: bool = True) -> Self:
"""

@@ -617,3 +634,3 @@ Only match granules that are available for download. The opposite of this

:param downloadable: True to require granules be downloadable
:returns: Query instance
:returns: self
"""

@@ -632,3 +649,3 @@

def entry_title(self, entry_title):
def entry_title(self, entry_title: str) -> Self:
"""

@@ -638,3 +655,3 @@ Filter by the collection entry title.

:param entry_title: Entry title of the collection
:returns: Query instance
:returns: self
"""

@@ -654,8 +671,10 @@

def __init__(self, mode=CMR_OPS):
def __init__(self, mode: str = CMR_OPS):
Query.__init__(self, "granules", mode)
self.concept_id_chars = ['G', 'C']
self.concept_id_chars = {"G", "C"}
def orbit_number(self, orbit1, orbit2=None):
""""
def orbit_number(
self, orbit1: FloatLike, orbit2: Optional[FloatLike] = None
) -> Self:
""" "
Filter by the orbit number the granule was acquired during. Either a single

@@ -666,7 +685,7 @@ orbit can be targeted or a range of orbits.

:param orbit2: upper limit of range
:returns: Query instance
:returns: self
"""
if orbit2:
self.params['orbit_number'] = quote('{},{}'.format(str(orbit1), str(orbit2)))
self.params['orbit_number'] = quote(f'{str(orbit1)},{str(orbit2)}')
else:

@@ -677,3 +696,3 @@ self.params['orbit_number'] = orbit1

def day_night_flag(self, day_night_flag):
def day_night_flag(self, day_night_flag: DayNightFlag) -> Self:
"""

@@ -683,3 +702,3 @@ Filter by period of the day the granule was collected during.

:param day_night_flag: "day", "night", or "unspecified"
:returns: Query instance
:returns: self
"""

@@ -690,11 +709,12 @@

day_night_flag = day_night_flag.lower()
if day_night_flag.lower() not in ["day", "night", "unspecified"]:
raise ValueError(
"day_night_flag must be 'day', 'night', or 'unspecified': "
f"{day_night_flag!r}."
)
if day_night_flag not in ['day', 'night', 'unspecified']:
raise ValueError("day_night_flag must be day, night or unspecified.")
self.params['day_night_flag'] = day_night_flag
self.params["day_night_flag"] = day_night_flag.lower()
return self
def cloud_cover(self, min_cover=0, max_cover=100):
def cloud_cover(self, min_cover: FloatLike = 0, max_cover: FloatLike = 100) -> Self:
"""

@@ -705,3 +725,3 @@ Filter by the percentage of cloud cover present in the granule.

:param max_cover: maximum percentage of cloud cover
:returns: Query instance
:returns: self
"""

@@ -718,10 +738,18 @@

if minimum > maxiumum:
raise ValueError("Please ensure min_cloud_cover is lower than max cloud cover")
raise ValueError(
"Please ensure min cloud cover is less than max cloud cover"
)
except ValueError:
raise ValueError("Please ensure min_cover and max_cover are both floats")
raise ValueError(
"Please ensure min_cover and max_cover are both floats"
) from None
except TypeError:
raise TypeError(
"Please ensure min_cover and max_cover are both convertible to floats"
) from None
self.params['cloud_cover'] = "{},{}".format(min_cover, max_cover)
self.params['cloud_cover'] = f"{min_cover},{max_cover}"
return self
def instrument(self, instrument=""):
def instrument(self, instrument: str) -> Self:
"""

@@ -731,3 +759,3 @@ Filter by the instrument associated with the granule.

:param instrument: name of the instrument
:returns: Query instance
:returns: self
"""

@@ -741,3 +769,3 @@

def platform(self, platform=""):
def platform(self, platform: str) -> Self:
"""

@@ -747,3 +775,3 @@ Filter by the satellite platform the granule came from.

:param platform: name of the satellite
:returns: Query instance
:returns: self
"""

@@ -757,4 +785,3 @@

def sort_key(self, sort_key=""):
def sort_key(self, sort_key: str) -> Self:
"""

@@ -765,42 +792,46 @@ See

Filter some defined sort_key;
Filter some defined sort_key;
use negative (-) for start_date and end_date to sort by ascending
:param sort_key: name of the sort key
:returns: Query instance
:returns: self
"""
valid_sort_keys = [
'campaign',
'entry_title',
'dataset_id',
'data_size',
'end_date',
'-end_date'
'granule_ur',
'producer_granule_id'
'project',
'provider',
'readable_granule_name',
'short_name',
'-start_date',
'start_date',
'version',
'platform',
'instrument',
'sensor',
'day_night_flag',
'online_only',
'browsable',
'browse_only',
'cloud_cover',
'revision_date']
# also covers if empty string
if sort_key not in valid_sort_keys:
raise ValueError("Please provide a valid sort_key for granules query see https://cmr.earthdata.nasa.gov/search/site/docs/search/api.html#sorting-granule-results for valid sort_keys")
valid_sort_keys = {
'campaign',
'entry_title',
'dataset_id',
'data_size',
'end_date',
'granule_ur',
'producer_granule_id',
'project',
'provider',
'readable_granule_name',
'short_name',
'start_date',
'version',
'platform',
'instrument',
'sensor',
'day_night_flag',
'online_only',
'browsable',
'browse_only',
'cloud_cover',
'revision_date',
}
# also covers if empty string and allows for '-' prefix (for descending order)
if not isinstance(sort_key, str) or sort_key.lstrip('-') not in valid_sort_keys:
raise ValueError(
"Please provide a valid sort key for granules query. See"
" https://cmr.earthdata.nasa.gov/search/site/docs/search/api.html#sorting-granule-results"
" for valid sort keys."
)
self.params['sort_key'] = sort_key
return self
def granule_ur(self, granule_ur=""):
def granule_ur(self, granule_ur: str) -> Self:
"""

@@ -811,3 +842,3 @@ Filter by the granules unique ID. Note this will result in at most one granule

:param granule_ur: UUID of a granule
:returns: Query instance
:returns: self
"""

@@ -820,4 +851,6 @@

return self
def readable_granule_name(self, readable_granule_name=""):
def readable_granule_name(
self, readable_granule_name: Union[str, Sequence[str]]
) -> Self:
"""

@@ -832,3 +865,3 @@ Filter by the readable granule name (producer_granule_id if present, otherwise producer_granule_id).

:param readable_granule_name: granule name or substring
:returns: Query instance
:returns: self
"""

@@ -838,3 +871,3 @@

readable_granule_name = [readable_granule_name]
self.params["readable_granule_name"] = readable_granule_name

@@ -845,3 +878,4 @@ self.options["readable_granule_name"] = {"pattern": True}

def _valid_state(self):
@override
def _valid_state(self) -> bool:

@@ -865,5 +899,5 @@ # spatial params must be paired with a collection limiting parameter

def __init__(self, mode=CMR_OPS):
def __init__(self, mode: str = CMR_OPS):
Query.__init__(self, "collections", mode)
self.concept_id_chars = ['C']
self.concept_id_chars = {"C"}
self._valid_formats_regex.extend([

@@ -873,3 +907,3 @@ "dif", "dif10", "opendata", "umm_json", "umm_json_v[0-9]_[0-9]"

def archive_center(self, center):
def archive_center(self, center: str) -> Self:
"""

@@ -879,3 +913,3 @@ Filter by the archive center that maintains the collection.

:param archive_center: name of center as a string
:returns: Query instance
:returns: self
"""

@@ -888,3 +922,3 @@

def keyword(self, text):
def keyword(self, text: str) -> Self:
"""

@@ -896,3 +930,3 @@ Case insentive and wildcard (*) search through over two dozen fields in

:param text: text to search for
:returns: Query instance
:returns: self
"""

@@ -905,3 +939,3 @@

def native_id(self, native_ids):
def native_id(self, native_ids: Union[str, Sequence[str]]) -> Self:
"""

@@ -911,3 +945,3 @@ Filter by native id.

:param native_id: native id for collection
:returns: Query instance
:returns: self
"""

@@ -922,3 +956,3 @@

def tool_concept_id(self, IDs):
def tool_concept_id(self, IDs: Union[str, Sequence[str]]) -> Self:
"""

@@ -930,3 +964,3 @@ Filter collections associated with tool concept ID (ex: TL2092786348-POCLOUD)

:param IDs: tool concept ID(s) to search by. Can be provided as a string or list of strings.
:returns: Query instance
:returns: self
"""

@@ -940,3 +974,3 @@

if ID.strip()[0] != "T":
raise ValueError("Only tool concept ID's can be provided (begin with 'T'): {}".format(ID))
raise ValueError(f"Only tool concept ID's can be provided (begin with 'T'): {ID}")

@@ -947,3 +981,3 @@ self.params["tool_concept_id"] = IDs

def service_concept_id(self, IDs):
def service_concept_id(self, IDs: Union[str, Sequence[str]]) -> Self:
"""

@@ -955,3 +989,3 @@ Filter collections associated with service ID (ex: S1962070864-POCLOUD)

:param IDs: service concept ID(s) to search by. Can be provided as a string or list of strings.
:returns: Query instance
:returns: self
"""

@@ -965,3 +999,5 @@

if ID.strip()[0] != "S":
raise ValueError("Only service concept ID's can be provided (begin with 'S'): {}".format(ID))
raise ValueError(
f"Only service concept IDs can be provided (begin with 'S'): {ID}"
)

@@ -972,3 +1008,4 @@ self.params["service_concept_id"] = IDs

def _valid_state(self):
@override
def _valid_state(self) -> bool:
return True

@@ -982,3 +1019,3 @@

def get(self, limit=2000):
def get(self, limit: int = 2000) -> Sequence[Any]:
"""

@@ -994,13 +1031,11 @@ Get all results up to some limit, even if spanning multiple pages.

results = []
results: List[Any] = []
page = 1
while len(results) < limit:
response = get(url, params={'page_size': page_size, 'page_num': page})
response = requests.get(
url, params={"page_size": page_size, "page_num": page}
)
response.raise_for_status()
try:
response.raise_for_status()
except exceptions.HTTPError as ex:
raise RuntimeError(ex.response.text)
if self._format == "json":

@@ -1019,3 +1054,3 @@ latest = response.json()['items']

def native_id(self, native_ids):
def native_id(self, native_ids: Union[str, Sequence[str]]) -> Self:
"""

@@ -1025,3 +1060,3 @@ Filter by native id.

:param native_id: native id for tool or service
:returns: Query instance
:returns: self
"""

@@ -1036,3 +1071,3 @@

def name(self, name):
def name(self, name: str) -> Self:
"""

@@ -1042,3 +1077,3 @@ Filter by name.

:param name: name of service or tool.
:returns: Query instance
:returns: self
"""

@@ -1058,5 +1093,5 @@

def __init__(self, mode=CMR_OPS):
def __init__(self, mode: str = CMR_OPS):
Query.__init__(self, "tools", mode)
self.concept_id_chars = ['T']
self.concept_id_chars = {"T"}
self._valid_formats_regex.extend([

@@ -1066,3 +1101,4 @@ "dif", "dif10", "opendata", "umm_json", "umm_json_v[0-9]_[0-9]"

def _valid_state(self):
@override
def _valid_state(self) -> bool:
return True

@@ -1076,5 +1112,5 @@

def __init__(self, mode=CMR_OPS):
def __init__(self, mode: str = CMR_OPS):
Query.__init__(self, "services", mode)
self.concept_id_chars = ['S']
self.concept_id_chars = {"S"}
self._valid_formats_regex.extend([

@@ -1084,3 +1120,4 @@ "dif", "dif10", "opendata", "umm_json", "umm_json_v[0-9]_[0-9]"

def _valid_state(self):
@override
def _valid_state(self) -> bool:
return True

@@ -1090,5 +1127,6 @@

class VariableQuery(ToolServiceVariableBaseQuery):
def __init__(self, mode=CMR_OPS):
def __init__(self, mode: str = CMR_OPS):
Query.__init__(self, "variables", mode)
self.concept_id_chars = ['V']
self.concept_id_chars = {"V"}
self._valid_formats_regex.extend([

@@ -1098,3 +1136,4 @@ "dif", "dif10", "opendata", "umm_json", "umm_json_v[0-9]_[0-9]"

def _valid_state(self):
@override
def _valid_state(self) -> bool:
return True
+196
-139
Metadata-Version: 2.1
Name: python-cmr
Version: 0.10.0
Version: 0.11.0
Summary: Python wrapper to the NASA Common Metadata Repository (CMR) API.

@@ -9,12 +9,20 @@ Home-page: https://github.com/nasa/python_cmr

Author-email: nasa/python_cmr@github.com
Requires-Python: >=3.8,<4.0
Requires-Python: >=3.8.1,<4.0.0
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Framework :: IPython
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.8
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
Requires-Dist: requests (>=2.26.0,<3.0.0)
Requires-Dist: typing-extensions (>=4.11.0,<5.0.0)
Project-URL: Repository, https://github.com/nasa/python_cmr

@@ -33,3 +41,8 @@ Description-Content-Type: text/markdown

[![PyPI](https://img.shields.io/pypi/v/python_cmr.svg)](https://pypi.python.org/pypi/python_cmr)
[![Downloads](https://img.shields.io/pypi/dm/python_cmr)](https://pypistats.org/packages/python_cmr)
[![Python versions](https://img.shields.io/pypi/pyversions/python_cmr.svg)](https://pypi.python.org/pypi/python_cmr)
[![Build Status](https://github.com/nasa/python_cmr/actions/workflows/python-app.yml/badge.svg)](https://github.com/nasa/python_cmr/actions)
[![CodeQL](https://github.com/nasa/python_cmr/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/nasa/python_cmr/actions/workflows/codeql-analysis.yml)
[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)

@@ -43,22 +56,31 @@ Python CMR is an easy to use wrapper to the NASA

>>> from cmr import CollectionQuery, GranuleQuery, ToolQuery, ServiceQuery, VariableQuery
```python
from cmr import CollectionQuery, GranuleQuery, ToolQuery, ServiceQuery, VariableQuery
>>> api = CollectionQuery()
>>> collections = api.archive_center("LP DAAC").keyword("AST_L1*").get(5)
api = CollectionQuery()
collections = api.archive_center("LP DAAC").keyword("AST_L1*").get(5)
>>> for collection in collections:
>>> print(collection["short_name"])
AST_L1A
AST_L1AE
AST_L1T
print("Collections:")
for collection in collections:
print(collection["short_name"])
>>> api = GranuleQuery()
>>> granules = api.short_name("AST_L1T").point(-112.73, 42.5).get(3)
api = GranuleQuery()
granules = api.short_name("AST_L1T").point(-112.73, 42.5).get(3)
>>> for granule in granules:
>>> print(granule["title"])
SC:AST_L1T.003:2149105822
SC:AST_L1T.003:2149105820
SC:AST_L1T.003:2149155037
print("Granule Titles:")
for granule in granules:
print(granule["title"])
```
``` text
Collections:
AST_L1A
AST_L1AE
AST_L1T
Granule Titles:
SC:AST_L1T.003:2149105822
SC:AST_L1T.003:2149105820
SC:AST_L1T.003:2149155037
```
Installation

@@ -69,9 +91,13 @@ ============

$ pip install python-cmr
```plain
pip install python-cmr
```
To install from github, perhaps to try out the dev branch:
To install from GitHub, perhaps to try out the dev branch:
$ git clone https://github.com/nasa/python_cmr
$ cd python-cmr
$ pip install .
```plain
git clone https://github.com/nasa/python_cmr
cd python-cmr
pip install .
```

@@ -87,131 +113,143 @@ Examples

# search for granules matching a specific product/short_name
>>> api.short_name("AST_L1T")
```python
# search for granules matching a specific product/short_name
api.short_name("AST_L1T")
# search for granules matching a specific version
>>> api.version("006")
# search for granules matching a specific version
api.version("006")
# search for granules at a specific longitude and latitude
>>> api.point(-112.73, 42.5)
# search for granules at a specific longitude and latitude
api.point(-112.73, 42.5)
# search for granules in an area bound by a box (lower left lon/lat, upper right lon/lat)
>>> api.bounding_box(-112.70, 42.5, -110, 44.5)
# search for granules in an area bound by a box (lower left lon/lat, upper right lon/lat)
api.bounding_box(-112.70, 42.5, -110, 44.5)
# search for granules in a polygon (these need to be in counter clockwise order and the
# last coordinate must match the first in order to close the polygon)
>>> api.polygon([(-100, 40), (-110, 40), (-105, 38), (-100, 40)])
# search for granules in a polygon (these need to be in counter clockwise order and the
# last coordinate must match the first in order to close the polygon)
api.polygon([(-100, 40), (-110, 40), (-105, 38), (-100, 40)])
# search for granules in a line
>>> api.line([(-100, 40), (-90, 40), (-95, 38)])
# search for granules in a line
api.line([(-100, 40), (-90, 40), (-95, 38)])
# search for granules in an open or closed date range
>>> 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 in an open or closed date range
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())
# only include granules available for download
>>> api.downloadable()
# only include granules available for download
api.downloadable()
# only include granules that are unavailable for download
>>> api.online_only()
# only include granules that are unavailable for download
api.online_only()
# search for collections/granules associated with or identified by concept IDs
# note: often the ECHO collection ID can be used here as well
# note: when using CollectionQuery, only collection concept IDs can be passed
# note: when uses GranuleQuery, passing a collection's concept ID will filter by granules associated
# with that particular collection.
>>> api.concept_id("C1299783579-LPDAAC_ECS")
>>> api.concept_id(["G1327299284-LPDAAC_ECS", "G1326330014-LPDAAC_ECS"])
# search for collections/granules associated with or identified by concept IDs
# note: often the ECHO collection ID can be used here as well
# note: when using CollectionQuery, only collection concept IDs can be passed
# note: when uses GranuleQuery, passing a collection's concept ID will filter by granules associated
# with that particular collection.
api.concept_id("C1299783579-LPDAAC_ECS")
api.concept_id(["G1327299284-LPDAAC_ECS", "G1326330014-LPDAAC_ECS"])
# search by provider
>>> api.provider('POCLOUD')
# search non-ops CMR environment
>>> from cmr import CMR_UAT
>>> api.mode(CMR_UAT)
# search by provider
api.provider('POCLOUD')
# search non-ops CMR environment
from cmr import CMR_UAT
api.mode(CMR_UAT)
```
Granule searches support these methods (in addition to the shared methods above):
# search for a granule by its unique ID
>>> api.granule_ur("SC:AST_L1T.003:2150315169")
# search for granules from a specific orbit
>>> api.orbit_number(5000)
# search for a granule by name
>>> api.short_name("MOD09GA").readable_granule_name(["*h32v08*","*h30v13*"])
```python
# search for a granule by its unique ID
api.granule_ur("SC:AST_L1T.003:2150315169")
# search for granules from a specific orbit
api.orbit_number(5000)
# search for a granule by name
api.short_name("MOD09GA").readable_granule_name(["*h32v08*","*h30v13*"])
# filter by the day/night flag
>>> api.day_night_flag("day")
# filter by the day/night flag
api.day_night_flag("day")
# filter by cloud cover percentage range
>>> api.cloud_cover(25, 75)
# filter by cloud cover percentage range
api.cloud_cover(25, 75)
# filter by specific instrument or platform
>>> api.instrument("MODIS")
>>> api.platform("Terra")
# filter by specific instrument or platform
api.instrument("MODIS")
api.platform("Terra")
# filter by a sort_key note: sort_keys are require some other fields to find some existing granules before they can be sorted
# filter by a sort_key note: sort_keys are require some other fields to find
# some existing granules before they can be sorted
api.parameters(short_name="OMNO2", version="003", provider='GES_DISC', sort_key='-start_date')
```
>>> api.parameters(short_name="OMNO2", version="003", provider='GES_DISC', sort_key='-start_date')
Collection searches support these methods (in addition to the shared methods above):
# search for collections from a specific archive center
>>> api.archive_center("LP DAAC")
```python
# search for collections from a specific archive center
api.archive_center("LP DAAC")
# case insensitive, wildcard enabled text search through most collection fields
>>> api.keyword("M*D09")
# case insensitive, wildcard enabled text search through most collection fields
api.keyword("M*D09")
# search by native_id
>>> api.native_id('native_id')
# search by native_id
api.native_id('native_id')
# filter by tool concept id
>>> api.tool_concept_id('TL2092786348-POCLOUD')
# filter by tool concept id
api.tool_concept_id('TL2092786348-POCLOUD')
# filter by service concept id
>>> api.service_concept_id('S1962070864-POCLOUD')
# filter by service concept id
api.service_concept_id('S1962070864-POCLOUD')
```
Service searches support the following methods
# Search via provider
>>> api = ServiceQuery()
>>> api.provider('POCLOUD')
# Search via native_id
>>> api.native_id('POCLOUD_podaac_l2_cloud_subsetter')
```python
# Search via provider
api = ServiceQuery()
api.provider('POCLOUD')
# Search via name
>>> api.name('PODAAC L2 Cloud Subsetter')
# Search via native_id
api.native_id('POCLOUD_podaac_l2_cloud_subsetter')
# Search via concept_id
>>> api.concept_id('S1962070864-POCLOUD')
# Search via name
api.name('PODAAC L2 Cloud Subsetter')
# Search via concept_id
api.concept_id('S1962070864-POCLOUD')
```
Tool searches support the following methods
# Search via provider
>>> api = ToolQuery()
>>> api.provider('POCLOUD')
```python
# Search via provider
api = ToolQuery()
api.provider('POCLOUD')
# Search via native_id
>>> api.native_id('POCLOUD_hitide')
# Search via native_id
api.native_id('POCLOUD_hitide')
# Search via name
>>> api.name('hitide')
# Search via name
api.name('hitide')
# Search via concept_id
>>> api.concept_id('TL2092786348-POCLOUD')
# Search via concept_id
api.concept_id('TL2092786348-POCLOUD')
```
Variable searches support the following methods
# Search via provider
>>> api = VariableQuery()
>>> api.provider('POCLOUD')
```python
# Search via provider
api = VariableQuery()
api.provider('POCLOUD')
# Search via native_id
>>> api.native_id('JASON_CS_S6A_L2_AMR_RAD_STATIC_CALIBRATION-AMR_Side_1-acc_lat')
# Search via native_id
api.native_id('JASON_CS_S6A_L2_AMR_RAD_STATIC_CALIBRATION-AMR_Side_1-acc_lat')
# Search via name
>>> api.name('/AMR_Side_1/acc_lat')
# Search via name
api.name('/AMR_Side_1/acc_lat')
# Search via concept_id
>>> api.concept_id('V2112019824-POCLOUD')
# Search via concept_id
api.concept_id('V2112019824-POCLOUD')
```

@@ -221,8 +259,10 @@ As an alternative to chaining methods together to set the parameters of your query, a method exists to allow you to pass

# search for AST_L1T version 003 granules at latitude 42, longitude -100
>>> api.parameters(
short_name="AST_L1T",
version="003",
point=(-100, 42)
)
```python
# search for AST_L1T version 003 granules at latitude 42, longitude -100
api.parameters(
short_name="AST_L1T",
version="003",
point=(-100, 42)
)
```

@@ -234,13 +274,15 @@ Note: the kwarg key should match the name of a method from the above examples, and the value should be a tuple if it's a

# inspect the number of results the query will return without downloading the results
>>> print(api.hits())
```python
# inspect the number of results the query will return without downloading the results
print(api.hits())
# retrieve 100 granules
>>> granules = api.get(100)
# retrieve 100 granules
granules = api.get(100)
# retrieve 25,000 granules
>>> granules = api.get(25000)
# retrieve 25,000 granules
granules = api.get(25000)
# retrieve all the granules possible for the query
>>> granules = api.get_all() # this is a shortcut for api.get(api.hits())
# retrieve all the granules possible for the query
granules = api.get_all() # this is a shortcut for api.get(api.hits())
```

@@ -250,11 +292,15 @@ By default the responses will return as json and be accessible as a list of python dictionaries. Other formats can be

>>> granules = api.format("echo10").get(100)
```python
granules = api.format("echo10").get(100)
```
We can add token to the api calls by setting headers using the following functions:
# Use token function for EDL echo-token or launchpad token
>>> api.token(token)
```python
# Use token function for EDL echo-token or launchpad token
api.token(token)
# Use bearer token function for EDL bearer tokens
>>> api.bearer_token(token)
# Use bearer token function for EDL bearer tokens
api.bearer_token(token)
```

@@ -278,6 +324,7 @@ The following formats are supported for both granule and collection queries:

- opendata
- umm\_json
- umm\_json\_vX\_Y (ex: umm\_json\_v1\_9)
- umm_json
- umm_json_vX_Y (ex: umm_json_v1_9)
# Developing
Developing
==========

@@ -287,5 +334,7 @@ python-cmr uses the [poetry](https://python-poetry.org/) build system. Download and install poetry before starting

## Install Dependencies
Install Dependencies
--------------------
With dev dependencies:
```shell

@@ -296,2 +345,3 @@ poetry install

Without dev dependencies:
```shell

@@ -301,3 +351,4 @@ poetry install --no-dev

## Update Dependencies
Update Dependencies
-------------------

@@ -308,3 +359,4 @@ ```shell

## Add new Dependency
Add new Dependency
------------------

@@ -314,3 +366,5 @@ ```shell

```
Development-only dependency:
```shell

@@ -320,3 +374,4 @@ poetry add --dev pytest

## Build project
Build project
-------------

@@ -327,3 +382,4 @@ ```shell

## Lint project
Lint project
------------

@@ -334,3 +390,4 @@ ```shell

## Run Tests
Run Tests
---------

@@ -337,0 +394,0 @@ ```shell

@@ -0,4 +1,8 @@

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "python-cmr"
version = "0.10.0"
version = "0.11.0"
description = "Python wrapper to the NASA Common Metadata Repository (CMR) API."

@@ -10,3 +14,15 @@ authors = ["python_cmr <nasa/python_cmr@github.com>"]

classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules"
"Development Status :: 4 - Beta",
"Environment :: Console",
"Framework :: IPython",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]

@@ -16,13 +32,32 @@ packages = [{ include = "cmr" }]

[tool.poetry.dependencies]
python = "^3.8"
python = "^3.8.1"
requests = "^2.26.0"
python-dateutil = "^2.8.2"
typing-extensions = "^4.11.0"
[tool.poetry.dev-dependencies]
flake8 = "^4.0.1"
[tool.poetry.group.dev.dependencies]
flake8 = "^7.0.0"
mypy = "^1.9.0"
pytest = "^6.2.5"
vcrpy = "^5.1.0"
types-python-dateutil = "^2.9.0.20240316"
vcrpy = "^6.0.0"
types-requests = "^2.31"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]
mypy_path = ["cmr", "tests"]
exclude = ".venv/|venv/"
pretty = true
strict = true
[[tool.mypy.overrides]]
module = [
"tests.*",
]
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = [
# TODO Remove when this is released: https://github.com/kevin1024/vcrpy/issues/780
"vcr.*",
]
ignore_missing_imports = true
+185
-136

@@ -11,3 +11,8 @@ This repository is a copy

[![PyPI](https://img.shields.io/pypi/v/python_cmr.svg)](https://pypi.python.org/pypi/python_cmr)
[![Downloads](https://img.shields.io/pypi/dm/python_cmr)](https://pypistats.org/packages/python_cmr)
[![Python versions](https://img.shields.io/pypi/pyversions/python_cmr.svg)](https://pypi.python.org/pypi/python_cmr)
[![Build Status](https://github.com/nasa/python_cmr/actions/workflows/python-app.yml/badge.svg)](https://github.com/nasa/python_cmr/actions)
[![CodeQL](https://github.com/nasa/python_cmr/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/nasa/python_cmr/actions/workflows/codeql-analysis.yml)
[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)

@@ -21,22 +26,31 @@ Python CMR is an easy to use wrapper to the NASA

>>> from cmr import CollectionQuery, GranuleQuery, ToolQuery, ServiceQuery, VariableQuery
```python
from cmr import CollectionQuery, GranuleQuery, ToolQuery, ServiceQuery, VariableQuery
>>> api = CollectionQuery()
>>> collections = api.archive_center("LP DAAC").keyword("AST_L1*").get(5)
api = CollectionQuery()
collections = api.archive_center("LP DAAC").keyword("AST_L1*").get(5)
>>> for collection in collections:
>>> print(collection["short_name"])
AST_L1A
AST_L1AE
AST_L1T
print("Collections:")
for collection in collections:
print(collection["short_name"])
>>> api = GranuleQuery()
>>> granules = api.short_name("AST_L1T").point(-112.73, 42.5).get(3)
api = GranuleQuery()
granules = api.short_name("AST_L1T").point(-112.73, 42.5).get(3)
>>> for granule in granules:
>>> print(granule["title"])
SC:AST_L1T.003:2149105822
SC:AST_L1T.003:2149105820
SC:AST_L1T.003:2149155037
print("Granule Titles:")
for granule in granules:
print(granule["title"])
```
``` text
Collections:
AST_L1A
AST_L1AE
AST_L1T
Granule Titles:
SC:AST_L1T.003:2149105822
SC:AST_L1T.003:2149105820
SC:AST_L1T.003:2149155037
```
Installation

@@ -47,9 +61,13 @@ ============

$ pip install python-cmr
```plain
pip install python-cmr
```
To install from github, perhaps to try out the dev branch:
To install from GitHub, perhaps to try out the dev branch:
$ git clone https://github.com/nasa/python_cmr
$ cd python-cmr
$ pip install .
```plain
git clone https://github.com/nasa/python_cmr
cd python-cmr
pip install .
```

@@ -65,131 +83,143 @@ Examples

# search for granules matching a specific product/short_name
>>> api.short_name("AST_L1T")
```python
# search for granules matching a specific product/short_name
api.short_name("AST_L1T")
# search for granules matching a specific version
>>> api.version("006")
# search for granules matching a specific version
api.version("006")
# search for granules at a specific longitude and latitude
>>> api.point(-112.73, 42.5)
# search for granules at a specific longitude and latitude
api.point(-112.73, 42.5)
# search for granules in an area bound by a box (lower left lon/lat, upper right lon/lat)
>>> api.bounding_box(-112.70, 42.5, -110, 44.5)
# search for granules in an area bound by a box (lower left lon/lat, upper right lon/lat)
api.bounding_box(-112.70, 42.5, -110, 44.5)
# search for granules in a polygon (these need to be in counter clockwise order and the
# last coordinate must match the first in order to close the polygon)
>>> api.polygon([(-100, 40), (-110, 40), (-105, 38), (-100, 40)])
# search for granules in a polygon (these need to be in counter clockwise order and the
# last coordinate must match the first in order to close the polygon)
api.polygon([(-100, 40), (-110, 40), (-105, 38), (-100, 40)])
# search for granules in a line
>>> api.line([(-100, 40), (-90, 40), (-95, 38)])
# search for granules in a line
api.line([(-100, 40), (-90, 40), (-95, 38)])
# search for granules in an open or closed date range
>>> 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 in an open or closed date range
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())
# only include granules available for download
>>> api.downloadable()
# only include granules available for download
api.downloadable()
# only include granules that are unavailable for download
>>> api.online_only()
# only include granules that are unavailable for download
api.online_only()
# search for collections/granules associated with or identified by concept IDs
# note: often the ECHO collection ID can be used here as well
# note: when using CollectionQuery, only collection concept IDs can be passed
# note: when uses GranuleQuery, passing a collection's concept ID will filter by granules associated
# with that particular collection.
>>> api.concept_id("C1299783579-LPDAAC_ECS")
>>> api.concept_id(["G1327299284-LPDAAC_ECS", "G1326330014-LPDAAC_ECS"])
# search for collections/granules associated with or identified by concept IDs
# note: often the ECHO collection ID can be used here as well
# note: when using CollectionQuery, only collection concept IDs can be passed
# note: when uses GranuleQuery, passing a collection's concept ID will filter by granules associated
# with that particular collection.
api.concept_id("C1299783579-LPDAAC_ECS")
api.concept_id(["G1327299284-LPDAAC_ECS", "G1326330014-LPDAAC_ECS"])
# search by provider
>>> api.provider('POCLOUD')
# search non-ops CMR environment
>>> from cmr import CMR_UAT
>>> api.mode(CMR_UAT)
# search by provider
api.provider('POCLOUD')
# search non-ops CMR environment
from cmr import CMR_UAT
api.mode(CMR_UAT)
```
Granule searches support these methods (in addition to the shared methods above):
# search for a granule by its unique ID
>>> api.granule_ur("SC:AST_L1T.003:2150315169")
# search for granules from a specific orbit
>>> api.orbit_number(5000)
# search for a granule by name
>>> api.short_name("MOD09GA").readable_granule_name(["*h32v08*","*h30v13*"])
```python
# search for a granule by its unique ID
api.granule_ur("SC:AST_L1T.003:2150315169")
# search for granules from a specific orbit
api.orbit_number(5000)
# search for a granule by name
api.short_name("MOD09GA").readable_granule_name(["*h32v08*","*h30v13*"])
# filter by the day/night flag
>>> api.day_night_flag("day")
# filter by the day/night flag
api.day_night_flag("day")
# filter by cloud cover percentage range
>>> api.cloud_cover(25, 75)
# filter by cloud cover percentage range
api.cloud_cover(25, 75)
# filter by specific instrument or platform
>>> api.instrument("MODIS")
>>> api.platform("Terra")
# filter by specific instrument or platform
api.instrument("MODIS")
api.platform("Terra")
# filter by a sort_key note: sort_keys are require some other fields to find some existing granules before they can be sorted
# filter by a sort_key note: sort_keys are require some other fields to find
# some existing granules before they can be sorted
api.parameters(short_name="OMNO2", version="003", provider='GES_DISC', sort_key='-start_date')
```
>>> api.parameters(short_name="OMNO2", version="003", provider='GES_DISC', sort_key='-start_date')
Collection searches support these methods (in addition to the shared methods above):
# search for collections from a specific archive center
>>> api.archive_center("LP DAAC")
```python
# search for collections from a specific archive center
api.archive_center("LP DAAC")
# case insensitive, wildcard enabled text search through most collection fields
>>> api.keyword("M*D09")
# case insensitive, wildcard enabled text search through most collection fields
api.keyword("M*D09")
# search by native_id
>>> api.native_id('native_id')
# search by native_id
api.native_id('native_id')
# filter by tool concept id
>>> api.tool_concept_id('TL2092786348-POCLOUD')
# filter by tool concept id
api.tool_concept_id('TL2092786348-POCLOUD')
# filter by service concept id
>>> api.service_concept_id('S1962070864-POCLOUD')
# filter by service concept id
api.service_concept_id('S1962070864-POCLOUD')
```
Service searches support the following methods
# Search via provider
>>> api = ServiceQuery()
>>> api.provider('POCLOUD')
# Search via native_id
>>> api.native_id('POCLOUD_podaac_l2_cloud_subsetter')
```python
# Search via provider
api = ServiceQuery()
api.provider('POCLOUD')
# Search via name
>>> api.name('PODAAC L2 Cloud Subsetter')
# Search via native_id
api.native_id('POCLOUD_podaac_l2_cloud_subsetter')
# Search via concept_id
>>> api.concept_id('S1962070864-POCLOUD')
# Search via name
api.name('PODAAC L2 Cloud Subsetter')
# Search via concept_id
api.concept_id('S1962070864-POCLOUD')
```
Tool searches support the following methods
# Search via provider
>>> api = ToolQuery()
>>> api.provider('POCLOUD')
```python
# Search via provider
api = ToolQuery()
api.provider('POCLOUD')
# Search via native_id
>>> api.native_id('POCLOUD_hitide')
# Search via native_id
api.native_id('POCLOUD_hitide')
# Search via name
>>> api.name('hitide')
# Search via name
api.name('hitide')
# Search via concept_id
>>> api.concept_id('TL2092786348-POCLOUD')
# Search via concept_id
api.concept_id('TL2092786348-POCLOUD')
```
Variable searches support the following methods
# Search via provider
>>> api = VariableQuery()
>>> api.provider('POCLOUD')
```python
# Search via provider
api = VariableQuery()
api.provider('POCLOUD')
# Search via native_id
>>> api.native_id('JASON_CS_S6A_L2_AMR_RAD_STATIC_CALIBRATION-AMR_Side_1-acc_lat')
# Search via native_id
api.native_id('JASON_CS_S6A_L2_AMR_RAD_STATIC_CALIBRATION-AMR_Side_1-acc_lat')
# Search via name
>>> api.name('/AMR_Side_1/acc_lat')
# Search via name
api.name('/AMR_Side_1/acc_lat')
# Search via concept_id
>>> api.concept_id('V2112019824-POCLOUD')
# Search via concept_id
api.concept_id('V2112019824-POCLOUD')
```

@@ -199,8 +229,10 @@ As an alternative to chaining methods together to set the parameters of your query, a method exists to allow you to pass

# search for AST_L1T version 003 granules at latitude 42, longitude -100
>>> api.parameters(
short_name="AST_L1T",
version="003",
point=(-100, 42)
)
```python
# search for AST_L1T version 003 granules at latitude 42, longitude -100
api.parameters(
short_name="AST_L1T",
version="003",
point=(-100, 42)
)
```

@@ -212,13 +244,15 @@ Note: the kwarg key should match the name of a method from the above examples, and the value should be a tuple if it's a

# inspect the number of results the query will return without downloading the results
>>> print(api.hits())
```python
# inspect the number of results the query will return without downloading the results
print(api.hits())
# retrieve 100 granules
>>> granules = api.get(100)
# retrieve 100 granules
granules = api.get(100)
# retrieve 25,000 granules
>>> granules = api.get(25000)
# retrieve 25,000 granules
granules = api.get(25000)
# retrieve all the granules possible for the query
>>> granules = api.get_all() # this is a shortcut for api.get(api.hits())
# retrieve all the granules possible for the query
granules = api.get_all() # this is a shortcut for api.get(api.hits())
```

@@ -228,11 +262,15 @@ By default the responses will return as json and be accessible as a list of python dictionaries. Other formats can be

>>> granules = api.format("echo10").get(100)
```python
granules = api.format("echo10").get(100)
```
We can add token to the api calls by setting headers using the following functions:
# Use token function for EDL echo-token or launchpad token
>>> api.token(token)
```python
# Use token function for EDL echo-token or launchpad token
api.token(token)
# Use bearer token function for EDL bearer tokens
>>> api.bearer_token(token)
# Use bearer token function for EDL bearer tokens
api.bearer_token(token)
```

@@ -256,6 +294,7 @@ The following formats are supported for both granule and collection queries:

- opendata
- umm\_json
- umm\_json\_vX\_Y (ex: umm\_json\_v1\_9)
- umm_json
- umm_json_vX_Y (ex: umm_json_v1_9)
# Developing
Developing
==========

@@ -265,5 +304,7 @@ python-cmr uses the [poetry](https://python-poetry.org/) build system. Download and install poetry before starting

## Install Dependencies
Install Dependencies
--------------------
With dev dependencies:
```shell

@@ -274,2 +315,3 @@ poetry install

Without dev dependencies:
```shell

@@ -279,3 +321,4 @@ poetry install --no-dev

## Update Dependencies
Update Dependencies
-------------------

@@ -286,3 +329,4 @@ ```shell

## Add new Dependency
Add new Dependency
------------------

@@ -292,3 +336,5 @@ ```shell

```
Development-only dependency:
```shell

@@ -298,3 +344,4 @@ poetry add --dev pytest

## Build project
Build project
-------------

@@ -305,3 +352,4 @@ ```shell

## Lint project
Lint project
------------

@@ -312,3 +360,4 @@ ```shell

## Run Tests
Run Tests
---------

@@ -315,0 +364,0 @@ ```shell