Skip to content

refactor(ogc): one entry into the engine; retire fetch_ogc_request - #368

Merged
thodson-usgs merged 3 commits into
DOI-USGS:mainfrom
thodson-usgs:refactor/one-ogc-entry
Aug 12, 2026
Merged

thodson-usgs merged 3 commits into
DOI-USGS:mainfrom
thodson-usgs:refactor/one-ogc-entry

Conversation

@thodson-usgs

Copy link
Copy Markdown
Collaborator

Stacked on #367 — this branch includes that PR's commit. Review only the last commit here (refactor(ogc): one entry into the engine; retire fetch_ogc_request); merge #367 first, then this PR reduces to its own diff.

What

Retires fetch_ogc_request and gives get_ogc_data one extra entry shape — a verbatim cql_body — so the OGC facade has a single orchestrated entry that is fully usable through the facade alone.

Why

fetch_ogc_request's interface could not state its own precondition: it took no base_url, so request construction inside it targeted whatever was ambient. Its only production caller, waterdata.get_cql, had to import the private ambient to make it usable, and then re-implemented get_ogc_data's body by hand across four OGC internals — the properties id-switch, request construction, execution, and finalization. The deletion test pointed one way: folding that orchestration into the engine concentrates it where id-switching, base-URL handling, and finalization already live.

How

  • get_ogc_data(..., cql_body=...) drives the prepared POST as a one-item FanOut (ADR 0008's plan shape) with the same retry, progress-line, and interruption semantics as the chunked path — and the same finalizer, so a resumed get_cql now returns the finished (df, BaseMetadata) shape; previously the finalizer was applied outside the executor and exc.call.resume() skipped it, returning a raw (frame, response) pair. A new test pins this.
  • The planner's cql-json passthrough stance is preserved: a verbatim body's size stays the server's judgement (no client-side byte gate on this path).
  • waterdata.cql consumes only the facade (get_ogc_data + prepare_request_args), dropping its four private cross-package imports; a new import-linter contract (cql-facade) pins that, mirroring the NGWMN facade contract.
  • The ogc facade shrinks from four names to three; fetch_ogc_request is deleted (it was internal — no public-surface change).

Testing

  • Full offline suite: 781 passed (includes the new resume-shape regression test).
  • mypy --strict, ruff, xenon, complexipy, and lint-imports (8 contracts, including the new one) all pass.

🤖 Generated with Claude Code

thodson-usgs and others added 2 commits August 10, 2026 17:15
The base URL, dialect, and row cap traveled by three mechanisms at
once: partial-bound finalizer arguments, ambient ContextVars in
ogc.context, and .get() fallbacks deep in request construction and
shaping. No interface stated that they were required, and the
service-neutral FanOut executor snapshotted a whole execution context
at construction solely to keep that adapter state alive across a
resume fired after the originating with-blocks had exited.

Bind them the way the finalizer was already bound instead:

- _construct_api_requests / _construct_cql_request take base_url (and
  dialect) as explicit parameters; get_ogc_data binds them with
  functools.partial into both the plan's build_request and the fetch,
  so planning-time sizing and a later exc.call.resume() rebuild every
  chunk against the values the call was created with.
- row_cap is a real parameter of _paginate/_walk_pages, forwarded to
  the transport paginate parameter that already existed, instead of
  being laundered through an ambient read inside a wrapper.
- _finalize_ogc / _deal_with_empty require base_url; the empty-frame
  schema lookup can no longer silently target the ambient default.
- FanOut drops the copy_context() snapshot and _resume_in_context:
  transport carries no adapter state (ADR 0006), and the progress
  reporter reaches the worker through the portal's normal
  calling-context copy.
- waterdata.get_cql states its target explicitly instead of entering
  a private ambient scope.
- ogc/context.py is deleted.

The captured-context regression test becomes a creation-time-binding
test; the request-construction unit tests bind the Water Data target
with the same partial pattern the engine uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fetch_ogc_request's interface could not state its own precondition: it
took no base_url, so request construction inside it targeted whatever
was ambient -- and its only production caller, waterdata.get_cql,
imported the private ambient to make it usable, then re-implemented
get_ogc_data's body by hand across four OGC internals (the
properties id-switch, request construction, execution, finalization).

Give get_ogc_data one extra entry shape instead: a verbatim ``cql_body``.
The engine's existing prologue handles the id-switch and finalizer; the
body is driven as a one-item FanOut with the same retry, progress, and
interruption semantics as the chunked path -- and the same finalizer, so
a resumed get_cql now returns the finished (df, BaseMetadata) shape
rather than a raw (frame, response) pair (previously the finalizer was
applied outside the executor and a resume skipped it).

- ogc facade shrinks to three names, each usable through the facade
  alone; fetch_ogc_request is deleted.
- waterdata.cql consumes only the facade (get_ogc_data +
  prepare_request_args), dropping four private cross-package imports;
  a new import-linter contract pins that, mirroring the NGWMN one.
- The planner's cql-json passthrough semantics are preserved: a
  verbatim body's size stays the server's judgement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thodson-usgs

thodson-usgs commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Health metrics vs main

Updated for 8d2eb59e (the /simplify pass) and re-baselined: #367 has merged, so main is now f31b4730 and everything below is #368 alone — not the two-PR stack the earlier version of this comment measured.

Same method as before (pyscn 1.29.0 / radon / xenon / complexipy / import-linter, isolated worktrees, ProjectRoot verified).

pyscn composite: 82/100 (B) — unchanged. Six of seven sub-scores identical to main; Architecture drops 87 → 82.

main (f31b4730) this PR (8d2eb59e)
Modules 56 56
Resolved dependency edges 135 132
Max dependency depth 8 8
Technical-debt estimate 160 h 160 h
Functions 361 359
High-risk entries 19 20 ⚠️
Package LOC (non-blank) 13,958 13,936

Architecture 87 → 82 — new, and it arrived with the /simplify commit

This is the one number that genuinely moved, and it is worth being precise about, because it is not what it looks like.

pyscn reports 26 architecture violations on both sides — the same 26 modules, with byte-identical descriptions. What changes is the severity of exactly two:

responsibility  dataretrieval.transport.retry   warning -> error
    "mixes 10 dependency concerns with fan-in 6 and fan-out 5"   (identical text both sides)
responsibility  dataretrieval.waterdata.utils   warning -> error
    "mixes 9 dependency concerns with fan-in 6 and fan-out 5"    (identical text both sides)

Weighted violations 26 → 34; compliance 86.73% → 82.38%. Measured across three trees:

compliance banded
main (f31b4730) 86.73% 87
PR before /simplify (3fcb0242) 86.67% 87
PR head (8d2eb59e) 82.38% 82

I isolated the cause: checking out 8d2eb59e and reverting only waterdata/cql.py to its pre-/simplify form restores 87, with every other /simplify edit still in place.

The trigger is that cql.py no longer imports dataretrieval.ogc — it now reaches the engine through waterdata.utils.get_ogc_data, like every other Water Data getter. That is the change ADR 0007 asks for, and it is what lets the facade-only import contract hold. Removing that edge takes the graph from 135 to 132 resolved edges, and the two modules above tip across a severity band whose own inputs — their fan-in, fan-out, and concern counts — did not change.

I did not reverse-engineer pyscn's severity formula, so I'll claim only what I measured: neither module gained a dependent or a dependency, their violation text is identical on both sides, and the flip is reproducibly caused by removing an edge elsewhere in the graph. That reads as a banding artifact of a smaller dependency graph, not as new coupling on transport.retry or waterdata.utils.

If the banded number matters more than the direction of the change, the way back to 87 is to restore the cql.py -> dataretrieval.ogc import — but that reintroduces exactly the duplication the /simplify pass removed (base_url, dialect, extra_id_cols, and the output_id default restated at the call site). My recommendation is to accept the 82.

High-risk entries 19 → 20

Still not a new complex function — it is ogc/engine.py's module scope crossing pyscn's cognitive-complexity-25 line:

ogc/engine.py::<module>   cyclomatic 2 -> 2   cognitive 24 -> 26   risk: low -> high

The /simplify pass shaved this from 27 to 26 by replacing the _fetch_cql closure with a functools.partial; it is now one point over the threshold.

18 of the 19 entries already flagged on main are <module> scopes (ogc/planning.py 140, nwis.py 119, nldi.py 88, ogc/requests.py 83, progress.py 82, transport/fanout.py 80, …), so at module scope this metric tracks "how much is in the file", not how gnarly any function is.

Per-function on the same change:

  • get_ogc_data cyclomatic 5 → 6, cognitive 2 → 3
  • fetch_ogc_request and its closure: removed
  • no new function at all — /simplify replaced the _fetch_cql closure with functools.partial(_walk_pages, GEOPANDAS, row_cap=max_rows)

complexipy — the per-function cognitive gate CI actually enforces, also at 25 — passes: the highest function in engine.py is _next_req_url at 8, unchanged by this PR, and get_ogc_data is 5.

Splitting the CQL branch into a private _fetch_cql_body helper would move engine.py's module aggregate back under pyscn's line. My read is still that it is not worth it — the aggregate would sit just under a threshold it is already only one point over, and the branch reads better where it is, next to the finalizer it shares.

Elsewhere

  • Maintainability index: ogc/engine.py 61.92 → 63.39 (up — deleting fetch_ogc_request more than pays for the added branch). ogc/requests.py (49.62) and ogc/policy.py (57.20) are unchanged now that refactor(ogc): close over request state, retiring ogc.context #367 is in main and no longer part of this diff. waterdata/cql.py 84.09 → 75.65 is the one regression; it stays rank A, and MI swings hard on a file this small — it shed ~18 lines while keeping the same two functions.
  • Duplication: 8.8968% on both sides, with clone groups (5), pairs (50), and total clones (25) identical. The denominator effect the earlier version of this comment described is gone now that main contains refactor(ogc): close over request state, retiring ogc.context #367.
  • Import contracts: 8 → 7, back to main's count — /simplify merged the new cql-facade contract into ogc-facade as a second source_modules entry instead of keeping a near-verbatim copy, and updated the ADR 0003 / ADR 0007 compliance paragraphs that described only the NGWMN half of that seam.

Merge gates (xenon --max-absolute C --max-modules B --max-average A, complexipy, lint-imports, mypy --strict, 781 tests): pass on both sides.

Route get_cql through Water Data's own wrapper rather than the OGC
facade. waterdata.utils.get_ogc_data exists to bind base_url, dialect,
and extra_id_cols and to default output_id from the collection map;
get_cql was restating all four, making it the only Water Data getter
reaching past the wrapper. It gains a cql_body passthrough instead, so
the Water Data binding stays stated once.

Also in the verbatim-CQL branch:

- Bind the fetcher with functools.partial, the idiom the comment three
  lines below already names, instead of a nested closure.
- Drop `or None` from the properties lookup: _switch_properties_id
  returns [] and never None, and _ogc_query_params guards with `if
  properties:`, so both spellings build the same request.
- Trim the cql_body docstring to the load-bearing contract, dropping
  the mechanism narration the inline comment below already carries.

Merge the new cql-facade import contract into ogc-facade, which it
duplicated except for source_modules, and update the two ADR compliance
paragraphs that still described only the NGWMN half of the seam.

The branch itself stays: an over-budget body with no args["filter"]
would raise Unchunkable client-side if folded into the planner, rather
than letting the server judge it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thodson-usgs
thodson-usgs marked this pull request as ready for review August 12, 2026 19:53
@thodson-usgs
thodson-usgs merged commit 0ff534c into DOI-USGS:main Aug 12, 2026
11 checks passed
@thodson-usgs
thodson-usgs deleted the refactor/one-ogc-entry branch August 12, 2026 19:54
thodson-usgs added a commit to thodson-usgs/dataretrieval-python that referenced this pull request Aug 12, 2026
Absorbs DOI-USGS#368, DOI-USGS#369, and DOI-USGS#371. Eight files conflicted; three of the
resolutions were more than textual:

- ``wateruse.py``: main edited the implementation this branch had
  already renamed to ``nwdc.py``, so git conflicted the shim against
  it. Kept the shim and ported DOI-USGS#371's ``run_paginated`` migration into
  ``nwdc.py``, preserving ``adapter="nwdc"``.
- ``cql.py``: took main's version wholesale. This branch's two changes
  there (``redirected(OGC_API_URL)`` and ``adapter="waterdata"``) are
  subsumed by DOI-USGS#368 routing ``get_cql`` through
  ``waterdata.utils.get_ogc_data``, which already applies both.
- ``ratings.py``: took main's rewritten implementation and re-applied
  this branch's only contribution to it (``redirected(STAC_URL)``),
  plus adapter scoping on both drives.

Three breaks were semantic, not textual -- git merged them cleanly and
they would have failed at import or call time, because main added
callers of names this branch renamed or deleted:

- ``transport/pagination.run_paginated`` imported ``_CONCURRENCY_DEFAULT``
  (deleted here in favour of ``configuration.DEFAULT_CONCURRENCY``) and
  called ``RetryPolicy.from_env`` (renamed to ``from_configuration``).
  It now takes an ``adapter`` argument and threads it to both the retry
  policy and the executor, which is what makes per-adapter ``retries``
  and ``concurrency`` tables reach the three getters that use it.
- ``ogc/engine``'s ``cql_body`` branch (new in DOI-USGS#368) called
  ``from_env`` and dropped the adapter; both fixed.
- ``get_ogc_data`` gained ``cql_body`` from main and ``adapter`` here;
  both parameters kept.

969 passed, mypy --strict clean, all hooks including import-linter pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTaSm7HmVb94RSJiKW4WAS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant