Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
c322402
fix(source): enable tag paramter for json requests to be Null
jacob-a-brown Apr 27, 2026
ec9f6e3
fix(config): update config/false agencies for bicarbonate
jacob-a-brown Apr 27, 2026
dcaf8d3
fix(USGS): use new USGS API endpoints for water level retrieval
jacob-a-brown Apr 27, 2026
036e9e7
fix(usgs): update transformer for combined-metadata endpoint
jacob-a-brown Apr 28, 2026
2425094
fix(usgs): update USGS water level retrieval for new APIs
jacob-a-brown Apr 28, 2026
500a0b1
fix(usgs): set limit to 50000 to avoid pagination/rate limits
jacob-a-brown Apr 29, 2026
d93869d
refactor(usgs): check usgs health with get request
jacob-a-brown Apr 29, 2026
12247f5
fix(config): set agencies for bicarbonate
jacob-a-brown Apr 29, 2026
4eaac1c
fix(nmbgmr): set timeout to 30 minutes and retry failures
jacob-a-brown Apr 29, 2026
b342922
fix(usgs): add timeout to usgs api calls and retry logic if errors
jacob-a-brown Apr 29, 2026
3f2d104
fix(usgs): add retry logic to pagination retrieval
jacob-a-brown Apr 29, 2026
6005f70
refactor(usgs): set datum in site transformer
jacob-a-brown Apr 29, 2026
dba8213
fix(usgs): return False if health check fails
jacob-a-brown Apr 29, 2026
b17bbde
fix(usgs): shorten error message
jacob-a-brown Apr 29, 2026
5570c36
fix(usgs): set limit of 1 for health check
jacob-a-brown Apr 29, 2026
836243e
fix(usgs): remove unused imports
jacob-a-brown Apr 29, 2026
4fd3ac6
fix(nmbgmr): retry when status_code!=200
jacob-a-brown Apr 29, 2026
1fcede3
fix(nmbgmr): remove print debugging statement
jacob-a-brown Apr 29, 2026
c1771d7
fix(nmbgmr): call self.warn not warning
jacob-a-brown Apr 29, 2026
a51b13e
fix(usgs): call self.warn not warning
jacob-a-brown Apr 29, 2026
43e2457
refactor(nmbgmr): change note style at top of doc
jacob-a-brown Apr 29, 2026
c17e806
refactor(retries): move rety logic to BaseSource
jacob-a-brown Apr 29, 2026
4a64c6f
refactor(nmbgmr): change timeout to 15 minutes
jacob-a-brown Apr 29, 2026
0e961a4
fix(nmbgmr): update developers note
jacob-a-brown Apr 30, 2026
8dd54d7
fix(source): return null string outside of while loop
jacob-a-brown Apr 30, 2026
32b6b25
fix(source): add URL to error message and log
jacob-a-brown Apr 30, 2026
31271c6
fix(usgs): remove hardcoded API key
jacob-a-brown Apr 30, 2026
deed95f
fix(usgs): have users pass USGS API keys
jacob-a-brown Apr 30, 2026
539fb13
fix(usgs): update README documentation
jacob-a-brown Apr 30, 2026
6897f28
fix(documentation): make README more readable
jacob-a-brown Apr 30, 2026
1d2bac9
fix(documentation): link to USGS API key acquisition
jacob-a-brown Apr 30, 2026
5f886bb
fix(usgs): exit if rate limit exceeded
jacob-a-brown Apr 30, 2026
f9908fd
fix(usgs): disallow partial records if a rate limit error is hit
jacob-a-brown Apr 30, 2026
7082957
fix(usgs): implement retries with exponential backoff
jacob-a-brown Apr 30, 2026
148208b
refactor(exceptions): put custom exceptions in own script
jacob-a-brown Apr 30, 2026
f760bbb
fix(usgs): catch JSONDecodeError and retry
jacob-a-brown Apr 30, 2026
1aff110
fix(usgs): check status_code for health check
jacob-a-brown Apr 30, 2026
0af670e
fix(usgs): revert persister.records on partial completion
jacob-a-brown Apr 30, 2026
690ba41
fix(partial records): don't persist partial records
jacob-a-brown Apr 30, 2026
7424177
fix(usgs): make site source USGS-NWIS to correspond with field measur…
jacob-a-brown Apr 30, 2026
013406d
feature(usgs): add dt params
jacob-a-brown Apr 30, 2026
7d38c37
note(usgs): type hint variables
jacob-a-brown Apr 30, 2026
8183300
test(usgs): use .env for api key for tests
jacob-a-brown Apr 30, 2026
9bf3fc2
fix(usgs): Handle USGS rate limit and partial/no data errors
jacob-a-brown Apr 30, 2026
43409f3
fix(test): don't override parent setup fixture
jacob-a-brown Apr 30, 2026
441ff81
fix(test): remove env var outside of fixture
jacob-a-brown Apr 30, 2026
e524ec7
fix(requirements): download python-dotenv
jacob-a-brown Apr 30, 2026
7af57d6
fix(test): load usgs api key from .env for every test
jacob-a-brown Apr 30, 2026
d7382bd
fix(unifier): remove partial persister.records on failure
jacob-a-brown Apr 30, 2026
245b01f
fix(usgs): use truthiness check for usgs api key header
jacob-a-brown Apr 30, 2026
f5b2e2f
fix(style): two blank lines between classes
jacob-a-brown Apr 30, 2026
85ab811
fix(usgs): fail health check on non 2xx status codes
jacob-a-brown Apr 30, 2026
5cf9904
test(usgs api key): test usgs api key is env var when set via --usgs-…
jacob-a-brown Apr 30, 2026
4f3b913
fix(test): reset usgs api key after each test
jacob-a-brown May 1, 2026
acd3360
Merge branch 'jab-nmbgmr-timeout-fix' into jab-usgs-fix
jacob-a-brown May 18, 2026
8bd3ab4
fix(source): add retry logic to _execute_json_request and _execute_te…
jacob-a-brown May 18, 2026
16201ba
fix(source): add retries and error handling for http requests
jacob-a-brown May 18, 2026
46c15f1
fix(usgs): check for HTTPError instead of HTTPStatusError to for heal…
jacob-a-brown May 18, 2026
3db9946
fix(unifier): Warn when unable to retrieve complete records after mul…
jacob-a-brown May 18, 2026
7f3cfa7
feat(unifier): Add more specific warning messages for incomplete records
jacob-a-brown May 18, 2026
60adf96
feat(usgs): add pagination check for water-level queries
jacob-a-brown May 18, 2026
22b0c33
fix(test): fix USGS API key handling in CLI tests
jacob-a-brown May 18, 2026
869e058
fix(logger): Handle potential file descriptor leaks by removing exist…
jacob-a-brown May 18, 2026
3a3f345
feat(error): log URL on error
jacob-a-brown May 18, 2026
ade753a
fix(tests): reset env var in USGS tests
jacob-a-brown May 18, 2026
e75be0a
fix(health): return False if exception is raised in health check
jacob-a-brown May 18, 2026
49abe13
fix(usgs): correct site collection error message
jacob-a-brown May 18, 2026
bab5252
fix(logger): Ensure only managed handlers are closed
jacob-a-brown May 18, 2026
eafa40f
fix(usgs): Add parameter code filter to USGS data retrieval if not si…
jacob-a-brown May 18, 2026
0c29a8a
feat(version): Update version to 0.10.1
jacob-a-brown May 18, 2026
3d6d41a
fix(usgs): update usgs parameter reader to enable None and number values
jacob-a-brown May 18, 2026
5a7c01f
fix(nmbgmr): Check for features in response to determine if API is wo…
jacob-a-brown May 18, 2026
982b72a
fix(usgs): make sure sites is always list
jacob-a-brown May 18, 2026
b9ac492
fix(health): update health checks to return false if get_records retu…
jacob-a-brown May 18, 2026
9560e0b
fix(st2 health): Update health checks to probe service directly
jacob-a-brown May 18, 2026
e40aa64
fix(usgs health check): decrease timeout for health check to avoid lo…
jacob-a-brown May 18, 2026
1d45571
fix(usgs): remove dead code for pagination
jacob-a-brown May 18, 2026
8b20e34
update(README): added note about USGS API key for die sites
jacob-a-brown May 18, 2026
0c685e1
fix(doc): update source _execute_json_request return type to always b…
jacob-a-brown May 18, 2026
074c17f
fix(documentation): remove repeated word in comment
jacob-a-brown May 18, 2026
45916b2
fix(README): use "for their APIs" instead of "to their APIs"
jacob-a-brown May 18, 2026
de660d8
fix(nmenv): list method hangs up service in health check
jacob-a-brown May 18, 2026
1e3d418
fix(readme): indicate api key for site and water level data from usgs
jacob-a-brown May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ Data comes from the following sources. We are continuously adding new sources as
- Available data: `water levels`
- [Pecos Valley Artesian Conservancy District (PVACD)](https://st2.newmexicowaterdata.org/FROST-Server/v1.1/Locations?$filter=properties/agency%20eq%20%27PVACD%27)
- Available data: `water levels`
- [USGS (NWIS)](https://waterdata.usgs.gov/nwis)
- [USGS (NWIS)](https://api.waterdata.usgs.gov/docs/)
- Available data: `water levels`
- **IMPORTANT:** The USGS now uses API keys. To prevent yourself from hitting the rate limit please [acquire an API key](https://api.waterdata.usgs.gov/signup/), save it, and provide it via the `--usgs-api-key` flag when gathering water level or site data from the USGS.
- [Water Quality Portal (WQP)](https://www.waterqualitydata.us/)
- Available data: `water levels`, `water quality`

Expand Down Expand Up @@ -189,6 +190,20 @@ The Data Integration Engine enables the user to obtain groundwater level and gro
- `--no-pvacd` to exclude Pecos Valley Artesian Convservancy District (PVACD) data
- `--no-wqp` to exclude Water Quality Portal (WQP) data

### USGS API Keys

The USGS now uses [API keys](https://api.waterdata.usgs.gov/signup/) to increase the query rate limit for their APIs. If you intend to include USGS water level data in your output please acquire an API key, save it somewhere, and provide it via the `--usgs-api-key` flag. For example:

```
die weave waterlevels --output-type timeseries_unified --usgs-api-key FAKE_API_KEY
```
Comment thread
jacob-a-brown marked this conversation as resolved.
Resolved
Comment thread
jacob-a-brown marked this conversation as resolved.
Resolved

or

```
die sites --usgs-api-key FAKE_API_KEY
```

### Geographic Filters [In Development]

The following flags can be used to geographically filter data:
Expand Down
13 changes: 12 additions & 1 deletion backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,19 @@ def get_config_and_false_agencies(self):
"nwis",
"pvacd",
]
elif self.parameter in [BICARBONATE]:
config_agencies = ["nmbgmr_amp", "nmed_dwb", "nmose_isc_seven_rivers", "wqp"]
false_agencies = [
"bor",
"bernco",
"cabq",
"ebid",
"nmose_roswell",
"nmose_pod",
"nwis",
"pvacd",
]
Comment thread
jacob-a-brown marked this conversation as resolved.
Resolved
elif self.parameter in [
BICARBONATE,
CALCIUM,
CHLORIDE,
FLUORIDE,
Expand Down
12 changes: 7 additions & 5 deletions backend/connectors/bor/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,16 @@ def __repr__(self):

def health(self):
try:
self.get_records()
return True
resp = self.get_records()
return bool(resp)
except Exception:
return False

def get_records(self):
# locationTypeId 10 is for wells
url = "https://data.usbr.gov/rise/api/location"
params = {"stateId": "NM", "locationTypeId": 10}
return self._execute_json_request(url, params)
return self._execute_json_request(url, params, tag="data")


def parse_dt(dt):
Expand Down Expand Up @@ -119,13 +119,14 @@ def get_records(self, site_record):
code = get_analyte_search_param(self.config.parameter, BOR_ANALYTE_MAPPING)

catalog_record_data = self._execute_json_request(
f"https://data.usbr.gov{site_record.catalogRecords[0]['id']}"
f"https://data.usbr.gov{site_record.catalogRecords[0]['id']}",
tag="data"
)
catalog_items = catalog_record_data["relationships"]["catalogItems"]["data"]

for i, item in enumerate(self._reorder_catalog_items(catalog_items)):

data = self._execute_json_request(f'https://data.usbr.gov{item["id"]}')
data = self._execute_json_request(f'https://data.usbr.gov{item["id"]}', tag="data")
if not data:
continue

Expand All @@ -142,6 +143,7 @@ def get_records(self, site_record):
return self._execute_json_request(
"https://data.usbr.gov/rise/api/result",
params={"itemId": data["attributes"]["_id"]},
tag="data"
)


Expand Down
10 changes: 6 additions & 4 deletions backend/connectors/isc_seven_rivers/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,16 @@ def __repr__(self):

def health(self):
try:
self.get_records()
return True
resp = self.get_records()
return bool(resp)
except Exception as e:
print("Failed to get records", e)
return False

def get_records(self):
return self._execute_json_request(
_make_url("getMonitoringPoints.ashx"),
tag="data",
)


Expand All @@ -100,7 +101,7 @@ def _get_analyte_id_and_name(self, analyte):
""" """
if self._analyte_ids is None:

resp = self._execute_json_request(_make_url("getAnalytes.ashx"))
resp = self._execute_json_request(_make_url("getAnalytes.ashx"), tag="data")
if resp:
self._analyte_ids = {r["name"]: r["id"] for r in resp}

Expand Down Expand Up @@ -164,7 +165,7 @@ def get_records(self, site_record):
self._source_parameter_name = analyte_id_and_name["name"]

return self._execute_json_request(
_make_url("getReadings.ashx"), params=params
_make_url("getReadings.ashx"), params=params, tag="data"
)


Expand All @@ -184,6 +185,7 @@ def get_records(self, site_record):
return self._execute_json_request(
_make_url("getWaterLevels.ashx"),
params=params,
tag="data",
)

def _clean_records(self, records):
Expand Down
31 changes: 23 additions & 8 deletions backend/connectors/nmbgmr/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@
)


# Set timeout to 15 minutes for analyte and water level requests since some sites have a large number of records and the NMBGMR API can be slow to respond.
# Don't use timeout=None since that can cause the request to hang indefinitely if there are issues with the API.
# Instead, catch timeout and other exceptions and retry the request up to 7 times with a delay between retries.

TIMEOUT=15*60

def _make_url(endpoint):
if os.getenv("DEBUG") == "1":
url = f"http://localhost:8000/latest/{endpoint}"
Expand All @@ -62,10 +68,13 @@ def __repr__(self):
return "NMBGMRSiteSource"

def health(self):
resp = self._execute_json_request(
_make_url("locations"), tag="features", params={"limit": 1}
)
return bool(resp)
try:
resp = self._execute_json_request(
_make_url("locations"), tag="features", params={"limit": 1}
)
return bool(resp)
except Exception:
return False

def get_records(self):
config = self.config
Expand All @@ -82,10 +91,10 @@ def get_records(self):
else:
params["parameter"] = "Manual groundwater levels"

# tags="features" because the response object is a GeoJSON
sites = self._execute_json_request(
_make_url("locations"), params, tag="features", timeout=30
_make_url("locations"), params, tag="features", timeout=TIMEOUT
)

if not config.sites_only:
for site in sites:
if get_bool_env_variable("IS_TESTING_ENV"):
Expand Down Expand Up @@ -119,14 +128,17 @@ def get_records(self, site_record):
analyte = get_analyte_search_param(
self.config.parameter, NMBGMR_ANALYTE_MAPPING
)

records = self._execute_json_request(
_make_url("waterchemistry"),
params={
"pointid": ",".join(make_site_list(site_record)),
"analyte": analyte,
},
tag="",
timeout=TIMEOUT
)

records_sorted_by_pointid = {}
for pointid in records.keys():
records_sorted_by_pointid[pointid] = records[pointid][analyte]
Expand Down Expand Up @@ -224,15 +236,18 @@ def get_records(self, site_record):
# just use manual waterlevels temporarily
url = _make_url("waterlevels/manual")

paginated_records = self._execute_json_request(url, params, tag="")
paginated_records = self._execute_json_request(url, params, tag="", timeout=TIMEOUT)

items = paginated_records["items"]
page = paginated_records["page"]
pages = paginated_records["pages"]

while page < pages:
page += 1
params["page"] = page
new_records = self._execute_json_request(url, params, tag="")

new_records = self._execute_json_request(url, params, tag="", timeout=TIMEOUT)

items.extend(new_records["items"])
pages = new_records["pages"]

Expand Down
8 changes: 6 additions & 2 deletions backend/connectors/nmenv/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ def __repr__(self):
return "DWBSiteSource"

def health(self):
return self.get_records(top=10, analyte=TDS)

try:
resp = self.get_records(top=10, analyte=TDS)
return bool(resp)
except Exception:
return False
Comment thread
jacob-a-brown marked this conversation as resolved.
Resolved
Comment thread
jacob-a-brown marked this conversation as resolved.
Resolved

def get_records(self, *args, **kw):

analyte = None
Expand Down
10 changes: 8 additions & 2 deletions backend/connectors/st_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,14 @@ def make_dt_filter(tag, start, end):

class STSiteSource(BaseSiteSource, STSource):
def health(self):
return self.get_records(top=10)

try:
# Health checks may run before config is initialized; probe the service directly.
service = self.get_service()
resp = list(service.locations().query().top(1).list())
return bool(resp)
except Exception:
return False
Comment thread
jacob-a-brown marked this conversation as resolved.
Resolved
Comment thread
jacob-a-brown marked this conversation as resolved.
Resolved

def get_records(self, *args, **kw):
service = self.get_service()

Expand Down
Loading