Skip to content

Commit d0490a1

Browse files
committed
refactor(ogc): simplify argument normalization
1 parent d77f388 commit d0490a1

2 files changed

Lines changed: 35 additions & 73 deletions

File tree

dataretrieval/ogc/requests.py

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -294,13 +294,18 @@ def _check_monitoring_location_id(
294294
return value
295295

296296

297-
def _get_args(
297+
def prepare_request_args(
298298
local_vars: dict[str, Any],
299299
exclude: set[str] | None = None,
300300
*,
301301
no_normalize: frozenset[str] | set[str] = _NO_NORMALIZE_PARAMS,
302302
) -> dict[str, Any]:
303-
"""Build the API-request kwargs dict from a getter's ``locals()``."""
303+
"""Build OGC request kwargs from a getter's ``locals()``.
304+
305+
Internal bookkeeping keys, caller-supplied exclusions, and ``None`` values
306+
are omitted. Identifiers and properties are validated; other iterables are
307+
normalized unless listed in ``no_normalize``.
308+
"""
304309
to_exclude = {"service", "output_id"}
305310
if exclude:
306311
to_exclude.update(exclude)
@@ -322,35 +327,5 @@ def _get_args(
322327
return args
323328

324329

325-
def prepare_request_args(
326-
local_vars: dict[str, Any],
327-
exclude: set[str] | None = None,
328-
*,
329-
no_normalize: frozenset[str] | set[str] = _NO_NORMALIZE_PARAMS,
330-
) -> dict[str, Any]:
331-
"""Public facade entry point for argument normalization.
332-
333-
Wraps :func:`_get_args` with the same inputs and behavior — builds the
334-
API-request kwargs dict from a getter's ``locals()``, excluding internal
335-
bookkeeping keys and normalizing iterable parameters. Service adapters
336-
(NGWMN, Water Data utils) use this rather than importing the private
337-
``_get_args`` directly.
338-
339-
Parameters
340-
----------
341-
local_vars : dict
342-
A getter's ``locals()`` snapshot.
343-
exclude : set of str, optional
344-
Extra keys to drop beyond the default set (``service``, ``output_id``).
345-
no_normalize : frozenset or set of str, optional
346-
Parameter names whose iterable values should NOT be pushed through
347-
``_normalize_str_iterable`` (e.g. date-range params, bounding box,
348-
numeric lists). Defaults to the OGC base set (date ranges + bbox);
349-
callers with additional numeric params pass a superset.
350-
351-
Returns
352-
-------
353-
dict
354-
Cleaned kwargs suitable for passing to request builders.
355-
"""
356-
return _get_args(local_vars, exclude, no_normalize=no_normalize)
330+
# Compatibility alias for existing private imports from ``ogc.engine``.
331+
_get_args = prepare_request_args

tests/headers_host_scoping_test.py

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
from __future__ import annotations
44

55
import asyncio
6-
import os
76
from unittest import mock
87

98
import httpx
9+
import pytest
1010

1111
from dataretrieval.utils import (
12-
_AUTHORIZED_API_KEY_HOST,
1312
HTTPX_ASYNC_DEFAULTS,
1413
_default_headers,
1514
_get,
@@ -21,79 +20,70 @@ class TestDefaultHeadersHostScoping:
2120

2221
FAKE_TOKEN = "test-fake-token-abc123"
2322

23+
@pytest.fixture(autouse=True)
24+
def _api_token(self, monkeypatch: pytest.MonkeyPatch) -> None:
25+
"""Install one harmless token for every host-scoping behavior test."""
26+
monkeypatch.setenv("API_USGS_PAT", self.FAKE_TOKEN)
27+
2428
def test_key_included_for_waterdata_host(self):
2529
"""Key IS added when target URL matches api.waterdata.usgs.gov."""
2630
url = "https://api.waterdata.usgs.gov/ogcapi/v0/collections/daily/items"
27-
with mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}):
28-
headers = _default_headers(url)
31+
headers = _default_headers(url)
2932
assert headers.get("X-Api-Key") == self.FAKE_TOKEN
3033

3134
def test_key_excluded_for_external_host(self):
3235
"""Key is NOT added for an external (non-USGS) host."""
3336
url = "https://nwis.waterservices.usgs.gov/nwis/iv/"
34-
with mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}):
35-
headers = _default_headers(url)
37+
headers = _default_headers(url)
3638
assert "X-Api-Key" not in headers
3739

3840
def test_key_excluded_for_wateruse_host(self):
3941
"""Key is NOT added for the NWDC water-use host (api.water.usgs.gov)."""
4042
url = "https://api.water.usgs.gov/nwaa-data/data"
41-
with mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}):
42-
headers = _default_headers(url)
43+
headers = _default_headers(url)
4344
assert "X-Api-Key" not in headers
4445

4546
def test_key_excluded_for_rating_asset_host(self):
4647
"""Key is NOT added for rating asset downloads (S3/external)."""
4748
url = "https://labs.waterdata.usgs.gov/sta/v1.1/Datastreams(123)/rating.rdb"
48-
with mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}):
49-
headers = _default_headers(url)
49+
headers = _default_headers(url)
5050
assert "X-Api-Key" not in headers
5151

5252
def test_key_excluded_for_lookalike_host(self):
5353
"""Key is NOT sent to a typosquatting/lookalike domain."""
5454
url = "https://api.waterdata.usgs.gov.evil.com/ogcapi/v0/daily/items"
55-
with mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}):
56-
headers = _default_headers(url)
55+
headers = _default_headers(url)
5756
assert "X-Api-Key" not in headers
5857

5958
def test_key_excluded_when_no_url_provided(self):
6059
"""Key is NOT added when target_url is None (legacy callers)."""
61-
with mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}):
62-
headers = _default_headers(None)
60+
headers = _default_headers(None)
6361
assert "X-Api-Key" not in headers
6462

65-
def test_key_excluded_when_no_token_set(self):
63+
def test_key_excluded_when_no_token_set(
64+
self, monkeypatch: pytest.MonkeyPatch
65+
) -> None:
6666
"""No key header at all when API_USGS_PAT is not set."""
67-
with mock.patch.dict(os.environ, {}, clear=True):
68-
# Ensure the env var is absent
69-
os.environ.pop("API_USGS_PAT", None)
70-
headers = _default_headers("https://api.waterdata.usgs.gov/ogcapi/v0/daily")
67+
monkeypatch.delenv("API_USGS_PAT")
68+
headers = _default_headers("https://api.waterdata.usgs.gov/ogcapi/v0/daily")
7169
assert "X-Api-Key" not in headers
7270

7371
def test_non_auth_headers_always_present(self):
7472
"""User-Agent, Accept, Accept-Encoding, lang are always present."""
7573
url = "https://example.com/any"
76-
with mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}):
77-
headers = _default_headers(url)
74+
headers = _default_headers(url)
7875
assert "User-Agent" in headers
7976
assert "Accept" in headers
8077
assert "Accept-Encoding" in headers
8178
assert "lang" in headers
8279
# Key should NOT be sent to example.com
8380
assert "X-Api-Key" not in headers
8481

85-
def test_authorized_host_constant(self):
86-
"""The authorized host constant is correct."""
87-
assert _AUTHORIZED_API_KEY_HOST == "api.waterdata.usgs.gov"
88-
8982
def test_generic_ogc_request_excludes_key_for_custom_host(self):
9083
"""A caller-supplied OGC base URL never inherits Water Data auth."""
9184
from dataretrieval.ogc.requests import _construct_api_requests, _ogc_base_url
9285

93-
with (
94-
mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}),
95-
_ogc_base_url("https://features.example.org/ogcapi"),
96-
):
86+
with _ogc_base_url("https://features.example.org/ogcapi"):
9787
request = _construct_api_requests("things")
9888
assert "X-Api-Key" not in request.headers
9989

@@ -107,7 +97,6 @@ def test_rating_download_scopes_headers_to_asset_url(self):
10797
feature = {"id": "site.rdb", "assets": {"data": {"href": asset_url}}}
10898
response = mock.Mock(text="rating body")
10999
with (
110-
mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}),
111100
mock.patch.object(ratings, "_get", return_value=response) as get,
112101
mock.patch.object(ratings, "_raise_for_non_200"),
113102
mock.patch.object(ratings, "read_rdb", return_value=pd.DataFrame()),
@@ -133,13 +122,12 @@ def handler(request: httpx.Request) -> httpx.Response:
133122
return httpx.Response(200, request=request)
134123

135124
url = "https://api.waterdata.usgs.gov/start"
136-
with mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}):
137-
_get(
138-
url,
139-
headers=_default_headers(url),
140-
follow_redirects=True,
141-
transport=httpx.MockTransport(handler),
142-
)
125+
_get(
126+
url,
127+
headers=_default_headers(url),
128+
follow_redirects=True,
129+
transport=httpx.MockTransport(handler),
130+
)
143131

144132
assert seen[0].headers.get("X-Api-Key") == self.FAKE_TOKEN
145133
assert "X-Api-Key" not in seen[1].headers
@@ -166,8 +154,7 @@ async def run() -> None:
166154
) as client:
167155
await client.get(url, headers=_default_headers(url))
168156

169-
with mock.patch.dict(os.environ, {"API_USGS_PAT": self.FAKE_TOKEN}):
170-
asyncio.run(run())
157+
asyncio.run(run())
171158

172159
assert seen[0].headers.get("X-Api-Key") == self.FAKE_TOKEN
173160
assert "X-Api-Key" not in seen[1].headers

0 commit comments

Comments
 (0)