| Title: | Audit the Revision and Production Lifecycle of NASA CMR Granules |
|---|---|
| Description: | Queries NASA's Common Metadata Repository (CMR) granule search API and distinguishes CMR metadata revision churn from actual changes to the underlying science data file, using UMM-JSON fields such as revision-id, ProviderDates, and DataGranule ProductionDateTime. Built to support decisions about when a downstream mirror (for example a Cloud-Optimized GeoTIFF conversion published to source.coop) needs to be regenerated, rather than reacting to every CMR metadata touch. |
| Authors: | Michael Sumner [aut, cre, cph] |
| Maintainer: | Michael Sumner <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.0.1 |
| Built: | 2026-10-15 00:54:25 UTC |
| Source: | https://github.com/hypertidy/cmrlifecycle |
Pages through cmr_find_collections() with page_num, pausing
delay seconds between requests, until every matching collection
has been fetched (or max_pages is hit). Useful for a full catalog
sweep – e.g. cmr_all_collections() with no filters returns
minimal metadata for all ~45,000 CMR collections, in ~23 requests
at the default page_size.
cmr_all_collections( keyword = NULL, short_name = NULL, provider = NULL, page_size = 2000, delay = 0.3, abstract = FALSE, max_pages = Inf, granule_count = TRUE )cmr_all_collections( keyword = NULL, short_name = NULL, provider = NULL, page_size = 2000, delay = 0.3, abstract = FALSE, max_pages = Inf, granule_count = TRUE )
keyword, short_name, provider
|
As in |
page_size |
Collections per request (CMR max, and the default here, is 2000). |
delay |
Seconds to pause between requests. Not a documented
CMR rate limit – just a politeness default for an unauthenticated,
multi-request sweep. Set to |
abstract |
Keep the |
max_pages |
Safety cap on the number of requests issued.
|
granule_count |
As in |
CMR's classic page_num/page_size paging is only documented to
work up to roughly 1,000,000 records (page_num * page_size); this
function stops with a warning if it would exceed that, rather than
silently looping. That ceiling is far above the current collection
count, so it's a safeguard, not an expected code path.
A tibble::tibble(), same columns as cmr_find_collections()
(minus abstract unless abstract = TRUE), one row per collection.
Reports progress via message() after each page.
Other collection functions:
cmr_collection_liveness(),
cmr_find_collections()
# every CMR collection, minimal columns (~23 requests, a few seconds # of `delay` pauses on top of network time) all_colls <- cmr_all_collections() # narrower sweeps are quick cmr_all_collections(provider = "LAADS")# every CMR collection, minimal columns (~23 requests, a few seconds # of `delay` pauses on top of network time) all_colls <- cmr_all_collections() # narrower sweeps are quick cmr_all_collections(provider = "LAADS")
Audit the CMR revision/production lifecycle of a collection's granules.
cmr_audit_revisions( short_name, from = as.Date("2024-01-01"), to = Sys.Date(), provider = NULL, gran_date_fun = .default_gran_date_fun, bounding_box = NULL, polygon = NULL )cmr_audit_revisions( short_name, from = as.Date("2024-01-01"), to = Sys.Date(), provider = NULL, gran_date_fun = .default_gran_date_fun, bounding_box = NULL, polygon = NULL )
short_name |
CMR collection short name |
from, to
|
Date range to sweep, chunked into 3-month CMR requests |
provider |
Optional CMR provider id (see |
gran_date_fun |
Function of the assembled tibble (with columns
|
bounding_box, polygon
|
Optional spatial filters, passed through
to every chunked |
A tibble::tibble() with one row per granule (deduplicated
across chunk boundaries), including gran_date and the lag columns
described in this file's header comment. Warns if gran_date comes
back entirely NA (see the MOD021KM example in the README).
Other granule functions:
cmr_find_granules(),
plot_cmr_lifecycle()
rev <- cmr_audit_revisions("MUR-JPL-L4-GLOB-v4.1", from = as.Date("2024-01-01")) plot_cmr_lifecycle(rev) # opaque GranuleUR (e.g. LAADS swath products) needs an explicit gran_date_fun cmr_audit_revisions("MOD021KM", from = as.Date("2026-08-01"), to = as.Date("2026-08-02"), gran_date_fun = function(df) as.Date(df$temporal_start))rev <- cmr_audit_revisions("MUR-JPL-L4-GLOB-v4.1", from = as.Date("2024-01-01")) plot_cmr_lifecycle(rev) # opaque GranuleUR (e.g. LAADS swath products) needs an explicit gran_date_fun cmr_audit_revisions("MOD021KM", from = as.Date("2026-08-01"), to = as.Date("2026-08-02"), gran_date_fun = function(df) as.Date(df$temporal_start))
Measures liveness directly, via a real cmr_find_granules() search
over a recent lookback window, rather than trusting UMM-C's
collection-level ends_at_present flag. That flag's reliability
turns out to depend heavily on the provider: a spot-check found
PO.DAAC leaves it FALSE on ~95% of its collections (including many
demonstrably still-active ones), while LAADS sets it reliably for
most of its catalog (see cmr_find_collections()'s docs). This
function sidesteps that unreliability by checking real recent
granule activity instead.
cmr_collection_liveness( short_name, provider = NULL, lookback_days = 60, delay = 0.2 )cmr_collection_liveness( short_name, provider = NULL, lookback_days = 60, delay = 0.2 )
short_name |
One or more CMR collection short names, as a
character vector – or a data.frame/tibble with a |
provider |
Optional CMR provider id(s): a single value applied
to every |
lookback_days |
How many days back from today to search for
granules. Default 60. Low-cadence collections (annual composites,
etc.) can show zero granules in 60 days while still genuinely
active – raise this if you suspect that, or cross-check
|
delay |
Seconds to pause between requests when |
A tibble::tibble() with one row per short_name:
short_name, provider (NA if not supplied), lookback_days,
recent_granule_count (subject to cmr_find_granules()'s
page_size cap – a count of 2000 may mean "at least 2000", not
exactly 2000), and is_live (recent_granule_count > 0).
Other collection functions:
cmr_all_collections(),
cmr_find_collections()
cmr_collection_liveness("MUR-JPL-L4-GLOB-v4.1") cmr_collection_liveness(c("VJ146A4", "MOD021KM"), provider = "LAADS") # straight from a filtered collection search cmr_all_collections(provider = "POCLOUD") |> dplyr::filter(!ends_at_present, is.na(temporal_end), granule_count > 0) |> cmr_collection_liveness()cmr_collection_liveness("MUR-JPL-L4-GLOB-v4.1") cmr_collection_liveness(c("VJ146A4", "MOD021KM"), provider = "LAADS") # straight from a filtered collection search cmr_all_collections(provider = "POCLOUD") |> dplyr::filter(!ends_at_present, is.na(temporal_end), granule_count > 0) |> cmr_collection_liveness()
Thin wrapper around CMR's collection search
(https://cmr.earthdata.nasa.gov/search/collections.umm_json), useful
for discovering a short_name to pass to cmr_audit_revisions() or
cmr_find_granules(), or for a quick keyword search across all collections
(title, abstract, science keywords, platforms, instruments, ...).
cmr_find_collections( keyword = NULL, short_name = NULL, provider = NULL, page_size = 100, page_num = 1, granule_count = TRUE )cmr_find_collections( keyword = NULL, short_name = NULL, provider = NULL, page_size = 100, page_num = 1, granule_count = TRUE )
keyword |
Free-text search term(s), e.g. |
short_name |
Exact (or CMR-wildcarded, with |
provider |
Optional CMR provider id (e.g. |
page_size |
Max collections to return per request (CMR hard limit 2000). |
page_num |
Which page of results to fetch (1-based). Mainly
useful via |
granule_count |
Fetch each collection's live granule count
(via a second request to |
A tibble::tibble() with one row per collection: short_name,
entry_title, concept_id, provider, version,
processing_level, abstract, region, s3_credentials_endpoint.
region is the AWS region (e.g. "us-west-2") a cloud-hosted
collection's data lives in – s3_urls from cmr_find_granules() are only
cheaply/directly readable from compute in that region; from
elsewhere you're reading over the public internet (egress charges,
no speed benefit), and some buckets block it outright. Both columns
are NA for collections with no direct S3 distribution (only an
https mirror). s3_credentials_endpoint is required regardless of
region: s3_urls aren't publicly readable – you exchange an
Earthdata Login token there for temporary AWS credentials.
temporal_start/temporal_end are the collection's declared
available date range (UMM-C TemporalExtents) – this is
provider-entered metadata, not derived from a live granule search,
so treat it as a stated range rather than a guarantee; for
collections with no UMM-G granules at all (a single static
download registered only at the collection level – some AADC
collections are like this) it's the only date range CMR has.
temporal_end is NA when ends_at_present is TRUE (an
actively-growing collection; any recorded end date would go stale
the moment a new granule lands). Be aware ends_at_present itself
is frequently left FALSE (unset) on collections that are, in
fact, still actively growing – a spot-check of 6 GHRSST-family
collections found only 1 correctly declared TRUE; the other 5
all had granules in the last 14 days despite FALSE. Treat
FALSE as "unset/unknown" rather than "this collection has
stopped" – check recent granules directly
(cmr_find_granules()) if liveness matters. If more collections
match than page_size allows, a message reports the total hit count – raise
page_size (or narrow keyword/provider) to see more, or use
cmr_all_collections() to page through everything. At least one
of keyword or short_name must be supplied (pass keyword = ""
to match every collection, as in cmr_all_collections()'s
default). The total hit count is also attached as the "hits"
attribute, for programmatic pagination.
Other collection functions:
cmr_all_collections(),
cmr_collection_liveness()
cmr_find_collections(keyword = "sea surface temperature", provider = "POCLOUD") cmr_find_collections(short_name = "MUR-JPL-L4-GLOB-v4.1")cmr_find_collections(keyword = "sea surface temperature", provider = "POCLOUD") cmr_find_collections(short_name = "MUR-JPL-L4-GLOB-v4.1")
Pull one page-set of granules for a date window, as UMM-JSON
cmr_find_granules( short_name, t0, t1, provider = NULL, page_size = 2000, bounding_box = NULL, polygon = NULL )cmr_find_granules( short_name, t0, t1, provider = NULL, page_size = 2000, bounding_box = NULL, polygon = NULL )
short_name |
CMR collection short name (e.g. "MUR-JPL-L4-GLOB-v4.1") |
t0, t1
|
Date range (inclusive), coercible with |
provider |
Optional CMR provider id, to disambiguate short names that exist under multiple providers |
page_size |
Max granules per request (CMR hard limit ~2000 for this non-scrolling approach) |
bounding_box |
Optional spatial filter: a length-4 numeric
vector |
polygon |
Optional spatial filter: a numeric vector of |
A tibble::tibble() with one row per granule, or NULL if
no granules matched. See cmr_audit_revisions() for the column
dictionary.
Other granule functions:
cmr_audit_revisions(),
plot_cmr_lifecycle()
cmr_find_granules("MUR-JPL-L4-GLOB-v4.1", as.Date("2024-01-01"), as.Date("2024-01-05")) # subset a high-volume swath product to a region of interest cmr_find_granules("MOD021KM", as.Date("2026-08-01"), as.Date("2026-08-01"), bounding_box = c(140, -45, 155, -35))cmr_find_granules("MUR-JPL-L4-GLOB-v4.1", as.Date("2024-01-01"), as.Date("2024-01-05")) # subset a high-volume swath product to a region of interest cmr_find_granules("MOD021KM", as.Date("2026-08-01"), as.Date("2026-08-01"), bounding_box = c(140, -45, 155, -35))
cmr_audit_revisions() resultStandard lifecycle diagnostic plots for a cmr_audit_revisions() result
plot_cmr_lifecycle(rev)plot_cmr_lifecycle(rev)
rev |
Output of |
rev, invisibly (called for its plotting side effects)
Other granule functions:
cmr_audit_revisions(),
cmr_find_granules()
rev <- cmr_audit_revisions("MUR-JPL-L4-GLOB-v4.1", from = as.Date("2024-01-01")) plot_cmr_lifecycle(rev)rev <- cmr_audit_revisions("MUR-JPL-L4-GLOB-v4.1", from = as.Date("2024-01-01")) plot_cmr_lifecycle(rev)