Skip to content

Commit 7983a81

Browse files
authored
Merge pull request #239 from vprivat-ads/http_headers
Allow to provide HTTP headers
2 parents c79fadb + 7d333f4 commit 7983a81

10 files changed

+98
-18
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
1010

1111
- Added publish.yml to automatically publish new releases to PyPI [#236](https://github.com/stac-utils/stac-validator/pull/236)
1212
- Configure whether to open URLs when validating assets [#238](https://github.com/stac-utils/stac-validator/pull/238)
13+
- Allow to provide HTTP headers [#239](https://github.com/stac-utils/stac-validator/pull/239)
1314

1415
## [v3.4.0] - 2024-10-08
1516

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ Options:
108108
with --pages. Defaults to one page.
109109
--no-assets-urls Disables the opening of href links when validating
110110
assets (enabled by default).
111+
--header KEY VALUE HTTP header to include in the requests. Can be used
112+
multiple times.
111113
-p, --pages INTEGER Maximum number of pages to validate via --item-
112114
collection. Defaults to one page.
113115
-v, --verbose Enables verbose output for recursive mode.
@@ -332,3 +334,9 @@ stac-validator https://spot-canada-ortho.s3.amazonaws.com/catalog.json --recursi
332334
```bash
333335
stac-validator https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a/items --item-collection --pages 2
334336
```
337+
338+
**--header**
339+
340+
```bash
341+
stac-validator https://stac-catalog.eu/collections/sentinel-s2-l2a/items --header x-api-key $MY_API_KEY --header foo bar
342+
```

requirements-dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ black
22
pytest
33
pytest-mypy
44
pre-commit
5+
requests-mock
56
types-jsonschema

setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
extras_require={
3434
"dev": [
3535
"pytest",
36+
"requests-mock",
3637
"types-setuptools",
3738
],
3839
},
@@ -41,5 +42,5 @@
4142
"console_scripts": ["stac-validator = stac_validator.stac_validator:main"]
4243
},
4344
python_requires=">=3.8",
44-
tests_require=["pytest"],
45+
tests_require=["pytest", "requests-mock"],
4546
)

stac_validator/stac_validator.py

+9
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ def collections_summary(message: List[Dict[str, Any]]) -> None:
114114
is_flag=True,
115115
help="Disables the opening of href links when validating assets (enabled by default).",
116116
)
117+
@click.option(
118+
"--header",
119+
type=(str, str),
120+
multiple=True,
121+
help="HTTP header to include in the requests. Can be used multiple times.",
122+
)
117123
@click.option(
118124
"--pages",
119125
"-p",
@@ -134,6 +140,7 @@ def main(
134140
collections: bool,
135141
item_collection: bool,
136142
no_assets_urls: bool,
143+
header: list,
137144
pages: int,
138145
recursive: bool,
139146
max_depth: int,
@@ -154,6 +161,7 @@ def main(
154161
collections (bool): Validate response from /collections endpoint.
155162
item_collection (bool): Whether to validate item collection responses.
156163
no_assets_urls (bool): Whether to open href links when validating assets (enabled by default).
164+
headers (dict): HTTP headers to include in the requests.
157165
pages (int): Maximum number of pages to validate via `item_collection`.
158166
recursive (bool): Whether to recursively validate all related STAC objects.
159167
max_depth (int): Maximum depth to traverse when recursing.
@@ -185,6 +193,7 @@ def main(
185193
links=links,
186194
assets=assets,
187195
assets_open_urls=not no_assets_urls,
196+
headers=dict(header),
188197
extensions=extensions,
189198
custom=custom,
190199
verbose=verbose,

stac_validator/utilities.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import ssl
44
from typing import Dict
55
from urllib.parse import urlparse
6-
from urllib.request import urlopen
6+
from urllib.request import Request, urlopen
77

88
import requests # type: ignore
99

@@ -77,7 +77,7 @@ def get_stac_type(stac_content: Dict) -> str:
7777
return str(e)
7878

7979

80-
def fetch_and_parse_file(input_path: str) -> Dict:
80+
def fetch_and_parse_file(input_path: str, headers: Dict = {}) -> Dict:
8181
"""Fetches and parses a JSON file from a URL or local file.
8282
8383
Given a URL or local file path to a JSON file, this function fetches the file,
@@ -87,6 +87,7 @@ def fetch_and_parse_file(input_path: str) -> Dict:
8787
8888
Args:
8989
input_path: A string representing the URL or local file path to the JSON file.
90+
headers: For URLs: HTTP headers to include in the request
9091
9192
Returns:
9293
A dictionary containing the parsed contents of the JSON file.
@@ -97,7 +98,7 @@ def fetch_and_parse_file(input_path: str) -> Dict:
9798
"""
9899
try:
99100
if is_url(input_path):
100-
resp = requests.get(input_path)
101+
resp = requests.get(input_path, headers=headers)
101102
resp.raise_for_status()
102103
data = resp.json()
103104
else:
@@ -150,9 +151,7 @@ def set_schema_addr(version: str, stac_type: str) -> str:
150151

151152

152153
def link_request(
153-
link: Dict,
154-
initial_message: Dict,
155-
open_urls: bool = True,
154+
link: Dict, initial_message: Dict, open_urls: bool = True, headers: Dict = {}
156155
) -> None:
157156
"""Makes a request to a URL and appends it to the relevant field of the initial message.
158157
@@ -161,6 +160,7 @@ def link_request(
161160
initial_message: A dictionary containing lists for "request_valid", "request_invalid",
162161
"format_valid", and "format_invalid" URLs.
163162
open_urls: Whether to open link href URL
163+
headers: HTTP headers to include in the request
164164
165165
Returns:
166166
None
@@ -169,11 +169,12 @@ def link_request(
169169
if is_url(link["href"]):
170170
try:
171171
if open_urls:
172+
request = Request(link["href"], headers=headers)
172173
if "s3" in link["href"]:
173174
context = ssl._create_unverified_context()
174-
response = urlopen(link["href"], context=context)
175+
response = urlopen(request, context=context)
175176
else:
176-
response = urlopen(link["href"])
177+
response = urlopen(request)
177178
status_code = response.getcode()
178179
if status_code == 200:
179180
initial_message["request_valid"].append(link["href"])

stac_validator/validate.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class StacValidate:
3434
links (bool): Whether to additionally validate links (only works in default mode).
3535
assets (bool): Whether to additionally validate assets (only works in default mode).
3636
assets_open_urls (bool): Whether to open assets URLs when validating assets.
37+
headers (dict): HTTP headers to include in the requests.
3738
extensions (bool): Whether to only validate STAC object extensions.
3839
custom (str): The local filepath or remote URL of a custom JSON schema to validate the STAC object.
3940
verbose (bool): Whether to enable verbose output in recursive mode.
@@ -56,6 +57,7 @@ def __init__(
5657
links: bool = False,
5758
assets: bool = False,
5859
assets_open_urls: bool = True,
60+
headers: dict = {},
5961
extensions: bool = False,
6062
custom: str = "",
6163
verbose: bool = False,
@@ -70,6 +72,7 @@ def __init__(
7072
self.links = links
7173
self.assets = assets
7274
self.assets_open_urls = assets_open_urls
75+
self.headers: Dict = headers
7376
self.recursive = recursive
7477
self.max_depth = max_depth
7578
self.extensions = extensions
@@ -125,7 +128,9 @@ def assets_validator(self) -> Dict:
125128
assets = self.stac_content.get("assets")
126129
if assets:
127130
for asset in assets.values():
128-
link_request(asset, initial_message, self.assets_open_urls)
131+
link_request(
132+
asset, initial_message, self.assets_open_urls, self.headers
133+
)
129134
return initial_message
130135

131136
def links_validator(self) -> Dict:
@@ -145,7 +150,7 @@ def links_validator(self) -> Dict:
145150
for link in self.stac_content["links"]:
146151
if not is_valid_url(link["href"]):
147152
link["href"] = root_url + link["href"][1:]
148-
link_request(link, initial_message)
153+
link_request(link, initial_message, True, self.headers)
149154

150155
return initial_message
151156

@@ -345,7 +350,9 @@ def recursive_validator(self, stac_type: str) -> bool:
345350
self.stac_file = st + "/" + address
346351
else:
347352
self.stac_file = address
348-
self.stac_content = fetch_and_parse_file(str(self.stac_file))
353+
self.stac_content = fetch_and_parse_file(
354+
str(self.stac_file), self.headers
355+
)
349356
self.stac_content["stac_version"] = self.version
350357
stac_type = get_stac_type(self.stac_content).lower()
351358

@@ -414,7 +421,7 @@ def validate_collections(self) -> None:
414421
Returns:
415422
None
416423
"""
417-
collections = fetch_and_parse_file(str(self.stac_file))
424+
collections = fetch_and_parse_file(str(self.stac_file), self.headers)
418425
for collection in collections["collections"]:
419426
self.schema = ""
420427
self.validate_dict(collection)
@@ -437,7 +444,7 @@ def validate_item_collection(self) -> None:
437444
"""
438445
page = 1
439446
print(f"processing page {page}")
440-
item_collection = fetch_and_parse_file(str(self.stac_file))
447+
item_collection = fetch_and_parse_file(str(self.stac_file), self.headers)
441448
self.validate_item_collection_dict(item_collection)
442449
try:
443450
if self.pages is not None:
@@ -450,7 +457,7 @@ def validate_item_collection(self) -> None:
450457
next_link = link["href"]
451458
self.stac_file = next_link
452459
item_collection = fetch_and_parse_file(
453-
str(self.stac_file)
460+
str(self.stac_file), self.headers
454461
)
455462
self.validate_item_collection_dict(item_collection)
456463
break
@@ -489,7 +496,7 @@ def run(self) -> bool:
489496
and not self.item_collection
490497
and not self.collections
491498
):
492-
self.stac_content = fetch_and_parse_file(self.stac_file)
499+
self.stac_content = fetch_and_parse_file(self.stac_file, self.headers)
493500

494501
stac_type = get_stac_type(self.stac_content).upper()
495502
self.version = self.stac_content["stac_version"]

tests/test_header.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Description: Test --header option
3+
4+
"""
5+
6+
import json
7+
8+
import requests_mock
9+
10+
from stac_validator import stac_validator
11+
12+
13+
def test_header():
14+
stac_file = "tests/test_data/v110/simple-item.json"
15+
url = "https://localhost/" + stac_file
16+
17+
no_headers = {}
18+
valid_headers = {"x-api-key": "a-valid-api-key"}
19+
20+
with requests_mock.Mocker(real_http=True) as mock, open(stac_file) as json_data:
21+
mock.get(url, request_headers=no_headers, status_code=403)
22+
mock.get(url, request_headers=valid_headers, json=json.load(json_data))
23+
24+
stac = stac_validator.StacValidate(url, core=True, headers=valid_headers)
25+
stac.run()
26+
assert stac.message == [
27+
{
28+
"version": "1.1.0",
29+
"path": "https://localhost/tests/test_data/v110/simple-item.json",
30+
"schema": [
31+
"https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json"
32+
],
33+
"valid_stac": True,
34+
"asset_type": "ITEM",
35+
"validation_method": "core",
36+
}
37+
]
38+
39+
stac = stac_validator.StacValidate(url, core=True, headers=no_headers)
40+
stac.run()
41+
assert stac.message == [
42+
{
43+
"version": "",
44+
"path": "https://localhost/tests/test_data/v110/simple-item.json",
45+
"schema": [""],
46+
"valid_stac": False,
47+
"error_type": "HTTPError",
48+
"error_message": "403 Client Error: None for url: https://localhost/tests/test_data/v110/simple-item.json",
49+
}
50+
]

tox.ini

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
envlist = py38,py39,py310,py311,py312,py313
33

44
[testenv]
5-
deps = pytest
5+
deps =
6+
pytest
7+
requests-mock
68
commands = pytest

tox/Dockerfile-tox

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ COPY . /code/
44
RUN export LC_ALL=C.UTF-8 && \
55
export LANG=C.UTF-8 && \
66
pip3 install . && \
7-
pip3 install tox==4.0.11 && \
7+
pip3 install tox==4.23.2 && \
88
tox

0 commit comments

Comments
 (0)