Skip to content

Commit f2fa8f2

Browse files
authored
Merge pull request #527 from unity-sds/develop
release/9.6.0
2 parents b210a79 + ffe4493 commit f2fa8f2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+968
-43
lines changed

.github/workflows/makefile.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- run: |
2424
python3 "${GITHUB_WORKSPACE}/setup.py" install_lib
2525
- run: |
26-
python3 -m pip uninstall botocore boto3 -y
26+
python3 -m pip uninstall botocore boto3 s3transfer -y
2727
- run: |
2828
# make file runnable, might not be necessary
2929
chmod +x "${GITHUB_WORKSPACE}/ci.cd/create_aws_lambda_zip.sh"

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [9.6.0] - 2025-02-03
9+
### Changed
10+
- [#517](https://github.com/unity-sds/unity-data-services/pull/517) feat: stac browser oidc cookie
11+
12+
## [9.5.2] - 2025-01-31
13+
### Fixed
14+
- [#524](https://github.com/unity-sds/unity-data-services/pull/524) fix: default boto3 from aws already has s3transfer library
15+
16+
## [9.5.1] - 2025-01-17
17+
### Fixed
18+
- [#502](https://github.com/unity-sds/unity-data-services/pull/502) fix: get granules pagination
19+
20+
## [9.5.0] - 2025-01-17
21+
### Changed
22+
- [#499](https://github.com/unity-sds/unity-data-services/pull/499) feat: duplicate granules diff index
23+
24+
## [9.4.2] - 2025-01-17
25+
### Fixed
26+
- [#498](https://github.com/unity-sds/unity-data-services/pull/498) fix: restructure bbox for geoshape
27+
828
## [9.4.1] - 2024-12-18
929
### Fixed
1030
- [#489](https://github.com/unity-sds/unity-data-services/pull/489) fix: delete bug

cumulus_lambda_functions/lib/uds_db/granules_db_index.py

Lines changed: 98 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,25 @@ def __init__(self):
3434

3535
@staticmethod
3636
def to_es_bbox(bbox_array):
37+
# lon = x, lat = y
38+
# lon, lat, lon, lat
39+
# x can be 170 to -170
40+
# 170, 0, -170, 10
41+
minX, minY, maxX, maxY = bbox_array
42+
43+
# Ensure the values are properly sorted
44+
# if minX > maxX:
45+
# minX, maxX = maxX, minX
46+
if minY > maxY:
47+
minY, maxY = maxY, minY
48+
3749
return {
3850
"type": "envelope",
39-
"coordinates": [
40-
[bbox_array[0], bbox_array[3]], # Top-left corner (minLon, maxLat)
41-
[bbox_array[2], bbox_array[1]] # Bottom-right corner (maxLon, minLat)
42-
]
51+
"coordinates": [[minX, maxY], [maxX, minY]],
52+
# "coordinates": [
53+
# [bbox_array[0], bbox_array[3]], # Top-left corner (minLon, maxLat)
54+
# [bbox_array[2], bbox_array[1]] # Bottom-right corner (maxLon, minLat)
55+
# ]
4356
}
4457

4558
@staticmethod
@@ -152,16 +165,20 @@ def create_new_index(self, tenant, tenant_venue, es_mapping: dict):
152165
self.__es.swap_index_for_alias(write_perc_alias_name, current_perc_index_name, new_perc_index_name)
153166
try:
154167
self.__es.migrate_index_data(current_perc_index_name, new_perc_index_name)
155-
except Exception as e:
168+
except:
156169
LOGGER.exception(f'failed to migrate index data: {(current_perc_index_name, new_perc_index_name)}')
157170
return
158171

159-
def get_latest_index(self, tenant, tenant_venue):
172+
def get_latest_index_name(self, tenant, tenant_venue):
160173
write_alias_name = f'{DBConstants.granules_write_alias_prefix}_{tenant}_{tenant_venue}'.lower().strip()
161174
write_alias_name = self.__es.get_alias(write_alias_name)
162175
if len(write_alias_name) != 1:
163176
raise ValueError(f'missing index for {tenant}_{tenant_venue}. {write_alias_name}')
164177
latest_index_name = [k for k in write_alias_name.keys()][0]
178+
return latest_index_name
179+
180+
def get_latest_index(self, tenant, tenant_venue):
181+
latest_index_name = self.get_latest_index_name(tenant, tenant_venue)
165182
index_mapping = self.__es.get_index_mapping(latest_index_name)
166183
if index_mapping is None:
167184
raise ValueError(f'missing index: {latest_index_name}')
@@ -201,53 +218,106 @@ def get_entry(self, tenant: str, tenant_venue: str, doc_id: str, ):
201218
raise ValueError(f"no such granule: {doc_id}")
202219
return result
203220

204-
def delete_entry(self, tenant: str, tenant_venue: str, doc_id: str, ):
221+
def __query_by_id_local(self, tenant: str, tenant_venue: str, doc_id: str, ):
205222
read_alias_name = f'{DBConstants.granules_read_alias_prefix}_{tenant}_{tenant_venue}'.lower().strip()
206-
result = self.__es.query({
223+
dsl = {
207224
'size': 9999,
208-
'query': {'term': {'_id': doc_id}}
209-
}, read_alias_name)
210-
if result is None:
211-
raise ValueError(f"no such granule: {doc_id}")
212-
for each_granule in result['hits']['hits']:
225+
'sort': [
226+
{'properties.datetime': {'order': 'desc'}},
227+
{'id': {'order': 'asc'}}
228+
],
229+
'query': {
230+
'term': {'_id': doc_id}
231+
}
232+
}
233+
result = self.__es.query(dsl, read_alias_name)
234+
if result is None or len(result['hits']['hits']) < 1:
235+
return []
236+
return result['hits']['hits']
237+
238+
def __delete_old_entries(self, dsl_result):
239+
for each_granule in dsl_result:
213240
LOGGER.debug(f"deleting {each_granule['_id']} from {each_granule['_index']}")
214241
delete_result = self.__es.delete_by_query({
215242
'query': {'term': {'id': each_granule['_id']}}
216243
}, each_granule['_index'])
217244
LOGGER.debug(f'delete_result: {delete_result}')
218245
if delete_result is None:
219246
raise ValueError(f"error deleting {each_granule}")
247+
return
248+
249+
def delete_entry(self, tenant: str, tenant_venue: str, doc_id: str, ):
250+
result = self.__query_by_id_local(tenant, tenant_venue, doc_id)
251+
if len(result) < 1:
252+
raise ValueError(f"no such granule: {doc_id}")
253+
self.__delete_old_entries(result)
220254
return result
221255

222256
def update_entry(self, tenant: str, tenant_venue: str, json_body: dict, doc_id: str, ):
257+
# find existing doc_id
258+
# if not found, throw error. Cannot update
259+
# if found, check index.
260+
# if latest index, proceed with update
261+
# if older index, proceed with get + delete
262+
# tweak meta locally, and add it.
223263
write_alias_name = f'{DBConstants.granules_write_alias_prefix}_{tenant}_{tenant_venue}'.lower().strip()
224264
json_body['event_time'] = TimeUtils.get_current_unix_milli()
225-
self.__es.update_one(json_body, doc_id, index=write_alias_name) # TODO assuming granule_id is prefixed with collection id
226-
LOGGER.debug(f'custom_metadata indexed')
265+
existing_entries = self.__query_by_id_local(tenant, tenant_venue, doc_id)
266+
if len(existing_entries) < 1:
267+
raise ValueError(f'unable to update {doc_id} as it is not found. ')
268+
latest_index_name = self.get_latest_index_name(tenant, tenant_venue)
269+
existing_entry = existing_entries[0]
270+
if existing_entry['_index'] == latest_index_name:
271+
LOGGER.debug(f'{doc_id} in latest index: {latest_index_name}. continuing with update')
272+
self.__es.update_one(json_body, doc_id, index=write_alias_name) # TODO assuming granule_id is prefixed with collection id
273+
self.__delete_old_entries(existing_entries[1:])
274+
return
275+
LOGGER.debug(f'{doc_id} in older index: {latest_index_name} v. {existing_entry["_index"]}')
276+
new_doc = {**existing_entry['_source'], **json_body}
277+
self.__es.index_one(new_doc, doc_id, index=write_alias_name) # TODO assuming granule_id is prefixed with collection id
278+
self.__delete_old_entries(existing_entries)
227279
return
228280

229281
def add_entry(self, tenant: str, tenant_venue: str, json_body: dict, doc_id: str, ):
282+
# find existing doc_id
283+
# if not found, add it
284+
# if found, and it is in latest index, add it.
285+
# if found, and it is in older index, add current one, and delete the older one.
286+
230287
write_alias_name = f'{DBConstants.granules_write_alias_prefix}_{tenant}_{tenant_venue}'.lower().strip()
231288
json_body['event_time'] = TimeUtils.get_current_unix_milli()
232-
# TODO validate custom metadata vs the latest index to filter extra items
289+
existing_entries = self.__query_by_id_local(tenant, tenant_venue, doc_id)
290+
if len(existing_entries) < 1:
291+
self.__es.index_one(json_body, doc_id, index=write_alias_name) # TODO assuming granule_id is prefixed with collection id
292+
return
293+
latest_index_name = self.get_latest_index_name(tenant, tenant_venue)
294+
existing_entry = existing_entries[0]
295+
if existing_entry['_index'] == latest_index_name:
296+
self.__es.index_one(json_body, doc_id, index=write_alias_name) # TODO assuming granule_id is prefixed with collection id
297+
self.__delete_old_entries(existing_entries[1:])
298+
return
233299
self.__es.index_one(json_body, doc_id, index=write_alias_name) # TODO assuming granule_id is prefixed with collection id
234-
LOGGER.debug(f'custom_metadata indexed')
300+
self.__delete_old_entries(existing_entries)
301+
# TODO validate custom metadata vs the latest index to filter extra items
235302
return
236303

237304
def dsl_search(self, tenant: str, tenant_venue: str, search_dsl: dict):
238305
read_alias_name = f'{DBConstants.granules_read_alias_prefix}_{tenant}_{tenant_venue}'.lower().strip()
239-
if 'sort' not in search_dsl:
240-
search_result = self.__es.query(search_dsl,
241-
querying_index=read_alias_name) if 'sort' in search_dsl else self.__es.query(
242-
search_dsl, querying_index=read_alias_name)
306+
if 'sort' not in search_dsl: # We cannot paginate w/o sort. So, max is 10k items:
307+
# This also assumes "size" should be part of search_dsl
308+
search_result = self.__es.query(search_dsl, querying_index=read_alias_name)
243309
LOGGER.debug(f'search_finished: {len(search_result["hits"]["hits"])}')
244310
return search_result
311+
# we can run paginate search
245312
original_size = search_dsl['size'] if 'size' in search_dsl else 20
313+
total_size = -999
246314
result = []
247315
duplicates = set([])
248316
while len(result) < original_size:
249317
search_dsl['size'] = (original_size - len(result)) * 2
250-
search_result = self.__es.query_pages(search_dsl, querying_index=read_alias_name) if 'sort' in search_dsl else self.__es.query(search_dsl, querying_index=read_alias_name)
318+
search_result = self.__es.query_pages(search_dsl, querying_index=read_alias_name)
319+
if total_size == -999:
320+
total_size = self.__es.get_result_size(search_result)
251321
if len(search_result['hits']['hits']) < 1:
252322
break
253323
for each in search_result['hits']['hits']:
@@ -257,11 +327,16 @@ def dsl_search(self, tenant: str, tenant_venue: str, search_dsl: dict):
257327
search_dsl['search_after'] = search_result['hits']['hits'][-1]['sort']
258328

259329
LOGGER.debug(f'search_finished: {len(result)}')
330+
if len(result) > original_size:
331+
result = result[:original_size]
260332
return {
261333
'hits': {
262334
"total": {
263-
"value": len(result)
335+
"value": total_size,
264336
},
265337
'hits': result
266338
}
267339
}
340+
341+
342+

cumulus_lambda_functions/uds_api/dapa/granules_dapa_query_es.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __generate_es_dsl(self):
3939
if self.__filter_input is not None:
4040
query_terms.append(CqlParser('properties').transform(self.__filter_input))
4141
query_dsl = {
42-
'track_total_hits': True,
42+
'track_total_hits': self.__offset is None,
4343
'size': self.__limit,
4444
# "collapse": {"field": "id"},
4545
'sort': [
@@ -228,11 +228,11 @@ def start(self):
228228
each_granules_query_result_stripped['links'].append(self_link)
229229
self.__restructure_each_granule_result(each_granules_query_result_stripped)
230230

231-
pagination_link = '' if len(granules_query_result['hits']['hits']) < self.__limit else ','.join([k if isinstance(k, str) else str(k) for k in granules_query_result['hits']['hits'][-1]['sort']])
231+
pagination_link = '' if len(granules_query_result['hits']['hits']) < 1 else ','.join([k if isinstance(k, str) else str(k) for k in granules_query_result['hits']['hits'][-1]['sort']])
232232
return {
233233
'statusCode': 200,
234234
'body': {
235-
'numberMatched': {'total_size': result_size},
235+
'numberMatched': {'total_size': -1 if self.__offset is not None else result_size},
236236
'numberReturned': len(granules_query_result['hits']['hits']),
237237
'stac_version': '1.0.0',
238238
'type': 'FeatureCollection', # TODO correct name?

cumulus_lambda_functions/uds_api/misc_api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ async def stac_entry(request: Request, response: Response):
4646
@router.get(f'/stac_entry')
4747
@router.get(f'/stac_entry/')
4848
async def stac_entry(request: Request, response: Response):
49+
"""
50+
How to re-load UCS
51+
https://github.com/unity-sds/unity-data-services/issues/381#issuecomment-2201165672
52+
:param request:
53+
:param response:
54+
:return:
55+
"""
4956
request_headers = dict(request.headers)
5057
LOGGER.debug(f'stac_entry - request_headers: {request_headers}')
5158
print(request_headers)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><meta id="meta-description" name="description" content=""><title>STAC Browser</title><script defer="defer" src="/to/be/replaced/stac_browser/js/chunk-vendors.eae96ced.js"></script><script defer="defer" src="/to/be/replaced/stac_browser/js/app.79583f94.js"></script><link href="/to/be/replaced/stac_browser/css/chunk-vendors.de510de6.css" rel="stylesheet"><link href="/to/be/replaced/stac_browser/css/app.b5efa536.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but STAC Browser doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="stac-browser"></div></body></html>
1+
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><meta id="meta-description" name="description" content=""><title>STAC Browser</title><script defer="defer" src="/to/be/replaced/stac_browser/js/chunk-vendors.eae96ced.js"></script><script defer="defer" src="/to/be/replaced/stac_browser/js/app.ad7851bb.js"></script><link href="/to/be/replaced/stac_browser/css/chunk-vendors.de510de6.css" rel="stylesheet"><link href="/to/be/replaced/stac_browser/css/app.b5efa536.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but STAC Browser doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="stac-browser"></div></body></html>

cumulus_lambda_functions/uds_api/stac_browser/js/app.79583f94.js.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

cumulus_lambda_functions/uds_api/stac_browser/js/app.79583f94.js renamed to cumulus_lambda_functions/uds_api/stac_browser/js/app.ad7851bb.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cumulus_lambda_functions/uds_api/stac_browser/js/app.ad7851bb.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cwl/stage-in-daac/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Test stage in data from DAAC
2+
3+
Update `stac_json` value in stage-in-job-01.yml
4+
5+
Run the following command with a CWL runner.
6+
7+
`cwltool stage-in-workflow.cwl stage-in-job-01.yml`

cwl/stage-in-daac/stage-in-job-01.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
download_dir: "granules"
2+
stac_json: "https://cmr.earthdata.nasa.gov/stac/LPCLOUD/collections/EMITL1BRAD_001/items?limit=2"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env cwl-runner
2+
cwlVersion: v1.2
3+
class: Workflow
4+
label: Workflow that executes the Sounder SIPS end-to-end chirp rebinngin workflow
5+
6+
$namespaces:
7+
cwltool: http://commonwl.org/cwltool#
8+
9+
requirements:
10+
SubworkflowFeatureRequirement: {}
11+
StepInputExpressionRequirement: {}
12+
InlineJavascriptRequirement: {}
13+
NetworkAccess:
14+
networkAccess: true
15+
16+
inputs:
17+
download_dir: string
18+
stac_json: string
19+
20+
outputs:
21+
data:
22+
type: Directory
23+
outputSource: stage-in/download_dir
24+
25+
steps:
26+
stage-in:
27+
run: "stage-in.cwl"
28+
in:
29+
stac_json: stac_json
30+
download_dir: download_dir
31+
out: [download_dir]

cwl/stage-in-daac/stage-in.cwl

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
cwlVersion: v1.2
2+
class: CommandLineTool
3+
4+
baseCommand: ["DOWNLOAD"]
5+
6+
requirements:
7+
DockerRequirement:
8+
dockerPull: ghcr.io/unity-sds/unity-data-services:9.4.0
9+
NetworkAccess:
10+
networkAccess: true
11+
EnvVarRequirement:
12+
envDef:
13+
DOWNLOAD_DIR: $(runtime.outdir)/$(inputs.download_dir)
14+
STAC_JSON: $(inputs.stac_json)
15+
LOG_LEVEL: '10'
16+
PARALLEL_COUNT: '-1'
17+
DOWNLOAD_RETRY_WAIT_TIME: '30'
18+
DOWNLOAD_RETRY_TIMES: '5'
19+
DOWNLOADING_ROLES: 'data'
20+
21+
EDL_BASE_URL: 'https://urs.earthdata.nasa.gov/'
22+
EDL_USERNAME: '/sps/processing/workflows/edl_username'
23+
EDL_PASSWORD: '/sps/processing/workflows/edl_password'
24+
EDL_PASSWORD_TYPE: 'PARAM_STORE'
25+
26+
VERIFY_SSL: 'TRUE'
27+
STAC_AUTH_TYPE: 'NONE'
28+
29+
inputs:
30+
download_dir:
31+
type: string
32+
stac_json:
33+
type: string
34+
35+
outputs:
36+
download_dir:
37+
type: Directory
38+
outputBinding:
39+
glob: "$(inputs.download_dir)"

cwl/stage-in-json-str/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Test stage in data from DAAC
2+
3+
Update `stac_json` value in stage-in-job-01.yml
4+
5+
Run the following command with a CWL runner.
6+
7+
`cwltool stage-in-workflow.cwl stage-in-job-01.yml`
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
download_dir: "granules"
2+
stac_json: '{"type":"FeatureCollection","stac_version":"1.0.0","numberMatched":2,"numberReturned":2,"features":[{"properties":{"datetime":"2016-08-22T00:05:22.000Z","start_datetime":"2016-08-22T00:05:22.000Z","end_datetime":"2016-08-22T00:11:22.000Z"},"bbox":[-7.02,-60.32,26.31,-36.16],"assets":{"data":{"title":"Download SNDR.SS1330.CHIRP.20160822T0005.m06.g001.L1_AQ.std.v02_48.G.200425095850.nc","href":"https://raw.githubusercontent.com/unity-sds/unity-tutorial-application/main/test/stage_in/SNDR.SS1330.CHIRP.20160822T0005.m06.g001.L1_AQ.std.v02_48.G.200425095850.nc"}},"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[6.18,-36.16],[-7.02,-56.04],[23.24,-60.32],[26.31,-38.94],[6.18,-36.16]]]},"stac_extensions":[],"id":"G2040068613-GES_DISC","stac_version":"1.0.0","collection":"C2011289787-GES_DISC","links":[{"rel":"self","href":"https://cmr.earthdata.nasa.gov:443/search/concepts/G2040068613-GES_DISC.stac"},{"rel":"parent","href":"https://cmr.earthdata.nasa.gov:443/search/concepts/C2011289787-GES_DISC.stac"},{"rel":"collection","href":"https://cmr.earthdata.nasa.gov:443/search/concepts/C2011289787-GES_DISC.stac"},{"rel":"root","href":"https://cmr.earthdata.nasa.gov:443/search/"},{"rel":"via","href":"https://cmr.earthdata.nasa.gov:443/search/concepts/G2040068613-GES_DISC.json"},{"rel":"via","href":"https://cmr.earthdata.nasa.gov:443/search/concepts/G2040068613-GES_DISC.umm_json"}]},{"properties":{"datetime":"2016-08-22T00:11:22.000Z","start_datetime":"2016-08-22T00:11:22.000Z","end_datetime":"2016-08-22T00:17:22.000Z"},"bbox":[-43.78,-81.77028018298317,23.22,-56.18],"assets":{"data":{"title":"Download SNDR.SS1330.CHIRP.20160822T0011.m06.g002.L1_AQ.std.v02_48.G.200425095901.nc","href":"https://raw.githubusercontent.com/unity-sds/unity-tutorial-application/main/test/stage_in/SNDR.SS1330.CHIRP.20160822T0011.m06.g002.L1_AQ.std.v02_48.G.200425095901.nc"}},"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-7.16,-56.18],[-43.78,-71.72],[20.73,-81.77],[23.22,-60.47],[-7.16,-56.18]]]},"stac_extensions":[],"id":"G2040068619-GES_DISC","stac_version":"1.0.0","collection":"C2011289787-GES_DISC","links":[{"rel":"self","href":"https://cmr.earthdata.nasa.gov:443/search/concepts/G2040068619-GES_DISC.stac"},{"rel":"parent","href":"https://cmr.earthdata.nasa.gov:443/search/concepts/C2011289787-GES_DISC.stac"},{"rel":"collection","href":"https://cmr.earthdata.nasa.gov:443/search/concepts/C2011289787-GES_DISC.stac"},{"rel":"root","href":"https://cmr.earthdata.nasa.gov:443/search/"},{"rel":"via","href":"https://cmr.earthdata.nasa.gov:443/search/concepts/G2040068619-GES_DISC.json"},{"rel":"via","href":"https://cmr.earthdata.nasa.gov:443/search/concepts/G2040068619-GES_DISC.umm_json"}]}],"links":[{"rel":"self","href":"https://cmr.earthdata.nasa.gov:443/search/granules.stac?collection_concept_id=C2011289787-GES_DISC&temporal%5B%5D=2016-08-22T00%3A10%3A00%2C2016-08-22T00%3A15%3A00&page_num=1"},{"rel":"root","href":"https://cmr.earthdata.nasa.gov:443/search/"}],"context":{"returned":2,"limit":1000000,"matched":2}}'

0 commit comments

Comments
 (0)