Skip to content

Fix live extraction on every dataset; release 0.1.0 - #13

Open
max578 wants to merge 37 commits into
mainfrom
fix/point-route-featurecollection
Open

Fix live extraction on every dataset; release 0.1.0#13
max578 wants to merge 37 commits into
mainfrom
fix/point-route-featurecollection

Conversation

@max578

@max578 max578 commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • collect_gee_data() failed on every live call (HTTP 400: the point set was
    sent as a Dictionary, not a FeatureCollection) and two cli template leaks hid
    the cause. Seven SLGA datasets, Sentinel-2 and Landsat also failed or
    returned an arbitrary scene on both routes. All 19 registered datasets now
    return data live on the raster and point routes.
  • One expression builder (R/expression_dataset.R) now serves read_gee()
    and collect_gee_data(): date window, spatial filter on the request
    geometry, mosaic for scene collections, per-dataset QA mask, SLGA
    depth/stat, scale/offset, NDVI with fill pixels masked.
  • Cache: in-session memory by default, disk persistence opt-in under
    tools::R_user_dir(); cached tables stored with qs2, rasters as GeoTIFF;
    format version in the cache key.
  • Release 0.1.0: jsonlite back in Imports (httr2 >= 1.3.0 no longer imports
    it), portable test fixture paths, a clearer resampling warning, NEWS.
  • Review round: dead code removed, print() replaced with cli, DESCRIPTION
    wording, @inheritParams, dedicated test expectations, lines wrapped,
    collect_gee_data() split into helpers.

Type of change

  • fix - bug fix
  • refactor - no user-visible change (shared builder, helper split)
  • test - recorded HTTP fixtures, opt-in live tests
  • chore - housekeeping (release bump)

What was verified, and how

Every defect was reproduced against the live service before the change and
re-checked after it. The point route was cross-checked against the official
Python client (earthengine-api 1.7.18): SMAP soil_moisture_am at 145E 30S
on 2022-06-01 gives 0.1736955 from geefetch and 0.1736955 from Python.

Gate Result
Offline suite 534 pass, 0 fail, 0 skip, 0 warning
Live suite (GEEFETCH_LIVE=1 GEEFETCH_PROJECT=... GEEFETCH_EMAIL=...) 11 pass
Live check of all 19 datasets on both routes 38 of 38 return data
R CMD check --as-cran geefetch_0.1.0.tar.gz 0 errors, 0 warnings, 0 notes
checktor one heuristic item (acronyms already expanded in DESCRIPTION)
pkgdown::build_site() builds
Full-size run (SMAP, 1,108 points x 669 days, 2,676 calls) 0 failures; agrees with the raster route at r = 0.988
Cache format benchmark, 687k-row table qs2 0.05 s write / 0.03 s read / 7.7 MB at two threads; gzip RDS 0.87 s / 0.11 s / 7.6 MB; fst 0.05 s / 0.03 s / 25 MB

Algorithm names used by the builder were checked against the live
algorithms listing (985 entries): Image.or, Filter.intersects,
GeometryConstructors.BBox / MultiPoint / Point,
ImageCollection.mosaic, Image.rename.

Commits

SHA Change
db0df88 fix(rest): send points to sampleRegions as a FeatureCollection invocation
ae17e02 test: record live HTTP fixtures; opt-in live integration tests
d083e8e fix(backend): state native scale, effective scale and remedy in the resampling warning
92891bc test(dispatcher): assert the resampling warning; valued raster in the cache test
6ee7469 fix(docs): escape the dataset-columns placeholder in the Rd
fa6c825 docs(news): record the fixes
830a8ef fix(datasets): one expression builder; SLGA, Sentinel-2, Landsat work live
af36c59 fix(deps): jsonlite back in Imports; portable fixture paths; live tests for SLGA and Sentinel-2
b71ed1a fix(points): drop a duplicated computeFeatures call; wrap long cli lines
99a2b93 chore(release): geefetch 0.1.0
60bf1a2 refactor: tighten comments and wrap long lines
e50673d refactor: wrap the last long line in the expression builder
5176bb0 docs: placeholder account details in the vignette and live-test notes
1f1cba7 refactor: remove dead internal functions
f372b59 fix(collect_gee_data): replace print() with cli output
7b84b13 feat(cache): disk caching opt-in; DESCRIPTION and example fixes
923f324 style: anyNA(), fixed = TRUE, file.path(), single-argument c()
1045e22 test: dedicated expect_* forms; drop ::: in tests
2b00bea docs: inherit shared @PARAM docs from one source
b46b26f style: call imported functions bare
5b27125 refactor(collect_gee_data): split into helpers
35671c2 style: wrap lines over 80 characters
c86b2d0 docs(spelling): word list
40df6ae docs(news): cache opt-in and progress-table changes
4f02671 refactor: remove the sixth dead function; shared-parameter topic
05bd3a8 style: shared parameters on a topic; remaining indentation and quote lints
44557e9 fix(cache): disk-cache switch in the package environment; DESCRIPTION acronyms
e1916f0 feat(cache): store non-raster cache entries with qs2
fffd3a9 test(cache): qs2 format and legacy-file misses
60d25df docs(news): qs2 cache format
0b1841a refactor(cache): thread count from an option, default two
c9028af fix(registry): name the catalogue record each SLGA dataset is checked against

Checks

  • devtools::document() run
  • devtools::test() passes, including new tests for the builder, point alignment, message escaping, project guard, recorded fixtures, cache format
  • lintr::lint_package(): no new lints in touched files; the object-usage linter still reports internal helpers it cannot resolve
  • NEWS.md updated
  • @examples runnable; commented-out code removed
  • Vignettes build (R CMD build)
  • air format . not run; files formatted by hand to the existing style

Notes for review

  • The recorded HTTP fixtures for the success, 401 and masked-point replies
    were captured from the live service with the project redacted. The 403 and
    429 fixtures are written from Google's documented error shapes and say so
    in their files.
  • Test replay points the client at a short host through
    options(geefetch.rest_base) in tests/testthat/setup.R, so fixture paths
    in the tarball stay under 100 bytes. Recording uses the real host.
  • The rgee backend still stops with a message pointing to the REST backend.
    Either implement it or reword the README before CRAN submission.
  • Regions over 2048 pixels are resampled with a warning; tiling is planned.
  • The last commit gives each SLGA dataset the catalogue record it is checked
    against: the reader samples CSIRO/SLGA/CLY and its siblings, but the
    public catalogue publishes one record for CSIRO/SLGA and none for the
    images inside it.

Linked issues

Closes #

Breaking changes?

No exported signature changed. collect_gee_data() now honours depth and
stat on the point route and returns NDVI rather than raw reflectance for
the two index datasets; both are documented behaviour that was not being
delivered. Disk caching is opt-in, so results persist across sessions only
after gee_cache_disk(TRUE) or the matching option; an existing on-disk
cache is rebuilt on first use under the new format.

@max578
max578 requested a review from adamhsparks as a code owner September 7, 2026 12:17
Max Moldovan added 13 commits September 7, 2026 22:36
…tion

The point route passed the point set as a GeoJSON object inside
constantValue, which the Expression grammar reads as a Dictionary, so
every live collect_gee_data() call failed with HTTP 400 and returned NA.
Build the collection as Collection over Feature invocations with
GeometryConstructors.Point geometries, the form the service accepts.

Also: pass service and condition text to cli as data, never as a
template (braces and backticks in the 400 message hid the cause);
abort instead of aligning by position when a reply lacks point_id;
abort on an empty project instead of forming projects// URLs; warn when
gee_auth() falls back to the refused earthengine-legacy project.
…tests

Record computeFeatures success, 401 and a masked-point reply from live
Earth Engine calls (project redacted); the 403 and 429 cassettes are
hand-authored from Google's documented error shapes and say so in their
files. Add test-live.R, skipped unless GEEFETCH_LIVE=1 and
GEEFETCH_PROJECT is set, covering SRTM and SMAP point extraction and one
raster read. setup.R keeps those variables when live tests are requested.
…resampling warning

.build_grid() warned that a region was resampled to fit the 2048x2048
pixel cap, but never said the dataset's native scale, the scale lost
by resampling, or how to avoid it. The warning now states the native
scale in metres, the effective scale after resampling, and suggests a
smaller region. read_gee() gains a "Region size and resampling"
section documenting this behaviour; max_dim itself stays internal, as
no read_gee()/read_*() alias exposes it as an argument.
… through a valued raster

Two testthat warnings, no failures: the SRTM dispatch test emitted the
region-resampling warning unasserted, and the cache-routing test wrote
a valueless SpatRaster template to .cache_set(), which cannot be
written as a GeoTIFF and so warned on the disk-cache fallback path.

The SRTM test now asserts the warning explicitly with expect_warning().
The cache-routing test now pre-populates the cache with a raster that
carries real cell values, since .rest_compute_pixels() always returns
one from real GEE response bytes; a template with no values never
reaches .cache_set() outside a test fixture. The disk-cache-write
fallback itself keeps independent coverage in a new test-cache.R case.
…alid HTML

collect_gee_data()'s @returns \describe{} used a bare
\item{<dataset columns>}{...}. Roxygen2's markdown parser read the
angle brackets as an inline HTML tag and emitted
\if{html}{\out{<dataset columns>}} verbatim, which R CMD check's HTML
manual validation flagged: "<dataset> is not recognized" and
"trimming empty <dt>", because a real HTML parser discards the
unrecognised tag, leaving the \describe{} term empty.

Wrapping the placeholder in backticks makes roxygen2 treat it as an
inline code span, emitting \verb{<dataset columns>} instead, which
Rd-to-HTML escapes as literal text in every output format. man/
collect_gee_data.Rd is hand-edited to this exact form rather than
regenerated, to avoid the unrelated reformatting a full
devtools::document() run under roxygen2 8.1.0 would apply to every Rd
file and NAMESPACE; the change was verified against roxygen2 8.1.0's
actual output on an isolated test package before being applied here.
…ng-warning uplift

Adds a development-version section above 0.0.0.9001 describing, in
user language, the five live-path fixes already committed in
b494e03 (FeatureCollection point extraction, verbatim service error
text, point_id-required alignment, empty-project abort, legacy-project
warning) plus this session's actionable region-resampling warning.
…-2 and Landsat work live

A live sweep of all 19 registered datasets found three defect classes
that the mocked suite could not see:

- The seven SLGA datasets pointed at CSIRO/SLGA, which is an
  ImageCollection; Image.load rejected it on both routes. Each dataset
  now names its attribute image (CSIRO/SLGA/CLY, ..., CSIRO/SLGA/pHc).
- The Sentinel-2 cloud mask called Image.Or; the algorithm is Image.or.
- No spatial filter preceded Collection.first, so scene-based
  collections (Sentinel-2, Landsat) returned an arbitrary scene, and the
  point route never applied QA masks, depth/stat, or computed NDVI.

Add .ee_dataset_image(), used by read_gee() handlers, .rest_extract_*()
and collect_gee_data(): load, date window, Filter.intersects on the
request geometry (BBox for rasters, MultiPoint for points), mosaic for
scene collections and first for global products, per-dataset QA mask,
band resolution (SLGA depth/stat, WorldClim variable), scale/offset, and
NDVI with fill pixels masked and the band renamed. collect_gee_data()
now forwards depth and stat and keys the cache on them.

Algorithm names checked against the live algorithms listing; the
masked-point cassette re-recorded for the new request body.
…sts for SLGA and Sentinel-2

httr2 1.3.0 no longer imports jsonlite, yet every resp_body_json() call
needs it; April's audit had removed it as a dead dependency.

R CMD check flagged the recorded cassette paths as over 100 bytes. The
REST base URL is now read through .gee_rest_base() so the test setup
can point replay at a short host; cassette directories and the fake
project name are shortened to match. Live tests keep the real host.

Ignore .DS_Store in the build. Regenerate man/ with roxygen2 8.1.0 (the
multi-line importFrom layout is the only unrelated change). NEWS lists
the dataset fixes.
@max578
max578 force-pushed the fix/point-route-featurecollection branch from 4380c00 to 5176bb0 Compare September 7, 2026 13:06

@adamhsparks adamhsparks left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using some automated checks locally these issues are flagged.

Using {checktor}

R 4.6.1> [I] results <- checktor::checktor()
── Package Doctor - Diagnostic Report ───────────────────────────────────────────────────────────────────────────────────────────────────────────── v0.1.0 ──
Examining package at: .


── Code Health Check ──

✔ No `T`/`F` usage foundNo hardcoded seed setting foundPotential unsuppressable `print()`/`cat()` usagecollect_gee_data.R:475
Treatment: Use {.code message()} or {.code if(verbose)} conditionsOption changes appear to be properly resetNo obvious home directory writing detectedTemp files without apparent cleanupsetup.R:33
Treatment: Add cleanup (unlink, on.exit, withr::local_tempfile, ...)
✔ No `.GlobalEnv` modification detectedNo `installed.packages()` usage foundNo `options(warn = -1)` usage foundNo software installation in functions detectedCore usage appears limited appropriatelyNo `library()`/`require()` calls in package code`Sys.setenv()` calls appear to be reset

── DESCRIPTION File Health Check ──

! Potential software name formatting issuesDescription: Python should be in single quotes
! Potential unexplained acronyms: "GEE, REST, GRDC"
Treatment: Consider explaining these acronymsLicense formatting appears correctTitle appears to be in Title CaseTitle length is within the 65-character guidelineTitle does not start with an articleTitle is free of redundant phrases
! Function names are single-quoted (reserve quotes for software names)
• Description: function name 'read_gee()' should not be quotedDescription: function name 'collect_gee_data()' should not be quoted
Treatment: Drop the single quotes around function names like 'fn()'`Authors@R` field found
! `Authors@R` has no `[cph]` (copyright holder)
• Authors@R lacks any [cph] (copyright holder) role
Treatment: Add role 'cph' to a person, e.g. role = c('aut','cre','cph')
ℹ No references found in DescriptionDescription length appears adequateDescription does not start with a forbidden phraseDescription quotes 'R' properlyLICENSE year looks current

── Documentation Health Check ──

✔ All function documentation has `\value` tagsExported functions include `\examples`Roxygen2 usage detected - ensure to run `roxygenize()` before submissionExample structure appears appropriate
! Commented-out code in `\examples`gee_auth.Rd: commented-out call in \examplesread_gee.Rd: commented-out call in \examples
Treatment: Remove the comment or make it a real exampleUnexported examples use `:::` where needed`\dontrun` use is appropriateExamples guard Suggested-package usage

── General Health Check ──

✔ Package size: "0.32 MB" (under 5 MB limit)
✔ No obvious URL issues foundNEWS file foundREADME relative links resolve to shipped files

── CRAN Policy Violations Check ──

✔ No `browser()` calls foundNo dangerous system calls found
! Potential file operations without temp directorycache.R:195 (saveRDS())
Treatment: Ensure file operations use temporary directoriesNetwork operations appear properly wrapped

── Diagnosis Summary ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
✖ Found 12 issues across 8 failed checks
Review the detailed diagnosis above for specific remedies.

Use `prescribe()` to get treatment recommendations.


── Recommended Next Steps
1. Apply the treatments suggested above
2. Run `devtools::check()` for standard R CMD check
3. Re-run `checktor()` to verify treatments
4. Submit to CRAN when diagnosis is cleanR 4.6.1> [I] checktor::prescribe(results)
── Treatment Recommendations ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

── Unsuppressable Output Issues
Treatment: Use {.code message()} or gate output on a verbose parameter
# Before
print('Processing...')
# After - option 1
message('Processing...')
# After - option 2
my_function <- function(data, verbose = TRUE) {
  if (verbose) cli::cli_inform('Processing...')
}

Using {goodpractice}.

R 4.6.1> [I] goodpractice::gp()
── Preparing goodpractice for geefetch ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
✔ Preparing: description [8ms]
✔ Preparing: code_structure [4ms]
✔ Preparing: namespace [5ms]
Registered S3 method overwritten by 'data.table':
  method           from
  print.data.table colorDFPreparing: covr [16.6s]
✔ Preparing: cyclocomp [24.8s]
✔ Preparing: package_structure [5ms]
✔ Preparing: lintr [4.8s]
✔ Preparing: rcmdcheck [1m 7.4s]
✔ Preparing: rd [55ms]
✔ Preparing: revdep [262ms]
✔ Preparing: roxygen2 [288ms]
✔ Preparing: spelling [475ms]
ℹ Package geefetchChecking that VignetteBuilder package knitr is installed.VignetteBuilder package knitr is installed.Preparing: urlchecker [6.4s]
Chunk start/end indices failed sanity checks in vignette.
Chunk start/end indices failed sanity checks in vignette.
Chunk start/end indices failed sanity checks in vignette.Preparing: vignette [20ms]
── It is good practice to ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
✖ keep functions short and focused. Long functions are harder to understand, test, and maintain. Consider splitting into smaller helpers. (Default limit of
  50 set with option 'goodpractice.function_length_limit').

    R/auth.R:110
    R/auth.R:249
    R/auth.R:325
    R/backend_rest.R:309
    R/backend_rest.R:385
    ... and 5 more linesremove or use internal functions that are defined but never called. Dead code increases maintenance burden.

    R/backend_rest.R:285
    R/backend_rest.R:637
    R/backend_rest.R:686
    R/collect_gee_data.R:385
    R/qa_masking.R:138
    ... and 1 more linewrite unit tests for all functions, and all package code in general. 88% of code lines are covered by test cases.

    R/auth.R:125
    R/auth.R:126
    R/auth.R:127
    R/auth.R:128
    R/auth.R:147
    ... and 183 more lineswrite short and simple functions. These functions have high cyclomatic complexity (>15): collect_gee_data (31). You can make them easier to reason about
  by encapsulating distinct steps of your function into subfunctions.avoid long code lines, it is bad for readability. Also, many people prefer editor windows that are about 80 characters wide. Try making your lines shorter
  than 80 characters

    R/auth.R:391:81
    R/auth.R:394:81
    R/backend_rest.R:9:81
    R/collect_gee_data.R:161:81
    R/collect_gee_data.R:456:81
    ... and 57 more linesuse `anyNA()` instead of `any(is.na(x))`. It is more efficient because it stops at the first `NA`.

    R/utils.R:89:25avoid passing the same argument twice in a function call. Duplicate arguments are silently ignored, which is likely a bug.

    R/auth.R:142:7
    R/auth.R:158:7
    R/auth.R:208:9
    R/auth.R:216:9
    R/backend_rest.R:330:7
    ... and 24 more linesuse `fixed = TRUE` in `grep()`/`grepl()`/ `sub()`/`gsub()` when the pattern is a plain string, not a regular expression. It is faster and clearer.

    R/backend_rest.R:468:33
    R/external_facts.R:39:20
    tests/testthat/test-coverage_boost2.R:8:21
    tests/testthat/test-coverage_boost2.R:9:21
    tests/testthat/test-coverage_boost2.R:10:21
    ... and 2 more linesuse `paste0()` instead of `paste(..., sep = '')` and `file.path()` instead of `paste(..., sep = '/')`.

    R/backend_rest.R:415:10
    R/external_facts.R:149:20avoid operators like `<<-` (global assignment) and `:::` (accessing internal package functions). These make code harder to understand and maintain.

    tests/testthat/test-backend_rest-http.R:41:21
    tests/testthat/test-backend_rest-http.R:44:30
    tests/testthat/test-backend_rest-http.R:45:19
    tests/testthat/test-backend_rest-http.R:66:15
    tests/testthat/test-backend_rest-http.R:86:15
    ... and 3 more linesavoid `c()` with a single argument or no arguments. `c('x')` is just `'x'`, and `c()` is better written as `NULL`.

    R/external_facts.R:88:53
    tests/testthat/test-validation.R:89:26
    tests/testthat/test-validation.R:96:24
    tests/testthat/test-validation.R:103:32
    tests/testthat/test-validation.R:122:45split `expect_true(a && b)` into separate `expect_true(a)` and `expect_true(b)` calls. Separate assertions give clearer failure messages.

    tests/testthat/test-coverage_boost.R:86:3use `expect_gt(x, y)` instead of `expect_true(x > y)`. Dedicated comparison expectations give better failure messages.

    tests/testthat/test-cache.R:10:3
    tests/testthat/test-cache.R:71:3
    tests/testthat/test-cache.R:93:3
    tests/testthat/test-coverage_boost.R:41:3
    tests/testthat/test-coverage_boost.R:87:3
    ... and 12 more linesprefer `expect_identical()` over `expect_equal()` when the comparison should be exact. `expect_equal()` uses `all.equal()` with tolerance, which can mask
  subtle type or attribute differences.

    tests/testthat/test-auth.R:50:3
    tests/testthat/test-cache.R:8:3
    tests/testthat/test-cache.R:16:3
    tests/testthat/test-cache.R:37:3
    tests/testthat/test-cache.R:42:3
    ... and 121 more linesuse `expect_length(x, n)` instead of `expect_equal(length(x), n)`. The dedicated function gives clearer failure messages.

    tests/testthat/test-cache.R:77:3
    tests/testthat/test-collect_gee_data.R:146:3
    tests/testthat/test-coverage_boost2.R:148:3
    tests/testthat/test-handler_registry.R:158:3
    tests/testthat/test-handler_registry.R:159:3
    ... and 1 more lineuse `expect_named(x, expected)` instead of `expect_equal(names(x), expected)`. The dedicated function gives clearer failure messages.

    tests/testthat/test-handler_registry.R:160:3use `expect_null()` instead of `expect_equal(x, NULL)` or `expect_identical(x, NULL)`. The dedicated function gives clearer failure messages.

    tests/testthat/test-expression_builder.R:11:3use `expect_s3_class(x, 'class')` instead of `expect_equal(class(x), 'class')`. The dedicated function gives clearer failure messages and handles multiple
  classes correctly.

    tests/testthat/test-coverage_boost2.R:159:3
    tests/testthat/test-validation.R:157:3use `expect_true()` instead of `expect_equal(x, TRUE)` and `expect_false()` instead of `expect_equal(x, FALSE)`. The dedicated functions give clearer
  failure messages.

    tests/testthat/test-expression_builder.R:10:3use `expect_type(x, 'type')` instead of `expect_equal(typeof(x), 'type')`. The dedicated function gives clearer failure messages.

    tests/testthat/test-auth.R:23:3
    tests/testthat/test-auth.R:24:3
    tests/testthat/test-auth.R:25:3
    tests/testthat/test-dispatcher.R:56:3use `@inheritParams` to avoid duplicating parameter documentation. Identical `@param` descriptions across files suggest shared docs should be inherited
  from a single source.

    R/backend_rest.R:514
    R/backend_rest.R:561
    R/backend_rest.R:637
    R/backend_rest.R:686
    R/collect_gee_data.R:260
    ... and 60 more linesfix spelling errors in package documentation. Use `spelling::spell_check_package()` to review misspelled words and add false positives to inst/WORDLIST.

    read_era5.Rd:43
    NEWS.md:105
    NEWS.md:155
    read_worldclim.Rd:53
    read_worldclim.Rd:54
    ... and 136 more lines

From these checks, I'd certainly remove dead code and lint the tests.

Using @inheritParams is also wise.

I note that this suggests using message() over print() and while I agree, I don't understand why we're not using {cli} here as it's an Import.

In cache.R, line 64 uses rlang::is_installed(), which is already imported, it does not need to be namespaced. I didn't check but there are likely to be other instances like this that should not be namespaced if the function is already imported.

For the cached files, also check and benchmark the use of {qs2}, I'm using it in the PRIM tool for fast read access of spatial data files in the Shiny web app.

Also with the cached files, be careful writing to the user space. I gave up on this for the {getCRUCLdata} because of issues with CRAN and the directory that I used. I only cache in-session using {httr2}.

Max Moldovan added 14 commits September 8, 2026 12:52
…lled

.ee_reduce_regions, .rest_extract_raster, .rest_extract_points
(backend_rest.R), .rest_extract_single_point (collect_gee_data.R), and
.ee_normalized_difference (qa_masking.R) were never called from any live
code path; the actual extraction routes go through
.rest_extract_raster_expr (handlers.R) and .rest_extract_batch_points /
.safe_extract_points_batch (collect_gee_data.R), and the S2/Landsat NDVI
computation is inline in expression_dataset.R. Drop the dead functions,
their now-orphaned tests, and correct a code comment in external_facts.R
that named the deleted qa_masking.R function.
.print_collection_info() called print(info_dt) directly, an
unsuppressable print() outside any if()/verbose guard checktor flags.
Format the dataset table as plain text with a new .format_dataset_table()
helper and emit it via cli::cli_verbatim() instead.
Disk caching under tools::R_user_dir() previously ran unconditionally
whenever cache = TRUE (the default), writing to the real user cache
directory in normal use and, because tests/testthat/setup.R redirected
a cache-dir option under the wrong (dotted) name, in most test runs too.
.cache_get()/.cache_set() now persist to disk only when
options(geefetch.cache.disk = TRUE) is set, via a new gee_cache_disk()
helper; the in-session memory cache is unchanged and remains the
default. gee_clear_cache() clears the memory store unconditionally and
the disk store when present. setup.R's redirect now uses the option
names the code actually reads, and its temp cache directory is removed
at teardown.

Also: single-quote 'Python', drop quotes around read_gee()/
collect_gee_data() in DESCRIPTION, expand GEE/REST/GRDC on first use,
add the "cph" role (DESCRIPTION already names "geefetch authors" as
copyright holder in LICENSE); reword two roxygen example comments whose
parenthetical asides tripped checktor's commented-code heuristic (no
code was actually commented out).
utils.R: any(is.na(dates)) -> anyNA(dates).
backend_rest.R: fixed = TRUE for the two plain-string grepl()/gsub()
patterns; the REST request URL builder now uses file.path() instead
of paste0() with literal slashes.
external_facts.R: toString() replaces paste(..., collapse = ", ")
for the bands fact value; a single-element c("B") becomes "B".
Tests: fixed = TRUE on grepl() calls matching a plain string; drop
c() around single numeric literals in test-validation.R.
Converts expect_equal()/expect_true() calls goodpractice flagged to the
dedicated forms: expect_identical() for exact comparisons (~120 sites),
expect_gt/gte/lt/lte() for comparisons, expect_length(), expect_named(),
expect_null(), expect_s3_class(), expect_true()/expect_false(), and
expect_type(). Splits one expect_true(a && b) into separate assertions.

Two expect_equal() calls in test-validation.R are deliberately left as
expect_equal(): seq.Date() returns an integer-backed Date while
as.Date() returns a double-backed one, so expect_identical() fails on
storage mode even though the dates are equal; a comment records why.

Drops geefetch::: qualification in test-backend_rest-http.R and
test-catalogue-diff.R -- internal functions are already visible to
testthat via devtools::load_all().
Adds an internal .gee_shared_params() (R/geefetch-package.R, @Keywords
internal so @inheritParams can resolve it) documenting the parameters
goodpractice found duplicated verbatim across files: did, meta, date,
coords, region, bands, backend, cache, verbose, max_tries,
initial_delay. All nine read_*() convenience wrappers, read_gee(),
collect_gee_data(), and the internal request/cache helpers now
`@inheritParams .gee_shared_params` instead of repeating the text;
each function keeps only the @PARAM entries unique to it (variable,
collection, depth, stat, dataset_id, ...).

The cache @PARAM wording is updated in the one shared place to
describe the memory-first/opt-in-disk model from the cache safety
change. read_gee()'s backend explanation (no-Python REST vs
server-side rgee) moves from its @PARAM into the function description,
since it no longer has its own @PARAM block to carry it.
Functions already imported via importFrom (data.table, rbindlist,
setcolorder; httr2's request/req_headers/req_body_json/req_retry/
req_perform/resp_status/resp_body_json/resp_body_raw; rlang's
is_installed/arg_match) are now called without the pkg:: prefix, per
the reviewer's note that a namespaced call to an already-imported
function is redundant. Functions not imported (data.table::set,
data.table::fifelse, httr2::req_error) are left namespaced. No
importFrom lines added; NAMESPACE is unchanged.

Three tests mocked is_installed() via
local_mocked_bindings(..., .package = "rlang"), which only intercepts
an explicit rlang::is_installed() call; now that the production code
calls it bare (import-style), the mock must land in geefetch's own
namespace instead, so .package = "rlang" is dropped from those calls
per testthat's documented pattern for imported functions.
…lexity

goodpractice flagged collect_gee_data() at cyclomatic complexity 31
(limit 15). Splits it into .cgd_resolve_datasets() (validate + resolve
aliases), .cgd_classify_datasets() (time-series vs static),
.cgd_build_scaffold() (the location x date data.table),
.cgd_extract_timeseries()/.cgd_extract_static() (the two API-call
loops, still mutating dt by reference via data.table::set()), and
.cgd_finish() (column order + na.rm). collect_gee_data() itself drops
to complexity 5; the extraction helpers, the most complex of the rest,
are 7 and 5.

Public signature and behaviour are unchanged; also drops a redundant
pts_for_date <- coords copy in the time-series loop (coords is never
mutated per-date, so the copy was always identical to coords).
Wraps every line goodpractice's lintr check flagged (>80 chars) across
R/ and tests/ (auto-generated httptest2 cassette fixtures under
tests/testthat/cf-*/ are left untouched -- they are recorded HTTP
response dumps, not hand-authored source). Long string literals are
split across paste0() calls or reformatted as multi-line calls; no
string content changes. Two test_that() titles that would otherwise
still run long move to paste0() rather than a raw embedded newline in
the title string. Two repeated helper-recapture.R comment lines and a
repeated "HTTP cassette missing" skip message (5 sites in
test-backend_rest-http.R) are reflowed; local_fake_auth() there also
gets a geefetch_env local variable instead of repeating
asNamespace("geefetch")$.geefetch_env three times.
spelling::spell_check_package() flagged 123 words not previously in
the wordlist: dataset/product identifiers (MOD13A2, LC09, SRTMGL1,
bio01-bio19, ...), acronyms (STAC, EOSDIS, GCP, MSI, DAAC, ...), author
surnames from citations (Didan, Farr, Sabater, Rossel, Viscarra,
Searle, Hijmans, Parra, Hulley, Nygard), numeric/unit tokens (1km,
30m, 2048x2048, 50N, ...), git/DOI hash fragments, and standard
technical terms the dictionary lacks (backtick, codebase, dropdown,
dispatchable, mosaicked, toolchain, defensibility, reflectances,
Isothermality). None are misspellings; all are added to the wordlist
rather than reworded. spelling::spell_check_package() now reports zero
errors.
Adds a User-visible changes section to the 0.1.0 heading for the two
behavioural changes from this review pass: disk caching is now opt-in
via gee_cache_disk() / options(geefetch.cache.disk = TRUE), and the
verbose progress table goes through cli instead of print().
…_shared_params

.validate_dataset() (utils.R) was called only from its own two tests,
never from any live code path -- the sixth uncalled internal function
goodpractice's dead-code check reported in the original review (the
other five were fixed in 1f1cba7); this pass's re-run of goodpractice
surfaced it explicitly. Removed it and its now-orphaned tests.

Adds @Rdname gee_shared_params to the shared-params dictionary
function so goodpractice's "documented function needs @export/@noRd/
@Rdname" check passes; the generated Rd file renames from
dot-gee_shared_params.Rd to gee_shared_params.Rd (the function's own
\name{} is unchanged). goodpractice's dead-code check still flags
.gee_shared_params itself as uncalled -- expected, since its only
"caller" is roxygen2's @inheritParams resolution at document() time,
not any runtime code path; noted as a residual in the review report.
…cument the shared-parameter topic's value; reword DESCRIPTION acronyms
Max Moldovan added 5 commits September 8, 2026 20:37
qs2::qd_save()/qd_read() write about 35x faster and read about 7x
faster than gzip RDS on a 687k-row table, at a comparable size on
disk, and beat fst on both speed and size. Rasters stay GeoTIFF via
terra::writeRaster(), unchanged.

Objects qdata can hold (vectors, lists, data.frames, matrices) use
.qdata via qd_save()/qd_read(); anything else falls back to the
general-purpose .qs2 via qs_save()/qs_read(). A shallow copy strips
a data.table's .internal.selfref pointer before qd_save() so qdata's
per-session externalptr warning never fires; setDT() restores it on
read.

The cache key now digests a .CACHE_FORMAT stamp alongside the dataset
ID and parameters, so a format change (like this one) shifts every
key and old on-disk files are simply missed rather than misread.
gee_clear_cache() still sweeps old .fst/.rds files alongside the new
extensions.

Thread count for qs2 I/O is computed once per session: respects
options(geefetch.threads) if set, otherwise detectCores() - 1 floored
at 1, capped at 2 under _R_CHECK_LIMIT_CORES_ for CRAN checks.

fst moves out of Suggests (no longer used); qs2 and parallel join
Imports.
Add round-trip tests for a data.table with Date, character, integer
and double columns, and for a numeric vector, asserting identical()
survives the qdata write/read cycle including the .internal.selfref
strip-and-restore.

Add a test that an old-format (pre-format-stamp) .rds file at the
path its key would have produced is missed, not read, once the
current .cache_hash() includes .CACHE_FORMAT. Add a test that
gee_clear_cache() still removes leftover .rds/.fst files alongside
current-format ones.

Rework the file-extension patterns in the existing gee_clear_cache()
tests and the corrupt-file test for the new .qdata/.qs2 extensions;
drop the now-meaningless "cache uses RDS when fst not available" test
(fst is no longer part of the cache at all).
… against

The seven SLGA datasets sample attribute images inside CSIRO/SLGA, but the
public catalogue publishes one record for the collection and none for the
images, so the live catalogue check failed for all seven. Each entry now
carries stac_id = "CSIRO/SLGA"; the check compares bands against that
record and confirms the sampled image belongs to it, and
gee_external_facts() cites it as the source. The sampled asset is unchanged.
@max578

max578 commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review and repair directions. Everything is applied on the branch except
two items I passed below, let me know if I need to reconsider.

Item Done
Dead internal functions six removed, with their tests
print() in package code replaced with cli output gated on verbose
Temp file in test setup cleaned up at teardown
DESCRIPTION 'Python' quoted, function names unquoted, GEE / REST / GRDC expanded, cph role added
Commented-out code in examples removed
Lines over 80 characters wrapped
anyNA, fixed = TRUE, paste0, single-argument c() done
Test expectations dedicated expect_* forms; ::: dropped in tests
@inheritParams shared parameters documented once on an internal topic
Spelling word list updated
Namespacing imported functions are now called bare, as you suggested
Cache in user space the default now caches in memory for the session only, as you do with httr2; writing under tools::R_user_dir() happens only after gee_cache_disk(TRUE) or the matching option
Cache format benchmarked as you suggested: on the real 687k-row point table qs2 at the package default of two threads writes in 0.05 s and reads in 0.03 s at 7.7 MB, against 0.87 s / 0.11 s / 7.6 MB for the gzip saveRDS it replaces and 0.05 s / 0.03 s / 25 MB for fst; on rasters uncompressed GeoTIFF is fastest and is what Earth Engine sends. Cached tables now use qs2, rasters stay GeoTIFF, fst is gone, threads are set by options(geefetch.threads), and the cache key carries a format version so old files are rebuilt rather than misread
collect_gee_data() complexity split into helpers; signature and output unchanged

Two items left as they are:

  • The "same argument passed twice" hits are cli's repeated i = and !
    bullet names, which is how cli builds message bodies.
  • The remaining goodpractice style items (function length, .CONSTANT
    names, na.rm, test file naming, export order) follow the package's
    existing conventions, happy to revisit any of them if you feel strongly.

checktor now reports one item, a heuristic: it flags REST and GRDC as
unexplained although each is expanded in DESCRIPTION. R CMD check --as-cran
is 0 errors, 0 warnings, 0 notes, and the suite is 534 tests green.

The repository flattening (#3) and precompiled vignettes (#1) will each come
as their own PR after this one.

@max578
max578 requested a review from adamhsparks September 9, 2026 00:55
Max Moldovan added 4 commits September 9, 2026 11:56
Rename the check_types column of gee_external_facts() (was oracle_types)
and the environment variable that enables the live catalogue checks
(GEEFETCH_CATALOGUE_CHECK, was GEEFETCH_ORACLE); reword the comments that
referred to internal review vocabulary.
The fallback message interpolated `{.GEE_DEFAULT_PROJECT}` directly. cli
3.4.0 and later read a brace expression starting with a dot as a style
name, not a value, so building the warning threw "Invalid cli literal"
and the call ended in an error instead of a warning. A first-time user
who runs gee_auth() before saving a project number hit that error.

Wrap the object in parentheses, the form already used for the project
line two calls below, and cover the path with a test that fails against
the old string.
…s it

Splitting the extraction loop into .cgd_extract_timeseries() and
.cgd_extract_static() moved the cli_progress_update() calls out of the
frame that created the bar. cli resolves a bar by the environment that
created it, so every update aborted with "Cannot find current progress
bar", and collect_gee_data() failed for any run of more than one API
call with its default verbose = TRUE.

Keep the id returned by cli_progress_bar(), pass it to the helpers, and
update and close by id. Every existing batch test passed verbose = FALSE,
so add one that shows the bar.
computePixels returns a GeoTIFF with no band descriptions, so terra named
the layer after the temporary file it was written to. That name reached
the user in print() output and became the column name in
terra::extract() results.
@max578

max578 commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

I've pushed the PR, here is the description (three commits):

  1. gee_auth() ended in an error instead of a warning when no project was given. The fallback message interpolated a dot-prefixed object, and cli 3.4.0 and later read a brace expression starting with a dot as a style name rather than a value, so formatting the warning threw. A first-time user met this on their first call. I checked the rest of the sources for the same shape — this was the only one.

  2. collect_gee_data() failed for any run of more than one API call under its default verbose = TRUE, with "Cannot find current progress bar". The helper split on this branch moved the progress updates out of the frame that created the bar, and cli resolves a bar by that frame. The bar's id is now passed through to the helpers. Every batch test here passed verbose = FALSE, which is why the suite stayed green over it.

  3. Rasters carried the name of the temporary GeoTIFF they were written to, and that name became the column name in terra::extract() output. Layers are now named after the bands that were requested.

Each fix has a test that fails against the old code. R CMD check --as-cran is 0 errors, 0 warnings, 0 notes, and CI is green on the new head.

collect_gee_data() counted one request per location, but the point route
sends all the locations together in one request per dataset and date. For
three locations over five dates with two datasets it announced 30 calls
and made 10, and the progress bar three lines below said so.

The estimate now uses the same expression that drives the bar, and says
that locations are batched. The test compares the announced number with
the calls actually made.
@max578

max578 commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

One more commit:

collect_gee_data() estimated API calls as dates x locations x datasets. The point route sends all locations in one call per dataset and date, so for three locations over five dates with two datasets it announced 30 and made 10. The progress bar right below said 10/10.

The estimate now uses the same expression as the bar and says the locations are batched. The test compares the announced number with the calls actually made.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants