Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
58e348b
refactor(config): decompose _resolve (CC 18 → 5)
thodson-usgs Aug 17, 2026
10ec251
refactor(config): decompose _show_adapter_overrides (CC 11 → 6)
thodson-usgs Aug 17, 2026
344ac0e
refactor(config): decompose show_configuration (CC 12 → 5)
thodson-usgs Aug 17, 2026
ef029dc
refactor(config): decompose _frame (CC 8 → 2)
thodson-usgs Aug 17, 2026
1081abf
refactor(config): decompose _load_file (CC 15 → 5)
thodson-usgs Aug 17, 2026
764fdf4
refactor(ogc/dates): reduce _format_api_dates CC 16→10
thodson-usgs Aug 17, 2026
9b7fac8
refactor(ogc/filters): reduce _split_top_level_or CC 16→9
thodson-usgs Aug 17, 2026
85b2618
refactor(ogc/filters): reduce _check_numeric_filter_pitfall CC 11→1
thodson-usgs Aug 17, 2026
f11fbe3
refactor(ogc/requests): reduce prepare_request_args CC 16→5
thodson-usgs Aug 17, 2026
6fbc19b
refactor(ogc/requests): reduce _partition_request_params CC 16→1
thodson-usgs Aug 17, 2026
8bf58ba
refactor(ogc/shaping): reduce _get_resp_data CC 12→4
thodson-usgs Aug 17, 2026
7b47e6c
refactor(ogc/planning): reduce ChunkPlan.__init__ CC 14→8
thodson-usgs Aug 17, 2026
228c5ce
refactor(ogc/planning): reduce ChunkPlan._plan CC 18→5
thodson-usgs Aug 17, 2026
60ccaa9
refactor(ogc/planning): reduce ChunkPlan._refine CC 22→6
thodson-usgs Aug 17, 2026
67be56c
refactor(nwis): extract helpers from _read_json to reduce complexity
thodson-usgs Aug 17, 2026
d0e8b65
refactor(nwis): extract _localize_datetime_index from format_response
thodson-usgs Aug 17, 2026
550e8cc
refactor(wqx): extract helpers from _attach_datetime_columns
thodson-usgs Aug 17, 2026
4b7bf5b
refactor(nldi): extract _validate_lat_long_origin from _get_features_…
thodson-usgs Aug 17, 2026
3fd4f3c
refactor(nldi): extract _search_basin and _search_flowlines from search
thodson-usgs Aug 17, 2026
0ab32cb
refactor(wqx): reduce _find_datetime_triplets complexity to 10
thodson-usgs Aug 17, 2026
6693837
refactor(interruptions): reduce FanOutInterrupted.__init__ CC 11 → 2
thodson-usgs Aug 17, 2026
472b48e
refactor(transport): reduce FanOut._run CC 16 → 3
thodson-usgs Aug 17, 2026
38c08ca
refactor(waterdata/ratings): reduce get_ratings CC 11 → 4
thodson-usgs Aug 17, 2026
12ec7bc
refactor(waterdata/stats): reduce _handle_nesting CC 11 → 1
thodson-usgs Aug 17, 2026
ed09d2c
build: ratchet complexipy threshold to 10
thodson-usgs Aug 17, 2026
22ef2e8
refactor(_wqx): reduce _find_datetime_triplets CC 10 → 8
thodson-usgs Aug 17, 2026
8ae080d
refactor(codes/states): reduce _to_state_one CC 10 → 7
thodson-usgs Aug 17, 2026
c3f8d80
refactor(ogc/dates): extract _all_blank predicate, CC 10→8
thodson-usgs Aug 17, 2026
28d102e
refactor(ogc/errors): hoist _clean_field, CC 10→7
thodson-usgs Aug 17, 2026
709fc18
refactor(ogc/planning): extract _try_build to remove Type-4 clone
thodson-usgs Aug 17, 2026
49bff25
refactor(ogc/planning): reduce _extract_axes cognitive complexity 11 → 2
thodson-usgs Aug 17, 2026
60199ab
refactor(shaping): reduce _empty_feature_frame CC 10 → 6
thodson-usgs Aug 17, 2026
aab8a3e
refactor(shaping): reduce _arrange_cols CC 10 → 7
thodson-usgs Aug 17, 2026
411624e
refactor(config): simplify known-adapter check
thodson-usgs Aug 17, 2026
fb55506
refactor: tighten the helpers extracted by the complexity pass
thodson-usgs Aug 17, 2026
ad3a34e
refactor(config): simplify scope validation
thodson-usgs Aug 17, 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
98 changes: 53 additions & 45 deletions dataretrieval/_configuration_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1113,16 +1113,9 @@ def _adapter_file_settings(
def _load_file(path: Path) -> _ParsedFile:
"""Parse the configuration file at *path*, caching until it changes on disk."""
global _file_cache
try:
st = path.stat()
except FileNotFoundError:
# No file is the normal case: continue to the built-in default. One
# shared empty instance rather than a fresh one per read -- nothing
# mutates a ``_ParsedFile``, and returning the same object each time is
# what lets callers memoize on its identity.
st = _stat_config_file(path)
if st is None:
return _NO_FILE
except OSError as exc:
raise ConfigurationError(f"could not access {path}: {exc}") from exc

if stat.S_ISDIR(st.st_mode):
raise ConfigurationError(
Expand All @@ -1134,32 +1127,42 @@ def _load_file(path: Path) -> _ParsedFile:
# opened, which is what ``DATARETRIEVAL_CONFIG=/dev/null`` asks for and the
# only coherent answer for a stream: settings are re-resolved on every
# request, so a FIFO would hand its contents to the first getter and
# nothing to the rest, making the API key vanish mid-run. (It would also
# block on open until a writer appeared.)
# nothing to the rest, making the API key vanish mid-run.
if not stat.S_ISREG(st.st_mode):
return _ParsedFile(exists=True)

# POSIX ``st_ctime_ns`` advances on any inode change, so the metadata stamp
# catches even a rewrite that restores the original mtime (``cp -p``, rsync
# ``--times``, an editor that preserves timestamps). Windows ctime is
# *creation* time, so there the stamp cannot see that class of edit and the
# content compare below is the only correct check -- worth the re-read,
# since serving a stale API key is the alternative.
#
# Dropping this gate (or dropping ctime from the stamp so Windows can use
# it) has been proposed repeatedly on the grounds that the re-read is
# wasteful. It is, but it is also the only thing standing between a
# timestamp-preserving write and a stale credential; a ctime-less stamp is
# identical across exactly that edit. ``test_file_edit_is_picked_up``
# pins the behavior. Please do not "optimize" it without a Windows-safe
# change detector.
#
# Measured, so the next reviewer does not have to re-derive it: forcing the
# Windows branch costs 27 us per settings read against 5 us with the stamp
# (5 syscalls instead of 1; a 64-byte file and a 6.5 kB one measure the
# same, since the content compare still spares the TOML parse). At the 8
# reads a one-chunk query performs that is ~175 us against a 100-500 ms
# round trip -- 0.04%, and the alternative is serving a stale key.
cached_parse = _cached_parse_by_metadata(path, st)
if cached_parse is not None:
return cached_parse

content, opened_st = _read_file_content(path)
parsed = _parse_or_reuse_cache(path, content)
_warn_on_loose_permissions(path, opened_st, parsed)
_file_cache = (path, _file_stamp(opened_st), content, parsed)
return parsed


def _stat_config_file(path: Path) -> os.stat_result | None:
"""Stat the config file, returning ``None`` if it does not exist."""
try:
return path.stat()
except FileNotFoundError:
return None
except OSError as exc:
raise ConfigurationError(f"could not access {path}: {exc}") from exc


def _cached_parse_by_metadata(path: Path, st: os.stat_result) -> _ParsedFile | None:
"""The cached parse when the metadata stamp still matches, else ``None``.

POSIX ``st_ctime_ns`` advances on any inode change, so the metadata stamp
catches even a rewrite that restores the original mtime. Windows ctime is
*creation* time, so there the stamp cannot see that class of edit and the
content compare in :func:`_parse_or_reuse_cache` is the only correct check
-- the re-read it forces is deliberate, and ``test_file_edit_is_picked_up``
pins it. Do not drop the ctime gate (or extend the stamp to Windows)
without a Windows-safe change detector.
"""
cached = _file_cache
if (
os.name != "nt"
Expand All @@ -1168,28 +1171,33 @@ def _load_file(path: Path) -> _ParsedFile:
and cached[1] == _file_stamp(st)
):
return cached[3]
return None


def _read_file_content(path: Path) -> tuple[bytes, os.stat_result]:
"""Read the file content and return it with the stat of the opened handle."""
try:
with path.open("rb") as handle:
content = handle.read()
opened_st = os.fstat(handle.fileno())
except OSError as exc:
raise ConfigurationError(f"could not read {path}: {exc}") from exc
return content, opened_st


def _parse_or_reuse_cache(path: Path, content: bytes) -> _ParsedFile:
"""Parse the TOML content, reusing the cache if content is unchanged."""
cached = _file_cache
if cached is not None and cached[0] is path and cached[2] == content:
parsed = cached[3]
else:
tomllib = _toml_parser()
try:
data = tomllib.loads(content.decode("utf-8"))
except UnicodeDecodeError as exc:
raise ConfigurationError(f"{path} is not valid UTF-8: {exc}") from exc
except tomllib.TOMLDecodeError as exc:
raise ConfigurationError(f"{path} is not valid TOML: {exc}") from exc
parsed = _interpret(data, path)
_warn_on_loose_permissions(path, opened_st, parsed)
_file_cache = (path, _file_stamp(opened_st), content, parsed)
return parsed
return cached[3]
tomllib = _toml_parser()
try:
data = tomllib.loads(content.decode("utf-8"))
except UnicodeDecodeError as exc:
raise ConfigurationError(f"{path} is not valid UTF-8: {exc}") from exc
except tomllib.TOMLDecodeError as exc:
raise ConfigurationError(f"{path} is not valid TOML: {exc}") from exc
return _interpret(data, path)


def _file_stamp(st: os.stat_result) -> _FileStamp:
Expand Down
76 changes: 49 additions & 27 deletions dataretrieval/_wqx.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,49 @@ def _build_utc_datetime(
)


def _build_triplet_datetime(
df: pd.DataFrame, prefix: str, columns: set[str]
) -> pd.Series | None:
"""Try each Time/TimeZone suffix pair and return a UTC Series, or None."""
for time_suffix, tz_suffix in _TIME_TZ_SUFFIXES:
time_col = prefix + time_suffix
tz_col = prefix + tz_suffix
if time_col in columns and tz_col in columns:
return _build_utc_datetime(df[prefix + "Date"], df[time_col], df[tz_col])
return None


def _find_datetime_triplets(df: pd.DataFrame) -> dict[str, pd.Series]:
"""Detect Date/Time/TimeZone column triplets and build UTC datetime columns.

Returns a mapping of new column names to UTC Series.
"""
columns = set(df.columns)
new_columns: dict[str, pd.Series] = {}

for col in df.columns:
if not col.endswith("Date"):
continue
prefix = col.removesuffix("Date")
target = prefix + "DateTime"
if target in columns or target in new_columns:
continue
utc_series = _build_triplet_datetime(df, prefix, columns)
if utc_series is not None:
new_columns[target] = utc_series

return new_columns


def _resolve_sort_key(df: pd.DataFrame) -> str | None:
"""Pick the canonical sort column, falling back to the first ``*Date``."""
if "Activity_StartDateTime" in df.columns:
return "Activity_StartDateTime"
if "ActivityStartDateTime" in df.columns:
return "ActivityStartDateTime"
return next((c for c in df.columns if c.endswith("Date")), None)


def _attach_datetime_columns(df: pd.DataFrame) -> pd.DataFrame:
"""Append a UTC ``<prefix>DateTime`` column per Date/Time/TimeZone triplet.

Expand Down Expand Up @@ -82,37 +125,16 @@ def _attach_datetime_columns(df: pd.DataFrame) -> pd.DataFrame:
and rows sorted by the activity-start datetime (if any date column
was detected).
"""
columns = set(df.columns)
new_columns = {}
first_date_col = None
for col in df.columns:
if not col.endswith("Date"):
continue
if first_date_col is None:
first_date_col = col
prefix = col.removesuffix("Date")
target = prefix + "DateTime"
if target in columns or target in new_columns:
continue
for time_suffix, tz_suffix in _TIME_TZ_SUFFIXES:
time_col = prefix + time_suffix
tz_col = prefix + tz_suffix
if time_col in columns and tz_col in columns:
new_columns[target] = _build_utc_datetime(
df[col], df[time_col], df[tz_col]
)
break
new_columns = _find_datetime_triplets(df)

if new_columns:
# Concat in one shot — per-column assignment on a wide CSV-derived
# frame triggers pandas' fragmentation PerformanceWarning.
df = pd.concat([df, pd.DataFrame(new_columns, index=df.index)], axis=1)
sort_key: str | None
if "Activity_StartDateTime" in df.columns:
sort_key = "Activity_StartDateTime"
elif "ActivityStartDateTime" in df.columns:
sort_key = "ActivityStartDateTime"
else:
sort_key = first_date_col

# The appended columns end in "DateTime", so the first "*Date" column is
# the same before and after the concat.
sort_key = _resolve_sort_key(df)
if sort_key is not None:
df = df.sort_values(by=sort_key, ignore_index=True)
return df
Expand Down
5 changes: 5 additions & 0 deletions dataretrieval/codes/states.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ def _to_state_one(value: str | int, to: str) -> str:
f'code ("WI"), or a two-digit ANSI/FIPS code ("55").'
)

return _format_state(name, to)


def _format_state(name: str, to: str) -> str:
"""Render a canonical state *name* in the ``to`` representation."""
if to == "name":
return name
if to == "postal":
Expand Down
Loading