Skip to content

Commit 3628b3c

Browse files
committed
docs(ogc): correct aggregation descriptions
Clarify rate-limit header selection, elapsed-duration aggregation, cross-chunk deduplication, GeoJSON output columns, and retry-policy wording.
1 parent 6175455 commit 3628b3c

7 files changed

Lines changed: 49 additions & 45 deletions

File tree

dataretrieval/ogc/chunking.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -438,12 +438,11 @@ def _combine_raw(self) -> tuple[pd.DataFrame, httpx.Response]:
438438
439439
Frames concatenate in sub-args *index* order (``sorted`` keys —
440440
deterministic, independent of parallel completion order). The
441-
aggregated response takes its headers from the most-recently-
442-
*completed* sub-request: the ``track`` closure in :meth:`_run`
443-
is the only writer of ``self._chunks`` and ``dict`` preserves
444-
insertion order, so the chunks' natural order is completion
445-
order and the last one carries the freshest
446-
``x-ratelimit-remaining``.
441+
aggregated response takes its headers from the response with the
442+
lowest reported ``x-ratelimit-remaining`` value. If no response
443+
reports that header, it falls back to the last completed response;
444+
``self._chunks`` preserves completion order because the ``track``
445+
closure in :meth:`_run` is its only writer.
447446
448447
Returns
449448
-------
@@ -542,8 +541,9 @@ def resume(self) -> tuple[pd.DataFrame, Any]:
542541
Combined data from every successful sub-request.
543542
response
544543
The finalized aggregate — a raw :class:`httpx.Response`
545-
(canonical URL, most-recently-completed sub-request's headers,
546-
cumulative elapsed time) by default, or whatever
544+
(canonical URL, headers from the response with the lowest reported
545+
remaining quota, and summed response elapsed durations) by default,
546+
or whatever
547547
:attr:`finalize` produces (e.g. ``BaseMetadata`` for the OGC
548548
getters).
549549
@@ -624,8 +624,9 @@ async def _run(self, max_concurrent: int | None) -> tuple[pd.DataFrame, Any]:
624624
Combined data from every sub-request.
625625
response
626626
The finalized aggregate — a raw :class:`httpx.Response`
627-
(canonical URL, most-recently-completed sub-request's headers,
628-
cumulative elapsed time) by default, or whatever
627+
(canonical URL, headers from the response with the lowest reported
628+
remaining quota, and summed response elapsed durations) by default,
629+
or whatever
629630
:attr:`finalize` produces (e.g. ``BaseMetadata`` for OGC getters).
630631
631632
Raises

dataretrieval/ogc/combining.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ def _set_response_url(response: httpx.Response, url: str | httpx.URL) -> None:
7272
def _lowest_remaining(responses: list[httpx.Response]) -> httpx.Response:
7373
"""The response reporting the lowest ``x-ratelimit-remaining``.
7474
75-
The rate-limit counter decreases monotonically within a window, so the
76-
smallest value any sub-request saw is the most-current "quota left after
77-
this call" — the right thing to surface. Under concurrent fan-out the
78-
last response *by index* need not be the one the server processed last, so
79-
pick the minimum (falling back to the last response if none report it).
75+
Within a rate-limit window, the counter decreases monotonically, so the
76+
smallest value observed is the most conservative value to surface. Under
77+
concurrent fan-out, the last response *by index* need not be the one the
78+
server processed last. Fall back to the last response when none reports
79+
the header.
8080
"""
8181
best: httpx.Response | None = None
8282
best_remaining: int | None = None
@@ -115,18 +115,19 @@ def _merge_response(
115115

116116

117117
def _combine_chunk_frames(frames: list[pd.DataFrame]) -> pd.DataFrame:
118-
"""Concatenate per-chunk frames and deduplicate non-null feature IDs.
118+
"""Concatenate per-chunk frames and deduplicate IDs across chunks.
119119
120120
Empty frames are ignored before concatenation so an empty plain
121121
:class:`pandas.DataFrame` cannot downgrade a real ``GeoDataFrame`` and
122122
strip its geometry or CRS. When every frame is empty, the first frame is
123123
returned to preserve its concrete type.
124124
125-
Deduplication is unconditional because overlap is possible on every plan
126-
axis. Filter clauses can match the same feature, and list inputs can contain
127-
repeated values or otherwise select overlapping records. Rows without an
128-
``id`` are preserved verbatim: pandas treats null values as duplicates, so
129-
applying ``drop_duplicates`` to those rows would silently lose data.
125+
When multiple non-empty frames are combined, non-null feature IDs are
126+
deduplicated regardless of the plan axis. Filter clauses can match the same
127+
feature, and list inputs can contain repeated values or otherwise select
128+
overlapping records. Rows without an ``id`` are preserved verbatim: pandas
129+
treats null values as duplicates, so deduplicating them would silently lose
130+
data.
130131
"""
131132
non_empty = [frame for frame in frames if not frame.empty]
132133
if not non_empty:
@@ -155,10 +156,10 @@ def _combine_chunk_responses(
155156
Fold per-sub-request responses into a single aggregated response.
156157
157158
For a multi-response input, returns a shallow copy of
158-
``responses[0]`` with ``.headers`` set to those of the most-depleted
159-
response (lowest ``x-ratelimit-remaining`` the quota actually left
160-
after the fan-out; see :func:`_lowest_remaining`), ``.elapsed`` set
161-
to total wall-clock across every response, and ``.url`` set to the
159+
``responses[0]`` with ``.headers`` set to those of the response reporting
160+
the lowest ``x-ratelimit-remaining`` value (the most conservative quota
161+
observation; see :func:`_lowest_remaining`), ``.elapsed`` set to the sum of
162+
the per-response elapsed durations, and ``.url`` set to the
162163
canonical original-query URL (when supplied) so ``BaseMetadata``
163164
reflects the user's full request rather than the first chunk.
164165
@@ -169,7 +170,7 @@ def _combine_chunk_responses(
169170
Parameters
170171
----------
171172
responses : list[httpx.Response]
172-
One response per completed sub-request, in execution order.
173+
One response per completed sub-request, in caller-provided order.
173174
canonical_url : str or None
174175
URL of the unchunked original request. ``None`` skips the URL
175176
override — used by the passthrough path (the fetcher's
@@ -191,8 +192,8 @@ def _combine_chunk_responses(
191192
if len(responses) == 1 and canonical_url is None:
192193
return responses[0]
193194

194-
# Headers come from the most-depleted response (lowest quota left after a
195-
# concurrent fan-out; ``_lowest_remaining`` returns the lone response as-is
195+
# Headers come from the response with the lowest reported remaining quota;
196+
# ``_lowest_remaining`` returns the lone response as-is
196197
# for a single-element list). ``_merge_response`` re-sums elapsed onto a
197198
# fresh copy, so repeated calls (e.g. via ``ChunkedCall.partial_response``
198199
# during resume) stay idempotent.

dataretrieval/ogc/engine.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -658,9 +658,9 @@ async def _paginate(
658658
response : httpx.Response
659659
A shallow copy of the first-page response, with ``.headers``
660660
rebuilt as a fresh ``httpx.Headers`` reflecting the last page and
661-
``.elapsed`` set to cumulative wall-clock. The canonical URL is
662-
preserved from the first page. The original first-page response
663-
is not mutated.
661+
``.elapsed`` set to the sum of the per-page response durations. The
662+
canonical URL is preserved from the first page. The original first-page
663+
response is not mutated.
664664
665665
Raises
666666
------

dataretrieval/ogc/retry.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,9 @@ def _classify_transient(
203203
if isinstance(exc, RateLimited):
204204
return QuotaExhausted, exc.retry_after
205205
if isinstance(exc, TransientError):
206-
# Every non-rate-limit typed transient is a service interruption. This
207-
# fallback keeps future TransientError subclasses resumable after their
208-
# inline retries are exhausted.
206+
# Every typed transient other than a rate-limit error is a service
207+
# interruption. This fallback keeps future TransientError subclasses
208+
# resumable after their inline retries are exhausted.
209209
return ServiceInterrupted, exc.retry_after
210210
if isinstance(exc, (httpx.HTTPError, httpx.InvalidURL)):
211211
return ServiceInterrupted, None
@@ -265,8 +265,8 @@ def _retryable(exc: BaseException) -> tuple[bool, float | None]:
265265
266266
Wrapped mid-pagination failures are not retried from page one; they instead
267267
escalate to a resumable :class:`ChunkInterrupted`. ``httpx.InvalidURL`` and
268-
non-transport ``httpx.HTTPError`` instances are resumable but deterministic,
269-
so they are classified without being retried.
268+
non-transport ``httpx.HTTPError`` instances are classified as resumable but
269+
excluded from automatic retry by policy.
270270
"""
271271
classification = _classify_transient(exc)
272272
if classification is None:

dataretrieval/ogc/shaping.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,13 @@ def _get_resp_data(
101101
102102
Notes
103103
-----
104-
The non-geopandas branch normalizes each feature's ``properties``
105-
object, flattening nested dictionaries with an underscore separator, then
106-
adds the top-level ``id`` and ``geometry.coordinates`` columns. The ``id``
107-
column is always added so the downstream service-specific rename works even
108-
when all IDs are missing; ``geometry`` is added only when coordinates are
109-
present. Feature-level envelope fields are deliberately excluded.
104+
The non-geopandas branch normalizes each feature's ``properties`` object,
105+
flattening nested dictionaries with an underscore separator, then adds the
106+
top-level ``id`` and a ``geometry`` column containing the coordinates. The
107+
``id`` column is always added so the downstream service-specific rename
108+
works even when all IDs are missing; ``geometry`` is added only when
109+
coordinates are present. Feature-level envelope fields are deliberately
110+
excluded.
110111
"""
111112
if body is None:
112113
body = resp.json()

dataretrieval/wateruse.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,9 @@ async def _one(request: httpx.Request) -> tuple[pd.DataFrame, httpx.Response]:
365365
results = await asyncio.gather(*(_one(req) for req in requests))
366366

367367
# Reuse the engine's combine helpers: drop empty frames and concat, and fold
368-
# the per-location responses into one (lowest-remaining rate-limit headers +
369-
# cumulative elapsed), keeping the first request's URL as the query identity.
368+
# the per-location responses into one (headers from the response with the
369+
# lowest reported remaining quota plus summed response durations), keeping
370+
# the first request's URL as the query identity.
370371
frames = [frame for frame, _ in results]
371372
responses = [resp for _, resp in results]
372373
return _combine_chunk_frames(frames), _combine_chunk_responses(

tests/waterdata_utils_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ def test_get_resp_data_always_materializes_id_column():
677677

678678

679679
def test_get_resp_data_flattens_nested_properties():
680-
"""Nested GeoJSON properties keep the historical underscore columns."""
680+
"""Nested GeoJSON properties keep underscore-separated column names."""
681681
resp = mock.MagicMock()
682682
resp.json.return_value = {
683683
"features": [

0 commit comments

Comments
 (0)