Skip to content

Commit 44f2aee

Browse files
authored
Document architecture baseline and add guardrails (DOI-USGS#349)
* chore(architecture): document baseline and add guardrails * docs(architecture): remove duplicated policy
1 parent 0b99c39 commit 44f2aee

13 files changed

Lines changed: 769 additions & 173 deletions

CONTRIBUTING.md

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ however writing code is not the only way to contribute.
2222
- [Style](#style)
2323
- [Docstrings](#docstrings)
2424
- [Quotes](#quotes)
25+
- [Updating Package Version](#updating-package-version)
2526
- [Documentation](#documentation)
2627
- [Contributing to the Documentation](#contributing-to-the-documentation)
2728
- [Adding Examples to the Documentation](#adding-examples-to-the-documentation)
@@ -79,30 +80,51 @@ Please do not combine multiple feature enhancements into a single pull request.
7980
Before you submit a pull request, check that it meets these guidelines:
8081

8182
1. If the pull request adds or modifies package functionality, unit tests
82-
should be written to test the new functionality
83-
2. If the pull request adds or modifies functionality, the documentation should
84-
be updated. To do so, either add or modify a functions docstring which will
85-
automatically become part of the API documentation
86-
3. The pull request should work for Python 3.9 and later - refer to the
87-
[python-package.yml file](https://github.com/DOI-USGS/dataretrieval-python/blob/main/.github/workflows/python-package.yml)
88-
for the latest versions of Python being tested by the continuous integration
89-
pipelines. This will be checked automatically by the CI pipelines once the
90-
pull request is opened.
83+
should be written to test the new functionality.
84+
2. If the pull request adds or modifies functionality, update the documentation
85+
or function docstrings that describe it.
86+
3. The pull request should work for Python 3.10 and later. Refer to the
87+
[Python package workflow](https://github.com/DOI-USGS/dataretrieval-python/blob/main/.github/workflows/python-package.yml)
88+
for the versions and operating systems currently tested by CI.
89+
4. Build-related changes should preserve the installed-wheel smoke test; tests
90+
run from a source checkout are not sufficient to prove package contents.
91+
5. Architecturally significant changes should update the
92+
[architecture documentation](docs/source/architecture/index.rst), add or
93+
supersede an ADR, and adjust the corresponding fitness function.
9194

9295
### Coding Standards and Style
9396

94-
Note that coding standards and style as described below are strong suggestions,
95-
the `dataretrieval` project does not strictly lint or enforce style guidelines
96-
via any automated processes or pipelines.
97+
The continuous integration and pre-commit configurations enforce formatting,
98+
linting, and strict type checking. Run the relevant checks before opening a PR:
99+
100+
```bash
101+
ruff check .
102+
ruff format --check .
103+
mypy
104+
coverage run -m pytest tests/
105+
coverage report -m
106+
```
107+
108+
For documentation changes, install `.[doc,nldi]` and run `make html` from
109+
`docs/`. The broader `make docs` target also runs doctests and network-dependent
110+
link checking.
97111

98112
#### Style
99113

100-
* Attempt to write code following the [PEP8 style guidelines](https://peps.python.org/pep-0008/) as much as possible
101-
* The public interface should emphasize functions over classes; however, classes
102-
can and should be used internally and in tests
103-
* Functions for downloading data from a specific web portal must be grouped
104-
within their own submodule
105-
* For example, all NWIS functions are located at `dataretrieval.nwis`
114+
* Follow the [PEP8 style guidelines](https://peps.python.org/pep-0008/).
115+
* The public interface should emphasize functions over classes; classes can and
116+
should be used internally and in tests.
117+
* Group public download functions by data portal. For example, modern Water
118+
Data functions belong in `dataretrieval.waterdata`; legacy NWIS functions
119+
remain quarantined in `dataretrieval.nwis` during deprecation.
120+
* Preserve the dependency direction documented in
121+
[`docs/source/architecture`](docs/source/architecture/index.rst): public
122+
facades depend on service/protocol adapters, which depend on stable shared
123+
policy and infrastructure. Shared OGC code must not import service adapters,
124+
and modern modules must not depend on legacy NWIS.
125+
* Treat underscore-prefixed helpers as implementation details. Existing
126+
cross-package uses are documented variances, not extension points for new
127+
code.
106128

107129
#### Docstrings
108130
* Docstrings should follow the [numpy standard](https://numpydoc.readthedocs.io/en/v1.5.0/format.html):
@@ -165,6 +187,14 @@ via any automated processes or pipelines.
165187
return re.search(r"(?i)(arr|avast|yohoho)!", message) is not None
166188
```
167189

190+
### Updating Package Version
191+
192+
The package version is derived automatically from Git tags by
193+
`setuptools_scm` (see `[tool.setuptools_scm]` in `pyproject.toml`), so there is
194+
no version string to edit by hand. To cut a release, tag the commit (for
195+
example, `git tag v1.2.3`) and push the tag; both the installed package version
196+
and the documentation's `version` and `release` values follow from it.
197+
168198
---
169199

170200
## Documentation

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
**08/02/2026:** Fixed source-distribution and wheel package discovery so the `dataretrieval.ogc` and `dataretrieval.waterdata` subpackages are included in installed artifacts. CI now builds and installs the wheel outside the source checkout before importing the core service modules. Added an architecture baseline, initial decision records, and executable dependency-direction guardrails for the existing modular-monolith boundaries.
2+
13
**06/23/2026:** **Breaking change (1.2.0):** the minimum supported Python is now **3.10** (`requires-python = ">=3.10"`). 3.9 support was already effectively broken — the `waterdata` module's dependencies (`anyio`, the test stack) require 3.10+, and the `waterdata` test modules already skipped on <3.10. `anyio` is now declared as a direct dependency (it is imported directly by `waterdata`), and the CI/ruff/mypy targets move to 3.10. Also fully removed the deprecated `variable_info` metadata property: the `NWIS_Metadata` override only warned and returned `None` (it relied on the defunct `get_pmcodes`), and the `BaseMetadata` abstract is gone too since nothing implemented it — accessing `.variable_info` now raises `AttributeError`. `site_info` is unaffected.
24

35
**06/23/2026:** **Breaking change (1.2.0):** removed the `nadp` module and the deprecated `samples` module ahead of the 1.2.0 release. `nadp` was deprecated on 05/01/2026 — NADP is not a USGS data source, so retrieve NADP data directly from https://nadp.slh.wisc.edu/. The `samples.get_usgs_samples` shim (a deprecated forward to the modern getter) is gone; use `waterdata.get_samples()` instead. `import dataretrieval.nadp` / `import dataretrieval.samples` now raise `ModuleNotFoundError`.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
ADR 0001: Retain a modular monolith with service adapters
2+
=========================================================
3+
4+
Status
5+
------
6+
7+
Accepted
8+
9+
Context
10+
-------
11+
12+
``dataretrieval`` is one Python library serving users who often combine several
13+
hydrologic data sources in one process or notebook. The upstream services have
14+
different protocols and schemas, but the package has one release lifecycle and
15+
maintainer group. Distributed deployment or separately versioned service
16+
packages would add operational and compatibility cost without improving the
17+
library's primary use cases.
18+
19+
Decision
20+
--------
21+
22+
Maintain one installable distribution organized as a modular monolith. Expose
23+
functions grouped by data portal. Keep service- and protocol-specific adapters
24+
independent behind those facades, and share infrastructure only where its
25+
contract is genuinely API-neutral.
26+
27+
Treat the OGC subsystem as a protocol component used by Water Data and NGWMN,
28+
not as a universal service framework. Do not force NLDI, StreamStats, WQP, or
29+
Water Use into OGC-shaped return values or paging semantics.
30+
31+
Consequences
32+
------------
33+
34+
- Users install and version one coherent package.
35+
- Cross-service API compatibility can be tested in one pipeline.
36+
- Component boundaries are source-level rather than deployment boundaries and
37+
therefore require import checks and review discipline.
38+
- Shared infrastructure must remain small enough that it does not become a god
39+
module.
40+
- A new service should begin as its own adapter and earn shared abstractions
41+
through demonstrated duplication rather than up-front generalization.
42+
43+
Compliance
44+
----------
45+
46+
``tests/architecture_test.py`` prevents shared OGC infrastructure from
47+
importing service adapters and prevents modern modules from depending on legacy
48+
NWIS. The installed-wheel CI job verifies that the whole monolith ships as one
49+
usable artifact.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
ADR 0002: Keep synchronous public APIs over async internals
2+
===========================================================
3+
4+
Status
5+
------
6+
7+
Accepted
8+
9+
Context
10+
-------
11+
12+
The established public API consists of synchronous functions used heavily in
13+
scripts, notebooks, pandas workflows, and teaching examples. OGC pagination and
14+
chunk fan-out benefit from asynchronous I/O, but exposing only async functions
15+
would be a broad breaking change and would complicate common notebook use.
16+
17+
Calling ``asyncio.run`` directly is also unsafe when a caller already has a
18+
running event loop, as Jupyter commonly does.
19+
20+
Decision
21+
--------
22+
23+
Keep public service getters synchronous. Async-capable implementations may run
24+
inside a short-lived anyio blocking portal and use ``httpx.AsyncClient`` for
25+
pagination and bounded fan-out. Internal async functions are implementation
26+
details, not a second public API promise.
27+
28+
Ambient per-call policy must propagate into the worker context. A resumable OGC
29+
call captures the context needed to rebuild its remaining requests after the
30+
original getter has returned.
31+
32+
Consequences
33+
------------
34+
35+
- Existing scripts and notebooks retain simple blocking call sites.
36+
- Concurrent network waits improve large paginated downloads.
37+
- Each top-level async-backed call pays worker-thread and portal startup cost.
38+
- Cancellation, context propagation, and client ownership need explicit tests.
39+
- A future public async API, if justified, should be additive and share the same
40+
lower-level contracts rather than duplicate behavior.
41+
42+
Compliance
43+
----------
44+
45+
Tests exercise calls inside an already running event loop, ambient context
46+
capture on resume, cancellation precedence, bounded in-flight work, and shared
47+
client ownership.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
ADR 0003: Direct dependencies toward stable policy
2+
==================================================
3+
4+
Status
5+
------
6+
7+
Accepted
8+
9+
Context
10+
-------
11+
12+
Service modules change when upstream APIs change. Error policy, generic data
13+
structures, and transport contracts should be more stable. If shared
14+
infrastructure imports service details, unrelated upstream changes propagate
15+
inward and reuse becomes unsafe. Several current private imports also act as
16+
unintended cross-package contracts.
17+
18+
Decision
19+
--------
20+
21+
Dependencies point from public facades to service/protocol adapters and then to
22+
stable shared policy and third-party infrastructure. In particular:
23+
24+
- ``dataretrieval.exceptions`` is a runtime-dependency-light leaf.
25+
- ``dataretrieval.ogc`` must not import Water Data, NGWMN, Water Use, or NWIS.
26+
- Modern modules must not import deprecated NWIS.
27+
- New non-OGC services must not obtain generic transport behavior by importing
28+
private OGC implementation symbols.
29+
30+
Underscore-prefixed symbols remain implementation details even when existing
31+
internal modules currently use them.
32+
33+
Consequences
34+
------------
35+
36+
- Stable policy can be reused without pulling in service schemas.
37+
- Static import checks can detect architectural erosion early.
38+
- Moving existing private seams requires compatibility-aware sequencing.
39+
- Some duplication is preferable to a premature abstraction that couples
40+
unlike upstream protocols.
41+
42+
Compliance
43+
----------
44+
45+
``tests/architecture_test.py`` parses runtime imports and enforces the rules
46+
that hold today. Its allowlist is the authoritative inventory of exact temporary
47+
cross-boundary imports; this ADR owns the direction and rationale rather than a
48+
second copy of that mutable inventory.
49+
50+
The allowlist should shrink as private seams move. Any growth requires explicit
51+
architecture review, and a change to the dependency policy requires this ADR to
52+
be superseded.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
ADR 0004: Use typed failures and bounded recovery
2+
=================================================
3+
4+
Status
5+
------
6+
7+
Accepted
8+
9+
Context
10+
-------
11+
12+
Remote hydrologic services fail through HTTP statuses, rate limits, timeouts,
13+
invalid payloads, and mid-pagination interruptions. Returning partial data after
14+
an undetected page failure is worse than raising. At the same time, retrying
15+
without limits can stall callers, multiply quota usage, or hide persistent
16+
faults.
17+
18+
Decision
19+
--------
20+
21+
All request failures exposed by public service modules derive from
22+
``DataRetrievalError`` and provide uniform ``status_code``, ``retry_after``, and
23+
``retryable`` attributes. Status mapping lives in one policy function.
24+
25+
Where automatic recovery is supported, retries are bounded, use exponential
26+
backoff with full jitter, honor only bounded ``Retry-After`` delays, and preserve
27+
cancellation. OGC fan-out retains completed subrequests and raises a typed
28+
``ChunkInterrupted`` with a handle that resumes only missing work. Fatal or
29+
unknown failures are not disguised as resumable transients.
30+
31+
This decision does not assert that every upstream API supports pagination,
32+
chunking, or resume. Those capabilities remain explicit per service until a
33+
shared API-neutral transport contract is introduced.
34+
35+
Consequences
36+
------------
37+
38+
- Callers can catch one stable base error and still branch on useful fields.
39+
- Mid-pagination failure cannot silently look like a complete dataset.
40+
- Retry can increase latency and request quota, so policy and defaults are part
41+
of observable behavior.
42+
- Partial OGC state requires careful serialization and finalization tests.
43+
- Expanding retry to another service requires service-specific idempotency and
44+
failure-contract tests.
45+
46+
Compliance
47+
----------
48+
49+
Tests cover status-to-type mapping, uniform fields, transport wrapping,
50+
pagination failure, retry exhaustion and jitter bounds, ``Retry-After`` limits,
51+
resume equivalence, partial-state stability, pickling, and cancellation
52+
precedence.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
ADR 0005: Quarantine and retire legacy NWIS
2+
===========================================
3+
4+
Status
5+
------
6+
7+
Accepted
8+
9+
Context
10+
-------
11+
12+
The legacy ``dataretrieval.nwis`` facade overlaps modern Water Data APIs and
13+
contains functions backed by retired services. Existing users still require a
14+
migration window, while new code should not acquire dependencies on behavior
15+
scheduled for removal.
16+
17+
Decision
18+
--------
19+
20+
Keep ``dataretrieval.nwis`` importable during its published deprecation window
21+
and remove it on or after 2027-05-06 through the project's compatibility and
22+
release process.
23+
24+
Add no new NWIS capabilities. Permit compatibility, security, and correctness
25+
fixes only. Every active deprecated function should warn once per user call and
26+
name a supported replacement where one exists. Modern modules must never import
27+
NWIS.
28+
29+
Do not treat WQP's legacy result profiles as part of NWIS retirement; they have
30+
separate upstream constraints and migration behavior.
31+
32+
Consequences
33+
------------
34+
35+
- Existing users have a predictable migration period.
36+
- Maintainers avoid investing in a second implementation of modern retrieval
37+
behavior.
38+
- Legacy integration tests may require special handling as upstream endpoints
39+
disappear.
40+
- Removal still requires release notes, replacement checks, and an intentional
41+
compatibility boundary.
42+
43+
Compliance
44+
----------
45+
46+
Deprecation tests verify one warning per public call and validate named Water
47+
Data replacements. ``tests/architecture_test.py`` prevents modern package
48+
modules from importing ``dataretrieval.nwis``.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Architecture Decision Records
2+
=============================
3+
4+
Architecture Decision Records (ADRs) capture why a significant structural or
5+
cross-cutting choice was made, the trade-offs it accepts, and how compliance is
6+
checked. They complement code and API documentation rather than repeating
7+
implementation details.
8+
9+
Statuses are ``Proposed``, ``Accepted``, ``Superseded``, or ``Rejected``. An
10+
accepted decision is not edited to reverse its meaning; a later ADR supersedes
11+
it and links back to the old record. Keep records concise and commit them with
12+
the change that makes the decision effective.
13+
14+
Use :doc:`template` when proposing a decision. Number accepted and proposed
15+
records sequentially.
16+
17+
.. toctree::
18+
:maxdepth: 1
19+
20+
0001-modular-monolith
21+
0002-sync-api-async-internals
22+
0003-dependency-direction
23+
0004-error-retry-resume
24+
0005-legacy-nwis
25+
template

0 commit comments

Comments
 (0)