python-cmr
Advanced tools
+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 | ||
| [](https://pypi.python.org/pypi/python_cmr) | ||
| [](https://pypistats.org/packages/python_cmr) | ||
| [](https://pypi.python.org/pypi/python_cmr) | ||
| [](https://github.com/nasa/python_cmr/actions) | ||
| [](https://github.com/nasa/python_cmr/actions/workflows/codeql-analysis.yml) | ||
| [](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 |
+44
-9
@@ -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 | ||
| [](https://pypi.python.org/pypi/python_cmr) | ||
| [](https://pypistats.org/packages/python_cmr) | ||
| [](https://pypi.python.org/pypi/python_cmr) | ||
| [](https://github.com/nasa/python_cmr/actions) | ||
| [](https://github.com/nasa/python_cmr/actions/workflows/codeql-analysis.yml) | ||
| [](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 |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
53859
6.08%7
16.67%787
6.06%