ADR 0007: Organize service adapters behind stable facades
Status
Accepted
Amended after acceptance under ADR 0000: Record each explanation once, in the place that owns it; the
Notes section records every clause added or corrected.
Context
A service facade can remain stable while its implementation grows for unrelated upstream collections. Keeping every Water Data getter in one module coupled together changes to time series, monitoring metadata, field measurements, reference catalogs, Samples, statistics, and generalized CQL queries. Active service modules also relied on Python’s implicit wildcard-export behavior, making their intended public surfaces difficult to distinguish from imported helpers.
Decision
dataretrieval.waterdata.api is a compatibility facade with no collection
logic. Implementation functions are grouped by collection family in
time_series, metadata, measurements, reference, samples, and
cql. Existing focused modules continue to own ratings, nearest-value
selection, Statistics API execution, shared Water Data policy, and type
vocabularies.
The facade re-exports the established functions and preserves their signatures,
their identity at dataretrieval.waterdata, and the private Samples
constants that compatibility tests rely on. It does not rewrite their
__module__: each function reports the family module that defines it, so a
traceback names a file that contains code. Collection-family modules do not
import one another; shared behavior belongs in Water Data policy, OGC, or
transport modules.
Active service and focused implementation modules declare explicit __all__
exports. Deprecated NWIS remains outside this modernization. Service adapters do
not import another adapter’s implementation to obtain transport behavior.
The __module__ rule above is scoped to this facade, where the family module
is a file that exists for a traceback to name. It is not a package-wide prohibition: the
legacy dataretrieval.utils names are split across private modules by
dependency and do report the documented path, because there the alternative is
a public, documented import location pointing at a private module.
Typed getters are the surface; exactly one generic query path accompanies
them. cql is the only untyped member of the collection families, and
deliberately so. The alternative in one direction – a single generic query
function replacing the typed getters – gives up the parameter documentation
and validation that are most of these getters’ value. The alternative in the
other – a cql= passthrough on every family – multiplies the generic path
by the number of collections while making each family’s surface partly untyped.
One generic path, named as such, keeps both properties.
Identifier columns are parsed as text. This one clause applies
package-wide, legacy NWIS included: it is about what an adapter returns, not
how it is organized. HUCs, parameter codes, FIPS codes, and monitoring-location
identifiers (site_no in NWIS) have significant leading zeros, and a bare
read_csv infers them as integers and
drops those zeros – "00060" becomes 60, so the value is wrong, with no indication,
rather than missing. Every adapter reading a USGS tabular response names its
identifier columns as str before parsing, which is why a two-pass header
read is not a redundancy to be optimized away.
Return contracts remain service-specific. Tabular services generally return a
(DataFrame, metadata) pair, while NLDI returns geospatial values directly,
StreamStats exposes response/domain objects, and ratings return parsed tables or
raw catalog features. Uniformity is not a reason to break these established
contracts.
Consequences
Collection changes affect fewer implementation and test files.
Existing package and
waterdata.apiimport paths remain stable.Explicit exports make accidental public-surface growth reviewable.
More modules mean a facade to maintain, plus executable signature and export snapshots.
Tests are described as public-contract, adapter-contract, component, or cross-component layers without moving established files.
Compliance
tests/contracts/public_api_test.py records Water Data imports, signatures,
facade identity, and compatibility names. tests/architecture_test.py
requires a logic-free facade, exact active-service exports, and separate OGC
request construction and schema execution. .importlinter keeps the
collection families independent of each other, restricts the facade-only
consumers (NGWMN and waterdata.cql) to the OGC facade, and prevents one
adapter from importing another. The identifier-column rule is covered by
tests/nwdc_test.py::test_huc12_id_kept_as_string_with_leading_zero and the equivalent
leading-zero assertions in the WQP and NWIS adapter tests.
Notes
The __module__ scoping note and the generic-path and identifier-column
clauses were added after the original decision; the rest of the record is
unchanged. They consolidate under ADR 0000 the rules the code was stating in
prose. The scoping note in particular records why _querying.py reassigning
__module__ is not a violation of this record, a question an audit raised.