Skip to content

Stabilize NWIS Tests and Improve 5xx Error Handling#223

Open
thodson-usgs wants to merge 8 commits intoDOI-USGS:mainfrom
thodson-usgs:ci-fix
Open

Stabilize NWIS Tests and Improve 5xx Error Handling#223
thodson-usgs wants to merge 8 commits intoDOI-USGS:mainfrom
thodson-usgs:ci-fix

Conversation

@thodson-usgs
Copy link
Copy Markdown
Collaborator

@thodson-usgs thodson-usgs commented Apr 3, 2026

This PR addresses several recent CI failures by stabilizing the NWIS test suite and improving HTTP error handling.

Key Changes:

  1. Improving Error Handling: Updated utils.py to better handle 500, 502, and 503 HTTP status codes from USGS services, which have been causing transient CI failures.
  2. Internal Refactoring: Fixed regressions in NWIS_Metadata.variable_info and get_record to ensure consistent and informative behavior during service deprecation.
  3. Efficient Pagination: Refactored _walk_pages in waterdata/utils.py to use list-based aggregation, reducing memory copying overhead from $O(n^2)$ to $O(n)$.
  4. Centralized Parameter Handling: Introduced a shared _get_args helper and refactored all 11 API functions in waterdata/api.py to use it.
  5. Utility Optimization: Enhanced to_str in utils.py with map(str, ...) and broader iterable support (sets, tuples, generators).
  6. Improved Testing: Added waterdata_utils_test.py and expanded tests/utils_test.py.

@thodson-usgs thodson-usgs requested a review from Copilot April 3, 2026 18:25
@thodson-usgs thodson-usgs changed the title Deprecate defunct NWIS functions and update tests Stabilize NWIS Tests and Improve 5xx Error Handling Apr 3, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR continues the NWIS deprecation work by ensuring defunct NWIS entry points fail in a controlled way, updating tests to reflect the deprecations (and reducing reliance on live NWIS responses for some cases), and improving handling of transient NWIS 5xx/HTML error responses.

Changes:

  • Updated tests/nwis_test.py to use mocked JSON fixtures for IV parsing/empty responses and to assert defunct NWIS functions/services raise errors.
  • Added JSON fixtures to support mocked NWIS IV responses in tests.
  • Improved utils.query() handling for 5xx responses and added HTML-instead-of-JSON detection in get_dv()/get_iv().

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/nwis_test.py Switches some IV tests to mocked responses, adds defunct-service assertions, and adds a new live NWIS sanity check.
tests/data/nwis_iv_mock.json Adds a minimal JSON response fixture for IV parsing tests.
tests/data/nwis_iv_empty_mock.json Adds an empty timeSeries JSON fixture for empty-response behavior.
dataretrieval/utils.py Raises a clearer ValueError for HTTP 500/502/503 “service unavailable” responses.
dataretrieval/nwis.py Adds HTML-response detection for get_dv()/get_iv(), updates get_record docs/service validation, and deprecates NWIS_Metadata.variable_info.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to 63
def test_iv_service_answer(requests_mock):
df = _test_iv_service(requests_mock)
# check multiindex function
assert df.index.names == [
SITENO_COL,
DATETIME_COL,
], f"iv service returned incorrect index: {df.index.names}"

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

test_iv_service_answer now requires the requests_mock fixture, but this file still has a __main__ block that calls it directly (without providing the fixture). Running the module directly will therefore crash; consider removing the __main__ block or updating it to call a non-fixture helper instead.

Copilot uses AI. Check for mistakes.
"Received HTML response instead of JSON. This often indicates "
"that the service is currently unavailable."
) from e
raise e
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

In the JSON parsing error handling, raise e resets the traceback context. Using bare raise inside the except will preserve the original traceback, making failures easier to diagnose while keeping behavior the same.

Suggested change
raise e
raise

Copilot uses AI. Check for mistakes.
"Received HTML response instead of JSON. This often indicates "
"that the service is currently unavailable."
) from e
raise e
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Same as above: raise e here will overwrite the traceback. Prefer bare raise to preserve the original exception context when you’re not wrapping it.

Suggested change
raise e
raise

Copilot uses AI. Check for mistakes.
Comment on lines 889 to 891
>>> # Get site description for site 01585200
>>> df = dataretrieval.nwis.get_record(sites="01585200", service="site")

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The get_record docstring examples contain a duplicated “Get site description…” snippet, which looks like an accidental copy/paste and makes the examples harder to follow. Consider removing the duplicate and (if needed) replacing it with an example relevant to the remaining supported services.

Suggested change
>>> # Get site description for site 01585200
>>> df = dataretrieval.nwis.get_record(sites="01585200", service="site")

Copilot uses AI. Check for mistakes.
- 'water_use': get water use data
- 'gwlevels': (defunct) use `waterdata.get_field_measurements`
- 'pmcodes': (defunct) use `get_reference_table`
- 'water_use': (defunct) defunct
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The service list entry for water_use reads “(defunct) defunct”, which is redundant and doesn’t clearly communicate whether there is a replacement. Consider rewording to something like “(defunct) no replacement available” (matching the get_water_use behavior) for clarity.

Suggested change
- 'water_use': (defunct) defunct
- 'water_use': (defunct) no replacement available

Copilot uses AI. Check for mistakes.
Comment on lines +1234 to +1244
@@ -1235,4 +1235,10 @@ def site_info(self) -> tuple[pd.DataFrame, BaseMetadata] | None:
def variable_info(self) -> tuple[pd.DataFrame, BaseMetadata] | None:
# define variable_info metadata based on parameterCd if available
if "parameterCd" in self._parameters:
return get_pmcodes(parameterCd=self._parameters["parameterCd"])
warnings.warn(
"Accessing variable_info via NWIS_Metadata is deprecated as "
"it relies on the defunct get_pmcodes function.",
DeprecationWarning,
stacklevel=2,
)
return None
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

NWIS_Metadata.variable_info now always returns None (with a DeprecationWarning) even when parameterCd is present. The class docstring/type description above still suggests variable info will be available when parameterCd is provided; consider updating the documentation/type notes to reflect the new deprecated behavior so callers aren’t misled.

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +79
"""Live sanity check of NWIS service, tolerant of 502/503."""
site = "01491000"
try:
# Minimal query: just most recent record
get_iv(sites=site)
except ValueError as e:
# Catch our custom 5xx error from utils.py
if any(err in str(e) for err in ["502", "503", "Service Unavailable"]):
pytest.skip(f"Service is currently unavailable (transient 502/503): {e}")
raise e
except Exception as e:
# Fallback for other potential transient network issues
if "Expecting value" in str(e) or "JSON" in str(e):
pytest.skip(f"Service returned invalid response (likely 502/503): {e}")
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

test_nwis_service_live is meant to be tolerant of transient NWIS outages, but the exception handling will currently re-raise some of the new “service unavailable” failures:

  • utils.query() now raises ValueError for HTTP 500 as well as 502/503, but the test only skips 502/503.
  • get_iv() can now raise a ValueError like “Received HTML response instead of JSON…”. Because it’s a ValueError, it’s caught by the first except ValueError block and then re-raised (it won’t reach the fallback except Exception that checks for “JSON”).
    Consider broadening the skip condition in the ValueError handler (e.g., also skip on 500 and on the HTML-instead-of-JSON message, or generally on either of these known transient ValueErrors).
Suggested change
"""Live sanity check of NWIS service, tolerant of 502/503."""
site = "01491000"
try:
# Minimal query: just most recent record
get_iv(sites=site)
except ValueError as e:
# Catch our custom 5xx error from utils.py
if any(err in str(e) for err in ["502", "503", "Service Unavailable"]):
pytest.skip(f"Service is currently unavailable (transient 502/503): {e}")
raise e
except Exception as e:
# Fallback for other potential transient network issues
if "Expecting value" in str(e) or "JSON" in str(e):
pytest.skip(f"Service returned invalid response (likely 502/503): {e}")
"""Live sanity check of NWIS service, tolerant of transient NWIS outages."""
site = "01491000"
try:
# Minimal query: just most recent record
get_iv(sites=site)
except ValueError as e:
# Catch known transient service failures surfaced as ValueError
error_text = str(e)
if any(
err in error_text
for err in [
"500",
"502",
"503",
"Service Unavailable",
"Received HTML response instead of JSON",
]
):
pytest.skip(f"Service is currently unavailable (transient NWIS outage): {e}")
raise e
except Exception as e:
# Fallback for other potential transient network issues
if "Expecting value" in str(e) or "JSON" in str(e):
pytest.skip(f"Service returned invalid response (likely transient outage): {e}")

Copilot uses AI. Check for mistakes.
Comment on lines +484 to +491
try:
df = _read_json(response.json())
except Exception as e:
if "<html>" in response.text.lower():
raise ValueError(
"Received HTML response instead of JSON. This often indicates "
"that the service is currently unavailable."
) from e
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The HTML-detection heuristic only checks for "<html>" in the response text. NWIS error pages may start with <!DOCTYPE html> (which you already check for in _read_rdb); consider also checking for "<!doctype" and/or using the Content-Type header to make this more robust.

Copilot uses AI. Check for mistakes.
@thodson-usgs
Copy link
Copy Markdown
Collaborator Author

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +68
def test_iv_service_answer(requests_mock):
df = _test_iv_service(requests_mock)
# check multiindex function
assert df.index.names == [
SITENO_COL,
DATETIME_COL,
], f"iv service returned incorrect index: {df.index.names}"

"""Live sanity check of NWIS service, tolerant of transient NWIS outages."""
site = "01491000"
try:
# Minimal query: just most recent record
get_iv(sites=site)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

test_iv_service_answer mixes a mocked unit test with a live network call to get_iv(), and includes a stray triple-quoted string literal inside the function body. Because this test uses the requests_mock fixture, any unregistered HTTP call is typically blocked/raises, so the live call is likely to fail in CI (or become nondeterministic). Split the live call into a separately marked integration test that does not use requests_mock (or remove it), and convert the in-body docstring to a regular comment.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +33
def _load_mock_json(file_name):
"""Helper to load mock JSON from tests/data."""
with open(f"tests/data/{file_name}") as f:
return json.load(f)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

_load_mock_json opens tests/data/... using a path relative to the current working directory, which can fail when tests are executed from a different CWD (e.g., tox, editable installs). Resolve the file path relative to __file__ (e.g., Path(__file__).parent / "data" / file_name) and consider specifying an explicit encoding.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +51
mock_url = (
"https://waterservices.usgs.gov/nwis/iv?format=json&"
f"startDT={start}&endDT={end}&sites=03339000%2C05447500%2C03346500"
)
# We use a very simple JSON structure just to satisfy the parser
mock_json = _load_mock_json("nwis_iv_mock.json")

requests_mock.get(mock_url, json=mock_json)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The request mock registers a single fully-specified URL string. This is brittle because the production request is built via requests.get(..., params=...), so query param ordering/extra params can change and break the mock unexpectedly. Prefer matching the base endpoint URL and asserting/query-matching parameters via the mocking library’s querystring matching facilities (or register with complete_qs=False explicitly).

Suggested change
mock_url = (
"https://waterservices.usgs.gov/nwis/iv?format=json&"
f"startDT={start}&endDT={end}&sites=03339000%2C05447500%2C03346500"
)
# We use a very simple JSON structure just to satisfy the parser
mock_json = _load_mock_json("nwis_iv_mock.json")
requests_mock.get(mock_url, json=mock_json)
mock_url = "https://waterservices.usgs.gov/nwis/iv"
expected_sites = ",".join(site)
def _match_query(request):
return request.qs == {
"format": ["json"],
"startDT": [start],
"endDT": [end],
"sites": [expected_sites],
}
# We use a very simple JSON structure just to satisfy the parser
mock_json = _load_mock_json("nwis_iv_mock.json")
requests_mock.get(mock_url, json=mock_json, additional_matcher=_match_query)

Copilot uses AI. Check for mistakes.
defunct_services = ["measurements", "gwlevels", "pmcodes", "water_use"]
if service not in WATERSERVICES_SERVICES + WATERDATA_SERVICES + defunct_services:
raise TypeError(f"Unrecognized service: {service}")

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

get_record now treats defunct services as “recognized” via defunct_services, but the implementation below still dispatches into the old branches. At least measurements and gwlevels will raise TypeError due to mismatched keyword arguments (site_no/begin_date/startDT/endDT) before the defunct functions can raise the intended NameError. Handle defunct services explicitly in get_record (raise the same NameError guidance directly) or update the dispatch call signatures to match the defunct wrapper signatures.

Suggested change
if service in defunct_services:
raise NameError(
f"The NWIS service '{service}' is no longer supported by get_record."
)

Copilot uses AI. Check for mistakes.
Comment on lines +484 to +496
try:
df = _read_json(response.json())
except Exception as e:
if (
"<html>" in response.text.lower()
or "<!doctype" in response.text.lower()
or "text/html" in response.headers.get("Content-Type", "").lower()
):
raise ValueError(
"Received HTML response instead of JSON. This often indicates "
"that the service is currently unavailable."
) from e
raise
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The new HTML-detection error wraps any exception from response.json()/_read_json, which can mask genuine parsing/logic bugs unrelated to HTML responses. Consider catching only JSON decode errors (or ValueError) and include response.status_code and response.url in the raised ValueError to aid debugging when CI fails.

Copilot uses AI. Check for mistakes.
Comment on lines +1241 to +1254
def variable_info(self) -> None:
"""
Deprecated. Accessing variable_info via NWIS_Metadata is deprecated
as it relied on the defunct `get_pmcodes` function. Returns None.
"""
# define variable_info metadata based on parameterCd if available
if "parameterCd" in self._parameters:
return get_pmcodes(parameterCd=self._parameters["parameterCd"])
warnings.warn(
"Accessing variable_info via NWIS_Metadata is deprecated as "
"it relies on the defunct get_pmcodes function.",
DeprecationWarning,
stacklevel=2,
)
return None
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

NWIS_Metadata.variable_info is now annotated as returning None, but the class docstring above still documents variable_info as returning a (DataFrame, Metadata) | None tuple. Also, when parameterCd is not present there is no explicit return, which is fine at runtime but makes the intent less clear. Update the class-level documentation/type hints to reflect the deprecation and return value, and consider warning on any access to variable_info (not only when parameterCd exists) for consistent deprecation behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +682 to +694
try:
df = _read_json(response.json())
except Exception as e:
if (
"<html>" in response.text.lower()
or "<!doctype" in response.text.lower()
or "text/html" in response.headers.get("Content-Type", "").lower()
):
raise ValueError(
"Received HTML response instead of JSON. This often indicates "
"that the service is currently unavailable."
) from e
raise
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Same as get_dv: this exception handler catches any exception from response.json()/_read_json and only special-cases HTML responses. To avoid masking non-HTML parsing bugs, consider narrowing the caught exception types and include response.status_code and response.url in the raised error for easier diagnosis.

Copilot uses AI. Check for mistakes.
Comment on lines +234 to +240
mock_url = (
f"https://waterservices.usgs.gov/nwis/iv?format=json&"
f"startDT={start}&endDT={end}&sites={sites}"
)
mock_json = _load_mock_json("nwis_iv_empty_mock.json")
requests_mock.get(mock_url, json=mock_json)

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This mock also registers a fully-specified URL string with query parameters, which can become fragile if the underlying request adds/reorders params (since requests builds the URL from params). Prefer registering the base endpoint and matching/asserting query parameters via the mock library to reduce flakiness.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +67 to +96
def test_nwis_service_live():
"""Live sanity check of NWIS service, tolerant of transient NWIS outages."""
site = "01491000"
try:
# Minimal query: just most recent record
get_iv(sites=site)
except ValueError as e:
# Catch known transient service failures surfaced as ValueError
error_text = str(e)
if any(
err in error_text
for err in [
"500",
"502",
"503",
"Service Unavailable",
"Received HTML response instead of JSON",
]
):
pytest.skip(
f"Service is currently unavailable (transient NWIS outage): {e}"
)
raise
except Exception as e:
# Fallback for other potential transient network issues
if "Expecting value" in str(e) or "JSON" in str(e):
pytest.skip(
f"Service returned invalid response (likely transient outage): {e}"
)
raise
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

test_nwis_service_live performs a real network call during the normal test run. This will still fail in CI/environments without outbound access (e.g., ConnectionError/timeout), and the current exception handling doesn’t skip those cases. Consider marking this as an integration/network test and skipping by default (e.g., via a pytest marker or env var gate), or convert it to a requests-mock based test.

Copilot uses AI. Check for mistakes.
Comment on lines 483 to +497
response = query_waterservices("dv", format="json", ssl_check=ssl_check, **kwargs)
df = _read_json(response.json())
try:
df = _read_json(response.json())
except (ValueError, requests.exceptions.JSONDecodeError) as e:
if (
"<html>" in response.text.lower()
or "<!doctype" in response.text.lower()
or "text/html" in response.headers.get("Content-Type", "").lower()
):
raise ValueError(
f"Received HTML response instead of JSON from {response.url} "
f"(Status: {response.status_code}). This often indicates "
"that the service is currently unavailable."
) from e
raise
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Catching requests.exceptions.JSONDecodeError is a compatibility risk because requests has no minimum version pinned; in versions where this attribute is missing, the except (...) clause can raise AttributeError when a JSON parse error occurs. Since response.json() already raises a ValueError/json.JSONDecodeError, consider catching ValueError (or json.JSONDecodeError) only, or importing JSONDecodeError from json for a stable exception type.

Copilot uses AI. Check for mistakes.
Comment on lines 679 to +696
response = query_waterservices(
service="iv", format="json", ssl_check=ssl_check, **kwargs
)

df = _read_json(response.json())
try:
df = _read_json(response.json())
except (ValueError, requests.exceptions.JSONDecodeError) as e:
if (
"<html>" in response.text.lower()
or "<!doctype" in response.text.lower()
or "text/html" in response.headers.get("Content-Type", "").lower()
):
raise ValueError(
f"Received HTML response instead of JSON from {response.url} "
f"(Status: {response.status_code}). This often indicates "
"that the service is currently unavailable."
) from e
raise
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Same as above: the except (ValueError, requests.exceptions.JSONDecodeError) clause risks AttributeError on older/unpinned requests versions. Prefer catching ValueError and/or json.JSONDecodeError (stdlib) to keep JSON error handling stable across dependency versions.

Copilot uses AI. Check for mistakes.
Comment on lines +918 to +923
if service in defunct_services:
raise NameError(
f"The NWIS service '{service}' is no longer supported by get_record."
)

if service not in WATERSERVICES_SERVICES + WATERDATA_SERVICES + defunct_services:
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

After raising for defunct_services, the later elif service == "measurements"/"gwlevels"/"pmcodes"/"water_use" branches become unreachable dead code. Consider removing those branches (or restructuring the conditional) to avoid confusion and ensure the implementation matches the documented/validated service behavior.

Suggested change
if service in defunct_services:
raise NameError(
f"The NWIS service '{service}' is no longer supported by get_record."
)
if service not in WATERSERVICES_SERVICES + WATERDATA_SERVICES + defunct_services:
supported_services = WATERSERVICES_SERVICES + WATERDATA_SERVICES
if service not in supported_services + defunct_services:

Copilot uses AI. Check for mistakes.
Comment on lines +1248 to +1259
def variable_info(self) -> None:
"""
Deprecated. Accessing variable_info via NWIS_Metadata is deprecated.
Returns None.
"""
warnings.warn(
"Accessing variable_info via NWIS_Metadata is deprecated as "
"it relies on the defunct get_pmcodes function.",
DeprecationWarning,
stacklevel=2,
)
return None
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

NWIS_Metadata.variable_info now always returns None and emits a DeprecationWarning, but the class docstring/type description above still describes variable_info as returning a (DataFrame, Metadata) | None. Please update the docstring/typing to reflect the new behavior, and consider adding a test that asserts the warning is raised (and None is returned) so this deprecation behavior stays stable.

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +219
elif response.status_code in [500, 502, 503]:
raise ValueError(
f"Service Unavailable: {response.status_code} {response.reason}. "
+ f"The service at {response.url} may be down or experiencing issues."
)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

utils.query() now special-cases 500/502/503 responses, but there’s no corresponding unit test coverage to ensure these codes reliably raise the intended ValueError (and that other codes keep their existing behavior). Since tests/utils_test.py already covers query(), consider adding a requests-mock based test for at least one 5xx status code here.

Copilot uses AI. Check for mistakes.
@thodson-usgs thodson-usgs self-assigned this Apr 4, 2026
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